目录
概述
本文章是极客时间《Linux性能优化》的课程读后感,建议大家可以去看下,参考资料中有地址。
Linux 是一个多任务操作系统,它支持远大于 CPU 数量的任务同时运行。当然,这些任务实际上并不是真的在同时运行,而是因为系统在很短的时间内,将 CPU 轮流分配给它们,造成多任务同时运行的错觉。
每个任务运行前,系统先帮它设置好 CPU 寄存器和程序计数器(Program Counter,PC)。这样CPU 就知道任务从哪里加载、又从哪里开始运行。
CPU 上下文含义
CPU 寄存器和程序计数器就是 CPU 上下文,因为它们都是 CPU 在运行任何任务前,必须的依赖环境。上下文是指某一时间点 CPU 寄存器和程序计数器的内容。
- CPU 寄存器是 CPU 内置的容量小、但速度极快的内存(与之对应的是 CPU 外部相对较慢的 RAM 主内存)。
- 程序计数器则是一个专用的寄存器,是用来存储 CPU 正在执行的指令位置、或者即将执行的下一条指令位置。
CPU 上下文切换
CPU上下文切换就是保存上一个任务运行的寄存器和计数器信息切换到加载下一个任务的寄存器和计数器的过程就是先把前一个任务的 CPU 上下文(也就是 CPU寄存器和程序计数器)保存起来,然后加载新任务的上下文到这些寄存器和程序计数器,最后再跳转到程序计数器所指的新位置,运行新任务。
而这些保存下来的上下文,会存储在系统内核中,并在任务重新调度执行时再次加载进来。这样就能保证任务原来的状态不受影响,让任务看起来还是连续运行。
上下文切换可以认为是内核(操作系统的核心)在 CPU 上对于进程(包括线程)进行以下的活动:
- 挂起一个进程,将这个进程在 CPU 中的状态(上下文)存储于内存中的某处,
- 在内存中检索下一个进程的上下文并将其在 CPU 的寄存器中恢复,
- 跳转到程序计数器所指向的位置(即跳转到进程被中断时的代码行),以恢复该进程。
CPU 上下文切换的类型
根据任务的不同,可以分为以下三种类型
- 进程上下文切换
- 线程上下文切换
- 中断上下文切换
进程上下文切换
内核把虚拟地址空间划分为两个部分,因此能够保护各个系统进程,使之彼此隔离。所有的现代CPU 都提供了几种特权级别,进程可以驻留在某一特权级别。每个特权级别都有各种限制,例如对执行 某些汇编语言指令或访问虚拟地址空间某一特定部分的限制。IA-32体系结构使用4种特权级别构成的系统, 各级别可以看作是环。内环能够访问更多的功能,外环则较少,如图所示:
Linux 按照特权等级,把进程的运行空间分为内核空间和用户空间。简而言之,在用户状态禁止访问内核空间。用户进程不能操作或读取内核空间中的数据,也无法执行内核空间中的代码。这是内核的专用领域。这种机制可防止进程无意间修改彼此的数据而造成相互干扰。从用户状态到核心态的切换通过系统调用的特定转换手段完成。
进程既可以在用户空间运行,又可以在内核空间中运行。进程在用户空间运行时,被称为进程的用户态,而陷入内核空间的时候,被称为进程的内核态。从⽤用户态到内核态的转变,需要通过系统调⽤用来完成。
举个用户态转内核态的例子说明如下:
当我们查看文件内容时,就需要多次系统调用来完成:
- 首先调用 open() 打开⽂件,
- 然后调用 read() 读取⽂件内容,并调⽤ write() 将内容写到标准输出,
- 最后调用 close() 关闭⽂件。
系统调用发生的CPU上下文切换做了些什么操作呢?
- CPU寄存器里原来的用户态的指令位置保存起来。
- CPU寄存器更新为内核态指令的新位置。
- 跳转到内核态运行内核任务。
系统调用结束后,CPU寄存器需要恢复原来保存的用户态,然后切换到用户空间,就行运行进程。通过这些步骤可以知道一次系统的调用过程,其实是发生了两次CPU上下文切换。
不过需要注意的是,在系统调用过程中,并不会涉及到虚拟内存等进程用户态的资源,也不会切换进程。这就跟我们通常说的进程上下文切换是不一样的,进程上下文切换,是指从一个进程切换到另一个进程。而系统调用过程中一直是同一个进程在运行。
进程上下文切换和系统调用的区别
进程是由内核来管理和调度的,进程的切换只能发生在内核态。所以,进程的上下文不仅包括了了虚拟内存、栈、全局变量等⽤用户空间的资源,还包括了内核堆栈、寄存器等内核空间的状态。
因此,进程的上下文切换就比系统调用时多了一步:在保存当前进程的 内核状态和 CPU 寄存器之前,需要先把该进程的虚拟内存、栈等保存下来;⽽加载了下一进程的内核态后,还需要刷新进程的虚拟内存和用户栈。
如下图所示,保存上下⽂和恢复上下文的过程并不是“免费”的,需要内核在 CPU 上运行才能完成。
进程上下文切换性能问题
根据 Tsuna 的测试报告,每次上下文切换都需要几十纳秒到数微秒的 CPU 时间。这个时间还是相当可观的,特别是在进程上下文切换次数较多的情况下,很容易导致 CPU 将大量时间耗费在寄存器、内核栈以及虚拟内存等资源的保存和恢复上,进而大大缩短了真正运行进程的时间。这也正是导致平均负载升高的一个重要因素。
Linux 通过 TLB(Translation Lookaside Buffer)来管理虚拟内存到物理内存的映射关系。当虚拟内存更新后,TLB 也需要刷新,内存的访问也会随之变慢。特别是在多处理器系统上,缓存是被多个处理器共享的,刷新缓存不仅会影响当前处理器的进程,还会影响共享缓存的其他处理器的进程。
进程上下文切换的场景
进程切换时才需要切换上下文,换句话说,只有在进程调度的时候,才需要切换上下⽂。Linux 为每个 CPU 都维护了了⼀个就绪队列, 将活跃进程(即正在运⾏和正在等待 CPU 的进程)按照优先级和等待 CPU 的时间排序,然后选择最需要 CPU 的进程,也就是优先级最⾼和等待 CPU 时间最长的进程来运⾏。
其他场景描述如下所示
- 为了保证所有进程可以得到公平调度,CPU 时间被划分为一段段的时间片,这些时间片再被轮流分配给各个进程。这样,当某个进程的时间片耗尽了,就会被系统挂起,切换到其它正在等待 CPU 的进程运行。
- 进程在系统资源不足(比如内存不足)时,要等到资源满足后才可以运行,这个时候进程也会被挂起,并由系统调度其他进程运行。
- 当进程通过睡眠函数 sleep 这样的方法将自己主动挂起时,自然也会重新调度。
- 当有优先级更高的进程运行时,为了保证高优先级进程的运行,当前进程会被挂起,由高优先级进程来运行。
- 发生硬件中断时,CPU 上的进程会被中断挂起,转而执行内核中的中断服务程序。
线程上下文切换
线程与进程最大的区别在于:线程是调度的基本单位,而进程则是资源拥有的基本单位。说白了,所谓内核中的任务调度,实际上的调度对象是线程;而进程只是给线程提供了虚拟内存、全局变量等资源。
所以,对于线程和进程,我们可以这么理解:
- 当进程只有一个线程时,可以认为进程就等于线程。
- 当进程拥有多个线程时,这些线程会共享相同的虚拟内存和全局变量等资源。这些资源在上下文切换时是不需要修改的。
- 另外,线程也有自己的私有数据,比如栈和寄存器等,这些在上下文切换时也是需要保存的。
线程上下文切换的场景
线程的上下文切换其实可以分为两种情况:
- 前后两个线程属于不同进程。此时,因为资源不共享,所以切换过程就跟进程上下文切换是一样。
- 前后两个线程属于同一个进程。此时,因为虚拟内存是共享的,所以在切换时,虚拟内存这些资源就保持不动,只需要切换线程的私有数据、寄存器等不共享的数据。
虽然同为上下文切换,但同进程内的线程切换,要⽐多进程间的切换消耗更少的资源,⽽这,也正是多线程代替多进程的⼀个优势。
中断上下文切换
为了快速响应硬件的事件,中断处理会打断进程的正常调度和执行,转而调用中断处理程序,响应设备事件。而在打断其他进程时,就需要将进程当前的状态保存下来,这样在中断结束后,进程仍然可以从原来的状态恢复运行。
跟进程上下文不同,中断上下文切换并不涉及到进程的用户态。所以,即便中断过程打断了一个正处在用户态的进程,也不需要保存和恢复这个进程的虚拟内存、全局变量等用户态资源。中断上下文,其实只包括内核态中断服务程序执行所必需的状态,包括 CPU 寄存器、内核堆栈、硬件中断参数等。
对同一个 CPU 来说,中断处理比进程拥有更高的优先级,所以中断上下文切换并不会与进程上下文切换同时发生。同样道理,由于中断会打断正常进程的调度和执行,所以大部分中断处理程序都短小精悍,以便尽可能快的执行结束。
另外,跟进程上下文切换一样,中断上下文切换也需要消耗 CPU,切换次数过多也会耗费大量的 CPU,甚至严重降低系统的整体性能。所以,当你发现中断次数过多时,就需要注意去排查它是否会给你的系统带来严重的性能问题。
减少CPU上下文切换
既然上下文切换会导致额外的开销,因此减少上下文切换次数便可以提高多线程程序的运行效率。
减少上下文切换的方法主要有无锁并发编程、CAS 算法、使用最少线程和使用协程。
- 无锁并发编程:多线程竞争时,会引起上下文切换(因为只有一个线程能进入临界区,获取锁失败的线程会被阻塞在临界区之外,线程此时挂起),所以多线程处理数据时,可以用一些办法来避免使用锁,如将数据的 ID 按照 Hash 取模分段,不同的线程处理不同段的数据。
- CAS 算法:Java 的 Atomic 包使用 CAS 算法来更新数据,而不需要加锁。(这种方式思想同无锁并发编程一样,只不过这是一种具体的且常见的实现手段)
- 使用最少线程:避免创建不需要的线程,比如任务很少,但是创建了很多线程来处理,这样会造成大量线程都处于等待状态。(使用少量线程不止可以减少上下文切换,同时也减少了系统的开销)
- 使用协程:在单线程里实现多任务的调度,并在单线程里维持多个任务间的切换。
CPU上下文切换次数查看
从上面我们解释了什么是CPU的上下文切换,那么这个时候会有一个问题,我们怎么知道CPU上下文切换的情况呢?
在Linux系统下vmstat 是一个常用的系统性能分析工具,主要用来分析系统的内存使用情况,也常用来分析 CPU 上下文切换和中断的次数。比如,下面就是一个 vmstat 的使用示例:
- r 表示运行队列(就是说多少个进程真的分配到CPU)。
- b 表示阻塞的进程。
- swpd 虚拟内存已使用的大小。
- free 空闲的物理内存的大小。
- buff Linux/Unix系统是用来存储,目录里面有什么内容,权限等的缓存。
- cache cache直接用来记忆我们打开的文件,给文件做缓冲。
- si 每秒从磁盘读入虚拟内存的大小。
- so 每秒虚拟内存写入磁盘的大小。
- bi 块设备每秒接收的块数量,这里的块设备是指系统上所有的磁盘和其他块设备,默认块大小是1024byte。
- bo 块设备每秒发送的块数量。
- in 每秒CPU的中断次数,包括时间中断。
- cs 每秒上下文切换次数。
- us 用户CPU时间。
- sy 系统CPU时间。
- id 空闲 CPU时间。
- wt 等待IO CPU时间。
vmstat 只给出了系统总体的上下文切换情况,要想查看每个进程的详细情况,就需要使用我们前面提到过的 pidstat 了。给它加上 -w 选项,你就可以查看每个进程上下文切换的情况了。
- pid, 进程ID。
- cswch,表示每秒自愿上下文切换(voluntary context switches)的次数。
- nvcswch,表示每秒非自愿上下文切换(non voluntary context switches)的次数。
- cmd, 命令
自愿上下文切换和非自愿上下文切换的含义是什么呢?
- 自愿上下文切换,是指进程无法获取所需资源,导致的上下文切换。比如说, I/O、内存等系统资源不足时,就会发生自愿上下文切换。
- 非自愿上下文切换,则是指进程由于时间片已到等原因,被系统强制调度,进而发生的上下文切换。比如说,大量进程都在争抢 CPU 时,就容易发生非自愿上下文切换。
结论
CPU 上下文切换,是保证 Linux 系统正常工作的核心功能之一,一般情况下不需要我们特别关注。但过多的上下文切换,会把 CPU 时间消耗在寄存器、内核栈以及虚拟内存等数据的保存和恢复上,从而缩短进程真正运行的时间,导致系统的整体性能大幅下降。
参考资料
- https://time.geekbang.org/column/article/69859

微信公众号名称:技术茶馆
微信公众号ID : Night_ZW