CPU上下文切换详解

概述

上篇博客我简单介绍了用户态以及内核态相关知识,在实际并发专题中,除了CPU状态的切换,CPU的上下文切换也非常重要。本篇博客我就来简单介绍下CPU上下文切换相关的知识。


上下文切换

本篇博客分以下几个模块展开:

  1. CPU 上下文
  2. CPU 上下文切换
  3. CPU 上下文切换的类型
  4. CPU 上下文切换的消耗
  5. 频繁上下文切换问题解决

1、CPU 上下文

我们知道 linux 操作系统是一个多任务处理系统,它支持远超过CPU数量的任务 “并发” 执行。这里的并发不是说同一时刻一起执行,而是指多个任务快速切换执行,因为切换的速度极快,给人一种多个任务同时执行的错觉。

而每个任务执行前,CPU都需要知道这个任务从哪里开始加载,从哪里开始执行,也就是需要CPU提前加载好 寄存器程序计数器

  • 寄存器:寄存器是CPU中一块内存较小但运算极快的内存区域。

  • 程序计数器:程序计数器是一种专用的寄存器,它主要保存当前或下一条需要被执行的指令地址。

这里所说的寄存器和程序计数器就是CPU的上下文,因为它们都是 CPU 在运行任何任务前,必须依赖的环境。


2、CPU 上下文切换

上下文切换是指 CPU 从一个进程或线程切换到另一个进程或线程。下面我通过进程的上下文切换举个例子:

  1. 挂起一个进程,将这个进程在 CPU 中的状态(上下文)存储于内存中某处
  2. 在要切换的进程内存中检索上下文并加载到CPU寄存器和程序计数器
  3. CPU 跳转到程序计数器所指向的位置,恢复进程执行。

3、CPU 上下文切换的种类

根据切换任务的不同,上下文切换一般分为以下四种:

  • 系统调用
  • 进程上下文切换
  • 线程上下文切换
  • 中断上下文切换

3-1、系统调用

上篇博客我们提到用户态和内核态,对应进程运行的空间就可以分为用户空间和内核空间。

进程既可以在用户空间运行,也可以在内核空间运行。当进程在用户空间运行时,我们称当前调度进程的CPU处于用户态,当进程在内核空间运行时,我们称当前CPU处于内核态

当任务调用 系统调用 时,CPU状态需要从用户态切换到内核态。在这个过程中实际就发生了上下文切换,整个过程如下所示:

  1. 首先,把 CPU 用户态的上下文(寄存器里指令位置)保存到内存某处
  2. 为了执行内核代码,CPU 需要加载内核态的上下文到寄存器
  3. CPU 跳转到内核态指令位置执行系统调用
  4. 系统调用执行完毕后,CPU切回用户态、进入用户空间,加载用户态的上下文并恢复执行

也就是说:一次系统调用会导致两次上下文切换。(用户态 > 内核态 > 用户态)

但系统调用的过程中并不会涉及虚拟内存等进程用户态的资源,也不会切换进程,这和平时说的进程上下文切换是不一样的:

  • 进程上下文切换,是指从一个进程切换到另一个进程运行
  • 系统调用过程中一直是同一个进程在运行

因此,系统调用的过程通常称为 特权模式切换,而不是上下文切换。


3-2、进程上下文切换

在操作系统中,进程(应用程序)在用户空间相互独立,互相不能察觉到对方的存在,也不能互相访问内存资源,而进程又是通过内核调度的,因此进程的切换只能发生在内核态。

因为在内核态切换的原因,进程的上下文除了包括虚拟内存、栈、全局变量等用户空间资源外,还包括内存堆栈、寄存器等内核空间的状态。

也就是说:进程的上下文切换在保存当前进程的内核状态和 CPU 寄存器之前,需先把该进程的虚拟内存、栈等保存下来;而加载了下一进程的内核态后,还需要刷新进程的虚拟内存和用户栈

Linux 为每个 CPU 维护了一个就绪队列,将活跃进程按照优先级和等待 CPU 的时间排序,然后选择最需要 CPU 的进程,也就是优先级最高和等待 CPU 时间最长的进程来运行。那么,进程在什么时候才会在内核态上下文切换呢?

  1. 进程执行完终止了,它之前使用的 CPU 会释放出来,这时再从就绪队列中拿一个新的进程来运行
  2. 为了保证所有进程可以得到公平调度,CPU 时间被划分为一段段的时间片,这些时间片被轮流分配给各个进程。当某个进程时间片耗尽了就会被系统挂起,切换到其它等待 CPU 的进程运行
  3. 进程在系统资源不足时,要等待资源满足后才可以运行,这时进程也会被挂起,并由系统调度其它进程运行。
  4. 当进程通过睡眠函数 sleep 主动挂起时,也会重新调度
  5. 当有优先级更高的进程运行时,为了保证高优先级进程的运行,当前进程会被挂起,由高优先级进程来运行。
  6. 发生硬件中断时,CPU 上的进程会被中断挂起,转而执行内核中的中断服务程序。

3-3、线程上下文切换

进程是操作系统资源分配的基本单位,线程才是操作系统调度的基本单位。也就是说,内核在调度进程时,实际上就是在调度线程。进程为它所拥有的所有线程提供虚拟内存和全局变量等资源

当进程中只有一个线程时,进程内部不存在线程上下文切换。当进程内部有多个线程时,因为所有线程共享虚拟内存和全局变量,因此这些资源在上下文切换时是不需要保存的,但线程有自己独享的资源,如栈或寄存器等,这些资源在上下文切换时是需要保存的。

根据切换前后线程所在进程是否相同,线程的上下文切换分为以下两种:

  1. 进程内部上下文切换时,因为共享进程资源,因此上下文切换只需要保存线程独占数据和寄存器
  2. 不同进程间上下文切换时,因为不存在共享数据,此时就等同于进程上下文切换

从这里就可以看出,进程内部上下文切换所需要保存和恢复的上下文数据较少,相应的资源损耗也就更少,效率更高,这也是多线程的一大优势。


3-4、中断上下文切换

为了快速响应硬件事件,中断处理会打断当前正在运行的进程,转而调用中断处理程序,响应设备事件。当在打断进程时,就需要把当前进程的状态保存起来,这样在中断处理程序执行结束后,进程可以从原来中断处接着运行。

跟进程上下文不同,中断上下文切换并不涉及到进程的用户态。所以,即便中断过程打断了一个正处在用户态的进程,也不需要保存和恢复这个进程的虚拟内存、全局变量等用户态资源。中断上下文,其实只包括内核态中断服务程序执行所必需的状态,包括 CPU 寄存器、内核堆栈、硬件中断参数等。

对同一个 CPU 来说,中断处理比进程拥有更高的优先级,所以中断上下文切换并不会与进程上下文切换同时发生。同样道理,由于中断会打断正常进程的调度和执行,所以大部分中断处理程序都短小精悍,以便尽可能快的执行结束。


4、上下文切换的消耗

通过前面的分析,我们可以看出不同的上下文切换所要消耗的资源也是不同的。进程间切换所要耗费的资源最多,进程内部线程间上下文切换所要消耗的资源较少。当执行系统调用时,会发生特权模式切换,切换前后都在同一个进程中,所要耗费的资源最少。

频繁的上下文切换可能会导致系统性能问题,特别是在进程上下文切换次数较多的情况下,很容易导致 CPU 将大量时间消耗在寄存器、内核栈、虚拟内存等资源的保存和恢复上,从而大大缩短了真正运行进程的时间。

Linux 通过 TLB(Translation Lookaside Buffer)来管理虚拟内存到物理内存的映射关系。当虚拟内存更新后,TLB 也需要刷新,内存的访问也会随之变慢。特别是多处理器系统上,缓存是被多个处理器共享的,刷新缓存不仅会影响当前处理器的进程,还会影响共享缓存的其它处理器的进程。


5、频繁上下文切换问题解决

如果系统真正因为频繁上下文切换导致性能问题时,可以通过以下方式排查问题:

  • 自愿上下文切换变多时,说明进程都在等待资源,有可能发生了 IO ,死锁等问题

  • 非自愿上下文切换变多时,说明进程都在被强制调度,也就是都在争抢 CPU,说明 CPU 的确成了瓶颈

  • 中断次数变多了,说明 CPU 被中断处理程序占用,还需要通过查看 /proc/interrupts 文件来分析具体的中断类型。


参考:
https://zhuanlan.zhihu.com/p/52845869
http://ifeve.com/context-switch-definition/
https://www.linuxblogs.cn/articles/18120200.html
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值