第15章:性能分析与调试

在现代 Linux 系统中,性能分析和调试是内核开发与系统优化的核心技能。本章深入探讨 Linux 内核提供的各种观测、分析和调试机制,从传统的 ftrace 到革命性的 eBPF,从静态跟踪点到动态探针,从崩溃转储到实时调试。我们将学习如何使用这些强大的工具来诊断性能瓶颈、追踪系统行为、定位内核错误,并最终实现系统的性能优化。

学习目标

完成本章学习后,您将能够:

  1. 掌握内核跟踪机制:理解 ftrace、kprobes、tracepoints 的原理与使用
  2. 精通性能分析工具:熟练使用 perf、火焰图等工具进行性能剖析
  3. 掌握内核调试技术:使用 KGDB、crash、kdump 调试内核问题
  4. 运用动态追踪技术:编写 eBPF/bpftrace 程序进行系统观测
  5. 性能优化实践:识别性能瓶颈并实施针对性优化

章节大纲

15.1 内核跟踪机制

Linux 内核跟踪机制提供了观测内核行为的窗口,让我们能够在不修改内核代码的情况下,深入了解系统的运行状态。从 2.6.27 版本引入 ftrace 开始,Linux 的可观测性得到了质的飞跃。

15.1.1 ftrace 框架

ftrace(function tracer)是 Linux 内核的官方跟踪框架,由 Steven Rostedt 开发。它不仅可以跟踪函数调用,还集成了各种跟踪器,成为内核调试和性能分析的瑞士军刀。

架构设计

ftrace 基于编译时插桩和运行时修改实现:

    用户空间接口 (/sys/kernel/debug/tracing/)
           |
    +------v------+
    | Trace Buffer |  <- 环形缓冲区(per-CPU)
    +------+------+
           |
    +------v------+
    |   Tracers   |  <- function, function_graph, wakeup, irqsoff
    +------+------+
           |
    +------v------+
    | Trace Events |  <- 静态跟踪点
    +------+------+
           |
    +------v------+
    |   Ftrace    |  <- 核心框架
    |   Core      |
    +------+------+
           |
    +------v------+
    | Mcount/Fentry| <- 函数入口钩子
    +--------------+

函数跟踪原理

GCC 编译器的 -pg 选项会在每个函数入口插入 mcount 调用(新版本使用 fentry):

/* 原始函数 */
void do_something(void) {
    /* function body */
}

/* 编译后(概念性表示) */
void do_something(void) {
    mcount();  /* 或 __fentry__() */
    /* function body */
}

ftrace 在运行时通过动态修改这些调用点来启用/禁用跟踪:

/* kernel/trace/ftrace.c 简化版 */
static int ftrace_modify_code(unsigned long ip, unsigned long old_code,
                              unsigned long new_code)
{
    unsigned char replaced[MCOUNT_INSN_SIZE];

    /* 使用 text_poke 或类似机制修改代码 */
    if (probe_kernel_read(replaced, (void *)ip, MCOUNT_INSN_SIZE))
        return -EFAULT;

    if (memcmp(replaced, &old_code, MCOUNT_INSN_SIZE) != 0)
        return -EINVAL;

    /* 原子性地替换指令 */
    text_poke_bp((void *)ip, &new_code, MCOUNT_INSN_SIZE, NULL);

    return 0;
}

使用示例

# 启用函数跟踪
echo function > /sys/kernel/debug/tracing/current_tracer
echo 1 > /sys/kernel/debug/tracing/tracing_on

# 设置函数过滤
echo 'tcp_*' > /sys/kernel/debug/tracing/set_ftrace_filter

# 查看跟踪结果
cat /sys/kernel/debug/tracing/trace

# 函数调用图跟踪
echo function_graph > /sys/kernel/debug/tracing/current_tracer

15.1.2 kprobes 动态探针

kprobes 是 Linux 内核的动态探测机制,允许在几乎任意内核代码位置设置探针,无需重新编译内核。它通过替换指令、使用断点或跳转指令来实现。

kprobes 家族

  1. kprobes:在指定地址设置探针
  2. kretprobes:在函数返回时触发
  3. jprobes:已废弃,被 kprobes 取代

实现机制

kprobes 的核心是指令替换和异常处理:

/* 简化的 kprobe 结构 */
struct kprobe {
    struct hlist_node hlist;
    kprobe_opcode_t *addr;        /* 探测点地址 */
    kprobe_pre_handler_t pre_handler;
    kprobe_post_handler_t post_handler;
    kprobe_opcode_t opcode;       /* 保存的原始指令 */
    struct arch_specific_insn ainsn;
};

/* 注册 kprobe 的核心流程 */
int register_kprobe(struct kprobe *p)
{
    /* 1. 解析符号地址 */
    addr = kprobe_addr(p);

    /* 2. 准备探测点 */
    ret = prepare_kprobe(p);

    /* 3. 插入断点指令 (x86: int3) */
    arch_arm_kprobe(p);

    /* 4. 添加到哈希表 */
    hlist_add_head(&p->hlist, &kprobe_table[hash]);

    return 0;
}

断点处理流程

    执行到探测点
         |
         v
    触发 int3 异常
         |
         v
    do_int3() 处理器
         |
         v
    kprobe_int3_handler()
         |
    +----+----+
    |         |
    v         v
pre_handler  单步执行原指令
    |         |
    v         v
post_handler 返回正常执行

使用示例

/* 内核模块中使用 kprobes */
static struct kprobe kp = {
    .symbol_name = "tcp_sendmsg",
};

static int handler_pre(struct kprobe *p, struct pt_regs *regs)
{
    pr_info("tcp_sendmsg: sock=%p, len=%lu\n",
            (void *)regs->di, regs->si);
    return 0;
}

static int __init kprobe_init(void)
{
    kp.pre_handler = handler_pre;
    register_kprobe(&kp);
    return 0;
}

15.1.3 tracepoints 静态跟踪点

tracepoints 是内核中预定义的静态跟踪点,相比动态探针具有更高的稳定性和更低的开销。

定义跟踪点

/* include/trace/events/sched.h */
TRACE_EVENT(sched_switch,
    TP_PROTO(struct task_struct *prev, struct task_struct *next),
    TP_ARGS(prev, next),

    TP_STRUCT__entry(
        __array(char, prev_comm, TASK_COMM_LEN)
        __field(pid_t, prev_pid)
        __field(int, prev_prio)
        __array(char, next_comm, TASK_COMM_LEN)
        __field(pid_t, next_pid)
        __field(int, next_prio)
    ),

    TP_fast_assign(
        memcpy(__entry->prev_comm, prev->comm, TASK_COMM_LEN);
        __entry->prev_pid = prev->pid;
        __entry->prev_prio = prev->prio;
        memcpy(__entry->next_comm, next->comm, TASK_COMM_LEN);
        __entry->next_pid = next->pid;
        __entry->next_prio = next->prio;
    ),

    TP_printk("prev_comm=%s prev_pid=%d prev_prio=%d ==> "
              "next_comm=%s next_pid=%d next_prio=%d",
              __entry->prev_comm, __entry->prev_pid, __entry->prev_prio,
              __entry->next_comm, __entry->next_pid, __entry->next_prio)
);

跟踪点实现

/* 跟踪点的静态调用优化 */
#define DEFINE_TRACE(name)                              \
    struct tracepoint __tracepoint_##name = {           \
        .name = #name,                                  \
        .key = STATIC_KEY_INIT_FALSE,                   \
        .funcs = NULL,                                  \
    };                                                   \
                                                        \
    static inline void trace_##name(proto)              \
    {                                                    \
        if (static_key_false(&__tracepoint_##name.key)) \
            __DO_TRACE(&__tracepoint_##name, args);     \
    }

15.1.4 跟踪事件子系统

跟踪事件子系统统一了各种跟踪机制的接口,提供了标准化的事件定义和过滤机制。

事件过滤器

# 设置过滤条件
echo 'prev_prio < 100' > /sys/kernel/debug/tracing/events/sched/sched_switch/filter

# 复合条件
echo 'irq == 1 && ret >= 0' > /sys/kernel/debug/tracing/events/irq/irq_handler_exit/filter

触发器机制

# 当满足条件时触发快照
echo 'snapshot if bytes_req > 4096' > \
    /sys/kernel/debug/tracing/events/kmem/kmalloc/trigger

# 触发栈追踪
echo 'stacktrace' > \
    /sys/kernel/debug/tracing/events/sched/sched_switch/trigger

15.2 性能计数器

Linux 性能计数器子系统(perf_events)提供了统一的接口来访问硬件性能计数器和软件事件,是现代性能分析的基石。

15.2.1 perf events 架构

perf_events 子系统由 Thomas Gleixner、Ingo Molnar 和 Arnaldo Carvalho de Melo 等人开发,从 2.6.31 版本开始成为内核的核心组件。

系统架构

    用户空间工具 (perf, pmu-tools)
              |
    +=========v=========+
    |   系统调用接口    |  <- perf_event_open()
    +===================+
              |
    +---------v---------+
    |  perf_event 核心  |  <- 事件管理、调度、采样
    +-------------------+
         /    |    \
        /     |     \
       v      v      v
    +-----+ +-----+ +-----+
    | CPU | | SW  | |Trace|
    | PMU | |Event| |Point|
    +-----+ +-----+ +-----+

核心数据结构

/* include/linux/perf_event.h */
struct perf_event {
    struct list_head        event_entry;
    struct perf_event       *group_leader;
    struct pmu              *pmu;           /* 性能监控单元 */

    enum perf_event_state   state;
    atomic64_t              count;          /* 事件计数 */
    atomic64_t              child_count;

    struct perf_event_attr  attr;           /* 用户配置 */
    u64                     hw_event;       /* 硬件事件配置 */

    struct perf_sample_data sample_data;    /* 采样数据 */
    struct ring_buffer      *rb;            /* 环形缓冲区 */

    /* 回调函数 */
    perf_overflow_handler_t overflow_handler;
    struct perf_event_context *ctx;
};

/* 性能监控单元抽象 */
struct pmu {
    struct list_head        entry;
    const char              *name;
    int                     type;

    /* PMU 操作接口 */
    void (*pmu_enable)      (struct pmu *pmu);
    void (*pmu_disable)     (struct pmu *pmu);
    int  (*event_init)      (struct perf_event *event);
    void (*add)             (struct perf_event *event, int flags);
    void (*del)             (struct perf_event *event, int flags);
    void (*start)           (struct perf_event *event, int flags);
    void (*stop)            (struct perf_event *event, int flags);
    void (*read)            (struct perf_event *event);
};

15.2.2 硬件性能监控单元(PMU)

现代处理器提供专门的硬件计数器来统计各种微架构事件,如缓存命中/未命中、分支预测、指令执行等。

Intel PMU 架构

/* arch/x86/events/intel/core.c */
static struct event_constraint intel_core_event_constraints[] = {
    INTEL_EVENT_CONSTRAINT(0x11, 0x2),  /* FP_ASSIST */
    INTEL_EVENT_CONSTRAINT(0x12, 0x2),  /* MUL */
    INTEL_EVENT_CONSTRAINT(0x13, 0x2),  /* DIV */
    INTEL_EVENT_CONSTRAINT(0x14, 0x1),  /* CYCLES_DIV_BUSY */
    INTEL_EVENT_CONSTRAINT(0x19, 0x2),  /* DELAYED_BYPASS */
};

/* 通用性能计数器配置 */
#define ARCH_PERFMON_EVENTSEL_EVENT    0x000000FFULL
#define ARCH_PERFMON_EVENTSEL_UMASK    0x0000FF00ULL
#define ARCH_PERFMON_EVENTSEL_USR       (1ULL << 16)
#define ARCH_PERFMON_EVENTSEL_OS        (1ULL << 17)
#define ARCH_PERFMON_EVENTSEL_EDGE      (1ULL << 18)
#define ARCH_PERFMON_EVENTSEL_INT       (1ULL << 20)
#define ARCH_PERFMON_EVENTSEL_ENABLE    (1ULL << 22)

硬件事件映射

/* 通用硬件事件到具体 CPU 事件的映射 */
static u64 intel_pmu_event_map(int hw_event)
{
    switch (hw_event) {
    case PERF_COUNT_HW_CPU_CYCLES:
        return 0x003c;  /* UNHALTED_CORE_CYCLES */
    case PERF_COUNT_HW_INSTRUCTIONS:
        return 0x00c0;  /* INST_RETIRED.ANY_P */
    case PERF_COUNT_HW_CACHE_REFERENCES:
        return 0x4f2e;  /* LLC Reference */
    case PERF_COUNT_HW_CACHE_MISSES:
        return 0x412e;  /* LLC Misses */
    case PERF_COUNT_HW_BRANCH_INSTRUCTIONS:
        return 0x00c4;  /* BR_INST_RETIRED.ALL_BRANCHES */
    case PERF_COUNT_HW_BRANCH_MISSES:
        return 0x00c5;  /* BR_MISP_RETIRED.ALL_BRANCHES */
    }
    return 0;
}

PMU 中断处理

/* 性能计数器溢出中断处理 */
static void intel_pmu_handle_irq(struct pt_regs *regs)
{
    struct cpu_hw_events *cpuc = this_cpu_ptr(&cpu_hw_events);
    int bit, loops;
    u64 status;

    /* 读取溢出状态 */
    rdmsrl(MSR_CORE_PERF_GLOBAL_STATUS, status);

    for_each_set_bit(bit, (unsigned long *)&status, X86_PMC_IDX_MAX) {
        struct perf_event *event = cpuc->events[bit];

        /* 处理溢出 */
        if (!intel_pmu_save_and_restart(event))
            continue;

        /* 生成采样数据 */
        perf_sample_data_init(&data, 0, event->hw.last_period);

        /* 调用溢出处理器 */
        if (perf_event_overflow(event, &data, regs))
            x86_pmu_stop(event, 0);
    }

    /* 清除溢出状态 */
    wrmsrl(MSR_CORE_PERF_GLOBAL_OVF_CTRL, status);
}

15.2.3 软件事件与跟踪点

除了硬件计数器,perf_events 还支持纯软件事件和跟踪点事件。

软件事件类型

enum perf_sw_ids {
    PERF_COUNT_SW_CPU_CLOCK         = 0,
    PERF_COUNT_SW_TASK_CLOCK        = 1,
    PERF_COUNT_SW_PAGE_FAULTS       = 2,
    PERF_COUNT_SW_CONTEXT_SWITCHES  = 3,
    PERF_COUNT_SW_CPU_MIGRATIONS    = 4,
    PERF_COUNT_SW_PAGE_FAULTS_MIN   = 5,
    PERF_COUNT_SW_PAGE_FAULTS_MAJ   = 6,
    PERF_COUNT_SW_ALIGNMENT_FAULTS  = 7,
    PERF_COUNT_SW_EMULATION_FAULTS  = 8,
};

/* 软件事件触发 */
static inline void perf_sw_event(u32 event_id, u64 nr, struct pt_regs *regs)
{
    if (static_key_false(&perf_swevent_enabled[event_id]))
        __perf_sw_event(event_id, nr, regs, addr);
}

跟踪点集成

/* 将跟踪点事件连接到 perf */
static int perf_trace_event_init(struct perf_event *event)
{
    struct trace_event_call *tp_event;

    tp_event = event->tp_event;
    if (!tp_event)
        return -ENOENT;

    /* 注册跟踪点处理器 */
    return perf_trace_event_reg(tp_event, event);
}

15.2.4 性能数据采集与分析

采样机制

/* 周期性采样配置 */
struct perf_event_attr attr = {
    .type           = PERF_TYPE_HARDWARE,
    .config         = PERF_COUNT_HW_CPU_CYCLES,
    .sample_period  = 100000,  /* 每 100000 个周期采样一次 */
    .sample_type    = PERF_SAMPLE_IP | PERF_SAMPLE_TID |
                      PERF_SAMPLE_TIME | PERF_SAMPLE_CALLCHAIN,
    .exclude_kernel = 0,
    .exclude_user   = 0,
};

环形缓冲区管理

/* kernel/events/ring_buffer.c */
struct ring_buffer {
    atomic_t                poll;
    local_t                 head;          /* 写入位置 */
    unsigned long           nest;
    local_t                 events;
    unsigned long           wakeup_stamp;
    local_t                 lost;

    long                    watermark;
    long                    aux_watermark;

    struct perf_event_mmap_page *user_page;  /* 用户映射页 */
    void                    *data_pages[];    /* 数据页数组 */
};

/* 写入采样数据 */
void perf_output_sample(struct perf_output_handle *handle,
                        struct perf_event_header *header,
                        struct perf_sample_data *data,
                        struct perf_event *event)
{
    /* 写入采样头 */
    perf_output_put(handle, *header);

    /* 写入 IP */
    if (sample_type & PERF_SAMPLE_IP)
        perf_output_put(handle, data->ip);

    /* 写入 TID */
    if (sample_type & PERF_SAMPLE_TID)
        perf_output_put(handle, data->tid_entry);

    /* 写入调用栈 */
    if (sample_type & PERF_SAMPLE_CALLCHAIN) {
        size_t size = data->callchain->nr;
        perf_output_put(handle, data->callchain->nr);
        perf_output_copy(handle, data->callchain->ips, size * 8);
    }
}

使用 perf 工具

# CPU 性能分析
perf record -F 99 -a -g -- sleep 10
perf report

# 缓存分析
perf stat -e cache-references,cache-misses ./program

# 调度延迟分析
perf sched record sleep 10
perf sched latency

# 锁竞争分析
perf lock record ./program
perf lock report

15.3 内核调试技术

内核调试是系统开发中最具挑战性的任务之一。与用户空间程序不同,内核运行在特权级别,一个错误就可能导致系统崩溃。Linux 提供了从源码级调试到崩溃分析的完整工具链。

15.3.1 KGDB 内核调试器

KGDB 是 Linux 内核的源码级调试器,通过串口或网络连接远程 GDB,实现对运行中内核的调试。它由 Jason Wessel 维护,从 2.6.26 版本开始进入主线。

架构设计

KGDB 采用客户端-服务器架构,内核作为调试存根(stub),外部 GDB 作为调试器:

    开发机器                     目标机器
    +-------+                   +--------+
    |  GDB  | <--- 串口/网络 --> |  KGDB  |
    +-------+                   | Kernel |
        |                       +--------+
        v                           |
    源码符号                     断点/单步

核心组件

/* kernel/debug/debug_core.c */
struct kgdb_state {
    int                     cpu;
    int                     pass_exception;
    unsigned long           threadid;
    long                    kgdb_usethreadid;
    struct pt_regs          *linux_regs;
    atomic_t                *send_ready;
};

/* KGDB 架构接口 */
struct kgdb_arch {
    unsigned char           gdb_bpt_instr[BREAK_INSTR_SIZE];
    unsigned long           flags;

    int  (*set_breakpoint)(unsigned long addr, char *saved_instr);
    int  (*remove_breakpoint)(unsigned long addr, char *bundle);
    int  (*set_hw_breakpoint)(unsigned long addr, int len, enum kgdb_bptype type);
    int  (*remove_hw_breakpoint)(unsigned long addr, int len, enum kgdb_bptype type);
    void (*disable_hw_break)(struct pt_regs *regs);
    void (*correct_hw_break)(void);
    void (*enable_nmi)(bool on);
};

断点实现机制

KGDB 支持软件断点和硬件断点:

/* 软件断点:替换指令为 int3 (x86) */
int kgdb_arch_set_breakpoint(struct kgdb_bkpt *bpt)
{
    int err;
    char opc[BREAK_INSTR_SIZE];

    /* 保存原始指令 */
    err = probe_kernel_read(opc, (char *)bpt->bpt_addr, BREAK_INSTR_SIZE);
    if (err)
        return err;
    memcpy(bpt->saved_instr, opc, BREAK_INSTR_SIZE);

    /* 写入断点指令 */
    err = probe_kernel_write((char *)bpt->bpt_addr, 
                            arch_kgdb_ops.gdb_bpt_instr, BREAK_INSTR_SIZE);
    return err;
}

/* 硬件断点:使用 CPU 调试寄存器 */
static int hw_break_reserve_slot(int breakno)
{
    int cpu, i;
    struct perf_event **pevent;

    for_each_online_cpu(cpu) {
        pevent = per_cpu_ptr(breakinfo[breakno].pev, cpu);
        *pevent = register_wide_hw_breakpoint(&attr, NULL, NULL);
        if (IS_ERR(*pevent))
            return PTR_ERR(*pevent);
    }
    return 0;
}

异常处理流程

当触发断点或收到调试信号时,KGDB 接管系统:

/* 进入 KGDB 的核心函数 */
static int kgdb_cpu_enter(struct kgdb_state *ks, struct pt_regs *regs,
                         int exception_state)
{
    unsigned long flags;
    int cpu = raw_smp_processor_id();

    /* 1. 停止其他 CPU */
    if (!kgdb_single_step)
        kgdb_roundup_cpus(flags);

    /* 2. 禁用中断 */
    local_irq_save(flags);

    /* 3. 进入调试循环 */
    while (1) {
        /* 等待 GDB 命令 */
        error = gdb_serial_stub(ks);

        if (error == DBG_PASS_EVENT) {
            /* 继续执行 */
            break;
        }

        /* 处理命令 */
        switch (ks->gdb_cmd) {
        case 'c':  /* continue */
        case 's':  /* single step */
            return 0;
        }
    }

    /* 4. 恢复执行 */
    kgdb_restore_cpus();
    local_irq_restore(flags);
}

使用配置

# 内核配置
CONFIG_KGDB=y
CONFIG_KGDB_SERIAL_CONSOLE=y
CONFIG_KGDB_KDB=y

# 启动参数
kgdboc=ttyS0,115200 kgdbwait

# GDB 连接
(gdb) target remote /dev/ttyS0
(gdb) set debug remote 1
(gdb) break sys_open
(gdb) continue

15.3.2 crash 工具与内核转储

crash 是 Red Hat 开发的内核崩溃分析工具,由 Dave Anderson 维护。它可以分析运行中的系统或崩溃转储文件(vmcore)。

工作原理

crash 通过解析内核符号表和数据结构,提供类似 GDB 的调试接口:

    vmcore/live system
           |
    +------v------+
    | crash tool  |
    +-------------+
           |
    解析内核结构
           |
    +------v------+
    | 符号表      |
    | debuginfo   |
    +-------------+

核心功能实现

/* crash 内部数据结构解析 */
struct task_context {
    ulong task;           /* task_struct 地址 */
    ulong thread_info;    /* thread_info 地址 */
    ulong pid;
    char comm[TASK_COMM_LEN];
    int processor;
    ulong ptask;          /* 父进程 */
    ulong mm_struct;      /* 内存描述符 */
    struct task_context *tc_next;
};

/* 读取内核内存 */
static int readmem(ulonglong addr, int memtype, void *buffer, 
                   long size, char *type, ulong error_handle)
{
    switch (memtype) {
    case KVADDR:
        /* 内核虚拟地址 */
        return read_kdump(addr, buffer, size);
    case PHYSADDR:
        /* 物理地址 */
        return read_physmem(addr, buffer, size);
    case FILEADDR:
        /* 文件偏移 */
        return read_vmcore(addr, buffer, size);
    }
}

常用命令实现

/* bt - 显示调用栈 */
void cmd_bt(void)
{
    struct bt_info bt_info, *bt;
    struct task_context *tc;
    ulong sp, ip;

    /* 获取当前任务上下文 */
    tc = CURRENT_CONTEXT();

    /* 初始化栈帧信息 */
    bt = &bt_info;
    bt->task = tc->task;
    bt->stackbase = GET_STACKBASE(tc->task);

    /* 展开调用栈 */
    sp = tc->thread_info + OFFSET(thread_info_cpu_context);
    ip = GET_PC(sp);

    while (sp < bt->stackbase) {
        /* 解析栈帧 */
        frame = GET_FRAME_POINTER(sp);
        function = closest_symbol(ip);

        fprintf(fp, "#%d [%016lx] %s at %016lx\n", 
                level++, sp, function, ip);

        /* 下一帧 */
        sp = frame;
        ip = GET_RETURN_ADDRESS(frame);
    }
}

使用示例

# 分析 vmcore
crash /usr/lib/debug/lib/modules/$(uname -r)/vmlinux /var/crash/vmcore

# 常用命令
crash> sys          # 系统信息
crash> bt           # 当前任务调用栈
crash> ps           # 进程列表
crash> log          # 内核日志
crash> dis -l function_name  # 反汇编
crash> struct task_struct ffff88007c8a0000  # 查看数据结构
crash> foreach bt   # 所有任务的调用栈

15.3.3 kdump 机制

kdump 是 Linux 的内核崩溃转储机制,使用 kexec 在系统崩溃时启动第二个内核来收集转储。

工作流程

    生产内核崩溃
         |
         v
    触发 panic()
         |
         v
    machine_kexec()
         |
         v
    加载捕获内核
         |
         v
    捕获内核启动
         |
         v
    收集 vmcore
         |
         v
    保存到磁盘

实现机制

/* kernel/kexec_core.c */
void __crash_kexec(struct pt_regs *regs)
{
    /* 已经有 CPU 在处理 */
    if (mutex_trylock(&kexec_mutex)) {
        if (kexec_crash_image) {
            struct pt_regs fixed_regs;

            /* 保存崩溃时的寄存器 */
            crash_setup_regs(&fixed_regs, regs);
            crash_save_vmcoreinfo();

            /* 关闭其他 CPU */
            machine_crash_shutdown(&fixed_regs);

            /* 跳转到捕获内核 */
            machine_kexec(kexec_crash_image);
        }
        mutex_unlock(&kexec_mutex);
    }
}

/* 保留内存区域 */
static int __init reserve_crashkernel(void)
{
    unsigned long long crash_size, crash_base;

    /* 解析 crashkernel= 参数 */
    ret = parse_crashkernel(boot_command_line, memblock_phys_mem_size(),
                           &crash_size, &crash_base);

    /* 预留内存 */
    memblock_reserve(crash_base, crash_size);

    /* 设置全局变量 */
    crashk_res.start = crash_base;
    crashk_res.end = crash_base + crash_size - 1;
}

vmcore 生成

/* fs/proc/vmcore.c */
static int __init vmcore_init(void)
{
    int rc = 0;

    /* 解析 ELF 头 */
    rc = parse_crash_elf_headers();
    if (rc)
        return rc;

    /* 创建 /proc/vmcore */
    proc_vmcore = proc_create("vmcore", S_IRUSR, NULL, &proc_vmcore_operations);

    return 0;
}

/* 读取 vmcore */
static ssize_t read_vmcore(struct file *file, char __user *buffer,
                          size_t buflen, loff_t *fpos)
{
    /* 读取 ELF 头 */
    if (*fpos < elfcorebuf_sz) {
        tsz = min(elfcorebuf_sz - (size_t)*fpos, buflen);
        if (copy_to_user(buffer, elfcorebuf + *fpos, tsz))
            return -EFAULT;
    }

    /* 读取程序段 */
    list_for_each_entry(m, &vmcore_list, list) {
        if (*fpos < m->offset + m->size) {
            tsz = min_t(size_t, m->offset + m->size - *fpos, buflen);
            start = m->paddr + (*fpos - m->offset);

            /* 拷贝物理内存 */
            if (copy_oldmem_page(pfn, buffer, tsz, offset))
                return -EFAULT;
        }
    }
}

配置使用

# 安装 kdump 工具
yum install kexec-tools

# 配置预留内存
# /etc/default/grub
GRUB_CMDLINE_LINUX="crashkernel=256M"

# 启动 kdump 服务
systemctl enable kdump
systemctl start kdump

# 测试崩溃
echo 1 > /proc/sys/kernel/sysrq
echo c > /proc/sysrq-trigger

15.3.4 动态打印与 pr_debug

动态调试(dynamic debug)允许在运行时控制调试信息的输出,无需重新编译内核。

实现原理

/* include/linux/dynamic_debug.h */
struct _ddebug {
    const char *modname;
    const char *function;
    const char *filename;
    const char *format;
    unsigned int lineno:18;
    unsigned int flags:8;
} __attribute__((aligned(8)));

/* pr_debug 宏展开 */
#define pr_debug(fmt, ...)                                      \
    dynamic_pr_debug(fmt, ##__VA_ARGS__)

#define dynamic_pr_debug(fmt, ...)                              \
do {                                                            \
    DEFINE_DYNAMIC_DEBUG_METADATA(descriptor, fmt);             \
    if (DYNAMIC_DEBUG_BRANCH(descriptor))                       \
        __dynamic_pr_debug(&descriptor, pr_fmt(fmt),           \
                          ##__VA_ARGS__);                       \
} while (0)

控制接口

/* lib/dynamic_debug.c */
static int ddebug_proc_write(struct file *file, const char __user *ubuf,
                            size_t len, loff_t *offp)
{
    char *words[MAXWORDS];
    struct ddebug_query query;

    /* 解析控制命令 */
    nwords = ddebug_tokenize(tmpbuf, words, MAXWORDS);

    /* 解析查询条件 */
    if (ddebug_parse_query(words, nwords, &query))
        return -EINVAL;

    /* 应用修改 */
    nfound = ddebug_change(&query, flags, mask);

    return len;
}

/* 修改调试标志 */
static int ddebug_change(const struct ddebug_query *query,
                        unsigned int flags, unsigned int mask)
{
    struct ddebug_table *dt;
    struct _ddebug *dp;

    list_for_each_entry(dt, &ddebug_tables, link) {
        for (dp = dt->ddebugs; dp < dt->ddebugs + dt->num_ddebugs; dp++) {
            /* 匹配查询条件 */
            if (!match_wildcard(query->module, dp->modname))
                continue;
            if (!match_wildcard(query->function, dp->function))
                continue;
            if (!match_wildcard(query->filename, dp->filename))
                continue;

            /* 更新标志 */
            newflags = (dp->flags & ~mask) | flags;
            if (newflags != dp->flags) {
                dp->flags = newflags;
                matched++;
            }
        }
    }
    return matched;
}

使用方法

# 启用特定文件的调试
echo 'file tcp_input.c +p' > /sys/kernel/debug/dynamic_debug/control

# 启用特定函数
echo 'func tcp_receive_skb +p' > /sys/kernel/debug/dynamic_debug/control

# 启用模块调试
echo 'module e1000e +p' > /sys/kernel/debug/dynamic_debug/control

# 查看当前设置
cat /sys/kernel/debug/dynamic_debug/control

# 带条件的调试
echo 'file drivers/net/* line 1-200 +p' > /sys/kernel/debug/dynamic_debug/control

15.4 动态追踪工具

动态追踪技术让我们能够在生产环境中安全地观测系统行为,无需停机或重新编译。从 SystemTap 到 eBPF 的演进,标志着 Linux 可观测性的革命性进步。

15.4.1 SystemTap 框架

SystemTap 是 Red Hat 开发的动态追踪系统,通过将高级脚本语言编译成内核模块来实现系统追踪。尽管现在 eBPF 更受欢迎,但 SystemTap 仍在许多企业环境中使用。

架构概览

    SystemTap 脚本 (.stp)
           |
    +------v------+
    |   stap 编译器|  <- 语法分析、语义检查
    +------+------+
           |
    +------v------+
    | C 代码生成   |  <- 生成内核模块源码
    +------+------+
           |
    +------v------+
    |  gcc 编译    |  <- 编译成 .ko 模块
    +------+------+
           |
    +------v------+
    | staprun 加载 |  <- 插入内核执行
    +------+------+
           |
    运行时数据收集

脚本语言特性

/* SystemTap 脚本示例 */
global reads

probe vfs.read {
    reads[execname(), pid()] <<< $count
}

probe timer.s(5) {
    printf("Top processes reading:\n")
    foreach ([name, pid] in reads- limit 10) {
        printf("%s[%d]: %d bytes\n", 
               name, pid, @sum(reads[name, pid]))
    }
    delete reads
}

/* 探针语法 */
probe kernel.function("tcp_sendmsg") {
    printf("TCP send from %s: %d bytes\n", 
           execname(), $size)
}

probe kernel.function("tcp_sendmsg").return {
    if ($return < 0)
        printf("TCP send failed: %d\n", $return)
}

运行时实现

/* runtime/transport/transport.c */
static int _stp_transport_init(void)
{
    /* 创建 relayfs 通道 */
    _stp_relay_data.rchan = relay_open("trace", 
                                       _stp_get_module_dir(),
                                       _stp_bufsize,
                                       n_subbufs,
                                       &_stp_relay_callbacks,
                                       NULL);

    /* 注册探针 */
    for (i = 0; i < _stp_num_probes; i++) {
        struct stap_probe *p = &_stp_probes[i];
        register_kprobe(&p->kprobe);
    }
}

/* 探针处理器模板 */
static int probe_NNNN(struct kprobe *inst, struct pt_regs *regs)
{
    struct context *c = per_cpu_ptr(contexts, smp_processor_id());

    /* 获取上下文 */
    c->regs = regs;
    c->pi = inst;

    /* 执行用户脚本逻辑 */
    probe_NNNN_enter(c);

    /* 输出数据 */
    _stp_print_flush();

    return 0;
}

15.4.2 eBPF 架构与原理

eBPF(extended Berkeley Packet Filter)是 Linux 内核的革命性技术,提供了安全、高效的内核可编程能力。由 Alexei Starovoitov 主导开发,已成为云原生可观测性的基石。

eBPF 架构

    用户空间                     内核空间
    +--------+                +------------+
    | eBPF   |                | eBPF 程序  |
    | 程序   | -- bpf() -->   | (字节码)   |
    +--------+                +-----+------+
                                   |
                              +----v-----+
                              | Verifier |  <- 安全验证
                              +----+-----+
                                   |
                              +----v-----+
                              | JIT 编译 |  <- 编译成机器码
                              +----+-----+
                                   |
                              +----v-----+
                              | 执行引擎 |  <- 在钩子点执行
                              +----------+

eBPF 指令集

/* include/uapi/linux/bpf.h */
struct bpf_insn {
    __u8    code;    /* 操作码 */
    __u8    dst_reg:4;  /* 目标寄存器 */
    __u8    src_reg:4;  /* 源寄存器 */
    __s16   off;     /* 偏移 */
    __s32   imm;     /* 立即数 */
};

/* eBPF 寄存器 */
#define BPF_REG_0   0   /* 返回值 */
#define BPF_REG_1   1   /* 参数 1 */
#define BPF_REG_2   2   /* 参数 2 */
#define BPF_REG_3   3   /* 参数 3 */
#define BPF_REG_4   4   /* 参数 4 */
#define BPF_REG_5   5   /* 参数 5 */
#define BPF_REG_6   6   /* callee saved */
#define BPF_REG_7   7   /* callee saved */
#define BPF_REG_8   8   /* callee saved */
#define BPF_REG_9   9   /* callee saved */
#define BPF_REG_10  10  /* 栈指针 */

/* 指令示例 */
BPF_MOV64_REG(BPF_REG_6, BPF_REG_1),     /* r6 = r1 */
BPF_LD_ABS(BPF_B, ETH_HLEN + 0),         /* 加载字节 */
BPF_JMP_IMM(BPF_JEQ, BPF_REG_0, 0x80, 2), /* if r0 == 0x80 jump */
BPF_MOV64_IMM(BPF_REG_0, 0),             /* r0 = 0 */
BPF_EXIT_INSN(),                          /* return r0 */

验证器(Verifier)

eBPF 验证器确保程序安全性,防止内核崩溃:

/* kernel/bpf/verifier.c */
static int do_check(struct bpf_verifier_env *env)
{
    struct bpf_insn *insns = env->prog->insnsi;
    struct bpf_verifier_state *state;
    int insn_cnt = env->prog->len;

    /* 遍历所有可能的执行路径 */
    for (i = 0; i < insn_cnt; i++) {
        struct bpf_insn *insn = &insns[i];
        u8 class = BPF_CLASS(insn->code);

        /* 检查内存访问 */
        if (class == BPF_LDX || class == BPF_STX) {
            err = check_mem_access(env, insn_idx, regno, off, size,
                                 t, value_regno, strict_alignment);
            if (err)
                return err;
        }

        /* 检查函数调用 */
        if (insn->code == (BPF_JMP | BPF_CALL)) {
            err = check_helper_call(env, insn->imm, insn_idx);
            if (err)
                return err;
        }

        /* 检查跳转 */
        if (BPF_CLASS(insn->code) == BPF_JMP) {
            err = check_cond_jmp_op(env, insn, &insn_idx);
            if (err)
                return err;
        }
    }

    /* 确保程序有返回 */
    if (state->curframe) {
        verbose(env, "program didn't return\n");
        return -EINVAL;
    }

    return 0;
}

/* 检查内存访问安全性 */
static int check_mem_access(struct bpf_verifier_env *env, int insn_idx,
                           u32 regno, int off, int bpf_size,
                           enum bpf_access_type t,
                           int value_regno, bool strict_alignment)
{
    struct bpf_reg_state *regs = cur_regs(env);
    struct bpf_reg_state *reg = regs + regno;

    /* 检查指针类型 */
    switch (reg->type) {
    case PTR_TO_MAP_VALUE:
        /* 检查 map 访问边界 */
        if (check_map_access(env, regno, off, size, false))
            return -EACCES;
        break;
    case PTR_TO_CTX:
        /* 检查上下文访问 */
        if (check_ctx_access(env, insn_idx, off, size, t, &reg_type))
            return -EACCES;
        break;
    case PTR_TO_STACK:
        /* 检查栈访问 */
        if (check_stack_boundary(env, regno, off, size, access_type))
            return -EACCES;
        break;
    }
    return 0;
}

JIT 编译器

将 eBPF 字节码编译成本地机器码:

/* arch/x86/net/bpf_jit_comp.c */
static int do_jit(struct bpf_prog *bpf_prog, int *addrs, u8 *image,
                 int oldproglen, struct jit_context *ctx)
{
    struct bpf_insn *insn = bpf_prog->insnsi;
    int insn_cnt = bpf_prog->len;
    u8 *prog = temp;

    for (i = 0; i < insn_cnt; i++, insn++) {
        const s32 imm32 = insn->imm;
        u32 dst_reg = insn->dst_reg;
        u32 src_reg = insn->src_reg;
        u8 b2 = 0, b3 = 0;
        s64 jmp_offset;
        u8 jmp_cond;

        switch (insn->code) {
        /* ALU 操作 */
        case BPF_ALU64 | BPF_ADD | BPF_X:
            EMIT3(0x48, 0x01, add_2reg(0xC0, dst_reg, src_reg));
            break;

        /* 内存加载 */
        case BPF_LDX | BPF_MEM | BPF_DW:
            EMIT3(0x48, 0x8B, add_2reg(0x40, dst_reg, src_reg));
            EMIT1(insn->off);
            break;

        /* 函数调用 */
        case BPF_JMP | BPF_CALL:
            func = (u8 *) __bpf_call_base + imm32;
            jmp_offset = func - (prog + 5);
            EMIT1_off32(0xE8, jmp_offset);
            break;

        /* 条件跳转 */
        case BPF_JMP | BPF_JEQ | BPF_X:
            EMIT3(0x48, 0x39, add_2reg(0xC0, dst_reg, src_reg));
            goto emit_cond_jmp;
        }
    }
}

eBPF Maps

Maps 是 eBPF 程序与用户空间通信的关键机制:

/* kernel/bpf/hashtab.c */
struct bpf_htab {
    struct bpf_map map;
    struct bucket *buckets;
    void *elems;
    atomic_t count;
    u32 n_buckets;
    u32 elem_size;
    struct lock_class_key lockdep_key;
};

/* 查找元素 */
static void *htab_map_lookup_elem(struct bpf_map *map, void *key)
{
    struct bpf_htab *htab = container_of(map, struct bpf_htab, map);
    struct hlist_nulls_head *head;
    struct htab_elem *l;
    u32 hash, key_size;

    /* 计算哈希值 */
    hash = htab_map_hash(key, key_size, htab->hashrnd);
    head = select_bucket(htab, hash);

    /* 查找链表 */
    hlist_nulls_for_each_entry_rcu(l, n, head, hash_node) {
        if (l->hash == hash && !memcmp(&l->key, key, key_size))
            return l->key + round_up(key_size, 8);
    }

    return NULL;
}

15.4.3 bpftrace 高级追踪

bpftrace 是 Brendan Gregg 和 Alastair Robertson 开发的高级追踪语言,提供了类似 DTrace 的简洁语法。

语言特性

#!/usr/bin/env bpftrace

/* 追踪系统调用延迟 */
tracepoint:raw_syscalls:sys_enter
{
    @start[tid] = nsecs;
}

tracepoint:raw_syscalls:sys_exit
/@start[tid]/
{
    @ns[comm] = hist((nsecs - @start[tid]) / 1000);
    delete(@start[tid]);
}

/* 追踪 TCP 重传 */
kprobe:tcp_retransmit_skb
{
    @retrans[comm, pid] = count();
}

/* 追踪块 I/O 延迟 */
kprobe:blk_account_io_start
{
    @iotime[arg0] = nsecs;
}

kprobe:blk_account_io_done
/@iotime[arg0]/
{
    @usecs = hist((nsecs - @iotime[arg0]) / 1000);
    delete(@iotime[arg0]);
}

END
{
    clear(@iotime);
    clear(@start);
}

内部实现

bpftrace 将高级语言编译成 eBPF 字节码:

/* AST 到 LLVM IR 的转换 */
void CodegenLLVM::visit(Call &call)
{
    if (call.func == "count") {
        // 生成计数器更新代码
        auto *map_lookup = b_.CreateCall(lookup_func, 
                                        {map_ptr, key_ptr});
        auto *counter = b_.CreateLoad(map_lookup);
        auto *incremented = b_.CreateAdd(counter, 
                                        b_.getInt64(1));
        b_.CreateStore(incremented, map_lookup);
    }
    else if (call.func == "hist") {
        // 生成直方图更新代码
        generate_histogram_update(call);
    }
}

15.4.4 性能优化案例

案例1:追踪内存分配热点

/* eBPF C 程序 */
#include <uapi/linux/ptrace.h>
#include <linux/mm.h>

BPF_HASH(allocs, u64, u64);
BPF_STACK_TRACE(stack_traces, 1024);

int trace_kmalloc(struct pt_regs *ctx, size_t size)
{
    u64 pid = bpf_get_current_pid_tgid();
    u64 stackid = stack_traces.get_stackid(ctx, 0);

    struct alloc_info info = {};
    info.size = size;
    info.stackid = stackid;
    info.timestamp = bpf_ktime_get_ns();

    allocs.update(&pid, &info);
    return 0;
}

案例2:网络延迟分析

#!/usr/bin/env bpftrace

/* TCP 连接延迟分析 */
kprobe:tcp_v4_connect
{
    @start[tid] = nsecs;
    @daddr[tid] = ((struct sockaddr_in *)arg1)->sin_addr.s_addr;
}

kretprobe:tcp_v4_connect
/@start[tid]/
{
    $lat = (nsecs - @start[tid]) / 1000;
    @conn_lat = hist($lat);

    if ($lat > 1000000) {  // > 1
        printf("Slow connection to %s: %d us\n",
               ntop(@daddr[tid]), $lat);
    }

    delete(@start[tid]);
    delete(@daddr[tid]);
}

案例3:文件系统性能瓶颈

/* 追踪 ext4 写入延迟 */
struct data_t {
    u64 ts;
    u64 delta;
    u32 pid;
    char comm[16];
    char filename[32];
};

BPF_PERF_OUTPUT(events);
BPF_HASH(start, u32);

int trace_ext4_write_begin(struct pt_regs *ctx)
{
    u32 pid = bpf_get_current_pid_tgid();
    u64 ts = bpf_ktime_get_ns();
    start.update(&pid, &ts);
    return 0;
}

int trace_ext4_write_end(struct pt_regs *ctx)
{
    u32 pid = bpf_get_current_pid_tgid();
    u64 *tsp = start.lookup(&pid);
    if (!tsp)
        return 0;

    struct data_t data = {};
    data.ts = bpf_ktime_get_ns();
    data.delta = data.ts - *tsp;
    data.pid = pid;
    bpf_get_current_comm(&data.comm, sizeof(data.comm));

    events.perf_submit(ctx, &data, sizeof(data));
    start.delete(&pid);
    return 0;
}

15.5 可视化分析工具

  • 15.5.1 火焰图生成与分析
  • 15.5.2 延迟热图
  • 15.5.3 调用图与依赖分析
  • 15.5.4 实时性能监控

本章小结

练习题

常见陷阱与错误

最佳实践检查清单