Linux内核深度剖析:四层动态切片实战方法论

📅 2026/7/10 2:32:39
Linux内核深度剖析:四层动态切片实战方法论
1. 项目概述这不是 Kernel 的入门课而是带你亲手“剖开”内核看心跳“Kernels: A Deep Dive”这个标题乍看像一本教科书副标题但在我过去十二年做系统底层开发、嵌入式调试和高性能服务优化的实战中它从来不是理论推演的代名词——而是一份手术刀级的操作手册。我带过的几十个团队里90%的新手工程师第一次真正“看见”内核在做什么不是靠读《Linux内核设计与实现》而是靠在一台干净的虚拟机里亲手编译一个最小可启动内核、挂载 initramfs、用 kprobe 动态打点观测进程 fork 的完整路径、再把 perf record 的火焰图和 /proc/sched_debug 的调度器快照对齐起来看——那一刻他们才明白什么叫“内核不是黑箱是可触摸的精密钟表”。这篇文章要讲的就是这套可复现、可验证、可教学的深度内核剖析方法论。它不预设你熟悉汇编或内存管理但要求你愿意关掉 IDE打开终端敲下make menuconfig它适合三类人正在啃《深入理解 Linux 内核》却卡在第三章的系统程序员、需要排查生产环境偶发 soft lockup 的 SRE 工程师、以及想为国产芯片平台移植实时补丁的嵌入式开发者。核心关键词——内核剖析、动态观测、启动流程、调度器行为、内存映射——每一个都会落到具体命令、具体文件、具体寄存器值上而不是停留在“内核负责进程管理”这种百科式描述。你可能会问现在都用容器和 Serverless 了还抠内核干啥我的回答很直接上周我们线上一个 Kafka 集群出现持续 300ms 的 P99 延迟抖动Prometheus 显示 CPU 使用率只有 45%网络和磁盘 I/O 完全正常。最后用bpftrace -e kprobe:try_to_wake_up { wakes count(); }发现每秒有 2.7 万次无效唤醒根源是某个自定义 cgroup v1 的 cpu.shares 配置错误导致调度器在 runqueue 上反复扫描空队列。这个问题任何应用层监控都看不到只有当你真正理解try_to_wake_up()函数里那个rq-nr_switches计数器和rq-nr_cpus_allowed位图的交互逻辑才能一眼定位。所以“Deep Dive”不是怀旧是现代云原生架构下最后一道不可绕过的性能护城河。2. 内核剖析的整体设计思路为什么必须放弃“静态阅读”转向“动态切片”2.1 传统学习路径的三大断层与真实代价我见过太多人陷在“读代码—看不懂—换书—再卡住”的死循环里。问题不在人而在方法。传统路径存在三个致命断层第一源码与运行态的断层。比如mm/memory.c里的handle_mm_fault()函数静态阅读时你看到的是页表项PTE的 set/clear 操作但实际运行中x86_64 下它可能触发 TLB miss 导致 microcode 级别的 page walkARM64 下则可能因 ASID 切换引发 TLB shootdown 中断。这些硬件细节源码注释不会写但它们直接决定一次缺页处理耗时是 150ns 还是 3.2μs。我曾为一个数据库引擎优化发现其随机读延迟方差极大最终用perf record -e mem-loads,mem-stores抓到大量MEM_LOAD_RETIRED.L1_MISS事件根源是内核在alloc_pages()时未对齐 hugepage boundary导致硬件 prefetcher 失效——这完全无法从 C 代码行间推断。第二抽象层与物理资源的断层。“进程”在课本里是 PCB 结构体在现实中是 CPU 寄存器上下文 TLB 条目 cache line 占用 中断控制器状态的集合体。当fork()返回后子进程的%rsp寄存器指向哪里它的 L1d cache 是否被父进程污染这些不摸到寄存器层面永远是模糊概念。我们曾在线上遇到一种诡异现象同一台机器上两个相同配置的 Java 应用一个 GC STW 时间稳定在 8ms另一个波动到 45ms。用rdmsr -a 0x1b读取 IA32_SYSENTER_ESP MSR 发现高延迟节点的 sysenter stack 被频繁切换追查到是某个驱动模块在__do_softirq()中调用了schedule_timeout()意外触发了内核栈切换——这种问题只看kernel/fork.c的copy_process()函数连边都摸不到。第三配置与行为的断层。CONFIG_PREEMPTy和CONFIG_PREEMPT_RTy看似只是 Kconfig 选项但前者仅让内核抢占点增加后者则重写了整个调度器锁机制将spinlock_t替换为rt_mutex_t并把中断线程化。这意味着开启 RT 补丁后local_irq_disable()不再关闭本地中断而是将中断上下文转为高优先级内核线程执行。某次我们为工业控制设备启用 PREEMPT_RT结果发现串口数据丢包率上升 300%最终定位到是serial_core.c中一个spin_lock(port-lock)在 RT 模式下被转化为优先级继承锁而用户空间串口工具又在低优先级线程中持有该锁长达 20ms——静态看代码毫无问题运行态却是灾难。2.2 我们的设计哲学“四层切片法”直击内核脉搏为弥合上述断层我提炼出“四层切片法”每一层对应一个可观测、可干预、可验证的内核切面且全部基于主线内核v6.1无需 patch启动切片Boot Slice从arch/x86/kernel/head_64.S的_startup_64开始用 QEMU GDB 单步跟踪第一条指令如何建立初始页表、如何跳转到start_kernel()。重点观测early_idt_handler_array如何处理早期异常setup_arch()中init_memory_mapping()如何将物理内存映射到内核线性地址空间。这一层解决“内核从哪来”的问题避免所有关于“内核加载地址”的玄学讨论。调度切片Sched Slice以kernel/sched/core.c的pick_next_task()为核心用 ftrace 动态插桩捕获每次任务切换前后的rq-nr_running、rq-nr_switches、p-se.vruntime值并关联sched_switchtracepoint 输出的 prev/next pid。我们自制了一个 Python 脚本自动解析 ftrace 输出生成“调度决策热力图”清晰显示哪个 CPU 的 runqueue 长期堆积、哪个进程的 vruntime 异常偏移——这比top或htop看到的 CPU% 真实十倍。内存切片MM Slice放弃cat /proc/meminfo这种汇总视图直接读/sys/kernel/debug/page_owner需开启CONFIG_PAGE_OWNER配合crash工具解析vmcore定位具体哪一行kmalloc()分配了泄漏的 4MB 连续内存。更进一步用pahole -C page查看struct page在内存中的实际布局确认page-lru和page-mapping是否因结构体填充padding导致 cache line false sharing——这是 NUMA 敏感型应用性能瓶颈的常见元凶。中断切片IRQ Slice用cat /proc/interrupts只能看到中断计数我们要看到中断生命周期。通过trace-cmd record -e irq:irq_handler_entry -e irq:irq_handler_exit抓取单次中断从进入 handler 到退出的完整时间戳再用perf script关联irq_work_run()的执行栈判断是否因irq_work积压导致软中断延迟。某次我们发现网卡 RX 中断处理耗时正常50μs但net_rx_action()执行却延迟 8ms最终发现是ksoftirqd/0线程被其他高优先级实时任务饿死——这只有在 IRQ Slice 下才能暴露。这套方法的核心逻辑是拒绝把内核当整体而把它当作一组松耦合、可观测、可独立调试的子系统集合。每个切片都有明确的入口点、可观测指标、可验证假设。比如调度切片的验证假设是“当rq-nr_running 1且rq-curr-se.vruntime显著小于rq-cfs.min_vruntime时pick_next_task_fair()必须返回rq-cfs.curr”。你可以用stress-ng --cpu 4 --timeout 10s制造负载然后用 ftrace 实时验证该假设是否被违反——这才是真正的“Deep Dive”。3. 核心细节解析与实操要点从编译第一个可调试内核开始3.1 构建可调试内核环境为什么必须自己编译而非用发行版内核很多工程师觉得“用apt install linux-image-debug就行”这是最大的误区。发行版内核为了通用性禁用了大量调试符号和追踪功能。以 Ubuntu 22.04 的5.15.0-107-generic为例其vmlinux文件大小仅 42MB而我们自己编译的带完整调试信息的内核是 386MB。关键差异在于缺少CONFIG_DEBUG_INFO_DWARF4y导致 GDB 无法解析struct task_struct的成员偏移p $rdi-pid会报错 “no symbol table”。禁用CONFIG_FTRACEy和CONFIG_FUNCTION_TRACERyftrace 的function_graphtracer 无法启用你将失去函数级调用栈追踪能力。CONFIG_KPROBESy被设为模块m而非内置ykprobe 在内核启动早期无法使用错过start_kernel()阶段的关键观测窗口。因此构建第一步必须是从 kernel.org 下载纯净源码手动配置全程可控。以下是经过我们团队在 37 台不同配置机器上验证的最小可行配置清单基于 v6.6# 1. 获取源码并解压 wget https://cdn.kernel.org/pub/linux/kernel/v6.x/linux-6.6.tar.xz tar -xf linux-6.6.tar.xz cd linux-6.6 # 2. 生成最小基础配置基于 x86_64 defconfig make x86_64_defconfig # 3. 启用关键调试选项这才是 Deep Dive 的基石 scripts/config \ -e CONFIG_DEBUG_INFO_DWARF4 \ -e CONFIG_FTRACE \ -e CONFIG_FUNCTION_TRACER \ -e CONFIG_FUNCTION_GRAPH_TRACER \ -e CONFIG_KPROBES \ -e CONFIG_KPROBE_EVENTS \ -e CONFIG_BPF_SYSCALL \ -e CONFIG_BPF_JIT \ -e CONFIG_PAGE_OWNER \ -e CONFIG_DEBUG_FS \ -e CONFIG_PROC_PAGE_MONITOR \ -d CONFIG_MODULE_SIG # 4. 编译使用 8 线程加速但保留符号表 make -j8 bzImage modules sudo make modules_install sudo cp arch/x86_64/boot/bzImage /boot/vmlinuz-6.6-dive提示CONFIG_MODULE_SIG被禁用是因为签名模块会干扰 kprobe 对内核函数的 hook而CONFIG_PAGE_OWNER是内存泄漏分析的黄金开关但它会带来约 5% 的内存分配开销仅在调试时启用。编译完成后不要急着重启。先用file vmlinux确认输出文件包含 DWARF 调试信息$ file vmlinux vmlinux: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), statically linked, BuildID[sha1]..., with debug_info, not stripped如果显示 “stripped”说明CONFIG_DEBUG_INFO_DWARF4未生效需检查.config文件。这个步骤看似琐碎但它是后续所有深度观测的前提——就像做外科手术前必须确认无影灯亮度足够否则一切精细操作都是徒劳。3.2 启动切片实操用 QEMU GDB 单步跟踪内核第一条指令QEMU 是最安全的内核调试沙盒无需重启物理机。我们采用-s -S参数组合让 QEMU 在启动时暂停并等待 GDB 连接# 启动 QEMU加载我们编译的内核并开放 GDB 调试端口 qemu-system-x86_64 \ -kernel /path/to/linux-6.6/arch/x86_64/boot/bzImage \ -initrd /path/to/initramfs.cgz \ # 最小 initramfs仅含 busybox -append consolettyS0 root/dev/ram rdinit/sbin/init \ -nographic \ -s -S \ # -s 等价于 -gdb tcp::1234-S 表示启动即暂停 -m 2G \ -cpu host此时 QEMU 窗口会静止等待 GDB 连接。新开终端启动 GDB 并加载内核符号# 启动 GDB加载 vmlinux注意不是 bzImage gdb vmlinux # 连接到 QEMU 的 GDB server (gdb) target remote :1234 # 设置架构为 64 位QEMU 默认可能是 32 位 (gdb) set architecture i386:x86-64 # 查看当前 CPU 状态确认在实模式还是保护模式 (gdb) info registers rax 0x0 0x0 rbx 0x0 0x0 rcx 0x0 0x0 rdx 0x0 0x0 rsi 0x10000 0x10000 rdi 0x10000 0x10000 rip 0xfffffffffffeffff 0xfffffffffffeffff # 注意rip 为 0xfffffffffffeffff这是实模式下的起始地址关键来了内核第一条有效指令在arch/x86/kernel/head_64.S的_startup_64标签处。但此时 CPU 还在实模式我们需要先执行到保护模式切换点。查阅head_64.S找到lgdt指令加载全局描述符表的位置# arch/x86/kernel/head_64.S startup_64: # ... 一些初始化 ... lgdt __BOOT_CS(__boot_gdt) movq %rax, %cr3 # 加载页表基址 movq $(X86_CR4_PAE | X86_CR4_PSE | X86_CR4_OSFXSR | X86_CR4_OSXMMEXCPT), %rax movq %rax, %cr4 movq $(X86_EFER_LME | X86_EFER_LMA), %rax wrmsr movq $(X86_CR0_PE | X86_CR0_PG | X86_CR0_WP | X86_CR0_AM), %rax movq %rax, %cr0 # 关键开启分页和保护模式 jmp *(__startup_64) # 跳转到 64 位模式入口我们在movq %rax, %cr0这一行下断点(gdb) b *0xffffffff810000000x123 # 具体偏移需用 objdump 查看此处为示意 (gdb) c当 GDB 停在movq %rax, %cr0时执行stepi单步观察cr0寄存器变化(gdb) stepi (gdb) info registers cr0 cr0 0x80050033 0x80050033 # bit 0 (PE) 和 bit 31 (PG) 已置位保护模式和分页已开启此时CPU 已进入 64 位长模式。下一步是跳转到__startup_64我们继续stepi直到看到rip变为0xffffffff81000000附近的大地址——这就是内核被加载到的虚拟地址。这时我们可以用list *$rip查看对应的 C 代码如start_kernel()真正实现从汇编到 C 的无缝衔接。实操心得很多新手卡在info registers看不到cr0是因为 GDB 默认不显示控制寄存器。必须用info registers cr0显式指定。另外objdump -d vmlinux | grep startup_64是查找准确地址的最快方法比翻源码快十倍。3.3 调度切片实操用 ftrace 解析一次 fork() 的完整调度链路fork()看似简单实则触发内核最复杂的调度路径之一。我们用 ftrace 捕获从用户态clone()系统调用到子进程首次获得 CPU 时间的全过程# 1. 清空 ftrace 缓冲区 echo 0 /sys/kernel/debug/tracing/tracing_on echo /sys/kernel/debug/tracing/trace # 2. 启用关键 tracepoints echo 1 /sys/kernel/debug/tracing/events/sched/sched_process_fork/enable echo 1 /sys/kernel/debug/tracing/events/sched/sched_wakeup/enable echo 1 /sys/kernel/debug/tracing/events/sched/sched_switch/enable echo 1 /sys/kernel/debug/tracing/events/syscalls/sys_enter_clone/enable echo 1 /sys/kernel/debug/tracing/events/syscalls/sys_exit_clone/enable # 3. 启动 tracing echo 1 /sys/kernel/debug/tracing/tracing_on # 4. 在另一终端执行 fork 测试 stress-ng --fork 1 --timeout 1s # 5. 停止 tracing 并查看结果 echo 0 /sys/kernel/debug/tracing/tracing_on cat /sys/kernel/debug/tracing/trace典型输出如下已精简bash-1234 [000] d... 12345.678901: sys_enter_clone: NR 56 bash-1234 [000] d... 12345.678905: sched_process_fork: commbash pid1234 child_commbash child_pid1235 bash-1234 [000] d... 12345.678910: sched_wakeup: commbash pid1235 prio120 success1 target_cpu0 bash-1234 [000] d... 12345.678915: sched_switch: prev_commbash prev_pid1234 prev_prio120 prev_stateR next_commbash next_pid1235 next_prio120关键洞察在于sched_wakeup和sched_switch的时间戳差值这里只有 5μs说明子进程被立即调度。但如果在高负载下这个差值可能达到毫秒级。我们写了一个解析脚本自动计算每个sched_wakeup到对应sched_switch的延迟并按 CPU 分组统计# parse_fork_delay.py import re from collections import defaultdict delays defaultdict(list) with open(/sys/kernel/debug/tracing/trace) as f: lines f.readlines() for i, line in enumerate(lines): if sched_wakeup in line and child_pid1235 in line: wake_time float(re.search(r(\d\.\d):, line).group(1)) # 向后查找同 pid 的 sched_switch for j in range(i, min(i100, len(lines))): if fsched_switch in lines[j] and fnext_pid1235 in lines[j]: switch_time float(re.search(r(\d\.\d):, lines[j]).group(1)) delays[cpu0].append(switch_time - wake_time) break for cpu, dlist in delays.items(): print(f{cpu}: avg delay {sum(dlist)/len(dlist)*1e6:.1f}μs)这个脚本揭示了调度器的真实响应能力。在我们的测试中当CONFIG_NO_HZ_FULLy无滴答模式开启时cpu0的平均唤醒延迟从 12μs 降至 3.2μs但cpu1却升至 85μs——因为nohz_full将 tick 迁移到了cpu0导致cpu1的hrtimer中断被延迟。这种细粒度的量化是任何宏观监控都无法提供的。注意sched_wakeup的success1表示唤醒成功但如果看到success0说明目标进程已被SIGSTOP暂停或处于不可中断睡眠D 状态这是排查“进程假死”的关键线索。4. 实操过程与核心环节实现从内存映射到中断生命周期的全链路验证4.1 内存切片用 page_owner 定位一次真实的内存泄漏假设我们有一个内核模块leak_mod.ko其中有一行kmalloc(4096, GFP_KERNEL)被调用但未释放。传统方式用slabtop只能看到kmalloc-4k缓存占用增长但无法定位到具体模块。page_owner是终极解决方案# 1. 开启 page_owner需内核配置支持 echo 1 /sys/kernel/debug/page_owner_enabled # 2. 加载泄漏模块 insmod leak_mod.ko # 3. 触发泄漏模块内部有循环分配 echo 1 /sys/module/leak_mod/parameters/trigger_leak # 4. 生成 page_owner 报告 echo m /proc/sysrq-trigger # 触发 sysrq生成 /sys/kernel/debug/page_owner报告位于/sys/kernel/debug/page_owner内容类似page:00000000abcd1234 refcount:1 mapcount:0 mapping:00000000efgh5678 index:0x0 flags: 0x2000000000000000() raw: 0200000000000000 ffffffffffffffff ffffffffffffffff 0000000000000000 page dumped because: page_owner is enabled ... stack trace: [ffffffff810a1234] kmalloc_order0x45/0x120 [ffffffff810a1345] kmalloc0x123/0x200 [ffffffffc0001234] leak_mod_init0x56/0x1000 [leak_mod] [ffffffff81002345] do_one_initcall0x56/0x120关键信息是stack trace部分它精确指出泄漏发生在leak_mod_init()函数的第 56 行。我们甚至可以反向解析leak_mod.ko的符号表# 获取模块的 .ko 文件地址 readelf -S leak_mod.ko | grep text # 然后用 addr2line 定位源码行 addr2line -e leak_mod.ko 0xc0001234实操心得page_owner会显著增加内存分配开销生产环境切勿长期开启。我们团队的标准流程是在 staging 环境开启 10 分钟复现问题后立即关闭。另外/proc/sysrq-trigger的m命令会 dump 当前所有 page owner如果内存很大文件会非常大建议先echo 1 /sys/kernel/debug/page_owner_disabled再echo m避免 OOM。4.2 中断切片用 trace-cmd 解析网卡 RX 中断的完整生命周期网络延迟问题往往藏在中断处理链路中。我们以 Intel e1000e 网卡为例用trace-cmd抓取一次完整 RX 中断# 1. 清空 trace 缓冲 trace-cmd reset # 2. 记录关键事件中断进入/退出、软中断执行、NAPI poll trace-cmd record \ -e irq:irq_handler_entry \ -e irq:irq_handler_exit \ -e irq:softirq_entry \ -e irq:softirq_exit \ -e napi:napi_poll # 3. 在另一终端发送一个 ping 包触发 RX ping -c 1 192.168.1.1 # 4. 停止记录并解析 trace-cmd report rx_trace.txtrx_trace.txt的关键片段e1000e-24 [000] d..1 12345.678901: irq_handler_entry: irq24 namee1000e e1000e-24 [000] d..1 12345.678905: irq_handler_exit: irq24 rethandled e1000e-24 [000] d..1 12345.678910: softirq_entry: vec3 [actionNAPI] e1000e-24 [000] d..1 12345.678915: napi_poll: napi00000000abcd1234 work1024 e1000e-24 [000] d..1 12345.678920: softirq_exit: vec3时间线清晰显示中断处理entry→exit耗时 4μs软中断执行entry→exit耗时 10μs其中napi_poll占用 5μs。但如果napi_poll的work值远小于网卡 ring buffer 大小如work1024而 ring size4096说明 NAPI 未一次清空缓冲区可能触发下一轮中断形成“中断风暴”。我们曾在一个高吞吐场景中发现work值稳定在 64而 ring size 是 2048导致每秒产生 12 万次中断CPU 花费 35% 时间在中断上下文切换上。解决方案是调大net.core.netdev_budget默认 300但必须同步调大net.core.netdev_max_backlog否则丢包率会上升——这是典型的“改一个参数牵动一整条链路”的内核特性。4.3 四层切片联动一次真实故障的全链路归因去年我们处理过一个经典案例某金融交易系统在每日 9:30 开市瞬间订单处理延迟从 200μs 飙升至 12ms持续 3 分钟后自动恢复。用四层切片法我们 17 分钟内定位根因启动切片dmesg | grep -i microcode发现开市前 1 分钟Intel 微码更新触发了microcode: updated early但cat /sys/devices/system/cpu/vulnerabilities/spec_store_bypass显示仍为Vulnerable说明微码未正确加载。这是起点。中断切片trace-cmd record -e irq:irq_handler_entry -e irq:irq_handler_exit抓取开市瞬间发现irq32GPU 相关中断计数激增 200 倍。lspci -vv -s 00:02.0显示这是集成显卡而交易系统 GUI 进程在开市时刷新行情图触发 GPU 渲染中断。调度切片ftrace显示sched_switch日志中ksoftirqd/0线程的prev_state长期为R运行态但next_comm却是java进程说明软中断处理严重阻塞了 Java 线程调度。内存切片cat /proc/$(pgrep java)/status | grep VmRSS发现 RSS 突增 1.2GBpstack $(pgrep java)显示大量线程卡在pthread_cond_wait。结合page_owner发现是 GUI 进程的 OpenGL 上下文创建占用了大量连续物理内存触发内核alloc_contig_range()进而导致kswapd频繁扫描内存间接饿死了ksoftirqd。最终方案是将 GUI 进程绑定到专用 CPU 核心taskset -c 7 ./gui_app并设置isolcpus7隔离该核心确保交易核心线程不受干扰。这个案例完美诠释了四层切片的价值单一层只能看到症状四层联动才能锁定病灶。5. 常见问题与排查技巧实录那些文档里不会写的“血泪经验”5.1 QEMU GDB 调试的五大“静默失败”场景及修复QEMU GDB 是最常用的内核调试组合但有五个场景会导致“看似在调试实则无效”且错误信息极其隐蔽GDB 加载错误的 vmlinux最常见的错误是gdb vmlinux-5.15但实际运行的是v6.6内核。症状是info registers正常但list *$rip显示 “No such file or directory”。修复用readelf -h vmlinux | grep Entry确认入口地址再用qemu-system-x86_64 -kernel指定的内核版本必须严格匹配。QEMU CPU 模式不匹配在 ARM64 主机上运行 x86_64 内核若未指定-cpu qemu64QEMU 可能降级为 i486 模式导致movq %rax, %cr0指令非法。症状是 GDB 停在0xfffffffffffeffff后无法stepi。修复强制指定-cpu host或-cpu qemu64,smep,smap。GDB 架构未切换x86_64 内核启动后GDB 默认仍是 i386 架构info registers只显示 32 位寄存器。症状是rip显示为0x0。修复set architecture i386:x86-64后再info registers才能看到rip,rax等 64 位寄存器。符号表路径错误当内核源码在/src/linux而vmlinux编译在/build/linuxGDB 默认在/src/linux查找源码。症状是list命令找不到文件。修复set substitute-path /src/linux /build/linux。QEMU 内存映射冲突某些 QEMU 版本如 6.2与内核 v6.6 存在内存布局 bug导致bzImage加载地址与内核期望的0xffffffff81000000冲突。症状是start_kernel()断点无法命中。修复升级 QEMU 到 7.2或在make menuconfig中修改CONFIG_PHYSICAL_START0x1000000。提示我们维护了一个一键检测脚本check_qemu_gdb.sh它自动执行上述五项检查并输出 PASS/FAIL新成员入职第一天就要跑通这个脚本。5.2 ftrace 的“幽灵丢失”问题为什么你的 tracepoint 没有输出ftrace 是内核观测的基石但新手常抱怨“明明启用了sched_switch却什么也看不到”。根本原因有三tracing_on 被意外关闭echo 0 /sys/kernel/debug/tracing/tracing_on后即使echo 1 /sys/kernel/debug/tracing/events/sched/sched_switch/enable也不会记录。必须echo 1 /sys/kernel/debug/tracing/tracing_on。我们习惯在启用事件后立即执行此命令。**缓冲区溢出被