ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

北交大操作系统实验:从答案到跑通,内核模块与进程调度实战

北交大操作系统实验:从答案到跑通,内核模块与进程调度实战 简介这份资源是北京交通大学操作系统课程的实验答案与报告合集面向正在修读操作系统实验课、需要参考实现思路与报告写法的本科生。内容覆盖进程管理、内存管理、文件系统、死锁与资源分配、页面置换算法等核心实验模块每个实验均配有可运行的代码实现与对应的实验报告文档便于对照理解原理与落地细节。压缩包共41个文件以26个C语言源文件和5个C源文件为主体另有汇编、头文件、目标文件及Markdown报告文档整体约73KB体量轻便、结构清晰。目前已有201人学习下载。读者可从中获取各实验的完整代码框架、调度与置换算法的具体实现、进程通信与文件系统模拟的参考方案以及实验设计、结果分析与问题排查的写作范例适合作为动手实践与报告撰写的对照材料。1. 北交大操作系统实验从“找答案”到“自己跑通”的分水岭北京交通大学操作系统实验答案和报告.zip 这个标题每年期末季都会被翻出来。我见过太多人拿到压缩包把报告里的截图和结论抄一遍交上去实验课过了但 fork 返回两次、信号量初值该填几、页面置换命中率怎么算脑子里依然是空的。真正拉开差距的不是有没有答案而是你有没有把答案里的代码在自己的 Linux 环境里跑一遍看它到底输出什么、为什么这么输出。操作系统实验的核心价值在于“可观测”进程调度能看到时间片轮转的甘特图内存管理能打印页表变化文件系统能追踪 inode 分配。这些不是背概念能替代的。这篇笔记面向正在做北交大操作系统实验、或者想把这套实验真正吃透的人从环境搭建、核心实验拆解、参数调试到避坑给出一条能复现的路径。答案和报告只是起点跑通并改对才是终点。2. 实验环境搭建把“答案能跑”变成“你的机器能跑”2.1 为什么答案里的代码在你机器上跑不起来操作系统实验对环境的敏感度远超普通编程作业。北交大的实验通常基于 Linux 内核模块、系统调用或用户态模拟器答案压缩包里的代码往往是在特定内核版本、特定 gcc 版本下编译通过的。你直接拿到 Ubuntu 22.04 上跑大概率遇到三类问题内核头文件路径不匹配、系统调用号在新内核里已经变了、某些旧 API 被标记为 deprecated 导致编译警告升级为错误。常见做法是先用uname -r确认当前内核版本再对照实验指导书里要求的内核版本。如果指导书没写就去看答案代码里#include的头文件路径比如linux/sched.h在不同版本里结构体成员差异很大。我一般会建议在虚拟机里装一个和实验要求一致的内核版本而不是硬改代码去适配新内核因为后者会引入你无法判断的副作用。另一个高频翻车点是权限。内核模块的insmod需要 root但很多人在普通用户下编译完直接insmod报Operation not permitted然后开始怀疑代码。先sudo -i切到 root 再操作能省掉一半的排查时间。2.2 最小可复现环境虚拟机加内核头文件下面这套步骤是我在多次实验里验证过的最小环境搭建流程适用于大多数需要编译内核模块或修改系统调用的实验。# 查看当前内核版本确认与实验要求是否一致 uname -r # 安装编译内核模块必需的包以 Ubuntu/Debian 为例 sudo apt update sudo apt install build-essential linux-headers-$(uname -r) -y # 验证头文件路径是否存在 ls /lib/modules/$(uname -r)/build # 创建一个工作目录避免污染系统目录 mkdir -p ~/os-lab cd ~/os-lab这段命令的逻辑是先确认内核版本再安装对应版本的头文件包最后验证/lib/modules/$(uname -r)/build这个软链接是否指向正确的头文件目录。如果这个路径不存在后续make一定会报No such file or directory。参数上linux-headers-$(uname -r)里的$(uname -r)会自动替换成当前内核版本号不要手动写死否则内核一升级就失效。提示如果你用的是 WSL2内核模块编译会受限建议换完整虚拟机。VMware 或 VirtualBox 里装 Ubuntu 20.04/22.04 都可以分配 2 核 4G 内存足够跑完所有实验。2.3 编译和加载第一个内核模块环境就绪后用一个最简单的 hello 模块验证工具链是否完整。这个模块不涉及任何实验逻辑只用来确认make、insmod、dmesg这条链路是通的。// hello.c - 最小内核模块用于验证编译和加载环境 #include linux/module.h #include linux/kernel.h #include linux/init.h static int __init hello_init(void) { printk(KERN_INFO hello module loaded\n); return 0; } static void __exit hello_exit(void) { printk(KERN_INFO hello module unloaded\n); } module_init(hello_init); module_exit(hello_exit); MODULE_LICENSE(GPL);配套的 Makefile 如下# Makefile - 内核模块标准编译模板 obj-m hello.o # KDIR 指向当前内核的头文件目录不要写死路径 KDIR : /lib/modules/$(shell uname -r)/build PWD : $(shell pwd) all: make -C $(KDIR) M$(PWD) modules clean: make -C $(KDIR) M$(PWD) cleanobj-m hello.o告诉内核构建系统把hello.c编译成可加载模块。KDIR用$(shell uname -r)动态获取内核版本保证换机器不用改 Makefile。make -C $(KDIR) M$(PWD) modules是先切到内核源码目录再回到当前目录编译模块这是内核模块编译的标准写法。编译加载的命令序列make # 编译生成 hello.ko sudo insmod hello.ko # 加载模块 dmesg | tail -5 # 查看内核日志应出现 hello module loaded sudo rmmod hello # 卸载模块 dmesg | tail -5 # 应出现 hello module unloaded如果dmesg里看不到输出先确认printk的日志级别是否被过滤再用sudo dmesg -w实时观察。insmod报Invalid module format通常是内核版本和头文件版本不一致回到 2.2 节重新检查。3. 进程管理与调度实验从 fork 返回到时间片观测3.1 fork 和 wait 的返回值到底怎么判断进程实验里最经典的翻车点是把fork()的返回值判断写错。fork()在父进程返回子进程 PID大于 0在子进程返回 0出错返回 -1。很多人写成if (pid 0)处理父进程结果父子进程行为完全颠倒输出顺序诡异然后开始怀疑调度器。正确的判断结构应该是#include stdio.h #include unistd.h #include sys/wait.h int main(void) { pid_t pid fork(); if (pid 0) { perror(fork failed); return 1; } else if (pid 0) { // 子进程分支pid 为 0 printf(child process, pid%d, parent pid%d\n, getpid(), getppid()); } else { // 父进程分支pid 为子进程的 PID printf(parent process, child pid%d\n, pid); wait(NULL); // 等待子进程结束避免僵尸进程 } return 0; }wait(NULL)的作用是父进程阻塞等待任意一个子进程结束参数为NULL表示不关心子进程的退出状态。如果不加wait子进程结束后父进程可能先退出子进程变成孤儿进程被 init 收养实验报告里如果要求观察父子进程退出顺序就会得到错误结论。参数上getpid()返回当前进程 PIDgetppid()返回父进程 PID。在子进程里调用getppid()得到的是父进程 PID但如果父进程已经退出这个值会变成 1init 进程。这个细节在写实验报告分析进程树时经常被忽略。3.2 用 time 命令观测调度策略的差异北交大的调度实验通常要求对比不同调度策略下的运行时间。与其在报告里抄理论值不如自己用time命令跑一遍拿到真实数据。# 编译一个 CPU 密集型程序用于测试 cat cpu_burn.c EOF #include stdio.h int main(void) { volatile long sum 0; for (long i 0; i 1000000000L; i) { sum i; } printf(sum%ld\n, sum); return 0; } EOF gcc -O0 -o cpu_burn cpu_burn.c # 用不同 nice 值运行观察 real/user/sys 时间差异 time nice -n 0 ./cpu_burn time nice -n 19 ./cpu_burnnice -n 0是默认优先级nice -n 19是最低优先级。在单核或负载较高的机器上real时间墙钟时间会有明显差异user时间用户态 CPU 时间基本不变。这个对比能直观说明 nice 值影响的是调度权重不是 CPU 执行速度。参数说明-O0关闭优化防止编译器把循环优化掉导致测试无效。volatile修饰sum也是同样目的防止编译器认为这个变量没被使用而删除循环。这两个细节不做测出来的时间会短得离谱实验结论就站不住脚。注意在虚拟机里跑时间测试结果受宿主机负载影响很大。建议在测试前关闭其他占用 CPU 的程序或者多次运行取平均值。如果real时间波动超过 20%说明环境干扰太大换物理机或调整虚拟机 CPU 预留。3.3 调度实验报告里该放什么数据很多人写调度实验报告只放一张理论上的时间片轮转示意图没有自己的实测数据。我的习惯是至少放三组数据不同 nice 值下的real/user/sys时间对比、不同进程数下的总完成时间、以及vmstat或top里观察到的上下文切换次数。# 在另一个终端运行观察上下文切换 vmstat 1 10vmstat输出里的cs列就是上下文切换次数。进程数增多时cs会上升如果上升幅度远超进程数比例说明调度开销在增大。这个数据放进报告里比抄一段“时间片轮转公平但开销大”的文字有说服力得多。4. 内存管理与文件系统实验参数调不对结果全白费4.1 页面置换算法的命中率怎么算才准页面置换实验通常要求实现 FIFO、LRU、OPT 三种算法并对比命中率。翻车最多的地方是页面引用串的生成方式不统一导致三种算法跑的不是同一组数据对比毫无意义。正确做法是先固定引用串再分别喂给三种算法。下面是一个可复现的测试框架# page_replace.py - 三种页面置换算法对比 def fifo(pages, frame_count): frames [] hits 0 for p in pages: if p in frames: hits 1 else: if len(frames) frame_count: frames.append(p) else: frames.pop(0) # 淘汰最早进入的 frames.append(p) return hits def lru(pages, frame_count): frames [] hits 0 for p in pages: if p in frames: hits 1 frames.remove(p) frames.append(p) # 移到最近使用位置 else: if len(frames) frame_count: frames.append(p) else: frames.pop(0) # 淘汰最久未使用的 frames.append(p) return hits # 固定引用串保证三种算法输入一致 pages [7,0,1,2,0,3,0,4,2,3,0,3,2,1,2,0,1,7,0,1] frame_count 3 print(FIFO hits:, fifo(pages, frame_count)) print(LRU hits:, lru(pages, frame_count))fifo里用frames.pop(0)淘汰列表头部元素因为头部是最早进入的。lru里命中时先remove再append把页面移到列表尾部表示最近使用淘汰时仍然pop(0)。两个函数共用同一个pages列表和frame_count保证对比条件一致。参数上frame_count是物理块数通常取 3 或 4。引用串长度建议不少于 20太短的话随机性太大命中率差异不明显。如果要更严谨可以生成多组随机引用串每组跑完取平均命中率。4.2 文件系统实验inode 和块大小的关系文件系统实验里经常要求计算不同块大小下的 inode 数量、最大文件大小等参数。这些计算依赖具体的文件系统格式不能凭感觉填。以 ext2 为例关键参数关系如下参数典型值影响块大小1024/2048/4096 字节块越大inode 数量越少大文件性能越好inode 大小128 字节决定直接/间接块指针数量每块 inode 数块大小 / inode 大小4096/128 32直接块指针12 个直接指向数据块一级间接1 个指向一个块块内存放块号计算最大文件大小时直接块贡献12 * 块大小一级间接贡献(块大小/4) * 块大小二级间接再乘一层。这个计算过程要写进报告不能只写结论。# 查看当前文件系统的块大小和 inode 信息 sudo dumpe2fs -h /dev/sda1 2/dev/null | grep -E Block size|Inode size|Inode countdumpe2fs -h只显示超级块信息不遍历整个磁盘速度快。grep过滤出块大小、inode 大小和 inode 总数三个关键值。如果你的根分区不是 ext2/ext4这个命令可能不适用可以用stat -f查看文件系统类型后再决定。提示实验报告里如果要求画 inode 和多级索引的结构图用表格或文字描述清楚每一级指针的数量和指向关系即可不需要画复杂的图。关键是让读者能根据你的描述算出最大文件大小。4.3 用 dd 和 stat 验证文件系统行为理论算完之后用实际命令验证一下能发现很多计算时忽略的细节。# 创建一个 10MB 的文件观察占用块数 dd if/dev/zero oftestfile bs1M count10 stat testfile # 创建一个 1 字节的文件观察是否仍占用一个块 echo -n a tinyfile stat tinyfilestat输出里的Blocks字段是占用的 512 字节块数IO Block是文件系统的块大小。10MB 文件占用的块数应该接近10*1024*1024/512但会有少量元数据开销。1 字节文件也会占用至少一个块这就是为什么大量小文件会浪费空间。这个实测结果放进报告比单纯抄“块大小影响空间利用率”要具体得多。5. 避坑与排查那些让实验报告重写的瞬间5.1 内核模块编译报错 “unknown symbol”现象insmod时报unknown symbol in moduledmesg里显示某个函数找不到。原因模块里调用了内核未导出的符号或者内核版本与编译时使用的头文件版本不一致。常见于使用了EXPORT_SYMBOL未标记的内部函数。解决先用modinfo hello.ko查看模块依赖再用grep 函数名 /proc/kallsyms确认该符号是否在内核符号表中。如果不在说明这个函数不能被模块直接调用需要换一种实现方式。如果是版本不一致重新安装对应版本的头文件并make clean后重新编译。5.2 fork 之后 printf 输出重复现象fork()之前调用了printf结果父进程和子进程都输出了同样的内容看起来像执行了两次。原因printf有缓冲区fork()会复制父进程的缓冲区到子进程。如果缓冲区在fork时还没刷新子进程会带着同样的缓冲内容一起输出。解决在fork()之前调用fflush(stdout)强制刷新缓冲区或者用write(STDOUT_FILENO, ...)替代printf因为write是无缓冲的系统调用。这个坑在实验报告里经常被误判为“fork 执行了两次”其实是缓冲区复制。5.3 页面置换命中率算出来是 100%现象三种算法的命中率都是 100%或者都接近 100%对比不出差异。原因引用串太短或者引用串里重复页面太多导致所有算法都能命中。另一个可能是frame_count设得太大超过了引用串中不同页面的数量。解决检查引用串里不同页面的数量是否大于frame_count。如果不同页面只有 3 个而frame_count也是 3那所有算法都会全部命中。把frame_count降到 2 或 3引用串长度增加到 20 以上并且确保不同页面数量明显大于frame_count。5.4 虚拟机里时间测试结果不可信现象time命令测出来的real时间波动极大同一程序两次运行差好几倍。原因虚拟机 CPU 被宿主机其他进程抢占或者虚拟机 CPU 核心数分配不足。real时间受外部负载影响user时间相对稳定。解决测试前关闭宿主机上占用 CPU 的程序给虚拟机分配至少 2 个 CPU 核心。如果条件允许在物理机或云主机上跑时间测试。报告里如果必须用虚拟机数据注明测试环境并多次取平均不要拿单次结果下结论。5.5 实验报告里的代码和答案一模一样现象查重时被判定抄袭或者老师一眼看出代码风格和答案一致。原因直接复制答案代码没有自己重写和调试。解决答案只用来对照逻辑代码自己敲一遍。变量名、注释、函数拆分方式都改成自己的习惯。更重要的是把调试过程中遇到的报错和解决过程写进报告这部分是答案里没有的也是最能体现你真正做过实验的证据。6. 把答案变成自己的一个可复用的实验验证习惯答案和报告压缩包最大的价值不是让你抄而是给你一个对照基准。我自己的习惯是拿到答案后先不看代码按实验指导书自己写一版跑通之后再把答案打开对比两版在关键逻辑上的差异。差异点往往就是实验要考察的核心比如 fork 返回值判断、信号量初值设置、页面置换的淘汰位置。具体操作上我会给每个实验建一个lab-xx目录里面放三样东西自己的代码、答案代码、一个diff.md记录两版差异和原因。这个diff.md在期末复习时比任何笔记都有用因为它记录的是你当时没想明白、后来搞懂了的点。验证方法上除了跑通答案里的测试用例我还会自己构造边界输入。比如进程实验里 fork 失败的情况、内存实验里frame_count为 1 的情况、文件系统实验里文件大小为 0 的情况。这些边界在答案里通常不会覆盖但考试和实际工作中经常遇到。最后一个技巧把实验报告里的“实验结果”部分写成可复现的命令序列而不是截图。截图会过时命令不会。比如“运行make sudo insmod hello.ko dmesg | tail观察到输出hello module loaded”这样的描述任何人拿到你的报告都能复现也经得起老师追问。我踩过最深的坑是第一次做内核模块实验时直接抄了答案的 Makefile结果答案里写死了/usr/src/linux-headers-5.4.0-42-generic而我机器上是 5.15 内核编译报错后我花了两个小时才找到这个写死的路径。从那以后我所有的 Makefile 都用$(shell uname -r)动态获取版本。这个习惯后来在换机器、升级内核时救了我很多次。希望帮到你。本文还有配套的精品资源点击获取
返回列表