性能测试必备知识(5)- 深入理解“CPU 上下文切换”
做性能测试的必备知识系列,可以看下面链接的文章哦
https://www.cnblogs.com/poloyy/category/1806772.html
前言
上一篇文章中,举例了大量进程等待 CPU 调度的场景
灵魂拷问
既然进程是在等待,并没有运行,为什么系统的平均负载还是会升高呢
回答
本文的重点:CPU 上下文切换就是罪魁祸首
先来聊聊 Linux
提出疑问
- 之前说最好一个 CPU 运行一个进程,这样 CPU 利用率刚刚好
- 但事实上我们的 Linux 会同时运行很多进程,包括系统态的和自己启动的进程,这不就违背了我们的美好初衷吗?
知识点来回答疑问
- Linux 是一个多任务操作系统
- 它支持远大于 CPU 数量的任务同时运行
- 但多任务其实并不是真的在同时运行
- 而是因为系统在很短时间内,将 CPU 轮流分配给它们,造成多任务同时运行的错觉
什么是 CPU 上下文
CPU 寄存器和程序计数器(PC)
- 在每个任务运行前,CPU 都需要知道任务从哪里加载、又从哪里开始运行
- 所以需要系统事先帮它设置好 CPU 寄存器和程序计数器
CPU 寄存器
CPU 内置的容量小,但速度极快的内存
程序计数器
用来存储 CPU 正在执行的指令位置,或者即将执行的下一条指令位置
CPU 上下文
CPU 寄存器和程序计数器是 CPU 在运行任何任务前,必须的依赖环境,所以也被叫做 CPU 上下文
CPU 上下文切换
分步骤去理解
- 先把前一个任务的 CPU 上下文(CPU 寄存器和程序计数器)保存起来
- 加载新任务的上下文到这些寄存器和程序计数器
- 最后再跳转到程序计数器所指的新位置,运行新任务
- 保存下来的上下文,会存储到系统内核中,并在任务重新调度执行时再次加载进来,这样能保证任务原来的状态不受影响,让任务看起来还是连续运行
灵魂拷问一
CPU 上下文切换无非就是更新了 CPU 寄存器的值嘛,但这些寄存器,本身就是为了快速运行任务而设计的,为什么会影响系统的 CPU 性能呢?
灵魂拷问二
- 上面老说到的【任务】到底是什么呢?
- 是进程,线程?是的,进程和线程是最常见的任务
- 那除此之外,还有其他的任务吗?
回答
硬件通过触发信号,会导致中断处理程序的调用,也是一种常见的任务
所以,根据任务的不同,CPU 的上下文切换可以分为不同的场景
- 进程上下文切换
- 线程上下文切换
- 中断上下文切换
系统调用
Linux 按照特权等级划分进程的运行空间
- 内核空间(Ring 0):具有最高权限,可以直接访问所有资源
- 用户空间(Ring 3):只能访问受限资源,不能直接访问内存等硬件设备,必须通过系统调用陷入到内核中,才能访问这些特权资源
也就是说,进程既可以在用户空间运行,称为进程的用户态
又可以在内核空间运行,称为进程的内核态
重点:用户态到内核态的转变需要通过系统调用来完成
系统调用的栗子
比如,当我们查看文件内容时,就需要多次系统调用来完成:
- 首先调用 open() 打开文件
- 然后调用 read() 读取文件内容
- 并调用 write() 将内容输出
- 最后调用 close() 关闭文件
系统调用的过程发生 CPU 上下文的切换
- CPU 寄存器里原来用户态的指令位置,需要先保存起来
- 为了执行内核态代码,CPU 寄存器需要更新为内核态指令的新位置
- 最后才是跳转到内核态运行内核任务
- 系统调用结束后,CPU 寄存器需要恢复原来保存的用户态
- 然后再切换回用户空间,继续运行进程
总结下
- 一次系统调用的过程,其实发生了两次 CPU 上下文切换【用户态切内核态,内核态再切回用户态】
- 系统调用过程中,并不会涉及到虚拟内存等进程用户态的资源,也不会切换进程
和进程上下文切换的不同
- 进程上下文切换:从一个进程切换到另一个进程运行
- 系统调用:一直是同一个进程在运行
总结
- 系统调用过程通常称为特权模式切换,而不是上下文切换
- 但实际上,系统调用过程中, CPU 上下文切换是无法避免的
进程上下文切换
基础知识点
- 在 Linux 中,进程是由内核来管理和调度
- 进程的切换只能发生在内核态
- 所以,进程的上下文不仅包括了虚拟内存、栈、全局变量等用户空间的资源,还包括了内核堆栈、寄存器等内核空间的状态
进程的上下文切换就比系统调用时多了一步
- 在保存当前进程的内核状态和 CPU 寄存器之前,需要先把该进程的虚拟内存、栈等保存下来【保存上下文】
- 而加载了下一进程的内核态后,还需要刷新进程的虚拟内存和用户栈【加载上下文】
保存上下文和加载上下文的过程需要内核在 CPU 上运行才能完成
进程上下文切换如何影响系统性能?
- 根据 Tsuna 的测试报告,每次上下文切换都需要几十纳秒到数微秒的 CPU 时间
- 这个时间还是略大的,特别是在进程上下文切换次数较多的情况下,很容易导致 CPU 将大量时间耗费在寄存器、内核栈以及虚拟内存等资源的保存和恢复上,进而大大缩短了真正运行进程的时间
- 这也正是上一篇文章中讲到的,导致平均负载升高的一个重要因素
TLB 也会受影响?
- TLB(Transaction Lookaside Buffer)来管理虚拟内存到物理内存的映射关系
- 当虚拟内存更新后,TLB 也需要刷新,内存的访问也会变慢
- 特别是在多处理器系统上,缓存是被多个处理器共享的,刷新缓存不仅会影响当前处理器的进程,还会影响共享缓存的其他处理器的进程
什么时候会切换进程上下文
- 顾名思义,只有在进程切换时才需要切换上下文
- 换句话说,只有在进程调度时才需要切换上下文
CPU 如何挑选进程来运行?
- Linux 为每个 CPU 都维护了一个等待队列
- 将活跃进程(正在运行和正在等待 CPU 的进程)按照优先级和等待 CPU 的时间排序
- 然后选择最需要 CPU 的进程,也就是优先级最高和等待 CPU 时间最长的进程来运行
进程什么时候才会被调度到 CPU 上运行?
1 - 主动释放
进程执行完终止了,会释放 CPU,这时候从等待队列中拿一个新的进程来运行
2 - 时间片轮转
- 为了保证所有进程可以得到公平调度,CPU 时间被划分为一段段的时间片
- 这些时间片再被轮流分配给各个进程,当某个进程的时间片耗尽了,就会被系统挂起,切换到其它正在等待 CPU 的进程运行
3 - 资源不足
进程在系统资源不足(比如内存不足)时,要等到资源满足后才可以运行,这个时候进程也会被挂起,并由系统调度其他进程运行
4 - sleep 函数
当进程通过睡眠函数 sleep 这样的方法将自己主动挂起时,自然也会重新调度
5 - 优先级更高
当有优先级更高的进程运行时,为了保证高优先级进程的运行,当前进程会被挂起,由高优先级进程来运行
6 - 硬中断
发生硬件中断时,CPU 上的进程会被中断挂起,转而执行内核中的中断服务程序
进程上下文切换类比场景
- 银行分配各个窗口给来办理业务的人
- 如果只有1个窗口开放,大部分都得等【系统资源不足】
- 如果正在办理业务的突然说自己不办了,那他就去旁边休息【sleep】
- 如果突然来了个VIP客户,可以强行插队【优先级高】
- 如果突然断电了,都得等。。【中断】
线程上下文切换
先来聊下线程和进程的关系
- 线程和进程的最大区别在于:线程是调度的基本单位,进程是资源分配的基本单位
- 内核中的任务调度,实际上的调度对象是线程
- 而进程只是给线程提供了虚拟内存、全局变量等资源
- 当进程只有一个线程时,可以任务进程=线程
- 当进程有多个线程时,线程会共享进程的虚拟内存和全局变量等资源,在线程上下文切换时这些资源是不需要修改的
- 线程也有独立的数据,比如栈、寄存器等,这些在线程上下文切换时是需要保存的
线程上下文切换的场景一
- 前后两个线程属于不同进程
- 此时,因为不同进程的资源不共享,所以线程上下文切换 等同于 进程上下文切换
线程上下文切换的场景二
- 前后两个线程属于同一个进程
- 此时,因为同一进程的资源是共享的,所以在切换时,虚拟内存这些资源就保持不动
- 只需要切换线程的私有数据、寄存器等不共享的数据
多线程的优势
线程上下文切换对比进程上下文切换,很明显切换消耗的资源会更少,所以多线程比多进程更有优势
中断上下文切换
中断处理
- 为了快速响应硬件的事件,中断处理会打断进程的正常调度和执行,转而调用中断处理程序,响应设备事件
- 在打断其他进程时,就需要将进程当前的状态保存下来,这样在中断结束后,进程仍然可以从原来的状态恢复运行
和进程上下文切换的不同点
- 中断上下文切换并不涉及到进程的用户态
- 即便中断过程打断了 一个正处在用户态的进程,也不需要保存和恢复这个进程的虚拟内存、全局变量等用户态资源
- 中断上下文,只包括内核态中断服务程序执行所必需的状态,包括 CPU 寄存器、内核堆栈、硬件中断参数
中断上下文不会和进程上下文切换同时发生
- 对同一个 CPU 来说,中断处理比进程拥有更高的优先级
- 由于中断会打断正常进程的调度和执行,所以大部分中断处理程序都短小精悍,以便尽可能快的执行结束
耗资源程度
- 跟进程上下文切换一样,中断上下文切换也需要消耗 CPU,切换次数过多也会耗费大量的 CPU,甚至严重降低系统的整体性能
- 当发现中断次数过多时,就需要注意去排查它是否会给你的系统带来严重的性能问题
CPU 上下文切换的总结
- CPU 上下文切换,是保证 Linux 系统正常工作的核心功能之一,一般情况下不需要关注【CPU 上下文切换是正常工作的核心功能】
- 但过多的上下文切换,会把 CPU 时间消耗在寄存器、内核栈、虚拟内存等数据的保存和恢复上,从而缩短进程真正的运行时间,导致系统的整体性能大幅下降【数据保存和恢复时间增加,进程运行时间减少,性能下降】