进程是操作系统的伟大发明之一。 它将应用程序屏蔽了CPU调度、内存管理等硬件细节,抽象出进程的概念,使应用程序可以集中精力实现自己的业务逻辑。 许多任务可以“同时”执行。 但在给用户带来便利的同时,也带来了一些额外的开销。 如下图所示,进程运行期间,CPU虽然忙于工作,但并没有完成任何用户工作,这就是进程机制带来的额外开销。
图1.jpg
在进程A切换到进程B的过程中,首先保存进程A的上下文,这样当A恢复运行时,就可以知道进程A的下一条指令是什么。 然后将B进程运行的上下文恢复到寄存器中。 这个过程称为上下文切换。 在进程少、切换不频繁的应用场景中,上下文切换的开销并不是什么大问题。 但现在Linux操作系统多用于高并发的网络程序后端服务器。 当单台机器支持数千个用户请求时,就必须讨论这种开销。 因为当用户进程请求Redis、Mysql数据等网络IO块时,或者进程时间片到了时,就会触发上下文切换。
图2.png
一个简单的进程上下文切换开销测试实验
废话不多说,我们用一个实验来测试一次上下文切换需要多少CPU时间! 实验方法是创建两个进程并在它们之间传递令牌。 其中一个进程在读取令牌时会阻塞。 另一个进程在发送令牌后等待返回时也会被阻塞。 这样进行一定次数的往返传输,然后统计它们的平均单次切换时间开销。
具体实验代码参见
# gcc main.c -o main # ./main./main Before Context Switch Time1565352257 s, 774767 us After Context SWitch Time1565352257 s, 842852 us
每次执行的时间都会有所不同,多次运行后平均上下文切换时间约为3.5us。 当然,这个数字因机器而异,建议在真机上测试。
我们之前测试系统调用时,最低值为200ns。 可见上下文切换的开销比系统调用的开销要大。 系统调用只是在进程中将用户态切换到内核态,然后再切换回来,而上下文切换则直接从进程A切换到进程B。显然这个上下文切换需要做更多的工作。
进程上下文切换的开销有哪些?
那么上下文切换时具体的CPU开销是多少呢? 费用有两种,一种是直接费用,一种是间接费用。
直接开销是CPU在切换时要做的事情,包括:
间接开销主要是指切换到新进程后,由于各种缓存不热,速度会变慢。 如果进程始终调度在一个 CPU 上会更好。 如果是跨CPU的话,之前热炒的TLB、L1、L2、L3因为运行的进程发生了变化,所以基于局部性原则缓存的代码和数据也都变了。 没用的,导致新进程穿透内存的IO会增加。 事实上,我们上面的实验并没有很好地衡量这种情况,所以实际的上下文切换开销可能会大于3.5us。
想要了解更详细操作流程的同学请参考《深入理解Linux内核》中的第3章和第9章。
更专业的测试工具——
它是一个多平台开源系统,用于评估系统的综合性能。 可以测试包括文档读写、内存操作、进程创建和销毁开销、网络等性能。 使用方法很简单,但是运行起来有点慢。 有兴趣的同学可以自己尝试一下。
这个工具的优点是可以进行多组实验,每组实验有2个流程、8个流程、16个流程。 每个进程使用的数据大小也在变化,充分模拟缓存未命中的影响。 我和他一起测试了一下,结果如下:
------------------------------------------------------------------------- Host OS 2p/0K 2p/16K 2p/64K 8p/16K 8p/64K 16p/16K 16p/64K ctxsw ctxsw ctxsw ctxsw ctxsw ctxsw ctxsw --------- ------------- ------ ------ ------ ------ ------ ------- ------- bjzw_46_7 Linux 2.6.32- 2.7800 2.7800 2.7000 4.3800 4.0400 4.75000 5.48000
显示的进程上下文切换时间在 2.7us 到 5.48 之间。
线程上下文切换耗时
我们之前测试了进程上下文切换的开销,我们将继续测试Linux中的线程。 看看它是否可以比进程快,可以快多少。
其实Linux下是没有线程的,只是为了迎合开发者的口味,轻量级的进程一出来就被称为线程。 轻量级进程和进程一样,有自己独立的进程描述符和自己独立的pid。 从操作系统的角度来看,调度和进程没有区别。 他们只是在等待队列的双向链表中选择一个,切换到运行状态。 只是轻量级进程与普通进程的区别在于它可以共享相同的内存地址空间、代码段、全局变量以及同一组打开的文件。
同一个()下的所有线程看到的是同一个pid,里面其实有一个tgid字段。 对于多线程程序来说,()系统调用实际上获取的是这个tgid,因此属于同一进程的多个线程看起来具有相同的PID。
我们通过实验来验证一下。 其原理与进程测试类似,创建20个线程,线程之间通过管道传输信号。 收到信号后唤醒,然后将信号传递给下一个线程,自己休眠。 本实验中单独考虑向管道传输信号的额外开销,并计入第一步。
# gcc -lpthread main.c -o main 0.508250 4.363495
每个实验的结果都会有一些差异。 以上结果是取多次结果后取平均值。 每次线程切换的成本约为3.8us。 从上下文切换的耗时角度来看,Linux线程(轻量级进程)与进程没有太大区别。
Linux相关命令
既然我们知道上下文切换会消耗更多的 CPU 时间,那么我们可以使用什么工具来检查 Linux 中发生了多少次切换呢? 如果上下文切换影响了系统的整体性能,我们有没有办法找出有问题的流程并进行优化呢?
# vmstat 1 procs -----------memory---------- ---swap-- -----io---- --system-- -----cpu----- r b swpd free buff cache si so bi bo in cs us sy id wa st 2 0 0 595504 5724 190884 0 0 295 297 0 0 14 6 75 0 4 5 0 0 593016 5732 193288 0 0 0 92 19889 29104 20 6 67 0 7 3 0 0 591292 5732 195476 0 0 0 0 20151 28487 20 6 66 0 8 4 0 0 589296 5732 196800 0 0 116 384 19326 27693 20 7 67 0 7 4 0 0 586956 5740 199496 0 0 216 24 18321 24018 22 8 62 0 8
或者
# sar -w 1 proc/s Total number of tasks created per second. cswch/s Total number of context switches per second. 11:19:20 AM proc/s cswch/s 11:19:21 AM 110.28 23468.22 11:19:22 AM 128.85 33910.58 11:19:23 AM 47.52 40733.66 11:19:24 AM 35.85 30972.64 11:19:25 AM 47.62 24951.43 11:19:26 AM 47.52 42950.50 ......
上图中的环境是生产环境机器,配置是8核8G的KVM虚拟机,环境是nginx+fpm,fpm数量为1000,平均每秒处理的用户界面请求在100左右其中,cs栏表示系统在1s内发生的上下文切换次数,1s左右的上下文切换次数已经达到了4W次。 粗略估计,每个核心每秒需要切换5K次左右,1s内切换上下文需要近20ms。 要知道这是一个虚拟机,虚拟化本身就会有一些额外的开销,而且它实际上会消耗CPU在用户界面逻辑处理、系统调用内核逻辑处理、网络连接处理、软中断等方面,所以开销20ms 实际上并不低。
那么更进一步,我们看看哪些进程会导致频繁的上下文切换?
# pidstat -w 1 11:07:56 AM PID cswch/s nvcswch/s Command 11:07:56 AM 32316 4.00 0.00 php-fpm 11:07:56 AM 32508 160.00 34.00 php-fpm 11:07:56 AM 32726 131.00 8.00 php-fpm ......
由于fpm是同步阻塞模式,每当请求Redis、Mysql时,都会阻塞并导致cswch/s自愿上下文切换,只有时间片到了才会触发/s非自愿切换。 可见fpm进程的切换大部分是自愿的,非自愿的相对较少。
如果你想查看某个特定进程的整体上下文切换情况,可以直接在/proc界面下查看,不过这是总计值。
grep ctxt /proc/32583/status voluntary_ctxt_switches: 573066 nonvoluntary_ctxt_switches: 89260
本节的结论
我们不需要记住上下文切换的作用。 我们只需要记住一个结论。 笔者的开发机器上上下文切换的成本约为2.7-5.48us。 你自己的机器可以使用我提供的代码或工具。 一些测试。
相对更准确,因为考虑了切换后Cache miss带来的额外开销。
个人公众号“内功开发与实践”打通了理论与实践的任督二脉。
参考