strace和dtruss这两个命令,对不少开发者来说可能听过名字,但真正用顺手的并不多。我刚开始接触系统调用跟踪时,也只觉得它们是"高级版的黑盒调试器",直到有一次线上服务莫名卡死、日志里什么都没留下,靠着strace一步步揪出问题,才真正意识到这类工具的价值。这篇文章不打算写成命令手册的复读机,而是结合我实际排查问题的经历,把strace和dtruss的核心思路、常用场景、参数选择以及那些文档里不会明说的坑,一次讲清楚。
不管你是做后端服务、写系统工具,还是搞移动端调试,只要你的程序跑在Linux或macOS上,这两个命令都值得花半小时掌握。它们能帮你回答一个很朴素的问题:程序到底在执行什么,卡在了哪里,为什么失败。
1. 系统调用跟踪到底是什么,为什么这两个命令值得学
1.1 一次离谱的"卡死"排查,让我认清了系统调用的价值
先讲个我自己的例子。之前维护过一个内部工具,定时任务偶尔会整个挂住,既不退出也不报错,CPU占用还很高。第一反应是上gdb看调用栈,结果栈顶停在某个锁等待上,看不出谁抢了锁。后来想到用strace看它到底在等什么系统调用,结果发现进程反复在执行futex调用,参数里带着FUTEX_WAIT,等一个地址的值变化,而这个地址对应的线程一直没释放锁。顺着这个线索,很快就定位到是某次异常分支没走解锁逻辑。整个过程里,strace没有直接告诉我"锁没释放",但把线程卡在哪个系统调用、等了多久、谁在等,全都摊开在眼前了。
类似的故事在排查"文件删不掉""端口被占用""程序启动失败"等场景里几乎天天发生。这类问题有个共同特征:应用日志里看不到错误,但内核层有明确的返回码。strace干的事情,就是在应用和内核之间架一个"摄像头",记录每一次系统调用的参数和返回值。dtruss则是在macOS上干同样的事,只是底层机制不同。
1.2 用户态与内核态的"边界外交":系统调用的运行机制
要理解strace和dtruss的价值,先得理解程序运行的基本模型。普通应用程序跑在CPU的用户态,能访问的资源和权限受限;而读写文件、创建进程、收发网络数据这类操作,必须通过内核来完成。应用层调用read()、write()、open()这类函数时,实际上会在底层触发对应的系统调用,CPU切换到内核态,由内核代执行,再把结果返回给用户态。
这个过程有点像你打电话给餐厅订餐:你只负责说出需求(系统调用的参数),接电话的人(内核)负责传达给后厨、确认订单并告诉你结果(返回值)。如果订单没成,接电话的人会告诉你一个原因——可能是菜品卖完了(文件不存在),可能是电话占线(资源被占用)。
strace和dtruss就是给这通"电话"加了录音。它们能记录你说了什么(系统调用名与参数)、对方回了什么(返回值与错误码)、这通电话打了多久(耗时)、打电话的频率(统计信息)等。了解了这个机制,你就能明白:这类工具不会告诉你业务逻辑为什么错,但能告诉你内核层面的每一步到底发生了什么,而这往往就是问题真正的根源。
1.3 strace与dtruss的关系:同一思路下的两副面孔
strace诞生于Linux世界,几乎所有主流Linux发行版都自带或可通过包管理器安装。dtruss则是基于macOS的DTrace框架封装的一个工具,默认随系统附带(位于/usr/bin/dtruss),主要服务于macOS和BSD系系统。两者解决的问题一致,但实现路径不同:strace使用ptrace系统调用直接拦截被跟踪进程的系统调用;dtruss基于DTrace动态插桩技术,在内核中动态注入探针,不需要像ptrace那样完全控制被跟踪进程。
从命令形态上看,strace -p 1234可以动态附加到运行中的进程,dtruss -p 1234同样支持。但dtruss在macOS上往往需要root权限,且默认输出格式和strace有明显差异。更关键的是,strace的参数体系更丰富,比如按系统调用类型过滤、统计汇总、跟踪子进程等;dtruss在参数覆盖面上略逊一筹,但胜在与macOS系统本身的兼容性更好。
所以,如果你主要在Linux服务器上排查问题,学strace是第一优先级;如果你搞macOS端的开发调试,dtruss就是日常必备。文章后面我会把两个命令的对比和适用场景拆开细讲。
2. 核心参数与输出解析:先把工具用明白
2.1 strace的常用参数和输出格式,这次讲透
strace的完整参数很多,但日常高频用到的其实就那么十几个。-f表示跟踪由目标进程创建的子进程,排查多进程程序时几乎必开;-e trace=网络表示只看网络相关的系统调用,常见的过滤值有文件(文件操作类)、进程(进程管理类)、网络、信号、内存等;-o指定输出文件;-T显示每个系统调用的耗时;-t在输出行前加时间戳,方便对齐日志。
还有几个容易被忽略但实用的参数:-c会汇总统计每个系统调用的次数和总耗时,很适合做性能粗筛;-p附加到指定进程,比如strace -p 9876;-e trace=openat,read,write这种写法可以精确指定要跟哪几个系统调用,避免输出爆炸。-s参数控制字符串类型参数的最大输出长度,默认32字节,遇到长路径或长URL时经常需要调大。
输出格式长这样:
openat(AT_FDCWD, "/etc/localtime", O_RDONLY|O_CLOEXEC) = 3 read(3, "TZif2\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0"..., 4096) = 4096 close(3) = 0第一列是系统调用名和参数,=右边是返回值。返回值如果为-1,后面跟的errno才是关键,比如ENOENT表示文件不存在,EACCES表示权限不足,ECONNREFUSED表示连接被拒绝。排查时养成习惯:看到失败的系统调用后,立刻去查对应的errno含义,比死盯业务日志高效太多。
2.2 dtruss的用法差异与需要注意的细节
dtruss最常用的参数是-c(统计各系统调用次数)、-e(显示错误返回值)、-f(跟踪子进程)、-p(附加进程)。与strace最大的差异在于默认输出格式:dtruss输出的第一列往往是进程ID和用户态/内核态切换的耗时(如dtrace的TIMESTAMP),同时系统调用名后面跟着一大串十六进制参数,看起来没有strace那么直观。
一个典型输出:
dtrace: description 'syscall::openat:entry' matched 1 probe PID COMMAND FD ARGS 1234 curl 24 openat(0x01000000, 0x7ff7bfef2f00, 0x0, 0x0) = 3 0这里ARGS列打印的是寄存器参数和栈上的参数,不像strace那样直接显示解析后的路径名。想让dtruss显示更易读的路径字符串,可以加-u参数(把用户态地址解析为字符串),但可靠性取决于当时进程的用户态内存是否可读。这一点上,dtruss的调试体验确实不如strace方便。
另一个需要注意的坑是:在macOS上运行dtruss,绝大多数情况下都要root权限。在较老版本的macOS上,DTrace还受安全限制约束,新版系统里虽然默认可用,但依然建议用sudo dtruss执行,避免权限不足导致无法附加到进程。
2.3 输出信息怎么看:从一堆日志里抓重点
不管是strace还是dtruss,输出量都可能非常巨大。一个简单的ls命令就能产生几十条系统调用,更不用说跑个网络服务了。所以学会"抓重点"是基本素养。
我的习惯是分三步走:
- 先看有没有返回值是-1的系统调用,有就先圈出来,这是错误的主线。
- 再看高频调用的分布,用
-c做统计,比如read、write、futex这类调用占了绝大多数时间,往往说明程序在IO或线程同步上花了大头。 - 最后结合业务场景定位。比如启动阶段失败,重点看
openat、execve、mmap;运行中卡住,重点看futex、poll、epoll_wait、nanosleep;网络超时,重点看connect、sendto、recvfrom的返回值和耗时。
这个思路同样适用于dtruss,只不过要把第2步的-c换成dtruss的-c输出(dtruss的统计输出包含系统调用次数、耗时等,格式和strace略有不同)。
3. 实操过程:从"看不懂"到"用得顺"的完整案例
3.1 场景一:程序启动即崩溃,用strace定位缺库问题
遇到过不少同事说"程序一启动就段错误,不知道怎么办"。这类问题如果发生在Linux下,用strace跟一遍启动流程,往往几秒就有结论。假设有一个叫myapp的二进制,启动直接崩:
strace -f -o /tmp/myapp.strace ./myapp-f跟踪所有子进程,-o把输出写到文件里,避免屏幕刷屏。打开输出文件,找到最后几条系统调用,往往就是崩之前的"临终现场":
openat(AT_FDCWD, "/lib/x86_64-linux-gnu/libssl.so.3", O_RDONLY|O_CLOEXEC) = -1 ENOENT (No such file or directory) write(2, "error while loading shared librar"..., 40) = 40这两行已经把问题说透了:程序在加载libssl.so.3时找不到文件。后续write到stderr的信息也印证了这一点。看完这个输出,直接ldd ./myapp确认依赖路径,然后把对应的so装好或调整LD_LIBRARY_PATH,问题解决。
这个场景里的关键点在于:strace能看清楚启动流程中每个文件被打开的位置和结果,比用ldd盲猜高效得多。如果换成dtruss,命令变成:
sudo dtruss -f -o /tmp/myapp.dtruss ./myapp同样看最后几条open/invoke记录,也能发现问题。不过dtruss对macOS的崩溃后路径显示有时不完整,所以我会优先看错误返回为-1的记录,再配合-e参数把所有错误的调用圈出来。
3.2 场景二:网络请求卡住,用strace追踪系统调用找原因
有次排查一个HTTP客户端请求卡住的问题,日志显示请求发出后一直没响应,但服务端说没收到请求。用strace挂上客户端进程后看到了完整链路:
socket(AF_INET, SOCK_STREAM|SOCK_CLOEXEC, IPPROTO_TCP) = 3 connect(3, {sa_family=AF_INET, sin_port=htons(8080), sin_addr=inet_addr("10.20.30.40")}, 16) = -1 EINPROGRESS (Operation now in progress) poll([{fd=3, events=POLLOUT}], 1, 30000) = 1 ([{fd=3, events=POLLOUT}]) getsockopt(3, SOL_SOCKET, SO_ERROR, [0], [4]) = 0connect返回EINPROGRESS是正常现象,表示连接建立中;随后poll等待可写事件。问题就出在poll返回了POLLOUT但实际连接并没有成功建立,因为接下来getsockopt取到的SO_ERROR是0,看起来一切正常。直到后续第一次sendto才暴露问题:sendto(3, ..., 0) = -1 EPIPE。
排查到最后发现是这个客户端和服务器之间的网络拓扑中,某个中间设备对SYN包设置了静默丢弃,导致TCP三次握手一直没完成,客户端一直等。单靠业务日志根本看不清这条TCP连接的建链状态,而strace把connect、poll、getsockopt每一步都记录下来了。
如果你用的是dtruss,查找网络调用时可以加-e trace=connect,recvfrom,sendto类似的过滤,dtruss也支持按系统调用名精确过滤(写法是-e connect或-e syscall::connect)。macOS下网络类问题同样可以按照这个思路定位,只是参数解析上需要多花点功夫。
3.3 场景三:用strace统计信息做性能粗筛
有时候并不是"程序出错",而是"程序变慢"。这时候可以用strace -c做一次系统调用级别的粗筛。比如我有个文件批处理脚本,处理一批文件越来越慢,先不急着优化业务代码,跑一遍统计:
strace -c -f ./batch_process.py结束后输出类似:
% time seconds usecs/call calls errors syscall ------ ----------- ----------- --------- --------- ---------------- 78.26 4.132000 413200 10 read 11.25 0.594000 5940 100 openat 5.41 0.285000 2850 50 write一目了然:read虽然只调用了10次,但总耗时占78%,每次平均413毫秒。这说明程序在读取大数据块时严重阻塞,大概率是磁盘IO瓶颈。顺着这个方向去查文件系统的IO性能、缓存设置、是否有大量随机读,比盲目优化代码快得多。
这个场景在dtruss下同样支持,sudo dtruss -c -f ./batch_process能输出类似统计。不过dtruss的统计信息颗粒度比较粗,没有strace按系统调用类型分层的细粒度,做性能分析时建议先用top、sample这类工具粗筛,再用dtruss精校。
3.4 场景四:macOS下用dtruss排查文件读写问题
有朋友遇到一个诡异问题:macOS上的程序读取一个配置文件,偶尔会读到旧内容,怀疑是文件缓存问题。在Linux下可以用strace -e trace=openat,read跟踪,换到macOS就得上dtruss:
sudo dtruss -e openat,read -p 4567附加到进程后,输出会显示:
PID COMMAND FD ARGS 4567 myapp 12 openat(0x01000000, 0x7ff7bfef2f00, 0x0, 0x0) = 13 0 4567 myapp 13 read(0xD, 0x7ff7bfef2e00, 0x400) = 1024 0看到read返回1024字节,但应用层可能因为内部缓冲逻辑没有用上这次读取的内容。这种问题用dtruss确认"内核层面确实读到了新数据"之后,就可以放心把矛头指向应用层——是缓存策略没刷新。换句话说,dtruss能把"数据到底有没有读到"这个事实摆清楚,避免在错误层次上反复猜。
macOS下dtruss对sudo的依赖比Linux更严格,如果你发现dtruss执行时报权限错误,先用sudo -i切换root环境试试。另外dtruss在较新macOS版本上对进程附加偶尔会报dtrace: failed to initialize dtrace之类的错误,遇到的话检查系统是否限制了DTrace的用户权限,必要时调整安全设置后再试(操作前先确认当前系统环境,不要盲目改动)。
4. 常见问题与排查技巧实录
4.1 踩坑记录:strace输出太大,把磁盘写满
我刚用strace时干过一次蠢事:直接strace -f -o /tmp/trace.log ./service,没做任何过滤,结果服务跑了十几分钟后,日志文件好几个GB,把磁盘占满,服务直接挂了。后来学乖了,高流量进程一律先用-c统计模式快速跑几十秒,确定热点系统调用后再精确过滤跟踪。
总结下来的输出控制原则:
- 能用过滤条件就尽量用,
-e trace=文件,-e trace=网络,或精确指定系统调用名。 - 必须全量输出时,用
-o写文件而不是打到屏幕,避免终端卡死。 - 加上
-s限制字符串长度,长URL和长路径会瞬间刷爆日志。 - 配合
timeout命令控制跟踪时长,比如timeout 30 strace -f -o /tmp/trace.log ./program,防止忘记手动停止。 - dtruss同样存在输出量大的问题,用
-e过滤和-o输出是基本操作。
4.2 为什么都要sudo才能跑:权限与安全机制
strace在Linux下跟踪他人进程时通常需要ptrace权限,很多发行版默认允许root执行,普通用户能否跟踪取决于/proc/sys/kernel/yama/ptrace_scope的配置。如果值为1,普通用户只能跟踪子进程;要跟踪其他用户进程就得root或改变配置。
dtruss在macOS上几乎默认就需要root,因为DTrace框架本身是个内核级调试机制,权限要求天然高。建议所有跨进程跟踪操作都直接上sudo,避免频繁遇到权限报错。
还有个容易被忽略的点:安全软件或容器环境可能阻止ptrace。如果你在容器里跑strace发现什么都没有输出,先检查容器是否给了CAP_SYS_PTRACE权限,没有的话加上--cap-add=SYS_PTRACE再试。
4.3 进程已经跑起来了,还能不能跟踪
日常排查中经常是"程序已经跑起来了,才知道需要跟踪"。strace和dtruss都支持附加到运行中的进程:
sudo strace -p 1234 -e trace=文件 -o /tmp/attach.log sudo dtruss -p 1234 -e openat,read -o /tmp/attach.dtruss这里有个技巧:如果-p附加时报Operation not permitted,除了权限问题,也可能是进程自身设置了PR_SET_DUMPABLE(一种避免被追踪的自我保护机制),这类进程即使root也可能附加失败。另外strace附加时的-f要谨慎,因为一旦加了-f,它会跟踪目标进程后续创建的所有子进程,输出量可能瞬间增大。我一般先不加-f,确认主进程的调用链后再决定是否展开。
4.4 几个容易被忽略但超级实用的细节
第一,strace对已经崩溃或退出的进程无能为力,但-o输出的最后几行往往就是崩溃前的"遗言"。如果程序启动闪退,别急着改代码,先strace看输出。
第二,-y参数可以打印文件描述符关联的文件路径。比如read(3, ...)不知道3对应哪个文件,加-y后输出变成read(3</etc/hosts>, ...),省去猜的功夫。dtruss输出里FD列也有类似信息,不过显示格式差异较大。
第三,-z参数可以只显示成功调用的末尾(静默模式),-Z则只显示失败的调用,排查错误时用-Z能大幅降低噪声。dtruss的-e参数也有类似效果,它会过滤掉返回成功的系统调用,只展示有错误的。
第四,strace的-xx能显示转义后的字符串,跟二进制协议调试时很有用。dtruss对二进制内容的输出一直比较弱,遇到这类需求建议换成tcpdump或Wireshark之类的抓包工具,别在系统调用层面硬啃。
第五,-i参数在strace中会打印指令指针地址,配合addr2line可以把崩溃位置定位到具体函数,这在没有调试符号的环境里是个绝招。dtruss没有直接对应的参数,但配合atos在macOS上可以实现类似目的。
以我自己的经验,一开始接触strace和dtruss时不要想着把所有参数背下来,先记住"附加进程-p、跟子进程-f、过滤-e、统计-c、写文件-o"这几个就够应付大多数场景了。等真遇到需要细致定位的问题,再回头翻参数的文档,印象会深刻得多。这两个命令看起来不起眼,但用好的话,能帮你把很多"玄学问题"变成"有据可查的逻辑问题",排查效率直接翻倍。