使用 strace 跟踪用户进程和 Linux 内核之间的交互。
系统调用 system call 是程序从内核请求服务的一种编程方式,而
strace是一个功能强大的工具,可让你跟踪用户进程与 Linux 内核之间的交互。要了解操作系统的工作原理,首先需要了解系统调用的工作原理。操作系统的主要功能之一是为用户程序提供抽象机制。
操作系统可以大致分为两种模式:
- 内核模式:操作系统内核使用的一种强大的特权模式
- 用户模式:大多数用户应用程序运行的地方 用户大多使用命令行实用程序和图形用户界面(GUI)来执行日常任务。系统调用在后台静默运行,与内核交互以完成工作。
系统调用与函数调用非常相似,这意味着它们都接受并处理参数然后返回值。唯一的区别是系统调用进入内核,而函数调用不进入。从用户空间切换到内核空间是使用特殊的 trap 机制完成的。
通过使用系统库(在 Linux 系统上又称为 glibc),大部分系统调用对用户隐藏了。尽管系统调用本质上是通用的,但是发出系统调用的机制在很大程度上取决于机器(架构)。
本文通过使用一些常规命令并使用
strace分析每个命令进行的系统调用来探索一些实际示例。这些示例使用 Red Hat Enterprise 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 ~]#如果没有安装,运行命令安装:
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目录是因为每个人都可以访问它,但是你可以根据需要选择另一个目录。)在
testdir目录下使用ls命令验证该文件已经创建:[root@sandbox tmp]# ls testdir/ file1 file2 [root@sandbox tmp]#你可能每天都在使用
ls命令,而没有意识到系统调用在其下面发挥的作用。抽象地来说,该命令的工作方式如下:命令行工具 -> 从系统库(glibc)调用函数 -> 调用系统调用
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 })通过查看上面的输出,你或许可以了解正在发生的事情。
opendir库函数打开一个名为testdir的目录,然后调用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:可执行的程序或 shell 命令2:系统调用(由内核提供的函数)3:库调用(在程序的库内的函数)4:特殊文件(通常出现在/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冗余选项,可以在每个系统调用中提供附加信息: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会显示文件的内容。如果没有给出参数,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找到)对运行中的进程运行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 上的系统调用。要了解它的其它命令行标志,请参考手册页和在线文档。

在 Linux 上用 strace 来理解系统调用
分享