00

把观察到的性能问题跟系统原理关联起来,特别是把系统从应用程序、库函数、系统调用、再到内核和硬件等不同的层级贯穿起来。

应用程序 –> 库函数 –> 系统调用 –> 内核 –> 硬件

01

linux_perf_tools_full

这张图告诉你,在 Linux 不同子系统出现性能问题后,应该用什么样的工具来观测和分析。

02 平均负载

uptime 输出含义

[centli@VM_0_14_centos ~]$ uptime
 10:43:05 up 346 days, 12:41,  1 user,  load average: 0.47, 0.23, 0.13
10:43:05			// 系统当前时间
up 346 days, 12:41,	// 系统运行时间
1 user			// 正在登录用户数
0.47			// 过去 1 分钟的平均负载
0.23			// 过去 5 分钟的平均负载
0.13			// 过去 15 分钟的平均负载

平均负载并不是单位时间内 CPU 使用率,而是单位时间内平均活跃进程数

平均负载为多少时合理?

把 CPU 刚好占满,最合理。

查看 CPU 个数

$ grep "model name" /proc/cpuinfo | wc -l
4

所以,平均负载为 load average: 4, 4, 4 最合理。

平均负载超过多少算高

经验值:高于 70%,即 4 核 CPU 平均负载高于 6.8

平均负载和 CPU 使用率

平均负载指单位时间内,处于可运行状态和不可中断状态的进程数。它不仅包含了正在使用 CPU 的进程,还包括等待 CPU等待 I/O 的进程。

活跃进程数 = 正在使用 CPU 的进程数 + 等待 CPU 的进程数 + 等待 I/O 的进程数

而 CPU 的使用率,是单位时间内 CPU 繁忙情况的统计,根平均负载并不一定完全对应。比如

  • CPU 密集型进程,使用大量 CPU 会导致平均负载升高,此时这两者是一致的;
  • I/O 密集型进程,等待 I/O 也会导致平均负载升高,但 CPU 使用率不一定很高;
  • 大量等待 CPU 的进程调度也会导致平均负载升高,此时的 CPU 使用率也会比较高。

相关工具

  • stress 是一个 Linux 系统压力测试工具,可以用作异常进程模拟平均负载升高的场景

  • sysstat 包含了常用的 Linux 性能工具,用来监控和分析系统的性能。其中包含 mpstat、pidstat

小结

  • 可运行状态的进程:正在使用 CPU 或者正在等待 CPU 的进程,即 ps aux 命令下 STAT 处于 R 状态(Running 或 Runnable)的进程。
  • 不可中断状态的进程:处于内核态关键流程中的进程,这些进程不可被打断,如等待硬件设备 I/O 响应,也就是 ps aux 命令 D 状态(Uninterruptible Sleep,也称为 Disk Sleep)的进程。比如,当一个进程向磁盘读写数据时,为了保证数据的一致性,在得到磁盘回复前,它是不能被其它进程或者中断打断的,这个时候的进程就处于不可中断状态。如果此时的进程被打断了,就容易出现磁盘数据与进程数据不一致的问题。

03 CPU 上下文切换

Linux 是一个多任务操作系统,它支持远大于 CPU 数量的任务“同时“运行。当然,这些任务实际上并不是真的在同时运行,而是因为系统在很短的时间内,将 CPU 轮流分配给它们,造成多任务同时运行的错觉。

CPU 由运算器、控制器、寄存器 组成。

  • 运算器,ALU
  • 控制器,PC、IR、PSR
  • 寄存器,R0……RN

CPU 寄存器,是 CPU 内置的容量小、但速度极快的内存。而程序计数器,则是用来存储 CPU 正在执行的指令的位置、或者即将执行的下一条指令位置。他们都是 CPU 在运行任何任务前,必须的依赖环境,因此也被叫做 CPU 上下文。

CPU 上下文切换,就是先把前一个任务的 CPU 上下文(也就是 CPU 寄存器和程序计数器)保存起来,然后加载新任务的上下文到这些寄存器和程序计数器,最后在跳转到程序计数器所指的新位置,运行新任务。

而这些保存下来的上下文,会存储在系统内核中,并在任务重新调度执行时再次加载进来。这样就能保证任务原来的状态不受影响,让任务开起来还是连续运行。

什么是任务

  • 进程
  • 线程
  • 中断:硬件通过触发信号,会导致中断处理程序的调用,也是一种常见的任务。

CPU 上下文切换的几个不同场景

  • 进程上下文切换
  • 线程上下文切换
  • 中断上下文切换

# 进程上下文切换

进程既可以在用户空间中运行,又可以在内核空间中运行。进程在用户空间运行时,被称为进程的用户态,而陷入内核空间的时候,被称为进程的内核态。

  • 内核空间具有最高权限,可以直接访问所有资源

  • 用户空间只能访问受限资源,不能直接访问内存等硬件设备,必须通过系统调用陷入到内核中,才能访问这些特权资源。

从用户态到内核态的转变,需要通过系统调用来完成。

系统调用的过程有没有发生 CPU 上下文的切换呢?

有。

CPU 寄存器里原来用户态的指令位置,需要先保存起来。接着为了执行内核态代码,

CPU 寄存器需要更新为内核态指令的新位置。最后才是跳转到内核态运行内核任务。

而系统调用结束后,CPU 寄存器需要恢复原来保存的用户态,然后再切换到用户空间,继续运行进程。所以,一次系统调用的过程,其实是发生了两次 CPU 上下文切换。

不过,需要注意的是,系统调用过程中,并不会涉及到虚拟内存等进程用户态的资源,也不会切换进程。这跟我们通常所说的进程上下文切换是不一样的:

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

所以,系统调用过程通常称为特权模式切换,而不是上下文切换。但实际上,系统调用过程中,CPU 的上下文切换还是无法避免的。

何时会进行继承调度

  • 某个进程执行完了
  • 某个进程时间片耗尽了
  • 某个系统资源不足
  • 某个进程通过睡眠函数 sleep 这样的方法将自己主动挂起
  • 当有优先级更高的进程运行时
  • 发生硬件中断时

# 线程上下文切换

线程与进程最大的区别在于,线程是调度的基本单位,进程是资源拥有的基本单位。

说白了,所谓内核中的任务调度,实际上的调度对象是线程;而进程只是给线程提供了虚拟内存、全局变量等资源。

所以,对于线程和进程,我们可以这么理解:

  • 当进程只有一个线程时,可以认为进程就等于线程。
  • 当进程拥有多个进程时,这些线程会共享相同的虚拟内存和全局变量等资源。这些资源在上下文切换时是不需要修改的。
  • 另外,线程也有自己的私有数据,比如栈和寄存器等,这些在上下文切换时也是需要保存的。

这么一来,线程的上下文切换其实就可以分为两种情况:

  • 前后两个线程属于不同进程。此时,因为资源不共享,所以切换过程就跟进程上下文切换是一样。
  • 前后两个线程属于同一个进程。此时,因为虚拟内存是共享的,所以在切换时,虚拟内存这些资源就保持不动,只需要切换线程的私有数据、寄存器等不共享的数据。

# 中断上下文切换

中断上下文,只在内核态

一个酒店,里面满满的客人在吃饭,

进程上下文切换,好比是:酒店里面的所有人速速离开酒店,让下一波客人吃。

中断上下文切换,好比是:酒店里的客人不需要离开酒店,但是服务员就是不给他们上菜,因为服务员去给 VIP 客户服务去了,好在 VIP 客户比较好说话,很快就伺候好了,服务员再来给酒店里的客人服务:“不好意思,这道菜的工序比较复杂,让您久等了“。由于就晚服务了一小会,客人也没多说什么。

线程上下文切换,好比是:这个酒店比较惨,全酒店就一双筷子,谁拿到筷子谁有吃饭权;拿不到筷子的原地等候。好在所有客人依然占有酒店座位、菜品,只是暂时没有筷子而已,但至少不会被请出去。