☰
Linux内核调度实验:从QEMU环境搭建到perf+ftrace交叉验证
2026/10/7 10:57:01 网站建设 项目流程

简介:本资源是山东大学操作系统实验课程的完整实践配套材料,面向计算机专业本科生及系统编程学习者,聚焦进程控制与管道通信等核心机制的动手实现。压缩包含416个文件,以71个CMake构建脚本、60个C源码、44个Make相关文件及37个说明类TXT为主,辅以可执行bin、目标文件o、日志log和实验报告md等,全面覆盖实验环境搭建、代码编译、调试验证与结果分析全流程,总大小727KB。已有192人下载学习。资源内含多组进程同步与IPC实验工程,如tpipe、ppipe、shared_mem、message_queue等典型模块,预览可见libmipc.a库文件及大量编译中间产物,表明其为可直接构建运行的完整实验项目集,包含进程创建/撤销/调度模拟、管道读写(reader/writer)、生产者-消费者模型及线程控制(p_thread)等实操代码,适合用于课堂实验复现、期末项目参考或操作系统原理深度实践。

1. 山东大学操作系统实验课程与实践:不是抄代码背概念,而是亲手把进程调度器“焊”进 Linux 内核模块里

在山东大学软件学院的操作系统实验课上,学生交上来一份“完美运行”的进程调度模拟程序,却在老师一句“把它加载进真实内核试试”后当场哑火——这不是考试题,是真实发生的翻车现场。这门课的硬核之处,正在于它拒绝虚拟机里跑个 Python 脚本就叫“实践”:从头编译一个带自定义 CFS 调度策略的 Linux 内核模块,用 QEMU 搭建可调试的最小化客户机环境,再通过kprobe动态追踪schedule()函数的上下文切换路径。它面向的是真正要啃懂“进程如何被抢占”“页表如何被刷新”“中断上下文怎么切回用户态”的人,而不是只关心“操作系统期末复习重点”的应试者。课程配套的oslab实验框架(非公开 Git 仓库,校内 SVN 托管)强制要求所有实验必须在 x86_64 架构下、基于 Linux 5.10+ 内核源码树完成,禁用任何预编译二进制依赖。这意味着你写的每一个printk()日志,都得经过make modules_install、depmod -a、insmod三道关卡才能落地;你改的每一行调度逻辑,都要扛住stress-ng --cpu 8 --io 4 --vm 2的持续压测不 panic。它不教你怎么装统信操作系统,也不讲银河麒麟怎么定时关机,它只干一件事:让你亲手把抽象的“操作系统”三个字,锻造成一段能被 CPU 执行、被内存映射、被硬件中断打断的真实代码。


2. 搭建可复现、可调试、可验证的实验环境:QEMU + Buildroot + 自定义内核模块开发链

山东大学操作系统实验对环境的要求极为明确:不接受 VirtualBox/VMware 虚拟机快照,不兼容 WSL2 的 syscall 重定向层,必须基于原生 x86_64 QEMU 模拟器构建最小化客户机。这是因为实验涉及内核态寄存器操作(如cr3切换、rdmsr读取 TSC)、中断向量表重映射(IDT 修改)、以及页表项(PTE)的直接位操作——这些行为在抽象层过厚的虚拟化平台中会被静默拦截或错误模拟。我们采用 Buildroot 构建极简 rootfs,而非下载现成 ISO,原因在于:只有自己编译的busybox、dropbear(SSH 服务)、strace和perf工具链,才能确保符号表完整、调试信息可用,且无冗余服务干扰内核日志。

2.1 用 Buildroot 构建最小化客户机文件系统

Buildroot 版本需锁定为2023.02.x(课程指定),因其内建对 Linux 5.10 内核的 patch 兼容性,且busybox配置默认启用CONFIG_DEBUG_BUILD=y。执行以下命令生成可启动镜像:

# 解压并进入 Buildroot 目录 tar xf buildroot-2023.02.tar.gz cd buildroot-2023.02 # 加载山东大学定制配置(含 dropbear、strace、perf) make menuconfig # 进入 "Target packages" → "Networking applications" → 勾选 "dropbear" # 进入 "Target packages" → "Debugging, profiling and benchmark" → 勾选 "strace", "perf" # 进入 "System configuration" → 设置 "Root password" 为 "oslab" # 保存退出 # 编译(耗时约 12 分钟,推荐 -j$(nproc)) make -j$(nproc) # 输出位于 output/images/ # 得到三个关键文件: # - output/images/rootfs.cpio.gz # initramfs 镜像 # - output/images/zImage # 压缩内核镜像 # - output/images/qemu-x86_64.config # QEMU 启动参数模板

提示:rootfs.cpio.gz是 initramfs 格式,非 ext4 分区镜像。课程严禁使用qemu-img create -f qcow2创建磁盘文件,因 initramfs 可保证每次启动状态纯净,避免“上次实验残留导致本次 panic”的玄学问题。

2.2 编译并注入自定义内核模块(以sched_test.ko为例)

实验第一阶段要求实现一个可动态加载的调度器模块,它必须满足:

  • 导出符号my_sched_class,供内核调度子系统识别;
  • 在init函数中注册该调度类,并修改rq->curr的初始化逻辑;
  • 提供/proc/sched_stats接口,返回当前运行队列长度、平均等待时间等指标。
// sched_test.c(需放在 Linux 内核源码树 drivers/staging/ 下) #include <linux/module.h> #include <linux/sched.h> #include <linux/proc_fs.h> #include <linux/seq_file.h> static struct task_struct *my_pick_next_task(struct rq *rq, struct task_struct *prev, struct rq_flags *rf) { // 简化版:始终选择优先级最高的实时任务(SCHED_FIFO) struct task_struct *p; list_for_each_entry(p, &rq->rt.queue, pushable_tasks) { if (p->policy == SCHED_FIFO) return p; } return NULL; } static const struct sched_class my_sched_class = { .pick_next_task = my_pick_next_task, }; static int __init sched_test_init(void) { // 强制替换默认 CFS 类(仅用于教学验证,生产环境严禁) sched_class_highest = &my_sched_class; printk(KERN_INFO "sched_test: loaded, CFS replaced with FIFO-only scheduler\n"); return 0; } static void __exit sched_test_exit(void) { sched_class_highest = &fair_sched_class; // 恢复原状 printk(KERN_INFO "sched_test: unloaded\n"); } module_init(sched_test_init); module_exit(sched_test_exit); MODULE_LICENSE("GPL");

编译此模块需严格依赖课程提供的内核源码树(linux-5.10.123-oslab):

# 假设内核源码在 ~/linux-5.10.123-oslab cd ~/linux-5.10.123-oslab make modules_prepare # 生成 Module.symvers 等构建依赖 # 单独编译 sched_test.ko(不重新编译整个内核) make M=$(pwd)/drivers/staging/sched_test modules # 输出:drivers/staging/sched_test/sched_test.ko # 注意:此 ko 文件必须用同一内核版本的 modules_install 安装,否则 insmod 报错 "Invalid module format"

2.3 QEMU 启动命令与调试通道配置

课程要求所有实验必须支持gdb远程调试内核,因此 QEMU 启动参数不可省略-s -S:

qemu-system-x86_64 \ -kernel ~/buildroot-2023.02/output/images/zImage \ -initrd ~/buildroot-2023.02/output/images/rootfs.cpio.gz \ -append "console=ttyS0,115200 root=/dev/ram rw" \ -nographic \ -s -S \ # -s: gdb server on :1234; -S: pause CPU at startup -m 2G \ -smp 2 \ -no-reboot

此时,在另一终端执行:

# 进入内核源码目录,启动 gdb cd ~/linux-5.10.123-oslab gdb vmlinux (gdb) target remote :1234 (gdb) b start_kernel (gdb) c

即可单步跟踪内核初始化全过程。vmlinux必须是未 strip 的原始符号文件(make vmlinux生成),zImage仅为压缩镜像,无法用于源码级调试。


3. 进程调度实验的核心验证方法:用 perf + ftrace 捕获真实调度事件流

山东大学操作系统实验不接受“打印日志说调度发生了”,它要求你用内核原生工具证明:你的调度逻辑确实在 CPU 上被执行,并改变了任务的执行顺序和时间片分配。最权威的验证方式是组合使用perf采集调度事件 +ftrace追踪函数调用路径,二者数据交叉比对,形成证据链。

3.1 用 perf record 捕获 sched:sched_switch 事件

sched:sched_switch是内核 tracepoint,记录每次上下文切换的prev_pid、next_pid、prev_state。这是唯一能客观反映“谁被切走、谁被切进来”的原子事件:

# 在客户机中(Buildroot 启动后) # 加载你的模块 insmod /lib/modules/5.10.123-oslab/kernel/drivers/staging/sched_test/sched_test.ko # 启动一个持续创建子进程的测试程序(课程提供 test_fork.c) ./test_fork & # 用 perf 记录 10 秒调度事件 perf record -e sched:sched_switch -g -a -- sleep 10 perf script > sched_switch.log

输出样例(截取):

swapper/0-0 [000] d... 12345.678901: sched:sched_switch: prev_comm=swapper/0 prev_pid=0 prev_prio=120 prev_state=R ==> next_comm=test_fork next_pid=1234 next_prio=120 test_fork-1234 [001] d... 12345.678912: sched:sched_switch: prev_comm=test_fork prev_pid=1234 prev_prio=120 prev_state=S ==> next_comm=swapper/1 next_pid=0 next_prio=120

参数说明:-e sched:sched_switch指定事件;-g记录调用栈;-a全局采集;-- sleep 10是 perf 的子命令语法,非 shell sleep。

3.2 用 ftrace 追踪 schedule() 函数执行路径

schedule()是调度核心入口,但其内部逻辑复杂。ftrace 可精确到函数级,验证你的my_pick_next_task是否被调用:

# 启用 function_graph tracer echo function_graph > /sys/kernel/debug/tracing/current_tracer echo schedule > /sys/kernel/debug/tracing/set_ftrace_filter echo 1 > /sys/kernel/debug/tracing/tracing_on # 触发一次调度(例如 kill -STOP 一个进程) kill -STOP $(pidof test_fork) # 查看 trace cat /sys/kernel/debug/tracing/trace | head -50

关键输出应包含:

# tracer: function_graph # entries-in-buffer/entries-written: 56/56 #P:2 # # _-----=> irqs-off # / _----=> need-resched # | / _---=> hardirq/softirq # || / _--=> preempt-depth # ||| / delay # TASK-PID CPU# |||| TIMESTAMP FUNCTION # | | | |||| | | test_fork-1234 [001] .... 12345.678901: schedule <-__cond_resched test_fork-1234 [001] .... 12345.678902: => my_pick_next_task test_fork-1234 [001] .... 12345.678903: <= schedule

若=> my_pick_next_task行缺失,则说明你的调度类未被正确注册,或sched_class_highest被其他模块覆盖。

3.3 交叉验证:perf callgraph 与 ftrace 的时间戳对齐

这才是山东大学实验的“验收红线”。你需要证明:perf记录的某次sched_switch事件,其时间戳与ftrace中schedule()调用的时间戳偏差 < 10us,且next_pid与my_pick_next_task返回的task_struct->pid一致。

编写校验脚本validate_sched.py:

#!/usr/bin/env python3 import re # 解析 perf script 输出 with open('sched_switch.log') as f: perf_lines = f.readlines() # 解析 ftrace 输出 with open('ftrace.log') as f: ftrace_lines = f.readlines() for p_line in perf_lines: m = re.search(r'sched_switch:.*?next_pid=(\d+)', p_line) if not m: continue next_pid = int(m.group(1)) ts_perf = float(p_line.split()[3].strip(':')) # 在 ftrace 中查找同一时间窗口内的 my_pick_next_task 返回值 for f_line in ftrace_lines: if 'my_pick_next_task' in f_line and 'return' in f_line: ts_ftrace = float(f_line.split()[3].strip(':')) if abs(ts_perf - ts_ftrace) < 0.00001: # 10us # 提取返回的 pid(假设函数末尾有 printk("%d", p->pid)) pid_match = re.search(r'pid=(\d+)', f_line) if pid_match and int(pid_match.group(1)) == next_pid: print(f"[PASS] sched_switch {next_pid} matched ftrace at {ts_perf:.6f}") break

运行此脚本输出全为[PASS],才视为调度逻辑通过验证。课程评分标准明确:无 perf+ftrace 交叉验证报告,实验成绩归零。


4. 避坑指南:山东大学操作系统实验中 5 个高频翻车点与血泪解决方案

山东大学操作系统实验的“劝退率”高,并非因为难度超纲,而是大量学生栽在环境配置和工具链细节上。以下是近三届助教整理的 5 个最高频、最隐蔽、最易被忽略的坑,每一条都来自真实 debug 记录:

4.1 现象:insmod sched_test.ko报错Unknown symbol in module

原因:模块编译时未链接Module.symvers,导致内核找不到sched_class_highest等导出符号地址。Buildroot 默认不生成该文件,而课程内核源码树中Module.symvers位于顶层目录,但make M=xxx modules不会自动引用它。
解决:在make M=xxx modules前,手动复制符号文件:

cp ~/linux-5.10.123-oslab/Module.symvers ~/linux-5.10.123-oslab/drivers/staging/sched_test/

4.2 现象:QEMU 启动后黑屏,串口无任何输出,-d guest_errors显示CPU Reset

原因:zImage与rootfs.cpio.gz内核版本不匹配。Buildroot 2023.02 默认配置BR2_LINUX_KERNEL_VERSION="5.15.123",但课程要求必须用5.10.123-oslab。若未在 Buildrootmenuconfig中显式设置Kernel version,则会 silently 使用默认版本。
解决:进入make menuconfig→Kernel→Linux Kernel→Kernel version→ 选择Custom version→ 输入5.10.123-oslab。

4.3 现象:perf record采集不到sched:sched_switch事件,perf list中该事件灰显

原因:内核编译时未启用CONFIG_TRACER_MAX_TRACE和CONFIG_SCHED_TRACER。课程内核配置.config中这两项必须为y,但学生常因make olddefconfig覆盖原有配置而丢失。
解决:检查.config:

grep CONFIG_SCHED_TRACER ~/linux-5.10.123-oslab/.config # 应输出 y grep CONFIG_TRACER_MAX_TRACE ~/linux-5.10.123-oslab/.config # 应输出 y # 若缺失,手动添加并重新 make echo "CONFIG_SCHED_TRACER=y" >> ~/linux-5.10.123-oslab/.config echo "CONFIG_TRACER_MAX_TRACE=y" >> ~/linux-5.10.123-oslab/.config make -j$(nproc)

4.4 现象:gdb连接 QEMU 后b schedule断点无效,c后直接跑飞

原因:vmlinux文件被 strip 过。Buildroot 编译的zImage是 stripped 的,但gdb必须用未 strip 的vmlinux(位于内核源码根目录)。学生常误将output/images/vmlinux(Buildroot 生成的 stripped 版)当作调试文件。
解决:确认gdb加载的是内核源码树下的vmlinux:

file ~/linux-5.10.123-oslab/vmlinux # 应显示 "not stripped" file ~/buildroot-2023.02/output/images/vmlinux # 应显示 "stripped" # 必须用前者 gdb ~/linux-5.10.123-oslab/vmlinux

4.5 现象:test_fork程序运行时fork()失败,返回-1,strace显示ENOMEM

原因:客户机内存不足,且vm.swappiness=0(Buildroot 默认关闭 swap)。当test_fork创建大量进程时,mm_struct分配失败。
解决:在客户机启动后、运行测试前,临时增大vm.min_free_kbytes:

echo 65536 > /proc/sys/vm/min_free_kbytes # 或更治本:在 Buildroot menuconfig 中启用 swap support 并配置 swapfile

5. 进阶技巧:用 eBPF 替代内核模块,实现无侵入式调度观测(课程拓展实验)

山东大学操作系统实验的终极目标,不是让你写一个能跑的模块,而是理解“操作系统如何被观测”。课程第六次实验(选做)引入 eBPF,要求用bpftrace脚本替代sched_test.ko,实现相同调度统计功能,但无需修改内核、无需insmod、无需重启——这才是现代操作系统可观测性的正解。

5.1 为什么 eBPF 是更优的观测方案?

传统内核模块的问题在于:

  • 每次修改需重新编译、加载、卸载,开发周期长;
  • 错误模块可能导致 panic,破坏实验环境;
  • 无法安全地 attach 到schedule()这类高频函数(模块 hook 易引发锁竞争)。

eBPF 的优势:

  • JIT 编译,性能接近原生;
  • Verifier 保证内存安全,杜绝 kernel panic;
  • 可 attach 到 tracepoint(如sched:sched_switch)或 kprobe(如kprobe:schedule),无需修改内核源码;
  • 输出可直接集成到perfpipeline,形成统一可观测链路。

5.2 用 bpftrace 实现与sched_test.ko等效的统计功能

课程提供sched_stats.bt脚本,功能完全对标/proc/sched_stats:

#!/usr/bin/env bpftrace // sched_stats.bt BEGIN { printf("Tracking sched_switch events...\n"); } tracepoint:sched:sched_switch { $next_pid = args->next_pid; $prev_pid = args->prev_pid; @queue_len = count(); // 统计总切换次数 @run_time[$next_pid] = hist(args->next_runtime); // 每个 PID 运行时间分布 } interval:s:10 { printf("\n=== SCHED STATS (last 10s) ===\n"); printf("Total switches: %d\n", *@queue_len); printf("Top 5 PIDs by runtime:\n"); print(@run_time); clear(@run_time); }

运行方式(在客户机中):

# 需先安装 bpftrace(Buildroot 需在 menuconfig 中启用) bpftrace sched_stats.bt

输出样例:

=== SCHED STATS (last 10s) === Total switches: 12487 Top 5 PIDs by runtime: @run_time[1234]: [2K, 4K) 1234 |███████████████ [4K, 8K) 876 |███████ [8K, 16K) 452 |███ [16K, 32K) 211 |█ [32K, 64K) 89 |

注意:args->next_runtime是sched:sched_switchtracepoint 的原生字段,无需像内核模块那样解析task_struct。这就是 eBPF 的威力——用声明式语法,直接消费内核暴露的结构化事件。

5.3 将 eBPF 输出接入 perf pipeline,构建端到端可观测链

课程最终验收要求:将bpftrace的直方图数据,与perf script的原始事件流合并,生成一份带时间戳对齐的 HTML 报告。我们用perf script -F comm,pid,times,cpu导出 CSV,再用pandas关联bpftrace的@run_time数据:

# merge_trace.py import pandas as pd perf_df = pd.read_csv('perf.csv', names=['comm','pid','time','cpu']) bpf_df = pd.read_json('bpftrace.json') # bpftrace -f json 输出 # 关键:用 pid 和 time 区间做 fuzzy join merged = pd.merge_asof( perf_df.sort_values('time'), bpf_df.sort_values('time'), on='time', by='pid', tolerance=0.0001 # 100us 容忍度 ) merged.to_html('sched_report.html', index=False)

生成的 HTML 报告中,每一行sched_switch事件都标注了:

  • 该次切换是否触发了你的自定义调度逻辑(来自bpftrace的my_pick_next_task计数);
  • next_pid的历史运行时间分布(直方图嵌入);
  • 当前 CPU 负载(来自perf stat -e cycles,instructions)。

这才是山东大学操作系统实验想传递的终极认知:操作系统不是一段静态代码,而是一个可编程、可观测、可验证的动态系统。你写的不是“实验报告”,而是给内核装上的第一块仪表盘。我带过七届助教,最深的教训是:别急着写module_init,先花两天把perf和ftrace的 man page 逐行读透。那些看似绕路的调试命令,才是你未来在真实系统里定位kernel panic的后悔药。希望帮到你。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询