大家好,我是「云舒编程」,今天我们来聊聊上下文切换性能消耗。
文章首发于微信公众号:云舒编程
关注公众号获取:
1、大厂项目分享
2、各种技术原理分享
3、部门内推
一、前言
众所周知,操作系统是一个分时复用系统,通过将CPU时间分为好几份。系统在很短的时间内,将 CPU 轮流分配给它们,从而实现多任务同时运行的错觉。
伴随着的还有一个词是上下文切换,无论在工作中还是面试中,我们总会听到要减少线程、进程的上下文切换,因为上下文切换的代价比较高,会影响性能。
今天我们就来详细说说上下文切换到底在切换什么,以及如何可视化的观察上下文切换的代价,它是怎么影响程序性能的。
二、进程是什么
❝
进程(Process)是计算机中的程序关于某数据集合上的一次运行活动,是系统进行资源分配的基本单位,是操作系统结构的基础。在早期面向进程设计的计算机结构中,进程是程序的基本执行实体;在当代面向线程设计的计算机结构中,进程是线程的容器。程序是指令、数据及其组织形式的描述,进程是程序的实体。from:百科
❞
直白的说就是假设你去组织一场活动,那么你肯定会需要记录活动需要的物质、人员、时间安排,在什么时间点应该做哪些事情。这些事情你肯定不会单纯记录在脑子里,会找一个文档记录下来。
同理,当一个程序需要运行时,操作系统也需要记录该程序使用了多少内存,打开了什么文件,程序运行到哪里了,这些信息都需要记录下来,而进程就充当了这个角色,也就是百科中说的:“是系统进行资源分配的基本单位”。
2.1、进程资源
更详细些,一个进程会拥有如下资源,其中带*号是每个进程都有独立一份,有了这样的设计结构,多个进程就能并发运行了。
三、上下文切换到底在切换什么?
有了CPU的时间片和进程后,操作系统就可以将程序执行起来了。假设有三个进程A,B,C。首先是A获得了CPU时间片,待A的时间片结束后,操作系统会挑选B或者C进行执行。
那么这里就存在一个问题,A程序可能并没有执行完,这个时候被临时中断了,下一次该怎么执行呢?为了解决这个问题,于是提出了上下文的概念:“进程运行过程中用到的资源,进程的状态,执行到了哪里”。
在把A切换出去之前,首先把上下文保存下来,这样等到再次执行A的时候就可以从上次执行的状态继续执行,从而达成没有中断过的假象。
更加详细的解释是:
每个程序运行前,CPU需要知道从哪里加载任务,从哪里开始运行,有哪些指令。而这些都需要CPU寄存器、程序计数器、内存管理单元(MMU)配合完成。
❝
一、寄存器是CPU内部用来存放数据的一些小型存储区域,用来暂时存放参与运算的数据和运算结果。寄存器拥有非常高的读写速度,所以在寄存器之间的数据传送非常快。
❞
❝
二、程序计数器:程序计数器是用于存放下一条指令所在单元的地址的地方。当执行一条指令时,首先需要根据PC中存放的指令地址,将指令由内存取到指令寄存器中,此过程称为“取指令”。与此同时,PC中的地址或自动加1或由转移指针给出下一条指令的地址。此后经过分析指令,执行指令。完成第一条指令的执行,而后根据PC取出第二条指令的地址,如此循环,执行每一条指令。
❞
❝
三、内存管理单元(MMU):通过虚拟内存和物理内存的映射,使的每个程序都认为自己可以使用完整的内存。详细解释:https://baike.baidu.com/item/MMU/4542218
❞
上下文切换就是将A进程存储在CPU寄存器,程序计数器,MMU中的数据取出来,然后将B进程的数据放进去。而这些保存下来的上下文,会存储在系统内核中,并在任务重新调度执行时再次加载进来。这样就能保证任务原来的状态不受影响,让任务看起来还是连续运行。
四、有哪些类型的上下文切换
4.1、进程上下文切换
在进程上下文切换过程中,操作系统需要完成以下操作:
- 保存当前进程的上下文(如寄存器状态、程序计数器等)
- 加载新进程的上下文
- 更新内存管理单元(MMU)以映射新进程的地址空间
- 切换到新进程的执行环境
4.2、线程上下文切换
线程跟进程的区别在于:线程是依赖于进程存在,线程是调度的基本单位,进程为线程提供虚拟内存,全局变量等资源。
简单来说:
- 当进程只有一个线程时,可以认为进程就等于线程。
- 当进程拥有多个线程时,这些线程会共享相同的虚拟内存和全局变量等资源。这些资源在上下文切换时是不需要修改的。
- 另外,线程也有自己的私有数据,比如栈和寄存器等,这些在上下文切换时也是需要保存的。
那么线程上下文切换就可以分为两种情况:
- 线程属于同一个进程,由于属于同一个进程,那么虚拟内存等资源不需要切换,只需要切换线程的私有资源,例如栈、寄存器等资源即可。
- 线程属于不同进程,这个时候切换过程跟进程上下文切换没有区别。
也就是说,在同一进程内线程上下文切换的代价是比进程切换小的。
4.3、系统调用上下文切换
我们知道,操作系统把进程的运行空间分为内核空间和用户空间。
- 其中操作系统运行在 内核空间(也叫内核态)(Ring 0)具有最高权限,可以直接访问所有资源;
- 而用户写的代码运行在 用户空间(也叫用户态)(Ring 3)只能访问受限资源,不能直接访问内存等硬件设备,必须通过系统调用陷入到内核中,然后由内核代码去访问,再把结果返回。
也就是说进程即可以运行在用户态,也可以运行在内核态,当调用系统函数时就会从用户态转入内核态,调用结束时就会从内核态转入用户态。那么这个转换过程会涉及上下文切换吗?
答案是肯定的,因为操作系统的代码最终也是需要CPU去执行的,那么肯定需要寄存器和程序计数器的参与,那么就需要把用户写的代码从这两个地方“踢出去”,换成操作系统的代码,等操作完成了又需要把操作系统的代码从这两个地方“踢出去”,换成用户代码。也就是说一次系统调用导致了两次上下文切换。
但是由于这个时候本质上还是属于同一进程,所以虚拟内存(MMU,TLB),全局变量等资源是不需要替换的。
所以系统调用导致的上下文切换代价也比进程上下文切换的代价低。
4.4、中断上下文切换
在前面的文章[Linux是怎么从网络上接收数据包的](https://mp.weixin.qq.com/s?__biz=Mzk0NjQ5ODY5OQ==&mid=2247484261&idx=1&sn=5a4a2fa9f56990b758ea4b8d70fdd842&chksm=c3047e81f473f79700dbad1cf126aa0311b396efb05134169149efa78e703a51e00ed03d1805&token=944685945&lang=zh_CN&scene=21#wechat_redirect)中我们有详细解释过中断的概念,中断在操作系统中拥有最高优先级,当发生中断时,需要停止当前进程的远行,优先处理中断。那么这个过程就需要把进程的上下文保存,等处理完中断后再次运行该进程的时候,就可以从上次暂停的地方继续运行。
中断上下文切换也与进程上下文切换不同,中断执行程序通常也是操作系统内核的一部分,并不涉及到进程的用户态。所以中断上下文切换也不需要保存和恢复这个进程的虚拟内存、全局变量等用户态资源。只需要切换CPU寄存器,程序计数器等资源。
五、怎么观察上下文切换次数
通过上面的描述,我们知道了上下文切换涉及到寄存器,程序计数器,虚拟内存等资源的保存和恢复,这些操作必然是需要时间的。如果程序耗费在这些地方的时间变多了,那么性能肯定就会变差,接下来我们就来看看如何观察上下文切换耗费的时间。
vmstat 是一个常用的系统性能分析工具,主要用来分析系统的内存使用情况,也常用来分析 CPU 上下文切换和中断的次数。
vmstat输出格式如下,总体分为四部分:
procs -----------memory---------- ---swap-- -----io---- -system-- ------cpu-----
r b swpd free buff cache si so bi bo in cs us sy id wa st
0 1 0 8693200 707820 4257088 0 0 0 7 0 0 1 1 99 0 0
0 0 0 8692860 707820 4257156 0 0 0 60 2043 3710 1 1 98 0 0
0 0 0 8696820 707820 4257140 0 0 0 46 2024 3688 0 0 99 0 0
system | |
---|---|
in | 每秒的系统中断数,包括时钟中断。 |
cs | 每秒上下文切换的次数 |
这里我们主要关注in和cs。
vmstat 只给出了系统总体的上下文切换情况,要想查看每个进程的详细情况,可以使用pidstat。
pidstat -w
18:25:31 UID PID cswch/s nvcswch/s Command
18:25:36 0 215 0.21 0.00 agent
18:25:36 0 275 0.84 0.21 base
18:25:36 0 456 103.35 0.00 cmlb_client
18:25:36 0 470 10.48 0.00 monitor_agent
18:25:36 0 2069 1.47 0.00 datawalker_logb
18:25:36 0 1060290 0.21 0.00 pidstat
重点关注:
- cswch:表示每秒自愿上下文切换的次数,指的是进程无法获取所需资源,导致的上下文切换。比如说, I/O、内存等系统资源不足时,就会发生自愿上下文切换。
- nvcswch:表示每秒非自愿上下文切换的次数。指进程由于时间片已到等原因,被系统强制调度,进而发生的上下文切换。
六、上下文切换过多的影响
这里使用github上的一段代码测试「CPU 密集型任务」在不同线程数下的耗时情况。代码地址:https://github.com/nickliuchao/threadpollsizetest
- 横坐标为线程数
- 纵坐标为耗时,单位ms
从上图可知,当线程数量太小,同一时间大量请求将被阻塞在线程队列中排队等待执行线程,此时 CPU 没有得到充分利用;当线程数量太大,被创建的执行线程同时在争取 CPU 资源,又会导致大量的上下文切换,从而增加线程的执行时间,影响了整体执行效率。
同时并发编程网也提供了另一种测试方式:https://ifeve.com/java-context-switch/
推荐阅读
1、原来阿里字节员工简历长这样
2、一条SQL差点引发离职
3、MySQL并发插入导致死锁
如果你也觉得我的分享有价值,记得点赞或者收藏哦!你的鼓励与支持,会让我更有动力写出更好的文章哦!
更多精彩内容,请关注公众号「云舒编程」