ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

系统调用跟踪工具 strace 与 dtruss:从内核边界排查到性能瓶颈定位

系统调用跟踪工具 strace 与 dtruss:从内核边界排查到性能瓶颈定位 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、futex2. straceLinux 下最趁手的系统调用跟踪工具要说 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 traceopen,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_modeS_IFREG|0644, st_size19475, ...}) 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 只需要一条命令就能把事情讲清楚。假设程序启动时报“Cant open config file”这时我们跟踪它做了什么strace -f -e traceopen,openat ./myapp 21 | 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. dtrussmacOS/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 21 | 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就21 | 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完整跑一遍把退出时最后一个报错调用记录下来。这一步花不了几秒钟却经常能把我直接带到问题现场。这种“先看边界再看逻辑”的排查顺序帮我省下的时间早就超过当初学命令的时间了。
返回列表