☰
操作系统实验报告实战:进程调度、作业调度与内存分配代码解析
2026/10/11 1:59:40 网站建设 项目流程

简介:这份广工操作系统实验报告完整Word版,面向计算机专业学生及操作系统课程学习者,帮助梳理进程调度、作业调度、可变式分区分配与简单文件系统四大实验的完整实现思路。报告以短进程优先、先来先服务、首次适应等经典算法为主线,配有PCB结构体定义、链表队列、sort排序函数等关键源码注释,并附程序运行结果与结果分析,便于对照理解调度流程与内存分配逻辑。资源包共1个doc文件,约570KB,内容按实验目的、内容要求、设计方案、数据结构说明、运行结果等模块组织,目录清晰,可直接用于课程实验参考与报告撰写。目前已有545人学习下载,适合需要完成操作系统实验、复习调度算法或整理实验报告的学习者参考使用。

1. 从一份老实验报告说起:进程调度、作业调度与内存分配到底怎么跑通

如果你手头正躺着一份操作系统实验报告,里面写着短进程优先、FCFS、HRN、首次适应、最佳适应这些词,但代码跑起来要么卡死、要么输出对不上,那这份资源就是冲着你来的。它是一份完整的 Word 版操作系统实验报告,覆盖进程调度、作业调度、可变式分区分配、简单文件系统四个实验,每个实验都带可编译的 C 语言源码和结果分析。适合两类人:一是正在做操作系统课程实验、需要一份能跑通的参考实现的同学;二是想回头补一补调度算法和内存分配底层逻辑的开发者。我拆这份报告时最大的感受是,它的代码不算优雅,但胜在结构完整、注释到位,链表操作和状态机逻辑都摆在明面上,改几个参数就能验证不同调度策略的差异。下面我按“先跑通、再改参数、最后避坑”的顺序,把这份资源里真正能落地的部分拆开讲。

2. 短进程优先调度:PCB 链表怎么建、sort 怎么插、调度序列怎么读

2.1 为什么用链表而不是数组

实验一的核心是短进程优先(SJF)调度。报告里用单链表管理进程控制块(PCB),每个节点包含进程名、状态、优先数、需要运行时间、运行时间。选链表而不是数组,原因很实际:进程数量在输入前不确定,链表可以动态malloc,插入时不需要预先分配固定大小。更关键的是,SJF 要求每次把新进程按ntime插入到有序位置,链表插入的指针操作比数组搬移更直观,也更容易在实验报告里画图说明。

PCB 结构体定义如下:

struct pcb { char name[10]; // 进程名 char state; // 状态:w 等待,r 运行,f 完成 int super; // 优先数(本实验未使用) int ntime; // 需要运行时间 int rtime; // 已运行时间 struct pcb* link;// 链表指针 } *ready = NULL, *p; typedef struct pcb PCB;

这里ready是就绪队列头指针,p是临时节点指针。getpch(type)宏封装了malloc,避免每次写一长串类型转换。

2.2 sort 函数的插入逻辑与边界

sort()是实验一最值得细看的部分。它把新进程按ntime升序插入链表,保证队首永远是最短进程。逻辑分三种情况:队列为空或新进程比队首还短,直接插队首;否则用first和second双指针遍历,找到第一个ntime比新进程大的节点,插到它前面;如果遍历完都没找到,说明新进程最长,插到队尾。

void sort() { PCB *first, *second; int insert = 0; if ((ready == NULL) || ((p->ntime) < (ready->ntime))) { p->link = ready; ready = p; } else { first = ready; second = first->link; while (second != NULL) { if ((p->ntime) < (second->ntime)) { p->link = second; first->link = p; second = NULL; insert = 1; } else { first = first->link; second = second->link; } } if (insert == 0) first->link = p; } }

参数说明:p->ntime是新进程需要运行时间,ready->ntime是队首进程需要运行时间。注意insert标志位,它区分了“插入到中间”和“追加到队尾”两种情况。如果漏掉这个标志,当新进程比所有现有进程都长时,first会停在最后一个节点,但first->link没有被赋值,新进程就丢了。这是链表插入最常见的翻车点。

2.3 输入、调度与输出怎么串起来

input()负责读入进程数,循环创建 PCB 节点,初始化rtime=0、state='w'、link=NULL,然后调用sort()插入就绪队列。main()里先调input(),再用getchar()吃掉输入缓冲区里的换行符,接着遍历ready链表打印调度序列。

void main() { int i, len, h = 0; char ch; input(); ch = getchar(); // 吃掉 scanf 留下的换行 printf("\n 调度序列为:"); p = ready; for (i = num; i > 0; i--) { printf(" %s", p->name); p = p->link; } printf("\n\n 进程已经完成.\n"); ch = getchar(); }

这里有个容易忽略的点:scanf("%d", &num)之后缓冲区里残留一个\n,如果不加getchar(),后面的ch = getchar()会直接读到这个换行符,程序看起来像“卡住”了。报告里用两个getchar()分别处理输入后和输出后的暂停,这是老式控制台程序的常见做法。

运行结果验证:输入 5 个进程,ntime分别为 3、1、4、2、5,调度序列应该是1、2、4、3、5对应的进程名。如果输出顺序不对,先检查sort()里的比较符号是不是写反了,再检查insert标志有没有在插入中间时置 1。

3. 作业调度双算法:FCFS 和 HRN 的代码差异与周转时间计算

3.1 FCFS 的排序依据从 ntime 换成 ctime

实验二的 FCFS 代码结构和实验一几乎一样,但sort()里的比较字段从ntime换成了ctime(提交时间)。这意味着队列按作业到达时间排序,先到先服务。PCB 结构体也扩展了:增加了ctime、stime、ftime、ttime、dtime五个字段,分别记录提交时间、开始时间、完成时间、周转时间和带权周转时间。

struct pcb { char name[10]; char state; int super; int ntime; // 需要运行时间 int rtime; // 运行时间 int ctime; // 提交时间 int stime; // 开始时间 int ftime; // 完成时间 int ttime; // 周转时间 float dtime; // 带权周转时间 struct pcb* link; } *ready = NULL, *p;

main()里的计算逻辑是重点:遍历链表时,如果当前作业的ctime大于前一个作业的ftime,说明 CPU 有空闲,stime取ctime;否则stime取前一个作业的ftime。然后ftime = stime + ntime,ttime = ftime - ctime,dtime = ttime / ntime。最后累加求平均。

if (second->ctime > first->ftime) second->stime = second->ctime; else second->stime = first->ftime; second->ftime = second->ntime + second->stime; second->ttime = second->ftime - second->ctime; x = second->ttime; y = second->ntime; second->dtime = (float)x / (float)y;

参数说明:first是前一个作业节点,second是当前作业节点。x和y是临时整型变量,用来做浮点除法前的类型转换。如果直接写second->ttime / second->ntime,两个 int 相除会截断小数部分,带权周转时间就错了。这是实验报告里反复强调的“整数除法陷阱”。

3.2 HRN 的响应比计算与动态选择

HRN(响应比高者优先)的代码在实验二后半段,结构和 FCFS 不同。它没有在输入时就排序,而是每次调度前遍历队列,计算所有等待作业的响应比,选最高的运行。响应比公式是(等待时间 + 要求服务时间) / 要求服务时间,代码里写成:

padv->super = (float)(times - padv->ctime + padv->ntime) / padv->ntime;

其中times是当前系统时间,padv->ctime是作业到达时间,times - padv->ctime就是等待时间。super()函数遍历ready队列,只对state=='W'且ctime <= times的作业计算响应比。hrn()函数里用min指针记录当前最高响应比的作业,iden标志区分“第一个候选”和“后续比较”。

void super() { JCB *padv; padv = ready; do { if (padv->state == 'W' && (padv->ctime) <= times) { padv->super = (float)(times - padv->ctime + padv->ntime) / padv->ntime; } padv = padv->next; } while (padv != NULL); }

running()函数负责把选中的作业从队列中摘除、计算时间、打印结果、释放内存。注意free(p)之后不能再访问p的字段,报告里在free之前完成了所有打印和累加,顺序是对的。

3.3 两种算法的输出对比与验证方法

FCFS 的输出是一张表格,列头是“进程名、开始时间、完成时间、周转时间、带权周转时间”,最后两行是平均周转时间和平均带权周转时间。HRN 的输出多了一步output()显示所有作业状态,再disp()显示当前运行作业的详情。

验证方法:手工算一组数据。假设三个作业 A(ctime=0, ntime=4)、B(ctime=2, ntime=3)、C(ctime=5, ntime=2)。FCFS 顺序是 A→B→C,A 的 ftime=4,B 的 stime=4、ftime=7,C 的 stime=7、ftime=9。HRN 在 times=0 时只有 A 可运行,A 跑完 times=4;此时 B 等待 2、C 等待 -1(未到达),B 响应比=(2+3)/3=1.67,选 B;B 跑完 times=7,C 响应比=(2+2)/2=2.0,选 C。如果程序输出和手工算的不一致,先检查times的更新时机,再检查super()里ctime <= times的条件有没有漏掉。

4. 可变式分区分配:首次适应与最佳适应的双向链表实现

4.1 空闲分区链的数据结构设计

实验三用双向链表管理空闲分区,每个节点是一个freearea结构,包含分区号、大小、地址、状态。头结点block_first和尾结点block_last不存实际数据,只做哨兵,简化插入和删除的边界处理。

typedef struct freearea { int ID; // 分区号 long size; // 分区大小 long address; // 分区地址 int state; // 状态:Free 或 Busy } ElemType; typedef struct DuLNode { ElemType data; struct DuLNode *prior; struct DuLNode *next; } DuLNode, *DuLinkList;

Initblock()初始化时,尾结点代表整个 640KB 内存,address=0、size=MAX_length、state=Free。头结点的next指向尾结点,尾结点的prior指向头结点。这种带头尾哨兵的双向链表,在插入和删除时不需要单独判断“是否在头部”或“是否在尾部”,代码更干净。

4.2 首次适应算法的查找与分裂

First_fit(int ID, int request)从block_first->next开始遍历,找到第一个state==Free且size >= request的节点。如果大小恰好相等,直接改状态和 ID;如果大于,则分裂出一个新节点,把剩余空间留在链表里。

Status First_fit(int ID, int request) { DuLinkList temp = (DuLinkList)malloc(sizeof(DuLNode)); temp->data.ID = ID; temp->data.size = request; temp->data.state = Busy; DuLNode *p = block_first->next; while (p) { if (p->data.state == Free && p->data.size == request) { p->data.state = Busy; p->data.ID = ID; return OK; } if (p->data.state == Free && p->data.size > request) { temp->prior = p->prior; temp->next = p; temp->prior->next = temp; p->prior = temp; p->data.size -= request; p->data.address += request; return OK; } p = p->next; } return ERROR; }

参数说明:ID是作业号,request是申请内存大小(KB)。分裂时,新节点temp插入到p前面,p的size减去request,address加上request,这样p仍然代表剩余空闲区,地址往后挪。注意temp->prior = p->prior和temp->prior->next = temp的顺序,先接前驱再接后继,否则指针会丢。

4.3 最佳适应算法的排序与分配

Best_fit的思路是遍历整个空闲链表,找到能满足request且size最小的节点。报告里的实现没有在每次分配后重新排序,而是遍历时用min指针记录当前最优节点,遍历完再分裂。

Status Best_fit(int ID, int request) { DuLNode *p = block_first->next; DuLNode *min = NULL; while (p) { if (p->data.state == Free && p->data.size >= request) { if (min == NULL || p->data.size < min->data.size) { min = p; } } p = p->next; } if (min == NULL) return ERROR; // 分裂逻辑与 First_fit 类似,对 min 节点操作 ... }

最佳适应的“最优”是局部最优:每次分配后剩余空间最小,但会留下大量难以利用的小碎片。报告里在结果分析部分明确写了这一点,这也是实验报告值得看的地方——它没有只贴代码,而是把算法的代价说清楚了。

4.4 回收时的四种合并情况

free(int ID)函数负责回收。遍历链表找到ID匹配的 Busy 节点,改状态为 Free,然后检查前驱和后继是否也是 Free,分四种情况合并:前后都空闲、只前空闲、只后空闲、都不空闲。合并时要注意地址和大小的更新,以及链表指针的摘除。

Status free(int ID) { DuLNode *p = block_first->next; while (p) { if (p->data.ID == ID && p->data.state == Busy) { p->data.state = Free; p->data.ID = 0; // 与前驱合并 if (p->prior != block_first && p->prior->data.state == Free) { p->prior->data.size += p->data.size; p->prior->next = p->next; p->next->prior = p->prior; p = p->prior; } // 与后继合并 if (p->next != block_last && p->next->data.state == Free) { p->data.size += p->next->data.size; p->next->next->prior = p; p->next = p->next->next; } return OK; } p = p->next; } return ERROR; }

注意p->prior != block_first和p->next != block_last这两个边界判断,哨兵节点不参与合并。如果漏掉,头结点或尾结点会被当成空闲区合并进去,链表结构就乱了。

5. 避坑与排查:这份实验报告代码最容易翻车的五个地方

5.1 现象:程序输入完进程数后直接跳过输入,开始打印空调度序列

原因:scanf("%d", &num)在缓冲区留下换行符,后续的scanf("%s", p->name)或getchar()读到了这个换行符。这是 C 语言控制台输入最经典的坑。

解决:在scanf读数字之后、读字符或字符串之前,加一句getchar()吃掉换行。报告里在input()之后和main()输出之后各加了一个getchar(),就是干这个的。如果还不行,用while (getchar() != '\n');清空整行。

5.2 现象:短进程优先的调度序列顺序不对,最长的进程排到了前面

原因:sort()里的比较符号写反了,或者insert标志没有在插入中间时置 1,导致新进程被追加到队尾而不是插入正确位置。

解决:检查if ((p->ntime) < (second->ntime))是不是写成了>。再检查insert初始化为 0,在插入中间的分支里置 1,循环结束后if (insert == 0) first->link = p;这一句有没有漏。漏掉的话,新进程比所有现有进程都长时,first停在最后一个节点,但first->link没被赋值,新进程就丢了。

5.3 现象:带权周转时间输出全是整数,小数部分被截断

原因:second->ttime / second->ntime两个 int 相除,结果自动截断为整数,再赋给 float 也补不回小数。

解决:在除法之前做强制类型转换,写成(float)second->ttime / (float)second->ntime。报告里用x和y两个 int 变量接住ttime和ntime,再(float)x / (float)y,效果一样。任何涉及“时间比”的计算都要检查这一点。

5.4 现象:内存回收后空闲分区链显示异常,出现大小为 0 的节点或地址重叠

原因:合并时没有正确更新地址和大小,或者漏掉了p->prior != block_first/p->next != block_last的边界判断,把头尾哨兵也合并了。

解决:合并前驱时,前驱的size加上当前节点的size,当前节点的next接到前驱的next,前驱的next的prior指回前驱。合并后继时,当前节点的size加上后继的size,当前节点的next指向后继的next,后继的next的prior指回当前节点。地址不需要手动改,因为前驱的地址本来就是低地址,合并后大小增加,地址不变。

5.5 现象:HRN 调度时某个作业的响应比一直是 0,或者选中的作业不是最高响应比

原因:super()里计算响应比的条件是padv->state == 'W' && padv->ctime <= times,如果times没有在每次运行后正确累加,或者ctime大于当前times的作业被误算,响应比就会出错。

解决:检查running()里times += p->ntime;这一句有没有漏。再检查hrn()里min指针的初始化和iden标志的逻辑:第一个满足条件的作业直接赋给min,后续作业只有super大于min->super时才替换。如果iden没有正确置 0,min会一直被第一个作业占着。

6. 简单文件系统的扩展思路与验证习惯

实验四的简单文件系统在报告里篇幅较短,但它的结构可以复用前三个实验的链表思路。文件控制块(FCB)可以用结构体数组或链表管理,每个 FCB 包含文件名、大小、起始块号、状态。创建文件时遍历 FCB 表找空闲项,写入时按块号分配,读取时按文件名查找 FCB 再定位数据块。报告里没有展开的细节,常见做法是用一个固定大小的字符数组模拟磁盘块,FCB 里记录起始块号和块数,删除文件时把 FCB 状态置为空闲并回收块号。

我一般会加一个验证步骤:创建三个文件,分别写入不同长度的内容,然后删除中间那个,再创建一个新文件,看新文件是否复用了被删除文件的块号。如果块号没有复用,说明回收逻辑漏了;如果新文件覆盖了未删除文件的数据,说明块号分配越界了。这个测试能同时验证分配、回收和边界检查。

另外,实验报告里的代码用的是scanf_s和conio.h,在非 Windows 环境或较新的编译器上可能报错。常见做法是把scanf_s换成scanf,把conio.h和clrscr()去掉或换成system("cls")。如果编译时报NULL未定义,检查有没有#include <stdio.h>或#include <stdlib.h>,报告里#define NULL0少了一个空格,应该是#define NULL 0,这个笔误会让编译器把NULL0当成一个未定义的标识符。

从那以后我每次拿到这类实验报告代码,都强制走一遍“编译→输入边界数据→手工验算→对比输出”的流程,尤其是链表插入和内存合并这两块,手工画一遍指针图再跑代码,能省掉大量调试时间。希望帮到你。

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

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询