- A+
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, "177ELF2113 3 >