Lab Week 16
实验内容:CPU 调度
- 编译运行 alg.19-1-scheduler-SJF-1.c,讨论其中模拟的 SJF 调度策略;
- 编译运行 alg.19-2-scheduler-RR-5.c 和 alg.19-2-scheduler-RR-5-logfile.c ,讨论其中模拟的 RR 调度策略;
- 在 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
可见紧急度越高的任务,响应时间会相对小