深入进程与资源利用
本章将带你深入探讨进程、内核与系统资源之间的关系。硬件资源共有三种基本类型:CPU、内存和I/O。进程争夺这些资源,而内核的任务是公平地分配它们。内核本身也是一种资源——一种软件资源,进程用它来执行诸如创建新进程和与其他进程通信等任务。
你在本章中看到的许多工具都被视为性能监控工具。当你的系统运行缓慢、你试图找出原因时,它们尤其有用。然而,你不应被性能问题分散注意力。试图优化一个已经正常工作的系统是浪费时间。大多数系统的默认设置都经过精心选择,因此只有在你有非常特殊的需求时才应更改它们。相反,请专注于理解这些工具实际测量的内容,这样你将深入了解内核的工作原理以及它与进程的交互方式。
8.1 跟踪进程
你在 2.16 节中学习了如何使用 ps 列出系统在特定时刻正在运行的进程。ps 命令列出当前进程及其使用统计信息,但它几乎无法告诉你进程随时间的变化情况。因此,它无法立即帮助你确定哪个进程占用了过多的 CPU 时间或内存。
top 程序为 ps 显示的信息提供了一个交互式界面。它显示当前系统状态以及 ps 列表显示的字段,并且每秒更新一次。也许最重要的是,top 将其显示中最活跃的进程(默认情况下是当前占用最多 CPU 时间的进程)列在最顶部。
你可以通过按键向 top 发送命令。其最常用的命令涉及更改排序顺序或过滤进程列表:
| 按键 | 功能 |
|---|---|
Spacebar | 立即更新显示 |
M | 按当前常驻内存使用量排序 |
T | 按总(累积)CPU 时间排序 |
P | 按当前 CPU 使用量排序(默认) |
u | 只显示一个用户的进程 |
f | 选择不同的统计信息来显示 |
? | 显示所有 top 命令的使用摘要 |
NOTE
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:文件的 inode 编号。
- NAME:文件名。
lsof(1) 手册页包含了你可能看到的每个字段的完整列表,但输出应该是自解释的。例如,查看 FD 字段中包含 cwd 的条目。这些行指示进程的当前工作目录。另一个例子是最后一行,它显示了一个用户 vi 进程(PID 1994)正在使用的临时文件。
NOTE
你可以以 root 用户或普通用户身份运行
lsof,但以 root 身份运行会获得更多信息。
8.2.2 使用 lsof
运行 lsof 有两种基本方法:
- 列出所有内容,将输出通过管道传递给类似
less的命令,然后搜索你要找的内容。由于产生的输出量很大,这可能会花费一些时间。 - 使用命令行选项缩小
lsof提供的列表范围。
你可以使用命令行选项提供一个文件名作为参数,让 lsof 只列出与该参数匹配的条目。例如,以下命令显示 /usr 及其所有子目录中打开的文件的条目:
$ lsof +D /usr要列出特定进程 ID 的打开文件,请运行:
$ lsof -p pid如需 lsof 众多选项的简要摘要,请运行 lsof -h。大多数选项与输出格式有关。(有关 lsof 网络功能的讨论,请参见第 10 章。)
NOTE
lsof高度依赖内核信息。如果你同时更新了内核和lsof的发行版,更新后的lsof可能无法工作,直到你使用新内核重新启动。
8.3 跟踪程序执行和系统调用
我们迄今看到的工具检查的是活动的进程。然而,如果你完全不知道为什么一个程序在启动后几乎立即死亡,那么 lsof 对你没有帮助。事实上,你甚至很难将一个失败的命令与 lsof 同时运行。
strace(系统调用跟踪)和 ltrace(库调用跟踪)命令可以帮助你发现程序尝试执行的操作。这些工具会产生极其大量的输出,但一旦你知道要查找什么,你将有更多信息可用于追踪问题。
8.3.1 strace
回想一下,系统调用是用户空间进程要求内核执行的特权操作,例如打开和读取文件中的数据。strace 实用工具会打印进程发出的所有系统调用。要查看其实际效果,请运行以下命令:
$ strace cat /dev/null默认情况下,strace 将其输出发送到标准错误。如果你想将输出保存到文件中,请使用 -o save_file 选项。你也可以通过将 2> save_file 附加到命令行来重定向,但你也会捕获正在检查的命令的任何标准错误输出。
在第 1 章中,你了解到当一个进程想要启动另一个进程时,它会调用 fork() 系统调用来生成自己的副本,然后该副本使用 exec() 系列系统调用中的一个来开始运行一个新程序。strace 命令在 fork() 调用之后立即开始对新的进程(原始进程的副本)进行操作。因此,该命令输出的第一行应该显示 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, "\177ELF\2\1\1\3\0\0\0\0\0\0\0\0\3\0>\0\1\0\0\0\260\34\2\0\0\0\0\0".., 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 可以非常有用。你甚至可以在那些 fork 或自我分离的守护进程上使用它。例如,要跟踪一个虚构的守护进程 crummyd 的系统调用,请输入:
$ strace -o crummyd_strace -ff crummyd在此示例中,strace 的 -o 选项将 crummyd 产生的任何子进程的操作记录到 crummyd_strace.pid 中,其中 pid 是子进程的进程 ID。
8.3.2 ltrace
ltrace 命令跟踪共享库调用。输出与 strace 类似,这就是为什么在此提及它,但它不跟踪任何内核级别的内容。请注意,共享库调用的数量远多于系统调用。你肯定需要过滤输出,而 ltrace 本身有许多内置选项可以帮助你。
NOTE
有关共享库的更多信息,请参见第 15.1.3 节。
ltrace命令不适用于静态链接的二进制文件。
8.4 线程
在 Linux 中,有些进程被分割成称为线程的部分。线程与进程非常相似——它有一个标识符(线程 ID,或 TID),并且内核像调度和运行进程一样调度和运行线程。然而,与通常不与其他进程共享内存和 I/O 连接等系统资源的独立进程不同,单个进程内的所有线程共享它们的系统资源和部分内存。
深入进程与资源利用
8.4 进程与线程
8.4.1 单线程与多线程进程
许多进程只有一个线程。只有一个线程的进程称为单线程进程,而有多个线程的进程称为多线程进程。所有进程最初都是单线程的。这个起始线程通常称为主线程。主线程可以启动新线程,从而使进程变为多线程,这与进程可通过fork()启动新进程的方式类似。
NOTE
当进程是单线程时,很少提及线程。除非多线程进程会对你所看到或体验到的情况产生影响,否则本书不会提及线程。
多线程进程的主要优势在于:当进程有大量工作需要完成时,线程可以在多个处理器上同时运行,从而可能加速计算。虽然你也可以通过多个进程实现同时计算,但线程的启动速度比进程快,而且线程间利用共享内存进行通信通常比进程通过网络连接或管道等通道进行通信更简单或更高效。
某些程序利用线程来克服管理多个 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 - 清单 8-1:使用 ps m 查看线程
此列表同时显示了进程和线程。在 PID 列中带有数字的每一行(如 1、2、3 处)表示一个进程,与正常的 ps 输出类似。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-2:使用 ps m 显示 PID 和 TID
此清单中的示例输出与清单 8-1 中显示的线程相对应。注意,单线程进程的 TID 与 PID 相同;这就是主线程。对于多线程进程 12534,线程 12534 也是主线程。
NOTE
通常,你不会像对待进程那样与单个线程交互。你需要详细了解多线程程序的编写方式才能对单个线程进行操作,即使如此,这样做也可能不是个好主意。
在资源监控方面,线程可能会混淆情况,因为多线程进程中的各个线程可以同时消耗资源。例如,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 的内置 time,因此请尝试对 ls 命令运行 time:
$ time ls在 ls 终止后,time 应打印出类似于以下内容的输出:
real 0m0.442s
user 0m0.052s
sys 0m0.091s
- 用户时间(user):CPU 运行程序自身代码所花费的秒数。有些命令运行得非常快,以至于 CPU 时间接近 0。
- 系统时间(sys 或 system):内核执行进程工作(例如读取文件和目录)所花费的时间。
- 实际时间(real)(也称为经过时间):从开始到结束运行进程所花费的总时间,包括 CPU 执行其他任务的时间。这个数字通常对性能测量来说不是很有用,但从经过时间中减去用户时间和系统时间可以让你大致了解进程等待系统和外部资源的时间。例如,等待网络服务器响应请求所花费的时间会显示在实际时间中,但不会显示在用户时间或系统时间中。
8.5.2 调整进程优先级
你可以更改内核调度进程的方式,以便赋予进程比其它进程更多或更少的 CPU 时间。内核根据调度优先级运行每个进程,优先级是一个介于 -20 到 20 之间的数字,-20 是最高优先级。(是的,这可能会令人困惑。)
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
在此 top 输出中,**PR(优先级)**列列出了内核当前对该进程的调度优先级。数字越大,如果其他进程需要 CPU 时间,内核调度该进程的可能性就越小。然而,调度优先级本身并不能决定内核是否给进程分配 CPU 时间,内核还可能根据进程消耗的 CPU 时间量在程序执行期间更改优先级。与优先级列相邻的是 **NI(nice 值)**列,它向内核调度器提供一个提示。在尝试影响内核决策时,这就是你需要关注的。内核会将 nice 值加到当前优先级上,以确定进程的下一个时间片。当你将 nice 值设置得更高时,你对其他进程就更“友好(nice)”,因为内核会优先调度它们。
默认情况下,nice 值为 0。现在,假设你在后台运行一个大型计算任务,而你不想让它拖慢你的交互会话。为了让该进程让位于其他进程,只在其他任务无事可做时才运行,你可以使用 renice 命令将 nice 值改为 20(其中 pid 是要更改的进程的进程 ID):
$ 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。尤其是某些网站(尤其是它们的广告),会导致Web浏览器变成资源消耗大户。
如果负载平均值上升到1左右,很可能有一个进程几乎一直在使用CPU。要识别该进程,请使用top命令;该进程通常会出现在列表顶部。
大多数现代系统拥有多个处理器核心或CPU,因此多个进程可以轻松同时运行。如果您有两个核心,负载平均值为1意味着在任何时刻可能只有一个核心处于活动状态;负载平均值为2意味着两个核心都刚好有足够的工作要做。
管理高负载
高负载平均值并不一定意味着您的系统出现了问题。一个拥有足够内存和I/O资源的系统可以轻松处理大量正在运行的进程。如果您的负载平均值很高,但系统仍然响应良好,请不要惊慌;只是有许多进程共享CPU而已。这些进程必须相互竞争处理器时间,因此它们完成计算所需的时间会比各自一直使用CPU时更长。另一种高负载平均值可能属于正常情况的情况是Web服务器或计算服务器,这些服务器上的进程启动和终止速度非常快,以至于负载平均值测量机制无法有效工作。
然而,如果负载平均值非常高,并且您感觉系统变慢了,那么您可能遇到了内存性能问题。当系统内存不足时,内核可能开始抖动(thrashing),即频繁地将内存交换到磁盘或从磁盘交换回来。此时,许多进程会变为就绪状态,但它们的物理内存可能不可用,因此它们会保持在就绪状态(从而增加了负载平均值)的时间比正常情况下长得多。接下来,我们将通过更详细地探讨内存来了解为什么会发生这种情况。
8.5.4 监控内存状态
检查系统整体内存状态的最简单方法之一是运行free命令或查看/proc/meminfo,以了解有多少真实内存被用于缓存和缓冲区。正如刚才提到的,性能问题可能由内存短缺引起。如果没有太多缓存/缓冲区内存被使用(并且其余真实内存已被占用),您可能需要更多内存。然而,很容易将机器上的每个性能问题都归咎于内存短缺。
内存如何工作
如第1章所述,CPU有一个内存管理单元(MMU),为内存访问增加了灵活性。内核通过将进程使用的内存分解成称为页(page)的更小块来协助MMU。内核维护一种称为页表(page table)的数据结构,它将进程的虚拟页地址映射到内存中的真实页地址。当一个进程访问内存时,MMU根据内核的页表将进程使用的虚拟地址转换为真实地址。
用户进程实际上不需要其所有内存页立即可用来运行。内核通常按需加载和分配页,这种机制称为按需分页或请求分页。为了理解其工作原理,请考虑一个程序如何作为新进程启动和运行:
- 内核将程序指令代码的开头部分加载到内存页中。
- 内核可能会为新进程分配一些工作内存页。
- 随着进程运行,它可能会遇到一个点,即其代码中的下一条指令不在内核最初加载的任何页中。此时,内核接管,将必要的页加载到内存中,然后让程序继续执行。
- 类似地,如果程序需要的工作内存多于最初分配的,内核会通过找到空闲页(或腾出空间)并将其分配给进程来处理。
您可以通过查看内核配置来获取系统的页大小:
$ getconf PAGE_SIZE
4096这个数字以字节为单位,4k是大多数Linux系统的典型值。
内核不会随意地将真实内存页映射到虚拟地址;也就是说,它不会将所有可用页放入一个大池子然后从中分配。真实内存有许多划分,这些划分取决于硬件限制、内核的连续页优化以及其他因素。然而,在您刚开始学习时,不必担心这些。
缺页异常
如果进程想要使用某个内存页时该页尚未就绪,进程会触发一个缺页异常(page fault)。发生缺页异常时,内核会从进程手中夺取CPU控制权,以便准备该页。缺页异常有两种类型:次要缺页和主要缺页。
次要缺页
当所需页实际上在主内存中,但MMU不知道其位置时,就会发生次要缺页。这种情况可能发生在进程请求更多内存时,或者MMU没有足够空间来存储某个进程的所有页位置时(MMU的内部映射表通常很小)。在这种情况下,内核会告知MMU该页的位置,并允许进程继续执行。次要缺页无需担心,进程运行时会发生很多次。
主要缺页
当所需的内存页根本不在主内存中时,就会发生主要缺页,这意味着内核必须从磁盘或其他慢速存储机制加载该页。大量的主要缺页会拖慢系统,因为内核需要做大量工作来提供这些页,从而剥夺正常进程的运行机会。
某些主要缺页是不可避免的,例如首次运行程序时从磁盘加载代码的情况。最大的问题出现在您开始耗尽内存时,这会迫使内核将工作内存页交换到磁盘上,以便为新页腾出空间,并可能导致抖动(thrashing)。
您可以使用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 23MINFL和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(换入换出的页面)、io(磁盘使用)、system(内核进入内核代码的次数),以及 cpu(系统不同部分使用的时间)。
以上输出对于一台负载不高的系统而言很典型。通常你会关注第二行输出——第一行是整个系统运行时间内的平均值。例如,此处系统已经将 320,416 KB 的内存交换到磁盘(swpd),并且有约 3,027,000 KB(约 3 GB)的真实空闲内存。尽管有部分交换空间被使用,但 si(换入)和 so(换出)列均为 0,表明内核当前没有从磁盘交换任何页面。buff 列表示内核用于磁盘缓冲区的内存量(参见第 4.2.5 节)。
在最右侧的 cpu 标题下,可以看到 CPU 时间在 us、sy、id 和 wa 列中的分布。这些列分别列出 CPU 花在用户任务、系统(内核)任务、空闲时间和等待 I/O 上的时间百分比。在上例中,几乎没有多少用户进程在运行(它们最多只占 CPU 的 1%);内核几乎无事可做,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 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 320284 2791608 200612 1157752 202 0 4960 854 3371 5714 27 3 51 18 ②
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:内存活动
如清单 8-3 中 ① 所示,CPU 开始出现一段持续的使用,尤其是用户进程。由于有足够的空闲内存,随着内核更多地使用磁盘,缓存和缓冲区空间的使用量开始增加。
稍后,我们看到一些有趣的现象:注意 ② 处,内核将一些曾换出的页面拉回了内存(si 列)。这意味着刚刚运行的程序可能访问了另一个进程共享的某些页面,这很常见——许多进程只会在启动时使用某些共享库中的代码。
同时,注意 b 列,有少量进程处于阻塞(无法运行)状态,正在等待内存页面。总体而言,空闲内存正在减少,但远未耗尽。磁盘活动也相当多,体现在 bi(块入)和 bo(块出)列的数字不断上升。
当内存耗尽时,输出会大不相同。随着空闲空间逐渐耗尽,缓冲区大小和缓存大小都会下降,因为内核越来越需要将空间让给用户进程。一旦内存完全耗尽,你会在 so(换出)列看到活动,内核开始将页面移动到磁盘,此时几乎所有其他输出列都会发生变化,反映出内核正在做多少工作。你会看到更多系统时间、更多进出磁盘的数据,以及更多进程被阻塞,因为它们想要使用的内存不可用(已被换出)。
我们没有探索所有 vmstat 的输出列。你可以在 vmstat(8) 手册页中深入了解它们,但可能需要先通过课程或书籍(如 Silberschatz、Gagne 和 Galvin 所著的《Operating System Concepts》第 10 版,Wiley,2018)学习更多内核内存管理知识,才能理解它们。
8.5.6 I/O 监控
默认情况下,vmstat 提供一些通用的 I/O 统计信息。虽然你可以使用 vmstat -d 获得非常详细的分区资源使用情况,但该选项产生的大量输出可能会让你不知所措。相反,可以尝试一个专门用于 I/O 的工具:iostat。
NOTE
本节讨论的许多 I/O 工具默认并未内置在大多数发行版中,但它们很容易安装。
使用 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 读取(例如读取分区表时)。
进程级 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,无论其他 I/O 如何。idle空闲(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 控制器允许你限制处理器时间,一个 memory 控制器,等等。
注意
尽管 systemd 广泛使用 cgroup 特性,并且系统上大多数(如果不是全部)cgroup 可能由 systemd 管理,但 cgroup 位于内核空间,并不依赖于 systemd。
8.6.1 区分 cgroup 版本
cgroup 有两个版本:v1 和 v2。不幸的是,目前两者都在使用中,并且可以在系统上同时配置,这可能导致混淆。除了功能集略有不同外,两个版本之间的结构差异可以总结如下:
- 在 cgroups v1 中,每种类型的控制器(cpu、memory 等)都有自己的 cgroup 集合。一个进程可以属于每个控制器的一个 cgroup,这意味着一个进程可以属于多个 cgroup。例如,在 v1 中,一个进程可以属于一个 cpu cgroup 和一个 memory cgroup。
- 在 cgroups v2 中,一个进程只能属于一个 cgroup。你可以为每个 cgroup 设置不同类型的控制器。
为了更直观地理解差异,考虑三组进程:A、B 和 C。我们希望对它们分别使用 cpu 和 memory 控制器。图 8-1 展示了 cgroups v1 的示意图。我们需要总共六个 cgroup,因为每个 cgroup 仅限于一个控制器。
CPU 控制器 Memory 控制器
cgroup A1 cgroup A2
cgroup B1 cgroup B2
cgroup C1 cgroup C2
图 8-1:cgroups v1。一个进程可以属于每个控制器的一个 cgroup。
图 8-2 展示了如何在 cgroups v2 中实现。我们只需要三个 cgroup,因为我们可以为每个 cgroup 设置多个控制器。
cgroup A
CPU 控制器
Memory 控制器
cgroup B
CPU 控制器
Memory 控制器
cgroup C
CPU 控制器
Memory 控制器
图 8-2:cgroups v2。一个进程只能属于一个 cgroup。
你可以通过查看 /proc/<pid> 中的 cgroup 文件来列出任何进程的 v1 和 v2 cgroup。从查看你的 shell 的 cgroup 开始:
$ 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 也是 v1 版本,但它没有控制器。这个 cgroup 仅用于管理目的(在此例中,由 systemd 配置)。
- 最后一行,数字 0,是 cgroups v2。这里没有显示控制器。在没有任何 cgroups v1 的系统上,这将是唯一的一行输出。
- 名称是分层的,看起来像文件路径的一部分。在此示例中,你可以看到一些 cgroup 被命名为
/user.slice,另一些被命名为/user.slice/user-1000.slice/session-2.scope。 - 名称
/testcgroup 1是为了展示在 cgroups v1 中,一个进程的 cgroup 可以完全独立。 user.slice下包含session的名称是登录会话,由 systemd 分配。当查看 shell 的 cgroup 时,你会看到它们。系统服务的 cgroup 将在system.slice下。
你可能已经推断出,cgroups v1 在一个方面比 v2 更灵活,因为你可以为进程分配不同的 cgroup 组合。然而,事实证明没有人实际以这种方式使用它们,而且这种方法比简单地为每个进程分配一个 cgroup 更复杂且难以实现部署。
由于 cgroups v1 正在被淘汰,从这一点开始,我们的讨论将集中在 cgroups v2 上。请注意,如果某个控制器正在 cgroups v1 中使用,则该控制器不能同时在 v2 中使用,因为存在潜在冲突。这意味着,如果你系统仍然使用 v1,我们将要讨论的控制器特定部分将无法正常工作,但如果你在正确的位置查找,仍然应该能够跟随 v1 的等价内容。
8.6.2 查看 cgroups
与传统的用于与内核交互的 Unix 系统调用接口不同,cgroup 完全通过文件系统访问,该文件系统通常挂载为 /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
maxmax 值表示此 cgroup 没有特定限制,但由于 cgroup 是分层的,沿子目录链向下的某个 cgroup 可能会限制它。
8.6.3 操作和创建 cgroups
尽管你可能永远不需要修改 cgroup,但操作起来很简单。要将进程放入 cgroup,请以 root 身份将其 PID 写入其 cgroup.procs 文件:
# echo pid > cgroup.procs许多对 cgroup 的修改都是这样工作的。例如,如果想限制 cgroup 的最大 PID 数量(比如 3000 个 PID),请按如下方式操作:
# echo 3000 > pids.max创建 cgroup 稍微复杂一些。从技术上讲,它就像在 cgroup 树中的某个位置创建一个子目录一样简单;当你这样做时,内核会自动创建接口文件。如果 cgroup 没有进程,即使存在接口文件,你也可以使用 rmdir 删除该 cgroup。可能让你出错的是管理 cgroup 的规则,包括:
- 只能将进程放入外层(“叶子”)cgroup。例如,如果你有名为
/my-cgroup和/my-cgroup/my-subgroup的 cgroup,则不能将进程放入/my-cgroup,但/my-cgroup/my-subgroup是可以的。(如果 cgroup 没有控制器,则例外,但我们不深入讨论。) - cgroup 不能拥有其父 cgroup 中没有的控制器。
- 必须为子 cgroup 明确指定控制器。通过
cgroup.subtree_control文件进行;例如,如果你希望子 cgroup 拥有cpu和pids控制器,请将+cpu +pids写入此文件。
这些规则的例外是位于层次结构底部的根 cgroup。你可以将进程放入此 cgroup。你这样做的一个原因可能是将进程从 systemd 的控制中分离出来。
8.6.4 查看资源利用率
除了能够通过 cgroup 限制资源外,你还可以查看所有进程在其 cgroup 中的当前资源利用率。即使没有启用任何控制器,你也可以通过查看其 cpu.stat 文件来查看 cgroup 的 CPU 使用情况:
$ cat cpu.stat
usage_usec 4617481
user_usec 2170266
system_usec 2447215由于这是 cgroup 整个生命周期内的累积 CPU 使用量,因此即使服务生成了许多最终终止的子进程,你也可以看到它如何消耗处理器时间。
如果启用了相应的控制器,你可以查看其他类型的利用率。例如,memory 控制器提供了 memory.current 文件用于查看当前内存使用量,以及 memory.stat 文件,其中包含 cgroup 生命周期内的详细内存数据。这些文件在根 cgroup 中不可用。
你可以从 cgroup 中获得更多信息。如何使用每个单独控制器的完整细节,以及创建 cgroup 的所有规则,都可以在内核文档中找到;只需在线搜索“cgroups2 documentation”即可找到。
现在,你应该对 cgroup 的工作原理有了很好的了解。理解其操作的基本原理有助于解释 systemd 如何组织进程。稍后,当你阅读关于容器的内容时,你会看到它们如何用于更不同的目的。
8.7 进阶主题
衡量和管理资源利用率的工具之所以如此之多,其中一个原因是不同类型的资源以多种不同的方式被消耗。在本章中,你已看到 CPU、内存和 I/O 作为系统资源,被进程、进程内的线程以及内核所消耗。
这些工具存在的另一个原因是资源是有限的,为了让系统表现良好,其组件必须努力消耗更少的资源。过去,许多用户共享一台机器,因此有必要确保每个用户都能公平地分享资源。如今,虽然现代桌面计算机可能没有多个用户,但它仍然有许多进程竞争资源。同样,高性能网络服务器需要密集的系统资源监控,因为它们运行许多进程以同时处理多个请求。
以下是资源监控与性能分析方面的进阶主题,你可能希望进一步探索:
-
sar(系统活动报告器):
sar软件包拥有许多与vmstat相似的持续监控能力,但它还能随时间记录资源利用率。借助sar,你可以回顾某个特定时间点,查看系统当时正在做什么。这在分析过去的系统事件时非常方便。 -
acct(进程记账):
acct软件包能够记录进程及其资源利用率。 -
配额(Quotas):你可以通过配额系统限制用户可使用的磁盘空间。
性能调优深入阅读
如果你对系统调优和性能特别感兴趣,Systems Performance: Enterprise and the Cloud(第2版,Brendan Gregg 著,Addison-Wesley,2020)提供了更详细的论述。
我们还没有涉及可用于监控网络资源利用率的众多工具。不过要使用这些工具,你首先需要了解网络是如何工作的。这正是我们接下来要讨论的内容。
[Image 2327 on Page 225] [Image 2321 on Page 225] [Image 2325 on Page 225] [Image 2317 on Page 225] [Image 2323 on Page 225] [Image 2316 on Page 225] [Image 2320 on Page 225] [Image 2315 on Page 225] [Image 2319 on Page 225]