1
2
3
4
5
6
7
作者:李晓辉

联系方式:

1. 微信:Lxh_Chat

2. 邮箱:939958092@qq.com

前面我们已经学习了 perf、strace 和 ltrace。它们解决的问题不太一样:

flowchart LR
    APP["应用程序"]

    APP --> LTRACE["ltrace<br/>库函数"]
    APP --> STRACE["strace<br/>系统调用"]
    APP --> PERF["perf<br/>性能热点"]

    STRACE --> KERNEL["Linux Kernel"]
    PERF --> KERNEL

    KERNEL --> STAP["SystemTap<br/>内核动态追踪"]

比如:

  • perf 发现:这个进程 CPU 很高,某个函数占了大量 CPU
  • strace 发现:这个进程正在疯狂调用某些系统调用
  • ltrace 发现:程序频繁调用某个用户态库函数

但是有时候我们还想继续往内核里面看:

这个内核函数到底被调用了多少次?

是谁调用的?

什么时候调用的?

文件 IO、网络、调度器内部到底发生了什么?

这时候,就可以考虑 SystemTap。


SystemTap 是什么?

SystemTap 是 Linux 上的动态追踪工具。它最大的特点是:

不需要重新编译 Linux 内核,也不需要重启服务器,就可以给正在运行的系统增加动态探测点。

简单来说:

1
2
3
4
5
6
7
8
9
正在运行的 Linux
↓
SystemTap
↓
插入探测点
↓
观察内核 / 进程行为
↓
输出或统计结果

所以可以把 SystemTap 理解成:

给正在运行的 Linux 内核“装上一根临时探针”。

探针发现感兴趣的事件以后,就执行我们定义好的处理逻辑。


SystemTap 到底怎么工作?

这里不用一上来就背一堆概念。你只需要先理解一件事情:

SystemTap 脚本最终会被编译成一个内核模块,然后加载到正在运行的内核中。

整体流程:

flowchart LR
    A["SystemTap 脚本"] --> B["解析"]
    B --> C["生成 C 代码"]
    C --> D["编译"]
    D --> E["内核模块 .ko"]
    E --> F["加载到 Linux 内核"]
    F --> G["开始追踪"]
    G --> H["输出 / 统计结果"]

所以 SystemTap 和普通 Shell 脚本有一个很大的区别。

Shell 脚本:

1
2
3
Shell脚本
↓
Shell解释执行

SystemTap:

1
2
3
4
5
6
7
8
9
10
11
SystemTap脚本
↓
转换
↓
C代码
↓
编译
↓
内核模块
↓
进入内核执行

这也是 SystemTap 强大,同时又需要谨慎使用的原因。


SystemTap 最核心的两个概念

学习 SystemTap,其实先记住两个词就够了:

1
2
Probe
Handler

可以把它们理解成:

Probe:什么时候触发?

Handler:触发以后干什么?

例如:

1
stap -e 'probe timer.s(1) { printf("hello\n"); exit() }'

这里:

1
probe timer.s(1)

就是定义:

1 秒以后触发。

而:

1
2
3
4
{
printf("hello\n")
exit()
}

就是:

触发以后执行什么操作。

因此 SystemTap 最基本的模型就是:

flowchart LR
    EVENT["事件发生"] --> PROBE["Probe<br/>定义触发点"]
    PROBE --> HANDLER["Handler<br/>处理逻辑"]
    HANDLER --> RESULT["输出 / 统计"]

以后看到 SystemTap 脚本,先找这两个东西,就不会觉得它很复杂。


安装 SystemTap

先看看当前运行的内核:

1
2
[root@localhost ~]# uname -r
6.12.0-211.16.1.el10_2.0.1.x86_64

安装 SystemTap:

1
2
[root@localhost ~]# dnf install systemtap -y

然后运行:

1
stap-prep

stap-prep 会根据当前运行的内核,检查并准备 SystemTap 所需要的内核开发和调试环境。

例如:

1
2
3
4
5
6
Configuring for kernel release 6.12.0-211.16.1.el10_2.0.1.x86_64

Need to install the following packages:

kernel-devel-6.12.0-211.16.1.el10_2.0.1.x86_64
kernel-debuginfo-6.12.0-211.16.1.el10_2.0.1.x86_64

如果自动准备失败,也可以手动安装:

1
2
3
4
dnf install \
kernel-devel-$(uname -r) \
kernel-debuginfo-$(uname -r) \
kernel-debuginfo-common-$(uname -m)-$(uname -r)

这里有一个非常容易混淆的地方:

kernel-debuginfo ≠ kernel-debug

kernel-debuginfo 主要提供调试符号等调试信息。

kernel-debug 则是另一种带特定调试配置的内核构建。

所以:

不要因为名字里都有 debug,就认为这两个包是一回事。

对于 SystemTap,需要关注的是与目标内核匹配的 kernel-debuginfo、kernel-debuginfo-common 和 kernel-devel 等包。


先别急着写复杂脚本

安装完成以后,我们先运行一个最简单的 SystemTap。

1
stap -e 'probe timer.s(1) { exit() }'

你可能会发现:

1
2
3
4
5
6
7
执行命令
↓
等待
↓
大约 1 秒
↓
自动退出

为什么?

因为:

1
timer.s(1)

表示:

1 秒定时器事件。

而:

1
exit()

表示:

退出 SystemTap。

所以这个脚本虽然没有什么实际用途,但是特别适合干一件事情:

验证 SystemTap 环境到底有没有配置成功。


看看 SystemTap 的完整执行过程

刚才我们执行的时候,如果加上 -v:

1
2
3
4
5
6
7
8
[root@localhost ~]# stap -v -e 'probe timer.s(1) { exit() }'
Pass 1: parsed user script and 486 library scripts using 588276virt/126084res/14056shr/144144data kb, in 30usr/210sys/96real ms.
Pass 2: analyzed script: 1 probe, 1 function, 0 embeds, 0 globals using 589860virt/128204res/14660shr/145728data kb, in 0usr/0sys/6real ms.
Pass 3: using cached /root/.systemtap/cache/4d/stap_4d50aa8186d907fcb385df3e7a4dbead_1045.c
Pass 4: using cached /root/.systemtap/cache/4d/stap_4d50aa8186d907fcb385df3e7a4dbead_1045.ko
Pass 5: starting run.
Pass 5: run completed in 0usr/130sys/2195real ms.

这其实就是 SystemTap 的完整工作流程。

可以记成:

flowchart LR
    P1["Pass 1<br/>解析脚本"]
    P2["Pass 2<br/>分析脚本 / 探测点"]
    P3["Pass 3<br/>生成 C"]
    P4["Pass 4<br/>编译 .ko"]
    P5["Pass 5<br/>加载并运行"]

    P1 --> P2 --> P3 --> P4 --> P5

平时不用死记每个 Pass 的细节。真正需要记住的是:

Pass 4:已经编译出了内核模块。

Pass 5:加载模块并真正开始追踪。

因此:

1
stap -p 4 your_script.stp

表示:

只编译到 Pass 4,不进入运行阶段。

这个参数后面的交叉插桩会用到。


SystemTap 真正开始有用了

前面的例子只是验证环境。真正排查性能问题,我们通常不会从零开始写脚本。因为 SystemTap 已经提供了大量官方示例。

先看看:

1
2
3
4
5
[root@localhost ~]# ls /usr/share/systemtap/examples
apps html index.txt io keyword-index.txt lwtools metadatabase.db process README stapgames
general index.html interrupt keyword-index.html locks memory network profiling security-band-aids virtualization
[root@localhost ~]#

还有一个非常方便的索引:

1
/usr/share/systemtap/examples/index.html

所以实际工作中,我更推荐大家养成一个习惯:

flowchart LR
    PROBLEM["遇到性能问题"]
    PROBLEM --> SEARCH["先找官方示例"]
    SEARCH --> EXIST["是否已经有类似脚本?"]
    EXIST -->|是| RUN["直接运行"]
    EXIST -->|否| WRITE["再考虑自己写脚本"]

不要一上来就自己写。


案例一:程序访问文件到底花了多少时间?

假设现在遇到一个问题:

程序访问文件很慢。

但是 iostat 看到的磁盘整体负载并不高。这时候可以尝试:

1
2
3
4
5
6
7
8
9
10
11
[root@localhost ~]# stap /usr/share/systemtap/examples/io/iotime.stp
348764 1056 (in:imjournal) access "/var/lib/rsyslog/imjournal.state.tmp" read: 0 write: 120
348778 1056 (in:imjournal) iotime "/var/lib/rsyslog/imjournal.state.tmp" time: 58
393665 1056 (in:imjournal) access "/run/log/journal/227d3258119347278234251bddccb33a/system.journal" read: 0 write: 0
732947 2811 (head) access "/etc/ld.so.cache" read: 0 write: 0
732995 2811 (head) access "/lib64/libc.so.6" read: 832 write: 0
732998 2811 (head) iotime "/lib64/libc.so.6" time: 3
733132 2811 (head) access 0x7f29798566c0 read: 0 write: 0
733174 2811 (head) access "/usr/share/locale/locale.alias" read: 2998 write: 0
733176 2811 (head) iotime "/usr/share/locale/locale.alias" time: 2

这个脚本会帮助我们观察进程访问文件时的 IO 情况。我们重点关注:

1
2
3
4
进程
文件
read / write
time

这样就可以进一步回答:

到底哪个进程访问了哪个文件?

读写过程中花了多少时间?

这和 iostat 的关注点是不一样的。

flowchart LR
    IOSTAT["iostat<br/>磁盘整体负载"]
    IOTIME["iotime.stp<br/>进程文件访问行为"]

    IOSTAT --> A["设备层面"]
    IOTIME --> B["进程 / 文件 / IO时间"]

所以:

iostat 更适合看设备整体情况。

iotime.stp 可以进一步观察具体进程的文件访问行为。


案例二:哪个进程疯狂调用系统调用?

再来看一个非常典型的问题。

假设:

1
某个服务器 CPU 使用率突然升高

我们怀疑:

是不是某个进程正在疯狂和内核交互?

可以运行:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
[root@localhost ~]# stap /usr/share/systemtap/examples/process/syscalls_by_proc.stp
Collecting data... Type Ctrl-C to exit and display results
^C#SysCalls Process Name
4728 who
3946 systemd-userwor
2700 bash
2340 head
1656 df
1291 stapio
1250 systemd
1209 sshd-session
1062 tail
900 sleep
80 (sd-worker)
50 in:imjournal
47 systemd-udevd
44 irqbalance
22 systemd-journal
15 systemd-userdbd
13 NetworkManager
8 gmain
5 stap
3 (udev-worker)
2 rs:main Q:Reg

运行完卡住了吧,哈哈,这时候不要着急。它正在后台:

1
2
3
4
5
收集数据
↓
统计系统调用
↓
累计结果

按:

1
Ctrl+C

以后,才会显示统计结果,例如:

1
2
3
4
5
6
#SysCalls  Process Name
9653 who
9081 sftp-server
8388 sshd-session
8129 systemd-userwor
...

这样我们就能快速知道:

哪些进程产生了大量系统调用。

整个思路非常简单:

flowchart LR
    PROCESS["多个进程"]
    PROCESS --> SYSCALL["系统调用"]
    SYSCALL --> STAP["SystemTap统计"]
    STAP --> RESULT["按进程汇总"]

这类工具非常适合回答:

“到底哪个进程正在疯狂进入内核?”


SystemTap 的权限

这里不用讲得特别复杂。SystemTap 最终需要加载内核模块,所以它不是一个普通的用户态工具。常见的两个用户组:

1
2
stapdev
stapusr

可以简单理解成:

用户组能做什么
stapdev可以编译、加载和运行 SystemTap 模块
stapusr只能运行已经准备好的 SystemTap 模块

因此:

stapdev 权限非常高,不应该随便授予普通用户。

如果企业希望实现:

1
2
3
普通用户
↓
只能运行已经审核过的模块

那么 stapusr 更符合最小权限的思路。


生产环境不能直接装?还有交叉插桩

这才是 SystemTap 里比较有实际价值的一部分。

假设生产服务器:

1
2
3
不允许安装 GCC
不允许安装 debuginfo
不允许安装完整开发环境

但是现在又确实需要 SystemTap。怎么办?

答案就是:

交叉插桩。

核心思想就一句话:

把编译放到生产环境之外,把运行留在生产服务器。

flowchart LR
    HOST["编译主机"]

    HOST --> SCRIPT["SystemTap脚本"]
    SCRIPT --> COMPILE["针对目标内核编译"]
    COMPILE --> KO["生成 .ko"]

    KO --> COPY["复制"]
    COPY --> PROD["生产服务器"]

    PROD --> RUNTIME["systemtap-runtime"]
    RUNTIME --> STAPRUN["staprun"]
    STAPRUN --> KERNEL["目标内核"]

也就是说:

1
2
3
4
5
6
7
8
9
编译主机
↓
生成 .ko
↓
生产服务器
↓
staprun
↓
运行

生产服务器不负责编译。


交叉插桩最重要的一点

这里千万不要理解成:

“我在一台 RHEL 服务器编译一个模块,然后拿到另一台 RHEL 服务器就能跑。”

不是。

内核模块必须针对目标内核环境构建。首先在生产服务器确认:

1
uname -r

例如:

1
6.12.0-211.16.1.el10_2.0.1.x86_64

然后编译环境需要准备目标内核对应的:

1
2
3
kernel-devel
kernel-debuginfo
kernel-debuginfo-common

然后:

1
2
3
4
stap -r <目标内核版本> \
your_script.stp \
-m mymodule \
-p 4

例如:

1
2
3
4
stap -r 6.12.0-211.16.1.el10_2.0.1.x86_64 \
your_script.stp \
-m mymodule \
-p 4

最终得到:

1
mymodule.ko

再把它复制到目标服务器的 SystemTap 模块目录。

目标服务器只需要:

1
dnf install systemtap-runtime -y

然后运行:

1
staprun mymodule.ko

所以记住一句话:

编译主机负责“造模块”,生产服务器负责“跑模块”。


SystemTap 为什么需要谨慎?

现在大家应该能理解 SystemTap 为什么需要谨慎了。因为它不是:

1
2
3
普通用户程序
↓
用户态运行

而是:

1
2
3
4
5
6
7
SystemTap脚本
↓
C代码
↓
内核模块
↓
Linux Kernel

所以错误的内核级追踪代码可能影响系统稳定性。尤其是使用 guru mode、嵌入 C 等高级能力时,更应该谨慎。

生产环境建议:

flowchart LR
    A["官方示例"] --> B["测试环境验证"]
    B --> C["生产环境短时间运行"]
    C --> D["收集数据"]
    D --> E["结束追踪"]

而不是:

“线上出问题了,我随便写个 SystemTap 脚本直接跑。”

这并不是一个好的生产实践。


SystemTap 和 eBPF有什么关系?

下一篇我们会讲 eBPF。现在不用提前把 eBPF 学完,只需要知道一个核心区别:

flowchart LR
    STAP["SystemTap"]
    STAP --> C["生成 C"]
    C --> KO["内核模块 .ko"]
    KO --> K1["Linux Kernel"]

    EBPF["eBPF"]
    EBPF --> PROG["eBPF 程序"]
    PROG --> VERIFY["Verifier"]
    VERIFY --> K2["受约束的内核执行环境"]

简单来说:

SystemTap:脚本最终生成并加载内核模块。

eBPF:程序需要经过内核 verifier 等机制检查,在受约束的执行模型中运行。

所以两者都可以做:

1
2
3
4
5
内核追踪
IO分析
网络分析
调度分析
性能分析

但底层实现和安全模型不同。下一篇我们再详细讲 eBPF。


到底什么时候使用 SystemTap?

到这里不要再继续背命令了。真正重要的是建立这个判断:

flowchart LR
    PROBLEM["发现性能问题"]

    PROBLEM --> TOP["top<br/>哪个进程有问题?"]

    TOP --> PERF["perf<br/>CPU热点在哪里?"]

    PERF --> STRACE["strace<br/>发生了哪些系统调用?"]

    STRACE --> STAP["SystemTap<br/>内核内部发生了什么?"]

    STAP --> ROOT["进一步定位根因"]

可以简单理解成:

工具主要回答的问题
top谁在消耗资源?
perfCPU 时间主要消耗在哪里?
strace进程调用了哪些系统调用?
ltrace程序调用了哪些库函数?
SystemTap内核内部发生了什么?

这才是这几个工具真正应该建立起来的关系。


最后记住几个 SystemTap 命令

验证 SystemTap:

1
stap -e 'probe timer.s(1) { exit() }'

运行脚本:

1
stap your_script.stp

查看详细执行过程:

1
stap -v your_script.stp

只编译到 Pass 4:

1
stap -p 4 your_script.stp

查看官方示例:

1
ls /usr/share/systemtap/examples

运行 IO 示例:

1
stap /usr/share/systemtap/examples/io/iotime.stp

运行系统调用统计:

1
stap /usr/share/systemtap/examples/process/syscalls_by_proc.stp

交叉编译:

1
2
3
4
stap -r <target-kernel-version> \
your_script.stp \
-m mymodule \
-p 4

运行预编译模块:

1
staprun mymodule.ko

总结

如果只记住这篇文章的几个核心知识点,其实就够了。

第一,SystemTap 是干什么的?

动态追踪正在运行的 Linux 系统,尤其适合深入观察内核行为。

第二,它怎么工作?

1
2
3
4
5
6
7
8
9
SystemTap脚本
↓
生成C代码
↓
编译
↓
内核模块
↓
加载运行

第三,脚本怎么看?

1
2
3
4
5
6
7
Probe
↓
什么时候触发?

Handler
↓
触发以后干什么?

第四,遇到实际问题怎么办?

先找官方示例:

1
/usr/share/systemtap/examples

比如:

1
2
3
4
5
6
7
iotime.stp
↓
观察文件 IO

syscalls_by_proc.stp
↓
统计进程系统调用

第五,生产服务器不方便编译怎么办?

使用:

交叉插桩。

1
2
3
4
5
6
7
编译主机
↓
生成目标内核对应的 .ko
↓
生产服务器
↓
staprun

最后再把整个性能分析链条串起来:

flowchart LR
    A["发现问题<br/>top"] --> B["CPU热点<br/>perf"]
    B --> C["系统调用<br/>strace"]
    C --> D["库函数<br/>ltrace"]
    D --> E["深入内核<br/>SystemTap"]
    E --> F["定位根因"]

到这里,SystemTap 就先学到这里。下一篇老李再聊聊eBPF。我们会换一种完全不同的思路,不再重点关注“怎么生成内核模块”,而是直接使用 BCC、bpftrace 等工具,开始真正做:

1
2
3
4
5
IO追踪
进程分析
系统调用
网络分析
调度分析

这样从 perf → strace → SystemTap → eBPF,整个 Linux 性能分析工具链就真正串起来了。