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

联系方式:

1. 微信:Lxh_Chat

2. 邮箱:939958092@qq.com
  1. Linux 性能调优系列(一) 建立科学的性能分析思路
  2. Linux 性能调优系列(二) 掌握基础性能监控命令
  3. Linux 性能调优系列(三) sysstat实现性能数据持久采集
  4. Linux 性能调优系列(四) 别只会用 sysstat,PCP 才是 Linux 性能监控的进阶玩法
  5. Linux 性能调优系列(五) 给你的服务器做一次全面体检
  6. Linux 性能调优系列(六) 想调内核参数?先搞懂 /proc、sysctl 和 sysfs
  7. Linux 性能调优系列(七) 还在手动改 sysctl?TuneD 智能性能调优实战来了
  8. Linux 性能调优系列(八) 进程太能吃资源怎么办?用 ulimit 和 systemd 给它加个"上限"
  9. Linux 性能调优系列(九) cgroup v2实战:防止一个烂进程干崩整台服务器
  10. Linux 性能调优系列(十) CPU 100% 到底是谁在忙?用 perf 把"元凶"揪出来
  11. Linux 性能调优系列(十一) 程序为什么卡住了?strace 和 ltrace 带你找到答案
  12. Linux 性能调优系列(十二) 服务器又卡了?SystemTap 带你杀到内核看看为啥
  13. Linux 性能调优系列(十三) 从 SystemTap 到 eBPF,Linux 性能追踪进入新时代
  14. Linux 性能调优系列(十四) CPU 这么忙,到底是谁决定“让谁先跑”?一文搞懂 Linux 调度器
  15. Linux 性能调优系列(十五) CPU 到底该先给谁?搞懂 Linux 实时调度
  16. Linux 性能调优系列(十六) Linux CPU 隔离实战,为什么实时线程绑核后延迟还会抖?
  17. Linux 性能调优系列(十七) CPU 占用不高却跑的慢?深挖 CPU 缓存性能瓶颈
  18. Linux 性能调优系列(十八) 程序申请了1GB内存,为什么系统却说它没用?
  19. Linux 性能调优系列(十九) 服务器硬盘慢得像蜗牛?先别急着换硬盘,这四招先把瓶颈找出来
  20. Linux 性能调优系列(二十) 磁盘 I/O 调度器怎么选?一文搞懂 mq-deadline、kyber、bfq 和 none
  21. Linux 性能调优系列(二十一) 磁盘为什么慢?从文件系统到 I/O 调优,一次讲透
  22. Linux 性能调优系列(二十二) Linux 网络调优别乱改 sysctl!先找瓶颈再调 TCP 缓冲区
  23. Linux 性能调优系列(二十三) MTU 从 1500 到 9000,巨帧真的能提升性能吗?
  24. Linux 性能调优系列(二十四) 从 CPU 省电到网卡卸载,聊聊 Linux 网络调优
  25. Linux 性能调优系列(二十五) 聊聊VirtIO、TuneD、virtio-mem 调优

前面我们已经学习了 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 性能分析工具链就真正串起来了。