一、实验目的
1、了解操作系统CPU管理的主要内容。
2、加深理解操作系统管理控制进程的数据结构--PCB。
3、掌握几种常见的CPU调度算法(FCFS、SJF、HRRF、RR)的基本思想和实现过程。
4、用C语言模拟实现CPU调度算法。
5、掌握CPU调度算法性能评价指标的计算方法。
6、通过对进程调度算法的模拟加深对进程概念和进程调度算法的理解。
二、实验内容
1、用C语言编写程序,模拟单处理器下先来先服务算法FCFS,要求显示各进程的到达时间、服务时间、完成时间,周转时间以及该算法的平均周转时间和平均带权周转时间。运行以下参考程序,给出结果截图并分析该算法的优缺点。(3分)
参考程序:
#include <stdio.h>
#include <stdlib.h>
struct PCB //先来先服务FCFS
{
char name[10]; //进程名
float arrivetime; //到达时间
float servetime; //服务时间
float finishtime; //完成时间
float roundtime; //周转时间
float daiquantime; //带权周转时间
};
struct PCB a[50];//定义进程数组
struct PCB *sortarrivetime(struct PCB a[], int n);//声明到达时间冒泡排序函数
void FCFS(struct PCB a[],int n,float *t1,float *t2);//先来先服务算法
//按到达时间进行冒泡排序
struct PCB *sortarrivetime(struct PCB a[], int n){
int i, j;
struct PCB t;
int flag; //标志变量,记录在每一趟冒泡中是否有元素交换,没有交换则结束冒泡
for (i = 1; i<n; i++) //外层循环控制比较趟数
{
flag = 0; //初始值设置为0
for (j = 0; j<n - i; j++) //内存循环控制每一趟的比较次数
{
if (a[j].arrivetime>a[j + 1].arrivetime) //将到达时间短的交换到前边
{
t = a[j];
a[j] = a[j + 1];
a[j + 1] = t;
flag = 1; //有交换,flag置1
}
}
if (flag == 0)//如果一趟排序中没发生任何交换,则排序结束
{
break;
}
}
return a; //返回排序后进程数组
}
//先来先服务算法
void FCFS(struct PCB a[],int n,float *t1,float *t2)
{
int i;
a[0].finishtime = a[0].arrivetime + a[0].servetime; //完成时间=到达时间+服务时间
a[0].roundtime = a[0].finishtime - a[0].arrivetime; //周转时间=完成时间-到达时间
a[0].daiquantime = a[0].roundtime / a[0].servetime; //带权时间=周转时间/服务时间
for (i = 1; i<n; i++)
{
if (a[i].arrivetime<a[i-1].finishtime)//当前到达时间在上一个作业结束时间之前
{
a[i].finishtime = a[i-1].finishtime + a[i].servetime;//完成时间=上一个完成时间+服务时间
a[i].roundtime = a[i].finishtime - a[i].arrivetime; //周转时间=完成时间-到达时间
a[i].daiquantime = a[i].roundtime / a[i].servetime; //带权时间=周转时间/服务时间
}
else //当前到达时间在上一个作业结束时间之后
{
a[i].finishtime = a[i].arrivetime + a[i].servetime;//完成时间=到达时间+服务时间
a[i].roundtime = a[i].finishtime - a[i].arrivetime; //周转时间=完成时间-到达时间
a[i].daiquantime = a[i].roundtime / a[i].servetime;//带权时间=周转时间/服务时间
}
}
printf("=============================================================\n");
printf("进程相关信息如下:\n\n");
printf("进程名 ");
printf("到达时间 ");
printf("服务时间 ");
printf("完成时间 ");
printf("周转时间 ");
printf("带权周转时间\n");
for (i = 0;i<n;i++)
{
printf("%-10s",a[i].name);
printf("%-10.0f",a[i].arrivetime);
printf("%-10.0f",a[i].servetime);
printf("%-10.0f",a[i].finishtime);
printf("%-10.0f",a[i].roundtime);
printf("%10.2f\n",a[i].daiquantime);
*t1 += a[i].roundtime;
*t2 += a[i].daiquantime;
}
}
int main()
{
float t1 ; //总周转时间
float t2 ; //总带权周转时间
float avr_t1 ; //平均周转时间
float avr_t2 ; //平均带权周转时间
int n, i;
char select = ' '; //选择算法变量标识
while (select != '2') //不为退出标识,保持循环
{
t1 = 0.0f;
t2 = 0.0f;
system("clear");
printf("\n请选择算法:1.先来先服务算法 2.退出程序\n\n请输入选择: ");
scanf("%c", &select);
if (select == '1') //先来先服务算法
{
printf("\n=====================先来先服务算法FCFS=====================\n\n");
printf("请输入进程数:");
scanf("%d", &n);
for (i = 0; i<n; i++)
{
printf("%d 进程名:", i + 1);
scanf("%s", a[i].name);
printf("到达时间:");
scanf("%f", &a[i].arrivetime);
printf("服务时间:");
scanf("%f", &a[i].servetime);
}
getchar();
sortarrivetime(a, n);//按到达时间先后进行冒泡排序
FCFS(a,n,&t1,&t2); //先来先服务算法
avr_t1 = t1 / n;
avr_t2 = t2 / n;
printf("\n");
printf("平均周转时间为:%2.2f\n", avr_t1);
printf("平均带权周转时间为:%2.2f\n", avr_t2);
getchar();
}
else if (select == '2')
{
exit(0);
}
else
{
printf("please enter right choose!\n");
}
}
return 0;
}
请同学按下列给出的数据测试运行结果:
进程 | 到达时间 | 服务时间 |
P1 | 0 | 4 |
P2 | 1 | 6 |
P3 | 2 | 3 |
P4 | 3 | 1 |
P5 | 7 | 2 |
要求给出编译及运行过程和运行结果:
分析该算法优缺点:
先来先服务 (FCFS) 算法是一种简单的调度算法,它按照作业到达的先后顺序进行调度,先到达的作业先得到执行。
优点:
- 简单直观: FCFS 算法是最简单、最直观的调度算法之一,易于实现和理解。
- 无饥饿现象: 由于按照到达顺序执行,不存在某些作业长时间等待而无法执行的情况,避免了饥饿现象。
缺点:
- 平均等待时间不稳定: FCFS 算法可能导致平均等待时间较长,特别是当某个长服务时间的作业先到达时,后续作业需要等待较长时间。
- 不适合短作业: 对于长作业和短作业混合的情况,FCFS 可能会导致短作业长时间等待,影响系统的响应时间和效率。
- 无法充分利用 CPU: FCFS 算法不考虑作业的执行时间长短,可能导致 CPU 的利用率较低。
综上所述,FCFS 算法适用于作业长度差异不大且不需要考虑作业执行时间的情况,但对于需要提高系统响应时间和利用率的情况,更复杂的调度算法如短作业优先 (SJF)、优先级调度、多级反馈队列等可能更加合适。
2.编程实现最短作业优先算法 SJF。(3分)
参考程序框架:
#include <stdio.h>
#include <stdlib.h>
struct PCB
{
char name[10]; //进程名
float arrivetime; //到达时间
float servetime; //服务时间
float finishtime; //完成时间
float roundtime; //周转时间
float daiquantime; //带权周转时间
};
struct PCB a[50];//初始化指针和数组
struct PCB *sortarrivetime(struct PCB a[], int n);//声明到达时间冒泡排序函数
void SJF(struct PCB a[], int n, float *t1, float *t2);//声明短作业优先算法函数
struct PCB *sortarrivetime(struct PCB a[], int n)
{ int i, j;
struct PCB t;
int flag; //标志变量,记录在每一趟冒泡中是否有元素交换,没有交换则结束冒泡
for (i = 1; i<n; i++) //外层循环控制比较趟数
{
flag = 0; //初始值设置为0
for (j = 0; j<n - i; j++) //内存循环控制每一趟的比较次数
{
if (a[j].arrivetime>a[j + 1].arrivetime) //将到达时间短的交换到前边
{
t = a[j];
a[j] = a[j + 1];
a[j + 1] = t;
flag = 1; //有交换,flag置1
}
}
if (flag == 0)//如果一趟排序中没发生任何交换,则排序结束
{
break;
}
}
return a; //返回排序后进程数组
}
//短作业优先算法
void SJF(struct PCB a[], int n, float *t1, float *t2)
{
int i,c,d;
struct PCB t;
a[0].finishtime = a[0].arrivetime + a[0].servetime; //完成时间=到达时间+服务时间
a[0].roundtime = a[0].finishtime - a[0].arrivetime; //周转时间=完成时间-提交时间
a[0].daiquantime = a[0].roundtime / a[0].servetime; //带权时间=周转时间/服务时间
for (i = 1; i < n; i++)
{
for (c = i; c < n - 1; c++)
{
for (d=c+1;d<n;d++) //d=i+1改成d=c+1
if ((a[i - 1].finishtime >= a[c].arrivetime)&&(a[i - 1].finishtime >= a[d].arrivetime) && (a[c].servetime > a[d].servetime))
{
t = a[c];
a[c] = a[d];
a[d] = t;
}
}
if (a[i].arrivetime<a[i - 1].finishtime) //当前到达时间在上一个作业结束时间之前
{
a[i].finishtime = a[i - 1].finishtime + a[i].servetime;
}
else //当前到达时间在上一个作业结束时间之后
{
a[i].finishtime = a[i].arrivetime + a[i].servetime;
}
}
printf("=============================================================\n");
printf("进程相关信息如下:\n\n");
printf("进程名 ");
printf("到达时间 ");
printf("服务时间 ");
printf("完成时间 ");
printf("周转时间 ");
printf("带权周转时间\n");
for (i = 0;i<n;i++)
{
printf("%-10s",a[i].name);
printf("%-10.0f",a[i].arrivetime);
printf("%-10.0f",a[i].servetime);
printf("%-10.0f",a[i].finishtime);
printf("%-10.0f",a[i].roundtime);
printf("%10.2f\n",a[i].daiquantime);
*t1 += a[i].roundtime;
*t2 += a[i].daiquantime;
}
}
int main()
{
float t1 ; //总周转时间
float t2 ; //总带权周转时间
float avr_t1 ; //平均周转时间
float avr_t2 ; //平均带权周转时间
int n, i;
char select = ' '; //选择算法变量标识
while (select != '2') //不为退出标识,保持循环
{
t1 = 0.0f;
t2 = 0.0f;
system("clear");
printf("请选择算法:1.短作业优先算法 2.退出程序\n\n请输入选择: ");
scanf("%c", &select);
if (select == '1') //短作业优先算法
{
printf("\n=====================短作业优先算法SJF=====================\n\n");
printf("请输入进程数:");
scanf("%d", &n);
for (i = 0; i<n; i++)
{
printf("%d 进程名:", i + 1);
scanf("%s", a[i].name);
printf("到达时间:");
scanf("%f", &a[i].arrivetime);
printf("服务时间:");
scanf("%f", &a[i].servetime);
}
getchar();
sortArrivalTime(a, n); //按到达时间进行冒泡排序
SJF(a, n, &t1, &t2); //调短作业优先算法
avr_t1=t1/n //平均周转时间
avr_t2=t2/n //平均带权周转时间
printf("\n");
printf("平均周转时间为:%2.2f \n", avr_t1);
printf("平均带权周转时间为:%2.2f \n", avr_t2);
getchar();
}
else if (select == '2')
{
exit(0);
}
else
{
printf("please enter right choose!\n");
getchar();
}
}
return 0;
}
编译及执行过程以及结果截屏:
分析该算法的优缺点:
短作业优先 (Shortest Job First, SJF) 算法是一种基于作业服务时间长度的调度算法。它会优先选择服务时间最短的作业来执行,以最小化平均等待时间和周转时间。
优点:
1. 最小化平均等待时间: SJF 算法能够在所有就绪作业中优先执行服务时间最短的作业,从而使平均等待时间最小化。
2. 最小化周转时间: 由于短作业先执行,因此整体作业完成时间相对较短,周转时间较小。
3. 高效利用 CPU: SJF 算法能够有效利用 CPU 资源,优先处理短作业,从而提高系统的响应速度和效率。
4. 避免长作业等待: SJF 算法能够避免长作业长时间占用 CPU 而导致其他作业等待过久的情况,降低了长作业对系统性能的影响。
缺点:
1. 无法预测作业的准确服务时间: SJF 算法要求预先知道每个作业的准确服务时间,但实际情况下很难预测作业的执行时间。
2. 可能导致长作业饥饿: 如果一些长服务时间的作业不断被短作业抢占 CPU 资源,长作业可能长时间等待,出现饥饿现象。
3. 可能引入优先级反转问题: 当有一个短作业不断到达并抢占 CPU,长作业需要等待,可能导致长作业优先级反转的问题。
4. 对响应时间不敏感: SJF 算法更注重长期的平均等待时间和周转时间,而对单个作业的响应时间不敏感,可能导致一些作业等待时间较长。
综上所述,SJF 算法在作业长度相差较大且服务时间可预测的情况下效果较好,但实际应用中需要注意预测服务时间的准确性、长作业饥饿的问题以及对响应时间的敏感度。针对不同的应用场景和系统需求,需要综合考虑 SJF 算法的优缺点,选择合适的调度算法来优化系统性能。
3.编程实现最高响应比优先算法HRN,并分析该算法的优缺点。(要求给出程序设计分析和调试通过的程序,并给出编译,运行步骤和执行结果截图。)(3分)
#include <stdio.h>
#include <stdlib.h>
struct PCB
{
char name[10]; //进程名
float arrivetime; //到达时间
float servetime; //服务时间
float finishtime; //完成时间
float roundtime; //周转时间
float daiquantime; //带权周转时间
};
struct PCB a[50];//初始化指针和数组
struct PCB *sortarrivetime(struct PCB a[], int n);//声明到达时间冒泡排序函数
void SJF(struct PCB a[], int n, float *t1, float *t2);//声明短作业优先算法函数
struct PCB *sortarrivetime(struct PCB a[], int n)
{ int i, j;
struct PCB t;
int flag; //标志变量,记录在每一趟冒泡中是否有元素交换,没有交换则结束冒泡
for (i = 1; i<n; i++) //外层循环控制比较趟数
{
flag = 0; //初始值设置为0
for (j = 0; j<n - i; j++) //内存循环控制每一趟的比较次数
{
if (a[j].arrivetime>a[j + 1].arrivetime) //将到达时间短的交换到前边
{
t = a[j];
a[j] = a[j + 1];
a[j + 1] = t;
flag = 1; //有交换,flag置1
}
}
if (flag == 0)//如果一趟排序中没发生任何交换,则排序结束
{
break;
}
}
return a; //返回排序后进程数组
}
void HRN(struct PCB a[], int n, float *t1, float *t2)
{
int i, j;
struct PCB t;
// 初始化第一个进程的完成时间、周转时间和带权周转时间
a[0].finishtime = a[0].arrivetime + a[0].servetime;
a[0].roundtime = a[0].finishtime - a[0].arrivetime;
a[0].daiquantime = a[0].roundtime / a[0].servetime;
for (i = 1; i < n; i++)
{
// 计算每个进程的响应比率并选择响应比率最高的进程进行执行
float max_ratio = -1.0f;
int max_index = -1;
for (j = i; j < n; j++)
{
// 检查进程是否在上一个进程完成后到达
if (a[j].arrivetime <= a[i - 1].finishtime)
{
// 计算响应比率:(等待时间 + 服务时间) / 服务时间
float ratio = (a[i - 1].finishtime - a[j].arrivetime + a[j].servetime) / a[j].servetime;
// 更新最高响应比率和对应的进程索引
if (ratio > max_ratio)
{
max_ratio = ratio;
max_index = j;
}
}
}
// 将具有最高响应比率的进程与当前位置进行交换
if (max_index != -1)
{
t = a[i];
a[i] = a[max_index];
a[max_index] = t;
}
// 更新当前进程的完成时间、周转时间和带权周转时间
a[i].finishtime = a[i - 1].finishtime + a[i].servetime;
a[i].roundtime = a[i].finishtime - a[i].arrivetime;
a[i].daiquantime = a[i].roundtime / a[i].servetime;
}
// 输出进程信息
printf("=============================================================\n");
printf("进程相关信息如下:\n\n");
printf("进程名 ");
printf("到达时间 ");
printf("服务时间 ");
printf("完成时间 ");
printf("周转时间 ");
printf("带权周转时间\n");
for (i = 0; i < n; i++)
{
printf("%-10s", a[i].name);
printf("%-10.0f", a[i].arrivetime);
printf("%-10.0f", a[i].servetime);
printf("%-10.0f", a[i].finishtime);
printf("%-10.0f", a[i].roundtime);
printf("%10.2f\n", a[i].daiquantime);
// 累加总周转时间和总带权周转时间
*t1 += a[i].roundtime;
*t2 += a[i].daiquantime;
}
}
int main()
{
float t1 ; //总周转时间
float t2 ; //总带权周转时间
float avr_t1 ; //平均周转时间
float avr_t2 ; //平均带权周转时间
int n, i;
char select = ' '; //选择算法变量标识
while (select != '2') //不为退出标识,保持循环
{
t1 = 0.0f;
t2 = 0.0f;
system("clear");
printf("请选择算法:1. 最高响应比优先算法 2.退出程序\n\n请输入选择: ");
scanf("%c", &select);
if (select == '1') //最高响应比优先算法
{
printf("\n=====================最高响应比优先算法SJF=====================\n\n");
printf("请输入进程数:");
scanf("%d", &n);
for (i = 0; i<n; i++)
{
printf("%d 进程名:", i + 1);
scanf("%s", a[i].name);
printf("到达时间:");
scanf("%f", &a[i].arrivetime);
printf("服务时间:");
scanf("%f", &a[i].servetime);
}
getchar();
sortarrivetime(a, n); //按到达时间进行冒泡排序
HRN(a, n, &t1, &t2);
avr_t1=t1/n; //平均周转时间
avr_t2=t2/n; //平均带权周转时间
printf("\n");
printf("平均周转时间为:%2.2f \n", avr_t1);
printf("平均带权周转时间为:%2.2f \n", avr_t2);
getchar();
}
else if (select == '2')
{
exit(0);
}
else
{
printf("please enter right choose!\n");
getchar();
}
}
return 0;
}
最高响应比优先算法(Highest Response Ratio Next, HRN)是一种作业调度算法,它根据作业的等待时间和服务时间来选择下一个执行的作业。
优点:
- 考虑作业的等待时间和服务时间: HRN 算法综合考虑作业的等待时间和服务时间,通过计算响应比率来决定作业的执行顺序,更能够反映作业的紧迫程度和优先级。
- 公平性较高: HRN 算法倾向于优先执行等待时间较长且服务时间较短的作业,从而提高作业的响应速度和整体性能,更加公平地分配 CPU 资源。
- 能够避免长作业饥饿: 类似于短作业优先算法,HRN 算法也能够避免长作业长时间等待,优先执行等待时间较短的作业,降低长作业饥饿的可能性。
缺点:
- 计算复杂度较高: HRN 算法需要计算每个作业的响应比率,涉及除法运算,因此算法的计算复杂度较高,特别是当作业数量较大时。
- 可能出现优先级反转问题: 如果某个长作业长时间等待,但在短作业到达后仍然没有得到执行,可能导致长作业的优先级被降低,出现优先级反转问题。
- 对服务时间的依赖性: HRN 算法对于作业的服务时间要求相对严格,需要较准确地预测每个作业的服务时间,否则可能导致算法效果不佳。
综上所述,HRN 算法在一定程度上能够提高作业的响应速度和系统的整体性能,但需要注意计算复杂度较高和对作业服务时间的依赖性。在实际应用中,需要根据具体场景和需求,权衡 HRN 算法的优点和缺点,选择合适的调度算法来优化系统性能。
4、编程实现时间片轮换算法,并分析算法的优缺点。(要求给出程序设计分析和调试通过的程序,并给出编译,运行步骤和执行结果截图。)(附加题)
#include <stdio.h>
#include <stdlib.h>
struct Process {
char name[10]; // 进程名
float arrival_time; // 到达时间
float service_time; // 服务时间
float remaining_time; // 剩余服务时间
float turnaround_time; // 周转时间
float weighted_turnaround_time; // 带权周转时间
};
// 时间片轮转调度算法实现
void roundRobin(struct Process processes[], int n, float time_slice) {
int completed_processes = 0;
float current_time = 0;
while (completed_processes < n) {
for (int i = 0; i < n; i++) {
if (processes[i].remaining_time > 0) {
// 执行当前进程直至时间片用完或进程执行完毕
if (processes[i].remaining_time <= time_slice) {
// 进程执行完毕
current_time += processes[i].remaining_time;
processes[i].remaining_time = 0;
completed_processes++;
// 计算周转时间和带权周转时间
processes[i].turnaround_time = current_time - processes[i].arrival_time;
processes[i].weighted_turnaround_time = processes[i].turnaround_time / processes[i].service_time;
} else {
// 时间片用完
current_time += time_slice;
processes[i].remaining_time -= time_slice;
}
}
}
}
}
int main() {
int n;
float time_slice;
printf("请输入进程数:");
scanf("%d", &n);
struct Process processes[n];
// 输入进程信息
for (int i = 0; i < n; i++) {
printf("进程名:");
scanf("%s", processes[i].name);
printf("到达时间:");
scanf("%f", &processes[i].arrival_time);
printf("服务时间:");
scanf("%f", &processes[i].service_time);
// 初始化剩余服务时间
processes[i].remaining_time = processes[i].service_time;
processes[i].turnaround_time = 0;
processes[i].weighted_turnaround_time = 0;
}
printf("请输入时间片大小:");
scanf("%f", &time_slice);
// 执行时间片轮转调度算法
roundRobin(processes, n, time_slice);
// 输出结果
printf("\n进程名\t到达时间\t服务时间\t周转时间\t带权周转时间\n");
for (int i = 0; i < n; i++) {
printf("%s\t%.1f\t\t%.1f\t\t%.1f\t\t%.2f\n",
processes[i].name, processes[i].arrival_time,
processes[i].service_time, processes[i].turnaround_time,
processes[i].weighted_turnaround_time);
}
return 0;
}
优点:
公平性: 时间片轮换算法保证了每个进程都能在一定时间内得到执行,避免了长时间等待的情况,因此具有较好的公平性。
简单易实现: 时间片轮换算法的实现相对简单直观,主要涉及将 CPU 时间划分为固定大小的时间片,并按顺序轮流执行进程。
响应时间较短: 对于需要快速响应的系统,时间片轮换算法能够保证每个进程都有机会在相对短的时间内执行一定量的任务,从而提高了系统的响应速度。
缺点:
低效性: 时间片轮换算法可能导致一定程度上的性能下降,特别是当时间片设置过小时,会增加进程上下文切换的频率,增加系统开销。
不适用于长作业: 对于长时间运行的作业,时间片轮换算法可能会导致频繁的上下文切换,影响系统的整体性能,而且长作业在等待执行时可能需要等待较长时间。
无法适应实时性要求: 时间片轮换算法无法有效应对对实时性要求较高的系统,因为它无法保证进程能够在规定的时间内完成任务。
平均等待时间较长: 如果时间片设置过大,会导致短作业也需要等待较长时间才能得到执行,从而增加了平均等待时间。
对系统负载敏感: 时间片轮换算法的效率很大程度上取决于时间片的大小和系统负载,需要合理调整时间片大小才能达到较好的性能表现。
三、实验总结和体会(1分)
通过本次实验,我学到了以下几点:
- 进程调度算法的实现:实际编写了几种常见的进程调度算法,包括短作业优先(SJF)算法和时间片轮转调度算法。这些算法是操作系统中重要的组成部分,用于决定进程如何在 CPU 上执行,从而影响系统的性能和响应速度。
- 算法的工作原理:深入理解了不同进程调度算法的工作原理和特点。例如,SJF算法通过选择服务时间最短的进程来优先执行,而时间片轮转调度算法则通过固定大小的时间片轮流执行进程,实现了一种公平的调度方式。
- 算法的优缺点:通过分析每种算法的优缺点,我意识到不同的调度算法适用于不同的场景。例如,SJF算法对短作业有较好的响应,但可能导致长作业等待过久;时间片轮转调度算法能够避免长作业等待,但可能引入较多的上下文切换。
- 编程技能的提升:通过实践编程,我加深了对 C 语言的理解和应用,掌握了结构体、函数、循环等编程基础知识。同时,我学会了如何组织和设计一个完整的进程调度程序,包括输入处理、算法实现和结果输出。
- 实践中的挑战和解决方案:在编写代码的过程中,遇到了一些挑战,如算法逻辑的设计和调试过程中的错误。通过分析问题、调试代码和查阅资料,我逐步解决了这些问题,提升了解决实际问题的能力。