1. 运行统计信息
1.1. CPU 使用分类
使用 top 的时候,在"%Cpu(s)"开头的这一行,会看到一串数值,也就是"0.0 us, 0.0 sy, 0.0 ni, 99.9 id, 0.0 wa, 0.0 hi, 0.0 si, 0.0 st",这里头的每一项值的含义如下所示:
下面使用一个例子来阐述一下这个,如图所示,它的上半部代表 Linux 用户态,下半部分代表内核态。为了方便阐述,这里假定只有一个 CPU。
-
假设一个用户程序开始运行了,那么就对应着第一个"us"框,"us"是"user"的缩写,代表 Linux 的用户态 CPU Usage。普通用户程序代码中,只要不是调用系统调用(System Call),这些代码的指令消耗的 CPU 就都属于"us"。
-
当这个用户程序代码中调用了系统调用,比如说 read() 去读取一个文件,这时候这个用户进程就会从用户态切换到内核态。内核态 read() 系统调用在读到真正 disk 上的文件前,就会进行一些文件系统层的操作。那么这些代码指令的消耗就属于"sy",这里就对应上面图里的第二个框。"sy"是 "system"的缩写,代表内核态 CPU 使用。
-
接下来,这个 read() 系统调用会向 Linux 的 Block Layer 发出一个 I/O Request,触发一个真正的磁盘读取操作。这时候,这个进程一般会被置为 TASK_UNINTERRUPTIBLE。而 Linux 会把这段时间标示成"wa",对应图中的第三个框。"wa"是"iowait"的缩写,代表等待 I/O 的时间,这里的 I/O 是指 Disk I/O。
-
紧接着,当磁盘返回数据时,进程在内核态拿到数据,这里仍旧是内核态的 CPU 使用中的"sy",也就是图中的第四个框。
-
然后,进程再从内核态切换回用户态,在用户态得到文件数据,这里进程又回到用户态的 CPU 使用,"us",对应图中第五个框。
-
这里我们假设一下,这个用户进程在读取数据之后,没事可做就休眠了。并且我们可以进一步假设,这时在这个 CPU 上也没有其他需要运行的进程了,那么系统就会进入"id"这个步骤,也就是第六个框。"id"是"idle"的缩写,代表系统处于空闲状态。
-
如果这时这台机器在网络收到一个网络数据包,网卡就会发出一个中断(interrupt)。相应地,CPU 会响应中断,然后进入中断服务程序。这时,CPU 就会进入"hi",也就是第七个框。"hi"是"hardware irq"的缩写,代表 CPU 处理硬中断的开销。由于我们的中断服务处理需要关闭中断,所以这个硬中断的时间不能太长。
-
发生中断后的工作是必须要完成的,如果这些工作比较耗时那怎么办呢?Linux 中有一个软中断的概念(softirq),它可以完成这些耗时比较长的工作。你可以这样理解这个软中断,从网卡收到数据包的大部分工作,都是通过软中断来处理的。那么,CPU 就会进入到第八个框,"si"。这里"si"是"softirq"的缩写,代表 CPU 处理软中断的开销。
无论是"hi"还是"si",它们的 CPU 时间都不会计入进程的 CPU 时间。这是因为本身它们在处理的时候就不属于任何一个进程。
1.2. 进程 CPU 使用率计算
Linux 中可以使用 top 命令查看 CPU 使用率,每个进程在 top 命令输出中都对应一行,其中 %CPU 那一列就是进程的实时 CPU 使用率了。比如显示,100% 就表示这个进程在这个瞬时使用了一个 CPU,200% 就是使用了 2 个 CPU。
那么这个数值的计算如下所示:
-
对于每个进程,top 会从 /proc 文件系统中每个进程对应的 stat 文件中读取第 14、15 个数值。
每个对应的 stat 文件是 /proc/[pid]/stat , [pid] 就是替换成具体一个进程的 PID 值。比如 PID 值为 1 的进程,这个文件就是 /proc/1/stat。这个 stat 文件实时输出了进程的状态信息,比如进程的运行状态(running 还是 sleeping)、父进程 PID、进程优先级、进程使用的内存等等。完整的 stat 文件内容和格式可以看 proc 文件系统的 Linux programmer’s manual(https://man7.org/linux/man-pages/man5/proc.5.html)。
而 stat 中第 14、15 项分别是 utime 和 stime。utime 表示进程的用户态部分在 Linux 调度中获得 CPU 的 ticks,stime 是表示进程的内核态部分在 Linux 调度中获得 CPU 的 ticks。需要注意的是,utime 和 stime 都是一个累计值,也就是从进程启动开始计算的。
ticks 是指,Linux 中有个自己的时钟,它会周期性地产生中断。每次中断都会触发 Linux 内核去做一次进程调度,而这一次中断就是一个 tick。因为是周期性的中断,比如 1 秒钟 100 次中断,那么一个 tick 作为一个时间单位看的话,也就是 1/100 秒。假如进程的 utime 是 130ticks,就相当于 130 * 1/100=1.3 秒,也就是进程从启动开始在用户态总共运行了 1.3 秒钟。
这个 1 秒钟 100 次中断,可以通过命令 "getconf CLK_TCK"获取。
-
那么对于一个进程的瞬时 CPU 使用率,我们可以通过这个公式计算:((utime_2 – utime_1) + (stime_2 – stime_1)) * 100.0 / (HZ * et * 1 )。
首先,我们可以假设进程的这个瞬时是 et 秒,那么在这个 et 秒的间隔中,也就是从 T1 时刻到 T2 时刻, ((utime_2 – utime_1) + (stime_2 – stime_1)) 是进程瞬时总的 CPU ticks。
其次,100的目的是产生百分比数值,比如没有这 100,可能结果是 0.1,那么有了这 100 之后就是 10 了。
最后是 HZ,它表示 1 秒钟里 ticks 的次数;et 则是那个瞬时的时间;1 则表示是 1 个 CPU。那么这三个相乘,就是在这个 et 时间里,1 个 CPU 所产生的 ticks 数目。
1.3. 系统 CPU 使用率计算
系统 CPU 的数据源也可以从 proc 文件系统里得到,这个文件是 /proc/stat。/proc/stat 文件的 cpu 这行有 10 列数据,这 10 列数据是系统自启动开始的累积的 ticks。可以在 proc 文件系统的 Linux programmer’s manual(https://man7.org/linux/man-pages/man5/proc.5.html)里,找到每一列数据的定义。而前 8 列数据正好对应 top 输出中"%Cpu(s)"那一行里的 8 项数据,也就是 user/system/nice/idle/iowait/irq/softirq/steal 这 8 项。
那么计算瞬时的 CPU 使用率跟上面是类似的:
-
首先假设系统这个瞬时时间为 1 秒钟,可以记录开始时刻 T1 的 ticks, 然后再记录 1 秒钟后 T2 时刻的 ticks,再把这两者相减,就可以得到这一秒钟里每种 CPU 使用类型使用的 ticks 了。
-
假如想要计算每种 CPU 使用类型的使用率百分比,其实就是把这 1 秒里的 ticks 相加得到一个总值,然后某一项的 ticks 除以这个总值,那么就是这一项的使用率百分比率了。
比如计算 idle CPU 的使用率就是:(1203 / 0 + 0 + 0 + 1203 + 0 + 0 + 0 + 0)=100%
总共有 12 个 CPU core。
1.4. Linux 中的 load average
通过 top 命令,我们可以看到 load average 一栏会有三个数值,它们分别代表过去 1 分钟,5 分钟,15 分钟在这个节点上的 Load Average。假如 Load Average 为 9,那么意味着将使用 9 个 CPU。
Load Average 是一种 CPU 资源需求的度量。举个例子,对于一个单个 CPU 的系统,如果在 1 分钟的时间里,处理器上始终有一个进程在运行,同时操作系统的进程可运行队列中始终都有 9 个进程在等待获取 CPU 资源。那么对于这 1 分钟的时间来说,系统的"load average"就是 1+9=10(因为那个始终运行的进程算使用了 1 个 CPU,而对于后头的 9 个进程,它们可能会使用 9 个 CPU),这个定义对绝大部分的 Unix 系统都适用。这个时候 Load Average 的理解如下:
- 当计算机上的 CPU 还有空闲的情况下,CPU Usage 可以直接反映到"load average"上,什么是 CPU 还有空闲呢?具体来说就是可运行队列中的进程数目小于 CPU 个数,这种情况下,单位时间进程 CPU Usage 相加的平均值应该就是"load average"的值。换句话说,是不是只要 cpu idle 不是0,取的都是 cpu usage * CPU core 总数。
- 计算机上的 CPU 满负载的情况下,计算机上的 CPU 已经是满负载了,同时还有更多的进程在排队需要 CPU 资源。这时"load average"就不能和 CPU Usage 等同了。比如对于单个 CPU 的系统,CPU Usage 最大只是有 100%,也就 1 个 CPU;而"load average"的值可以远远大于 1,因为"load average"看的是操作系统中可运行队列中进程的个数。
比如,在一台有 4 个 CPU 的计算机节点上运行了 2 个满负载的线程,每个线程各占 100% 的 CPU,两个就是 200%,那么这个时候 load average 就是 2,等于 (CPU Usage 的 50% * 4 个 CPU),也就是第一种情况。
那么假如,同样在这个 4 个 CPU 的计算机节点上运行了 6 个满负载的线程。那么这 6 个线程的 CPU Usage 是 400%,而 load average 则为 4+2 = 6。
那么,如果一台2个cpu的机器,跑了8个进程,每个进程使用一个cpu的10%,此时的 load average 应该是 0.8。
但是,对于 Linux 来说,Load Average 除了可运行队列中的进程数目,等待队列中的 UNINTERRUPTIBLE 进程数目也会增加 Load Average,也就是将会统计以下这两种情况的进程:
- Linux 进程调度器中可运行队列(Running Queue)一段时间(1 分钟,5 分钟,15 分钟)的进程平均数。
- Linux 进程调度器中休眠队列(Sleeping Queue)里的一段时间的 TASK_UNINTERRUPTIBLE 状态下的进程平均数。
所以,最后的公式是:Load Average = 可运行队列进程平均数 + 休眠队列中不可打断的进程平均数
为什么需要统计这两种情况?Linux 早期的时候,那时候开发者 Matthias 有这么一个发现,比如把快速的磁盘换成了慢速的磁盘,运行同样的负载,系统的性能是下降的,但是 Load Average 却没有反映出来。这是因为 Load Average 只考虑运行态的进程数目,而没有考虑等待 I/O 的进程。所以,他认为 Load Average 如果只是考虑进程运行队列中需要被调度的进程或线程平均数目是不够的,因为对于处于 I/O 资源等待的进程都是处于 TASK_UNINTERRUPTIBLE 状态的,它们同样也会竞争系统资源,所以它会影响到应用程序的性能。这之后加了一个补丁将休眠队列中不可打断的进程也考虑进去了。
可以这么理解:
cpu:比做一条高速公路
进程占用:比做一辆辆汽车
高速公路上的拥堵情况(load average)= 正在跑的汽车 + 收费站排队等待进入的汽车 + 服务站加油、吃饭的汽车(D进程);
把D进程考虑进去是因为,服务站里的汽车随时可能进入高速公路,造成拥堵程度增加
1.4.1. Load Average 值升高,应用的性能将会下降
因为 Load Average 升高很有可能是因为 TASK_UNINTERRUPTIBLE 状态的进程引起的(我们可以运行 ps aux | grep “ D ” ,就可以看到容器中有多少 TASK_UNINTERRUPTIBLE 状态)。而处于 TASK_UNINTERRUPTIBLE 状态的进程很可能是因为进程对 disk I/O 访问或信号量(Semaphore)锁访问时,没有获得资源,所以会感觉应用的性能下降了。而且对 disk I/O 的访问还是对信号量的访问,其实都是对 Linux 系统里的资源的一种竞争,那么 Load Average 值升高表示竞争越激烈,所以性能也一定程度会下降。
在 Linux 内核中有数百处调用点,它们会把进程设置为 D 状态(即 TASK_UNINTERRUPTIBLE 状态),这些主要集中在 disk I/O 的访问和信号量(Semaphore)锁的访问上,因此 D 状态的进程在 Linux 里是很常见的。
可以
cat /proc/<pid>/stack
, 看到进程停在内核中的哪个函数上,结合内核的代码,可以“猜一下”大概是在哪个信号量上。