注:
本文是四个调度算法的第一篇算法。
本文是根据CSDN上某一FCFS调度算法魔改来的,所以FCFS的算法不会发到网站。
我是个菜鸡,发文是为了纪念自己完成了代码,以及累计自己的经验。
如有知识错误或者算法有逻辑漏洞请各位大佬高抬贵手。
实验环境:win10、VS2019(C++)
PS:虽然是cpp文件,实际上是披着cpp的c
运行前,首先要在预处理器上插入_CRT_SECURE_NO_WARNINGS,避免运行错误,在插入前要用;隔开。
具体位置就在解决方案下面一栏(名字是自己起的),右键属性即可找到。
像这个样子
代码部分核心思路有三段:
一、将输入的链表按提交时间从小到大码好
这一部分的核心思路是为了保证后面某一单元节点的提交时间远大于之前所有运行时间和最早提交时间的总和的情况下,即使其执行时间最短,其执行顺序也会放入到它之前的代码段后。
这段思路和下一个核心思路是相连的。
上代码:
void linkpcb()//先将链表尾插法连接起来 //如果行不通就执行方案2 即将这些先按到达顺序排序后,通过dolevel进行分层,在用sort进行最短排序
{
int insert = 0;
if ((ready == NULL) || ((p->tr) <= (ready->tr) && p->tn < ready->tn )) /*提交兼执行时间最小者,插入队首*/
{
p->link = ready;
ready = p;
}
else /* 进程比较提交时间,插入适当的位置中*/
{
first = ready;
second = first->link;
while (second != NULL)
{
if ((p->tr) <= (second->tr)) /*若插入作业比当前作业提交时间小,*/
{
if (p->tr == second->tr)
{
while (second != NULL)//比较执行时间
{
if (p->tn <= second->tn)
{
p->link = second;//p插入到second前面
first->link = p;
second = NULL;//second只能在连接好的链表上运行
insert = 1;
}
else
{
first = first->link;
second = second->link;
}
}
}
else
{
/*插入到当前进程前面*/
p->link = second;//p插入到second前面
first->link = p;
second = NULL;//second只能在连接好的链表上运行
insert = 1;
}
}
else /* 插入进程提交时间最高,则插入到队尾*/
{
first = first->link;
second = second->link;
}
}
if (insert == 0) first->link = p;//循环到最后若依旧没有比second大的,则first此时是p的前一个
}
}
(这段代码就是CSDN上某一FCSF的唯一核心代码段,且几乎没有修改)
在if ((ready == NULL) || ((p->tr) <= (ready->tr) && p->tn < ready->tn ))一行中,ready == NULL是用来判断链表是否为空的情况下建立表头,而(p->tr) <= (ready->tr) && p->tn < ready->tn 一行则是有两个判断:
1.新的单元节点p它的提交时间是否小于等于新的表头?
这里的思路是和FCSF一脉相承的思路,比较提交时间,成功则单元节点p成为新表头,在原先的FCSF中是没有等于的判断条件,而这里添加 = 的原因是为了能在提交时间相同的情况下,比较执行时间的大小,也就是接下来要说的第二点。
2.新的单元节点它的执行时间是否小于表头单元节点的执行时间?
这个是在SJF或是HRN中单独增添的比较条件,是专门用来比较执行时间的,若p的执行时间小于表头,则成为新新表头,如果大于等于,判断条件不符合,就会和那些大于表头提交时间的单元节点一起放到下面的代码段中排序。
综上,可以看出这行代码段的具体功能:
1.此行代码段仅判断能不能成为表头的条件(注:表头!表头!表头!)
2.筛选条件包括提交时间小的,且执行时间小的
3.形成的链表的头单元节点永远是执行时间最小且提交时间最早的(这也是我后来才想到的)
说完表头的问题就可以说明下面else的内容了,在原先的FCSF代码段中设立的first和second指针变量的功能是作为标记位使用的。
简单来说:first标记一个单元节点,second标记下一个单元节点,如果p符合条件就插入到这两个节点中间,即 first->link = p;(上一个单元节点的下一个地址指向p)和 p->link = second;(p的下一个地址指向second所在的单元节点)。
这两步就完成了链表的插入连接。
如果不符合条件first和second就会逐一向下一个单元节点走,即 first = first->link;second = second->link; 其实first的下一个单元节点是second,所以理论上在不符合条件的情况下 first = second;是成立的,但这么做要注意first和second代码行的顺序问题,否则就会变成second先到了下一个单元节点,first再被second赋值,两个指针指向一个单元节点。
那么说了半天要符合判断条件,判断条件又是什么呢?
这一行if ((p->tr) <= (second->tr))的作用就体现出来了:判断新的单元节点p的提交时间是否小于等于second(即下一个单元节点)的提交时间。
此时头结点由于已经判断了提交时间和执行时间的最小时间,所以头结点以后所有的单元节点都只会比头结点大,这样first就可以安心从头结点开始,而不用担心单元节点p比first的提交时间或执行时间要小的问题了,同样的second从头结点的下一个节点开始,介于此时first指向的也是头结点,所以second = first->link;这行代码是没问题的。
之前表头中讲过 = 的判断条件是SJF、HRN中单独添加的判断条件,目的就是在提交时间相同时筛选出执行时小的,而提交时间小、执行时间大的和提交时间大的一起放在了后面,同样是这里if ((p->tr) <= (second->tr))的内容了。
同样的 = 也是加的,原版判断提交时间失败后就直接转到else中,原理上述几段已经讲过,就不再赘述。
这个判断内容相比于原版,多了一个 if (p->tr == second->tr),一个else,和一个循环。
即: p->link = second;
first->link = p;
second = NULL;
insert = 1;
这四行才是FCSF原本的if内容。
这个if (p->tr == second->tr)就是在提交时间相等情况下,比较执行时间的一个门槛。当然里面的内容和前面都是大同小异的,所以代码也就复制粘贴过来,稍微改个条件就结束了。
最后讲讲这个代码段唯一没讲到的东西:while循环
从大体看,这个循环就是促使first和second一直往下一个节点走的机制,当second = NULL;时,即second已经不在链表当中(最后一个单元节点的指针域指向NULL,second->link = NULL;second = second->link;的时候就说明second已经出离链表了),这时候循环条件不符,结束循环。
而作为second的上一个单元节点指针first则成为了最后一个单元节点,这种情况下,insert = 1标记循环结束也没找到符合单元节点p的情况,根据标记进行判断,让p插入到最后一个单元节点first后面。也就是这行代码if (insert == 0) first->link = p;
从循环作用看,依旧是这个嵌套循环,外循环负责判断提交时间的大小,内循环则负责提交时间相同情况下,执行时间的大小。而里循环的 if (p->tn <= second->tn)判断中带 = 的原因是将执行时间相等的也放到second前面,避免提交、执行时间都相等的单元节点会碰到执行时间大但执行时间未知(或大或小或相等已经此时没有意义)的情况发生。
二、计算提交时间和当前总时间的关系
计算第一个提交的提交时间和执行时间的总值sum,因为第一个提交,所以第一个执行,所有提交时间小于sum的单元节点都是可以在第一个单元节点执行后能继续运行的,而不是前一个以及执行结束,而后面还没提交的情况出现。
代码:
void dolevel()
{
if(second->tr <= sum)
{
sum = sum + second->tn;
}
else
{
sum = second->tr + second->tn;
second->flag = 0;
}
second = second->link;
}
在结构体中立一个小旗子flag = 1;
如果出现断档的情况,直接砍倒小旗子 flag = 0。
sum的值由新的提交时间+执行时间重新计算。而sum也可以看作是等待时间。
这样就会出现断档的首节点,每一档的层次就出现了。
具体思路如下:
在经过上一个核心思路后,单元节点的排序是这样的:
提交时间:小->大
提交时间内执行时间:小->大
从提交时间上看,已经在链表中分出了层次,而我们要做的就是将这些层次区分出来,最简单的办法就是将每个层次的开头做标记,flag = 0;的情况就出现了。
但我们并不是要根据提交时间来输出,否则也不用这么麻烦。分层的目的是防止有提交时间超过之前所有单元节点执行时间累计之和,但执行时间极小的进程过来插队捣乱的情况发生。
所以砍倒小旗子的地方,即分层断点就放在负责判断这种情况if(second->tr <= sum)的else中。
三、将链表再次排序输出,排序方法按最短执行时间大小决定
先上代码:
void sort() /* 建立对作业进行提交时间排列函数,分出第一层*/
{
first = ready;//first归位
p = ready;
last = ready;
if (last->flag == 0)//置1
{
last->flag = 1;
}
while ((last->flag == 1 && last->link != NULL))//一层中找到最小的 最后一个会被强制退出
{
if (last->tn < p->tn)
{
p = last;
}
last = last->link;
if (last->link == NULL)//在最后一个被踢出前先进行比较
{
if (last->tn < p->tn)
{
p = last;
}
}
}
last = p->link;
if (ready == p)//防止最小可能是头单元节点造成ready成空指针
{
ready = ready->link;
}
first->link = last;
p->link = NULL;
check();
disp(p);
eti += p->ti;
ewi += p->wi;
running();
}
这段代码的思路是在同一个档次内不断循环,查找执行时间最小的节点p,输出并销毁当前节点,直至此档内全部输出完毕。也就是while ((last->flag == 1 && last->link != NULL);这个循环的功能。
last->flag == 1判断层级
last->link != NULL判断是否运行结束
if (last->tn < p->tn)找出执行时间最小的节点,p负责记录当前执行时间最小的节点,而last负责遍历当前层级的所有节点,即:last = last->link;
代码行if (last->link == NULL)中判断last是否是倒数第二个单元节点,如果是则跳转到最后一个单元节点。
因为判断是while,所以当last是最后一个单元节点时,如果是last->link = NULL;last在最后一个节点会指向NULL的所在地址,这个地址位置,且无法输出、判断、比较,致使程序直接退出运行。
如果设置成last == NULL;的情况时,因为last的地址本身为NULL,所以循环条件直接被打破,最后一个节点并没有参与到循环的运算当中。
至于do while做循环,在最开始的设计中并没有考虑到它,直到代码总体完成后才意识到,要改成do while需要不止一处修改代码段,而且在do while中有类似的问题需要去解决,所以为了保住自己的头发,我放弃了思考.jpg
说完大体的循环,就剩下一些比较零碎的部分:
1.if (last->flag == 0)此行代码是在遇见层次断点时能让这个单元节点在循环中执行下去而设置的,而此时上一个层次的单元节点应尽数输出完毕,所以不用考虑不同层次混淆的问题。
2.last = p->link;这行在循环之后的代码是为了能找到p的下一个单元节点,等删除p节点时,链表能连接起来。
3. if (ready == p)防止执行时间最小的可能是头单元节点造成ready成空指针。
完整代码:
#include "stdio.h"
#include <stdlib.h>
#include <conio.h>
#define getpch(type) (type*)malloc(sizeof(type))
#define NULL 0
//定义作业jcb结构体
struct jcb
{
char name[100];
int tr;//作业提交时间
int tn;//作业运行时间
int ts;//作业开始时间
int tf;//作业完成时间
float ti;//作业周转时间
float wi;//作业带权周转时间
int flag;//顺序
struct jcb* link;
}*ready = NULL, * h, * p;
typedef struct jcb JCB;
JCB* first, * second, * last;
int time = 0;
int num = 0; //作业数量
int sum = 0;//目前总时间
float eti = 0.0;
float ewi = 0.0;
void linkpcb()//先将链表尾插法连接起来 //如果行不通就执行方案2 即将这些先按到达顺序排序后,通过dolevel进行分层,在用sort进行最短排序
{
int insert = 0;
if ((ready == NULL) || ((p->tr) <= (ready->tr) && p->tn < ready->tn )) /*提交兼执行时间最小者,插入队首*/
{
p->link = ready;
ready = p;
}
else /* 进程比较提交时间,插入适当的位置中*/
{
first = ready;
second = first->link;
while (second != NULL)
{
if ((p->tr) <= (second->tr)) /*若插入作业比当前作业提交时间小,*/
{
if (p->tr == second->tr)
{
while (second != NULL)//比较执行时间
{
if (p->tn <= second->tn)
{
p->link = second;//p插入到second前面
first->link = p;
second = NULL;//second只能在连接好的链表上运行
insert = 1;
}
else
{
first = first->link;
second = second->link;
}
}
}
else
{
/*插入到当前进程前面*/
p->link = second;//p插入到second前面
first->link = p;
second = NULL;//second只能在连接好的链表上运行
insert = 1;
}
}
else /* 插入进程提交时间最高,则插入到队尾*/
{
first = first->link;
second = second->link;
}
}
if (insert == 0) first->link = p;//循环到最后若依旧没有比second大的,则first此时是p的前一个
}
}
void dolevel()
{
if(second->tr <= sum)
{
sum = sum + second->tn;
}
else
{
sum = second->tr + second->tn;
second->flag = 0;
}
second = second->link;
}
void destroy() /*建立作业撤消函数(进程运行结束,卸载作业)*/
{
free(p);
}
void running() /* 建立作业就绪函数(作业运行时间到,置就绪状态*/
{
if (p->link == NULL)
destroy(); /* 调用destroy函数*/
}
void disp(JCB* pr) /*建立作业显示函数,用于显示当前作业*/
{
printf(" %s\t", pr->name);
printf(" %d\t", pr->tr);
printf(" %d\t", pr->tn);
printf(" %d\t", pr->ts);
printf(" %d\t", pr->tf);
printf(" %-0.2f\t", pr->ti);
printf(" %-0.2f\t", pr->wi);
printf("\n");
}
void check() /* 建立作业查看函数 */
{
//累加变量time 记录当前用时 !!!
if (time >= p->tr)
{
p->ts = time;
p->tf = p->ts + p->tn;
time = p->tf;
}
else
{
p->ts = p->tr;
p->tf = p->ts + p->tn;
time = p->tf;
}
p->ti = p->tf - p->tr;
p->wi = p->ti / p->tn;
//!!!
}
void sort() /* 建立对作业进行提交时间排列函数,分出第一层*/
{
first = ready;//first归位
p = ready;
last = ready;
if (last->flag == 0)//置1
{
last->flag = 1;
}
while ((last->flag == 1 && last->link != NULL))//一层中找到最小的 最后一个会被强制退出
{
if (last->tn < p->tn)
{
p = last;
}
last = last->link;
if (last->link == NULL)//在最后一个被踢出前先进行比较
{
if (last->tn < p->tn)
{
p = last;
}
}
}
last = p->link;
if (ready == p)//防止最小可能是头单元节点造成ready成空指针
{
ready = ready->link;
}
first->link = last;
p->link = NULL;
check();
disp(p);
eti += p->ti;
ewi += p->wi;
running();
}
void input() /* 建立作业控制块函数*/
{
int i;
printf("\n 请输入即将运行的进程总数目");
scanf("%d", &num);
for (i = 0; i < num; i++)
{
p = getpch(JCB);
printf("\n 输入作业名:");
scanf("%s", p->name);
printf("\n 输入作业提交时间:");
scanf("%d", &p->tr);
printf("\n 输入作业运行时间:");
scanf("%d", &p->tn);
printf("\n");
p->ts = 0;
p->tf = 0;
p->ti = 0.0f;
p->wi = 0.0f;
p->link = NULL;
p->flag = 1;
linkpcb();
}//此时排序顺序是 提交顺序 内 有执行时间的顺序
p->link = NULL;
printf("\n 名称 \t 提交 \t 运行 \t 开始 \t 完成 \t 周转\t 带权周转 \n");
second = ready->link;//第二个
sum = ready->tr + ready->tn;
p = ready;
check();//对最先到达那个进行特殊输出
disp(p);
eti += p->ti;
ewi += p->wi;
ready = ready->link;
p->link = NULL;
running();
do
{
dolevel();
} while (second->link != NULL);//second此时是末尾节点
for (i = 1; i < num; i++)
{
sort(); /* 调用sort函数,对提交顺序和执行时间进行比较*/
}
}
int main() /*主函数*/
{
input();//此时已经按提交顺序排好了
eti /= num;
ewi /= num;
printf("\n该组作业平均周转时间为:%0.2f", eti);
printf("\n该组作业平均带权周转时间为:%0.2f", ewi);
return 0;
}
input函数中,由于头结点不存在等待时间sum,所以second = ready->link;在dolevel函数时是从第二个单元节点进行计算的。也由于这个原因,头结点没有断点,只能被单独输出。(注:头结点理论上是可以设置断点的,大体思路是将dolevel函数运行前sum初始化为0,second指向头结点)
eti作为周转函数、ewi作为带权周转函数进行计算。
PS:由于篇幅、时间关系,其他非核心函数内容会在响应比、时间片轮转法中详细讲述。
另外代码段中的注释有很多在编写代码时手懒没做更改,所以错误注释会比较多