flowchart TD
A[Linux 追踪与性能工具全景] --> B[基础机制 / 经典工具]
A --> C[内核追踪框架]
A --> D[现代可编程平台]
B --> B1[ptrace<br>进程调试与追踪的系统调用]
B1 --> B2[strace<br>追踪系统调用]
B1 --> B3[ltrace<br>追踪动态库函数调用]
C --> C1[ftrace<br>内核内置追踪器]
C --> C2[perf<br>性能事件与硬件计数器]
D --> D1[eBPF<br>内核沙箱程序]
D1 --> D2[kprobe / kretprobe<br>动态追踪内核函数]
D1 --> D3[uprobe / uretprobe<br>动态追踪用户态函数]
D1 --> D4[tracepoint<br>内核预置静态追踪点]
ptrace - process trace
核心原理
https://www.man7.org/linux/man-pages/man2/ptrace.2.html
1
2
3
4
|
#include <sys/ptrace.h>
long ptrace(enum __ptrace_request request, pid_t pid,
void *addr, void *data);
|
ptrace() 系统调用(system call)提供了一种机制,使得一个进程(tracer)可以观察和控制另一个进程(tracee)的执行,并检查和更改被追踪者的内存和寄存器。它主要用于实现断点调试和系统调用跟踪。它是一切用户态调试工具的基石——gdb、strace、ltrace 底层都依赖它。
tracee首先需要被附加到tracer上。附加及其后续命令都是按线程进行的:在一个多线程进程中,每个线程都可以被单独附加到一个(可能是不同的)追踪器上,或者保持未附加状态从而不被调试。因此,“被追踪者”始终指代“(一个)线程”,而绝不是“一个(可能是多线程的)进程” , ptrace 命令始终通过以下形式的调用来发送给特定的被追踪者:ptrace(PTRACE_foo, pid, ...)其中 pid 是对应 Linux 线程的线程 ID
一个进程可以通过调用 fork() 并让生成的子进程执行 PTRACE_TRACEME,随后(通常)再执行 execve() 来启动跟踪。或者,一个进程可以使用 PTRACE_ATTACH或 PTRACE_SEIZE 开始跟踪另一个进程。
在被跟踪期间,即使信号被忽略,被追踪者也会在每次收到信号时停止。(SIGKILL 是例外,它会照常生效。)追踪器将在其下一次调用 waitpid()(或相关的“wait”系列系统调用)时收到通知;该调用将返回一个状态值,其中包含指示被追踪者停止原因的信息。当被追踪者停止时,追踪器可以使用各种 ptrace 请求来检查和修改被追踪者。然后,追踪器使被追踪者继续执行,并可以选择忽略已传递的信号。
sequenceDiagram
participant Tracer as Tracer (父进程)
participant Kernel as Linux Kernel
participant Tracee as Tracee (子进程)
Tracer->>Kernel: ptrace(PTRACE_TRACEME)
Tracer->>Kernel: fork()
Kernel-->>Tracer: child pid
Note over Tracee: 子进程开始执行
Tracee->>Kernel: execve("target")
Kernel-->>Tracer: SIGTRAP — 子进程暂停
Tracer->>Kernel: ptrace(PTRACE_GETREGS, pid, ...)
Kernel-->>Tracer: 寄存器内容
Tracer->>Kernel: ptrace(PTRACE_PEEKDATA, pid, addr, ...)
Kernel-->>Tracer: 内存内容
Tracer->>Kernel: ptrace(PTRACE_SYSCALL, pid, ...)
Note over Tracee: 恢复执行,进入系统调用
Kernel-->>Tracer: 系统调用入口/出口 暂停
Tracer->>Kernel: ptrace(PTRACE_CONT, pid, ...)
Note over Tracee: 继续执行
关键请求(request)
| 请求常量 |
作用 |
PTRACE_TRACEME |
子进程标记自己为"可被追踪" |
PTRACE_ATTACH |
附加到已运行的进程 |
PTRACE_DETACH |
解除附加 |
PTRACE_CONT |
继续执行 tracee |
PTRACE_SYSCALL |
在系统调用入口/出口暂停 |
PTRACE_SINGLESTEP |
单步执行一条指令 |
PTRACE_GETREGS/SETREGS |
读写寄存器 |
PTRACE_PEEKDATA/POKEDATA |
读写内存 |
demo
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
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
|
// minimal_ptrace_tracer.c
#include <sys/ptrace.h>
#include <sys/wait.h>
#include <sys/user.h>
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
int main(int argc, char *argv[]) {
if (argc < 2) {
fprintf(stderr, "Usage: %s <program> [args...]\n", argv[0]);
exit(1);
}
pid_t pid = fork();
if (pid == 0) {
// --- 子进程 (tracee) ---
ptrace(PTRACE_TRACEME, 0, NULL, NULL);
execvp(argv[1], &argv[1]); // 执行目标程序
perror("execvp");
exit(1);
}
// --- 父进程 (tracer) ---
int status;
waitpid(pid, &status, 0); // 等待 exec 后的 SIGTRAP
ptrace(PTRACE_SETOPTIONS, pid, 0,
PTRACE_O_TRACESYSGOOD); // 让 syscall 暂停可识别
struct user_regs_struct regs;
int call_count = 0;
while (1) {
// 进入系统调用
ptrace(PTRACE_SYSCALL, pid, 0, 0);
waitpid(pid, &status, 0);
if (WIFEXITED(status)) break;
// 读取寄存器,获取系统调用号
ptrace(PTRACE_GETREGS, pid, 0, ®s);
#ifdef __x86_64__
long syscall_nr = regs.orig_rax; // x86_64 下系统调用号在 orig_rax
#else
long syscall_nr = regs.orig_eax; // x86 下在 orig_eax
#endif
printf("[syscall #%d] nr = %ld\n", ++call_count, syscall_nr);
// 退出系统调用
ptrace(PTRACE_SYSCALL, pid, 0, 0);
waitpid(pid, &status, 0);
if (WIFEXITED(status)) break;
// 读取返回值
ptrace(PTRACE_GETREGS, pid, 0, ®s);
#ifdef __x86_64__
long ret = regs.rax;
#else
long ret = regs.eax;
#endif
printf("[syscall #%d] return = %ld\n", call_count, ret);
}
printf("--- total syscalls: %d ---\n", call_count);
return 0;
}
|
编译运行:
1
2
|
gcc -o minimal_ptrace_tracer minimal_ptrace_tracer.c
./minimal_ptrace_tracer /bin/ls /tmp
|
输出片段:
1
2
3
4
5
6
7
8
|
[syscall #1] nr = 0 # read
[syscall #1] return = 0
[syscall #2] nr = 1 # write
[syscall #2] return = 24
[syscall #3] nr = 2 # open
[syscall #3] return = 3
...
--- total syscalls: 127 ---
|
ubuntu 上 系统调用号可以通过 /usr/include/x86_64-linux-gnu/asm/unistd_64.h 或 man syscalls 查阅。
1
2
3
4
5
6
7
8
9
10
11
|
// cat /usr/include/x86_64-linux-gnu/asm/unistd_64.h
#ifndef _ASM_UNISTD_64_H
#define _ASM_UNISTD_64_H
#define __NR_read 0
#define __NR_write 1
#define __NR_open 2
#define __NR_close 3
#define __NR_stat 4
#define __NR_fstat 5
#define __NR_lstat 6
|
ptrace 的局限
- 每次系统调用产生 4 次上下文切换(tracee → kernel → tracer → kernel → tracee),性能开销极大
- 每次暂停都会触发
waitpid(),大量系统调用时退化严重
- 不支持同时追踪多个线程的独立系统调用(需
PTRACE_O_TRACECLONE 等选项配合)
- ptrace 不是为生产环境设计的——它是调试基础设施,不是性能工具
这就是为什么后来出现了 bpftrace、perf、eBPF 等更现代的工具——它们在内核中直接处理事件,无需反复陷入 tracer 进程。
ptrace 源码
elixir.bootlin.com/glibc—–ptrace.c
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
26
27
|
// glibc/sysdeps/unix/sysv/linux/ptrace.c
long int
ptrace (enum __ptrace_request request, ...)
{
long int res, ret;
va_list ap;
pid_t pid;
void *addr, *data;
va_start (ap, request);
pid = va_arg (ap, pid_t);
addr = va_arg (ap, void *);
data = va_arg (ap, void *);
va_end (ap);
if (request > 0 && request < 4)
data = &ret;
res = INLINE_SYSCALL (ptrace, 4, request, pid, addr, data);
if (res >= 0 && request > 0 && request < 4)
{
__set_errno (0);
return ret;
}
return res;
}
|
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
|
// glibc/sysdeps/unix/sysv/linux/aarch64/sysdep.h
INLINE_SYSCALL (ptrace, 4, request, pid, addr, data);
# define INLINE_SYSCALL(name, nr, args...) \
({ unsigned long _sys_result = INTERNAL_SYSCALL (name, , nr, args); \
if (__builtin_expect (INTERNAL_SYSCALL_ERROR_P (_sys_result, ), 0))\
{ \
__set_errno (INTERNAL_SYSCALL_ERRNO (_sys_result, )); \
_sys_result = (unsigned long) -1; \
} \
(long) _sys_result; })
# define INTERNAL_SYSCALL(name, err, nr, args...) \
INTERNAL_SYSCALL_RAW(SYS_ify(name), err, nr, args)
#define SYS_ify(syscall_name) (__NR_##syscall_name)
# define INTERNAL_SYSCALL_RAW(name, err, nr, args...) \
({ long _sys_result; \
{ \
LOAD_ARGS_##nr (args) \
register long _x8 asm ("x8") = (name); \
asm volatile ("svc 0 // syscall " # name \
: "=r" (_x0) : "r"(_x8) ASM_ARGS_##nr : "memory"); \
_sys_result = _x0; \
} \
_sys_result; })
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
|
// arch/arm64/kernel/sys.c
const syscall_fn_t sys_call_table[__NR_syscalls] = {
[0 ... __NR_syscalls - 1] = __arm64_sys_ni_syscall,
#include <asm/unistd.h>
};
// include/uapi/asm-generic/unistd.h
/* kernel/ptrace.c */
#define __NR_ptrace 117
__SYSCALL(__NR_ptrace, sys_ptrace)
//对于 ptrace,最终会指向 kernel/ptrace.c 中定义的 sys_ptrace 函数
SYSCALL_DEFINE4(ptrace, long, request, long, pid, unsigned long, addr,
unsigned long, data)
{
...
}
|
flowchart LR
A["用户程序调用 ptrace()"] --> B["glibc 封装函数<br>处理可变参数"]
B --> C["执行软中断 / 专用指令<br>(如 int $0x80 / syscall)"]
C --> D["CPU 切换至内核态<br>根据系统调用号查找表"]
D --> E["内核系统调用入口<br>(如 kernel/ptrace.c 中的 sys_ptrace)"]
E --> F["执行具体功能分支<br>(如 PTRACE_ATTACH)"]
F --> G["调用内核内部函数<br>(如 ptrace_attach)"]
strace - trace system calls and signals
strace (1) - Linux manual page - man7.org
https://github.com/strace/strace/blob/master/src/strace.c
strace 是 ptrace 最著名的上层封装。它拦截并记录进程发起的所有系统调用(syscall),包括参数、返回值和执行时间。
strace 工作流程
graph TD
A[ptrace] --> B{选择模式}
B --> C[PTRACE_ATTACH attach模式]
B --> D[strace启动]
C --> E[执行tracer逻辑]
E --> F[wait]
F --> G[系统调用?]
G --> H[ptrace PTRACE_SYSCALL]
H --> I[wait]
I --> J[等待系统调用结束]
J --> K[屏幕输出]
D --> L[父进程 fork]
L --> M[子进程]
L --> N[父进程]
M --> O[ptrace PTRACE_TRACEME]
O --> P[execv 执行命令]
N --> Q[wait]
Q --> R[等待system call执行]
R --> S[ptrace PTRACE_SYSCALL]
S --> T[wait]
T --> U[等待system call结束]
U --> V[屏幕输出]
K --> W[停止trace?]
V --> W
W --> X[exit?]
strace option
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
26
27
28
|
strace -h
## Startup
Attaches to the process with the process ID pid and begin tracing
-p pid
-p "$(pidof PROG)" ; -p "$(pidof PROG)"
-E var=val Runs the command with the environment variable var=val set for execution
## Tracing
-f Traces child processes as they are created by currently traced processes as a result of the fork(), vfork() and clone() system calls.
### 追踪子进程(fork/vfork/clone)
strace -f gcc -c main.c
## Filtering
-e expr
### 只追踪特定的系统调用(过滤器)
strace -e trace=open,read,write ls
### 追踪所有以 open 开头的系统调用
strace -e trace=%file ls
### 追踪网络相关 syscall
strace -e trace=%network curl https://example.com
## Output format
-o filename
### 将输出保存到文件
strace -o trace.log ls
## Statistics
-c Counts time, calls, and errors
|
实战案例:诊断 “No such file” 问题
1
2
|
# 假设程序启动时报错找不到文件
strace -e trace=open,openat,stat,fstat,access ./myapp 2>&1 | grep -i "ENOENT"
|
输出示例:
1
|
openat(AT_FDCWD, "/opt/myapp/config.yaml", O_RDONLY) = -1 ENOENT (No such file or directory)
|
实战案例:分析性能瓶颈
1
|
strace -c -p $(pgrep my_server)
|
1
2
3
4
5
6
7
8
|
% time seconds usecs/call calls errors syscall
------ ----------- ----------- --------- --------- ----------------
45.32 0.423452 2117 200 poll
32.18 0.300123 150 200 read
12.45 0.116234 58 200 write
10.05 0.093812 93 100 accept4
------ ----------- ----------- --------- --------- ----------------
100.00 0.933621 700 700 0 total
|
如果 poll 占用 45% 的时间且调用次数不多但每次耗时很长,通常说明程序在等待 I/O 事件——可能是 epoll 超时设置太长。
strace 的代价
strace 会使目标进程的运行速度下降 10~100 倍。原因:
flowchart LR
subgraph Normal [正常执行]
A1[用户态代码] -->|syscall| B1[内核]
B1 -->|返回| A1
end
subgraph Strace [strace 下]
A2[用户态代码] -->|syscall| C[内核会暂停]
C --> D[发送 SIGTRAP]
D --> E[strace 进程被调度]
E --> F[读取 tracee 寄存器]
F --> G[strace 打印到终端]
G -->|PTRACE_SYSCALL| C
C -->|syscall 执行| C
C --> D2[再次发送 SIGTRAP]
D2 --> E2[strace 读取返回值]
E2 -->|PTRACE_CONT| A2
end
每个系统调用从2 次上下文切换变成 4 次以上的上下文切换 + 额外的 tracer 解码工作。生产环境慎用!
ltrace - A library call tracer
ltrace (1) - Linux manual page - man7.org
https://github.com/dkogan/ltrace
ltrace就是library trace,相比于strace,可以追踪用户态的动态库函数。原理也是ptrace。那为什么ltrace可以追踪动态库函数呢?程序调用动态库函数需要走ld-linux.so(linux的动态链接器),ltrace会劫持这个跳转过程,从而记录动态库函数信息。
ltrace 拦截并记录进程对共享库(.so)的函数调用,例如 malloc、free、printf、pthread_create 等。
工作原理
ltrace 的核心机制与 strace 不同——它依赖动态链接器的符号解析机制,通过在 PLT(Procedure Linkage Table)条目上设断点来拦截库函数调用。
flowchart TD
A[目标程序] -->|调用 printf| B[PLT 条目]
B -->|首次调用?| C{已解析?}
C -->|否| D["动态链接器\nld-linux.so"]
D -->|解析符号地址| E[GOT 表]
E -->|写入实际地址| B
C -->|是| F[实际函数]
subgraph ltrace介入
L1["ltrace 在 PLT/GOT 处\n设 PTRACE_POKEDATA 断点"]
L2["断点触发 → ltrace\n读取参数"]
L3["恢复执行 → 断点再触发\n读取返回值"]
end
B --> L1
L1 --> L2
L2 --> F
F --> L3
具体步骤:
-
ltrace 通过 ptrace(PTRACE_TRACEME) 或 PTRACE_ATTACH 附加到目标进程
-
它读取目标进程的 ELF 动态符号表(.dynsym 节)和 PLT 表
-
对每个要追踪的库函数,在对应的 GOT/PLT 入口处插入断点(即写入 int3 指令 → 0xCC)
-
当目标进程调用该函数时,触发断点 → ltrace 暂停,读取参数
-
ltrace 将断点临时移除,单步执行真正的函数,再重新下断点来捕获返回值
ltrace 的追踪范围
| 能追踪 |
不能追踪 |
通过 PLT 调用的共享库函数(printf, malloc 等) |
内联函数(编译时被展开) |
显式 dlsym() 查找的函数调用 |
静态链接的函数 |
| C++ 的虚函数(通过 vtable 间接调用)——部分支持 |
不经过 PLT 的内部调用 |
系统调用(通过退化为 strace 模式 -S) |
汇编级别的直接 syscall(syscall/sysenter 指令) |
常用命令与实战
基本使用
1
2
3
4
5
6
7
8
|
# 追踪 ls 的库函数调用
ltrace ls
# 只看 C 标准库调用
ltrace -e "libc.so*" ls
# 只看 malloc/free
ltrace -e malloc+free ls
|
输出:
1
2
3
4
5
6
7
8
9
10
11
|
ls->strlen(".") = 1
ls->strlen("..") = 2
ls->opendir(".") = 0x55a8...
ls->readdir(0x55a8...) = 0x55a8...
ls->strlen("file1.txt") = 10
ls->__errno_location() = 0x7f...
ls->malloc(4096) = 0x55a9...
ls->readdir(0x55a8...) = NULL
ls->closedir(0x55a8...) = 0
...
+++ exited (status 0) +++
|
混合追踪——库函数 + 系统调用
1
2
|
# -S 让 ltrace 同时显示系统调用
ltrace -S ls 2>&1 | head -20
|
输出同时包含 SYS_xxx(系统调用)和普通库函数:
1
2
3
4
5
6
7
8
9
10
11
|
libc_start_main([0x...]) = 0x...
SYS_brk(NULL) = 0x55...
SYS_access("/etc/ld.so.preload", 4) = -2
SYS_openat(0, 0x7f..., 0x20000, 0, 0) = 3
SYS_fstat(3, 0x7ff...) = 0
SYS_mmap(0, 0x220c0, 1, 2050, 3, 0) = 0x7f...
SYS_close(3) = 0
...
strlen(".") = 1
SYS_getdents64(3, 0x55a..., 32768) = 112
...
|
按库或函数名过滤
1
2
3
4
5
6
7
8
9
10
11
|
# 只追踪 libc 中的调用
ltrace -e "libc.so*" ./myapp
# 只追踪数学库的函数
ltrace -e "libm.so*" ./compute
# 排除某些函数(不追踪 putchar)
ltrace -e "!putchar" ./myapp
# 只显示函数名,不显示参数和返回值
ltrace -n 2 ./myapp
|
实战案例:诊断内存泄漏
1
2
|
# 统计 malloc/free/realloc 调用次数和内存分配量
ltrace -e malloc+free+realloc -c ./leaky_app
|
1
2
3
4
5
6
|
% time seconds usecs/call calls function
------ ----------- ----------- --------- --------------------
60.32 0.523412 132 3965 malloc
39.68 0.344123 87 3955 free
------ ----------- ----------- --------- --------------------
100.00 0.867535 7920 total
|
如果 malloc 比 free 多很多次,基本可以断定有内存泄漏。
实战案例:排查在哪次库调用中崩溃
1
2
3
4
5
|
# 追踪并显示调用栈
ltrace -n 3 -o trace.log ./crashy_app
# -n 3 显示 3 层调用深度
# -o 输出到文件
|
ltrace 的局限
- 仅适用于动态链接的程序——静态链接的程序没有 PLT,ltrace 无法工作
- 无法追踪不经过 PLT 的函数调用(如
static 函数、内联函数、宏)
- 对多线程程序的支持不如 strace 完善(
-f 选项在 ltrace 中较新,仍有边缘情况)
- 断点机制本身会引入额外延迟(每次调用多出 2 次 trap)
- 较新版本的 glibc 使用
VDSO(虚拟动态共享对象)实现的系统调用(如 gettimeofday)可能跳过了 PLT
ftrace - function trace
function trace是Linux内核提供的trace框架。主要用来追踪linux内核函数的执行流程。ftrace的原理是编译器会在编译内核时给所有函数预埋探针指令,不使用时零开销,需要使用时探针指令走到ftrace的逻辑。所以,从原理上看ftrace也是只能对内核函数做操作和kprobe差不多,但是没有kprobe灵活。
ftrace 能帮我们分析内核特定的事件,譬如调度,中断等,也能帮我们去追踪动态的内核函数,以及这些函数的调用栈还有栈的使用这些。它也能帮我们去追踪延迟,譬如中断被屏蔽,抢占被禁止的时间,以及唤醒一个进程之后多久开始执行的时间。
ftrace - Function Tracer — The Linux Kernel documentation
ftrace.c - kernel/trace/ftrace.c - Linux source code v7.1.3 - Bootlin Elixir Cross Referencer
工作原理
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
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
|
// init/main.c
asmlinkage __visible void __init __no_sanitize_address start_kernel(void)
{
...
trap_init();
mm_init(); // 内存分配器就绪
poking_init();
ftrace_init(); // ← 这里
/* trace_printk can be enabled here */
early_trace_init();
sched_init(); // 调度器初始化
...
}
// kernel/trace/ftrace.c
void __init ftrace_init(void)
{
extern unsigned long __start_mcount_loc[];
extern unsigned long __stop_mcount_loc[];
unsigned long count, flags;
int ret;
local_irq_save(flags);
ret = ftrace_dyn_arch_init(); // 架构相关的动态 ftrace 初始化
local_irq_restore(flags);
if (ret) goto failed;
count = __stop_mcount_loc - __start_mcount_loc;
// ↑ 链接器生成的段,记录了所有 -pg 编译时插入的 mcount 调用点
if (!count) { ... goto failed; }
last_ftrace_enabled = ftrace_enabled = 1; // 启用 ftrace
ret = ftrace_process_locs(NULL,
__start_mcount_loc,
__stop_mcount_loc);
// ↑ 将每个 mcount 调用点的地址记录到动态 ftrace 的哈希表中,
// 并将它们初始化为 nop (空操作)
set_ftrace_early_filters(); // 应用内核启动参数指定的早期 filter
return;
failed:
ftrace_disabled = 1; // 失败则禁用 ftrace
}
|
ftrace_init() 的本质工作就是:把内核中所有函数的动态跟踪入口从 call 替换为 nop,让内核在默认情况下零开销运行,同时准备好数据结构,使得运行时随时可以通过写 tracefs 文件动态激活任意函数的跟踪
当内核用 -pg 或 -mfentry 编译时,每个函数的第一条指令都会被编译器插入一个调用; 如果不编译 ftrace → 函数入口没有这个 call
1
2
3
|
func:
call __fentry__ # 或者 call mcount
<函数实际代码>
|
这个call __fentry__ 是所谓的动态跟踪入口——它是一个空壳,用来让 ftrace “挂钩子
启动时 ftrace_init() 做的事情就是把这些 call 全部替换成 nop:
1
2
3
4
5
6
7
|
初始状态(编译后):
func:
call __fentry__ ← 5字节调用指令
ftrace_init 后(默认状态):
func:
nop; nop; nop; nop; nop ← 5字节空操作
|
这样默认情况下内核运行时没有任何额外开销——每个函数入口就是 5 个 nop
当用户启用跟踪时(例如 echo do_sys_open > /sys/kernel/tracing/set_ftrace_filter):
1
2
3
4
5
6
|
// ftrace 内部用 stop_machine 将指定函数的 nop 替换回 call
write_cr3(...) // 同步所有 CPU
func:
call ftrace_caller ← 再次 patch 成 call,指向 ftrace 框架
<函数实际代码>
|
这次 call 指向的是 ftrace_caller,它负责调用注册的回调函数(比如记录函数调用日志、追踪器、kprobe 等)。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
|
# sudo /lib/modules/6.8.0-134-generic/build/scripts/extract-vmlinux /boot/vmlinuz-6.8.0-134-generic > /tmp/vmlinux
# sudo grep -w schedule /boot/System.map-6.8.0-134-generic
ffffffff8225d4e0 T schedule
# objdump -d --start-address=0xffffffff8225d4e0 --stop-address=0xffffffff8225d510 /tmp/vmlinux
ffffffff8225d4e0: e8 2b cc e5 fe call 0xffffffff810ba110
# grep -w ffffffff810ba110 /boot/System.map-6.8.0-134-generic
ffffffff810ba110 T __fentry__
# grep FUNCTION_TRACER /boot/config-6.8.0-134-generic
# cat /proc/sys/kernel/ftrace_enabled
1
# sudo ls -d /sys/kernel/tracing/trace
/sys/kernel/tracing/trace
# sudo ls -d /sys/kernel/debug/tracing
/sys/kernel/debug/tracing
# mount |grep tracefs
tracefs on /sys/kernel/tracing type tracefs (rw,nosuid,nodev,noexec,relatime)
tracefs on /sys/kernel/debug/tracing type tracefs (rw,nosuid,nodev,noexec,relatime)
# trace-cmd show
|
flowchart LR
subgraph 编译时
A[源码] -->|gcc -pg| B["每个函数入口插入\ncall __fentry__"]
B --> C[编译后的内核镜像]
end
subgraph 启动时
C --> D["ftrace init: 将\n所有 __fentry__ 替换为 nop"]
D --> E["运行中的内核\n零开销"]
end
subgraph 追踪时
F[echo function > current_tracer]
F --> G["ftrace 将匹配函数的\nnop → call ftrace_caller"]
G --> H["ftrace_caller 收集\nPC + 时间戳"]
H --> I[写入 trace ring buffer]
end
E --> F
ftrace 的源码
ftrace 的源码位于内核目录 kernel/trace/ 下,核心文件包括:
| 文件 |
作用 |
kernel/trace/ftrace.c |
动态插桩的核心——nop↔call __fentry__ 的重写 |
kernel/trace/trace_functions.c |
function tracer 的实现 |
kernel/trace/trace_functions_graph.c |
function_graph tracer 的实现 |
kernel/trace/ring_buffer.c |
无锁环形缓冲区 |
kernel/trace/trace_events.c |
tracepoint 事件系统 |
include/linux/ftrace.h |
ftrace API 头文件 |
常用命令与实战
ftrace 的接口在 /sys/kernel/tracing/ 下。在 4.1 之前,所有 ftrace 追踪控制文件都位于 debugfs 文件系统中,该文件系统通常位于 /sys/kernel/debug/tracing。 为了向后兼容,当挂载 debugfs 文件系统时,tracefs 文件系统将自动挂载/sys/kernel/debug/tracing,位于 tracefs 文件系统中的所有文件也将位于该 debugfs 文件系统目录中.
关键文件:
| 文件 |
作用 |
available_tracers |
列出可用的 tracer |
current_tracer |
设置/查看当前 tracer |
available_filter_functions |
可追踪的内核函数列表 |
set_ftrace_filter |
过滤要追踪的函数 |
set_ftrace_notrace |
排除某些函数 |
trace |
读取追踪结果 |
tracing_on |
开关追踪(1/0) |
buffer_size_kb |
缓冲区大小 |
trace_stat/ |
函数调用统计 |
1
2
3
4
5
6
7
8
9
10
11
12
13
|
root@ubuntu24:/sys/kernel/debug/tracing# cat trace
# tracer: nop
#
# entries-in-buffer/entries-written: 0/0 #P:2
#
# _-----=> irqs-off/BH-disabled
# / _----=> need-resched
# | / _---=> hardirq/softirq
# || / _--=> preempt-depth
# ||| / _-=> migrate-disable
# |||| / delay
# TASK-PID CPU# ||||| TIMESTAMP FUNCTION
# | | | ||||| | |
|
基础使用:追踪内核函数调用
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
|
# 进入 ftrace 目录
cd /sys/kernel/tracing
# 查看可用的 tracer
cat available_tracers
# 输出: function function_graph nop ...
# 启用 function tracer
echo function > current_tracer
# 开启追踪
echo 1 > tracing_on
# 执行一个操作(触发内核调用)
ls -la /tmp > /dev/null
# 关闭追踪
echo 0 > tracing_on
# 查看追踪结果
cat trace | head -30
|
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
26
27
28
29
30
31
|
root@ubuntu24:/sys/kernel/debug/tracing# cat trace | head -30
# tracer: function
#
# entries-in-buffer/entries-written: 102555/16376050 #P:2
#
# _-----=> irqs-off/BH-disabled
# / _----=> need-resched
# | / _---=> hardirq/softirq
# || / _--=> preempt-depth
# ||| / _-=> migrate-disable
# |||| / delay
# TASK-PID CPU# ||||| TIMESTAMP FUNCTION
# | | | ||||| | |
tail-455584 [000] ...1. 609848.244283: next_uptodate_folio <-filemap_map_pages
tail-455584 [000] ...1. 609848.244283: set_pte_range <-filemap_map_pages
tail-455584 [000] ...1. 609848.244283: folio_add_file_rmap_ptes <-set_pte_range
tail-455584 [000] ...1. 609848.244283: next_uptodate_folio <-filemap_map_pages
tail-455584 [000] ...1. 609848.244283: set_pte_range <-filemap_map_pages
tail-455584 [000] ...1. 609848.244283: folio_add_file_rmap_ptes <-set_pte_range
tail-455584 [000] ...1. 609848.244283: next_uptodate_folio <-filemap_map_pages
tail-455584 [000] ...1. 609848.244284: set_pte_range <-filemap_map_pages
tail-455584 [000] ...1. 609848.244284: folio_add_file_rmap_ptes <-set_pte_range
tail-455584 [000] ...1. 609848.244284: next_uptodate_folio <-filemap_map_pages
tail-455584 [000] ...1. 609848.244284: set_pte_range <-filemap_map_pages
tail-455584 [000] ...1. 609848.244284: folio_add_file_rmap_ptes <-set_pte_range
tail-455584 [000] ...1. 609848.244284: next_uptodate_folio <-filemap_map_pages
tail-455584 [000] ...1. 609848.244284: _raw_spin_unlock <-filemap_map_pages
tail-455584 [000] ..... 609848.244284: __rcu_read_unlock <-filemap_map_pages
tail-455584 [000] ..... 609848.244284: __rcu_read_unlock <-filemap_map_pages
tail-455584 [000] ..... 609848.244284: __rcu_read_unlock <-do_read_fault
tail-455584 [000] ..... 609848.244285: __rcu_read_lock <-handle_mm_fault
|
每行格式:进程名-PID [CPU] 标志 时间戳: 函数名 <- 调用者
function_graph —— 可视化调用链
1
2
3
4
5
|
echo function_graph > current_tracer
echo 1 > tracing_on
ls /tmp > /dev/null
echo 0 > tracing_on
cat trace | head -40
|
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
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
|
root@ubuntu24:/sys/kernel/debug/tracing# cat trace | head -40
# tracer: function_graph
#
# CPU DURATION FUNCTION CALLS
# | | | | | | |
1) 1.328 us | mutex_unlock();
1) 0.180 us | syscall_exit_to_user_mode_prepare();
1) 0.173 us | fpregs_assert_state_consistent();
1) | x64_sys_call() {
1) | __x64_sys_dup2() {
1) | ksys_dup3() {
1) 0.162 us | _raw_spin_lock();
1) 0.159 us | expand_files();
1) | do_dup2() {
1) 0.167 us | _raw_spin_unlock();
1) | filp_close() {
1) | filp_flush() {
1) 0.158 us | dnotify_flush();
1) 0.181 us | locks_remove_posix();
1) 0.786 us | }
1) | fput() {
1) | task_work_add() {
1) 0.159 us | kick_process();
1) 0.478 us | }
1) 0.789 us | }
1) 2.024 us | }
1) 2.649 us | }
1) 3.569 us | }
1) 3.871 us | }
1) 4.179 us | }
1) 0.159 us | syscall_exit_to_user_mode_prepare();
1) | task_work_run() {
1) 0.173 us | _raw_spin_lock_irq();
1) 0.165 us | _raw_spin_unlock_irq();
1) | ____fput() {
1) | __fput() {
1) 0.158 us | __cond_resched();
1) 0.163 us | locks_remove_file();
1) 0.158 us | ima_file_free();
1) | mutex_lock() {
1) 0.159 us | __cond_resched();
|
缩进表示调用深度,DURATION 列是函数执行时间。
按函数名过滤
1
2
3
4
5
6
7
8
9
10
11
12
|
# 只追踪与 ext4 文件系统相关的函数
echo ext4_* > set_ftrace_filter
# 确认生效
cat set_ftrace_filter
# 启用追踪
echo function > current_tracer
echo 1 > tracing_on
cp bigfile /tmp/
echo 0 > tracing_on
cat trace | head -30
|
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
26
27
28
29
30
31
|
root@ubuntu24:/sys/kernel/debug/tracing# cat trace | head -30
# tracer: function
#
# entries-in-buffer/entries-written: 178/178 #P:2
#
# _-----=> irqs-off/BH-disabled
# / _----=> need-resched
# | / _---=> hardirq/softirq
# || / _--=> preempt-depth
# ||| / _-=> migrate-disable
# |||| / delay
# TASK-PID CPU# ||||| TIMESTAMP FUNCTION
# | | | ||||| | |
bash-454555 [001] ..... 610139.068645: ext4_file_getattr <-vfs_getattr_nosec
bash-454555 [001] ..... 610139.068646: ext4_getattr <-ext4_file_getattr
bash-454555 [001] ..... 610139.068647: ext4_file_getattr <-vfs_getattr_nosec
bash-454555 [001] ..... 610139.068647: ext4_getattr <-ext4_file_getattr
bash-454555 [001] ..... 610139.068651: ext4_file_getattr <-vfs_getattr_nosec
bash-454555 [001] ..... 610139.068651: ext4_getattr <-ext4_file_getattr
cp-457239 [000] ..... 610139.069206: ext4_file_open <-do_dentry_open
cp-457239 [000] ..... 610139.069206: ext4_sample_last_mounted <-ext4_file_open
cp-457239 [001] ..... 610139.069914: ext4_file_read_iter <-__kernel_read
cp-457239 [001] ..... 610139.069921: ext4_file_read_iter <-__kernel_read
cp-457239 [001] ..... 610139.069922: ext4_file_read_iter <-__kernel_read
cp-457239 [001] ..... 610139.069929: ext4_file_open <-do_dentry_open
cp-457239 [001] ..... 610139.069930: ext4_sample_last_mounted <-ext4_file_open
cp-457239 [001] ..... 610139.069931: ext4_file_read_iter <-__kernel_read
cp-457239 [001] ..... 610139.069932: ext4_file_read_iter <-__kernel_read
cp-457239 [001] ..... 610139.069934: ext4_xattr_security_get <-__vfs_getxattr
cp-457239 [001] ..... 610139.069934: ext4_xattr_get <-ext4_xattr_security_get
cp-457239 [001] ..... 610139.069935: ext4_xattr_ibody_get <-ext4_xattr_get
|
tracepoint 事件追踪
ftrace 也支持预定义的 tracepoint 事件(性能远高于动态函数追踪):
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
|
# 查看可用的事件
ls /sys/kernel/tracing/events/ | head -20
# output: block irq kmem net sched syscalls ...
# 启用系统调用 tracepoint
echo 1 > events/syscalls/sys_enter_openat/enable
echo 1 > events/syscalls/sys_exit_openat/enable
# 追踪
echo 1 > tracing_on
cat /etc/passwd > /dev/null
echo 0 > tracing_on
# 查看
cat trace | tail -10
|
1
2
|
bash-454555 [001] ...1. 610338.749840: sys_openat(dfd: ffffff9c, filename: 64ac99b1a560, flags: 241, mode: 1b6)
bash-454555 [001] ...1. 610338.749857: sys_openat -> 0x3
|
实战案例:排查内核函数频繁调用导致的性能问题
1
2
3
4
5
6
7
8
9
|
# 统计函数调用次数(类似 strace -c 的内核版本)
echo nop > current_tracer
echo 1 > /sys/kernel/tracing/events/syscalls/sys_enter_write/enable
# 等待一段时间
sleep 5
# 查看计数
cat /sys/kernel/tracing/per_cpu/cpu0/stats
|
1
2
3
4
5
6
7
8
9
|
root@ubuntu24:/sys/kernel/debug/tracing# cat /sys/kernel/tracing/per_cpu/cpu0/stats
entries: 0
overrun: 0
commit overrun: 0
bytes: 0
oldest event ts: 610338.745551
now ts: 610533.149505
dropped events: 0
read events: 0
|
或者使用 trace_stat:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
|
# 1. 设置 tracer 为 function
echo function > /sys/kernel/tracing/current_tracer
# 2. 开启 function profiler(这一步会重置计数器)
echo 1 > /sys/kernel/tracing/function_profile_enabled
# 3. 开始追踪
echo 1 > /sys/kernel/tracing/tracing_on
# --- 运行您的测试负载 ---
# 例如: ./your_benchmark_tool
# 或者简单的压力测试: stress-ng --cpu 4 --timeout 5s
stress-ng --cpu 4 --timeout 5s
# 4. 停止追踪
echo 0 > /sys/kernel/tracing/tracing_on
# 5. 关闭 profiler
echo 0 > /sys/kernel/tracing/function_profile_enabled
# 6. 查看函数调用统计排名
cat /sys/kernel/tracing/trace_stat/functions
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
|
root@ubuntu24:/sys/kernel/debug/tracing# cat /sys/kernel/tracing/trace_stat/function0
Function Hit Time Avg s^2
-------- --- ---- --- ---
root@ubuntu24:/sys/kernel/debug/tracing# cat /sys/kernel/tracing/trace_stat/function1
Function Hit Time Avg s^2
-------- --- ---- --- ---
ext4_lookup 4 125.270 us 31.317 us 522.599 us
ext4_dx_find_entry 2 96.880 us 48.440 us 53.892 us
ext4_search_dir 4 86.536 us 21.634 us 530.259 us
ext4_match 286 62.061 us 0.216 us 0.001 us
ext4_getblk 6 18.148 us 3.024 us 5.078 us
ext4_bread_batch 2 10.719 us 5.359 us 16.791 us
ext4_map_blocks 6 9.737 us 1.622 us 1.034 us
ext4_bread 4 9.589 us 2.397 us 0.034 us
ext4_fname_prepare_lookup 4 4.269 us 1.067 us 0.250 us
ext4_inode_block_valid 6 4.054 us 0.675 us 0.186 us
ext4_es_lookup_extent 6 2.444 us 0.407 us 0.046 us
ext4_sb_block_valid 6 2.021 us 0.336 us 0.068 us
ext4_fname_free_filename 4 1.140 us 0.285 us 0.015 us
ext4_fname_from_fscrypt_name 4 1.016 us 0.254 us 0.005 us
ext4_fname_setup_ci_filename 4 0.897 us 0.224 us 0.003 us
ext4_htree_next_block 2 0.478 us 0.239 us 0.000 us
|
实战案例:追踪内核模块初始化
1
2
3
4
5
6
7
|
# 只追踪某个模块的函数
echo :mod:ext4 > set_ftrace_filter
echo function_graph > current_tracer
echo 1 > tracing_on
mount /dev/sdb1 /mnt
echo 0 > tracing_on
cat trace | head -20
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
|
# tracer: function_graph
#
# CPU DURATION FUNCTION CALLS
# | | | | | | |
1) | /* sys_openat(dfd: ffffff9c, filename: 716a83a5938f, flags: 80000, mode: 0) */
1) | /* sys_openat -> 0x3 */
1) | /* sys_openat(dfd: ffffff9c, filename: 716a83a29140, flags: 80000, mode: 0) */
1) | /* sys_openat -> 0x3 */
1) | /* sys_openat(dfd: ffffff9c, filename: 716a83a29690, flags: 80000, mode: 0) */
1) | /* sys_openat -> 0x3 */
1) | /* sys_openat(dfd: ffffff9c, filename: 716a83a29be0, flags: 80000, mode: 0) */
1) | /* sys_openat -> 0x3 */
1) | /* sys_openat(dfd: ffffff9c, filename: 716a83a2a110, flags: 80000, mode: 0) */
1) | /* sys_openat -> 0x3 */
1) | /* sys_openat(dfd: ffffff9c, filename: 716a83a2a6b0, flags: 80000, mode: 0) */
1) | /* sys_openat -> 0x3 */
1) | /* sys_openat(dfd: ffffff9c, filename: 716a839cd1db, flags: 80000, mode: 0) */
1) | /* sys_openat -> 0x3 */
1) | /* sys_openat(dfd: ffffff9c, filename: 716a837d48b0, flags: 80000, mode: 0) */
1) | /* sys_openat -> 0xfffffffffffffffe */
|
ftrace 的优势与局限
优势:
- 零用户态开销——所有工作在内核态完成,没有
ptrace 的上下文切换
- 无侵入——动态函数插桩,不干扰目标函数执行
- 支持生产环境——可在负载下运行(但需注意 ring buffer 溢出)
- 追踪整个内核——不仅是用户进程的上下文,还包括中断、软中断、调度等
- function_graph 模式——天然支持调用链可视化
局限:
- 只能追踪内核态——不能观察用户态代码
- 使用门槛高——通过 tracefs文件系统操作,需要了解内核函数名和子系统
- function tracer 的开销——每秒数万次函数调用时仍会产生 ~5-10% 的性能损失
- ring buffer 溢出——高负载下可能丢失事件(
overwrite 模式)
- 调试能力有限——不能像 ptrace 那样读/写内存和寄存器
综合调试案例
一个 HTTP 服务器响应慢
假设有一个 http_server 响应缓慢,我们进行分层排查:
Step 1: 整体概览 — strace -c
1
|
strace -c -p $(pgrep http_server | head -1)
|
结果:
1
2
3
4
5
|
% time seconds usecs/call calls errors syscall
------ ----------- ----------- --------- --------- ----------------
90.12 8.123456 40123 202 poll
5.23 0.471234 123 382 writev
4.65 0.419001 110 380 readv
|
→ 90% 的时间花在 poll 上,且每次平均 40ms → 可能在等待 I/O 超时。
Step 2: 查看 poll 的细节
1
|
strace -e trace=poll -p $(pgrep http_server | head -1) -ttt 2>&1
|
1
2
3
4
|
1728000001.123456 poll([{fd=4, events=POLLIN}], 1, 50000) = 1
1728000001.123512 poll([{fd=4, events=POLLIN}], 1, 50000) = 1
1728000001.125678 poll([{fd=4, events=POLLIN}], 1, 50000) = 0
1728000001.175678 poll([{fd=4, events=POLLIN}], 1, 50000) = 1
|
观察发现:poll 超时设置为 50ms,有些调用确实等到超时返回了 = 0(没有事件)→ 说明上游请求不够频繁,或 keepalive 空转。
Step 3: 查看内存分配模式
1
|
ltrace -e malloc+free -c -p $(pgrep http_server | head -1)
|
1
2
3
4
|
% time seconds usecs/call calls function
------ ----------- ----------- --------- --------------------
55.32 0.523412 132 3965 malloc
44.68 0.344123 87 3955 free
|
→ malloc/free 数量接近,不太像泄漏;但调用次数多(4000+),可以考虑引入对象池。
Step 4: 查看内核态行为 — ftrace
如果怀疑是内核调度或文件系统层面的问题:
1
2
3
4
5
6
7
8
9
|
cd /sys/kernel/tracing
# 追踪 http_server 相关的系统调用内核路径
echo do_sys_poll > set_ftrace_filter
echo function_graph > current_tracer
echo 1 > tracing_on
# ... 复现问题 ...
echo 0 > tracing_on
cat trace | grep "http_serv" | head -30
|
用 strace 排查"命令找不到”
1
2
3
4
5
|
$ which some_cmd
/usr/local/bin/some_cmd
$ some_cmd
bash: some_cmd: command not found # ???
|
用 strace 追踪 shell:
1
|
strace -e trace=execve,stat,openat bash -c "some_cmd" 2>&1
|
1
2
3
4
5
6
7
|
execve("/usr/local/bin/some_cmd", ...) = -1 ENOEXEC
execve("/bin/some_cmd", ...) = -1 ENOENT
openat(AT_FDCWD, "/usr/local/bin/some_cmd", O_RDONLY) = 3
read(3, "#! /usr/bin/python3\n", 80) = 23
close(3)
execve("/usr/local/bin/some_cmd", ...) = -1 ENOEXEC
...
|
→ 原来是写错了,脚本开头是 #! /usr/bin/python3(多了空格)而不是正确的 #!/usr/bin/python3,导致内核不识别为解释器脚本。
用 ltrace 定位 crash 前的最后一次库调用
1
2
3
4
5
|
# 程序运行时突然崩溃
ltrace -n 5 -o /tmp/ltrace.log ./myapp
# 查看日志尾部
tail -20 /tmp/ltrace.log
|
1
2
3
4
5
|
myapp->free(0x55a8...) = <void>
myapp->strlen("data") = 4
myapp->malloc(512) = 0x55a9...
myapp->memcpy(0x55a9..., 0x55a8..., 1024) = 0x55a9...
--- SIGSEGV (Segmentation fault) ---
|
→ 注意到 malloc(512) 但 memcpy 拷贝了 1024 字节 ——缓冲区溢出导致在 memcpy 内部触发 SIGSEGV。
用 ftrace 排查内核调度延迟
1
2
3
4
5
6
7
8
9
10
11
12
13
14
|
cd /sys/kernel/tracing
# 追踪调度器唤醒延迟
echo 0 > tracing_on
echo > trace
echo __schedule > set_ftrace_filter
echo function_graph > current_tracer
echo 1 > tracing_on
sleep 0.1 # 触发调度
echo 0 > tracing_on
cat trace
|
输出:
1
2
3
4
5
6
7
8
|
0) 0.125 us | __schedule();
0) 0.062 us | pick_next_task_fair();
0) 0.187 us | pick_next_entity();
0) 0.125 us | set_next_entity();
0) 0.062 us | context_switch();
0) 0.187 us | switch_mm_irqs_off();
0) 0.125 us | switch_to();
0) 4.562 us | } /* __schedule */
|
可以清晰看到一次上下文切换在内核中各子函数的耗时分布。
| 维度 |
strace |
ltrace |
ftrace |
ptrace |
| 追踪粒度 |
系统调用 |
共享库函数 |
内核函数 + tracepoint |
系统调用 + 信号 + 内存 |
| 追踪层级 |
用户态边界 |
用户态 |
内核态 |
用户态/内核态边界 |
| 是否需要 ptrace |
✅ 是 |
✅ 是 |
❌ 内核内置 |
本身就是 |
| 是否需要源码 |
否 |
否 |
否(需内核符号) |
否 |
| 侵入性 |
高(进程挂起) |
高(断点指令) |
低(nop 替换) |
极高 |
| 性能影响 |
10~100x 降速 |
5~50x 降速 |
~5-10%(function tracer) |
取决于使用模式 |
| 典型场景 |
排障、审计、教学 |
内存问题、库分析 |
内核调试、性能分析 |
调试器开发、逆向 |
| 启动方式 |
命令行 |
命令行 |
tracefs + echo |
系统调用 API |
| 生产环境可用 |
❌ |
❌ |
✅ 可用(慎) |
❌ |
| 多线程支持 |
✅ -f |
⚠️ 部分 |
✅ 天然 per-CPU |
⚠️ 需额外选项 |
| 调用栈/图 |
❌ 没有 |
❌ 没有 |
✅ function_graph |
❌ 需要自行实现 |