linux trace -- ptrace strace ltrace ftrace

  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, &regs);
#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, &regs);
#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.hman 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 不是为生产环境设计的——它是调试基础设施,不是性能工具

这就是为什么后来出现了 bpftraceperfeBPF 等更现代的工具——它们在内核中直接处理事件,无需反复陷入 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)的函数调用,例如 mallocfreeprintfpthread_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

具体步骤:

  1. ltrace 通过 ptrace(PTRACE_TRACEME)PTRACE_ATTACH 附加到目标进程

  2. 它读取目标进程的 ELF 动态符号表.dynsym 节)和 PLT 表

  3. 对每个要追踪的库函数,在对应的 GOT/PLT 入口处插入断点(即写入 int3 指令 → 0xCC

  4. 当目标进程调用该函数时,触发断点 → ltrace 暂停,读取参数

  5. 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

如果 mallocfree 多很多次,基本可以断定有内存泄漏。

实战案例:排查在哪次库调用中崩溃

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 “挂钩子

  • 初始化为 nop(零开销)

启动时 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

  • 运行时动态 patch

当用户启用跟踪时(例如 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 动态插桩的核心——nopcall __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 ❌ 需要自行实现
Licensed under CC BY-NC-SA 4.0
使用 Hugo 构建
主题 StackJimmy 设计