目录

4.1 CPU

本节我们来介绍 CPU 相关的操作系统原理。

1. CPU 相关的操作原理

我们从下面几个方面入手来讲解 CPU 相关的操作系统原理:

  1. CPU 架构
  2. 中断处理
  3. 调度器: CPU 分配的内核子系统
  4. 进程:
    • 上下文切换
    • 僵尸进程

最后我们会说一说 CPU 检测的相关指标

2. CPU 架构

2.1 CPU 的组成

/images/linux_pf/cpu.png

一颗通用的双核处理器的组成如上。具体的组件就不一一解释了。

可以看到内存可能会同时被缓存在不同处理的多个 CPU 缓存中,缓存一致性确保了 CPU 永远访问正确的内存状态。

2.2 CPU 互联

多处理器架构的 CPU 互联与系统的内存架构(同一内存访问 UMA,或者 NUMA)有关。常见的CPU有两种连接方式:

  1. 共享系统总线: 在处理器数量增加增加的情况下,会因为共享总线而出现扩展性问题
  2. 专用互联: 互联不仅仅是处理器,还可以是其他组件。

处理器之间的私有连接提供了无须竞争的访问以及比共享系统总线更高的带宽。除了外部互联,处理器还有核间通信用的内部互联。

一个早期的 intel 共享总线的四处理器如下所示: /images/linux_pf/uma_intel.png

一个四处理器的Intel QPI 架构如下所示:

/images/linux_pf/QPI.png

2.3 MMU 和 TLB

/images/linux_pf/mmu_tlb.png

  • I$: 一级指令缓存: 按照虚拟地址空间寻址
  • D$: 一级数据缓存: 按照虚拟地址空间寻址
  • TLB: 转移后备缓冲器
  • E$: 二级缓存:按照物理内存地址寻址

MMU 负责虚拟地址到物理地址的转换,主存里的页表由MMU(硬件)直接读取,处理缓存未命中情况。

2.4 指令的执行

CPU 指令的执行包括预期,解码,执行,内存访问,寄存器写回。内存访问往往需要几十个CPU周期,这段期间指令执行陷入停滞,这段时间称为停滞周期,因此需要CPU缓存降低内存访问的周期数。

CPI和IPC:

  • CPI: 每指令周期数,IPC 的倒数,代表了指令处理的效率,是 CPU 使用率的本质,CPI 较高代表 CPU 经常陷入停滞
  • IPC: 每周期指令数, instructions per cycle

2.5 CPU性能计数器

CPU性能计数器(CPC) 有很多别名包括性能监测点计数器(PIC)、性能监控单元(PMU)、硬件时间和性能监控事件。它们是可以计数低级 CPU 活动的处理器寄存器。通常包括下列计数器:

  1. CPU 周期: 包括停滞周期和停滞周期类型
  2. CPU 指令:
  3. 一级,二级,三级缓存访问: 命中,未命中
  4. 浮点单元操作
  5. 内存 I/O: 读写停滞周期
  6. 资源 I/O: 读写停滞周期

每个 CPU 有少量,通常是 2-8 个,可编程记录类似事件的寄存器,哪些寄存器可用取决于处理器的型号。计数器计量哪些事件通过事件选择和 UMASK 确定。事件选择确定要计数的事件类型,UMASK 确定子类型或者子类型组。

3. 中断处理

3.1 中断和中断线程

/images/linux_pf/interrupt.png

中断被内核用来响应设备的服务请求,分为:

  1. 中断服务程序: 需要通过注册来处理设备中断,这类程序需要运行的尽可能块,以减少对活动线程中断的影响。如果中断要做的工作不少,尤其是可能被阻塞,最好通过中断线程来处理,由内核调度
  2. 从中断开始到中断被服务之间的时间叫做中断延时

中断是一种异步的事件处理机制,用来提高系统的并发处理能力。

3.2 Linux 中断处理

Linux 将中断处理过程分成了两个阶段:

  • 上半部分:
    • 用于快速处理中断,运行在中断禁止模式,会推迟新的中断的产生
    • 主要处理跟硬件紧密相关的或时间敏感的工作。
  • 下半部分:
    • 可以作为tasklet 或者工作队列,之后通常作为内核线程由内核做调度
    • 用来延迟处理上半部未完成的工作

以网络接收到数据包为例:

  1. 对上半部来说,既然是快速处理,其实就是要把网卡的数据读到内存中,然后更新一下硬件寄存器的状态(表示数据已经读好了),最后再发送一个软中断信号,通知下半部做进一步的处理。
  2. 而下半部被软中断信号唤醒后,需要从内存中找到网络数据,再按照网络协议栈,对数据进行逐层解析和处理,直到把它送给应用程序。

所以:

  1. 上半部直接处理硬件请求,也就是我们常说的硬中断,特点是快速执行;
  2. 下半部则是由内核触发,也就是我们常说的软中断,特点是延迟执行。

实际上上半部会打断 CPU 正在执行的任务,然后立即执行中断处理程序。而下半部以内核线程的方式执行,并且每个 CPU 都对应一个软中断内核线程,名字为 “ksoftirqd/CPU 编号”,比如说, 0 号 CPU 对应的软中断内核线程的名字就是 ksoftirqd/0。

3.1 软中断 tasklet 和工作队列

首先软中断有三种实现方式:

  1. softirq: 也叫软中断,为避免歧义,用英文表示
  2. tasklet:
  3. work queue:

4. 调度器

4.1 调度器简介

分时系统,通过划分执行时间,让多个进程同时运行。进程在处理器上和CPU间的调度是由调度器完成的。调度器操作线程(Linux 中是任务 task),并将它们隐射到 CPU 上。

/images/linux_pf/kernel_schduler.png

  • VCX: 资源上下文切换
  • ICX: 非自愿上下文切换
  • Premption: 抢占
  • Time Sharing: 分时
  • ON-PROC: 运行中
  • RUNNABLE: 可运行

调度器功能及实现

调度器要实现如下功能:

  1. 分时: 可运行多线程
  2. 抢占: 高优先级线程在变为可运行状态时,能抢占当前运行的线程
  3. 负载均衡: CPU 之间的负载均衡

在 Linux 上

  1. 分时通过系统时钟中断调用 scheduler_tick() 实现
  2. scheduler_tick 调用调度器类函数管理优先级和称为时间片的 CPU时间单位的到期事件
  3. 当线程变成可运行状态后就出发抢占,调度类函数 check_preempt_curr() 被调用
  4. 线程切换有 _schedule()管理,后者通过 pick_next_task() 选择最高优先级的线程运行
  5. 负载均衡由 load_balance() 函数负责执行

空闲线程

内核"空闲"线程只在没有其他可运行线程的时候才在 CPU 上运行,通常被设计为通知CPU执行停止指令或者减速以节省资源,CPU会在下一次硬件中断醒来。

4.2 CPU 运行队列

/images/linux_pf/cpu_run_queue.png 运行队列由调度器管理。花在等待 CPU 运行上的时间称为运行队列延时,又称调度器延时。

对于多处理器系统,内核通常为每个 CPU 提供一个运行队列,并尽量使得线程每次都被放到同一队列之中。目的是为了提高内存本地性,更高效的使用缓存。为每个 CPU 提供一个运行队列避免了队列操作的线程同步开销(mutex 锁)。

5.进程

进程包括进程地址空间内的数据和内核里的元数据(上下文):

  1. 内核上下文包含各种进程属性和统计信息
  2. 每一线程都包含一些元数据,包括在内核上下文里自己的优先级以及用户地址空间里自己的栈。

/images/linux_pf/process_enviroment.png

5.1上下文切换

在前面的文章我们说过,用户进程通过系统调用执行内核特权操作时,会做上下文切换,从用户态进入到内核态。但是根据切换的任务的不提供,上下文切换包括多种类型:

  1. 进程上下文切换
  2. 线程上下文切换
  3. 中断上下文切换
  4. 系统调用的上下文切换

首先我们要明白的是,无论什么类型的上下文切换, CPU 寄存器和程序计数器(Program Counter,PC)都需要保存和重载,这一部又分称为CPU 上下文切换。其次不同的类型上下文切换因为不同任务间共享层次不同,需要交换的内容也就不同。

系统调用的上下文切换

前面我们说过,执行系统调用时,一个进程的线程有两个栈: 一个用户级别栈和一个内核级别的栈。线程被阻塞时,用户级别的栈在系统调用期间不会改变。所以系统调用过程中,并不会涉及到虚拟内存等进程用户态的资源,上下文交换需要完成的操作其实很少。

进程上下文切换

/images/linux_pf/process_enviroment.png

进程的上下文不仅包括了虚拟内存、栈、全局变量等用户空间的资源,还包括了内核堆栈、寄存器等内核空间的状态。这些资源在进程上下文切换时都要保存和恢复。

Linux 通过 TLB(Translation Lookaside Buffer)来管理虚拟内存到物理内存的映射关系。当虚拟内存更新后,TLB 也需要刷新。在多处理器系统上,缓存是被多个处理器共享的,进程切换也会导致缓存被刷新。

由此可见进程上下文切换成本较高。

线程切换

通常线程切换说的是同一进程内的线程,因为不同进程内的线程切换叫做进程切换。

线程会共享相同的虚拟内存和全局变量等资源。这些在上下文切换时是不需要修改的。需要保存的仅仅是线程的私有数据,比如栈和寄存器等。

中断上下文切换

为了快速响应硬件的事件,中断处理会打断进程的正常调度和执行,转而调用中断处理程序,响应设备事件。因此中断上下文切换并不涉及到进程的用户态,只包括内核态中断服务程序执行所必需的状态,包括 CPU 寄存器、内核堆栈、硬件中断参数等。

可以查看上下文切换到工具有 vmstat, pidstat -tw

5.2 僵尸进程

正常情况下,当一个进程创建了子进程后,它应该通过系统调用 wait() 或者 waitpid() 等待子进程结束,回收子进程的资源;而子进程在结束时,会向它的父进程发送 SIGCHLD 信号,所以,父进程还可以注册 SIGCHLD 信号的处理函数,异步回收资源。如果父进程没这么做,或是子进程执行太快,父进程还没来得及处理子进程状态,子进程就已经提前退出,那这时的子进程就会变成僵尸进程。子进程的回收可以参考Python: 僵尸进程的产生和清除方法

通常,僵尸进程持续的时间都比较短,在父进程回收它的资源后就会消亡;或者在父进程退出后,由 init 进程回收后也会消亡。一旦父进程没有处理子进程的终止,还一直保持运行状态,那么子进程就会一直处于僵尸状态。大量的僵尸进程会用尽 PID 进程号,导致新进程不能创建,所以这种情况一定要避免。

对于僵尸进程,我们通常需要通过 pstree 找到其父进程,然后在父进程中解决。

5.3 进程状态

/images/linux_pf/process_status.png 进程状态有如下几种

状态 含义
D 不可中断睡眠,表示进程正在跟硬件交互,并且交互过程不允许被其他进程或中断打断
目的是为了保护进程数据和硬件的一致性
R 正在运行或可运行(处于就绪排队中)
S 可中断睡眠 (休眠中, 受阻, 在等待某个条件的形成或接受到信号)
T 已停止的 进程收到SIGSTOP, SIGSTP, SIGTIN, SIGTOU信号后停止运行
收到 SIGCONT 信号进程会回复运行
W 正在换页(2.6.内核之前有效)
X 死进程 (未开启)
Z 僵尸进程,进程已终止, 进程资源未被回收(比如进程的描述符、PID 等)
I Idle 的缩写,也就是空闲状态,用在不可中断睡眠的内核线程上,没有任何负载
D 状态的进程会导致平均负载升高,I 状态的进程不会
< 高优先级(not nice to other users)
N 低优先级(nice to other users)
L 页面锁定在内存(实时和定制的IO)
s 表示这个进程是一个会话的领导进程
会话是指共享同一个控制终端的一个或多个进程组。
l 多线程(使用 CLONE_THREAD,像NPTL的pthreads的那样)
+ 在前台进程组,进程组表示一组相互关联的进程

6. CPU 监测指标

与 CPU 相关的专业术语或者指标包括:

  1. 系统负载
  2. CPU 使用率
  3. 中断计数
  4. 上下文切换
  5. CPU 缓存命中率

6.1 进程状态与系统负载

1
2
3
4
5
6
7
8
> uptime
# 过去 1 分钟、5 分钟、15 分钟的平均负载(Load Average)
 05:10:14 up  8:37,  2 users,  load average: 0.21, 0.06, 0.06
 
> watch -d uptime

# 查看 CPU 个数
> grep 'model name' /proc/cpuinfo | wc -l

uptime 中定义的系统负载与进程状态有关,平均负载是指单位时间内,系统处于可运行状态(R)和不可中断状态(D)的平均进程数,因此平均负载跟CPU使用率没有必然联系。

6.2 CPU 使用率

CPU 使用率是通过测量 CPU 未运行内核空闲线程的时间得出的。CPU 使用率的测量包括了除此之外的所有时钟周期,包括内核停滞周期。CPU 可能会因为经常停滞等待 I/O 而导致高使用率,而不仅是执行指令。

如何测量时间跟 CPU 时间片有关:

  1. Linux 通过事先定义的节拍率(内核中表示为 HZ),来定义时间片的长短。HZ 是内核的可配选项,可以通过查询 /boot/config 内核选项来查看它的配置值。
  2. 时间片到期时,会触发系统计时器中断即clock()例程,会更新系统时钟和 jiffies 计数器;为了方便用户空间程序,内核还提供了一个用户空间节拍率 USER_HZ,它总是固定为 100,也就是 1/100 秒
  3. 根据计数器操作系统统计 CPU 运行不同运行程序的时间
    • /proc/stat 提供了 CPU 运行不同程序的计数信息
    • /proc/[pid]/stat 提供了每个进程运行情况的计数信息
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
grep 'CONFIG_HZ=' /boot/config-$(uname -r)

cat /proc/stat
cpu  772 0 812 17070 27 0 20 0 0 0
cpu0 772 0 812 17070 27 0 20 0 0 0
intr 38176 154 74 0 0 0 0 0 0 0 0 0 0 208 0 0 280 0 0 0 395 0 6282 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
ctxt 83630
btime 1591280448
processes 2288
procs_running 2
procs_blocked 0
softirq 52342 1 20713 43 422 6208 0 100 0 0 24855

诸如 top, ps, mpstat, pidstat -u 正是使用上面的数据计算的 CPU 使用率,不同的是计算的方式和周期不同而已。对于CPU使用率的深入分析,需要借助 perf 等高级分析工具。

短时进程

top, ps, pidstat -u 这类展示系统概要和进程快照的工具很难发现短时进程导致 CPU 使用率高的问题,需要使用记录事件的工具来配合诊断。下面是一些常见的分析思路:

  1. 需要时刻关注处于 Running 状态的进程数(top 命令 Tasks 行有不同状态进程的计数)
  2. 使用 pstree 可以查看进程树,有助于发现多进程问题
  3. sar -w 1: 可以实时统计系统每秒创建的任务数(进程数)
  4. execsnoop: 是一个专为短时进程设计的工具,它通过 ftrace 实时监控进程的 exec() 行为,并输出短时进程的基本信息

对于CPU使用率的深入分析,需要借助 perf 等高级分析工具。

CPU 使用率的相关指标

下面这些指标在各种 CPU 监测工具中都能看到,使用 man proc 可以看到他们的说明,但是最好是能记住:

指标 缩写 含义
user us 代表用户态 CPU 时间。注意,它不包括下面的 nice 时间,但包括了 guest 时间。
nice ni 代表低优先级用户态 CPU 时间,也就是进程的 nice 值被调整为 1-19 之间时的 CPU 时间。这里注意,nice 可取值范围是 -20 到 19,数值越大,优先级反而越低。
system sys 代表内核态 CPU 时间。
idle id 代表空闲时间。注意,它不包括等待 I/O 的时间
iowait wa 代表等待 I/O 的 CPU 时间,出现 iowait 有两个条件,一是进程在等io,二是等io时没有进程可运行
irq hi 代表处理硬中断的 CPU 时间。
softirq si 代表处理软中断的 CPU 时间。
steal st 代表当系统运行在虚拟机中的时候,被其他虚拟机占用的 CPU 时间。
guest guest 代表通过虚拟化运行其他操作系统的时间,也就是运行虚拟机的 CPU 时间。
guest_nice gnice 代表以低优先级运行虚拟机的时间。而我们通常所说的 CPU 使用率,就是除了空闲时间外的其他时间占总 CPU 时

注意:通常我们收的 CPU 使用率,就是除了空闲时间外的其他时间占总 CPU 时

6.3 中断计数

中断的计数信息位于:

  1. /proc/softirqs: 提供了软中断的计数信息
  2. /proc/interrupts: 提供了硬中断的计数信息
1
2
3
4
5
# 动态查看中断计数器的变化
> watch -d cat /proc/interrupts

# 动态查看软中断的变化
> watch -d cat /proc/softirqs

硬中断

硬中断的类型很多,常见的需要我们知道,包括:

  1. 重调度中断(RES): 又称处理器中断,与CPU之间实现缓存一致性和负载均衡有关

软中断

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
# 查看软中断计数
> cat /proc/softirqs
                    CPU0
          HI:          1
       TIMER:     526599
      NET_TX:       4262
      NET_RX:     121304
       BLOCK:      42438
BLOCK_IOPOLL:          0
     TASKLET:        358
       SCHED:          0
     HRTIMER:          0
         RCU:     113508

# 查看软中断内核线程
> ps aux | grep softirq
root  6  0.0  0.0  0  0 ?  S    09:13   0:00 [ksoftirqd/0]

/proc/softirqs 文件记录了所有的软中断类型以及它们在 CPU 上的发生次数。除了发生次数,我们要注意的是同一种软中断在不同 CPU 上的分布情况。正常情况下,同一种中断在不同 CPU 上的累积次数应该差不多。

6.4 上下文切换次数

从性能分析角度,上下文切换可以分为:

  1. 无法获取资源而导致的自愿上下文切换: 通常意味发生了锁,磁盘等资源竞争
  2. 被系统强制调度导致的非自愿上下文切换: 通常以为的应用程序负载较高

但过多的上下文切换,会将原本运行进程的 CPU 时间,消耗在寄存器、内核栈以及虚拟内存等数据的保存和恢复上,缩短进程真正运行的时间,成为性能瓶颈。可以使用 vmstat, pidstat -w 查看系统和进程的上下文切换次数。

6.5 CPU 缓存命中率