《从零手写操作系统 (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定位到具体函数或系统调用 - 用火焰图可视化热点
- 修改代码/配置后对比验证
下一章我们将探讨操作系统安全:从内核加固、沙箱机制到漏洞利用与防御。
转载自 CSDN-专业IT技术社区
原文链接:https://blog.csdn.net/anew___/article/details/169625057



