欢迎光临
我们一直在努力

《从零手写操作系统 (62):性能调优与实战——火焰图、perf、eBPF与内核追踪》

《从零手写操作系统 (62):性能调优与实战——火焰图、perf、eBPF与内核追踪》

前言:从"能跑"到"跑得快"

        前61章我们完成了操作系统的核心功能:进程调度、内存管理、文件系统、网络协议栈、虚拟化。但一个生产级系统还需要解决性能问题:

  • 系统调用延迟高?
  • CPU缓存命中率低?
  • 内存分配碎片化?
  • 锁竞争激烈?
  • IO吞吐不足?

        本章介绍Linux内核性能调优的完整工具链,从观测到优化,形成闭环。


性能调优方法论
Brendan Gregg的性能调优黄金法则
  • 观测先行:不要猜测,用数据说话
  • USE方法:对每个资源检查
    • Utilization(利用率):资源忙的时间占比
    • Saturation(饱和度):队列长度/等待时间
    • Errors(错误):错误计数
  • 从宏观到微观:系统级 → 进程级 → 函数级 → 指令级
  • 性能分析工具全景图

    层级 工具
    ─────────────────────────────────────────────
    系统级 top, vmstat, iostat, sar, dstat
    进程级 ps, pidstat, strace, lsof
    函数级 perf, ftrace, SystemTap
    指令级 objdump, gdb, valgrind
    内核追踪 eBPF/bcc, bpftrace, kprobe/uprobe
    可视化 Flame Graph, FlameScope, offcputime


    实战代码
    perf基础:CPU性能分析

    # ★ 采样CPU热点函数(99Hz,持续10秒)
    perf record -F 99 -a -g — sleep 10
    perf report

    # ★ 查看CPU缓存命中率(硬件性能计数器)
    perf stat -e cache-references,cache-misses,L1-dcache-loads,L1-dcache-load-misses \\
    -e LLC-loads,LLC-load-misses \\
    -a sleep 10

    # ★ 分析系统调用开销
    perf record -e syscalls:sys_enter_* -a — sleep 10
    perf report

    # ★ 查看上下文切换频率
    perf record -e context-switches -a — sleep 10
    perf report

    // ★ 使用perf_event_open系统调用进行自定义采样
    #include <linux/perf_event.h>
    #include <sys/ioctl.h>

    int setup_perf_event(void) {
    struct perf_event_attr attr = {
    .type = PERF_TYPE_HARDWARE,
    .size = sizeof(attr),
    .config = PERF_COUNT_HW_CPU_CYCLES, // CPU周期
    .sample_freq = 99, // 99Hz采样
    .disabled = 1,
    .exclude_kernel = 0,
    .exclude_hv = 1,
    };

    // 监控当前进程
    int fd = perf_event_open(&attr, 0, -1, -1, 0);
    if (fd < 0) {
    perror("perf_event_open");
    return -1;
    }

    // 启动采样
    ioctl(fd, PERF_EVENT_IOC_ENABLE, 0);

    return fd;
    }

    // ★ 读取采样数据
    void read_perf_data(int fd) {
    struct perf_event_mmap_page *header;
    void *data_page;

    header = mmap(NULL, PAGE_SIZE, PROT_READ, MAP_SHARED, fd, 0);
    data_page = mmap(NULL, PAGE_SIZE, PROT_READ, MAP_SHARED, fd, PAGE_SIZE);

    // 读取环形缓冲区中的采样数据
    u64 head = header->data_head;
    while (header->data_tail != head) {
    struct perf_sample *sample = data_page + (header->data_tail % PAGE_SIZE);
    printf("IP: %lx, period: %llu\\n", sample->ip, sample->period);
    header->data_tail += sizeof(*sample);
    }
    }

    生成火焰图(Flame Graph)

            火焰图是可视化CPU热点的利器,宽度代表耗时占比,高度代表调用栈深度。

    # ★ 1. 采集带调用栈的数据
    perf record -F 99 -a -g — sleep 30

    # ★ 2. 导出为文本格式
    perf script > perf.out

    # ★ 3. 使用FlameGraph工具生成SVG
    git clone https://github.com/brendangregg/FlameGraph.git
    cd FlameGraph
    ./stackcollapse-perf.pl < ../perf.out | ./flamegraph.pl > flame.svg

    # ★ 4. 生成off-CPU火焰图(分析阻塞/等待)
    perf record -e sched:sched_switch -a -g — sleep 30
    perf script | ./stackcollapse-perf.pl | ./flamegraph.pl –color=io > offcpu.svg

    # ★ 5. 差异化火焰图(对比优化前后)
    ./difffolded.pl before.folded after.folded | ./flamegraph.pl > diff.svg

    # ★ 自定义火焰图生成器(Python实现核心逻辑)
    import sys
    from collections import defaultdict

    def parse_perf_script(filename):
    """解析perf script输出,提取调用栈"""
    stacks = defaultdict(int)
    current_stack = []

    with open(filename) as f:
    for line in f:
    if line.startswith('\\t'):
    # 调用栈行:提取函数名
    func = line.split()[0]
    current_stack.append(func)
    elif current_stack:
    # 采样行:记录栈
    key = ';'.join(reversed(current_stack))
    stacks[key] += 1
    current_stack = []

    return stacks

    def generate_flamegraph(stacks, output_file):
    """生成火焰图数据(简化版)"""
    # 按宽度排序,宽的在前
    sorted_stacks = sorted(stacks.items(), key=lambda x: -x[1])

    with open(output_file, 'w') as f:
    for stack, count in sorted_stacks[:100]: # 前100个热点
    f.write(f"{stack} {count}\\n")

    if __name__ == '__main__':
    stacks = parse_perf_script(sys.argv[1])
    generate_flamegraph(stacks, 'flamegraph.txt')

    eBPF:内核级动态追踪

            eBPF允许在内核中运行沙箱程序,无需修改内核代码即可追踪系统行为。

    // ★ eBPF程序:追踪系统调用延迟
    // 编译:clang -O2 -target bpf -c syscall_latency.bpf.c -o syscall_latency.bpf.o

    #include <linux/bpf.h>
    #include <bpf/bpf_helpers.h>
    #include <linux/sched.h>

    // ★ 定义eBPF Map(哈希表)
    struct {
    __uint(type, BPF_MAP_TYPE_HASH);
    __uint(max_entries, 10240);
    __type(key, u32); // pid
    __type(value, u64); // 开始时间
    } start_map SEC(".maps");

    // ★ 定义eBPF Map(输出)
    struct {
    __uint(type, BPF_MAP_TYPE_PERF_EVENT_ARRAY);
    __uint(key_size, sizeof(u32));
    __uint(value_size, sizeof(u32));
    } events SEC(".maps");

    struct event {
    u32 pid;
    u64 duration_ns;
    char comm[TASK_COMM_LEN];
    };

    // ★ kprobe:系统调用入口
    SEC("kprobe/sys_read")
    int trace_read_entry(struct pt_regs *ctx) {
    u32 pid = bpf_get_current_pid_tgid() >> 32;
    u64 ts = bpf_ktime_get_ns();
    bpf_map_update_elem(&start_map, &pid, &ts, BPF_ANY);
    return 0;
    }

    // ★ kretprobe:系统调用返回
    SEC("kretprobe/sys_read")
    int trace_read_return(struct pt_regs *ctx) {
    u32 pid = bpf_get_current_pid_tgid() >> 32;
    u64 *start_tsp = bpf_map_lookup_elem(&start_map, &pid);

    if (start_tsp) {
    u64 duration = bpf_ktime_get_ns() – *start_tsp;

    // 过滤短调用(< 1ms)
    if (duration > 1000000) {
    struct event evt = {};
    evt.pid = pid;
    evt.duration_ns = duration;
    bpf_get_current_comm(&evt.comm, sizeof(evt.comm));

    // 发送事件到用户空间
    bpf_perf_event_output(ctx, &events, BPF_F_CURRENT_CPU,
    &evt, sizeof(evt));
    }

    bpf_map_delete_elem(&start_map, &pid);
    }
    return 0;
    }

    char LICENSE[] SEC("license") = "GPL";

    # ★ 用户空间程序:加载并读取eBPF数据
    from bcc import BPF
    import ctypes as ct

    # 编译并加载eBPF程序
    b = BPF(src_file="syscall_latency.bpf.c")

    # ★ 定义事件结构
    class Event(ct.Structure):
    _fields_ = [
    ("pid", ct.c_uint),
    ("duration_ns", ct.c_ulonglong),
    ("comm", ct.c_char * 16),
    ]

    # ★ 打开perf event缓冲区
    b["events"].open_perf_buffer(lambda cpu, data, size:
    print_event(cpu, b["events"].event(data)))

    def print_event(cpu, data):
    event = ct.cast(data, ct.POINTER(Event)).contents
    print(f"[{event.comm.decode()}] PID={event.pid} "
    f"duration={event.duration_ns/1e6:.2f}ms")

    # ★ 循环读取事件
    while True:
    try:
    b.perf_buffer_poll()
    except KeyboardInterrupt:
    exit()

    ftrace:内核函数追踪

    # ★ 启用ftrace
    mount -t tracefs nodev /sys/kernel/debug/tracing
    cd /sys/kernel/debug/tracing

    # ★ 追踪特定函数
    echo sys_read > set_ftrace_filter
    echo function > current_tracer
    cat trace_pipe

    # ★ 追踪函数调用图(带耗时)
    echo function_graph > current_tracer
    echo vfs_read >> set_graph_function
    cat trace_pipe

    # ★ 追踪调度延迟
    echo sched_wakeup > set_event
    echo sched_switch >> set_event
    cat trace_pipe

    # ★ 使用trace-cmd简化操作
    trace-cmd record -p function_graph -g vfs_read sleep 1
    trace-cmd report

    // ★ 在内核模块中使用ftrace
    #include <linux/ftrace.h>

    static int (*orig_vfs_read)(struct file *, char __user *, size_t, loff_t *);

    // ★ kprobe:拦截vfs_read
    static int kprobe_vfs_read(struct kprobe *p, struct pt_regs *regs) {
    struct file *file = (struct file *)regs->di;
    pr_info("vfs_read called: file=%s\\n", file->f_path.dentry->d_name.name);
    return 0;
    }

    static struct kprobe kp = {
    .symbol_name = "vfs_read",
    .pre_handler = kprobe_vfs_read,
    };

    static int __init my_trace_init(void) {
    register_kprobe(&kp);
    return 0;
    }

    static void __exit my_trace_exit(void) {
    unregister_kprobe(&kp);
    }

    module_init(my_trace_init);
    module_exit(my_trace_exit);

    内存性能分析

    # ★ 查看内存分配器统计
    cat /proc/slabinfo # Slab分配器
    cat /proc/buddyinfo # 伙伴系统空闲页
    cat /proc/vmstat | grep -E 'alloc|free|page'

    # ★ 追踪内存分配延迟
    perf record -e kmem:mm_page_alloc -a — sleep 10
    perf report

    # ★ 检测内存泄漏(kmemleak)
    echo scan > /sys/kernel/debug/kmemleak
    cat /sys/kernel/debug/kmemleak

    # ★ 分析页面回收压力
    vmstat 1 10
    # 关注 si/so(swap in/out)和 free(空闲内存)

    # ★ 查看NUMA拓扑与内存分布
    numactl –hardware
    numastat -p $(pgrep -x your_process)

    // ★ 自定义内存分配器性能测试
    #include <sys/time.h>

    struct alloc_stat {
    u64 total_allocs;
    u64 total_frees;
    u64 total_bytes;
    u64 max_alloc_time_ns;
    u64 total_alloc_time_ns;
    };

    static struct alloc_stat stats;

    // ★ 包装malloc,统计分配耗时
    void *tracked_malloc(size_t size) {
    struct timespec start, end;
    clock_gettime(CLOCK_MONOTONIC, &start);

    void *ptr = malloc(size);

    clock_gettime(CLOCK_MONOTONIC, &end);
    u64 elapsed = (end.tv_sec – start.tv_sec) * 1000000000 +
    (end.tv_nsec – start.tv_nsec);

    stats.total_allocs++;
    stats.total_bytes += size;
    stats.total_alloc_time_ns += elapsed;
    if (elapsed > stats.max_alloc_time_ns)
    stats.max_alloc_time_ns = elapsed;

    return ptr;
    }

    // ★ 打印统计
    void print_alloc_stats(void) {
    printf("Allocations: %llu\\n", stats.total_allocs);
    printf("Total bytes: %llu\\n", stats.total_bytes);
    printf("Avg alloc time: %llu ns\\n",
    stats.total_alloc_time_ns / stats.total_allocs);
    printf("Max alloc time: %llu ns\\n", stats.max_alloc_time_ns);
    }

    IO性能分析与优化

    # ★ 查看磁盘IO统计
    iostat -xz 1 # 设备级IO统计
    iotop -o # 进程级IO监控
    pidstat -d 1 # 每进程IO统计

    # ★ 追踪IO延迟分布
    biolatency # bcc工具:IO延迟直方图

    # ★ 分析IO调度器
    cat /sys/block/sda/queue/scheduler # 查看当前调度器
    echo mq-deadline > /sys/block/sda/queue/scheduler # 切换调度器

    # ★ 查看页缓存命中率
    sar -B 1 # 页缓存统计
    vmstat 1 # 关注 bi/bo(块IO)

    // ★ 使用io_uring实现高性能异步IO
    #include <liburing.h>

    int setup_io_uring(struct io_uring *ring, int depth) {
    // 初始化io_uring,提交队列和完成队列深度
    return io_uring_queue_init(depth, ring, 0);
    }

    // ★ 提交异步读请求
    int submit_async_read(struct io_uring *ring, int fd, void *buf,
    size_t count, off_t offset, void *userdata) {
    struct io_uring_sqe *sqe = io_uring_get_sqe(ring);
    if (!sqe) return -1;

    io_uring_prep_read(sqe, fd, buf, count, offset);
    io_uring_sqe_set_data(sqe, userdata);

    // 提交到内核
    return io_uring_submit(ring);
    }

    // ★ 处理完成事件
    void process_completions(struct io_uring *ring) {
    struct io_uring_cqe *cqe;

    // 非阻塞获取完成事件
    while (io_uring_peek_cqe(ring, &cqe) == 0) {
    if (cqe->res < 0) {
    fprintf(stderr, "IO error: %s\\n", strerror(-cqe->res));
    } else {
    // 处理读取的数据
    void *userdata = io_uring_cqe_get_data(cqe);
    handle_read_complete(userdata, cqe->res);
    }
    io_uring_cqe_seen(ring, cqe);
    }
    }

    锁竞争分析

    # ★ 使用perf分析锁等待
    perf record -e lock:lock_acquire,lock:lock_contended -a — sleep 10
    perf report

    # ★ 使用eBPF追踪锁延迟
    # bcc工具:lockstat
    lockstat -a # 显示所有锁的统计

    # ★ 分析自旋锁持有时间
    perf record -e lock:lock_acquire,lock:lock_release -a — sleep 10
    perf script | ./lock_analysis.py

    // ★ 在内核模块中追踪锁竞争
    #include <linux/spinlock.h>

    struct tracked_spinlock {
    spinlock_t lock;
    u64 acquire_time;
    u64 total_hold_time;
    u64 max_hold_time;
    u64 contention_count;
    };

    // ★ 包装spin_lock,统计持有时间
    static inline void tracked_spin_lock(struct tracked_spinlock *tsl) {
    u64 start = ktime_get_ns();

    // 尝试获取锁
    if (!spin_trylock(&tsl->lock)) {
    // 锁竞争:记录等待
    tsl->contention_count++;
    spin_lock(&tsl->lock);
    }

    tsl->acquire_time = start;
    }

    static inline void tracked_spin_unlock(struct tracked_spinlock *tsl) {
    u64 hold_time = ktime_get_ns() – tsl->acquire_time;
    tsl->total_hold_time += hold_time;
    if (hold_time > tsl->max_hold_time)
    tsl->max_hold_time = hold_time;

    spin_unlock(&tsl->lock);
    }


    性能调优检查清单
    类别检查项工具
    CPU 热点函数、缓存命中率、分支预测 perf, cachestat
    内存 分配延迟、页回收、NUMA亲和性 perf, numastat
    IO 延迟分布、队列深度、吞吐量 iostat, biolatency
    网络 TCP重传、连接建立延迟、带宽 ss, tcpdump, netstat
    锁 竞争频率、持有时间、死锁 lockstat, perf
    调度 上下文切换、运行队列长度 pidstat, runqlat

    总结

            性能调优是一个观测 → 假设 → 实验 → 验证的迭代过程:

  • 用top/vmstat/iostat发现异常指标
  • 用perf/eBPF定位到具体函数或系统调用
  • 用火焰图可视化热点
  • 修改代码/配置后对比验证
  •         下一章我们将探讨操作系统安全:从内核加固、沙箱机制到漏洞利用与防御。

    赞(0)
    未经允许不得转载:171主机测评 » 《从零手写操作系统 (62):性能调优与实战——火焰图、perf、eBPF与内核追踪》
    分享到: 更多 (0)

    评论 抢沙发

    • 昵称 (必填)
    • 邮箱 (必填)
    • 网址