理解 CPU 利用率

从 top 命令说起

在 Linux shell 上执行 top 命令,可以看到这样一行 CPU 利用率的数据:

%Cpu(s):  0.1 us,  0.0 sy,  0.0 ni, 99.9 id,  0.0 wa,  0.0 hi,  0.0 si,  0.0 st

这里引用一下 top 命令的 Linux man-pages 里面的介绍:

us, user: time running un-niced user processes sy, system: time running kernel processes ni, nice: time running niced user processes id, idle: time spent in the kernel idle handler wa, IO-wait: time waiting for I/O completion hi: time spent servicing hardware interrupts si: time spent servicing software interrupts st: time stolen from this vm by the hypervisor

/proc/stat

简单介绍一下 Linux 计算 CPU 利用率的基本方法。

/proc/stat 存储的是系统的一些统计信息。在我的机器上的某一时刻,内容如下:

[linjinhe@localhost ~]$ cat /proc/stat 
cpu  117450 5606 72399 476481991 1832 0 2681 0 0 0
cpu0 31054 90 19055 119142729 427 0 1706 0 0 0
cpu1 22476 3859 18548 119155098 382 0 272 0 0 0
cpu2 29208 1397 19750 119100548 462 0 328 0 0 0
cpu3 34711 258 15045 119083615 560 0 374 0 0 0
intr 41826673 113 102 0 0 0 0 0 0 1 134 0 0 186 0 0 0 81 0 0 256375 0 0 0 29 0 602 143 16 442 94859 271462 25609 4618 8846 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 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 58634924
btime 1540055659
processes 5180062
procs_running 1
procs_blocked 0
softirq 49572367 5 22376247 238452 3163482 257166 0 4492 19385190 0 414733

我们只关注第一行 cpu (下面第二行是我加上的注释)。

cpu  117450 5606   72399   476481991  1832     0    2681   0      0         0
      (us)  (ni)    (sy)     (id)      (wa)   (hi)  (si)  (st) (guest) (guest_nice)

前面一节,对于 CPU 利用率描述,Linux man-pages 用的都是 time( time running, time spent,time stolen)这个单词。这里的统计数据,其实就是 CPU 从系统启动至当前,各项(us, sy, ni, id, wa, hi, si, st)占用的时间,单位是 jiffies。通过 sysconf(_SC_CLK_TCK) 可以获得 1 秒被分成多少个 jiffies 。一般是 100,即 1 jiffies == 0.01 s。(st、guest、guest_nice 和虚拟化/虚拟机相关,如果这些值太高,说明虚拟化的实现或者宿主机有问题。不是本文关注的重点。)

计算 CPU 使用率的基本原理就是从 /proc/stat 进行采样和计算。最简单的方法,一秒采样一次 /proc/stat,如:

第 N 秒采样得到 cpu_total1 = us1 + ni1 + sy1 + id1 + wa1 + hi1 + si1 + st1 + guest1 + guest_nice1 第 N+1 秒采样得到 cpu_total2 = us2 + ni2 + sy2 + id2 + wa2 + hi2 + si2 + st2 + guest2 + guest_nice2 us 的占比为 (us2 - us1) / (cpu_total2 - cpu_total1)

nice

nice - run a program with modified scheduling priority

nice 是一个可以修改进程调度优先级的命令,具体可以参考 man-pages。在 Linux 中,一个进程有一个 nice 值,代表的是这个进程的调度优先级。

越 nice (nice 值越大)的进程,调度优先级越低。怎么理解这句话?进程调度本质上是进程间对 CPU 这一有限资源的争抢,越 nice 的进程,越会“谦让”,所以它的获得 CPU 的机会就越低。

上面的 CPU 利用率里面,将用户态进程使用的 CPU 分成 niced 和 un-niced 两部分,没什么本质差别。平时很少遇到要使用 nice 命令的场景(我个人从来没遇到过)。

理解 us

知道了 us 代表的意义后,简单写个代码控制一下 us

#include <pthread.h>
#include <stdio.h>
#include <assert.h>
#include <vector>
#include <string>

void* CpuUsWorker(void* arg)
{
    uint64_t i = 0;
    while (true)
    {
        i++;
    }   
    return nullptr;
}

void CpuUs(int n)
{
    std::vector<pthread_t> pthreads(n);
    for (int i = 0; i < n; i++)
    {
        assert(pthread_create(&pthreads[i], nullptr, CpuUsWorker, nullptr) == 0);
    }

    for (const auto& tid : pthreads)
    {
        assert(pthread_join(tid, nullptr) == 0);
    }
}

int main(int argc, char** argv)
{
    if (argc != 2)
    {
        fprintf(stderr, "Usage: %s threads\n", argv[0]);
        return -1;
    }   
    CpuUs(std::stoi(argv[1]));
    return 0;
}

测试的机器是 4 个核。代码比较简单,一个线程可以跑满一个核。下面是我的测试结果:

./cpu_us 1
%Cpu(s): 25.0 us,  0.0 sy,  0.0 ni, 75.0 id,  0.0 wa,  0.0 hi,  0.0 si,  0.0 st
./cpu_us 2
%Cpu(s): 50.0 us,  0.1 sy,  0.0 ni, 49.9 id,  0.0 wa,  0.0 hi,  0.0 si,  0.0 st
./cpu_us 3
%Cpu(s): 75.1 us,  0.0 sy,  0.0 ni, 24.9 id,  0.0 wa,  0.0 hi,  0.0 si,  0.0 st

理解 ni

  1. ni 代表的是 niced 用户态进程消耗的 CPU。
  2. nice 可以调整运行程序的 nice 值。

下面是我的测试结果,可以看出 ni 变成 25%,符合预期。

nice ./cpu_us 1
%Cpu(s):  0.1 us,  0.0 sy, 25.0 ni, 74.9 id,  0.0 wa,  0.0 hi,  0.0 si,  0.0 st

下面是 top 命令显示的 nice ./cpu_us 1 的进程信息,NI 这一列就是 nice 值,其值为 10。

PID    USER    PR  NI    VIRT    RES    SHR S  %CPU %MEM     TIME+ COMMAND          
6905 linjinhe  30  10   23024    844    700 S 100.0  0.0   0:03.06 cpu_us 

下面是 ./cpu_us 1 的进程信息,其值为 0。

PID   USER      PR  NI    VIRT    RES    SHR S  %CPU %MEM     TIME+ COMMAND  
6901 linjinhe   20   0   23024    844    700 S 100.0  0.0   0:12.36 cpu_us  

理解 sy

一般情况下,如果 sy 过高,说明程序调用 Linux 系统调用的开销很大。这个也可以简单写个程序验证一下。

#include <pthread.h>
#include <stdio.h>
#include <assert.h>
#include <vector>
#include <string>

void* NoopWorker(void* arg)
{
    return nullptr;
}

void* CpuSyWorker(void* arg)
{
    while (true)
    {
        pthread_t tid;
        assert(pthread_create(&tid, nullptr, NoopWorker, nullptr) == 0);
        assert(pthread_detach(tid) == 0);
    }
}

void CpuSy(int n)
{
    std::vector<pthread_t> pthreads(n); 
    for (int i = 0; i < n; i++)
    {
        assert(pthread_create(&pthreads[i], nullptr, CpuSyWorker, nullptr) == 0);
    }
    for (const auto& tid : pthreads)
    {
        assert(pthread_join(tid, nullptr) == 0);
    }
}

int main(int argc, char** argv)
{
    if (argc != 2)
    {
        fprintf(stderr, "Usage: %s threads\n", argv[0]);
        return -1;
    }

    CpuSy(std::stoi(argv[1]));
}

测试结果:

./cpu_sy 1
%Cpu(s):  8.8 us, 59.3 sy,  0.0 ni, 31.3 id,  0.0 wa,  0.0 hi,  0.6 si,  0.0 st

大量的系统调用让 sy 飙升。不同的系统调用开销不一样,pthread_create 的开销比较大。

理解 wa

wa 这一项,连相关的 Linux man-pages 都说它不太靠谱。所以千万不要看到 wa 很高就觉得系统的 I/O 有问题。

  1. The CPU will not wait for I/O to complete; iowait is the time that a task is waiting for I/O to complete. When a CPU goes into idle state for outstanding task I/O, another task will be scheduled on this CPU.
  2. On a multi-core CPU, the task waiting for I/O to complete is not running on any CPU, so the iowait of each CPU is difficult to calculate.
  3. The value in this field may decrease in certain conditions.

说一下我的理解:

  1. 假设有个单核的系统。CPU 并不会真的“死等” I/O。此时的 CPU 实际是 idle 的,如果有其它进程可以运行,则运行其它进程,此时 CPU 时间就不算入 iowait。如果此时系统没有其它进程需要运行,则 CPU 需要“等”这次 I/O 完成才可以继续运行,此时“等待”的时间算入 iowait。
  2. 对于多核系统,如果有 iowait,要算给哪个 CPU?这是个问题。
  3. wa 高,不能说明系统的 I/O 有问题。如果整个系统只有简单任务不停地进行 I/O,此时的 wa 可能很高,而系统磁盘的 I/O 也远远没达到上限。
  4. wa 低,也不能说明系统的 I/O 没问题。假设机器进行大量的 I/O 任务把磁盘带宽打得慢慢的,同时还有计算任务把 CPU 也跑得满满的。此时 wa 很低,但系统 I/O 压力很大。
#include <pthread.h>
#include <stdio.h>
#include <assert.h>
#include <vector>
#include <string>
#include <fcntl.h>
#include <unistd.h>

void* CpuWaWorker(void* arg)
{
    std::string filename = "test_" + std::to_string(pthread_self());
    int fd = open(filename.c_str(), O_CREAT | O_WRONLY);
    assert(fd > 0);
    while (true)
    {
        assert(write(fd, filename.c_str(), filename.size()) > 0);
        assert(write(fd, "\n", 1) > 0);
        assert(fsync(fd) == 0);
    }
    return nullptr;
}

void CpuWa(int n)
{
    std::vector<pthread_t> pthreads(n);
    for (int i = 0; i < n; i++)
    {
        assert(pthread_create(&pthreads[i], nullptr, CpuWaWorker, nullptr) == 0);
    }

    for (const auto& tid : pthreads)
    {
        assert(pthread_join(tid, nullptr));
    }
}

int main(int argc, char** argv)
{
    if (argc != 2)
    {
        fprintf(stderr, "Usage: %s threads\n", argv[0]);
        return -1;
    }   
    CpuWa(std::stoi(argv[1]));
    return 0;
}
./cpu_wa 10
%Cpu(s):  0.3 us,  6.3 sy,  0.0 ni, 50.0 id, 41.1 wa,  0.0 hi,  2.3 si,  0.0 s

在上面这个例子中,我用多个线程不停地进行小 I/O 把 wa 的值刷上去了,但是其实占用的 I/O 带宽很小,我的测试机是 SSD 的,此时的 I/O 压力并不大。

再看一个例子:

./cpu_wa 10
./cpu_us 3
%Cpu(s): 75.3 us,  3.5 sy,  0.0 ni,  8.2 id, 10.3 wa,  0.0 hi,  2.7 si,  0.0 st

可以看到,明明同样执行了 ./cpu_wa 10, wa 竟然因为同时进行 ./cpu_us 3 而降下来!!!参考上面第 4 点。

理解 si 和 hi

系统调用会触发软中断,所以在上面的一些例子执行时,si 也会有所变化,如:

./cpu_wa 10
%Cpu(s):  0.3 us,  6.3 sy,  0.0 ni, 50.0 id, 41.1 wa,  0.0 hi,  2.3 si,  0.0 s

网卡收到数据包后,网卡驱动会通过软中断通知 CPU。这里用 iperf 网络性能测试工具做一下实验。

$ iperf -s -i 1  # 服务端

$ iperf -c 192.168.1.4 -i 1 -t 60 # 客户端,可以开几个 terminal 执行多个客户端,这样 si 的变化才会比较明显
%Cpu(s):  1.7 us, 74.1 sy,  0.0 ni,  8.0 id,  0.0 wa,  0.0 hi, 16.2 si,  0.0 st

硬件中断的话,暂时找不到什么测试方法,实际应用中应该也比较少遇到。

理解 st

st 和虚拟化相关,这里说说我的理解。

利用虚拟化技术,一台 32 CPU 核心的物理机,可以创建出几十上百个单 CPU 核心的虚拟机。这在公有云场景下,简称“超卖”。 大部分情况下,物理服务器的资源有大量是闲置的。此时,“超卖”并不会造成明显影响。 当很多虚拟机的 CPU 压力变大,此时物理机的资源明显不足,就会造成各个虚拟机之间相互竞争、相互等待。 st 就是用来衡量被 Hypervisor “偷去” 给其它虚拟机使用的 CPU。这个值越高,说明这台物理服务器的资源竞争越激烈。

(云厂商会不会把他们的内核给改了,把 st 改成 0 不让你发现这种情况?)

理解 id

CPU 空闲,感觉这个从应用层的角度没什么难理解的。

这里推荐一篇文章 What's a CPU to do when it has nothing to do?,有兴趣的可以看一下。

(本文完)

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

发表于

我来说两句

0 条评论
登录 后参与评论

相关文章

来自专栏FreeBuf

一则邮件攻击样本分析分享

通过邮件投递病毒文件是网络攻击常用的一种方式,因此防御邮件攻击是每个安全团队都需要重点考虑的内容。中兴通讯每天都会收到数万封外部邮件,为了及时检测每封邮件是否含...

972
来自专栏杨海春的专栏

常用机器性能评估工具

软件系统跑在机器上,处理能力受硬件制约,所以,单机处理能力会有上限。评估机器处理能力的上限,检查程序的瓶颈在哪,有助于程序性能分析。主要的几大硬件:CPU、内存...

6110
来自专栏恰同学骚年

《大型网站技术架构》读书笔记之六:永无止境之网站的伸缩性架构

此篇已收录至《大型网站技术架构》读书笔记系列目录贴,点击访问该目录可获取更多内容。

882
来自专栏漫漫全栈路

ASP.NET MVC学习笔记01初始

技术栈跳来跳去,最后还是选择回归最初。从Asp.Net的WebFrom到PHP到Python的Django,最后还时回到了最熟悉的.net平台。三层之前只做过...

3576
来自专栏影子

给Ionic写一个cordova(PhoneGap)插件

46510
来自专栏文渊之博

数据库压缩备份提高备份效率

背景     在数据库的备份过程中有很多参数,前几日发现公司的备份数据库job运行的很慢,就去研究了一下,发现在备份程序中都没有启用压缩,加上压缩以后有发现效率...

2319
来自专栏北京马哥教育

《大型网站技术架构》读书笔记之六:永无止境之网站的伸缩性架构

一、网站架构的伸缩性设计 01、不同功能进行物理分离实现伸缩 (1)纵向分离:将业务处理流程上得不同部分分离部署,实现系统的伸缩性; ? (2)横向分离:将不同...

3389
来自专栏IT技术精选文摘

缓存更新的套路

看到好些人在写更新缓存数据代码时,先删除缓存,然后再更新数据库,而后续的操作会把数据再装载的缓存中。然而,这个是逻辑是错误的。试想,两个并发操作,一个是更新操作...

3767
来自专栏chenssy

【追光者系列】Hikari连接池配多大合适?

首先声明一下观点:How big should HikariCP be? Not how big but rather how small!连接池的大小不是设置...

1641
来自专栏ATYUN订阅号

RNN示例项目从开发到部署(三):在AWS上部署深度学习模型

虽然我很喜欢为自己建立数据科学和编程项目,但我同样乐于与世界上的任何人在线分享它。幸好,我们以使用AWS(Amazon Web Services),这样我们可以...

1353

扫码关注云+社区