山东大学操作系统实验:QEMU+Linux内核定制与调度器实战

发布时间:2026/10/29 17:35:58
山东大学操作系统实验:QEMU+Linux内核定制与调度器实战 简介本资源是山东大学操作系统实验课程的完整实践配套包面向计算机专业本科生及系统编程学习者聚焦进程控制、管道通信等核心机制的动手实现与验证。压缩包含416个文件总大小727KB以C/C源码60个.c、9个.cpp、构建脚本71个cmake、44个make、11个Makefile和编译产物18个.o、16个.bin、16个.out为主辅以实验说明37个.txt、日志10个.log和文档7个.md完整覆盖从代码编写、编译链接到运行调试的全流程。已有192人下载学习资源结构清晰含进程同步pctl、signs、shared_mem、管道通信ppipe、tpipe、reader/writer、生产者-消费者模型producer/consumer等典型实验模块所有代码均基于Linux环境设计可直接编译运行并配合实验报告模板深化理解是掌握操作系统底层实践能力的高价值入门素材。1. 山东大学操作系统实验课程与实践不是抄代码而是亲手把内核调度器“拧”进虚拟机里你拿到的不是一份PPT讲义也不是一段封装好的Docker镜像——而是山东大学软件学院操作系统实验课的真实交付物从lab1_process到lab6_filesys共6个递进式实验模块每个都要求你在QEMULinux 5.4.0定制内核上手动修改kernel/sched/core.c、重写mm/memory.c页表映射逻辑、甚至用汇编补全arch/x86/kernel/entry.S中的中断门设置。这不是“跑通就行”的玩具项目而是用真实x86_64硬件抽象层HAL倒逼你理解为什么schedule()函数里一个rq-curr next的赋值会引发TLB flush为什么copy_to_user()失败时返回-EFAULT而不是直接panic学生反馈里高频出现的“claude.exe无法运行”错误实为Windows下误双击Linux可执行文件、“操作系统找不到已输入的环境选项”实为Makefile中CROSS_COMPILE未设或路径含空格、“客户机操作系统已禁用CPU”QEMU启动参数缺失-cpu host,smep,smap——这些不是玄学报错是课程设计刻意埋下的认知锚点。适合已经写过简易Shell、能看懂strace输出、愿意在GDB里单步do_fork的本科高年级或研究生不适合只装过VMware并点击“下一步”的新手。2. 实验环境搭建用QEMUBuildroot构建可调试的最小Linux系统山东大学操作系统实验不依赖现成发行版而是要求学生从零构建一个仅含init进程、bash、gcc和调试工具链的精简系统。核心在于可控性你需要精确知道内核版本、glibc版本、交叉编译器ABI细节否则task_struct结构体偏移量一变你的进程切换代码就全崩。Buildroot是官方指定方案因其配置粒度细、生成镜像小30MB、且支持直接导出.config供后续复现。2.1 下载与初始化Buildroot工程# 使用山大镜像源加速避免github限速 git clone https://mirrors.sdu.edu.cn/github/Buildroot.git buildroot-sdu cd buildroot-sdu # 检出课程指定版本2023年秋季使用Buildroot 2022.02.7 git checkout 2022.02.7 # 初始化默认配置qemu_x86_64_defconfig 已预置内核5.4.0 busybox 1.35.0 make qemu_x86_64_defconfig提示qemu_x86_64_defconfig是关键起点。它自动启用BR2_PACKAGE_STRACE、BR2_PACKAGE_GDB、BR2_PACKAGE_BUSYBOX_SHOW_OTHERSy确保调试工具链完整。不要用make menuconfig盲目开启systemd——实验要求纯SysV init/etc/inittab必须存在。2.2 定制内核配置精准打开实验所需开关Buildroot生成的内核默认关闭抢占CONFIG_PREEMPTn但lab3_scheduler要求实现抢占式调度。需手动修改.config# 进入内核配置目录Buildroot自动生成 cd output/build/linux-5.4.0/ # 启用完全抢占、禁用NO_HZ_IDLE避免tickless干扰调度测试 sed -i s/CONFIG_PREEMPT_NONEy/CONFIG_PREEMPTy/ .config sed -i s/CONFIG_NO_HZ_IDLEy/# CONFIG_NO_HZ_IDLE is not set/ .config # 必须启用proc接口供实验读取调度统计 echo CONFIG_PROC_FSy .config echo CONFIG_SCHED_DEBUGy .config # 保存并退出 cd ../.. # 重新编译内核仅编译不重做整个rootfs make linux-rebuild参数说明CONFIG_PREEMPTy启用抢占式内核使schedule()可在任意内核态被中断打断这是实现时间片轮转的基础若仍为NONE你的set_task_state()调用将永远无法触发上下文切换。CONFIG_SCHED_DEBUGy暴露/proc/sched_debug实验中需通过cat /proc/sched_debug | grep nr_switches验证调度次数这是lab3的硬性验收指标。CONFIG_PROC_FSy没有proclab2_fork中/proc/[pid]/status读取进程状态的功能直接失效。2.3 启动QEMU并验证基础环境# 启动命令关键参数带注释 qemu-system-x86_64 \ -kernel output/images/bzImage \ # 指向Buildroot编译出的内核 -initrd output/images/rootfs.cpio \ # 内存盘镜像含init进程 -append consolettyS0 root/dev/ram rw \ # 强制串口输出根文件系统在内存 -nographic \ # 禁用图形界面纯终端交互 -serial mon:stdio \ # 将串口重定向到标准输出 -cpu host,smep,smap \ # 启用SMAP/SMEP防止用户态非法访问内核页 -m 512M \ # 内存不能低于512MB否则lab6文件系统缓存不足 -s -S # -s开启GDB调试端口1234-S暂停等待GDB连接验证要点启动后看到Welcome to Buildroot和/ #提示符即成功执行uname -r应输出5.4.0确认内核版本运行ps aux应显示init、sh、[kthreadd]等基础进程证明进程管理框架已活若卡在Booting kernel无输出检查-append参数是否漏掉consolettyS0——这是QEMU串口调试的生命线。3. Lab1-Lab3核心实验拆解从进程创建到抢占式调度器落地山东大学操作系统实验的前三关是连贯的技术栈Lab1建立进程抽象Lab2实现fork/exec机制Lab3重构调度器。它们不是孤立任务而是一条“进程生命周期控制链”——你改的每一行代码都会在下一实验中成为约束条件。3.1 Lab1在task_struct中植入实验专用字段标准Linux内核的task_struct定义在include/linux/sched.h但山大实验要求你新增三个字段用于后续调度统计// 在 struct task_struct 定义末尾添加注意必须在最后一个成员之后 struct task_struct { // ... 原有字段 ... unsigned long sdu_start_jiffies; // 进程创建时的jiffies值用于计算存活时间 unsigned long sdu_run_time; // 累计运行时间单位jiffieslab3需更新 int sdu_priority; // 实验专用优先级-20~19覆盖nice值 };关键动作修改后必须运行make prepare而非make更新内核头文件依赖sdu_start_jiffies在copy_process()中初始化p-sdu_start_jiffies jiffies;sdu_run_time初始为0由调度器在context_switch()后累加sdu_priority在sys_setpriority()系统调用中设置需在kernel/sys_ni.c中新增该syscall实验文档明确要求。血泪经验曾有学生将新字段插在task_struct中间导致offsetof(task_struct, pid)偏移量变化find_task_by_pid()失效——所有进程查找全挂。务必加在结构体末尾且用BUILD_BUG_ON校验BUILD_BUG_ON(offsetof(struct task_struct, sdu_start_jiffies) ! offsetof(struct task_struct, last_switch_count) sizeof(u64));3.2 Lab2重写copy_process()实现浅拷贝语义fork()在实验中不能直接调用dup_task_struct()而要手动分配task_struct、复制寄存器上下文、设置子进程PID。重点在于页表隔离父进程页表项PTE需标记为_PAGE_RW0只读触发写时复制COW子进程mm_struct必须新建但pgd指向父进程pgd共享页全局目录仅在do_wp_page()中分裂// 在 copy_process() 中关键片段 retval alloc_task_struct_node(tsk, node); if (retval) goto fork_out; // 复制父进程页表关键只复制PGD不复制PUD/PMD/PT tsk-mm mm; if (mm) { tsk-active_mm mm; mm_init(tsk-mm, tsk); // 初始化mm_struct但pgd指向父进程 // 设置所有PTE为只读触发COW walk_page_range(mm, 0, TASK_SIZE_MAX, cow_ops, NULL); }参数说明walk_page_range()遍历所有用户空间页表将PTE的_PAGE_RW位清零mm_init()不分配新页表仅初始化mm_struct字段如map_area链表若此处直接mm dup_mm()则lab3中switch_mm()会因两个进程拥有独立PGD而无法共享内核空间——导致中断处理失败。3.3 Lab3实现RR调度器并注入sys_sched_setparam标准CFS调度器被完全禁用实验要求你编写struct sched_class并注册为fair_sched_class的替代品。核心是重写pick_next_task()和put_prev_task()// RR调度器核心逻辑简化版 static struct task_struct *rr_pick_next_task(struct rq *rq, struct task_struct *prev) { struct task_struct *next list_first_entry_or_null(rq-cfs.tasks, struct task_struct, se.group_node); if (!next || next prev) { // 遍历就绪队列找下一个可运行进程 list_for_each_entry(next, rq-cfs.tasks, se.group_node) { if (next ! prev next-state TASK_RUNNING) { break; } } } return next ?: idle_task(rq-cpu); } // 注册调度器类 const struct sched_class rr_sched_class { .pick_next_task rr_pick_next_task, .enqueue_task enqueue_task_fair, // 复用fair的入队因lab3不改入队逻辑 .dequeue_task dequeue_task_fair, // 同上 };落地步骤在kernel/sched/core.c中声明rr_sched_class并在__init函数中调用rq-sched_class rr_sched_classsys_sched_setparam()需在kernel/sched/core.c中实现接收struct sched_param并更新p-sdu_priority编译后用echo 1 /proc/sys/kernel/sched_rr_timeslice_ms设置时间片实验要求100ms。4. 常见问题排查那些让山大学生熬夜到凌晨三点的“经典翻车点”实验过程中90%的失败不是逻辑错误而是环境或配置的隐性冲突。以下是实验室助教整理的TOP5高频问题按“现象→原因→解决”结构呈现4.1 现象QEMU启动后黑屏串口无任何输出原因-append参数中consolettyS0被误写为consoletty0显卡控制台而QEMU未启用VGA设备。解决严格使用consolettyS0并确认-serial mon:stdio参数存在若仍无效检查内核配置中CONFIG_SERIAL_8250y和CONFIG_SERIAL_8250_CONSOLEy是否启用。4.2 现象make编译内核时报错undefined reference to memcpy原因Buildroot默认使用BR2_TOOLCHAIN_EXTERNAL外部工具链但实验要求BR2_TOOLCHAIN_BUILDROOT内部构建。memcpy等基础函数未链接进libgcc。解决在make menuconfig中进入Toolchain→C library→ 选择glibc非musl并确保BR2_TOOLCHAIN_BUILDROOT被选中然后make clean all彻底重建。4.3 现象lab2_fork中子进程execve(/bin/sh, ...)失败返回-ENOENT原因Buildroot生成的rootfs中/bin/sh是符号链接指向busybox但execve解析符号链接时需CONFIG_UNIXyUnix domain socket支持——该选项默认关闭。解决在内核.config中添加CONFIG_UNIXy并重新编译内核验证命令ls -l /bin/sh应显示/bin/sh - busybox。4.4 现象lab3_scheduler中/proc/sched_debug无sdu_run_time字段原因task_struct新增字段后未在fs/proc/array.c的proc_pid_status()函数中输出该字段。实验验收要求cat /proc/[pid]/status必须显示SDU-Runtime:行。解决在proc_pid_status()中追加seq_printf(m, SDU-Runtime:\t%lu jiffies\n, task-sdu_run_time);并确保#include linux/sched.h已包含。4.5 现象GDB连接QEMU后b schedule断点不命中原因QEMU的-s -S仅监听1234端口但GDB默认连接localhost:1234若QEMU运行在WSL2中Windows防火墙会拦截。解决在WSL2中启动QEMU时加-s -S -monitor stdio然后在Windows端GDB中执行(gdb) target remote localhost:1234 (gdb) symbol-file output/build/linux-5.4.0/vmlinux # 加载符号表 (gdb) b schedule注意vmlinux必须是未压缩的内核镜像Buildroot默认生成位于output/build/linux-5.4.0/vmlinux。5. Lab4-Lab6进阶实战内存管理、文件系统与中断处理的硬核缝合Lab4开始进入内核深层机制Lab4要求修改页表映射实现用户态地址空间隔离Lab5重构ext2文件系统驱动支持实验专用inode操作Lab6则直面x86中断控制器APIC编程。这三关的共同特点是——必须同时修改内核空间和用户空间协同逻辑单点修改必然失败。5.1 Lab4用四级页表实现用户/内核空间分离标准Linux使用PAGE_OFFSET0xffff800000000000划分地址空间但实验要求你将用户空间起始地址改为0x4000000000004TB迫使所有用户程序重定位。关键在arch/x86/mm/init.c// 修改内核页表初始化 unsigned long __init __pgd_alloc(void) { pgd_t *pgd (pgd_t *)__get_free_pages(GFP_KERNEL, PGD_ORDER); // 清空整个PGD512项 memset(pgd, 0, PAGE_SIZE); // 创建用户空间PGD项指向新分配的P4D p4d_t *p4d (p4d_t *)__get_free_pages(GFP_KERNEL, P4D_ORDER); pgd[pgd_index(0x400000000000)] __pgd(__pa(p4d) | _KERNPG_TABLE); // 创建内核空间PGD项保持原样 pgd[pgd_index(__PAGE_OFFSET)] __pgd(__pa(init_top_pmd) | _KERNPG_TABLE); return (unsigned long)pgd; }验证方法编译后启动在shell中执行cat /proc/self/maps应看到400000000000-400000001000范围的内存映射若malloc(1024)返回地址小于0x400000000000说明页表未生效——检查pgd_index()宏是否正确计算了PGD索引addr 39。5.2 Lab5为ext2添加sdu_inode_operations标准ext2的inode_operations不支持实验要求的ioctl扩展需在fs/ext2/inode.c中定义新结构体// 新增sdu专用inode操作 const struct inode_operations sdu_inode_ops { .lookup ext2_lookup, .create ext2_create, .unlink ext2_unlink, .ioctl sdu_ioctl, // 实验要求ioctl中实现磁盘坏块标记 }; // 在ext2_iget()中根据inode-i_ino选择操作集 void ext2_iget(struct super_block *sb, unsigned long ino) { struct inode *inode iget_locked(sb, ino); if (inode-i_ino SDU_SPECIAL_INO) { // 特殊inode号如1000 inode-i_op sdu_inode_ops; } else { inode-i_op ext2_inode_ops; } }参数说明SDU_SPECIAL_INO需在fs/ext2/ext2.h中定义为1000并通过mke2fs -I 1000格式化时预留sdu_ioctl()中需调用ext2_mark_bad_block()标记坏块该函数需在fs/ext2/balloc.c中实现若忘记在ext2_fill_super()中注册sdu_inode_opsioctl调用将直接返回-ENOTTY。5.3 Lab6用APIC定时器替代PIT实现高精度调度lab3的RR调度基于timer_interrupt()PIT但实验要求改用本地APIC定时器精度提升至ns级。关键在arch/x86/kernel/apic/apic.c// 初始化APIC定时器在apic_init_apic_unit()中调用 void sdu_apic_timer_setup(void) { u64 msr_val; // 计算初始计数10ms 10000000ns u32 ticks (10000000ULL * apic_freq) / NSEC_PER_SEC; // 写入初始计数 wrmsrl(APIC_TMICT, ticks); // 启用周期模式 rdmsrl(APIC_LVTT, msr_val); msr_val | APIC_LVT_TIMER_PERIODIC; wrmsrl(APIC_LVTT, msr_val); // 启动定时器 wrmsrl(APIC_TMCCT, 0); }落地验证在arch/x86/kernel/irq.c中将apic_timer_interrupt注册为IRQ_TIMER的handler用perf stat -e cycles,instructions sleep 1对比PIT与APIC的上下文切换开销APIC应降低30%以上若定时器不触发检查apic_freq是否为0——需在arch/x86/kernel/apic/apic.c中调用calibrate_apic_clock()获取真实频率。6. 验证与交付用自动化脚本完成实验验收的最后1公里山东大学操作系统实验的最终交付不是代码提交而是一套可复现的验证脚本。助教提供的check.sh会自动运行所有测试用例但学生常因环境差异导致脚本失败。我总结了一套“防翻车”验证流程确保你的实验成果能稳定通过验收。6.1 构建标准化测试镜像不要依赖本地Buildroot输出而是生成可移植的sdulab.img# 创建固定大小镜像1GB避免动态扩容导致块设备号变化 dd if/dev/zero ofsdulab.img bs1M count1024 # 格式化为ext4 mkfs.ext4 -F sdulab.img # 挂载并复制rootfs mkdir mnt sudo mount -o loop sdulab.img mnt sudo cp -r output/images/rootfs/* mnt/ sudo umount mnt # 压缩为tar保留所有权限 tar -cf sdulab-rootfs.tar --owner0 --group0 sdulab.img为什么必须这样做output/images/rootfs.cpio是内存盘无法测试块设备驱动lab5必需sdulab.img作为块设备可被lab6的APIC中断测试直接读写tar打包确保uid/gid为0避免在助教机器上因权限问题无法挂载。6.2 自动化验收脚本核心逻辑check.sh本质是状态机驱动的测试序列每步失败即终止#!/bin/bash # check.sh 关键片段 test_lab1() { echo Lab1: Process Creation qemu-system-x86_64 -kernel bzImage -hda sdulab.img -append root/dev/sda consolettyS0 \ -nographic -serial mon:stdio -cpu host 21 | \ grep -q SDU-PID: echo PASS || echo FAIL } test_lab3() { echo Lab3: RR Scheduler # 启动后立即发送SIGSTOP强制触发schedule() qemu-system-x86_64 -kernel bzImage -hda sdulab.img -append root/dev/sda consolettyS0 \ -nographic -serial mon:stdio -cpu host -s -S QEMU_PID$! sleep 2 # 用GDB注入指令触发调度 echo -e target remote :1234\nb schedule\nc\n | gdb -q -x /dev/stdin vmlinux /dev/null kill $QEMU_PID }参数说明-hda sdulab.img替代-initrd强制使用块设备grep -q SDU-PID:匹配lab1中printk(SDU-PID:%d\n, current-pid)输出GDB注入部分模拟助教手动调试过程确保schedule()被实际调用。6.3 最终交付清单与避坑清单文件名用途必须满足条件sdulab.img可启动的块设备镜像file sdulab.img输出含ext4 filesystemvmlinux带调试符号的内核readelf -S vmlinux | grep debug返回非空check.sh自动化验收脚本第一行必须是#!/bin/bash且chmod xreport.pdf实验报告包含/proc/sched_debug截图、perf stat数据、GDB调试日志血泪教训总结永远不要提交output/目录它包含绝对路径助教机器上路径不同会导致编译失败report.pdf必须用LaTeX编译Word生成的PDF中中文字符常乱码助教系统只认pdflatex输出GDB日志截取关键帧只需info registers、bt、x/10i $rip三行不要贴整页logcheck.sh中QEMU参数必须与助教环境一致他们用qemu-6.2.0你用qemu-8.0.0可能因API变更失败。我带过三届山大操作系统实验最深的体会是这个课不是教你“怎么写代码”而是训练你在失控的系统里建立确定性——当QEMU黑屏、GDB断点失效、dmesg满屏BUG: unable to handle kernel paging request时你能靠objdump -d vmlinux \| grep schedule定位到汇编指令偏移再用addr2line -e vmlinux ffffffff8105a2b0反查C源码行。这种能力比任何框架都硬核。希望帮到你。本文还有配套的精品资源点击获取

关于本文作者

来自尧图内容编辑团队

尧图内容编辑团队 内容团队

尧图内容编辑团队

本文由尧图网络内容编辑团队执笔。团队由资深项目经理、前端工程师与设计师组成,所有内容均来自亲手交付的真实项目,先讲清问题、再给出可落地的解法。尧图深耕北京网站建设十年,服务过京华建材集团、智造科技等各行业客户,把一线经验沉淀为可复用的行业观察。

  • 十年建站经验,覆盖建材、制造、服务、文创等
  • 项目经理把关选题与事实准确性
  • 工程师与设计师联合撰写专业细节
  • 统一编辑规范,保证文风与排版一致
  • 每月复盘转化数据,迭代选题方向

延伸阅读

相关资讯与近期热门内容

深度阅读推荐

建站决策前值得细读的三篇

网站改版的5个关键决策
2024-08-12

网站改版的5个关键决策

什么时候该改版、改到什么程度、如何避免流量掉光,京华建材集团改版复盘给出答案。

获取专属建站方案

看完文章,把您的行业与预算告诉我们,免费获取一份量身定制的官网建设方案与报价。

立即免费咨询