anew___头像

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

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

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

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

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

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


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

总结

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

  1. 用top/vmstat/iostat发现异常指标
  2. 用perf/eBPF定位到具体函数或系统调用
  3. 用火焰图可视化热点
  4. 修改代码/配置后对比验证

        下一章我们将探讨操作系统安全:从内核加固、沙箱机制到漏洞利用与防御。

转载自 CSDN-专业IT技术社区

原文链接:https://blog.csdn.net/anew___/article/details/169625057

文章来源转载

评论

评论列表

微信小程序
QQ小程序

关于作者

点赞数:0
关注数:0
粉丝:0
文章:0
关注标签:0
加入于:--