news 2026/11/5 4:04:04

操作系统课程设计实战:进程调度、页面置换与文件系统排错

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
操作系统课程设计实战:进程调度、页面置换与文件系统排错

简介:面向计算机科学专业学生的东北大学操作系统课程设计资料,围绕进程管理、内存管理、文件系统与设备管理等核心主题,提供可运行、可对照的课程实践样例。压缩包共24个文件,约2.11MB,以21个C++源文件为主体,另有头文件、可执行程序及1份docx课程设计报告,便于从代码实现与书面总结两个层面理解课设要求。已有281人学习,说明其具有一定的参考与复用价值。源码重点呈现文件系统的完整实现链路,涵盖文件与目录的创建、打开、读写、复制、重命名和删除,以及数据块分配与回收、格式化、登录注销等模块,能够帮助读者把抽象的操作系统概念落到具体代码和数据结构上。配套报告可用于梳理设计思路、模块划分与实验结论,适合课程设计参考、期末复习、实验答辩准备及二次开发。

1. 答辩前夜跑三遍出三个结果的进程调度,问题出在哪儿

A 同学在答辩前一晚把操作系统课程设计跑了一遍,进程调度的输出顺序和白天对不上,同一份输入跑三次出来三种甘特图。翻车点不在算法,而在他用头插法维护就绪队列、又拿系统时间做了随机种子。这套计科方向的操作系统课设包,把进程调度、内存管理、文件系统、同步互斥四块内容塞进一个菜单驱动的程序里,make一条命令编译,运行后按数字选功能。它适合两类人:一类是刚学完操作系统原理、要把作业交上去的;另一类是想借课设摸清 PCB、页表、目录项这些数据结构到底怎么在内存里排布的。真正值得拆的不是那几个经典算法,而是这些结构体之间怎么互相引用、状态怎么流转。

2. 整体骨架:四模块菜单驱动的目录结构与编译链路

拿到一个课设包,我第一件事不是看算法,是看它怎么组织文件、怎么编译。因为算法写错了还能改,工程结构乱了就没法往上叠功能。这套课设的骨架很朴素:每个功能模块一对.c/.h,一个main.c负责菜单分发,一个公共头文件放全局常量,Makefile把所有目标文件链起来。朴素归朴素,对课程设计来说这个结构是最不容易出错的。

2.1 目录结构和每个文件的职责边界

os-course-design/ ├── Makefile ├── include/ │ ├── common.h # 全局常量、状态枚举、通用宏 │ ├── pcb.h # 进程控制块与队列操作声明 │ ├── memory.h # 页表、帧表、置换算法声明 │ ├── fs.h # 目录项、FAT、打开文件表声明 │ └── sync.h # 信号量、银行家算法声明 ├── src/ │ ├── main.c # 菜单循环,负责输入解析与模块调度 │ ├── pcb.c # PCB 创建/销毁、就绪队列/阻塞队列维护 │ ├── scheduler.c # FCFS、SJF、RR、优先级四种调度 │ ├── memory.c # 地址转换、FIFO/LRU/OPT 三种置换 │ ├── fs.c # 创建/删除/读写文件、目录遍历 │ └── sync.c # PV 操作模拟、银行家算法安全性检查 └── data/ ├── processes.txt # 进程初始配置 └── access_seq.txt # 页面访问序列

这个划分的关键在于include/和src/分离,common.h里只放常量和跨模块的类型。很多同学把PAGE_SIZE、MAX_PROC分散写在各自的.c里,改一个值要翻五个文件,最后内存模块按 1024 算、文件模块按 512 算,页内偏移全错。常见做法是这类全局量统一在common.h里用#define定义,并在注释里写清单位。

data/目录放输入用例这点容易被忽略。把进程配置和页面访问序列外置成文本,改用例不用重新编译,答辩时现场换一组数据演示也方便。读取时用fscanf按行解析,格式固定成"进程名 到达时间 服务时间 优先级"四列,中间用空格分隔。

2.2 公共数据结构和编译链路

common.h承担的另一件事是把所有枚举和常量集中,避免各模块对状态值理解不一致。比如进程状态,调度模块认为是 0/1/2,同步模块又自定义一套,最后switch永远进不了正确分支。集中定义之后,所有模块引用同一套符号。

编译链路用最基础的Makefile,把src/*.c编译成obj/*.o再链接,而不是直接gcc *.c -o main。区别在于增量编译:改一个文件只重编一个目标文件,几百行的项目感受不明显,等到文件系统模块写到两千行,全量编译的等待就很折磨人了。

CC = gcc CFLAGS = -std=c99 -Wall -Wextra -g -Iinclude SRCS = $(wildcard src/*.c) OBJS = $(patsubst src/%.c, obj/%.o, $(SRCS)) TARGET = osdemo all: $(TARGET) $(TARGET): $(OBJS) $(CC) $(CFLAGS) -o $@ $^ obj/%.o: src/%.c @mkdir -p obj $(CC) $(CFLAGS) -c -o $@ $< clean: rm -rf obj $(TARGET) .PHONY: all clean

这段的关键是-Iinclude把头文件目录加进搜索路径,源码里就能写#include "pcb.h"而不用写相对路径;-std=c99是为了让for (int i = 0; ...)这种写法合法,老版本的默认标准会报错;-Wall -Wextra建议一直开着,指针类型不匹配、未使用变量这类问题在课设里非常常见,早报出来早改。obj/%.o规则里那句mkdir -p obj前面加@是为了不把建目录的命令回显到终端,输出干净一些。-g保留调试符号,gdb ./osdemo之后能直接list到源码行。

2.3 菜单驱动的主循环与输入容错

main.c的结构是while (1)套switch,打印菜单、读一个整数、分发到对应模块,输入0退出。这种写法没什么技术含量,但输入容错是所有课设演示翻车的重灾区:用户输入一个字母,scanf("%d")读不到数字,缓冲区里留着脏字符,后续每次读都失败,程序进入死循环疯狂刷菜单。

稳妥的写法是用fgets读一整行进缓冲区,再用sscanf解析。读失败就清空提示重来,读成功但值超范围也提示重来。多写十行代码,答辩现场能少一次尴尬。

/* 读取一个菜单选项,非法输入返回 -1 由调用方处理 */ int read_choice(void) { char line[32]; int val; if (fgets(line, sizeof(line), stdin) == NULL) /* EOF 视为退出 */ return 0; if (sscanf(line, "%d", &val) != 1) { /* 解析失败 */ printf("输入不是数字,请重新选择:"); return -1; } return val; /* 由调用方判断范围 */ }

fgets最多读sizeof(line) - 1个字符,保证不会越界;sscanf的返回值是成功匹配的项数,等于 1 才算拿到了整数。注意fgets会把换行符也读进来,这对%d没影响,但如果后面要读字符串,记得把末尾的\n去掉,否则文件名会多一个换行符,fopen直接返回NULL——这个坑我在文件系统那章还会提一次。

3. 进程管理拆解:PCB 状态机、时间片轮转与银行家算法

进程管理是这类课设里代码量最大、也最能体现设计水平的一块。很多人写出来的是一个for循环遍历数组算周转时间,那不叫模拟,叫计算器。真正的模拟得有 PCB、有队列、有状态迁移,还得能随时打印当前系统快照。

3.1 PCB 结构体与进程状态机

PCB 是所有进程相关操作的锚点,字段设计得好不好直接决定后面写不写得动。核心字段分三类:标识类(pid、name)、调度类(到达时间、服务时间、剩余时间、优先级)、统计类(开始时间、完成时间、等待时间)。再加一个next指针,把 PCB 串成链表,队列操作就不用挪数据。

typedef enum { ST_NEW = 0, ST_READY, ST_RUNNING, ST_BLOCKED, ST_TERMINATED } proc_state_t; typedef struct pcb { int pid; /* 进程唯一标识,从 1 递增 */ char name[16]; /* 显示名,最长 15 字符 */ proc_state_t state; /* 当前状态 */ int priority; /* 数值越小优先级越高 */ int arrive_time; /* 到达时刻 */ int service_time; /* 需要 CPU 的总时长 */ int remain_time; /* 剩余时长,RR 每次减 quantum */ int start_time; /* 首次获得 CPU 的时刻 */ int finish_time; /* 完成时刻,用于算周转时间 */ struct pcb *next; /* 就绪/阻塞队列的后继指针 */ } pcb_t;

remain_time和service_time分开存,是为了让同一份 PCB 既能算总周转时间,又能在时间片轮转里判断"这一轮跑完之后还有没有剩"。start_time只在第一次被调度时赋值,用-1初始化,判一下就能知道是不是首次运行。next指针让 PCB 同时可以挂在就绪队列、阻塞队列上,注意不要在多个队列间共享同一个节点而忘了清指针,否则打印队列时会顺着野指针跑到天上去,段错误的位置跟真正的错误点能差好几百行。

状态迁移遵循标准五态模型:创建后进就绪队列;调度器选中变成运行态;时间片用完回到就绪;请求 I/O 或等待信号量进阻塞;阻塞事件完成回就绪;运行结束转终止。代码里每次改state都应该紧跟一次队列的摘除和插入,两者写在一起,不要分开放在不同函数里,不然迟早出现"状态是 READY 但人不在就绪队列"的幽灵进程。

3.2 时间片轮转调度的实现

RR 的核心是"剩余时间减时间片,没跑完就回队尾"。用链表实现的话,取队头、算一把、插队尾,三步搞定。这里最容易翻车的是时间片参数的选取和边界处理:时间片大于所有进程的剩余时间时,RR 会退化成 FCFS;时间片取 1 时上下文切换开销太大,模拟里表现为打印的片段极多。

/* 时间片轮转:ready 为就绪队列头指针,quantum 为时间片 */ void rr_schedule(pcb_t *ready, int quantum, int *clock) { pcb_t *cur; int slice; while (ready != NULL) { cur = ready; /* 取队头 */ ready = ready->next; /* 出队 */ if (cur->start_time < 0) cur->start_time = *clock; /* 首次运行记录开始时间 */ slice = (cur->remain_time < quantum) ? cur->remain_time : quantum; /* 最后一次可能不足一片 */ cur->state = ST_RUNNING; printf("[%3d -> %3d] %s 运行 %d 个单位\n", *clock, *clock + slice, cur->name, slice); *clock += slice; cur->remain_time -= slice; if (cur->remain_time == 0) { /* 跑完了,转终止 */ cur->state = ST_TERMINATED; cur->finish_time = *clock; } else { /* 没跑完,回队尾 */ cur->state = ST_READY; enqueue(&ready, cur); } } }

quantum从命令行参数或菜单里传进来,常用值 1、2、4,答辩演示建议用 2,输出长度适中。clock用指针传是因为它要在整个调度过程中累加,每个进程结束时都得读到最新的全局时刻。slice单独算一步是为了处理最后一个时间片不足的情况——如果直接减quantum,remain_time会变成负数,后续统计等待时间时算出离谱的值。enqueue是尾插函数,把节点追加到链表末尾;如果写成头插,队列顺序就反了,同一组输入每次跑出来的甘特图都不一样,这就是开头那个场景的成因。

周转时间、带权周转时间、平均等待时间这些指标,统一在调度结束后遍历所有 PCB 算一遍,不要在调度循环里边跑边打印。分开之后逻辑清晰,也方便把结果重定向到文件里做对比。

3.3 同步互斥与银行家算法

同步这块的课设要求通常是两个:用 PV 操作模拟生产者—消费者,用银行家算法避免死锁。前者考察信号量的wait/signal语义,后者考察安全性检查的搜索过程。

银行家算法的核心是"试分配—检查安全性—回滚或确认"。安全性检查的逻辑是:拿当前可用资源available,找一个need <= available的进程,假设它跑完并归还资源,更新available,标记完成,循环往复。能全部标记完就是安全状态,否则不安全。

/* 安全性检查:n 进程数,m 资源种类数 need[i][j] = max[i][j] - alloc[i][j] safe_seq 输出安全序列,返回 1 表示安全 */ int is_safe(int n, int m, int avail[], int need[][8], int alloc[][8], int safe_seq[]) { int work[8], finish[8] = {0}; int count = 0, found, i, j; for (j = 0; j < m; j++) work[j] = avail[j]; /* work 是当前可用量副本 */ while (count < n) { found = 0; for (i = 0; i < n; i++) { if (finish[i]) continue; for (j = 0; j < m; j++) if (need[i][j] > work[j]) break; /* 资源不够,换下一个 */ if (j == m) { /* 这个进程能跑完 */ for (j = 0; j < m; j++) work[j] += alloc[i][j]; /* 归还它占用的资源 */ safe_seq[count++] = i; finish[i] = 1; found = 1; } } if (!found) return 0; /* 一轮下来没人能跑,不安全 */ } return 1; }

work数组必须是avail的副本,直接改avail会让后续的请求判断全部错乱。finish数组初始化为全 0,每轮扫描都从头开始找未完成的进程,找到一个就跳出内层j循环并更新资源。found标记这一轮有没有进展,没有就说明陷入循环等待,判定不安全。这段代码的坑在于"只找一条安全序列就返回"——有些实现找到第一个可行进程就递归下去,回溯写错会导致明明安全的状态被判成不安全,正确的做法是用上面这种迭代扫描,每轮把所有能跑的进程都标记掉,正确性容易保证。

生产者—消费者的模拟则要注意empty、full、mutex三个信号量的初始值:empty = 缓冲区大小,full = 0,mutex = 1。PV 的顺序不能反,生产者必须先wait(empty)再wait(mutex),如果反过来,缓冲区满时生产者占着互斥锁等空位,消费者进不来,直接死锁——这个坑几乎每届都有人踩。

4. 内存与文件系统:页面置换、地址转换和 FAT 目录怎么落地

内存和文件系统这两块在课设里经常被写成"算几个数打印出来",代码量和进程调度差一大截。原因多半是没把"模拟"和"计算"分清楚。模拟要有真实的页表、帧表、目录项结构,操作的是这些结构,而不是直接用公式套结果。

4.1 页表、帧表与地址转换

分页式内存管理需要两张表:页表(逻辑页号到物理帧号的映射)和帧表(物理帧的占用情况与访问信息)。页表项里的有效位决定这次访问是不是缺页,访问位和修改位则给置换算法提供依据。

#define PAGE_SIZE 1024 /* 页大小,单位字节 */ #define PAGE_NUM 64 /* 逻辑页数 */ #define FRAME_NUM 16 /* 物理帧数 */ typedef struct { int frame_no; /* 映射到的物理帧号,-1 表示未装入 */ int valid; /* 有效位:1 在内存,0 缺页 */ int access_time; /* 最近一次访问时刻,LRU 用 */ int dirty; /* 修改位 */ } pte_t; typedef struct { int occupied; /* 是否被占用 */ int page_no; /* 该帧当前装的是哪个逻辑页 */ int pid; /* 所属进程 */ } frame_t;

frame_no用-1表示未装入,比用 0 更安全,因为 0 是合法的帧号。地址转换的流程是:逻辑地址除以PAGE_SIZE得页号,取模得页内偏移;查页表,有效则拼出物理地址,无效则触发缺页处理,调用置换算法选一个帧淘汰,更新两张表。

这套结构在单进程场景下看不出价值,一旦扩展成多进程,frame_t里的pid字段就派上用场了——淘汰时要判断这帧属于谁,进程结束时要把自己的帧全部释放。很多实现偷懒不写pid,最后没法做进程退出时的资源回收,内存泄漏的表现是跑几十次之后帧全被占满,新进程一律缺页。

4.2 FIFO、LRU 与 OPT 三种置换算法

三种算法的区别只在"选谁淘汰"这一步,其他流程共用。把它们写成同一个框架下的三个策略函数,比复制三遍代码干净得多。

/* 返回被淘汰的帧号;policy: 0=FIFO 1=LRU 2=OPT */ int pick_victim(pte_t page[], frame_t frame[], int t, int policy, int access_seq[], int seq_len) { int victim = 0, i, best; switch (policy) { case 0: /* FIFO:先装入的先淘汰 */ for (i = 1; i < FRAME_NUM; i++) if (frame[i].page_no < frame[victim].page_no) victim = i; break; case 1: /* LRU:最久未访问的淘汰 */ best = page[frame[0].page_no].access_time; for (i = 1; i < FRAME_NUM; i++) { int at = page[frame[i].page_no].access_time; if (at < best) { best = at; victim = i; } } break; case 2: /* OPT:往后看,最晚被访问的淘汰 */ best = -1; for (i = 0; i < FRAME_NUM; i++) { int next = seq_len; /* 默认之后不再被访问 */ int j; for (j = t + 1; j < seq_len; j++) if (access_seq[j] == frame[i].page_no) { next = j; break; } if (next > best) { best = next; victim = i; } } break; } return victim; }

FIFO 这里用page_no近似装入顺序,更严谨的做法是在frame_t里加一个load_time字段,避免页号大小和装入顺序无关导致的错误。LRU 依赖每次访问时更新access_time,漏更新一次后面全错。OPT 需要完整访问序列做前瞻,next初始化为seq_len表示"以后不再访问",这是它被淘汰的最高优先级;内层循环找第一个匹配的位置就break,因为只需要最近的将来。

三种算法的缺页率对比表可以直接用这套代码跑出来:

算法特点缺页率表现实现难度
FIFO按装入顺序淘汰,实现最简单中等,可能出现 Belady 异常低
LRU按最近访问时间淘汰,局部性好通常最低中,需维护访问时间
OPT淘汰未来最晚访问的页,理论最优最低,但不可实现高,需完整访问序列

Belady 异常指 FIFO 在帧数增加时缺页反而变多,这个现象用课设里的访问序列跑几组不同FRAME_NUM就能复现,答辩时是个不错的加分点。

4.3 文件系统的目录项与 FAT 表

文件系统模拟通常要求实现创建、删除、读写、列目录。最省事的模型是 FAT:目录项记录文件名、起始块、大小,FAT 表记录每个块的下一块指针,用-1表示链尾,用-2表示空闲块。

#define BLOCK_SIZE 512 #define MAX_BLOCKS 1024 #define NAME_LEN 12 typedef struct { char name[NAME_LEN]; /* 文件名,最长 11 字符 + 结束符 */ int start_block; /* 起始块号,-1 表示空目录项 */ int size; /* 文件大小,单位字节 */ int attr; /* 0 普通文件,1 目录 */ } dir_entry_t; int fat[MAX_BLOCKS]; /* -2 空闲,-1 链尾,其他值为下一块号 */ dir_entry_t dir_table[64];

写入流程是:从 FAT 里找空闲块,串成链,把数据按BLOCK_SIZE切分写入对应块,最后更新目录项的start_block和size。删除时沿链遍历,把每块标记回-2。读取时顺着 FAT 走到-1为止,每块读BLOCK_SIZE字节。

这里有个隐藏的崩溃点:start_block和size必须同步更新。如果先改了size而块链没串好,读文件时会读到-2标记的空闲块,程序要么读出一堆垃圾,要么在 FAT 表里绕圈死循环。稳妥的做法是先把整条链建好,再一次性更新目录项,中间任何一步失败就回滚已分配的块。

NAME_LEN设 12 是为了装下 8.3 格式的文件名加结束符。如果只给 8,文件名超过 7 个字符就会截断,strcmp比较时永远找不到刚创建的文件,现象是"创建成功但列目录里没有"。这个坑用strncpy的时候还会再犯一次——strncpy不保证补\0,目标缓冲区必须先memset清零。

5. 跑不通先看这里:五个高频翻车点排查手册

课设代码跑不起来,八成不是算法不会,是环境、指针、缓冲区这些工程细节没处理好。下面五条是我自己踩过也在别人的代码里见过无数次的,按"现象→原因→解决"写,遇到问题照着对。

5.1 编译过了链接报 undefined reference

现象:每个.c单独gcc -c都能过,一链接就报undefined reference to 'enqueue',而且报错的那个函数明明写了。

原因有两种。一是Makefile的SRCS用手写列表,新增了scheduler.c忘了加进去,目标文件根本没生成。二是头文件里声明了函数但忘了写#define保护宏,pcb.h被两个.c同时包含时,声明重复倒不致命,但如果头文件里定义了变量(比如int clock;),就会变成多个定义的符号冲突。

解决:SRCS改成$(wildcard src/*.c)自动收集;所有头文件加#ifndef PCB_H / #define PCB_H / #endif三段式保护;全局变量在头文件里用extern int clock;声明,在某个.c里定义一次。改完make clean && make重新走一遍。

5.2 同一组输入每次跑出来结果都不一样

现象:进程调度的执行顺序、页面置换的缺页次数,重复执行三次出现三种结果。

原因:就绪队列用了头插法,入队顺序和到达顺序相反,而进程的到达时间又是按数组顺序隐式假设的;或者代码里调用了srand(time(NULL))生成随机到达时间,但打印的顺序依赖链表遍历顺序,链表顺序本身又受插入顺序影响。还有一种隐蔽情况是遍历链表时在循环体内改动了next指针,导致部分节点被跳过。

解决:所有队列统一用尾插,写一个enqueue函数,所有入队走它,不要在业务代码里手写node->next = head。随机数据改成从data/processes.txt读,保证可复现。遍历链表时用两个指针,一个cur走,一个prev记,删节点只动prev->next,cur在循环末尾统一后移。

5.3 进程结束后段错误,gdb 显示在 free 之后

现象:调度跑到最后一个进程,打印完统计信息就 Segmentation fault,gdb定位在free(pcb)那一行或者之后几行。

原因:进程终止时只从就绪队列摘了节点,但阻塞队列、等待队列里还挂着同一个 PCB 指针。free之后这些队列成了悬空指针,后续任何一次遍历都会访问已释放内存。多队列共享节点是链表实现的经典难题。

解决:写一个remove_from_all_queues(pcb_t *p),在free之前把所有可能挂载的队列都扫一遍,找到就摘。更彻底的做法是 PCB 不直接free,统一放到一个"已终止"数组里,程序结束时一次性释放;模拟程序内存占用不大,这种延迟释放完全可接受,还能保留统计信息。

5.4 缺页率算出来大于 1

现象:内存模块输出"缺页率 1.35",明显不合理。

原因:分子分母搞混了。缺页率是"缺页次数 / 总访问次数",有人把总访问次数写成页面数(去重后的),或者把缺页次数里把"置换次数"和"缺页次数"重复计数。更常见的是访问序列读取时多读了一行,while (!feof(fp))配合fscanf会导致最后一行被读两次,访问次数虚高,缺页次数也跟着虚高。

解决:feof的正确用法是在每次读取之后判断,而不是用它做循环条件。改成while (fscanf(fp, "%d", &x) == 1),返回值为 1 才说明读到了有效数据。计数变量在初始化时清零,每次访问只自增一次,缺页分支里再单独自增缺页计数,两个变量职责分开。

5.5 Windows 下conio.h相关功能编译不过

现象:在 Linux 或者较新的编译器上编译,报conio.h: No such file or directory,或者getch、kbhit未定义。

原因:conio.h是某些老式编译环境特有的控制台头文件,标准 C 里没有,跨平台编译必然失败。课设代码里用它多半是为了"按任意键继续"。

解决:把"按任意键继续"改成读一行输入,用fgets或者getchar()配一个循环清空缓冲区。如果一定要做无回显的单键读取,Linux 下需要改终端属性(termios),代码量大概二十行,比引一个非标准头文件划算。更简单的办法是直接删掉暂停功能,菜单循环本来就靠用户输入驱动,不需要额外的等待。

6. 二次开发:把课设改成能演示的小系统

课设交完就扔,是最可惜的。这套代码的结构已经具备扩展基础,稍微改改就能变成一个能截图、能讲故事的小项目。我一般从三个方向动手,按投入产出比排序。

第一个方向是把输出结构化。现在所有模块都是printf打表格,把同样的信息改成输出 JSON 行,前端或者 Python 脚本读进去画甘特图、画内存帧的热力分布,演示效果立刻不一样。改造成本极低,只需要在打印函数旁边加一个print_json分支,用宏或者全局标志切换输出格式。进程调度的每一条[start, end]记录就是甘特图的一根柱子,页面置换的每次命中/缺页就是一行色块,数据都是现成的。

第二个方向是补测试。课设代码几乎没有测试,改一行就不知道会不会崩。给每个模块写一个极简的assert用例:银行家算法给一组已知安全、一组已知不安全的数据,断言返回值;页面置换用固定访问序列断言缺页次数;FIFO 队列做入队出队再入队,断言顺序。这些用例放进tests/目录,Makefile加一个test目标,改完代码跑一遍,比肉眼检查靠谱得多。

第三个方向是把输入彻底外置。现在进程配置和访问序列已经是文件了,但调度算法、时间片、页大小这些还写死在代码里。加一个config.ini或者命令行参数解析,让./osdemo -a rr -q 2这种形式直接跑指定算法。这样同一份程序可以在演示时切换不同算法做对比,不用反复改代码重新编译。

改造点改动位置工作量展示价值
JSON 输出scheduler.c、memory.c打印函数半天高,可配图表
单元测试新增tests/,Makefile加目标一天中,提升可信度
命令行参数main.c入口解析半天中,演示更灵活
多进程内存memory.c加 pid 隔离两天高,贴近真实系统

这些改造做完,代码量大概多三成,但项目性质从"作业"变成了"能演示的系统"。面试时讲清楚 PCB 怎么在多个队列间流转、页表项的有效位和访问位分别解决什么问题、FAT 链断了怎么回滚,比背概念有说服力得多。

那次答辩前一晚的三种输出,最后查出来就是头插法加随机种子两处,改完只有十行。从那以后我每次写队列相关的课设代码,第一件事就是先把enqueue写成尾插并单独测一遍,再往下写业务逻辑——这个习惯帮我省掉的调试时间,比任何算法技巧都多。希望帮到你。

本文还有配套的精品资源,点击获取

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/11/5 4:03:38

指针仪表检测:XML标注解析、格式转换与YOLO训练配置

简介&#xff1a;面向目标检测任务的指针仪表图像数据集&#xff0c;适合机器学习研究者与开发者训练和评估仪表识别、定位及读数提取模型。压缩包共785个文件&#xff0c;以783张jpg实拍图像为主体&#xff0c;另含2个json标注/配置文件&#xff0c;整体约950.3MB&#xff0c;…

作者头像 李华
网站建设 2026/11/5 4:03:23

DeepSeek提示词生产力:分阶段生成与人工校验

简介&#xff1a;清华大学新媒体研究中心发布的《普通人如何抓住DeepSeek红利》PDF&#xff0c;面向职场人、学生及AI应用初学者&#xff0c;围绕DeepSeek的功能边界、提示词驱动与提问鉴别能力展开&#xff0c;帮助读者在工作写作、学习提效、生活减负和社交沟通中形成可操作的…

作者头像 李华
网站建设 2026/11/5 4:02:33

维普AI降重实测:2026年11月维普AIGC检测96.46%降到0%,免费降AI 1000字

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/11/5 4:01:28

链路聚合的负载分担算法:哈希因子怎么选

一、链路聚合负载分担概述 链路聚合&#xff08;Link Aggregation&#xff09;是一种网络技术&#xff0c;通过将多个物理链路组合成一个逻辑链路&#xff0c;提高带宽和可靠性。负载分担&#xff08;Load Balancing&#xff09;是链路聚合中的一个重要功能&#xff0c;通过哈希…

作者头像 李华
网站建设 2026/11/5 4:01:12

会自我轮转的 Agent:从“命中率 99.9% 还是很贵“到实测省 76%

会自我轮转的 Agent&#xff1a;从"命中率 99.9% 还是很贵"到实测省 76%我在 DeepSeek Harness&#xff08;DSH&#xff09;上做了一个上下文预算插件&#xff0c;用 1181 步真实数据校准了成本模型&#xff0c;并把长会话的每步成本压到 1/3.3。 这篇文章把四组实验…

作者头像 李华
网站建设 2026/11/5 3:59:46

Pydantic 对接大模型:强制结构化输出,实现工业数据采集零解析错误

上个月接了个行业竞品数据的采集需求&#xff0c;前同事留下的近四百行XPath与正则表达式&#xff0c;赶上对方官网一次前端重构&#xff0c;直接全线报错。通宵改了半宿解析逻辑&#xff0c;没过一周对方又换了个页面布局&#xff0c;人直接麻了。 做过网页数据提取的开发者应…

作者头像 李华