1. 先把“系统调用”讲透:为什么跟踪这一层能解决你八成的问题
看到标题里挂着“(-Aaa-) 系统调用跟踪命令strace和dtruss”这种社区味儿十足的写法,我大概猜得到楼主就是在聊两个非常老的排障工具:Linux 上的 strace,以及 macOS/BSD 上的 dtruss。这两条命令的名字不一样,背后的实现原理也不一样,但解决的问题是同一个——把程序“偷偷摸摸”发给操作系统内核的每一笔请求都摊开给你看。这层请求,就叫系统调用(system call)。
很多刚接触 Linux 的读者会有一种错觉:程序就是我写的那些 if、for、函数调用,出问题无非看日志、打断点。但真实情况是,你的代码再花哨,最终要读文件、发网络包、开线程、拿内存,都必须找操作系统代办。如果你想知道“代办”到底办成没有、花了多久、卡在哪一步,系统调用跟踪就是唯一的直接观测窗口。
1.1 内核态和用户态之间,系统调用究竟扮演什么角色
CPU 在执行代码时,会区分权限等级。普通程序跑在受限环境里,叫用户态;操作系统核心跑在高权限环境里,叫内核态。用户态程序不能直接访问网卡、磁盘文件描述符表和进程调度队列,唯一合法的入口就是系统调用。
我常拿“便民服务窗口”来打比方。你拿着材料去办事,不需要走进后面的档案室翻柜子,只需要把材料从窗口递进去,里面的人帮你查、帮你盖章、帮你把结果递出来。用户态就是办事大厅,内核态就是档案室,而系统调用就是这个窗口。程序的每次open(打开文件)、read(读取数据)、sendto(发送网络包)、clock_gettime(取当前时间),都像递了一次材料。
这个窗口的“营业记录”就是系统调用跟踪工具的输出:哪年哪月哪日、哪个进程、哪个窗口、递了什么材料、里面给了什么回复。strace 和 dtruss 就是这个窗口的监控摄像头加录音笔。
而“窗口服务”是有成本的。发起一次系统调用,CPU 需要做一次模式切换、检查参数、复制数据、更新内核内部状态,再切回用户态。单次系统调用的开销可能在几百纳秒到几微秒之间,看起来微不足道。但如果程序写得不讲究,一个循环里频繁调用read或者gettimeofday,积少成多就会造成可感知的性能损失。跟踪工具能非常直观地把这类浪费“钉子户”揪出来。
1.2 为什么断点调试器做不到系统调用跟踪的事
可能会有人问:我平时用 gdb、lldb 断点,卡住时看调用栈,不是也能定位问题吗?断点调试和系统调用跟踪是两套不同维度的手段。调试器在用户态代码里打断点,能够看到程序内部变量和函数调用关系,但看不到内核态的处理结果。
比如程序去读一个文件,如果你只知道fopen返回了NULL,你还需要猜是文件不存在、权限不够、还是路径有符号链接问题。用系统调用跟踪一看,底层的openat返回了-1 EACCES,真相立刻浮出水面。再比如程序启动慢,调试器只能告诉你“卡在某一行”,系统调用跟踪则会告诉你“这 5 秒全都耗在nanosleep上”,连统计报表都给你列好。所以两条腿都要会走,调试器管应用逻辑,strace 和 dtruss 管系统边界。
常见系统调用分组如下,跟踪时可以根据热点精准筛选:
- 文件操作:
open、openat、read、write、close、lseek、stat、fstat、unlink、rename - 进程控制:
fork、clone、execve、exit、wait4、kill - 内存管理:
mmap、munmap、mprotect、brk - 网络通信:
socket、connect、bind、listen、accept、sendto、recvfrom - 时间等待:
clock_gettime、nanosleep、futex
2. strace:Linux 下最趁手的系统调用跟踪工具
要说 strace 的江湖地位,基本等同于外科医生的手术刀。它基于 Linux 的 ptrace 机制工作:调试者可以附着到一个正在运行的进程上,让它每次进入系统调用或者从系统调用返回时停下来,把相关信息记录下来再放行。这个思路虽然简单,却极其有效,以至于现在很多性能剖析工具的内核也沿用了类似机制。
2.1 从一行命令开始:strace ./a.out 到底输出什么
你不需要任何配置,拿到一个编译好的二进制就能直接跟踪:
strace ./a.out默认情况下,strace 会把系统调用信息输出到标准错误流(stderr)。如果你只想记录调用名、参数和返回结果,这个最简命令已经够用了。但如果程序输出很多,你会很难看清;更常见的做法是把跟踪结果落到文件里:
strace -tt -T -f -o /tmp/trace.log ./a.out这里讲一下我在实际项目里最常用的参数组合,每个参数都有真实价值:
| 参数 | 作用 | 什么时候必须用 |
|---|---|---|
-tt | 输出微秒级时间戳,含时分秒 | 排查启动慢、卡顿问题时必须开 |
-T | 显示每个系统调用的耗时 | 判断哪个调用是瓶颈时必须开 |
-f | 跟踪 fork 出的子进程 | 程序是多进程、脚本或守护进程时必开 |
-o file | 把结果写入文件 | 输出量大、需要翻查时强烈建议 |
-e trace=open,read | 只跟踪指定系统调用 | 缩小范围、避免日志爆炸时用 |
-s 256 | 控制字符串参数的长度 | 想看完整文件路径、命令参数时用 |
-y | 显示文件描述符对应的具体路径 | 分析 fd 泄漏时非常有用 |
-c | 输出系统调用汇总统计 | 快速给出性能热点时用 |
打开/tmp/trace.log,你能看到类似这样的内容:
35123 10:47:29.856481 execve("./a.out", ["./a.out"], 0x7fff...) = 0 <0.000187> 35123 10:47:29.856790 brk(NULL) = 0x55f730c63000 <0.000011> 35123 10:47:29.857931 openat(AT_FDCWD, "/etc/ld.so.cache", O_RDONLY|O_CLOEXEC) = 3 <0.000023> 35123 10:47:29.857998 fstat(3, {st_mode=S_IFREG|0644, st_size=19475, ...}) = 0 <0.000009> 35123 10:47:29.858053 mmap(NULL, 19475, PROT_READ, MAP_PRIVATE, 3, 0) = 0x7f3a89d8c000 <0.000017> 35123 10:47:29.858068 close(3) = 0 <0.000008>从左往右读:进程号、精确到微秒的时间点、系统调用名、括号内参数、等号后面的返回值,以及尖括号里的耗时。返回值是-1时还会附带错误码,比如-1 EACCES (Permission denied),这对定位权限问题几乎是直击靶心。
2.2 权限问题的标准排查流程:跟着 openat 走一遍
权限错误大概是 Linux 下最常见的吐槽点之一。程序明明在自己的目录里,打开配置文件却失败。不跟踪的话,你可能反复检查文件权限、用户组、目录所有权,然后怀疑人生。用 strace 只需要一条命令,就能把事情讲清楚。
假设程序启动时报“Can't open config file”,这时我们跟踪它做了什么:
strace -f -e trace=open,openat ./myapp 2>&1 | grep config.yaml大概率会看到这一行:
openat(AT_FDCWD, "/opt/myapp/config.yaml", O_RDONLY) = -1 EACCES (Permission denied)看到 EACCES,问题就明确了:文件系统确实返回了“拒绝访问”,而不是“文件不存在”。接下来用ls -l看文件 owner,用namei -l /opt/myapp/config.yaml逐层看目录权限,通常三五分钟就能定位到是当前进程用户对父目录没有 x 权限,还是文件属主不对。这一步比盲猜高效得多。
如果你的程序开了大量文件描述符,想搞清楚每个 fd 到底对应哪个文件,-y参数会直接把 fd 翻译成具体路径。比如某天/dev/stdin不见了、某日志文件被占用,-y会输出信息如5</var/log/nginx/access.log>,一眼定位。
2.3 用 -c 把 strace 变成性能分析工具
strace 不只是排错工具,还能用来做“管窥式”性能分析。当你怀疑某个程序忙得没道理,但又不确定热点在哪时,可以跑一个带-c的统计版本:
strace -c ./myheavy等程序退出后,strace 会输出一张漂亮的分类统计表,按耗时比例排序列出所有系统调用。我这里模拟过一次结果:
% time seconds usecs/call calls errors syscall ------ ----------- ----------- --------- --------- ---------------- 94.07 9.324201 186484 50 clock_nanosleep 2.15 0.213500 35678 6 read 1.23 0.122400 120 1020 openat这里就不存在什么玄学了:将近 94% 的时间都花在clock_nanosleep上,意味着程序大部分时间都在“睡觉”,而不是在干活。真正的健康程序,热点通常应该集中在read、write、mmap这类真实 I/O 上;如果一个程序大头全在futex或nanosleep,那多半是线程同步等待或者代码主动 sleep 太多。
3. dtruss:macOS/BSD 上另一套逻辑的跟踪方案
很多人从 Linux 切到 macOS 的第一反应是:怎么没有 strace?严格说,BSD 系有truss,但 macOS 默认根本没装这个。macOS 上的正牌系统调用跟踪工具是dtruss,它甚至不是独立实现,而是构建在 DTrace 之上的一层封装。注意,这个“不是独立实现”非常关键——它决定了 dtruss 的用法、权限和边界都跟 strace 不太一样。
3.1 DTrace 的探针机制和 dtruss 的基本用法
DTrace 是 Sun Microsystems 当年搞出来的一套动态跟踪框架,后来被带到了 macOS 和 FreeBSD。它的设计思路是在系统关键路径上预埋很多“探针”,用户通过脚本在探针上做采集,不用改动目标程序。dtruss就是官方示例脚本堆出来的一个专用工具,专门把syscall和syscall返回这两类探针的数据整理成人可读的表格。
最简单的用法:
sudo dtruss -f ./my_own_binary注意两点:第一,通常需要sudo,因为你观察的是内核事件;第二,苹果对调试工具附着第三方进程有安全限制,因此跟踪系统自带或强签名应用时可能报错,跟踪你自己编译的小程序则一般没有大问题。如果要在系统环境中调试特定进程,需要在“开发者模式”或调试权限允许范围内操作,具体以当前系统版本的安全设置界面为准。这里不展开说怎么“放开限制”,因为正常开发场景下我们跟踪的都是自己能构建的程序,足够用。
3.2 dtruss 的输出到底长什么样
dtruss的输出格式与 strace 有明显区别,它更接近 DTrace 脚本的排版风格。下面是一段典型输出(细节因系统版本略有差异):
dtrace: system probe description ... PID/THRD SYSCALL(trace) ARGS 1234/1 open "/private/var/log/myapp/app.log", 0x0, 0x0 1234/1 open -> -1 / EACCES你依然能看到系统调用名、参数和返回值,但括号、时间戳和耗时的表达方式没有 strace 那么统一。因此,我看 dtruss 输出时会更依赖它给出的->返回值和整条调用链的先后顺序,而不是盯着排版格式。配合-t open,read,write之类的选项筛选调用名称,也可以让日志干净不少。
3.3 macOS 上 dtruss 不够用时,还有哪些替代方案
dtruss有时会给人一种“不如 strace 生猛”的感觉,这不是错觉,因为 macOS 的系统机制和调试权限体系比 Linux 更复杂。我的实践经验是,在 macOS 上排查问题,可以准备一套组合拳:
dtruss:定位系统调用级别的问题,优先对付自己写的小工具;fs_usage:专门跟踪文件系统读写活动,输出非常直观,适合查“哪个文件被疯狂读”;log stream:把统一日志系统(unified logging)的实时流水拉出来,适合查系统组件和应用框架的行为;Instruments:图形化采样工具,性能方向的问题用起来更顺手;sample:对进程做堆栈采样,几秒钟出一份“统计报告”,适合判断 CPU 在用户态还是内核态。
有读者可能会问:是不是直接用这些替代品就行,不用学 dtruss?我的回答是:dtruss 依然是唯一能让你看到“系统调用计划表”的通用命令行工具,别的方案各有侧重。花十分钟学会它,关键时刻很有价值。
4. 一份实测记录:系统调用跟踪定位“卡住”那一刻
工具讲再多,不如看一次完整排查。下面这个例子是我在客户环境里真实处理过的,稍作脱敏处理,整个过程很能说明strace和dtruss是怎么把“等待”从黑盒里挖出来的。
4.1 症状:服务启动稳定耗掉 7 秒
现场环境是一个容器里的后台服务,每次启动都要卡大约 7 秒才打印“ready”。运维同事怀疑磁盘慢,因为启动阶段会读取多个配置文件。第一次直觉没错的话,应该看到一堆openat、read花费不少时间。但我在容器里执行:
strace -tt -T -f -o /tmp/start.log ./service_start_cmd把 7 秒的启动过程完整记录下来之后,先看执行第 5 秒附近的时间线,发现连续出现了一长串clock_nanosleep,每个都精确地睡上 140 毫秒左右,反复 50 次。再往前翻,第一次connect调用返回了-1 ETIMEDOUT,之后程序进入重试等待。
也就是说,启动慢跟磁盘一点关系都没有,而是服务在尝试连接一个下游端口时超时了,每次失败后都睡 140 毫秒再重试,一轮连接池预热加各种健康检查,7 秒就这么没了。如果没有系统调用跟踪,我可能会去调磁盘参数或者换 SSD,耽误一两天。
4.2 验证:从时间戳排序还原整条因果链
strace的-tt时间戳在这里起到了决定性作用。我把日志里所有clock_nanosleep和connect的行按时间排序,可以清楚看到“超时 -> 等待 -> 重试 -> 超时”是循环往复的。光看业务日志,只能看到“connected failed”刷了若干行,但每行之间真正发生了什么,业务日志是缄默的。
回到 mac 上,这类网络重试问题同样可以用dtruss观察,只不过需要先确认进程是哪个,再用:
sudo dtruss -f -t connect,nanosecond_sleep -p <PID> 2>&1 | tee connect.log严格说 macOS 上的 sleep 探针名字会因内核版本有差异,现场以dtrace -l | grep sleep查到的为准。大方向就是:定时找connect和等待类调用,一旦出现大量“连不上 + 睡眠”交替,八成是外部依赖超时配得过于激进。
4.3 一个更快的判断方法:让统计表帮你指路
面对陌生程序,我一般不会从头到尾读每条系统调用,而是先跑带-c的统计:
strace -c -f ./target_program看统计表里哪几类调用占的时间最多,再回头看这几类调用的完整参数。这种“先看全局报表,再下钻明细”的思路,和日常性能工具体验很像,能大幅缩短排查时间。
5. 多次踩坑后沉淀的注意事项
这节内容是我自己反复踩出来的经验,也是常规手册里不会提醒你的部分。工具本身很简单,真正让新手翻车的大多发生在使用方式上。
5.1 不跟子进程,等于漏掉半个世界
很多程序会通过fork派生子进程,或者用脚本拉起别的二进制。如果你只对父进程执行strace ./parent,很可能看到进程启动后啥都没干就退出,而真正的逻辑全跑在子进程里。遇到这种情况,第一反应必须加-f。
加-f之后,日志会混入多个 PID 的行,加上-ff还会按 PID 拆成多个文件,保留完整现场。我遇到过最隐蔽的一次是:Java 启动脚本里嵌了 Python 子进程,Python 又调用了外部可执行文件,查了半天最后靠-ff找到真正的罪魁祸首。
注意:
-f会显著放大日志量,生产环境跟踪时要格外小心,建议配合-o落盘而不是直接往终端刷。
5.2 别把输出和程序本身的输出混在一起
默认情况下 strace 往 stderr 输出,而程序本身也可能往 stderr 打日志。如果不做区分,屏幕上两股水流搅在一起,新手很容易被误导。我的习惯是全程-o /tmp/trace.log,程序启动动作保持独立输出,最后再翻日志文件。如果确实想看实时 stream,就2>&1 | grep ...,但要把 grep 条件写粗一点,避免把关键系统调用过滤掉。
5.3 附着到运行中的进程时,要小心“观测者效应”
strace -p <PID>可以在不重启进程的情况下附着。但它基于 ptrace,附着动作本身会让目标进程短暂停顿,并且会让程序运行变慢。如果目标进程是千万级 QPS 的线上服务,直接附着产生的停顿可能是灾难性的。我的原则是:优先在压测环境、沙盒环境复现问题;必须上生产时,只用-c做轻量统计,且提前知会相关同事。
还有一个冷知识:如果 strace 异常退出,目标进程可能停在“被暂停”状态。当发现目标进程全卡住,先检查是不是还有残留的strace进程在盯着它,用ps -ef | grep strace确认后,再对目标进程补发一次 SIGCONT 恢复运行。
5.4 dtruss 的角色定位要摆正
在 macOS 上,想靠 dtruss 一把梭是不现实的。它的权限边界、探针命名、输出格式都与 Linux 上的 strace 有差异,说它是“另一个 strace”其实不准确。更合理的定位是:dtruss 是系统调用层面的观察利器,但遇到应用框架内部的调度问题,还是得配合 Instruments 或 log stream。调试时如果 dtruss 权限不足,优先构造一个最小可复现程序来跟踪,通常比折腾系统配置更高效。
6. 一些对这个工具的定位思考:别把它当万能钥匙,也别让它吃灰
我已经不止一次在评论区看到有人问“这年头还需要学 strace 吗”。我可以很负责任地说,需要,而且越早掌握越划算。系统调用跟踪的底层思路——从边界观测、从结果反推原因——是跨平台、跨语言、跨框架通用的。你在 Linux 上用 strace 学会读openat = -1 EACCES,到了 macOS 用 dtruss 看到类似错误时,也会立刻反应过来是权限问题,因为错误码和语义是相通的。
但我也有个反面体会:不要把工具神话。strace 只能看到“发了什么系统调用”,看不到完整业务逻辑和堆栈,更看不到内核内部每次调用的详细执行路径。遇到逻辑性 Bug,它的作用可能不如一个断点调试器;遇到环境性、资源性、时序性问题,它则是调试器望尘莫及的。正确姿势是把它们放进你的工具箱,跟 lsof、perf、dtrace、Instruments 组成一套组合拳,该用哪个用哪个。
就我个人的习惯,任何程序在新环境里第一次跑不通,第一步永远是strace -ff -o /tmp/trace.log完整跑一遍,把退出时最后一个报错调用记录下来。这一步花不了几秒钟,却经常能把我直接带到问题现场。这种“先看边界,再看逻辑”的排查顺序,帮我省下的时间早就超过当初学命令的时间了。