系统调用是程序向内核请求服务的一种编程方式,而 strace 是一个强大的工具,允许您跟踪用户进程和 Linux 内核之间的薄层。
要了解操作系统如何工作,您首先需要了解系统调用如何工作。操作系统的主要功能之一是为用户程序提供抽象。
操作系统大致可以分为两种模式
- 内核模式: 操作系统内核使用的特权和强大模式
- 用户模式: 大多数用户应用程序运行的地方
用户主要使用命令行实用程序和图形用户界面 (GUI) 来完成日常任务。系统调用在后台静默工作,与内核交互以完成工作。
系统调用与函数调用非常相似,这意味着它们接受并处理参数和返回值。唯一的区别是系统调用进入内核,而函数调用不进入内核。从用户空间到内核空间的切换是使用特殊的 陷阱 机制完成的。
大多数这些都被系统库(在 Linux 系统上又名 glibc)对用户隐藏了。即使系统调用本质上是通用的,但发出系统调用的机制在很大程度上取决于机器。
本文通过使用一些通用命令并使用 strace 分析每个命令发出的系统调用,探索了一些实际示例。这些示例使用红帽企业 Linux,但这些命令在其他 Linux 发行版上应该工作方式相同
[root@sandbox ~]# cat /etc/redhat-release
Red Hat Enterprise Linux Server release 7.7 (Maipo)
[root@sandbox ~]#
[root@sandbox ~]# uname -r
3.10.0-1062.el7.x86_64
[root@sandbox ~]#
首先,确保您的系统上安装了所需的工具。您可以使用以下 RPM 命令验证是否安装了 strace;如果已安装,您可以使用 -V 选项检查 strace 实用程序版本号
[root@sandbox ~]# rpm -qa | grep -i strace
strace-4.12-9.el7.x86_64
[root@sandbox ~]#
[root@sandbox ~]# strace -V
strace -- version 4.12
[root@sandbox ~]#
如果这不起作用,请通过运行以下命令安装 strace
yum install strace
为了本示例的目的,在 /tmp 中创建一个测试目录,并使用 touch 命令创建两个文件,如下所示
[root@sandbox ~]# cd /tmp/
[root@sandbox tmp]#
[root@sandbox tmp]# mkdir testdir
[root@sandbox tmp]#
[root@sandbox tmp]# touch testdir/file1
[root@sandbox tmp]# touch testdir/file2
[root@sandbox tmp]#
(我使用 /tmp 目录是因为每个人都可以访问它,但如果您愿意,可以选择另一个目录。)
使用 ls 命令在 testdir 目录中验证文件是否已创建
[root@sandbox tmp]# ls testdir/
file1 file2
[root@sandbox tmp]#
您可能每天都在使用 ls 命令,而没有意识到系统调用在其底层工作。这里存在抽象;以下是此命令的工作方式
Command-line utility -> Invokes functions from system libraries (glibc) -> Invokes system calls
ls 命令在内部调用来自 Linux 系统上的系统库(又名 glibc)的函数。这些库调用执行大部分工作的系统调用。
如果您想知道从 glibc 库中调用了哪些函数,请使用 ltrace 命令,后跟常规的 ls testdir/ 命令
ltrace ls testdir/
如果未安装 ltrace,请通过输入以下命令安装它
yum install ltrace
屏幕上会转储大量输出;不用担心——只需按照步骤操作即可。来自 ltrace 命令输出的一些重要的库函数与本示例相关,包括
opendir("testdir/") = { 3 }
readdir({ 3 }) = { 101879119, "." }
readdir({ 3 }) = { 134, ".." }
readdir({ 3 }) = { 101879120, "file1" }
strlen("file1") = 5
memcpy(0x1665be0, "file1\0", 6) = 0x1665be0
readdir({ 3 }) = { 101879122, "file2" }
strlen("file2") = 5
memcpy(0x166dcb0, "file2\0", 6) = 0x166dcb0
readdir({ 3 }) = nil
closedir({ 3 })
通过查看上面的输出,您可能可以理解正在发生的事情。一个名为 testdir 的目录正在被 opendir 库函数打开,然后调用 readdir 函数,该函数正在读取目录的内容。最后,调用 closedir 函数,该函数关闭先前打开的目录。现在忽略其他 strlen 和 memcpy 函数。
您可以看到正在调用哪些库函数,但本文将重点介绍由系统库函数调用的系统调用。
与上面类似,要了解调用了哪些系统调用,只需将 strace 放在 ls testdir 命令之前,如下所示。再次,一堆乱码将被转储到您的屏幕上,您可以在此处继续操作
[root@sandbox tmp]# strace ls testdir/
execve("/usr/bin/ls", ["ls", "testdir/"], [/* 40 vars */]) = 0
brk(NULL) = 0x1f12000
<<< truncated strace output >>>
write(1, "file1 file2\n", 13file1 file2
) = 13
close(1) = 0
munmap(0x7fd002c8d000, 4096) = 0
close(2) = 0
exit_group(0) = ?
+++ exited with 0 +++
[root@sandbox tmp]#
运行 strace 命令后屏幕上的输出只是运行 ls 命令所做的系统调用。每个系统调用都为操作系统服务于特定目的,它们可以大致分为以下几类
- 进程管理系统调用
- 文件管理系统调用
- 目录和文件系统管理系统调用
- 其他系统调用
分析转储到屏幕上的信息的更简单方法是使用 strace 方便的 -o 标志将输出记录到文件中。在 -o 标志后添加合适的文件名,然后再次运行命令
[root@sandbox tmp]# strace -o trace.log ls testdir/
file1 file2
[root@sandbox tmp]#
这次,没有输出转储到屏幕上——ls 命令按预期工作,显示文件名并将所有输出记录到文件 trace.log。该文件仅针对简单的 ls 命令就包含近 100 行内容
[root@sandbox tmp]# ls -l trace.log
-rw-r--r--. 1 root root 7809 Oct 12 13:52 trace.log
[root@sandbox tmp]#
[root@sandbox tmp]# wc -l trace.log
114 trace.log
[root@sandbox tmp]#
查看示例 trace.log 中的第一行
execve("/usr/bin/ls", ["ls", "testdir/"], [/* 40 vars */]) = 0
- 该行的第一个单词 execve 是正在执行的系统调用的名称。
- 括号内的文本是提供给系统调用的参数。
- = 符号后的数字(在本例中为 0)是 execve 系统调用返回的值。
现在输出看起来不那么吓人了吧?您可以应用相同的逻辑来理解其他行。
现在,将您的注意力缩小到您调用的单个命令,即 ls testdir。您知道命令 ls 使用的目录名称,那么为什么不在您的 trace.log 文件中 grep 搜索 testdir 并看看您得到什么?详细查看结果的每一行
[root@sandbox tmp]# grep testdir trace.log
execve("/usr/bin/ls", ["ls", "testdir/"], [/* 40 vars */]) = 0
stat("testdir/", {st_mode=S_IFDIR|0755, st_size=32, ...}) = 0
openat(AT_FDCWD, "testdir/", O_RDONLY|O_NONBLOCK|O_DIRECTORY|O_CLOEXEC) = 3
[root@sandbox tmp]#
回顾上面对 execve 的分析,您能说出此系统调用是做什么的吗?
execve("/usr/bin/ls", ["ls", "testdir/"], [/* 40 vars */]) = 0
您不需要记住所有系统调用或它们的作用,因为您可以在需要时参考文档。手册页来救援!在运行 man 命令之前,确保安装了以下软件包
[root@sandbox tmp]# rpm -qa | grep -i man-pages
man-pages-3.53-5.el7.noarch
[root@sandbox tmp]#
请记住,您需要在 man 命令和系统调用名称之间添加一个 2。如果您使用 man man 阅读 man 的手册页,您可以看到第 2 节是为系统调用保留的。同样,如果您需要有关库函数的信息,则需要在 man 和库函数名称之间添加一个 3。
以下是手册的章节编号以及它们包含的页面类型
1. Executable programs or shell commands
2. System calls (functions provided by the kernel)
3. Library calls (functions within program libraries)
4. Special files (usually found in /dev)
运行以下 man 命令以及系统调用名称以查看该系统调用的文档
man 2 execve
根据 execve 手册页,这将执行在参数中传递的程序(在本例中,它是 ls)。可以为 ls 提供其他参数,例如本示例中的 testdir。因此,此系统调用只是使用 testdir 作为参数运行 ls
'execve - execute program'
'DESCRIPTION
execve() executes the program pointed to by filename'
下一个名为 stat 的系统调用使用 testdir 参数
stat("testdir/", {st_mode=S_IFDIR|0755, st_size=32, ...}) = 0
使用 man 2 stat 访问文档。 stat 是获取文件状态的系统调用——请记住,Linux 中的所有内容都是文件,包括目录。
接下来,openat 系统调用打开 testdir。 注意返回的 3。这是文件描述符,稍后的系统调用将使用它
openat(AT_FDCWD, "testdir/", O_RDONLY|O_NONBLOCK|O_DIRECTORY|O_CLOEXEC) = 3
到目前为止,一切顺利。现在,打开 trace.log 文件并转到 openat 系统调用之后的行。您将看到 getdents 系统调用被调用,它完成了执行 ls testdir 命令所需的大部分工作。现在,从 trace.log 文件中 grep getdents
[root@sandbox tmp]# grep getdents trace.log
getdents(3, /* 4 entries */, 32768) = 112
getdents(3, /* 0 entries */, 32768) = 0
[root@sandbox tmp]#
getdents 手册页将其描述为 获取目录条目,这正是您想要做的。请注意,getdents 的参数是 3,它是来自上面 openat 系统调用的文件描述符。
现在您有了目录列表,您需要一种在终端中显示它的方法。因此,grep 搜索另一个系统调用 write,它用于在日志中写入终端
[root@sandbox tmp]# grep write trace.log
write(1, "file1 file2\n", 13) = 13
[root@sandbox tmp]#
在这些参数中,您可以看到将显示的文件名:file1 和 file2。关于第一个参数 (1),请记住在 Linux 中,当任何进程运行时,默认情况下会为其打开三个文件描述符。以下是默认文件描述符
- 0 - 标准输入
- 1 - 标准输出
- 2 - 标准错误
因此,write 系统调用正在标准显示器(即终端,由 1 标识)上显示 file1 和 file2。
现在您知道哪些系统调用为 ls testdir/ 命令完成了大部分工作。但是 trace.log 文件中的其他 100 多个系统调用呢?操作系统必须做很多内务处理才能运行进程,因此您在日志文件中看到的很多内容都是进程初始化和清理。阅读整个 trace.log 文件,尝试了解发生了什么使 ls 命令工作。
现在您知道如何分析给定命令的系统调用,您可以将此知识用于其他命令,以了解正在执行哪些系统调用。 strace 提供了许多有用的命令行标志,使您可以更轻松地使用它,下面介绍其中一些标志。
默认情况下,strace 不包括所有系统调用信息。但是,它有一个方便的 -v verbose 选项,可以提供有关每个系统调用的其他信息
strace -v ls testdir
在运行 strace 命令时,始终使用 -f 选项是一个好习惯。它允许 strace 跟踪当前正在跟踪的进程创建的任何子进程
strace -f ls testdir
假设您只想知道系统调用的名称、它们运行的次数以及每个系统调用花费的时间百分比。您可以使用 -c 标志来获取这些统计信息
strace -c ls testdir/
假设您想专注于特定的系统调用,例如专注于 open 系统调用而忽略其余的。您可以使用 -e 标志,后跟系统调用名称
[root@sandbox tmp]# strace -e open ls testdir
open("/etc/ld.so.cache", O_RDONLY|O_CLOEXEC) = 3
open("/lib64/libselinux.so.1", O_RDONLY|O_CLOEXEC) = 3
open("/lib64/libcap.so.2", O_RDONLY|O_CLOEXEC) = 3
open("/lib64/libacl.so.1", O_RDONLY|O_CLOEXEC) = 3
open("/lib64/libc.so.6", O_RDONLY|O_CLOEXEC) = 3
open("/lib64/libpcre.so.1", O_RDONLY|O_CLOEXEC) = 3
open("/lib64/libdl.so.2", O_RDONLY|O_CLOEXEC) = 3
open("/lib64/libattr.so.1", O_RDONLY|O_CLOEXEC) = 3
open("/lib64/libpthread.so.0", O_RDONLY|O_CLOEXEC) = 3
open("/usr/lib/locale/locale-archive", O_RDONLY|O_CLOEXEC) = 3
file1 file2
+++ exited with 0 +++
[root@sandbox tmp]#
如果您想专注于多个系统调用怎么办?不用担心,您可以使用相同的 -e 命令行标志,并在两个系统调用之间用逗号分隔。例如,要查看 write 和 getdents 系统调用
[root@sandbox tmp]# strace -e write,getdents ls testdir
getdents(3, /* 4 entries */, 32768) = 112
getdents(3, /* 0 entries */, 32768) = 0
write(1, "file1 file2\n", 13file1 file2
) = 13
+++ exited with 0 +++
[root@sandbox tmp]#
到目前为止的示例已跟踪显式运行的命令。但是,已经运行并且正在执行的命令呢?例如,如果您想跟踪只是长期运行的进程的守护程序怎么办?为此,strace 提供了一个特殊的 -p 标志,您可以向其提供进程 ID。
不要在守护程序上运行 strace,而是以 cat 命令为例,如果您提供文件名作为参数,它通常会显示文件的内容。如果未给出任何参数,则 cat 命令只是在终端等待用户输入文本。输入文本后,它会重复给定的文本,直到用户按下 Ctrl+C 退出。
从一个终端运行 cat 命令;它将显示一个提示符并在那里等待(记住 cat 仍在运行且尚未退出)
[root@sandbox tmp]# cat
从另一个终端,使用 ps 命令查找进程标识符 (PID)
[root@sandbox ~]# ps -ef | grep cat
root 22443 20164 0 14:19 pts/0 00:00:00 cat
root 22482 20300 0 14:20 pts/1 00:00:00 grep --color=auto cat
[root@sandbox ~]#
现在,使用 -p 标志和 PID(您上面使用 ps 找到的 PID)在正在运行的进程上运行 strace。运行 strace 后,输出声明了进程附加到的内容以及 PID 号。现在,strace 正在跟踪 cat 命令发出的系统调用。您看到的第一个系统调用是 read,它正在等待来自 0 或标准输入(即运行 cat 命令的终端)的输入
[root@sandbox ~]# strace -p 22443
strace: Process 22443 attached
read(0,
现在,移回您将 cat 命令留在运行状态的终端,然后输入一些文本。我输入了 x0x0 用于演示目的。请注意 cat 如何简单地重复了我输入的内容;因此,x0x0 出现了两次。我输入了第一个,第二个是 cat 命令重复的输出
[root@sandbox tmp]# cat
x0x0
x0x0
移回 strace 附加到 cat 进程的终端。您现在看到两个额外的系统调用:较早的 read 系统调用,现在在终端中读取 x0x0 ,以及另一个用于 write 的系统调用,它将 x0x0 写回终端,以及再次新的 read 系统调用,它正在等待从终端读取。请注意,标准输入 (0) 和标准输出 (1) 都在同一个终端中
[root@sandbox ~]# strace -p 22443
strace: Process 22443 attached
read(0, "x0x0\n", 65536) = 5
write(1, "x0x0\n", 5) = 5
read(0,
想象一下,当针对守护程序运行 strace 以查看其在后台执行的所有操作时,这有多么有用。按 Ctrl+C 杀死 cat 命令;这也将杀死您的 strace 会话,因为该进程不再运行。
如果您想查看所有系统调用的时间戳,只需将 -t 选项与 strace 一起使用即可
[root@sandbox ~]#strace -t ls testdir/
14:24:47 execve("/usr/bin/ls", ["ls", "testdir/"], [/* 40 vars */]) = 0
14:24:47 brk(NULL) = 0x1f07000
14:24:47 mmap(NULL, 4096, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0x7f2530bc8000
14:24:47 access("/etc/ld.so.preload", R_OK) = -1 ENOENT (No such file or directory)
14:24:47 open("/etc/ld.so.cache", O_RDONLY|O_CLOEXEC) = 3
如果您想知道系统调用之间花费的时间怎么办? strace 有一个方便的 -r 命令,可以显示执行每个系统调用所花费的时间。非常有用,不是吗?
[root@sandbox ~]#strace -r ls testdir/
0.000000 execve("/usr/bin/ls", ["ls", "testdir/"], [/* 40 vars */]) = 0
0.000368 brk(NULL) = 0x1966000
0.000073 mmap(NULL, 4096, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0x7fb6b1155000
0.000047 access("/etc/ld.so.preload", R_OK) = -1 ENOENT (No such file or directory)
0.000119 open("/etc/ld.so.cache", O_RDONLY|O_CLOEXEC) = 3
结论
strace 实用程序对于理解 Linux 上的系统调用非常方便。要了解其其他命令行标志,请参阅手册页和在线文档。
2 条评论