Nachos操作系统实习-lab2

内容一:总体概述

本次lab的主要内容是充分理解Linux中存在的进程/线程调度算法,并在理解Nachos线程调度机制的基础上动手实现两个调度算法——基于优先级的抢占式调度算法和时间片轮转调度算法。
本次lab的重点在于理解Nachos的线程调度机制,在理解的基础上进一步拓展其功能,尤其是实现时间片轮转调度算法时手动开关中断的想法非常巧妙。

内容二:任务完成情况

任务完成列表(Y/N)

Exercise1Exercise2Exercise3Challenge1
完成情况YYYY

具体Exercise的完成情况

Exercise1 调研

我调研的是Linux中采用的进程/线程调度算法,主要有以下六种:
(1) 先来先服务(FCFS)调度算法。
FCFS算法每次从就绪队列中选择最靠前的进程,调度它上CPU运行,直到其运行结束或被中断。FCFS算法的优点是算法实现简单;缺点是当调度了一个长作业占用CPU资源运行的时候,会使得后面的短作业等很长时间,因此适合于CPU繁忙性作业,不适合于I/O繁忙型作业。
(2) 短作业优先(SJF)调度算法。
SJF算法会从就绪队列中选择一个估计运行时间最短的进程,调度它上CPU运行,直到运行结束或被中断。SJF算法的优点是算法实现简单;缺点是长作业可能一直得不到调度,从而产生饥饿现象,且对进程运行时间的估计不一定准确,因此不一定能真正做到最短的进程先被调度。
(3) 优先级调度算法。
优先级调度算法根据作业运行的优先级来实现调度,每次从就绪队列中选择优先级最高的进程,调度它上CPU运行,根据新加入就绪队列的更高优先级进程能否抢占CPU资源又可以分成非剥夺式优先级调度算法和剥夺式优先级调度算法。其中非剥夺式优先级调度算法在遇到更高优先级进程加入就绪队列时,会保留正占用CPU资源的进程,直到其由于自身原因退出;剥夺式优先级调度算法会立即暂停正在占用CPU资源的进程,进行上下文切换为刚加入就绪队列的更高优先级的进程。进程的优先级根据创建后是否可以改变又可以分成静态优先级和动态优先级,静态优先级是在进程创建时确定的,创建后不再改变,由进程类型、进程对资源需求等确定;动态优先级是在进程运行中可以动态调整的,主要由进程占用CPU时间、就绪进程等待CPU时间等确定。
(4) 高响应比优先调度算法。
高响应比优先调度算法是对FCFS调度算法和SJF调度算法的一个综合,对就绪队列中的每个作业,根据其等待时间和估计运行时间计算响应比(响应比 = (等待时间+要求服务时间) / 要求服务时间),然后选取响应比最高的进程,调度它上CPU运行。这样,当两个进程等待时间相同时,会优先选择估计运行时间短的进程;当两个进程估计运行时间相同时,会优先选择先来的进程;当一个长作业等待时间足够长时其响应比会增高因而也不会导致长时间饥饿。
(5) 时间片轮转调度算法
时间片轮转调度算法将所有就绪进程按照到达时间顺序排成队列,选取第一个进程,调度它上CPU运行,但是当运行了一个时间片之后,无论当前进程是否运行完毕或收到中断信号,都会将其释放,调度下一个进程上CPU运行,刚刚释放的进程如果没有执行完,就会再次被添加到就绪队列的队尾等待下次运行。时间片如果足够大,以至于所有进程都可以在一个时间片内完成,那么时间片轮转调度算法就会退化成FCFS调度算法;时间片如果太小,那么CPU将会频繁在不同的进程间切换,使得开销增大,处理效率降低。
(6) 多级反馈队列调度算法
多级反馈队列调度算法集合了时间片轮转调度算法和优先级调度算法,实现了在进程运行时动态调整优先级大小和时间片大小。多级反馈队列调度算法会设置多个就绪队列,每个队列拥有一个优先级,且从第一个队列开始优先级依次降低;每个队列中进程上CPU执行的时间片也不相同,从第一个队列开始时间片依次增加;当一个新进程被创建时,该进程首先会被加入第一个队列的末尾,按照FCFS调度原则等待CPU的调度,当CPU调度该进程时,如果一个时间片内该进程结束,则撤出队列,否则加入第二个队列的末尾,继续按照FCFS调度原则等待调度,依次进行,如果一直到最后一个队列仍未完成,则在最后一个队列中采用时间片轮转的调度算法等待CPU调度;CPU会按照优先级顺序调度,只有当第一个队列中的进程调度完之后才会调度下一个队列,当某个进程占用CPU资源时,如果一个更高优先级的进程被创建,则立即暂停当前占用CPU的进程,将其放回所属队列的末尾,将新创建的更高优先级进程调度上CPU运行。

Linux中的进程状态可以分为五种:运行状态(TASK_RUNNING)、可中断睡眠状态(TASK_INTERRUPTIBLE)、不可中断睡眠状态(TASK_UNINTERRUPTIBLE)、暂停状态(TASK_STOPPED)、僵死状态(TASK_ZOMBIE)。
运行状态又可分为正在占用CPU运行的状态和就绪状态,当一个进程被fork函数创建后会进入就绪状态,当被调度上CPU执行的时候就转换为在CPU上的运行状态,当运行的时间片到了就又会释放CPU资源回到就绪状态。
当正在CPU上运行的进程需要等待资源到位的时候,就会进入睡眠状态。睡眠状态分为可中断睡眠状态和不可中断睡眠状态。其中,可中断睡眠状态又称浅度睡眠状态,当等待的资源到位或者收到信号时,进程会从可中断睡眠状态转换到就绪状态;不可中断睡眠状态又称为深度睡眠状态,只有当等待的资源到位时,才会转换到就绪状态。
正在CPU上运行的进程收到信号SIGSTOP、SIGTSTP、SIGTTIN或SIGTTOU时会进入暂停状态,只有当收到SIGCONT信号时会回到就绪状态。
正在CPU上运行的进程如果运行完毕,但是还没有释放其进程控制块,那么就进入僵死状态。

Exercise2 源代码阅读

scheduler.h中定义了Scheduler类,其中定义了私有成员变量readyList,它是一个指向List的指针,用来作为就绪队列存储准备好但还未运行的线程,类中还声明了一些成员函数,包括构造函数、析构函数、ReadyToRun()函数用来将线程设置为ready状态并添加到就绪队列中、FindNextToRun()函数根据调度算法寻找下一个调度上CPU的线程、Run()函数调度线程占用CPU资源运行、Print()函数打印就绪队列中的信息。
scheduler.cc中是对Scheduler类中成员函数的实现,其中Run()函数的具体执行步骤是:首先检查当前将被替换的线程(旧线程)是否存在栈溢出;接着令当前线程的指针指向将要调度上CPU运行的线程(新线程)并设置新线程的状态为RUNNING;之后调用switch.s中定义的SWITCH函数来实现新旧线程上下文的切换;最后判断threadToBeDestroyed是否不为空,如果不为空则说明旧线程已经运行完毕,所以我们将其删除。
switch.s中用汇编语言定义了两个操作,ThreadRoot和SWITCH。ThreadRoot是第一次创建线程时由SWITCH函数调用的函数,SWITCH的作用是实现新旧线程上下文的切换。SWITCH的具体实现步骤是:首先获取指向旧线程的指针,将当前CPU寄存器的信息保存到旧线程的machineState中,然后保存栈指针和返回地址;接下来让esp指向新线程的栈,恢复寄存器的值和栈指针,完成新旧线程上下文的切换。
timer.h中定义了Timer类,用来模拟硬件的时钟中断,其中定义了私有成员变量randomize,判断是否使用随机中断;handler,中断处理函数;arg,传给中断处理函数的参数。类中还声明了一些成员函数,包括构造函数、析构函数、TimerExpired()函数每当时钟发生中断时就调用、TimeOfNextInterrupt()计算下一次发生时钟中断的时间。
timer.cc中是对Timer类中的成员函数的实现,其中构造函数在初始化完成后会将一个时钟中断插入等待处理的中断队列,之后当接收到一个时钟中断时,TimerHandler会被调用,它又会调用TimerExpired函数,通过TimeOfNextInterrupt函数计算下一个时钟中断发生的时间并将下一个时钟中断插入等待处理的中断队列,之后调用时钟中断处理函数。

Exercise3 线程调度算法扩展

第一步:在thread.h中为Thread类添加私有成员变量priority表示优先级,限定优先级范围为0-9(0最高,9最低),并定义get_priority()函数和set_priority()函数分别用于获取当前线程的优先级和设置当前线程的优先级。此外,为Thread的构造函数添加第二个参数pri,缺省值为0。在thread.cc中在Thread类的构造函数中为priority赋值。

第二步:在scheduler.cc中对ReadyToRun()函数进行修改,当把一个线程插入就绪队列时,采用List类提供的SortedInsert方法,根据优先级大小插入到适当位置。由于要实现基于优先级的抢占式调度算法,所以在ReadyToRun()函数中进行一次判断,如果当前插入就绪队列线程的优先级比正在CPU上运行的线程优先级更高,那么就让现在占用CPU运行的线程暂停,即令其调用Yield()函数。

void
Scheduler::ReadyToRun (Thread *thread)
{
    DEBUG('t', "Putting thread %s on ready list.\n", thread->getName());

    thread->setStatus(READY);
    // readyList->Append((void *)thread);
    readyList->SortedInsert((void *)thread, thread->get_priority());

    // when a thread is forked and has larger priority
    // cause the current thread to yield
    if (thread->get_priority() < currentThread->get_priority()){
        currentThread->Yield();
    }
}

第三步:由于线程切换时,可能下一次调度上CPU运行的线程仍然是刚刚换下来的线程,所以在thread.cc中对Thread类的Yield()函数稍作修改,将scheduler->ReadyToRun(this)语句提到寻找下一个运行的线程之前,这样就保证了下一次选择的线程仍然可以是刚刚换下来的线程。

第四步:在threadtest.cc中定义测试函数,我对于已有的ThreadTest1()函数进行了修改,在其中创建了四个线程,线程1优先级为9,线程2优先级为0,线程3优先级为5,线程4优先级为1。之后我让四个线程分别Fork出一个子线程去调用SimpleThread()函数,并注释掉SimpleThread()中线程主动让出CPU的代码,观察线程的运行顺序,输出结果如下:
在这里插入图片描述
可以看到优先级最高为0的线程2先运行,之后是优先级为1的线程4运行,之后是优先级为5的线程3运行,最后是优先级为9的线程1运行。

第五步:为了进一步测试更高优先级线程对正在运行低优先级线程的抢占性,我又定义了ThreadTest4()函数,在其中创建优先级为9的线程1,并Fork出一个子线程调用CheckPriority1()函数,CheckPriority1()函数与SimpleThread()函数类似,主不过在第一次循环的时候创建了优先级为5的线程2,并Fork出一个子线程去调用CheckPriority2()函数,相似地,CheckPriority2()函数中在第一次循环的时候创建了优先级为0的线程3,并Fork出一个子线程去调用SimpleThread()函数,观察线程的运行顺序,输出结果如下:
在这里插入图片描述
可以看到,当线程1运行了一次以后,CPU会被新创建的具有更高优先级的线程2抢占,同理线程2运行了一次以后,CPU会被新创建的具有更高优先级的线程3抢占,直到线程3运行完毕,再继续线程2的运行,最后继续线程1的运行。

Challenge1 增加全局线程管理机制
我选择的是实现“时间片轮转算法”。
第一步: 首先在system.cc中定义一个timer,使得每隔一定的时间就会发出一个时间中断,在stats.h中定义了宏TimerTicks为100表示这个时间间隔。

第二步:在threadtest.cc中定义测试时间片轮转调度算法的测试函数RR_Thread()和ThreadTest5()。ThreadTest5()中会创建三个线程并分别Fork()出三个子线程去调用RR_Thread()函数。为了让时钟前进,RR_Thread()函数会在每次打印输出之后手动关闭中断再打开中断,这样时钟就会前进10个ticks。具体实现如下:

void
RR_Thread(int which)
{
    int num;

    for (num = 0; num < 20; num++) {
        printf("*** thread %s priority %d looped %d times, current time: %d\n",
      currentThread->getName(), currentThread->get_priority(), num, stats->totalTicks);
        interrupt->SetLevel(IntOff);
        interrupt->SetLevel(IntOn);
    }
}

观察线程的运行顺序,输出结果如下:
在这里插入图片描述
可以看到线程按照1->2->3->4的顺序依次占用CPU运行,每隔一定的时间片(100 ticks)进行线程的切换,直到运行结束。

内容三:遇到的困难以及解决方法

困难1 对于timer的中断机制不清楚以至于实现时间片轮转算法卡住

在实现时间片轮转算法的时候,我遇到的最大问题就是不知道怎么去让时钟前进,而这个困难正是因为我对于timer的中断机制不清楚造成的。于是我静下心来把timer和interrupt部分代码仔细研读了一遍,犹如醍醐灌顶,原来困扰我的问题是这么简单,只需要手动开关中断来让时钟前进就好了。这次困难的解决让我意识到得先学扎实学透才能做出东西,一知半解是做不好东西的。

内容四:收获及感想

通过完成这次lab,我对Nachos的线程调度机制有了更深入的理解。对Linux中进程/线程调度算法的调研,让我更加了解了多种多样的进程/线程调度策略,让我不禁惊叹开发者构思的巧妙。
通过亲手实现优先级抢占调度算法和时间片轮转调度算法,我对于这些调度策略的理解又更深了一层,纸上得来终觉浅,绝知此事要躬行!

内容五:对课程的意见和建议

我建议讨论课的时候可以大家多交流交流自己lab的实现方式,看看有没有一些比较好的想法,也可以看看大家有没有碰到一些相同的问题,可以讨论一起解决,一起进步。

内容六:参考文献

[1] Andrew S. Tanenbaum著.陈向群 马洪兵 译 .现代操作系统[M].北京:机械工业出版社,2011:47-95.
[2] Linux之进程调度算法: https://blog.csdn.net/qq_43414142/article/details/90676984
[3] Linux进程状态的转换: https://blog.csdn.net/jinkang_zhao/article/details/71367924

  • 0
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 3
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值