跳到主要内容

9.9 Ftrace 实战:从栈溢出监控到 Android 排毒

好了,Instances 的话题先告一段落。

有了这么多工具,具体怎么用?Ftrace 就像一把瑞士军刀,功能多到你可能会拿着刀不知道该往哪剁。本节我们要看几个真实的「杀猪」现场——不是真的杀猪,是杀 Bug。

我们先看一个隐蔽但致命的内核问题:栈溢出。


监控内核栈使用情况——Ftrace 的「测谎仪」

你可能知道,每个活着的线程都有两套栈:一套用户态栈,一套内核态栈。

用户态栈很大,也很「软」,它是动态增长的。在普通的 Linux 发行版上,它的上限(RLIMIT_STACK)通常是 8 MB。如果你不够用,内核会自动帮你扩容,直到撑爆这个限制。

但内核态栈完全是另一套生物。

它是固定的,硬的,而且非常小。

  • 在 32 位系统上,通常只有 8 KB
  • 在 64 位系统上,通常是 16 KB

这就带来一个严重的后果:溢出。如果你的内核代码递归太深,或者局部变量开得太大,内核栈就会溢出。这不是什么「抛个异常」那么简单,这通常意味着系统立刻锁死,或者直接 Kernel Panic。这是那种会让你在深夜三点盯着控制台怀疑人生的 Bug。

⚠️ 配置提示

有两个内核选项能稍微缓解这种痛苦:

  • CONFIG_VMAP_STACK:开启后,内核栈会从 vmalloc 区域分配。这允许内核设置一个「守卫页」,一旦栈溢出触碰到守卫页,内核会触发 Oops 并优雅地杀掉进程,而不是直接把整台机器带走。
  • CONFIG_THREAD_INFO_IN_TASK:这能进一步减轻栈溢出带来的连锁反应。

如果你在编译内核,记得把它们勾上。

正因为内核栈这么脆弱,监控它在运行时用了多少,就成了一个非常有价值的调试手段。Ftrace 自带了一个专门的工具做这件事——Stack Tracer(栈跟踪器)

要启用它,确保你的内核配置里有 CONFIG_STACK_TRACER=y(通常默认是开的)。它不由 tracefs 直接控制,而是通过 /proc 伪文件 /proc/sys/kernel/stack_tracer_enabled 来开关,默认是关的。

让我们来一次实战演练。我们将开启 ftrace 的栈跟踪器,跑一轮采样,看看哪些内核函数是「吃栈怪兽」(注意,这需要 root 权限):

第一步——开启栈跟踪器

echo 1 > /proc/sys/kernel/stack_tracer_enabled

第二步——跑一轮采样

我们需要一个脚本来辅助。我写了一个很简单的脚本 ch9/ftrace/ftrc_1s.sh,它会开启 ftrace 并记录内核在 1 秒钟内的活动。在这个窗口期内,我们会尽可能触发栈的深度使用。

cd /sys/kernel/tracing
<...>/ch9/ftrace/ftrc_1s.sh
[...]

第三步——查看最大栈深度和细节

现在,数据已经记下来了。我们主要关心两个文件:

  • stack_max_size:自系统启动以来(或者自上一次清空以来)观测到的最大栈使用量。
  • stack_trace:产生这个最大深度的那个时刻的调用栈详情。
cat stack_max_size
cat stack_trace

下面的截图展示了一次运行的结果。在这个例子里,内核栈的最大使用量超过了 4000 字节(接近 4KB,这意味着在 32 位系统上一半的栈空间已经没了)。

(此处插入 Figure 9.19 的截图描述:显示 stack_max_size 为 4160 字节,以及其下方的详细函数调用栈)

如果你看到这个数字逼近了 8 KB(32 位)或 16 KB(64 位),你的心跳应该加速一下。那意味着离崩溃只有几步之遥了。

关于 Stack Tracer 的更多细节,可以翻看官方内核文档:Documentation/trace/ftrace.html


Android 是怎么用 Ftrace 给系统排毒的

工具好不好用,要看战场在哪里。

Android Open Source Project (AOSP) 不仅是用 Ftrace,简直是把它当成了核武器。在 Android 内部开发中,他们其实是在 Ftrace 之上包了一层壳,但也允许直接用。

AOSP 的官方文档里有一段话非常硬核,我建议你背下来:

"However, every single difficult performance bug in 2015 and 2016 was ultimately root-caused using dynamic ftrace."

「然而,2015 和 2016 年每一个困难的性能 Bug,最终都是靠 dynamic ftrace 找到了根本原因。」

这口气不小,但如果你看过 Android 设备的复杂性,就会明白这并不是在吹牛。

文档里特别提到了 Ftrace 的几个杀手级应用场景:

  1. 调试不可中断睡眠(Uninterruptible Sleep) 为什么难调?因为进程睡了,日志可能也就停了。但用 Ftrace 的 function_graphfunction tracer 配合过滤器,你可以每一次代码进入 uninterruptible_sleep 函数时都抓一个内核栈快照。这就像是给昏迷的病人每分钟做一次心电图。

  2. 确认驱动「霸占」CPU 的时间 驱动程序可能会为了「原子性」而长时间关中断或关抢占。这在单线程代码里看似安全,但在多核系统里是灾难。

    还记得上一节我们提到的那些「延迟跟踪器」吗?

    • irqsoff
    • preemptoff
    • preemptirqsoff

    AOSP 文档直接点名:这些工具主要用于证实驱动程序是不是把中断或抢占关得太久了。

真实案例:拍照后的卡顿

文档里举了一个真实的例子:一台 Pixel XL 手机。 现象:拍完一张 HDR 照片后,立刻旋转取景框,界面出现了明显的卡顿(Jank)。 手段:就是靠 Ftrace 抓出来的。通过 trace 他们发现,某个关键路径被某个长耗时操作堵住了,导致了响应延迟。 (详见:https://source.android.com/devices/tech/debug/ftrace)

不仅是这个例子,AOSP 文档还总结了一些常见模式:

  • 驱动失职:驱动程序把硬件中断(IRQ)或抢占关了太久,导致系统响应变慢。
  • 软中断(Softirq)过长:软中断会禁止内核抢占。如果处理软中断的时间太长,别的任务就切不进来,系统感觉就像死机了一样。

这东西非常有趣。如果你在写嵌入式 Linux 或者 Android 驱动,不懂 Ftrace,基本上就是在闭眼开车。


Netflix 的云端实战:预告片

这种「穷追猛打」的精神不仅限于手机界。

在云端,Ftrace 也是定海神针。这里再预告一个更精彩的真实案例:使用 perf-tools 脚本(也就是我们前面提到的、Brendan Gregg 写的那套基于 Ftrace 的前端工具集)来调试 Netflix Linux 实例上的数据库磁盘 I/O 问题。

我们会在后面专门有一节("Investigating a database disk I/O issue on Netflix cloud instances with perf-tools")来讲这个。

那里的战场从手机变成了云服务器,武器从原始 Ftrace 变成了更友好的脚本,但核心逻辑是一样的:看见不可见之物