恒美微站
首页
关于我们
建站服务
主题模板
案例展示
资讯中心
联系我们
南开大学软件学院操作系统实验:从内核模块到调度与内存管理实战
首页
资讯中心
/
南开大学软件学院操作系统实验:从内核模块到调度与内存管理实战
南开大学软件学院操作系统实验:从内核模块到调度与内存管理实战
发布时间:2026/10/8 1:46:02
简介这份资源是南开大学软件学院操作系统课程的配套课件面向计算机相关专业学生及备考操作系统的学习者用于梳理课程核心概念与期末复习要点。内容围绕操作系统的基本概念、组成、类型、服务、结构与特征展开涵盖进程创建与终止、临界区互斥访问、中断与陷入的区别、系统调用类型、假脱机技术以及类Unix与Windows系统对比等知识点并附有期末知识点整理便于对照复习。资源包共1个PDF文件大小约1.3MB以课件讲义形式呈现适合课堂同步学习与考前集中回顾。目前已有739人学习下载读者可借此快速建立操作系统的整体知识框架掌握进程管理、内存缓存、设备管理与文件管理等模块的关键结论理清多道批处理、分时与实时系统的异同为课程考试和后续深入学习打下基础。1. 南开大学软件学院操作系统课从背算法到跑通内核模块中间差了什么如果你正在搜“南开大学软件学院操作系统”大概率不是想听一遍 PV 操作的定义而是想知道这门课到底要求做到什么程度、实验环境怎么搭、那些调度算法和内存管理代码怎么才能在自己机器上跑起来。我当年第一次做操作系统实验时把银行家算法背得滚瓜烂熟结果连一个最简单的内核模块都没编译过这就是典型的“理论会了、动手翻车”。南开软件学院的操作系统教学核心不是让你默写概念而是让你理解一个进程从创建到销毁、一块内存从申请到回收、一次磁盘 IO 从请求到完成底层到底发生了什么。这篇文章面向三类人正在修这门课、需要把实验做出来的同学考研复试要手写代码解释调度逻辑的选手以及工作后发现自己对线程池、零拷贝、页缓存这些概念只停留在八股层面的工程师。我会按“先建立可运行的环境再逐个拆解核心机制最后落到能自己扩展”的顺序讲每一步都给可复现的命令和代码。2. 把实验环境搭起来从零编译一个可加载的内核模块2.1 为什么操作系统实验不能只在用户态模拟很多同学第一反应是用 Python 写个模拟器打印一下进程切换的日志觉得就算完成了。但操作系统课的核心价值在于让你直面“特权级切换、内存映射、中断处理”这些用户态根本碰不到的东西。南开软件学院的操作系统实验通常要求基于 Linux 内核做修改或编写模块因为只有在内核态你才能真实地操作 task_struct、mm_struct、页表这些数据结构。用户态模拟能帮你理解算法逻辑但理解不了“为什么自旋锁在单核上要关抢占”“为什么 copy_to_user 可能睡眠”。所以第一步不是写算法而是让一个自己编译的模块能在系统里 insmod 成功。2.2 最小内核模块的编译与加载先确认你的开发环境。我一般用 Ubuntu 22.04 或 24.04内核头文件必须和当前运行内核版本一致。下面这套命令是血泪经验版本对不上编译出来的模块 insmod 会直接报 “invalid module format”。# 查看当前内核版本 uname -r # 安装对应版本的头文件以 6.8.0-45-generic 为例实际替换成你的版本 sudo apt update sudo apt install -y build-essential linux-headers-$(uname -r) # 验证头文件目录存在 ls /lib/modules/$(uname -r)/build接下来写一个最简模块只做加载和卸载时的打印// hello_os.c #include linux/init.h #include linux/module.h #include linux/kernel.h MODULE_LICENSE(GPL); MODULE_AUTHOR(YourName); MODULE_DESCRIPTION(Minimal module for OS lab); static int __init hello_os_init(void) { printk(KERN_INFO hello_os: module loaded\n); return 0; } static void __exit hello_os_exit(void) { printk(KERN_INFO hello_os: module unloaded\n); } module_init(hello_os_init); module_exit(hello_os_exit);对应的 Makefile 必须用内核源码树里的 kbuild 体系不能自己手写 gcc 命令obj-m hello_os.o KDIR : /lib/modules/$(shell uname -r)/build PWD : $(shell pwd) all: $(MAKE) -C $(KDIR) M$(PWD) modules clean: $(MAKE) -C $(KDIR) M$(PWD) clean编译和加载make sudo insmod hello_os.ko dmesg | tail -5 sudo rmmod hello_os dmesg | tail -5逻辑说明module_init和module_exit分别指定加载和卸载时执行的函数printk是内核态打印输出到环形缓冲区用dmesg查看。参数说明KERN_INFO是日志级别数字越小优先级越高MODULE_LICENSE(GPL)必须写否则内核会标记为污染。这一步跑通你才有资格进入后面的进程和内存实验。2.3 用 QEMU 隔离实验环境避免反复重装直接在自己主力机上 insmod 有风险一个空指针就可能 panic 导致文件系统损坏。我习惯用 QEMU 跑一个最小根文件系统把编译好的模块传进去测试。常见做法是下载 busybox 编译一个 initramfs或者直接用发行版提供的 cloud image。启动命令示例qemu-system-x86_64 \ -m 1024 \ -kernel /boot/vmlinuz-$(uname -r) \ -initrd ./initramfs.cpio.gz \ -append consolettyS0 nokaslr \ -nographic \ -s -S-s -S是给 gdb 调试用的nokaslr关闭地址随机化方便你对照源码看地址。这个环境里你可以随意 insmod、rmmod甚至改内核参数崩了重启 QEMU 只要几秒。很多同学在物理机上折腾一下午重装系统两小时就是没做隔离。3. 进程调度实验把 CFS 的 vruntime 算清楚3.1 调度器到底在调度什么操作系统课讲调度最容易陷入“先来先服务、短作业优先、时间片轮转”的算法罗列。但 Linux 实际用的 CFS完全公平调度器核心只有一个概念vruntime。每个进程有一个虚拟运行时间调度器每次选 vruntime 最小的那个。vruntime 的增长速度和进程权重成反比权重又由 nice 值决定。你 nice 值越低优先级越高vruntime 涨得越慢被调度的机会就越多。南开软件学院的操作系统实验里通常会要求你修改调度策略或者统计调度延迟这时候你必须能读懂kernel/sched/fair.c里的update_curr和pick_next_task_fair。3.2 用 tracepoint 观测真实调度行为不要靠 printf 在内核里打日志那会改变调度时序。正确做法是用 ftrace 的 sched_switch tracepoint。下面这套命令可以抓取 5 秒内的调度切换# 确保 tracefs 已挂载 mount | grep tracefs || sudo mount -t tracefs nodev /sys/kernel/tracing # 清空旧数据开启 sched_switch 事件 echo 0 | sudo tee /sys/kernel/tracing/tracing_on echo /sys/kernel/tracing/trace echo 1 | sudo tee /sys/kernel/tracing/events/sched/sched_switch/enable # 开始记录 echo 1 | sudo tee /sys/kernel/tracing/tracing_on sleep 5 echo 0 | sudo tee /sys/kernel/tracing/tracing_on # 查看结果 sudo cat /sys/kernel/tracing/trace | head -50输出里你会看到prev_comm、prev_pid、next_comm、next_pid以及prev_state。参数说明prev_state为R表示可运行但被抢占S表示可中断睡眠D表示不可中断睡眠。如果你在写调度分析实验这个输出就是最原始的数据源。我一般会写个 Python 脚本解析这个 trace统计每个进程被切换出去的次数和运行时长分布。3.3 修改 nice 值观察 vruntime 差异写一个简单的 CPU 密集型程序启动两个实例一个 nice 0一个 nice 10然后用perf或者/proc/pid/schedstat看运行时间比例。# 编译一个死循环 cat cpu_hog.c EOF int main() { volatile unsigned long x 0; while (1) { x; } return 0; } EOF gcc -O0 cpu_hog.c -o cpu_hog # 启动两个一个普通一个低优先级 ./cpu_hog PID1$! nice -n 10 ./cpu_hog PID2$! # 等 10 秒后看 schedstat sleep 10 cat /proc/$PID1/schedstat cat /proc/$PID2/schedstat kill $PID1 $PID2schedstat三个字段分别是在 CPU 上运行的时间ns、在运行队列等待的时间ns、被调度的时间片次数。正常情况下 nice 0 的运行时间大约是 nice 10 的 10 倍左右因为权重比约为 1024:110。这个实验能让你直观感受到“公平”不是平均而是按权重分配。如果你要改调度器代码改的就是set_load_weight里的映射表。4. 内存管理实验从缺页异常到写时复制4.1 为什么 malloc 之后不马上分配物理页操作系统课必讲虚拟内存但很多人没亲手验证过“malloc 返回的地址在第一次写之前没有物理页”。Linux 采用按需分页malloc只是扩大了mm_struct里的 VMA虚拟内存区域真正的物理页在第一次访问触发缺页异常时才分配。你可以用/proc/pid/maps和/proc/pid/smaps观察这个变化。cat mem_test.c EOF #include stdio.h #include stdlib.h #include string.h #include unistd.h int main() { size_t size 100 * 1024 * 1024; // 100MB char *p malloc(size); printf(malloc returned: %p\n, p); printf(PID: %d, press enter to touch memory\n, getpid()); getchar(); memset(p, A, size); printf(memory touched, press enter to exit\n); getchar(); free(p); return 0; } EOF gcc -O0 mem_test.c -o mem_test ./mem_test在第一个回车前另开终端执行cat /proc/$(pgrep mem_test)/smaps | grep -A5 100000000附近你会看到 Rss 为 0。第二个回车后Rss 变成约 100MB。参数说明Rss是常驻内存集Pss是比例共享内存Shared_Clean/Dirty区分共享页。这个实验直接对应“缺页异常处理”和“匿名页分配”两个知识点。4.2 用 mprotect 触发写时复制写时复制COW是 fork 高效的关键。fork 之后父子进程共享物理页但页表项被标记为只读。任何一方写入就会触发保护异常内核复制一份新的物理页。你可以用mprotect手动模拟这个过程#include stdio.h #include stdlib.h #include string.h #include sys/mman.h #include unistd.h int main() { size_t size 4096; char *p mmap(NULL, size, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0); strcpy(p, hello); printf(before mprotect: %s\n, p); // 改为只读 mprotect(p, size, PROT_READ); printf(after mprotect read-only, try write...\n); // 这一行会触发 SIGSEGV除非你处理信号 // p[0] H; // 改回可写 mprotect(p, size, PROT_READ | PROT_WRITE); p[0] H; printf(after restore write: %s\n, p); munmap(p, size); return 0; }逻辑说明mprotect修改 VMA 的权限位写入只读页会触发do_page_fault内核检查 VMA 权限后决定是发送 SIGSEGV 还是执行 COW。参数说明MAP_PRIVATE表示私有映射写时复制MAP_ANONYMOUS表示不关联文件匿名页。如果你在实验里要统计缺页次数可以用getrusage的ru_minflt和ru_majflt。4.3 用 /proc/vmstat 看全局缺页统计# 记录当前值 grep -E pgfault|pgmajfault /proc/vmstat # 运行你的内存测试程序 ./mem_test # 再次查看 grep -E pgfault|pgmajfault /proc/vmstatpgfault是次要缺页页在内存但页表未建立pgmajfault是主要缺页需要读磁盘。匿名页的首次访问通常是次要缺页因为零页已经在内存里。这个区分在写实验报告时非常关键很多同学把两者混为一谈。5. 避坑与排查操作系统实验里最容易翻车的 5 个点5.1 insmod 报 “invalid module format”现象编译成功但insmod提示模块格式无效。原因编译时用的内核头文件版本和当前运行内核不一致或者编译器版本差异导致 vermagic 不匹配。解决用modinfo hello_os.ko | grep vermagic查看模块的 vermagic和uname -r对比。必须安装linux-headers-$(uname -r)并且用同一个 gcc 版本编译。如果还是不行检查是否开启了 CONFIG_MODVERSIONS需要make modules_prepare。5.2 QEMU 启动后卡在 “Booting the kernel”现象QEMU 输出这行后无响应。原因initramfs 里没有正确的/init或者控制台配置错误。解决确认-append里的consolettyS0和-nographic匹配initramfs 里的 init 脚本必须有可执行权限并且最后要exec /bin/sh或者挂载真正的根文件系统。我一般会在 init 里加echo init started来定位。5.3 ftrace 抓不到 sched_switch现象trace文件为空。原因tracefs 没挂载或者事件没使能或者tracing_on被其他工具关了。解决按顺序执行mount -t tracefs、echo 1 events/sched/sched_switch/enable、echo 1 tracing_on。注意tracing_on和events/.../enable是两个独立开关都要打开。另外如果系统用了trace-cmd之类的工具可能会抢占 tracefs。5.4 内存测试程序 Rss 不增长现象memset之后/proc/pid/smaps里 Rss 还是 0。原因编译器优化把memset删了或者malloc被优化成calloc但没实际触碰。解决用-O0编译并且把memset的结果打印出来或者用volatile指针写入。另外如果系统开启了内存压缩zramRss 可能被压缩看Swap和SwapPss更准确。5.5 修改内核代码后编译报错 “undefined reference”现象你加了一个函数调用但链接时报未定义。原因内核符号没有导出EXPORT_SYMBOL或者你调用了用户态库函数。解决内核模块只能调用内核导出的符号用grep在/proc/kallsyms里查。如果确实需要修改内核源码里的EXPORT_SYMBOL并重新编译整个内核。不要试图在模块里#include stdio.h内核里没有 libc。6. 进阶技巧用 eBPF 观测调度延迟并验证你的修改如果你已经把前面的实验跑通想进一步验证自己对调度器的理解最有效的手段是用 eBPF 写一个调度延迟追踪工具。不需要改内核源码也不需要重启直接 attach 到sched_wakeup和sched_switch两个 tracepoint计算一个进程从被唤醒到真正上 CPU 的时间。下面是一个用 BCC 写的最小示例#!/usr/bin/env python3 from bcc import BPF program r BPF_HASH(start, u32, u64); TRACEPOINT_PROBE(sched, sched_wakeup) { u32 pid args-pid; u64 ts bpf_ktime_get_ns(); start.update(pid, ts); return 0; } TRACEPOINT_PROBE(sched, sched_switch) { u32 pid args-next_pid; u64 *tsp start.lookup(pid); if (tsp ! 0) { u64 delta bpf_ktime_get_ns() - *tsp; bpf_trace_printk(pid %d wakeup-switch: %llu ns\n, pid, delta); start.delete(pid); } return 0; } b BPF(textprogram) print(Tracing scheduler latency... Ctrl-C to stop.) b.trace_print()逻辑说明sched_wakeup记录进程被唤醒的时间戳sched_switch在切换到该进程时计算差值。参数说明args-pid是唤醒的目标进程args-next_pid是即将上 CPU 的进程。这个脚本能直接量化“调度延迟”如果你修改了 CFS 的sysctl_sched_latency或者sysctl_sched_min_granularity可以用它对比修改前后的延迟分布。我一般会跑一个stress-ng --cpu 4制造负载然后观察延迟的 P99 变化。验证方法上还有一个更简单的技巧用perf sched latency直接看内核自带的统计。sudo perf sched record -- sleep 5 sudo perf sched latency输出会按进程列出平均延迟、最大延迟和运行次数。如果你在实验里改了调度策略这个命令就是你的后悔药——改之前跑一次改之后再跑一次数据不会骗人。最后说个我自己的习惯每次改内核参数前先用sysctl -a | grep sched把默认值存下来改完对比出问题直接sysctl -w恢复不用重编译。操作系统这门课动手踩过的坑才是真正记住的知识希望帮到你。本文还有配套的精品资源点击获取