Linux工作原理8深入了解进程和资源利用率

  • Linux工作原理8深入了解进程和资源利用率已关闭评论
  • 38 次浏览
  • A+
所属分类:linux技术
摘要

本章将带你深入了解进程、内核和系统资源之间的关系。有三种基本的硬件资源: CPU、内存和 I/O。进程会争夺这些资源,而内核的工作就是公平地分配资源。内核本身也是一种资源–进程用来执行创建新进程和与其他进程通信等任务的软件资源。


8 深入了解进程和资源利用率

本章将带你深入了解进程、内核和系统资源之间的关系。有三种基本的硬件资源: CPU、内存和 I/O。进程会争夺这些资源,而内核的工作就是公平地分配资源。内核本身也是一种资源--进程用来执行创建新进程和与其他进程通信等任务的软件资源。

本章中的许多工具都被视为性能监控工具。如果系统运行缓慢,而你又想找出原因,那么这些工具就特别有用。不过,你不应该被性能所困扰。试图优化已经正常运行的系统是在浪费时间。大多数系统的默认设置都是经过精心选择的,因此只有当你有非常特殊的需求时才应该更改它们。相反,集中精力了解工具实际测量的内容,你就会对内核如何工作以及如何与进程交互有更深入的了解。

8.1 跟踪进程

在第 2.16 节中,我们学习了如何使用 ps 列出特定时间内系统上运行的进程。ps 命令会列出当前进程及其使用统计信息,但它几乎不会告诉你进程是如何随时间变化的。因此,它无法立即帮助你确定哪个进程占用了过多的 CPU 时间或内存。

顶部程序为 ps 显示的信息提供了一个交互界面。它显示当前系统状态以及 ps 列表显示的字段,并且每秒更新一次。也许最重要的是,top 会将最活跃的进程(默认情况下,当前占用 CPU 时间最多的进程)列在显示屏顶部。

你可以通过按键向 top 发送命令。最常用的命令是更改排序顺序或过滤进程列表:

  • 空格键 立即更新显示
  • M 按当前常驻内存使用量排序
  • T 按 CPU 总使用量(累计)排序
  • P 按当前 CPU 使用率排序(默认值)
  • u 仅显示一个用户的进程
  • f 选择要显示的不同统计数据
  • ? 显示所有顶级命令的使用情况摘要

注意:top 按键命令区分大小写。

两个类似的工具 atop 和 htop 提供了一系列增强的视图和功能。它们的大部分额外功能都增加了其他工具中的功能。例如,htop 与 lsof 命令共享下一节所述的许多功能。

8.2 使用 lsof 查找打开的文件

lsof 命令列出打开的文件和使用这些文件的进程。由于 Unix 非常重视文件,因此 lsof 是查找故障点最有用的工具之一。但 lsof 并不局限于普通文件,它还可以列出网络资源、动态库、管道等。

8.2.1 读取 lsof 输出

在命令行上运行 lsof 通常会产生大量输出。下面是你可能看到的输出片段。该输出(为提高可读性稍作调整)包括来自 systemd(init)进程和正在运行的 vi 进程的打开文件:

# lsof  COMMAND  PID   USER   FD    TYPE    DEVICE  SIZE/OFF    NODE NAME systemd    1   root  cwd     DIR    8,1      4096          2 / systemd    1   root  rtd     DIR    8,1      4096          2 / systemd    1   root  txt     REG    8,1   1595792    9961784 /lib/systemd/systemd systemd    1   root  mem     REG    8,1   1700792    9961570 /lib/x86_64-linux-gnu/libm-2.27.so systemd    1   root  mem     REG    8,1    121016    9961695 /lib/x86_64-linux-gnu/libudev.so.1  --snip-- vi      1994   juser  cwd    DIR    8,1      4096    4587522 /home/juser vi      1994   juser   3u    REG    8,1     12288     786440 /tmp/.ff.swp  --snip-- 

输出结果在顶行列出了以下字段:

  • COMMAND 持有文件描述符的进程的命令名称。
  • PID 进程 ID。
  • USER 运行进程的用户。
  • FD 该字段可以包含两种元素。在前面的大多数输出中,FD 列显示文件的用途。FD 字段还可以列出打开文件的文件描述符--进程与系统库和内核一起使用的用于识别和操作文件的数字;最后一行显示的文件描述符为 3。
  • TYPE 文件类型(常规文件、目录、套接字等)。
  • DEVICE 保存文件的设备的主次号。
  • SIZE/OFF 文件大小。
  • NODE 文件的节点编号。
  • NAME 文件名。

lsof(1)手册中列出了每个字段的详细内容,但输出结果应该不言自明。例如,查看 FD 字段中带有 cwd 的条目。这些行表示进程的当前工作目录。另一个例子是最后一行,它显示了用户的 vi 进程(PID 1994)正在使用的临时文件。

注意:您可以以根用户或普通用户身份运行 lsof,但以根用户身份运行会获得更多信息。

8.2.2 使用 lsof

运行 lsof 有两种基本方法:

列出所有内容并将输出导入类似 less 的命令,然后搜索要找的内容。由于会产生大量输出,这可能需要一段时间。
使用命令行选项缩小 lsof 提供的列表范围。
你可以使用命令行选项提供文件名作为参数,让 lsof 只列出与参数匹配的条目。例如,以下命令会显示 /usr 及其所有子目录中已打开文件的条目:

$ lsof +D /usr 

要列出特定进程 ID 的打开文件,请运行

$ lsof -p pid 

要简要了解 lsof 的众多选项,请运行 lsof -h。大多数选项与输出格式有关。(有关 lsof 网络功能的讨论,请参阅第 10 章)。

注意: lsof 高度依赖内核信息。如果对内核和 lsof 都进行了发行版更新,更新后的 lsof 可能无法工作,直到使用新内核重新启动。

8.3 跟踪程序执行和系统调用

到目前为止,我们所见过的工具都能检查活动进程。但是,如果你不知道为什么程序启动后几乎立即就会死亡,那么 lsof 也帮不了你。事实上,你甚至很难在运行失败命令的同时运行 lsof。

strace(系统调用跟踪)和ltrace(库跟踪)命令可以帮助你发现程序试图做什么。这些工具产生的输出量非常大,但一旦你知道了要查找什么,你就能掌握更多信息来追踪问题。

8.3.1 跟踪

回顾一下,系统调用是用户空间进程要求内核执行的特权操作,例如打开和读取文件中的数据。strace 实用程序会打印进程进行的所有系统调用。要查看它的运行情况,请运行以下命令:

$ strace cat /dev/null 

默认情况下,strace 会将输出发送到标准错误中。如果要将输出保存到文件中,请使用 -o save_file 选项。你也可以在命令行中追加 2> save_file 来重定向,但这样也会捕获正在检查的命令中的任何标准错误。

在第 1 章中,我们了解到当一个进程想要启动另一个进程时,它会调用 fork() 系统调用来生成一个自身的副本,然后该副本会使用 exec() 系列系统调用来开始运行一个新程序。就在 fork() 调用之后,strace 命令开始在新进程(原始进程的副本)上运行。因此,该命令输出的第一行应显示 execve() 正在运行,随后是内存初始化调用 brk(),如下所示:

execve("/bin/cat", ["cat", "/dev/null"], 0x7ffef0be0248 /* 59 vars */) = 0 brk(NULL)                               = 0x561e83127000 

输出的下一部分主要涉及加载共享库。除非你真的想深入研究共享库系统,否则可以忽略这部分内容:

access("/etc/ld.so.nohwcap", F_OK)      = -1 ENOENT (No such file or directory) openat(AT_FDCWD, "/etc/ld.so.cache", O_RDONLY|O_CLOEXEC) = 3 fstat(3, {st_mode=S_IFREG|0644, st_size=119531, ...}) = 0 mmap(NULL, 119531, PROT_READ, MAP_PRIVATE, 3, 0) = 0x7fa9db241000 close(3)                                = 0  --snip-- openat(AT_FDCWD, "/lib/x86_64-linux-gnu/libc.so.6", O_RDONLY|O_CLOEXEC) = 3 read(3, "177ELF21133>1260342"..., 832) = 832 

此外,跳过 mmap 输出,直到输出接近末尾的行,看起来像这样:

fstat(1, {st_mode=S_IFCHR|0620, st_rdev=makedev(0x88, 1), ...}) = 0 openat(AT_FDCWD, "/dev/null", O_RDONLY) = 3 fstat(3, {st_mode=S_IFCHR|0666, st_rdev=makedev(0x1, 3), ...}) = 0 fadvise64(3, 0, 0, POSIX_FADV_SEQUENTIAL) = 0 mmap(NULL, 139264, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0x7fa9db21b000 read(3, "", 131072)                     = 0 munmap(0x7fa9db21b000, 139264)          = 0 close(3)                                = 0 close(1)                                = 0 close(2)                                = 0 exit_group(0)                           = ? +++ exited with 0 +++ 

这部分输出显示了命令的运行情况。首先,看看 openat() 调用(open( ) 的一个小变体),它打开了一个文件。3 是表示成功的结果(3 是内核在打开文件后返回的文件描述符)。在下面,你可以看到 cat 从 /dev/null 读取的内容(read()调用,文件描述符也是 3)。然后就没什么可读取的了,所以程序关闭了文件描述符,并通过 exit_group() 退出。

当命令遇到错误时会发生什么?试试 strace cat not_a_file,检查输出结果中的 open() 调用:

openat(AT_FDCWD, "not_a_file", O_RDONLY) = -1 ENOENT (No such file or directory) 

因为 open() 无法打开文件,所以返回 -1 表示出错。你可以看到,strace 报告了确切的错误,并给出了错误的简短描述。

文件丢失是 Unix 程序中最常见的问题,因此如果系统日志和其他日志信息帮不上忙,而你又无处可去时,strace 就能派上用场。你甚至可以把它用在分叉或分离的守护进程上。例如,要追踪一个名为 crummyd 的虚构守护进程的系统调用,请输入

$ strace -o crummyd_strace -ff crummyd 

在此示例中,strace 的 -o 选项会将 crummyd 产生的任何子进程的操作记录到 crummyd_strace.pid,其中 pid 是子进程的进程 ID。

8.3.2 ltrace

ltrace 命令用于跟踪共享库调用。其输出结果与 strace 类似,因此在此提及,但它不会跟踪内核级别的任何内容。需要注意的是,共享库调用比系统调用多得多。你肯定需要过滤输出,ltrace 本身有许多内置选项可以帮助你。

有关共享库的更多信息,请参见第 15.1.3 节。ltrace 命令不适用于静态链接的二进制文件。

8.4 线程

在 Linux 中,某些进程被划分为称为线程的部分。线程与进程非常相似--它有一个标识符(线程 ID,或 TID),内核会像进程一样调度和运行线程。不过,与通常不与其他进程共享内存和 I/O 连接等系统资源的独立进程不同,单个进程中的所有线程都共享系统资源和部分内存。

8.4.1 单线程进程和多线程进程

许多进程只有一个线程。有一个线程的进程是单线程进程,有多个线程的进程是多线程进程。所有进程一开始都是单线程的。这个起始线程通常称为主线程。主线程可以启动新线程,使进程成为多线程进程,这与进程调用 fork() 启动新进程的方式类似。

注意:当进程是单线程时,很少会提到线程。除非多线程进程会对你所看到或体验到的东西产生影响,否则本书不会提及线程。

多线程进程的主要优势在于,当进程有很多事情要做时,线程可以在多个处理器上同时运行,从而可能加快计算速度。虽然也可以通过多个进程实现同时计算,但线程的启动速度比进程快,而且线程使用共享内存进行通信往往比进程通过网络连接或管道等通道进行通信更容易或更高效。

有些程序使用线程来解决管理多个 I/O 资源的问题。传统上,进程有时会使用 fork() 启动一个新的子进程,以处理新的输入或输出流。线程提供了类似的机制,而无需启动新进程。

8.4.2 查看线程

默认情况下,ps 和 top 命令的输出只显示进程。要在 ps 中显示线程信息,请添加 m 选项。清单 8-1 显示了一些输出示例。

$ ps m   PID TTY      STAT   TIME COMMAND  3587 pts/3    -      0:00 bash1     - -        Ss     0:00 -  3592 pts/4    -      0:00 bash2     - -        Ss     0:00 - 12534 tty7     -    668:30 /usr/lib/xorg/Xorg -core :03     - -        Ssl+ 659:55 -     - -        Ssl+   0:00 -     - -        Ssl+   0:00 -     - -        Ssl+   8:35 -  

该列表显示了进程和线程。与正常的 ps 输出一样,PID 列(1、2 和 3 处)中带有数字的每一行代表一个进程。PID 列中带有破折号的行代表与进程相关的线程。在此输出中,位于 1 和 2 的进程各有一个线程,但位于 3 的进程 12534 是多线程的,有四个线程。

如果想用 ps 查看 TID,可以使用自定义输出格式。清单 8-2 仅显示了 PID、TID 和命令:

$ ps m -o pid,tid,command   PID   TID    COMMAND  3587     -    bash     -  3587    -  3592     -    bash     -  3592    -  12534    -    /usr/lib/xorg/Xorg -core :0     - 12534    -     - 13227    -     - 14443    -     - 14448    - 

该清单中的示例输出与清单 8-1 中显示的线程相对应。请注意,单线程进程的 TID 与 PID 相同;这是主线程。对于多线程进程 12534,线程 12534 也是主线程。

注意:通常情况下,您不会像对待进程那样与单个线程交互。你需要对多线程程序的编写方式了如指掌,才能同时对一个线程采取行动,即便如此,这样做也未必是个好主意。

线程会在资源监控方面造成混乱,因为多线程进程中的单个线程可以同时消耗资源。例如,top 默认不显示线程,需要按 H 键才能打开。对于即将看到的大多数资源监控工具,你都需要做一些额外的工作来打开线程显示。

8.5 资源监控简介

现在我们将讨论资源监控的一些主题,包括处理器(CPU)时间、内存和磁盘 I/O。我们将检查全系统范围以及每个进程的利用率。

为了提高性能,很多人都会接触 Linux 内核的内部运作。然而,大多数 Linux 系统在发行版的默认设置下性能良好,你可能要花费数天时间来调整机器的性能,却得不到有意义的结果,尤其是如果你不知道要注意什么的话。因此,在使用本章的工具进行实验时,与其考虑性能,不如看看内核在进程间分配资源时的运行情况。

8.5.1 测量 CPU 时间

要监视一个或多个特定进程的运行时间,请使用 top 的 -p 选项,语法如下

$ top -p pid1 [-p pid2 ...] 

要想知道一条命令在其生命周期内占用了多少 CPU 时间,可以使用 time。不幸的是,这里有一些混乱,因为大多数 shell 都有一个内置的 time 命令,但并不提供大量的统计数据,而 /usr/bin/time 中有一个系统实用程序。你可能会先遇到内置的 bash shell,所以试试用 ls 命令运行 time:

$ time ls real    0m0.442s user    0m0.052s sys     0m0.091s 

用户时间(user)是 CPU 运行程序自身代码所花费的秒数。系统时间(sys 或 system)是内核执行进程工作(例如读取文件和目录)所花费的时间。最后,实际时间(real)(也称运行时间)是进程从开始到结束运行所花费的总时间,包括 CPU 执行其他任务所花费的时间。通常情况下,这个数字对性能测量并无太大帮助,但从已用时间中减去用户和系统时间,可以大致了解进程等待系统和外部资源的时间。例如,等待网络服务器响应请求的时间会显示在已用时间中,但不会显示在用户或系统时间中。

参考资料

8.5.2 调整进程优先级

您可以改变内核调度进程的方式,使该进程获得比其他进程更多或更少的 CPU 时间。内核根据每个进程的调度优先级运行进程,优先级介于 -20 和 20 之间,其中 -20 为最高优先级。(是的,这可能会引起混淆)。下面的NI表示优先级:

$ ps -o pid,comm,%cpu,nice   PID COMMAND         %CPU  NI 14219 bash             0.0   0 15381 ps               0.0   0 [xurongzhong@localhost ~]$ ps -l F S   UID   PID  PPID  C PRI  NI ADDR SZ WCHAN  TTY          TIME CMD 0 S  1006 14219 14211  0  80   0 - 29246 do_wai pts/3    00:00:00 bash 0 R  1006 15388 14219  0  80   0 - 38332 -      pts/3    00:00:00 ps 

ps -l 命令会列出进程的当前优先级,但使用 top 命令更容易看到优先级的实际情况:

$ top Tasks: 244 total,   2 running, 242 sleeping,   0 stopped,   0 zombie Cpu(s): 31.7%us,  2.8%sy,  0.0%ni, 65.4%id,  0.2%wa,  0.0%hi,  0.0%si,  0.0%st Mem:   6137216k total,  5583560k used,   553656k free,    72008k buffers Swap:  4135932k total,   694192k used,  3441740k free,   767640k cached    PID USER      PR  NI  VIRT  RES  SHR S %CPU %MEM    TIME+  COMMAND            28883 bri       20   0 1280m 763m  32m S   58 12.7 213:00.65 chromium-browse      1175 root      20   0  210m  43m  28m R   44  0.7  14292:35 Xorg                 4022 bri       20   0  413m 201m  28m S   29  3.4   3640:13 chromium-browse      4029 bri       20   0  378m 206m  19m S    2  3.5  32:50.86 chromium-browse      3971 bri       20   0  881m 359m  32m S    2  6.0 563:06.88 chromium-browse      5378 bri       20   0  152m  10m 7064 S    1  0.2  24:30.21 xfce4-session               3821 bri       20   0  312m  37m  14m S    0  0.6  29:25.57 soffice.bin          4117 bri       20   0  321m 105m  18m S    0  1.8  34:55.01 chromium-browse      4138 bri       20   0  331m  99m  21m S    0  1.7 121:44.19 chromium-browse      4274 bri       20   0  232m  60m  13m S    0  1.0  37:33.78 chromium-browse      4267 bri       20   0 1102m 844m  11m S    0 14.1  29:59.27 chromium-browse                  2327 bri       20   0  301m  43m  16m S    0  0.7 109:55.65 xfce4-panel                 

在该顶部输出中,PR(优先级)列列出了内核当前进程的计划优先级。数字越大,表示如果其他进程需要 CPU 时间,内核越不可能调度该进程。不过,内核并不会仅凭计划优先级就决定是否给进程分配 CPU 时间,内核还可能在程序执行过程中根据进程消耗的 CPU 时间改变优先级。

紧挨着优先级列的是 NI(nice value)列,它为内核调度程序提供了提示。当你试图影响内核的决定时,你所关心的就是这一点。内核会将友好值与当前优先级相加,以确定进程的下一个时隙。当你将 nice 值设置得更高时,你就会对其他进程更 “友好”,因为内核会优先处理它们。

默认情况下,“友好 ”值为 0。现在,假设你正在后台运行一项大型计算,不想让交互会话陷入困境。要让该进程排在其他进程之后,只在其他任务无事可做时运行,可以使用 renice 命令(其中 pid 是要更改的进程的 ID)将 nice 值改为 20:

$ renice 20 pid 

如果你是超级用户,可以将 nice 值设置为负数,但这样做几乎总是个坏主意,因为系统进程可能得不到足够的 CPU 时间。事实上,你可能并不需要过多修改 nice 值,因为许多 Linux 系统只有一个用户,而且该用户并不执行太多实际计算。(当一台机器上有许多用户时,nice 值要重要得多)。

8.5.3 使用负载平均值衡量 CPU 性能

CPU 整体性能是比较容易衡量的指标之一。负载平均值是当前准备运行的进程的平均数量。也就是说,它是对任何给定时间内能够使用 CPU 的进程数量的估计,包括正在运行的进程和等待使用 CPU 的进程。在考虑平均负载时,请记住系统中的大多数进程通常都在等待输入(例如来自键盘、鼠标或网络的输入),这意味着它们还没有准备好运行,不应该对平均负载产生任何影响。只有实际运行的进程才会影响平均负载。

  • 使用 uptime

除了内核运行的时间外,uptime 命令还会告诉你三个负载平均值:

$ uptime ... up 91 days, ... load average: 0.08, 0.03, 0.01 

三个粗体数字分别是过去 1 分钟、5 分钟和 15 分钟的负载平均值。正如你所看到的,这个系统并不繁忙:在过去 15 分钟内,所有处理器平均只有 0.01 个进程在运行。换句话说,如果你只有一个处理器,那么在过去的 15 分钟中,它运行用户空间应用程序的时间仅占 1%。

传统上,除了编译程序或玩游戏外,大多数台式机系统的平均负载为 0。平均负载为 0 通常是个好兆头,因为这意味着你的处理器没有受到挑战,你可以节省电能。

不过,与过去相比,当前桌面系统上的用户界面组件往往会占用更多的 CPU。特别是某些网站(尤其是其广告)会导致网络浏览器成为资源消耗大户。

如果平均负载上升到 1 左右,则可能是一个进程几乎一直在占用 CPU。要识别该进程,请使用 top 命令;该进程通常会出现在显示屏的顶部。

大多数现代系统都有一个以上的处理器内核或 CPU,因此可以轻松地同时运行多个进程。如果有两个内核,负载平均值为 1 意味着任何时候都可能只有一个内核处于活动状态,而负载平均值为 2 则意味着两个内核都有足够的时间工作。

  • 管理高负载

平均负载高并不一定意味着系统有问题。拥有足够内存和 I/O 资源的系统可以轻松处理许多正在运行的进程。如果您的平均负载很高,但系统响应仍然很好,请不要惊慌;系统只是有很多进程在共享 CPU。这些进程必须相互竞争处理器时间,因此,它们执行计算所需的时间会比它们各自一直使用 CPU 所需的时间长。另一种情况是,网络或计算服务器的平均负载较高,这可能是正常的,因为在这种情况下,进程启动和终止的速度会非常快,以至于平均负载测量机制无法有效发挥作用。

但是,如果平均负载很高,而且你感觉到系统正在减速,那么你可能遇到了内存性能问题。当系统内存不足时,内核会开始急速运行,或从磁盘快速交换内存。当这种情况发生时,许多进程将准备运行,但它们的内存可能不可用,因此它们在准备运行状态(造成平均负载)下停留的时间会比正常情况下长得多。接下来,我们将通过对内存进行更详细的研究来了解为什么会出现这种情况。

8.5.4 监控内存状态

检查系统整体内存状态的最简单方法之一是运行 free 命令或查看 /proc/meminfo 以了解缓存和缓冲区使用了多少实际内存。如前所述,内存不足会导致性能问题。如果使用的缓存/缓冲区内存不多(而其余的实际内存被占用),则可能需要更多内存。不过,如果把机器出现的所有性能问题都归咎于内存不足,那就太容易了。

8.5.4.1 内存如何工作

正如第 1 章所述,CPU 有一个内存管理单元 (MMU),以增加内存访问的灵活性。内核协助 MMU 将进程使用的内存分解成更小的块,称为页。内核维护着一个名为页表的数据结构,它将进程的虚拟页地址映射到内存中的实际页地址。当进程访问内存时,MMU 会根据内核页表将进程使用的虚拟地址转换为实际地址。

用户进程实际上并不需要所有内存页都立即可用才能运行。内核通常会在进程需要时加载和分配内存页;这种系统被称为按需分页或按需分页。要了解该系统的工作原理,请考虑一个程序是如何作为一个新进程启动和运行的:

  • 内核会将程序的指令代码加载到内存页中。
  • 内核可能会为新进程分配一些工作内存页。
  • 随着进程的运行,它可能会到达一个点,即其代码中的下一条指令不在内核最初加载的任何页中。这时,内核会接管进程,将必要的页面加载到内存中,然后让程序继续执行。
  • 同样,如果程序需要的工作内存超过了最初分配的内存,内核会通过寻找空闲页面(或腾出空间)并将其分配给进程来处理。

你可以通过查看内核配置来获取系统的页面大小:

$ getconf PAGE_SIZE 4096 

这个数字的单位是字节,对于大多数 Linux 系统来说,4k 是一个典型值。

内核不会随意将实际内存的页面映射到虚拟地址;也就是说,内核不会将所有可用页面放到一个大内存池中,然后再从那里分配。实际内存有许多分区,这些分区取决于硬件限制、内核对连续页面的优化以及其他因素。不过,刚开始使用时,你不需要担心这些问题。

8.5.4.2 页面故障(Page Faults)

如果某个内存页在某个进程想要使用它时还未就绪,该进程就会触发页面故障。发生页面故障时,内核会从进程手中夺取 CPU 的控制权,以便使页面就绪。有两种页面故障:次要页面故障和主要页面故障。

  • 次要页面故障(Minor page faults)

当所需页面实际上在主内存中,但 MMU 不知道它在哪里时,就会发生次要页面故障。这种情况可能发生在进程请求更多内存或 MMU 没有足够空间存储进程的所有页面位置时(MMU 的内部映射表通常很小)。在这种情况下,内核会告诉 MMU 有关页面的信息,并允许进程继续运行。轻微的页面故障没什么好担心的,很多都是在进程运行时发生的。

  • 主要页面故障(Major page faults)

当所需内存页根本不在主内存中时,就会发生主要页面故障,这意味着内核必须从磁盘或其他慢速存储机制中加载它。大量的主要页面故障会导致系统瘫痪,因为内核必须做大量的工作来提供页面,从而剥夺了正常进程运行的机会。

有些重大页面故障是不可避免的,比如第一次运行程序时从磁盘加载代码时出现的页面故障。最大的问题发生在内存开始耗尽时,这会迫使内核开始将工作内存页交换到磁盘,以便为新页腾出空间,并可能导致崩溃。

你可以使用 ps、top 和 time 命令深入查看单个进程的页面故障。你需要使用系统版本的 time (/usr/bin/time),而不是 shell 内置的。下面是 time 命令显示页面故障的一个简单示例(cal 命令的输出无关紧要,因此我们将其重定向到 /dev/null,将其丢弃):

$ /usr/bin/time cal > /dev/null 0.00user 0.00system 0:00.06elapsed 0%CPU (0avgtext+0avgdata 3328maxresident)k 648inputs+0outputs (2major+254minor)pagefaults 0swaps 

从粗体文字中可以看出,当该程序运行时,有 2 次主要页面故障和 254 次次要页面故障。主要的页面错误发生在内核第一次需要从磁盘加载程序时。如果再次运行这条命令,可能就不会出现任何主要的页面错误,因为内核已经缓存了磁盘上的页面。

如果你想查看进程运行时的页面错误,可以使用 top 或 ps。运行 top 时,使用 f 更改显示字段,并选择 nMaj 作为显示主要页面故障数的列之一。选择 vMj(自上次更新以来的主要页面故障数)可能会对追踪可能发生错误的进程有所帮助。

使用 ps 时,可以使用自定义输出格式查看特定进程的页面故障。下面是 PID 20365 的示例:

$ ps -o pid,min_flt,maj_flt 20365   PID  MINFL  MAJFL	 20365 834182     23 

MINFL 和 MAJFL 列显示了次要和主要页面故障的数量。当然,你也可以将其与其他进程选择选项结合起来,详见 ps(1) 手册页面。

按进程查看页面故障可以帮助你找到某些有问题的组件。不过,如果你对系统的整体性能感兴趣,就需要一个工具来总结所有进程的 CPU 和内存运行情况。

8.5.5 使用 vmstat 监控 CPU 和内存性能

在众多可用来监控系统性能的工具中,vmstat 命令是历史最悠久、开销最小的命令之一。你会发现它非常方便,能让你高层次地了解内核页面交换的频率、CPU 的繁忙程度以及 I/O 资源的使用情况。

释放 vmstat 强大功能的诀窍在于了解其输出。例如,下面是 vmstat 2 的一些输出,它每两秒报告一次统计数据:

$ vmstat 2 procs -----------memory---------- ---swap-- -----io---- -system-- ----cpu----  r  b   swpd   free   buff  cache   si   so    bi    bo   in   cs us sy id wa  2  0 320416 3027696 198636 1072568    0    0     1     1    2    0 15  2 83  0  2  0 320416 3027288 198636 1072564    0    0     0  1182  407  636  1  0 99  0  1  0 320416 3026792 198640 1072572    0    0     0    58  281  537  1  0 99  0  0  0 320416 3024932 198648 1074924    0    0     0   308  318  541  0  0 99  1  0  0 320416 3024932 198648 1074968    0    0     0     0  208  416  0  0 99  0  0  0 320416 3026800 198648 1072616    0    0     0     0  207  389  0  0 100  0 

输出分为几类:procs 表示进程,memory 表示内存使用情况,swap 表示从 swap 中调入和调出的页面,io 表示磁盘使用情况,system 表示内核切换到内核代码的次数,cpu 表示系统不同部分使用的时间。

前面的输出结果是一个没什么运行的系统的典型输出结果。你通常会从第二行输出开始查看--第一行是系统整个运行时间的平均值。例如,这里系统有 320,416KB 内存被交换到磁盘 (swpd),实际可用内存约为 3,027,000KB (3GB)。尽管使用了一些交换空间,但值为零的 si(swap-in)和 so(swap-out)列显示内核目前没有从磁盘交换任何东西。buff 列表示内核用于磁盘缓冲区的内存量(参见第 4.2.5 节)。

在最右边的 CPU 标题下,可以看到 us、sy、id 和 wa 列中 CPU 时间的分布情况。这四列分别列出了 CPU 用于用户任务、系统(内核)任务、空闲时间和等待 I/O 的时间百分比。在前面的示例中,运行的用户进程不多(最多占用 1% 的 CPU);内核几乎什么也没做,而 CPU 在 99% 的时间里无所事事。

清单 8-3 展示了大程序启动时的情况。

procs -----------memory---------- ---swap-- -----io---- -system-- ----cpu----  r  b   swpd   free   buff  cache   si   so    bi    bo   in   cs us sy id wa  1  0 320412 2861252 198920 1106804    0    0     0     0 2477 4481 25  2 72  0 1  1  0 320412 2861748 198924 1105624    0    0     0    40 2206 3966 26  2 72  0  1  0 320412 2860508 199320 1106504    0    0   210    18 2201 3904 26  2 71  1  1  1 320412 2817860 199332 1146052    0    0 19912     0 2446 4223 26  3 63  8  2  2 34 2791608 200612 1157752  202    0  4960   854 3371 5714 27  3 51 18 2  1  1 320252 2772076 201076 1166656   10    0  2142  1190 4188 7537 30  3 53 14  0  3 320244 2727632 202104 1175420   20    0  1890   216 4631 8706 36  4 46 14 

从清单 8-3 中的 1 处可以看到,CPU 在很长一段时间内开始有一些使用,尤其是来自用户进程的使用。由于有足够的可用内存,随着内核对磁盘的使用增加,缓存和缓冲空间的使用量也开始增加。

随后,我们会看到一些有趣的现象:请注意,在 2 处,内核会将一些曾经被交换出去的页面拉入内存(si 列)。这意味着刚刚运行的程序可能访问了另一个进程共享的一些页面,这很常见--许多进程只有在启动时才会使用某些共享库中的代码。

从 b 列还可以看出,有几个进程在等待内存页时被阻塞(无法运行)。总的来说,可用内存在减少,但还没有耗尽。此外,磁盘活动也相当频繁,这从 bi(输入块)和 bo(输出块)列中不断增加的数字可以看出。

当内存耗尽时,输出结果会截然不同。随着可用空间的耗尽,缓冲区和缓存的大小都会减小,因为内核越来越需要空间来运行用户进程。一旦没有剩余空间,内核开始将页面移动到磁盘上时,你会在 so(已交换)列中看到活动,此时几乎所有其他输出列都会发生变化,以反映内核的工作量。你会看到更多的系统时间、更多的数据进出磁盘、更多的进程被阻塞,因为它们想使用的内存不可用(已被换出)。

我们尚未探讨 vmstat 输出的所有列。你可以在 vmstat(8) 手册页面中深入了解它们,但你可能需要先从课堂或 Silberschatz、Gagne 和 Galvin 的《操作系统概念》第 10 版(Wiley,2018 年)等书中学习更多关于内核内存管理的知识,才能理解它们。

8.5.6 I/O 监控

默认情况下,vmstat 提供一些常规 I/O 统计数据。虽然可以使用 vmstat -d 获取非常详细的每个分区的资源使用情况,但您可能会被该选项产生的大量输出所淹没。请尝试使用名为 iostat 的 I/O 工具。

我们将在此讨论的许多 I/O 工具默认情况下并不内置在大多数发行版中,但它们很容易安装。

8.5.6.1 使用iostat

与vmstat一样,在不带任何选项的情况下运行时,iostat会显示机器当前运行时间的统计数据:

$ iostat [kernel information] avg-cpu:  %user   %nice %system %iowait  %steal   %idle            4.46    0.01    0.67    0.31    0.00   94.55  Device:            tps    kB_read/s    kB_wrtn/s    kB_read    kB_wrtn sda               4.67         7.28        49.86    9493727   65011716 sde               0.00         0.00         0.00       1230          0  

顶部的 avg-cpu 部分报告的 CPU 利用率信息与你在本章中看到的其他实用程序相同,所以请跳到底部,它将显示每个设备的以下信息:

  • tps 每秒数据传输的平均次数
  • kB_read/s 平均每秒读取的千字节数
  • kB_wrtn/s 每秒平均写入千字节数
  • kB_read 读取的千字节总数
  • kB_wrtn 写入的千字节总数

与 vmstat 的另一个相似之处是,您可以提供一个时间间隔参数,如 iostat 2,以便每两秒更新一次。使用间隔参数时,你可能希望使用 -d 选项(如 iostat -d 2)只显示设备报告。

默认情况下,iostat 输出会忽略分区信息。要显示所有分区信息,请使用 -p ALL 选项。因为一个典型的系统有很多分区,所以会有很多输出。下面是你可能看到的部分内容:

$ iostat -p ALL --snip-- Device:            tps    kB_read/s    kB_wrtn/s    kB_read    kB_wrtn --snip-- sda               4.67         7.27        49.83    9496139   65051472 sda1              4.38         7.16        49.51    9352969   64635440 sda2              0.00         0.00         0.00          6          0 sda5              0.01         0.11         0.32     141884     416032 scd0              0.00         0.00         0.00          0          0 --snip-- sde               0.00         0.00         0.00       1230          0 

在这个例子中,sda1、sda2 和 sda5 都是 sda 磁盘的分区,因此读取和写入列会有一些重叠。不过,分区列的总和并不一定等于磁盘列。虽然从 sda1 的读取也算作从 sda 的读取,但请记住,您可以直接从 sda 读取,例如在读取分区表时。

8.5.6.2 每进程 I/O 利用率和监控:iotop

如果需要更深入地了解单个进程使用的 I/O 资源,iotop 工具可以提供帮助。使用 iotop 与使用 top 类似。它会生成一个持续更新的显示屏,显示使用最多 I/O 资源的进程,并在顶部显示一个总摘要:

# iotop Total DISK READ:       4.76 K/s | Total DISK WRITE:     333.31 K/s   TID  PRIO  USER     DISK READ  DISK WRITE  SWAPIN     IO>    COMMAND             260 be/3 root        0.00 B/s   38.09 K/s  0.00 %  6.98 % [jbd2/sda1-8]  2611 be/4 juser       4.76 K/s   10.32 K/s  0.00 %  0.21 % zeitgeist-daemon  2636 be/4 juser       0.00 B/s   84.12 K/s  0.00 %  0.20 % zeitgeist-fts  1329 be/4 juser       0.00 B/s   65.87 K/s  0.00 %  0.03 % soffice.b~ash-pipe=6  6845 be/4 juser       0.00 B/s  812.63 B/s  0.00 %  0.00 % chromium-browser 19069 be/4 juser       0.00 B/s  812.63 B/s  0.00 %  0.00 % rhythmbox 

除了用户、命令和读/写列外,请注意还有 TID 列而不是 PID 列。iotop 工具是为数不多的能显示线程而非进程的工具之一。

PRIO(优先级)列表示 I/O 优先级。它与CPU优先级类似,但会影响内核为进程安排I/O读写的速度。在诸如 be/4 这样的优先级中,be 表示调度级别,数字表示优先级。与 CPU 优先级一样,数字越小越重要;例如,内核会为优先级为 be/3 的进程留出比优先级为 be/4 的进程更多的 I/O 时间。

内核使用调度类为 I/O 调度添加更多控制。你将从 iotop 中看到三种调度类:

  • 尽力(be Best effort)。内核会尽最大努力为该类别的 I/O 进行公平调度。大多数进程都在这个 I/O 调度类别下运行。
  • rt 实时(Real time)。无论如何,内核都会将任何实时 I/O 调度到其他 I/O 类别之前。
  • idle 闲置。只有在没有其他 I/O 需要执行时,内核才会为该类执行 I/O。空闲调度类没有优先级。

您可以使用 ionice 工具检查和更改进程的 I/O 优先级;详情请查看 ionice(1) 手册。不过,您可能永远都不需要担心 I/O 优先级。

8.5.7 使用 pidstat 监控每个进程

你已经看到如何使用 top 和 iotop 等实用程序监控特定进程。不过,这种显示会随着时间的推移而刷新,而且每次更新都会擦除之前的输出。通过 pidstat 实用程序,你可以看到进程在一段时间内的资源消耗情况,就像 vmstat 一样。下面是一个监控进程 1329 的简单示例,每秒更新一次:

pidstat -p 1329 1 Linux 5.4.0-48-generic (duplex)         11/09/2020      _x86_64_        (4 CPU)  09:26:55 PM   UID  PID    %usr %system  %guest    %CPU   CPU  Command 09:27:03 PM  1000  1329    8.00   0.00    0.00    8.00     1  myprocess 09:27:04 PM  1000  1329    0.00   0.00    0.00    0.00     3  myprocess 09:27:05 PM  1000  1329    3.00   0.00    0.00    3.00     1  myprocess 09:27:06 PM  1000  1329    8.00   0.00    0.00    8.00     3  myprocess 09:27:07 PM  1000  1329    2.00   0.00    0.00    2.00     3  myprocess 09:27:08 PM  1000  1329    6.00   0.00    0.00    6.00     2  myprocess 

默认输出会显示用户时间和系统时间的百分比,以及 CPU 时间的总百分比,甚至还会告诉你进程是在哪个 CPU 上运行的。(这里的 %guest 一栏有点奇怪--它是进程在虚拟机中运行的时间百分比。除非你运行的是虚拟机,否则不用担心这个问题)。

虽然 pidstat 默认显示的是 CPU 利用率,但它还能做更多。例如,你可以使用 -r 选项监控内存,使用 -d 选项打开磁盘监控。试一试,然后查看 pidstat(1) 手册页面,了解更多关于线程、上下文切换或本章讨论的其他选项。

8.6 控制组(cgroups)

到目前为止,你已经了解了如何查看和监控资源使用情况,但如果你想限制进程的消耗量,而不是使用 nice 命令查看的消耗量,该怎么办呢?有几种传统的系统可以做到这一点,例如 POSIX rlimit 接口,但现在 Linux 系统上大多数类型的资源限制最灵活的选择是 cgroup(控制组)内核功能。

其基本思想是将多个进程放入一个 cgroup,这样就可以在整个组的基础上管理这些进程所消耗的资源。例如,如果你想限制一组进程累计消耗的内存量,cgroup 就可以做到这一点。

创建 cgroup 后,可以向其中添加进程,然后使用控制器来改变这些进程的行为方式。例如,有一个 cpu 控制器允许您限制处理器时间,还有一个内存控制器,等等。

注意:尽管 systemd 广泛使用了 cgroup 功能,而且系统中的大多数(如果不是全部)cgroup 都由 systemd 管理,但 cgroup 位于内核空间,并不依赖于 systemd。

8.6.1 区分cgroup版本

cgroups 有两个版本:1 和 2,不幸的是,这两个版本目前都在使用,并且可以同时在系统中配置,这可能会导致混淆。除了功能设置略有不同外,两个版本在结构上的差异可归纳如下:

在 cgroups v1 中,每种类型的控制器(CPU、内存等)都有自己的一组 cgroups。每个进程可以属于每个控制器的一个 cgroup,也就是说,一个进程可以属于多个 cgroup。例如,在 cgroups v1 中,一个进程可以属于一个 CPU cgroup 和一个内存 cgroup。
在 cgroups v2 中,一个进程只能属于一个 cgroup。您可以为每个 cgroup 设置不同类型的控制器。
为直观了解两者的区别,请考虑三组进程 A、B 和 C。图 8-1 显示了 cgroups v1 的示意图。我们总共需要 6 个 cgroups,因为每个 cgroups 只能使用一个控制器。

Linux工作原理8深入了解进程和资源利用率

图 8-2 显示了 cgroups v2 的操作方法。我们只需要三个 cgroup,因为每个 cgroup 可以设置多个控制器。

Linux工作原理8深入了解进程和资源利用率

通过查看 /proc/ 中的 cgroup 文件,可以列出任何进程的 v1 和 v2 cgroup。您可以先使用此命令查看 shell 的 cgroups:

$ cat /proc/self/cgroup 12:rdma:/ 11:net_cls,net_prio:/ 10:perf_event:/ 9:cpuset:/ 8:cpu,cpuacct:/user.slice 7:blkio:/user.slice 6:memory:/user.slice 5:pids:/user.slice/user-1000.slice/session-2.scope 4:devices:/user.slice 3:freezer:/ 2:hugetlb:/testcgroup 1 1:name=systemd:/user.slice/user-1000.slice/session-2.scope 0::/user.slice/user-1000.slice/session-2.scope 

如果在您的系统上输出明显较短,请不要惊慌;这只是意味着您可能只有 cgroups v2。 这里的每一行输出都以数字开头,是不同的 cgroup。下面是一些阅读提示:

  • 数字 2-12 代表 cgroups v1,其控制器列在数字旁边。
  • 数字 1 也是版本 1,但它没有控制器。该 cgroup 仅用于管理目的(在本例中,systemd 配置了它)。
  • 最后一行,数字 0 代表 cgroups v2,这里看不到控制器。在没有 cgroups v1 的系统上,这将是唯一的一行输出。
  • 名称是分层的,看起来就像文件路径的一部分。在此示例中可以看到,一些 cgroups 被命名为 /user.slice,另一些则被命名为 /user.slice/user-1000.slice/session-2.scope。
  • 创建 /testcgroup 1 名称是为了说明在 cgroups v1 中,进程的 cgroups 可以完全独立。
  • user.slice 下包含 session 的名称是由 systemd 分配的登录会话。你在查看 shell 的 cgroups 时会看到它们。系统服务的 cgroups 将位于 system.slice 下。

您可能已经猜到 cgroups v1 在某一方面比 v2 更灵活,因为您可以为进程分配不同的 cgroups 组合。但事实证明,没有人真正这样使用它们,而且这种方法的设置和实施比简单地为每个进程分配一个 cgroup 更加复杂。

由于 cgroups v1 即将被淘汰,从现在起我们将重点讨论 cgroups v2。请注意,如果在 cgroups v1 中使用控制器,由于潜在的冲突,该控制器不能同时在 v2 中使用。这意味着,如果您的系统仍在使用 v1,我们将要讨论的与控制器相关的部分将无法正常工作,但如果您找对了地方,还是可以跟上 v1 中的相应部分。

8.6.2 查看 cgroups

与内核交互的传统 Unix 系统调用接口不同,cgroups 完全通过文件系统访问,文件系统通常挂载为 /sys/fs/cgroup 下的 cgroup2 文件系统。(如果同时运行 cgroups v1,则可能位于 /sys/fs/cgroup/unified 下)。

让我们来看看 shell 的 cgroup 设置。打开 shell,从 /proc/self/cgroup(如前所述)中找到它的 cgroup。然后在 /sys/fs/cgroup(或 /sys/fs/cgroup/unified)中查找。你会发现一个以该名称命名的目录;切换到该目录并四处看看:

$ cat /proc/self/cgroup 0::/user.slice/user-1000.slice/session-2.scope $ cd /sys/fs/cgroup/user.slice/user-1000.slice/session-2.scope/ # 注意目录和上面有不同 $ ls 

注意:在桌面环境中,cgroup 名称可能很长,因为每启动一个新应用程序,就会创建一个新的 cgroup。

在这里的许多文件中,主要的 cgroup 界面文件都以 cgroup 开头。首先查看 cgroup.procs(使用 cat 也可以),它列出了 cgroup 中的进程。类似的文件 cgroup.threads 也包括线程。

要查看 cgroup 当前使用的控制器,请查看 cgroup.controllers:

$ cat cgroup.controllers memory pids 

大多数用于 shell 的 cgroup 都有这两个控制器,它们可以控制内存使用量和 cgroup 中的进程总数。要与控制器交互,请查找与控制器前缀匹配的文件。例如,如果想查看 cgroup 中运行的线程数,请查阅 pids.current:

$ cat pids.current 4 

要查看 cgroup 可消耗的最大内存,请查看 memory.max:

$ cat memory.max max 

值为 max 意味着该 cgroup 没有特定限制,但由于 cgroup 是分级的,子目录链上的 cgroup 可能会限制它。

8.6.3 操作和创建 cgroups

尽管您可能永远都不需要更改 cgroup,但要做到这一点还是很容易的。要将进程放入 cgroup,请以根用户身份将其 PID 写入 cgroup.procs 文件:

# echo pid > cgroup.procs 

对 cgroup 的许多更改都是这样进行的。例如,如果要限制一个 cgroup 的最大 PID 数量(比如说,限制为 3000 个 PID),可按如下方法操作:

# echo 3000 > pids.max 

创建 cgroups 比较麻烦。从技术上讲,只需在 cgroup 树中创建一个子目录,内核就会自动创建接口文件。如果 cgroup 没有进程,即使界面文件存在,也可以用 rmdir 删除 cgroup。可能会让你出错的是 c 组的管理规则,包括

只能将进程放在外层(“叶子”)c 组中。例如,如果你有名为 /my-cgroup 和 /my-cgroup/my-subgroup 的 cgroup,你就不能把进程放在 /my-cgroup 中,但 /my-cgroup/my-subgroup 是可以的(如果 cgroup 没有控制器则例外,但我们就不深究了)。
一个 cgroup 不能有不在其父 cgroup 中的控制器。
您必须为子 cgroup 明确指定控制器。例如,如果想让子 cgroup 拥有 cpu 和 pids 控制器,就在该文件中写入 +cpu +pids。
这些规则的一个例外是位于层次结构基本路径上的根 cgroup。您可以将进程放在这个 cgroup 中。这样做的一个原因是为了使进程脱离 systemd 的控制。

8.6.4 查看资源使用情况

除了可以按 c 组限制资源外,还可以查看所有进程在其 c 组中的当前资源利用率。即使没有启用控制器,您也可以通过查看 cgroup 的 cpu.stat 文件来查看其 CPU 使用率:

$ cat cpu.stat usage_usec 4617481 user_usec 2170266 system_usec 2447215 

因为这是 cgroup 整个生命周期内累积的 CPU 使用情况,所以即使服务产生了许多最终终止的子进程,也能看到它是如何消耗处理器时间的。

如果启用了相应的控制器,还可以查看其他类型的使用情况。例如,内存控制器允许访问 memory.current 文件(当前内存使用情况)和 memory.stat 文件(包含 cgroup 生命周期内的详细内存数据)。这些文件在根 c 组中不可用。

你可以从 cgroups 中获得更多。如何使用每个控制器的全部细节,以及创建 cgroups 的所有规则,都可以在内核文档中找到;只需在网上搜索 “cgroups2 文档”,就能找到。

不过现在,你应该对 cgroups 的工作原理有一个很好的了解。了解它们的基本操作有助于解释 systemd 如何组织进程。稍后,当你阅读有关容器的内容时,你会发现容器的用途与此大不相同。

8.7 更多主题

衡量和管理资源利用率的工具如此之多,原因之一是不同类型的资源消耗方式各不相同。在本章中,你将看到 CPU、内存和 I/O 作为系统资源被进程、进程中的线程和内核消耗。

这些工具存在的另一个原因是,资源是有限的,要使系统运行良好,其组件必须努力消耗更少的资源。过去,许多用户共用一台计算机,因此有必要确保每个用户都能公平地分享资源。现在,虽然一台现代台式电脑可能没有多个用户,但仍有许多进程在争夺资源。同样,高性能网络服务器也需要密集的系统资源监控,因为它们运行许多进程来同时处理多个请求。

你可能想了解的资源监控和性能分析方面的其他主题包括

  • sar(系统活动报告程序 sar 软件包具有 vmstat 的许多连续监控功能,但它还能记录一段时间内的资源利用情况。使用 sar,您可以回顾特定时间,查看系统当时在做什么。这对分析过去的系统事件非常有用。
  • acct(进程会计) acct 软件包可以记录进程及其资源使用情况。
  • Quotas 通过配额系统,你可以限制用户可以使用的磁盘空间。

如果你对系统调整和性能特别感兴趣,请参阅《系统性能》(Systems Performance: Brendan Gregg 著的第 2 版《企业与云》(Addison-Wesley,2020 年)中有更多详细介绍。

此外,我们还没有涉及到许多用于监控网络资源利用率的工具。不过,要使用这些工具,首先需要了解网络是如何工作的。这就是我们接下来要做的。