fat16.3

Lab Week 16

实验内容:CPU 调度

  1. 编译运行 alg.19-1-scheduler-SJF-1.c,讨论其中模拟的 SJF 调度策略;
  2. 编译运行 alg.19-2-scheduler-RR-5.c 和 alg.19-2-scheduler-RR-5-logfile.c ,讨论其中模拟的 RR 调度策略;
  3. 在 alg.19-2-scheduler-RR-5.c 的框架上编写程序实现实时系统的 Least Laxity First调度:输入一个包含若干任务的到达时刻、执行时间和截止时刻的固定任务表,固定一个调度时间间隔,给出单 CPU 下的 LLF/LSF 调度结果的甘特图,并计算任务的平均响应时间(任务从到达至实际完成的时间)。

编译运行 alg.19-1-scheduler-SJF-1.c

最短作业优先算法(SJF)将每个进程与其下次 CPU 执行的长度关联起来。
当 CPU 变为空闲时,它会被赋给具有最短 CPU 执行的进程。如果两个进程具有同样长度的 CPU 执行,则考虑其它策略。

可以证明该算法可以做到任务的平均等待时间最小。

struct {
    int arrival;
    int burst;
    int prio;
    int status;
    pid_t pid;
    pid_t tid;
} task_table[MAX_T]; 

创建任务的结构体,包括到达时间,突发时间,优先级,状态。

struct {
	int task_no;
	int prio;
} prio_queue[MAX_T + 1];
int prio_queue_len = 0;

一个用堆实现的优先队列。以prio为优先度排序。

int push_prio_queue(int task_no)

有限队列的push函数,将编号为task_no的任务入队。

int pop_prio_queue(void)

弹出优先度最高的任务,返回任务的编号。

int init_task_table(void)

alg.19-0-task-table-SJF.txt读取进程信息,存到task_table中,并输出有用的信息。返回任务数。

void task_worker(int burst) /* reentrant function to simulate the burst time */

模拟任务占用了cpu burst的时间。

void time_counter(int i)
{
    for (int j = 0; j < i; j++) {
        usleep(TIME_Q);
    }
    return;
}

先定义了量子时间,让程序休眠量子时间的 i 倍的时间。

void scheduler(void)
{
	int task_no;

	while (1) {
//		printf("sem_task: %ld %ld %ld %ld\n", sem_task[0].__align, sem_task[1].__align, sem_task[2].__align, sem_task[3].__align);
		pthread_mutex_lock(&mutex);
		task_no = pop_prio_queue(); /* select the first task */
		if(task_no != -1) {
		    task_table[task_no].status = RUNNING;
			sem_post(&sem_task[task_no]); /* scheduling selected task */
			pthread_mutex_unlock(&mutex);
			sem_wait(&sem_sche); /* scheduler sleeping */
		} else {
			pthread_mutex_unlock(&mutex);
		}
	}
}

有互斥锁mutex锁住任务表和优先队列。因为每个线程和scheduler都会修改任务表和优先队列。故在操作之前上锁,操作后解锁。

有信号量sem_sche用作唤醒scheduler,每个线程执行任务时,scheduler会休眠,执行完后将其唤醒。

scheduler不断从优先队列队头取任务,并解锁任务。改变该任务的状态,让该任务可以被线程执行。

 static void *thread_ftn(void *arg)
{
    int *numptr = (int *)arg;
    int num = *numptr;

	int curr_time;
	while (1) {
		curr_time = timer;
		if(curr_time >= task_table[num].arrival) {
			break;
		}
	}
    task_table[num].status = READY;
	task_table[num].prio = task_table[num].burst; /* SJF */
	task_table[num].pid = getpid();
	task_table[num].tid = gettid();
	pthread_mutex_lock(&mutex);
	push_prio_queue(num);
	pthread_mutex_unlock(&mutex);
    sem_wait(&sem_task[num]); /* waiting for scheduling */
	printf("timer: %d running task num = %d, burst = %d\n", timer, num, task_table[num].burst);
    task_worker(task_table[num].burst); /* task process simulator */
    task_table[num].status = TERM; /* task terminated */
	sem_post(&sem_sche); /* wake up the scheduler */
    pthread_exit(NULL);
}

这是每个线程要执行的函数,传入参数指示该线程的任务变换。完善该任务的信息,并将该任务加入优先队列(上锁解锁)。
等待scheduler允许自己执行该任务。
任务执行完后,唤醒scheduler,让scheduler分配其他任务。

int main(void)

主函数初始化信号量,读入任务信息,创建对应数量的线程执行该任务,运行scheduler分配任务。等待每个线程结束,销毁信号量。

运行结果如下
在这里插入图片描述
可见任务执行的顺序为brust从小到大的顺序,按照了SJF策略。程序不能正常退出,因为scheduler中没有break。加上后程序即可正常结束。

SJF是一种朴素的调度策略,用贪心的思想使得总等待时间最小。
但任务的brust time在实际中并非很好确定,需要根据上一次和历史的执行时间来预测。使得该算法有点偏差。
且总等待时间并非一个很好的衡量指标,没有考虑任务本身的优先级。随着执行时间短的任务动态加入队列,会导致一些brust time 长的任务一直不能完成。

alg.19-2-scheduler-RR-5.c

时间片轮转(RR)调度算法是专门为分时系统设计的。
该算法中,将一个较小时间单元定义为时间量或时间片。大小通常为 10~100ms。任务就绪队列作为循环队列。CPU 调度程序循环整个就绪队列,为每个进程分配不超过一个时间片的 CPU。

CPU空闲时,从任务队列的队头取进程执行。若执行的时间小于一个时间片,则该任务就完成了。
若执行时间大于一个时间片,则定时器中断,并中断操作系统,停止执行该任务。将该任务加入就绪队列队尾。并再从队头取任务。

struct {
    int arrival; /* arrival time in quantum number */
    int burst; /* burst time in quantum number */
	long int exp_us; /* experienced time of this task in usecond*/
    int prio; /* not used */
    int status; /* NEW, READY, SCHE or TERM */
    pid_t pid;
	pid_t tid; /* not used */
} task_table[MAX_T]; 
int max_num; /* lenth of static task table */

一个任务表存储任务,包含到达时间,突击时间,已经执行的时间,pid,tid等信息。

struct {
	int task_no;
	int next;
} sche_queue[MAX_T]; /* static link list */
int sche_queue_len = 0; /* number of entries in the cyclic READY/SCHE queue */
int sche_queue_front = -1;
int sche_queue_end = -1;
int free_queue_front = -1;

创建个FIFO的队列,作为就绪任务队列。

long int time_us(void)

返回精确的系统时间,分时系统的基础。

int init_task_table(void)

从alg.19-0-task-table-RR.txt读入任务信息,存入task_table中,并输出关键的任务信息。

void task_worker(int task_no), void busy_burst(int task_no)

模拟系统执行程序,因为系统计时可能不精确,需要每个100单位直接掐断一次,记录程序已经运行的时间。已经运行时间超过设定时间时就退出。

static int task_pro(void *arg)

与19.1相同,有保护队列和任务表的互斥锁mutex,确保它们每次只有一个线程访问。

每个线程执行的函数。传入的指针指向该线程要执行的任务编号。
先精致睡眠,等待任务在到达时间准时到达。
完善该任务的信息。
开始while(1)循环,直到任务队列不为空,将任务加入循环队列中。

随后等待scheduler唤醒这个进程。
进程唤醒后开始执行任务。
执行完后更改信息,输出信息。

void scheduler(void)

调度器,在队列不空的情况下,每次循环扫描队列。
若碰到READY的任务,则设为SCHE,让它执行一个时间片后,挂起。
若碰到SCHE的任务,让它继续执行一个时间片后,挂起。
若碰到TERM的任务,上锁,修改队列,解锁。并输出有关信息。

int main(void)

主函数先初始化任务队列。初始化信号量。
初始化任务表,用clone为每个任务的继承父进程空间的进程。
然后运行scheduler
等待每个任务都完成后摧毁信号量,退出。
在这里插入图片描述
可见在任务2还没到的时候,循环执行任务0,1,3
在任务2到达后,循环执行任务0,1,2,3。
虽然时间不精确,但大致做到了分时执行。到一个时间片的时间后就切断当前进程。扫描队列中的下一个任务,并执行。
scheduler不会休眠,一直定时切换进程,直到全部任务完成。

alg.19-2-scheduler-RR-5-logfile.c

程序功能性与上一个程序完全一样,只是删掉了所以任务信息输出。采用了更直观的输出方式,在scheduler扫描到哪个任务就输出它的编号。展示CPU的执行任务顺序。但每个任务在TERM时都会被输出一次。
在这里插入图片描述
可见执行顺序大致与上一个程序相同。
任务4到达比较晚,在前半段主要循环执行任务0,1,2,3
任务4到达后,循环执行任务0,、1、2、3、4
任务3、4需要的执行时间短,结束后循环执行1、2

RR算法能让每个任务公平地使用CPU,排队轮流使用CPU。可以通过限制任务队列长度,让执行时间长的程序也能在有限等待后被完全执行。能让CPU一直工作。
但同样没有考虑任务的优先级,到达时间等因素。

编写程序实现实时系统的 Least Laxity First调度

最低松弛度优先(LLF)算法根据任务的紧急程度确定其优先度,每个任务都有个DDL。
定义
松弛度=DDL-需要运行的时间-当前时间
可以衡量它的紧急程度

属于可抢占调度。若新到来了一个最紧急的任务,它会中断CPU当前执行的任务,立刻抢占CPU。

程序实现思路主要是将19.2的循环队列,换成19.1的优先队列。
优先队列的优先度设置为任务松弛度,可从队头找到最紧急的任务。
同样设置时间片。
每到一个时间片的时间节点,调度器就挂起当前正在执行的任务,并执行队头的任务。其中需要一些特判,在任务完成后,使得它能顺利出队。

static int task_pro(void *arg)
{
    int *numptr = (int *)arg;
    int task_no = *numptr;

	usleep(TIME_q * 1000 * task_table[task_no].arrival);
	printf("task_no %d arrival_us at %ld\n", task_no, time_us() - init_us); 
    task_table[task_no].status = READY;

	task_table[task_no].pid = getpid();


	pthread_mutex_lock(&mutex);
	push_prio_queue(task_no);//不考虑优先队列的容量的,上锁后直接将任务加入优先队列中 
	pthread_mutex_unlock(&mutex);	
		

    sem_wait(&sem_task[task_no]); /* waiting for scheduling */
    task_worker(task_no); /* task process simulator */

	printf("\ntask_no: %d terminated at %ld\n", task_no, time_us() - init_us);
    task_table[task_no].status = TERM; /* task terminated */

    _exit(0);
}

每个进程执行的函数,通过传入的指针获得任务编号,完善任务信息。
然后将该任务加入优先队列。
等待scheduling 唤醒该任务。
task_worker能精确执行相应时间
最后将任务状态改为TERM。

void scheduler(void)
{
	int task_no;
	int posn, pre_posn, dele_posn;
	int ret, silence_counter = 0;
    	long exp_us, burst_us;

	printf("scheduler start . . .\n");
	
	int nowtask=-1;

	while (1) 
	{
		if(prio_queue_len!=0)
		{
			pthread_mutex_lock(&mutex);
			task_no = prio_queue[1].task_no;// 取出优先队列队头的任务 
			pthread_mutex_unlock(&mutex);
			if(task_table[task_no].status!=TERM)
			printf("task %d is going to be executed\n",task_no);
			
			silence_counter = 0;
			if (nowtask!=-1) 
			{
				if (task_table[nowtask].status!=TERM)// 如果当前有在执行任务,且它没有TERM,则将它挂起 
				kill(task_table[nowtask].pid, SIGSTOP);
			}
			
			if (task_table[task_no].status==READY)// 如果将要执行的任务为READY,则将其改为SCHE 
			{
				task_table[task_no].status=SCHE;
				nowtask= task_no;
				sem_post(&sem_task[task_no]);//启动该任务 
			}
			else if (task_table[task_no].status==SCHE)
			{
				nowtask= task_no;
				kill(task_table[task_no].pid, SIGCONT);//继续执行该任务 
			}
			else
			{
				nowtask=-1;//要执行的任务已经执行完了,直接选择空过, 
				
				pthread_mutex_lock(&mutex);
				pop_prio_queue();//出队,等待下一轮选任务 
			
				pthread_mutex_unlock(&mutex);
			}
			
			usleep(3000*TIME_q);
			
		}
		else 
		{
			printf("silence_counter = %d\n", silence_counter);
			if(silence_counter > 3) {
				break;
			}
			sleep(1);
			silence_counter++;
		}
	}
	
	return;
}

调度器代码部分,每次取队头的任务。若取不出来,就数3个数,数到3就退出。
若当前有在执行任务,且该任务不在TERM状态,就将其挂起。
若队头任务可被执行,则开始执行队头的任务(即使紧急度最高的)
程序运行结果如下
在这里插入图片描述
其中每列第3个数为该任务的DDL
为了精简输出,每一句execute间隔为3个单位时间,导致输出与实际执行时间存在一些误差。
可见任务的执行顺序都是按照紧急度排序执行。在时刻16任务4到来,任务4 的紧急度最高,立刻抢占了CPU,挂起了正在执行的任务2.
甘特图如下在这里插入图片描述
可见程序执行与理论的几乎没有偏差。
任务0响应时间 8
任务1响应时间 46
任务2应时间 18
任务3响应时间 53
任务4响应时间 6
平均响应时间 26.2
可见紧急度越高的任务,响应时间会相对小

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值