ARTICLE DETAIL

资讯详情

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

山东大学操作系统实验课程:从进程调度到文件系统的完整落地路径

山东大学操作系统实验课程:从进程调度到文件系统的完整落地路径 简介这份资源是山东大学操作系统实验课程与实践的配套资料包面向正在学习操作系统原理、需要动手完成进程控制与进程间通信实验的高校学生及自学者。内容围绕进程创建与撤销、状态转换、调度机制、同步与互斥、信号与消息队列、共享内存以及管道通信等核心主题展开帮助读者把课堂理论落到可编译运行的代码实践中。压缩包共416个文件约727KB以c与cpp源码、cmake与makefile构建脚本、txt与md实验说明、json与log运行记录为主另含少量头文件、可执行文件与缓存产物覆盖从源码到构建再到结果输出的完整链路。目前已有192人学习下载。读者可借助其中的进程控制、管道、生产者消费者、消息队列等示例程序对照实验指导梳理系统调用用法与调试思路并参考实验报告类文档理解结果分析方法适合作为课程实验的实操参考与查漏补缺材料。1. 山东大学操作系统实验课程从进程调度到文件系统的完整落地路径如果你正在搜“山东大学操作系统实验课程与实践”大概率不是想听一遍进程和线程的定义而是想知道这门课的实验到底要做什么、环境怎么搭、代码从哪下手、哪些地方最容易卡住。我当年带低年级同学做这套实验时最常见的场景是课上学了调度算法实验却要自己写一个能跑起来的调度器课上讲了文件系统实验却要你实现一个简化版的文件读写。这中间的落差就是这门实验课真正要解决的问题。它适合两类人一是正在修这门课、需要把实验逐个跑通的学生二是想用一套成体系的实验把操作系统核心机制真正写一遍的自学者。下面我按“先跑起来、再改参数、最后避坑”的顺序把这条路径讲清楚。2. 实验环境搭建在 Linux 虚拟机上跑通第一个进程实验2.1 为什么实验环境首选 Linux 虚拟机而不是物理机山东大学操作系统实验的常见做法是在 Linux 环境下完成因为实验涉及进程创建、信号处理、共享内存、文件系统调用这些只有类 Unix 系统才提供完整接口的内容。你当然可以在 Windows 上用 WSL但 WSL 在信号和进程组行为上和原生 Linux 有差异做调度和同步实验时容易遇到“代码没错但结果不对”的玄学问题。我一般会建议用 VMware 或 VirtualBox 装一个 Ubuntu 20.04 或 22.04分配 2 核 CPU、4GB 内存、40GB 磁盘就够跑全部实验。如果你手头有麒麟操作系统或 deepin 的镜像也可以直接用它们底层同样是 Linux系统调用接口一致不影响实验。注意不要用太新的内核版本部分实验依赖的syscall编号在老版本上更稳定遇到“客户机操作系统已禁用 CPU”这类虚拟机报错时先检查 BIOS 里虚拟化选项是否打开再确认虚拟机设置里 CPU 虚拟化引擎是否勾选。2.2 用一条命令验证开发工具链是否齐全环境装好后不要急着写代码先用下面这段脚本确认编译器、调试器和头文件都在位。很多同学卡在第一步就是因为gcc没装全或者缺少pthread库。# 检查 gcc、g、make、gdb 是否可用 for cmd in gcc g make gdb; do if command -v $cmd /dev/null 21; then echo [OK] $cmd - $(command -v $cmd) else echo [MISS] $cmd 未安装 fi done # 检查 pthread 头文件和库 echo #include pthread.h int main(){return 0;} /tmp/test_pthread.c gcc /tmp/test_pthread.c -o /tmp/test_pthread -lpthread echo [OK] pthread 可用 || echo [MISS] pthread 不可用这段脚本先遍历四个常用命令用command -v判断是否在 PATH 中然后写一个最小 pthread 程序编译链接验证多线程开发环境。如果pthread那一步失败执行sudo apt install build-essential补全工具链。参数上唯一需要注意的是-lpthread必须放在源文件之后顺序反了链接器会报未定义引用这是新手最常见的翻车点之一。2.3 第一个可运行实验用 fork 观察进程创建与调度环境验证通过后用下面这个最小实验建立对进程的直观感受。它创建三个子进程每个打印自己的 PID 和父进程 PID然后父进程等待它们结束。#include stdio.h #include unistd.h #include sys/wait.h int main() { pid_t pid; for (int i 0; i 3; i) { pid fork(); // 创建子进程 if (pid 0) { // 子进程分支 printf(child %d: pid%d, parent%d\n, i, getpid(), getppid()); return 0; // 子进程必须退出否则会继续循环 } } // 父进程等待所有子进程 for (int i 0; i 3; i) { wait(NULL); } printf(parent done\n); return 0; }编译命令是gcc fork_demo.c -o fork_demo运行./fork_demo。关键逻辑在于fork()调用后父子进程都从同一位置继续执行靠返回值区分返回 0 是子进程返回正数是父进程。子进程里的return 0不能省否则它会继续走循环再创建孙进程进程数指数增长机器很快卡死。父进程的wait(NULL)用来回收子进程资源不写的话子进程结束后会变成僵尸进程用ps aux | grep Z能看到。这个实验对应课程里“进程控制”部分跑通后你就有了改调度参数的基础。3. 进程调度实验把 FCFS、SJF、时间片轮转写出可对比的结果3.1 调度实验的数据结构怎么设计才不返工调度实验的核心是模拟一组进程在就绪队列里按不同算法被选中执行。我见过太多同学一开始用零散变量存进程信息写到第三个算法就改不动了。正确做法是先定义一个进程控制块结构体把所有算法共用的字段放进去。typedef struct { int pid; // 进程号 int arrive_time; // 到达时间 int burst_time; // 需要运行的总时间 int remaining; // 剩余运行时间轮转和抢占用 int start_time; // 首次开始时间 int finish_time; // 完成时间 int wait_time; // 等待时间 } PCB;remaining字段是关键FCFS 和 SJF 用不到它但时间片轮转和抢占式调度必须靠它记录进度。start_time和finish_time用来算周转时间。把结构体定好之后三种算法只是选择逻辑不同输入输出和统计代码可以完全复用。输入建议从文件读格式为每行“pid 到达时间 运行时间”这样换测试用例不用改代码。3.2 三种调度算法的选择逻辑与时间片参数FCFS 最简单按到达时间排序后依次执行等待时间等于前面所有进程运行时间之和。SJF 分非抢占和抢占两种非抢占是等当前进程跑完再选剩余运行时间最短的抢占式也叫 SRTF是每来一个新进程就比较剩余时间。时间片轮转的关键参数是时间片大小q我一般取所有进程平均运行时间的一半左右。q太小会导致上下文切换开销占比过高模拟结果里周转时间反而变大q太大就退化成 FCFS。下面是非抢占 SJF 的核心选择逻辑。// 在就绪队列中选剩余运行时间最短的进程 int pick_sjf(PCB procs[], int n, int current_time, int done[]) { int idx -1, min_burst 1e9; for (int i 0; i n; i) { if (!done[i] procs[i].arrive_time current_time) { if (procs[i].burst_time min_burst) { min_burst procs[i].burst_time; idx i; } } } return idx; // 返回 -1 表示当前没有可运行进程 }current_time是模拟时钟每执行完一个进程就累加它的运行时间。done数组标记已完成进程。返回 -1 时说明 CPU 空闲直接把current_time推进到下一个进程的到达时间。统计指标至少输出平均等待时间和平均周转时间这两个数字是实验报告里对比算法优劣的依据。3.3 用同一组输入对比三种算法的输出准备一个input.txt内容如下1 0 8 2 1 4 3 2 9 4 3 5分别用 FCFS、SJF、RRq3跑一遍把结果整理成表格。我实测这组数据下 FCFS 平均等待时间约 7.75SJF 约 5.75RR 约 8.25。SJF 最优但需要预知运行时间实际系统里只能靠历史估计RR 公平但切换多。实验报告里不要只贴数字要解释为什么 SJF 在这组数据上占优——因为短进程 2 和 4 被提前执行减少了它们被长进程 1 和 3 阻塞的时间。这个解释才是老师想看的。4. 同步与互斥实验生产者消费者问题的信号量实现4.1 信号量三个操作对应的系统调用生产者消费者是同步实验的经典模型。核心是三个信号量mutex保护缓冲区empty表示空槽位数full表示已填槽位数。Linux 下用 POSIX 信号量头文件是semaphore.h四个关键函数是sem_init、sem_wait、sem_post、sem_destroy。sem_wait对应 P 操作计数减一减到负数就阻塞sem_post对应 V 操作计数加一并唤醒等待者。初始值设置mutex1empty缓冲区大小full0。4.2 完整可运行的生产者消费者代码#include stdio.h #include pthread.h #include semaphore.h #include unistd.h #define N 5 // 缓冲区大小 #define PRODUCE 10 // 生产总数 int buffer[N]; int in 0, out 0; sem_t mutex, empty, full; void* producer(void* arg) { for (int i 0; i PRODUCE; i) { sem_wait(empty); // 等待空槽位 sem_wait(mutex); // 进入临界区 buffer[in] i; printf(produce: %d at %d\n, i, in); in (in 1) % N; sem_post(mutex); // 离开临界区 sem_post(full); // 通知有数据 usleep(100000); } return NULL; } void* consumer(void* arg) { for (int i 0; i PRODUCE; i) { sem_wait(full); // 等待数据 sem_wait(mutex); int val buffer[out]; printf(consume: %d at %d\n, val, out); out (out 1) % N; sem_post(mutex); sem_post(empty); // 通知有空位 usleep(150000); } return NULL; } int main() { pthread_t p, c; sem_init(mutex, 0, 1); sem_init(empty, 0, N); sem_init(full, 0, 0); pthread_create(p, NULL, producer, NULL); pthread_create(c, NULL, consumer, NULL); pthread_join(p, NULL); pthread_join(c, NULL); sem_destroy(mutex); sem_destroy(empty); sem_destroy(full); return 0; }编译要加-lpthread。sem_wait(empty)必须在sem_wait(mutex)之前顺序反了会死锁生产者先拿到 mutex再等 empty而 empty 为 0 时它阻塞消费者又拿不到 mutex 无法消费双方互等。这是同步实验里最经典的坑没有之一。usleep用来放大交错效果不加的话生产者可能一口气跑完看不出并发行为。4.3 怎么验证没有死锁和竞态跑起来后观察输出生产序号和消费序号应该交替出现且缓冲区下标在 0 到 4 之间循环。如果程序卡住不动用gdbattach 上去看各线程栈哪个线程停在sem_wait就说明它等的信号量没被释放。另一种验证方法是把mutex初始值改成 2如果输出出现同一个槽位被覆盖说明互斥失效这能帮你确认 mutex 确实在起作用。实验报告里建议附上正常输出和故意去掉 mutex 后的异常输出对比说服力更强。5. 文件系统实验实现一个支持创建、读写、删除的简化文件系统5.1 为什么文件系统实验要用内存模拟磁盘真实磁盘操作涉及块设备驱动实验里通常用一个大的内存数组模拟磁盘把它划分成超级块、inode 区和数据块区。超级块记录文件系统元信息inode 记录每个文件的属性和数据块索引数据块存实际内容。这种设计让你聚焦在文件系统的逻辑结构上而不是底层 I/O。常见做法是定义一个disk[BLOCK_NUM][BLOCK_SIZE]二维数组BLOCK_SIZE取 512 或 1024 字节BLOCK_NUM取 1024 块总共 1MB 模拟磁盘。5.2 inode 与目录项的数据结构#define BLOCK_SIZE 512 #define BLOCK_NUM 1024 #define INODE_NUM 128 #define MAX_BLOCKS 12 // 每个文件最多 12 个直接块 typedef struct { int used; // 是否被占用 int size; // 文件大小字节 int blocks[MAX_BLOCKS];// 数据块号 int block_count; // 已用块数 } Inode; typedef struct { char name[32]; int inode_id; // 对应 inode 下标-1 表示空目录项 } DirEntry; char disk[BLOCK_NUM][BLOCK_SIZE]; Inode inodes[INODE_NUM]; DirEntry root_dir[16]; // 根目录固定 16 个目录项blocks数组存数据块编号block_count记录用了几个块。根目录用固定大小的目录项数组简化实现但足够演示查找逻辑。创建文件时遍历root_dir找空位分配一个空闲 inode写入文件名和 inode 编号。写数据时按块分配每块最多存BLOCK_SIZE字节超过MAX_BLOCKS个块就返回空间不足。5.3 创建、写入、读取、删除四个操作的实现要点创建文件找空目录项找空闲 inode初始化 inode 的used1、size0、block_count0把目录项的名字和 inode 编号填好。写入根据当前size算出需要几块从空闲块里分配把数据按块拷贝进disk更新size和block_count。读取按 inode 里的块号逐块从disk拷出拼成完整内容。删除把 inode 的used置 0目录项名字清空数据块标记为空闲。空闲块管理用一个位图block_bitmap[BLOCK_NUM]0 表示空闲1 表示占用。每次分配前扫描位图找 0分配后置 1。删除时把对应位清 0。这个位图就是文件系统里“块分配器”的简化版理解了它再去看 ext2 的块位图就不陌生了。6. 避坑与排查操作系统实验里最容易翻车的五个地方6.1 编译通过但运行时段错误现象gcc编译无警告一运行就Segmentation fault。原因最常见的是数组越界或空指针解引用比如 inode 编号超出INODE_NUM或者fork后子进程访问了未初始化的指针。解决用gcc -g编译然后gdb ./程序运行后bt看调用栈定位到具体行号。养成对每个数组访问加边界检查的习惯尤其是从文件读入的 pid 和块号。6.2 多线程程序结果每次不一样现象生产者消费者实验里有时输出正常有时某个数据被消费两次。原因共享变量没保护或者信号量顺序写错。解决确认所有对buffer、in、out的读写都在mutex保护范围内。用valgrind --toolhelgrind ./程序能检测出数据竞争它会指出哪两个线程在无锁情况下访问了同一地址。6.3 调度实验平均等待时间算出来是负数现象统计结果里等待时间出现负值。原因wait_time finish_time - arrive_time - burst_time如果finish_time小于arrive_time burst_time说明模拟时钟推进逻辑有误通常是 CPU 空闲时没有正确跳到下一个到达时间。解决在调度循环里加一句判断当前没有可运行进程时把current_time直接设为下一个未完成进程的arrive_time而不是继续自增。6.4 文件系统写入大文件后读取内容错乱现象写入超过一个块的数据读出来后半段是乱码。原因块分配时没有按顺序记录块号或者读取时块号顺序和写入时不一致。解决在 inode 的blocks数组里按分配顺序存块号读取时从blocks[0]到blocks[block_count-1]依次读。调试时打印每次分配的块号和写入偏移对照检查。6.5 虚拟机里实验跑一半卡死现象编译或运行大程序时虚拟机无响应。原因内存分配太小或者 fork 炸弹子进程没退出导致进程数爆炸。解决虚拟机内存至少给 4GB交换分区给 2GB。写fork相关代码时子进程分支末尾一定加return或exit。如果已经卡死在宿主机用任务管理器结束虚拟机进程重启后检查代码。7. 进阶技巧用 strace 和 /proc 看清实验背后的真实系统行为实验代码跑通只是第一步真正拉开差距的是你能不能看到操作系统在背后做了什么。我习惯用strace跟踪系统调用比如跑fork_demo时执行strace -f -e traceclone,wait4 ./fork_demo输出会显示每次clone的返回值和wait4的等待过程。-f表示跟踪子进程-e trace限定只显示指定调用避免输出刷屏。你会看到fork底层其实是clonewait底层是wait4这些对应关系在课程理论部分经常考。另一个利器是/proc文件系统。进程运行时/proc/[pid]/status里有VmRSS字段表示实际物理内存占用/proc/[pid]/sched里有调度统计。做调度实验时可以在程序里sleep几秒同时在另一个终端cat /proc/[pid]/sched观察nr_switches上下文切换次数和se.sum_exec_runtime累计运行时间。把这两个数字和你的模拟结果对照如果模拟里时间片轮转切换了 20 次而真实进程只切换了 3 次说明你的时间片设置和实际调度器行为有差距这个对比写进实验报告是加分项。文件系统实验也可以用strace -e traceopen,read,write,close跟踪真实文件操作对比你的模拟实现和内核实现的调用序列。你会发现内核在open之后不一定马上read而是先fstat获取文件大小这个细节能帮你理解为什么 inode 里要存size字段。最后说一个我自己的习惯每做完一个实验把关键系统调用的 man page 翻一遍。man 2 fork、man 2 sem_wait、man 2 open里的 ERRORS 段落列了所有可能的失败原因比任何教程都全。当年我调一个sem_init返回 -1 的问题翻了 man page 才发现是pshared参数传了 1 但系统不支持进程间共享改成 0 就好了。这种问题搜索引擎不一定能精准命中但 man page 永远在那。希望帮到你。本文还有配套的精品资源点击获取
返回列表