在上一篇文章中,我们通过直接操作 /sys/kernel/debug/tracing 接口,成功排查了 kworker 高负载和软中断风暴问题。然而,纯手工操作 ftrace 的体验并不友好:频繁的 echo 命令容易出错,海量的文本日志难以分析,更别提在多核环境下进行时间维度的对比了。本文将介绍两个强大的工具——trace-cmdkernelshark,它们将彻底改变你分析 Linux 内核性能问题的方式。

为什么是 trace-cmd?

trace-cmd 本质上是 ftrace 的命令行封装,它解决了手工操作的一系列痛点。想象一下,你正在排查一个棘手的性能问题,需要同时设置多个追踪点并捕获数据。使用传统方式,你需要在不同的文件系统路径间来回切换,而 trace-cmd 只需一条命令即可完成全部操作。

手工 ftrace 的典型操作流程如下:

# 手工操作需要 10+ 步
cd /sys/kernel/debug/tracing
echo 0 > tracing_on
echo nop > current_tracer
echo > trace
echo function_graph > current_tracer
echo do_sys_open > set_graph_function
echo 1234 > set_ftrace_pid
echo 1 > events/sched/sched_switch/enable
echo 1 > tracing_on
sleep 10
echo 0 > tracing_on
cat trace > /tmp/output.txt

而使用 trace-cmd,一切变得异常简洁:

trace-cmd record -p function_graph -g do_sys_open -P 1234 -e sched:sched_switch sleep 10
trace-cmd report > /tmp/output.txt

下表直观对比了两者的差异:

特性手工 ftracetrace-cmd
命令复杂度10+ 步骤1 条命令
数据持久化手动重定向自动保存 trace.dat
数据分享难以移植二进制格式,可跨机器分析
多核分析手动区分 CPU 列自动标记 CPU 信息
图形化分析不支持集成 kernelshark
插件扩展不支持支持 Python 插件
学习成本高(需理解 ftrace 接口)低(类似 perf 命令)

安装 trace-cmd 非常简单,在大多数 Linux 发行版上都可以通过包管理器直接安装:

# Ubuntu/Debian
sudo apt install trace-cmd kernelshark
# RHEL/CentOS
sudo yum install trace-cmd kernelshark
# 验证安装
trace-cmd --version
# 输出:trace-cmd version 3.1.2
kernelshark --version
# 输出:KernelShark version 2.2.0

核心优势:trace-cmd 不仅简化了操作,更重要的是它提供了数据持久化能力。你可以将追踪结果保存为 trace.dat 文件,方便后续分析或分享给团队成员。

⚙️ trace-cmd 基础实战

掌握 trace-cmd 的核心命令是高效分析的第一步。它提供了类似 Git 的子命令风格,每个子命令都有明确的职责。例如,record 用于记录追踪数据,report 用于读取和分析数据。

核心命令一览:

trace-cmd record    # 记录 trace 数据(生成 trace.dat)
trace-cmd report    # 解析 trace.dat 并输出文本
trace-cmd show      # 查看当前 ftrace 状态
trace-cmd reset     # 重置 ftrace 配置
trace-cmd list      # 列出可用的 tracers/events
trace-cmd stat      # 显示 ftrace 统计信息

record 命令详解:这是最常用的子命令,它允许你指定要追踪的事件、进程和 CPU 核心。通过灵活的 -e 参数,你可以精确控制追踪范围,避免产生过多无关数据。

在 record 命令中,你还可以利用 -P 参数指定追踪特定进程,或者使用 -C 参数指定 CPU 核心,从而精准定位问题域。

# 基本语法
trace-cmd record [选项] [命令]
# 常用选项
-p <tracer>          # 指定 tracer(function/function_graph/nop)
  -e <event>           # 启用 event(支持通配符)
    -P <pid>             # 过滤 PID
      -g <function>        # function_graph 的函数过滤
        -l <function>        # function tracer 的函数过滤
          -F                   # 过滤当前命令的进程(自动设置 PID)
          -O <option>          # 设置 trace_options
            -b <size>            # 缓冲区大小(KB)
              -o <file>            # 输出文件名(默认 trace.dat)
                -d                   # 实时输出(不保存文件)
                -s <usecs>           # 采样间隔(微秒)

让我们通过几个实战示例来加深理解。假设你是一名 Java 开发者,正在排查一个高并发服务的性能瓶颈,你可能会这样使用 trace-cmd:

示例 1:追踪进程的系统调用

# 追踪 ls 命令的所有系统调用
trace-cmd record -e 'syscalls:*' ls /tmp
# 查看结果
trace-cmd report | less

示例 2:追踪特定函数的调用链(这在分析 C++ 或 Rust 代码时尤其有用)

# 追踪 vfs_read 的调用链
trace-cmd record -p function_graph -g vfs_read cat /etc/hosts
# 查看结果
trace-cmd report

示例 3:追踪运行中进程的调度行为(对于 Python 或 Node.js 应用同样适用)

# 追踪 PID 1234 的调度事件 10 秒
trace-cmd record -e sched:sched_switch -e sched:sched_wakeup -P 1234 sleep 10
# 查看结果
trace-cmd report | grep -E "perception|sched_switch"

示例 4:自定义输出文件

# 保存到指定文件
trace-cmd record -o network_trace.dat -e net:* -e irq:* sleep 5
# 分析
trace-cmd report -i network_trace.dat

⚠️ 注意:在追踪时务必控制事件数量,否则可能生成 GB 级别的数据文件,反而拖慢系统性能。

️ kernelshark:时间线可视化利器

如果说 trace-cmd 是瑞士军刀,那么 kernelshark 就是它的可视化驾驶舱。它提供了一个直观的图形界面,让你能够从时间维度观察多核 CPU 上发生的一切。

kernelshark 的界面主要分为几个区域:顶部是事件列表,中间是 CPU 时间线,底部是事件详情。这种布局让你既能宏观把握整体趋势,又能微观查看每个事件的细节。

┌─────────────────────────────────────────────────────────────────┐
│ 文件  编辑  过滤  工具  帮助                                      │
├─────────────────────────────────────────────────────────────────┤
│ ⏵ CPU 0  ████▓▓░░████▓▓░░████▓▓░░  sched_switch, irq_handler   │
│ ⏵ CPU 1  ░░████▓▓░░████▓▓░░████▓  sched_switch, kworker       │
│ ⏵ CPU 2  ▓▓░░████▓▓░░████▓▓░░██  sched_wakeup, net_rx        │
│ ⏵ CPU 3  ████░░▓▓████░░▓▓████░░  softirq_entry, tcp_v4_rcv   │
├─────────────────────────────────────────────────────────────────┤
│ 时间轴:  |-------|-------|-------|-------|-------|          │
│          1234.0   1234.5   1235.0   1235.5   1236.0          │
├─────────────────────────────────────────────────────────────────┤
│ Event 详情:                                                     │
│ CPU: 2  Time: 1234.567890                                       │
│ Event: net:netif_receive_skb                                    │
│ dev=eth0 len=1400 protocol=IP                                   │
└─────────────────────────────────────────────────────────────────┘

核心功能包括:多核时间线视图、事件过滤、任务跟踪、延迟测量等。具体功能如下表所示:

功能说明快捷键
缩放放大/缩小时间轴滚轮 / +/-
平移左右移动时间轴拖拽 / ←→
过滤显示/隐藏特定 eventCtrl+F
搜索查找特定事件Ctrl+S
标记标记感兴趣的时间点M
测量测量两点间的时间差右键选择
CPU 过滤只显示特定 CPU点击 CPU 行
导出导出选中区域Ctrl+E

启动 kernelshark 非常简单,只需加载 trace-cmd 生成的数据文件即可:

# 分析 trace.dat
kernelshark trace.dat
# 或者直接从 trace-cmd 启动
trace-cmd record -e 'sched:*' -e 'irq:*' sleep 5
kernelshark trace.dat

技巧:在 kernelshark 中,你可以使用鼠标拖拽选择时间范围,放大查看特定时间段的事件,这对于定位毫秒级的延迟问题至关重要。

实战案例 1:网络中断风暴引发 CPU 调度冲突

让我们通过一个真实案例来演示这两个工具的威力。在一个自动驾驶感知系统中,工程师发现系统出现间歇性延迟峰值,正常响应时间约 20ms,但有时会飙升到 150ms。通过 mpstat 观察,发现 CPU 3 的软中断占用率异常增高。

首先,使用 mpstat 确认问题现象:

CPU    %usr   %sys   %irq   %soft   %idle
  0    25.3    4.2    0.5     2.1    68.0
  1    23.8    5.1    0.4     1.8    69.0
  2    78.5    8.2    0.3     1.5    11.5   ← perception 进程
  3    12.3    3.5    2.8    65.4    16.0   ← 软中断异常!

接下来,使用 trace-cmd 记录相关事件,重点关注调度器和中断处理:

# 记录 10 秒的调度、中断、网络事件
trace-cmd record \
-e sched:sched_switch \
-e sched:sched_wakeup \
-e irq:irq_handler_entry \
-e irq:irq_handler_exit \
-e irq:softirq_entry \
-e irq:softirq_exit \
-e net:netif_receive_skb \
-b 20480 \
sleep 10
# 生成 trace.dat(约 50MB)
ls -lh trace.dat
# 输出:-rw-r--r-- 1 user user 52M Feb 13 14:23 trace.dat

然后,加载数据到 kernelshark 进行分析:

kernelshark trace.dat

在 kernelshark 的时间线视图中,我们可以清晰地看到:CPU 2 上运行的 perception 进程(一个 C++ 实现的图像处理模块)被频繁打断,而 CPU 3 上则堆满了 softirq_entry(NET_RX) 事件。每当 CPU 3 的软中断密集时,CPU 2 上的进程就被调度出去。

时间线分析结果:

时间: 1234.500s - 1234.550s(50ms 窗口)
CPU 2: [perception]────────[idle]──[perception]──[idle]──[perception]
          ^20ms            ^中断    ^5ms       ^中断    ^8ms
CPU 3: [ksoftirqd]████████████████████████████████████
       softirq    softirq  softirq  softirq  (连续 NET_RX)

通过 kernelshark 的测量工具,我们精确地测量了 perception 进程被抢占的时间:从 1234.520s 到 1234.545s,整整 25ms 的延迟,与业务层观察到的延迟峰值完全吻合。

最后,使用 trace-cmd report 进一步确认根因:

# 统计 CPU 3 的软中断频率
trace-cmd report | grep 'CPU 3.*softirq_entry' | wc -l
# 输出:145672  (10 秒内 145,672 次 = 14,567 次/秒)
# 查看网络包接收频率
trace-cmd report | grep 'netif_receive_skb' | grep 'CPU 3' | wc -l
# 输出:145672  (与软中断一致)
# 查看包大小
trace-cmd report | grep netif_receive_skb | head -5

输出结果:

ksoftirqd/3-25   [003] 1234.567: netif_receive_skb: dev=eth1 len=66
ksoftirqd/3-25   [003] 1234.568: netif_receive_skb: dev=eth1 len=66
ksoftirqd/3-25   [003] 1234.569: netif_receive_skb: dev=eth1 len=66

根因非常明确:网卡 eth1 以约 15k 包/秒的速率发送 66 字节的小包,而所有中断都被分配到了 CPU 3。这导致 CPU 3 的软中断占用率高达 65%,调度器无法及时调度其他进程。

解决方案是启用 RPS(Receive Packet Steering)来分散中断负载:

# 方案 1:分散网卡中断到多个 CPU
cat /proc/interrupts | grep eth1
# 输出:128:  12345678  ...  eth1-TxRx-0
# 将 eth1 中断分散到 CPU 0-2
echo 07 > /proc/irq/128/smp_affinity  # 二进制 0111
# 方案 2:启用 RPS(Receive Packet Steering)
echo f > /sys/class/net/eth1/queues/rx-0/rps_cpus  # 使用 CPU 0-3

优化后的效果验证:

# 再次记录 10 秒
trace-cmd record -e 'sched:*' -e 'irq:*' sleep 10
# 用 kernelshark 对比
kernelshark trace.dat

优化后的时间线显示,中断负载被均匀分散到了多个 CPU 核心,冲突明显缓解:

CPU 0: [ksoftirqd]██░░██░░  (25% softirq)
CPU 1: [ksoftirqd]██░░██░░  (25% softirq)
CPU 2: [perception]████████  (不再被中断)
CPU 3: [ksoftirqd]██░░██░░  (25% softirq)

性能对比数据如下:

指标优化前优化后改善
CPU 3 软中断65%16%↓ 75%
perception P99 延迟150ms25ms↓ 83%
调度等待时间25ms1.2ms↓ 95%

实战案例 2:跨核心延迟传播分析

第二个案例涉及一个典型的跨核心协作问题。数据融合模块(data_fusion)在 CPU 0 上处理完数据后,需要唤醒 CPU 2 上的决策模块(planning)。但 planning 的唤醒延迟从预期的 1ms 飙升到了 15ms。

使用 trace-cmd 记录唤醒路径:

# 记录调度和 IPI(核间中断)事件
trace-cmd record \
-e sched:sched_switch \
-e sched:sched_wakeup \
-e sched:sched_waking \
-e irq:irq_handler_entry \
-e irq:irq_handler_exit \
-O stacktrace \
sleep 5

在 kernelshark 的时间线中,我们可以观察到完整的唤醒链路:

  • 1234.500s:CPU 0 上的 data_fusion 调用 sched_waking 唤醒 planning
  • 1234.502s:CPU 0 发送 IPI 到 CPU 2(irq_handler_entry: reschedule
  • 1234.515s:CPU 2 上的 sched_switch 才真正切换到 planning

问题在于:CPU 2 在 1234.502s 到 1234.515s 期间到底在运行什么?kernelshark 显示,CPU 2 正在运行一个低优先级的 background_worker 任务,且已经连续运行了 13 秒。即使 planning 是 high-priority 进程(nice=-10),也等待了 13ms。

根因很快被锁定:background_worker 关闭了抢占(很可能持有了自旋锁)。

为了验证这个假设,我们检查了抢占关闭的时间点:

# 使用 function_graph 追踪 CPU 2
trace-cmd record \
-p function_graph \
-P $(pgrep background_worker) \
-g '*lock*' \
sleep 5
trace-cmd report | grep -A 10 "spin_lock"

发现结果证实了我们的猜测:

background_worker-5678 [002] 1234.502: spin_lock() {
  _raw_spin_lock();
  critical_section() {
    expensive_computation() {
      ... (13ms)
    }
  }
  _raw_spin_unlock();
}

解决方案是修复驱动代码中的锁使用问题,将自旋锁替换为可睡眠的互斥锁(适用于允许睡眠的上下文):

// 原代码(持锁时间过长)
spin_lock(&data_lock);
expensive_computation();  // 13ms!
spin_unlock(&data_lock);
// 优化后
local_data = copy_data_under_lock();  // 0.1ms
expensive_computation(local_data);    // 无锁执行
update_result_under_lock();           // 0.1ms

优化后,唤醒延迟从 15ms 降低到了 1.2ms,效果显著。

高级功能与最佳实践

除了基础功能外,trace-cmd 和 kernelshark 还提供了一些高级特性,能帮助你更高效地分析问题。

过滤器功能:当追踪数据量过大时,可以使用过滤器只保留感兴趣的事件。例如,只追踪特定进程的事件:

# 只显示特定进程的事件
trace-cmd report -F 'common_pid == 1234'
# 只显示特定 CPU
trace-cmd report -F 'common_cpu == 2'
# 组合条件
trace-cmd report -F 'common_pid == 1234 && common_cpu == 2'
# 只显示特定时间范围
trace-cmd report -S 1234.5 -E 1235.0

kernelshark 也支持强大的过滤语法,你可以根据事件类型、进程名、CPU 核心等条件进行过滤:

# 显示所有调度事件
sched:*
# 显示 CPU 2 的所有事件
cpu == 2
# 显示特定进程
comm ~ "perception"
# 组合条件
(comm ~ "perception" || comm ~ "planning") && cpu == 2

数据导出与分析:你可以将分析结果导出为文本格式,方便与其他工具(如 Python 脚本)集成进行深度分析:

# 导出为 CSV(用于 Python 分析)
trace-cmd report -F 'cpu == 3' -O csv > cpu3_events.csv
# Python 分析脚本
import pandas as pd
df = pd.read_csv('cpu3_events.csv')
print(df.groupby('event')['duration'].agg(['count', 'mean', 'max']))

以下是一些实践建议,希望能帮助你更好地使用这些工具:

# (1) 控制缓冲区大小(避免内存不足)
trace-cmd record -b 10240 ...  # 10MB per CPU
# (2) 使用事件过滤减少数据量
trace-cmd record -e 'sched:sched_switch' -F 'prev_pid == 1234 || next_pid == 1234' ...
# (3) 限制追踪时间
trace-cmd record ... sleep 10  # 最多 10 秒
# (4) 实时查看(不保存文件)
trace-cmd record -d -e 'net:*' | head -100
# (5) 压缩输出文件
trace-cmd record -o trace.dat ... && gzip trace.dat

kernelshark 的分析技巧总结:

任务操作
找到延迟峰值搜索 sched_switch,查看
测量中断处理时间标记 irq_handler_entry 和 irq_handler_exit,查看时间差
对比优化效果加载两个 trace.dat,使用 “Compare” 功能
导出关键区域选中时间范围,右键 “Export Selection”
查看调用栈记录时加 ,双击事件查看

当然,任何工具都有其性能开销,使用时需要权衡:

配置trace.dat 大小(10秒)CPU 开销
~5 MB< 1%
~20 MB2-3%
~50 MB5-8%
~100 MB10-15%
> 500 MB30-50%

总结与展望

通过本文的学习,我们掌握了 trace-cmd 和 kernelshark 这两个强大的性能分析工具。它们不仅简化了 ftrace 的操作流程,更通过可视化手段让我们能够直观地洞察多核 CPU 的复杂交互。

核心要点回顾:

  • trace-cmd 将复杂的 ftrace 操作简化为一条命令
  • kernelshark 提供直观的时间线可视化,快速定位多核问题
  • 数据持久化 功能便于团队协作和离线分析
  • ✅ 结合使用多种工具,形成完整的性能分析工具链

工具选择建议:

场景推荐工具原因
单核热点分析perf采样开销低
多核调度分析trace-cmd + kernelshark可视化时间线
中断延迟分析trace-cmd + kernelshark清晰显示中断处理流程
函数调用链分析trace-cmd function_graph完整调用树
实时监控ftrace(手工)灵活性高
生产环境eBPF + bpftrace开销最低

常用命令速查表:

# === 记录常见场景 ===
# 调度分析
trace-cmd record -e 'sched:*' sleep 10
# 中断分析
trace-cmd record -e 'irq:*' -e 'softirq:*' sleep 10
# 网络分析
trace-cmd record -e 'net:*' -e 'irq:*' sleep 10
# 块 IO 分析
trace-cmd record -e 'block:*' sleep 10
# 特定进程
trace-cmd record -e 'sched:*' -P $(pgrep perception) sleep 10
# 函数调用链
trace-cmd record -p function_graph -g vfs_read cat /etc/hosts
# === 分析与查看 ===
trace-cmd report                      # 文本输出
trace-cmd report | less               # 分页查看
trace-cmd report -F 'cpu == 2'        # 过滤 CPU
kernelshark trace.dat                 # 图形化分析
# === 管理 ===
trace-cmd reset                       # 重置 ftrace
trace-cmd show                        # 查看当前配置
trace-cmd list -e                     # 列出所有 events
[AFFILIATE_SLOT_1]

无论你是 C++ 系统工程师,还是使用 Java、TypeScript 或 Python 开发上层应用的开发者,理解底层调度行为都能帮助你写出更高效的代码。trace-cmd 和 kernelshark 正是连接上层应用与内核行为的桥梁。

下一章,我们将深入探讨系统调用阻塞分析与高并发优化,学习阻塞 IO、非阻塞 IO 和异步 IO 的性能差异,并通过实战案例解决高并发场景下的典型问题。

[AFFILIATE_SLOT_2]

Dive Deep into System Optimization - 从基础观测到高级优化,从 CPU 到存储,从内核到用户态,深入 Linux 性能分析的每一个角落。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传prev_state == D-O stacktrace-e sched:*-e 'sched:*' -e 'irq:*'-e 'sched:*' -e 'irq:*' -e 'net:*'-p function -l 'vfs_*'-p function_graph