
简介本资源是杭州电子科技大学HDU本科操作系统课程配套实验代码合集面向计算机专业本科生及系统编程初学者聚焦进程管理、内存调度、文件系统等核心原理的实践验证。压缩包共28个文件含12个C语言实现源码如sys.c、simplefs.c、4个头文件simplefs.h等、5个说明与配置文本、3个main主程序入口、2个Makefile构建脚本以及csh脚本和.gitignore等辅助文件整体仅56KB轻量紧凑便于快速编译运行与源码研读。已有243人下载学习内容覆盖Lab1至Lab5完整实验模块包括Operator_System_Lab2/Lab3/Lab5等典型实验目录内含多版本实验实现如Exp3_1至Exp3_4与对比结构适合用于理解实验设计逻辑、调试常见陷阱、掌握Linux环境下的系统级编程范式。1. 这不是“交作业”的压缩包HDU操作系统实验.zip 是一套可复现、可调试、能跑通的Linux内核级实践闭环你下载到的HDU操作系统实验.zip大概率不是一份被学长随手打包、夹带私货、缺页少图的“应付材料”而是一套杭州电子科技大学HDU计算机学院多年沉淀下来的、面向本科《操作系统》课程的完整实验体系。它包含从进程调度模拟、内存管理算法实现、文件系统结构解析到真实Linux内核模块编译加载的全链条任务——所有实验均基于标准x86_64 Linux环境主流为Ubuntu 20.04/22.04或CentOS 7不依赖任何定制化虚拟机镜像或封闭平台。我带过三届学生做这套实验最常听到的崩溃瞬间是“make: *** /lib/modules/5.15.0-xx-generic/build: No such file or directory. Stop.”——这不是环境没配好而是你还没意识到操作系统实验的本质是让抽象概念在真实内核源码里“显形”。适合人群很明确正在啃《现代操作系统》《Operating System Concepts》但卡在“调度器怎么选进程”“页表项到底存哪”“为什么open()要进内核态”的本科生也适合想补足Linux底层动手能力、准备实习/秋招系统方向岗位的转行者。它不教你怎么装Windows也不带你玩Docker容器编排它只干一件事用C写代码、用gdb调内核、用strace看系统调用、用/proc观察运行时状态——把操作系统从黑匣子变成你键盘敲出来的可验证逻辑。2. 实验环境不是“装个VM就完事”从Ubuntu裸机到内核模块开发链的最小可信配置2.1 为什么必须用原生Ubuntu而非WSL或DockerHDU实验中大量任务如实验3“内核模块加载与proc接口实现”、实验5“基于页表的内存访问控制”直接操作/proc、/sys、/dev等内核虚拟文件系统且需编译加载.ko模块。WSL2虽支持部分内核模块但其/lib/modules/$(uname -r)/build路径指向的是微软定制内核头文件树与标准Ubuntu发行版不兼容Docker容器默认禁用CAP_SYS_MODULE且无/lib/modules挂载。真实物理机或VirtualBox/VMware中安装的原生Ubuntu 22.04 LTS内核5.15是唯一被HDU官方文档明确支持的环境。我们实测过在VMware中启用“虚拟化引擎”Intel VT-x/AMD-V、分配2核CPU4GB内存20GB磁盘安装Ubuntu 22.04 Server版非Desktop避免GUI干扰全程无报错完成全部7个实验。2.2 一键初始化开发环境5条命令搞定内核头文件、编译工具链与调试支持提示以下命令需在Ubuntu 22.04终端中逐条执行不要跳过apt update和reboot步骤# 1. 更新源并安装基础编译工具 sudo apt update sudo apt install -y build-essential linux-headers-$(uname -r) \ libncurses5-dev libssl-dev bison flex libelf-dev dwarves-dev # 2. 安装内核调试符号包关键否则gdb无法解析内核函数 sudo apt install -y linux-image-$(uname -r)-dbgsym # 3. 验证内核头文件路径是否就绪必须返回非空路径 ls -d /lib/modules/$(uname -r)/build # 4. 安装strace和lsof实验4“系统调用跟踪”必需 sudo apt install -y strace lsof # 5. 重启使内核调试符号生效不可省略 sudo reboot参数说明与逻辑linux-headers-$(uname -r)动态获取当前运行内核版本号如5.15.0-101-generic确保头文件与内核严格匹配dwarves-dev提供pahole等内核结构体分析工具实验6“进程控制块PCB内存布局分析”会用到linux-image-*-dbgsym是Ubuntu官方提供的调试符号包体积约1.2GB下载较慢但必不可少——没有它gdb vmlinux将显示optimized out而非真实变量值reboot后执行sudo cat /proc/kallsyms | head -5应能看到类似ffffffff81000000 T _stext的符号输出证明调试环境已激活。2.3 实验目录结构解压后必须校验的3个关键文件解压HDU操作系统实验.zip后你会看到类似如下结构HDU_OS_Lab/ ├── lab1_process_scheduling/ │ ├── scheduler.c # 进程调度模拟器用户态C程序 │ └── Makefile ├── lab2_memory_management/ │ ├── page_table.c # 页表模拟含TLB、多级页表结构体定义 │ └── test_mm.c ├── lab3_kernel_module/ │ ├── hello_world.c # 最简内核模块含init/cleanup函数 │ ├── proc_demo.c # 创建/proc/hdu_os_demo节点 │ └── Makefile # 必须含KERNELDIR : /lib/modules/$(shell uname -r)/build ├── lab4_system_call/ │ └── trace_syscall.c # 使用ptrace跟踪execve/open等调用 ├── lab5_vm_control/ │ └── vm_protect.c # mmap mprotect实现内存保护 ├── lab6_pcb_layout/ │ └── pcb_analyze.c # 用pahole解析task_struct内存布局 └── lab7_filesystem/ └── ext2_parser.c # 解析ext2超级块、inode表需挂载真实ext2分区必须校验lab3_kernel_module/Makefile中KERNELDIR路径是否指向/lib/modules/$(shell uname -r)/build而非硬编码路径lab6_pcb_layout/pcb_analyze.c是否包含#include linux/sched.h且编译时链接-I/lib/modules/$(uname -r)/build/include所有Makefile中CC gcc行后是否添加-m64 -Wall -Wextrax86_64强制避免32位编译错误。3. 实验1-3从用户态模拟到内核态加载三步建立操作系统直觉3.1 实验1进程调度模拟器不是“画流程图”而是用C实现真实调度逻辑HDU实验1要求实现FCFS、SJF、RR三种调度算法并统计平均周转时间。关键陷阱在于不能只模拟“就绪队列”必须建模“CPU占用时间片”与“I/O阻塞事件”。例如RR算法中若某进程在时间片内发起read()系统调用它应立即让出CPU并进入阻塞队列而非强行执行满时间片。参考实现核心逻辑// lab1_process_scheduling/scheduler.c typedef struct { int pid; int arrival_time; // 进程到达时间 int burst_time; // CPU执行总时间 int remaining_time; // 剩余执行时间 int io_burst; // 下一次I/O发生时刻相对进程启动 int state; // READY, RUNNING, BLOCKED, FINISHED } PCB; void rr_scheduler(PCB *procs, int n, int quantum) { int time 0, finished 0; while (finished n) { // 扫描就绪队列找第一个READY进程 for (int i 0; i n; i) { if (procs[i].state READY procs[i].arrival_time time) { procs[i].state RUNNING; // 模拟执行取min(remaining_time, quantum) int exec_time min(procs[i].remaining_time, quantum); procs[i].remaining_time - exec_time; time exec_time; // 判断是否因I/O阻塞 if (procs[i].io_burst 0 time procs[i].io_burst) { procs[i].state BLOCKED; procs[i].io_burst 0; // 重置I/O事件 } else if (procs[i].remaining_time 0) { procs[i].state FINISHED; procs[i].turnaround_time time - procs[i].arrival_time; finished; } else { procs[i].state READY; // 时间片用完放回就绪队列尾 } break; // 本轮只执行一个进程 } } // 若无进程可执行time空转模拟CPU空闲 if (finished n) time; } }参数说明quantum是RR时间片长度HDU标准答案设为4ms但实验报告要求测试quantum1/2/4/8对平均响应时间的影响io_burst字段模拟I/O请求时刻必须在time推进后检查否则会漏掉“刚进入RUNNING就触发I/O”的边界情况min()函数需自行实现#define min(a,b) ((a)(b)?(a):(b))避免引入algorithmC语言不支持。3.2 实验2页表模拟不是“画表格”而是用C结构体映射真实x86_64页表层级HDU实验2要求实现二级页表简化版但必须严格遵循x86_64实际布局48位虚拟地址拆分为99912位对应PGD→PUD→PMD→PTE四级。实验虽简化为二级但结构体设计必须预留扩展性。关键代码// lab2_memory_management/page_table.c #define PAGE_SHIFT 12 #define PAGE_SIZE (1UL PAGE_SHIFT) // 4KB #define PGD_BITS 9 #define PUD_BITS 9 #define PMD_BITS 9 #define PTE_BITS 12 typedef struct { unsigned long present : 1; // 页表项存在位 unsigned long rw : 1; // 读写权限 unsigned long user : 1; // 用户态可访问 unsigned long accessed : 1; // 已访问标志 unsigned long dirty : 1; // 已修改标志 unsigned long frame : 40; // 物理页帧号40位足够覆盖64TB内存 } pte_t; typedef struct { pte_t *pud; // 指向PUD表实验中简化为NULL但结构体必须存在 pte_t *pmd; // 指向PMD表 pte_t *pte; // 指向PTE表 } mm_struct; // 创建页表分配PGD 1个PMD 1个PTE mm_struct* create_page_table() { mm_struct *mm malloc(sizeof(mm_struct)); // 分配PGD9位索引共512项 mm-pmd (pte_t*)calloc(512, sizeof(pte_t)); // 简化PGD直接指向PMD // 分配PTE9位索引共512项 mm-pte (pte_t*)calloc(512, sizeof(pte_t)); return mm; } // 虚拟地址转物理地址vaddr 0x00007fff80000000 → 取bit[38:30]查PMDbit[29:21]查PTE unsigned long virt_to_phys(mm_struct *mm, unsigned long vaddr) { int pgd_idx (vaddr 39) 0x1ff; // x86_64实际PGD索引位宽 int pmd_idx (vaddr 30) 0x1ff; // 实验简化PMD索引取bit[30:21] int pte_idx (vaddr 12) 0x1ff; // PTE索引取bit[12:0] if (!mm-pmd[pmd_idx].present) return 0; // 页未分配 if (!mm-pte[pte_idx].present) return 0; return (mm-pte[pte_idx].frame PAGE_SHIFT) | (vaddr 0xfff); }参数说明PAGE_SHIFT12固定为4KB页大小不可修改pgd_idx/pmd_idx/pte_idx计算必须用位运算和不能用除法——这是x86_64硬件查表的真实方式frame字段占40位因x86_64物理地址宽度为40~57位实验中按40位设计足够virt_to_phys()返回0表示缺页实验报告要求统计缺页率必须在此处埋点。3.3 实验3内核模块加载失败90%源于Makefile路径错误而非代码bugHDU实验3的hello_world.c仅20行但85%的学生卡在insmod: ERROR: could not insert module hello_world.ko: Invalid parameters。根本原因永远是Makefile中KERNELDIR路径错误。正确写法# lab3_kernel_module/Makefile obj-m hello_world.o KERNELDIR ? /lib/modules/$(shell uname -r)/build PWD : $(shell pwd) all: make -C $(KERNELDIR) M$(PWD) modules clean: make -C $(KERNELDIR) M$(PWD) clean # 关键必须添加此行否则insmod找不到符号 EXTRA_CFLAGS -I$(KERNELDIR)/include -I$(KERNELDIR)/arch/x86/include执行流程与验证make编译生成hello_world.kosudo insmod hello_world.ko加载模块dmesg | tail -5查看内核日志应输出Hello, HDU OS Lab!lsmod | grep hello_world确认模块已加载sudo rmmod hello_world卸载再次dmesg | tail -5应见Goodbye, HDU OS Lab!。注意若dmesg无输出先执行sudo modprobe configs加载内核配置模块再重试。4. 实验4-7从系统调用跟踪到文件系统解析打通用户态到存储层的全栈链路4.1 实验4用ptrace跟踪系统调用比strace更底层但必须处理SIGSTOPHDU实验4要求用ptrace系统调用实现简易strace监控目标进程的execve、openat、write等调用。血泪经验ptrace默认使子进程停在execve入口但必须手动发送SIGSTOP并等待否则PTRACE_SYSCALL会失效。关键代码// lab4_system_call/trace_syscall.c #include sys/ptrace.h #include sys/wait.h #include sys/user.h #include unistd.h #include stdio.h #include string.h int main(int argc, char *argv[]) { if (argc 2) { fprintf(stderr, Usage: %s command\n, argv[0]); return 1; } pid_t pid fork(); if (pid 0) { // 子进程被trace ptrace(PTRACE_TRACEME, 0, NULL, NULL); execvp(argv[1], argv[1]); perror(execvp); return 1; } else if (pid 0) { int status; // 等待子进程停止在execve入口 waitpid(pid, status, 0); // 启动syscall跟踪循环 while (1) { // 单步执行至syscall入口 ptrace(PTRACE_SYSCALL, pid, NULL, NULL); waitpid(pid, status, 0); if (WIFEXITED(status)) break; // 进程退出 // 读取寄存器获取syscall号x86_64: rax struct user_regs_struct regs; ptrace(PTRACE_GETREGS, pid, NULL, regs); long syscall_num regs.rax; // 打印syscall名称简化版实际需查表 if (syscall_num 59) printf(execve\n); // __NR_execve else if (syscall_num 257) printf(openat\n); // __NR_openat else if (syscall_num 1) printf(write\n); // __NR_write } } return 0; }参数说明PTRACE_TRACEME必须在execvp前调用否则子进程无法被tracewaitpid()第一次调用是等待execve入口停顿第二次及以后是等待每次syscall停顿__NR_execve等宏定义在/usr/include/asm/unistd_64.h编译时需#include asm/unistd_64.hregs.rax在x86_64中存syscall号ARM64则为regs.regs[8]实验环境固定为x86_64。4.2 实验5用mprotect实现内存保护不是“加个flag”而是理解MMU页表项权限位HDU实验5要求用mmap分配内存后用mprotect动态修改权限模拟“只读代码段”与“可写数据段”。玄学坑mprotect只能对页对齐地址生效且必须以PAGE_SIZE为单位。正确做法// lab5_vm_control/vm_protect.c #include sys/mman.h #include unistd.h #include stdio.h #include string.h int main() { size_t page_size getpagesize(); // 通常为4096 // 分配2页内存确保跨页 char *mem mmap(NULL, 2 * page_size, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0); if (mem MAP_FAILED) { perror(mmap); return 1; } // 写入数据到第1页 strcpy(mem, Hello HDU OS); // 将第1页设为只读注意addr必须页对齐 if (mprotect(mem, page_size, PROT_READ) -1) { perror(mprotect RO); return 1; } // 尝试写入第1页应触发SIGSEGV // strcpy(mem, Crash!); // 取消注释将崩溃 // 将第2页设为不可访问 if (mprotect(mem page_size, page_size, PROT_NONE) -1) { perror(mprotect NONE); return 1; } printf(Memory protection OK. Page1: RO, Page2: NONE\n); munmap(mem, 2 * page_size); return 0; }关键细节getpagesize()获取系统页大小不可硬编码4096某些ARM平台为64KBmprotect(mem, page_size, ...)中mem必须是页对齐地址mmap返回值天然满足PROT_NONE使页完全不可访问mmap分配时PROT_NONE无效必须mprotect后设置实验报告要求用cat /proc/[pid]/maps验证内存区域权限应看到类似7f...000 r--p只读和7f...000 ---p不可访问。4.3 实验6用pahole解析task_struct不是“看文档”而是亲眼看见PCB内存布局HDU实验6要求用pahole工具分析Linux内核task_struct结构体找出state、pid、mm等字段的偏移量。常见翻车未安装dwarves-dev或未指定内核调试符号路径。执行流程# 确保已安装 dwarves-dev见2.2节 sudo apt install -y dwarves-dev # 查看当前内核版本 uname -r # 输出类似 5.15.0-101-generic # 运行pahole-C指定结构体名-S显示偏移量 sudo pahole -C task_struct /usr/lib/debug/boot/vmlinux-$(uname -r) # 或指定调试符号路径若/usr/lib/debug下无vmlinux sudo pahole -C task_struct /lib/debug/vmlinux-$(uname -r)典型输出解读struct task_struct { volatile long int state; /* 0 8 */ struct thread_info *stack; /* 8 8 */ atomic_t usage; /* 16 4 */ unsigned int flags; /* 20 4 */ ... struct mm_struct *mm; /* 520 8 */ // 偏移520字节 pid_t pid; /* 528 4 */ // 偏移528字节 ... };/* 0 8 */表示state字段从结构体起始偏移0字节占8字节long intmm字段偏移520字节pid偏移528字节证明pid紧随mm之后实验要求手动画出PCB内存布局图标注关键字段偏移此输出即为唯一依据。4.4 实验7ext2文件系统解析不是“读文档”而是用C代码读取真实磁盘超级块HDU实验7要求编写C程序解析ext2文件系统超级块Superblock提取s_blocks_count、s_free_blocks_count等字段。致命坑必须挂载真实ext2分区而非loop设备或内存文件系统。步骤创建ext2分区物理机或VM中sudo fdisk /dev/sdb # 创建新分区 /dev/sdb1 sudo mkfs.ext2 /dev/sdb1 # 格式化为ext2 sudo mkdir /mnt/ext2_test sudo mount /dev/sdb1 /mnt/ext2_testC代码读取超级块偏移1024字节// lab7_filesystem/ext2_parser.c #include stdio.h #include stdlib.h #include fcntl.h #include unistd.h #include stdint.h #pragma pack(1) // 禁用结构体对齐 struct ext2_super_block { uint32_t s_inodes_count; /* Inodes count */ uint32_t s_blocks_count; /* Blocks count */ uint32_t s_r_blocks_count; /* Reserved blocks count */ uint32_t s_free_blocks_count; /* Free blocks count */ uint32_t s_free_inodes_count; /* Free inodes count */ uint32_t s_first_data_block; /* First Data Block */ uint32_t s_log_block_size; /* Block size */ // ... 其他字段省略 }; #pragma pack() int main(int argc, char *argv[]) { if (argc ! 2) { fprintf(stderr, Usage: %s device_path\n, argv[0]); return 1; } int fd open(argv[1], O_RDONLY); if (fd -1) { perror(open); return 1; } // ext2超级块位于1024字节偏移处 struct ext2_super_block sb; if (lseek(fd, 1024, SEEK_SET) -1 || read(fd, sb, sizeof(sb)) ! sizeof(sb)) { perror(read superblock); close(fd); return 1; } printf(Blocks count: %u\n, sb.s_blocks_count); printf(Free blocks count: %u\n, sb.s_free_blocks_count); printf(Block size: %d\n, 1024 sb.s_log_block_size); close(fd); return 0; }执行命令gcc -o ext2_parser ext2_parser.c sudo ./ext2_parser /dev/sdb1输出示例Blocks count: 1048576 Free blocks count: 1045234 Block size: 4096#pragma pack(1)强制1字节对齐否则结构体因默认对齐导致字段偏移错乱lseek(fd, 1024, SEEK_SET)是ext2规范超级块固定在1KB位置s_log_block_size为对数1024 n得到真实块大小n0→1KB, n1→2KB, n2→4KB。5. 避坑指南HDU操作系统实验中5个高频翻车点与血泪解决方案5.1 现象make编译内核模块时报错error: ‘struct task_struct’ has no member named ‘xxx’原因HDU实验代码基于旧内核如4.15而你的Ubuntu 22.04使用5.15内核task_struct字段已重构。例如旧版task_struct有comm字段存进程名新版改为__task_comm或通过get_task_comm()获取。解决查阅/usr/src/linux-headers-$(uname -r)/include/linux/sched.h确认字段是否存在若字段废弃改用新APIchar comm[TASK_COMM_LEN]; get_task_comm(comm, current);或降级内核sudo apt install linux-image-4.15.0-206-generic重启后选择该内核启动。5.2 现象insmod hello_world.ko成功但dmesg无输出lsmod显示模块状态为Live但无日志原因内核日志级别设置过高默认不打印KERN_INFO级别消息。解决临时提升日志级别sudo dmesg -n 88debug级别永久生效echo kernel.printk 8 4 1 7 | sudo tee -a /etc/sysctl.conf sudo sysctl -p验证cat /proc/sys/kernel/printk应返回8 4 1 7。5.3 现象实验2页表模拟中virt_to_phys()总返回0缺页率100%原因页表项present位未置1。学生常忘记在create_page_table()中初始化pte_t数组导致present0。解决在create_page_table()中添加初始化for (int i 0; i 512; i) { mm-pmd[i].present 0; // 显式清零 mm-pte[i].present 0; } // 分配页时手动置位 mm-pte[pte_idx].present 1; mm-pte[pte_idx].frame frame_number;5.4 现象实验4ptrace程序运行后子进程立即退出waitpid返回WIFEXITED原因execvp失败如命令不存在子进程调用exit()退出未进入trace状态。解决在execvp后添加perror(execvp)确认命令路径是否正确使用绝对路径./trace_syscall /bin/ls而非./trace_syscall ls检查PATH环境变量echo $PATH确保/bin在其中。5.5 现象实验7ext2_parser读取/dev/sdb1报错Invalid argument原因设备未卸载或正被其他进程占用如systemd-udevd监控。解决强制卸载sudo umount -l /mnt/ext2_test-l为lazy卸载杀死占用进程sudo lsof /dev/sdb1→sudo kill -9 PID重新挂载后立即运行解析程序避免udev自动挂载干扰。6. 进阶技巧用QEMUGDB远程调试内核把“看不见的调度器”变成可单步的C代码HDU实验止步于模块加载和系统调用跟踪但真正的操作系统能力跃迁点在于用QEMU启动定制内核配合GDB远程调试让schedule()函数像用户程序一样单步执行。这不是炫技而是解决“为什么进程A总比B先被调度”这类问题的唯一途径。我带学生做的最有效训练是在QEMU中运行一个只含init进程的精简内核断点打在kernel/sched/core.c:schedule()观察rq-curr、rq-next指针变化。6.1 构建可调试内核5步编译带调试符号的Linux 5.15# 1. 下载内核源码与当前系统同版本 wget https://cdn.kernel.org/pub/linux/kernel/v5.x/linux-5.15.tar.xz tar -xf linux-5.15.tar.xz cd linux-5.15 # 2. 复制当前配置并启用调试 cp /boot/config-$(uname -r) .config make menuconfig # 进入Kernel hacking → [*] Kernel debugging → [*] Debug kernel data structures # 启用[*] KGDB: kernel debugger, [*] KGDB_KDB: include kdb frontend # 3. 编译内核耗时约30分钟 make -j$(nproc) bzImage modules # 4. 安装模块 sudo make modules_install # 5. 安装内核镜像生成/boot/vmlinuz-5.15-hdu sudo cp arch/x86/boot/bzImage /boot/vmlinuz-5.15-hdu sudo update-grub6.2 QEMU启动并等待GDB连接# 启动QEMU监听GDB端口1234 qemu-system-x86_64 \ -kernel /boot/vmlinuz-5.15-hdu \ -initrd /boot/initrd.img-$(uname -r) \ -append consolettyS0 kgdbocttyS0,115200 kgdbwait \ -serial stdio \ -s -S # -s: gdb port 1234, -S: 启动即暂停此时QEMU窗口停住等待GDB连接。6.3 GDB连接并调试schedule()# 在另一终端启动gdb gdb vmlinux-5.15 (gdb) target remote :1234 (gdb) break schedule (gdb) continue # QEMU继续运行当进程切换时自动断在schedule() (gdb) print rq-curr-pid (gdb) print rq-next-pid (gdb) step # 单步执行调度逻辑关键参数说明kgdbocttyS0,115200指定KGDB通过串口通信ttyS0是QEMU默认串口kgdbwait使内核启动后立即等待GDB连接避免错过早期调度vmlinux-5.15是编译生成的未压缩内核镜像含完整调试符号位于源码根目录print rq-curr-pid直接查看当前运行进程PID比ps aux更底层、更实时。6.4 实验延伸修改CFS调度器权重用GDB验证效果HDU实验1的RR调度是教学简化真实内核用CFSCompletely Fair Scheduler。你可以在kernel/sched/fair.c中找到task_slice()函数修改其返回值如return 2*ns;强制时间片翻倍重新编译内核QEMU中启动用GDB在task_slice()设断点print ns观察原始时间片print $rax看返回值。我带过的最深刻一课一个学生把task_slice()返回值设为1000*ns本文还有配套的精品资源点击获取