https://github.com/icoty/nachos-3.4-Lab
本实习希望通过修改Nachos系统平台的底层源代码,达到“扩展调度算法”的目标。本次实验主要是要理解Timer、Scheduler和Interrupt之间的关系,从而理解线程之间是如何进行调度的。
Exercise1 | Exercise2 | Exercise3 | Challenge1 | |
---|---|---|---|---|
第一部分 | Y | Y | Y | Y |
调研Linux或Windows中采用的进程/线程调度算法。具体内容见课堂要求。
linux-4.19.23进程调度策略:SCHED_OTHER分时调度策略,SCHED_FIFO实时调度策略(先到先服务),SCHED_RR实时调度策略(时间片轮转)。
内核代码:内核为每个cpu维护一个进程就绪队列,cpu只调度由其维护的队列上的进程:
vi linux-4.19.23/kernel/sched/core.c:
……
#define CREATE_TRACE_POINTS
#include
DEFINE_PER_CPU_SHARED_ALIGNED(struct rq, runqueues);
……
vi linux-4.19.23/kernel/sched/sched.h:
/*
* This is the main, per-CPU runqueue data structure.
*
* Locking rule: those places that want to lock multiple runqueues
* (such as the load balancing or the thread migration code), lock
* acquire operations must be ordered by ascending &runqueue.
*/
struct rq {
/* runqueue lock: */
raw_spinlock_t lock; // 锁保证互斥访问runqueue
……
struct cfs_rq cfs; // 所有普通进程的集合,采用cfs调度策略
struct rt_rq rt; // 所有实时进程的集合,采用实时调度策略
struct dl_rq dl; // struct dl_rq空闲进程集合
……
};
// cfs_rq就绪队列是一棵红黑树。
/* CFS-related fields in a runqueue */
struct cfs_rq {
……
struct rb_root_cached tasks_timeline; // 红黑树的树根
/*
* 'curr' points to currently running entity on this cfs_rq.
* It is set to NULL otherwise (i.e when none are currently running).
*/
struct sched_entity *curr; // 指向当前正运行的进程
struct sched_entity *next; // 指向将被唤醒的进程
struct sched_entity *last; // 指向唤醒next进程的进程
struct sched_entity *skip;
……
};
vi linux-4.19.23/include/linux/sched.h:实时进程调度实体struct sched_rt_entity,双向链表组织形式;空闲进程调度实体struct sched_dl_entity,红黑树组织形式;普通进程的调度实体sched_entity,每个进程描述符中均包含一个该结构体变量,该结构体有两个作用:
……
// 普通进程的调度实体sched_entity,使用红黑树组织
struct sched_entity {
/* For load-balancing: */
struct load_weight load;
unsigned long runnable_weight;
struct rb_node run_node; // 红黑树节点
struct list_head group_node;
unsigned int on_rq;
……
#ifdef CONFIG_FAIR_GROUP_SCHED
int depth;
struct sched_entity *parent; // 当前节点的父节点
/* rq on which this entity is (to be) queued: */
struct cfs_rq *cfs_rq; // 当前节点所在的就绪队列
/* rq "owned" by this entity/group: */
struct cfs_rq *my_q;
#endif
……
};
// 实时进程调度实体,采用双向链表组织
struct sched_rt_entity {
struct list_head run_list; // 链表组织
unsigned long timeout;
unsigned long watchdog_stamp;
unsigned int time_slice;
unsigned short on_rq;
unsigned short on_list;
struct sched_rt_entity *back;
#ifdef CONFIG_RT_GROUP_SCHED
struct sched_rt_entity *parent;
/* rq on which this entity is (to be) queued: */
struct rt_rq *rt_rq; // 当前节点所在的就绪队列
/* rq "owned" by this entity/group: */
struct rt_rq *my_q;
#endif
} __randomize_layout;
// 空闲进程调度实体,采用红黑树组织
struct sched_dl_entity {
struct rb_node rb_node;
……
};
……
vi linux-4.19.23/kernel/sched/sched.h:内核声明了一个调度类sched_class的结构体类型,用来实现不同的调度策略,可以看到该结构体成员都是函数指针,这些指针指向的函数就是调度策略的具体实现,所有和进程调度有关的函数都直接或者间接调用了这些成员函数,来实现进程调度。此外,每个进程描述符中都包含一个指向该结构体类型的指针sched_class,指向了所采用的调度类。
……
struct sched_class {
const struct sched_class *next;
void (*enqueue_task) (struct rq *rq, struct task_struct *p, int flags);
void (*dequeue_task) (struct rq *rq, struct task_struct *p, int flags);
void (*yield_task) (struct rq *rq);
bool (*yield_to_task)(struct rq *rq, struct task_struct *p, bool preempt);
void (*check_preempt_curr)(struct rq *rq, struct task_struct *p, int flags);
……
};
……
code/threads/scheduler.h和code/threads/scheduler.cc:scheduler类是nachos中的进程调度器,维护了一个挂起的中断队列,通过FIFO进行调度。
code/threads/switch.s:switch.s模拟内容是汇编代码,负责CPU上进程的切换。切换过程中,首先保存当前进程的状态,然后恢复新运行进程的状态,之后切换到新进程的栈空间,开始运行新进程。
code/machine/timer.h和code/machine/timer.cc:Timer类用以模拟硬件的时间中断。在TimerExired中,会调用TimeOfNextInterrupt,计算出下次时间中断的时间,并将中断插入中断队列中。初始化时会调用TimerExired,然后每次中断处理函数中都会调用一次TimerExired,从而时间系统时间一步步向前走。需要说明的是,在运行nachos时加入-rs选项,会初始化一个随机中断的Timer。当然你也可以自己声明一个非随机的Timer,每隔固定的时间片执行中断。时间片大小的定义位于ststs.h中,每次开关中断会调用OneTick(),当Ticks数目达到时间片大小时,会出发一次时钟中断。
扩展线程调度算法,实现基于优先级的抢占式调度算法。
思路:更改Thread类,加入priority成员变量,同时更改初始化函数对其初始化,并完成对应的set和get函数。scheduler中的FindNextToRun负责找到下一个运行的进程,默认是FIFO,找到队列最开始时的线程返回。我们现在要实现的是根据优先级来返回,仅需将插入readyList队列的方法按照优先级从高到低顺序插入SortedInsert,那么插入时会维护队列中的Thread按照优先级排序,每次依旧从头取出第一个,即为优先级最高的队列。抢占式调度则需要在每次中断发生时尝试进行进程切换,如果有优先级更高的进程,则运行高优先级进程。
// 基于优先级的可抢占式调度策略和FIFO调度策略
Thread * Scheduler::FindNextToRun (bool bySleep)
{
// called by threadsleep,直接调度,不用判断时间片
if(bySleep){
lastSwitchTick = stats->systemTicks;
return (Thread *)readyList->SortedRemove(NULL); // 与Remove()等价,都是从队头取
}else{
int ticks = stats->systemTicks - lastSwitchTick;
// 这里设置了运行的最短时间TimerSlice,防止频繁切换消耗CPU资源
// 测试优先级抢占调度时需要屏蔽这句,因为调用Yield()的线程运行时间很短
// 会直接返回NULL
/*if(ticks < TimerSlice){
// 不用切换
return NULL;
}else*/{
if(readyList->IsEmpty()){
return NULL;
}
Thread * next = (Thread *)readyList->SortedRemove(NULL);
// 基于优先级可抢占调度策略,自己添加的宏,Makefile编译添加: -DSCHED_PRIORITY
#ifdef SCHED_PRIORITY
// nextThread优先级高于当前线程则切换,否则不切换
if(next->getPriority() < currentThread->getPriority()){
lastSwitchTick = stats->systemTicks;
return next;
}else{
readyList->SortedInsert(next, next->getPriority());
return NULL;
}
#else // FIFO策略需要取消Makefile编译选项:-DSCHED_PRIORITY
lastSwitchTick = stats->systemTicks;
return next;
#endif
}
}
}
// 线程主动让出cpu,在FIFO调度策略下能够看到多个线程按顺序运行
void SimpleThread(int which)
{
for (int num = 0; num < 5; num++) {
int ticks = stats->systemTicks - scheduler->getLastSwitchTick();
printf("userId=%d,threadId=%d,prio=%d,loop:%d,lastSwitchTick=%d,systemTicks=%d,usedTicks=%d,TimerSlice=%d\n",currentThread->getUserId(),currentThread->getThreadId(),currentThread->getPriority(),num,scheduler->getLastSwitchTick(),stats->systemTicks,ticks,TimerSlice);
// 时间片轮转算法,判断时间片是否用完,
// 如果用完主动让出cpu,针对nachos内核线程算法
/*if(ticks >= TimerSlice){
//printf("threadId=%d Yield\n",currentThread->getThreadId());
currentThread->Yield();
}*/
// 非抢占模式下,多个线程同时执行该接口的话,会交替执行,交替让出cpu
// 基于优先级抢占模式下,优先级高的线程运行结束后才调度低优先级线程
currentThread->Yield();
}
}
// threadtest.cc:
// 创建四个线程,加上主线程共五个,优先值越小优先级越高
void ThreadPriorityTest()
{
Thread* t1 = new Thread("forkThread1", 1);
printf("-->name=%s,threadId=%d\n",t1->getName(),t1->getThreadId());
t1->Fork(SimpleThread, (void*)1);
Thread* t2 = new Thread("forkThread2", 2);
printf("-->name=%s,threadId=%d\n",t2->getName(),t2->getThreadId());
t2->Fork(SimpleThread, (void*)2);
Thread* t3 = new Thread("forkThread3", 3);
printf("-->name=%s,threadId=%d\n",t3->getName(),t3->getThreadId());
t3->Fork(SimpleThread, (void*)3);
Thread* t4 = new Thread("forkThread4", 4);
printf("-->name=%s,threadId=%d\n",t4->getName(),t4->getThreadId());
t4->Fork(SimpleThread, (void*)4);
currentThread->Yield();
SimpleThread(0);
}
// 运行结果,优先级1最高,最先执行完,其次是优先为2的线程,直到所有线程结束
root@yangyu-ubuntu-32:/mnt/nachos-3.4-Lab/nachos-3.4/threads# ./nachos -q 3
-->name=forkThread1,threadId=1
-->name=forkThread2,threadId=2
-->name=forkThread3,threadId=3
-->name=forkThread4,threadId=4
userId=0,threadId=1,prio=1,loop:0,lastSwitchTick=50,systemTicks=60,usedTicks=10,TimerSlice=30
userId=0,threadId=1,prio=1,loop:1,lastSwitchTick=50,systemTicks=70,usedTicks=20,TimerSlice=30
userId=0,threadId=1,prio=1,loop:2,lastSwitchTick=50,systemTicks=80,usedTicks=30,TimerSlice=30
userId=0,threadId=1,prio=1,loop:3,lastSwitchTick=50,systemTicks=90,usedTicks=40,TimerSlice=30
userId=0,threadId=1,prio=1,loop:4,lastSwitchTick=50,systemTicks=100,usedTicks=50,TimerSlice=30
userId=0,threadId=2,prio=2,loop:0,lastSwitchTick=110,systemTicks=120,usedTicks=10,TimerSlice=30
userId=0,threadId=2,prio=2,loop:1,lastSwitchTick=110,systemTicks=130,usedTicks=20,TimerSlice=30
userId=0,threadId=2,prio=2,loop:2,lastSwitchTick=110,systemTicks=140,usedTicks=30,TimerSlice=30
userId=0,threadId=2,prio=2,loop:3,lastSwitchTick=110,systemTicks=150,usedTicks=40,TimerSlice=30
userId=0,threadId=2,prio=2,loop:4,lastSwitchTick=110,systemTicks=160,usedTicks=50,TimerSlice=30
userId=0,threadId=3,prio=3,loop:0,lastSwitchTick=170,systemTicks=180,usedTicks=10,TimerSlice=30
userId=0,threadId=3,prio=3,loop:1,lastSwitchTick=170,systemTicks=190,usedTicks=20,TimerSlice=30
userId=0,threadId=3,prio=3,loop:2,lastSwitchTick=170,systemTicks=200,usedTicks=30,TimerSlice=30
userId=0,threadId=3,prio=3,loop:3,lastSwitchTick=170,systemTicks=210,usedTicks=40,TimerSlice=30
userId=0,threadId=3,prio=3,loop:4,lastSwitchTick=170,systemTicks=220,usedTicks=50,TimerSlice=30
userId=0,threadId=4,prio=4,loop:0,lastSwitchTick=230,systemTicks=240,usedTicks=10,TimerSlice=30
userId=0,threadId=4,prio=4,loop:1,lastSwitchTick=230,systemTicks=250,usedTicks=20,TimerSlice=30
userId=0,threadId=4,prio=4,loop:2,lastSwitchTick=230,systemTicks=260,usedTicks=30,TimerSlice=30
userId=0,threadId=4,prio=4,loop:3,lastSwitchTick=230,systemTicks=270,usedTicks=40,TimerSlice=30
userId=0,threadId=4,prio=4,loop:4,lastSwitchTick=230,systemTicks=280,usedTicks=50,TimerSlice=30
userId=0,threadId=0,prio=6,loop:0,lastSwitchTick=290,systemTicks=300,usedTicks=10,TimerSlice=30
userId=0,threadId=0,prio=6,loop:1,lastSwitchTick=290,systemTicks=310,usedTicks=20,TimerSlice=30
userId=0,threadId=0,prio=6,loop:2,lastSwitchTick=290,systemTicks=320,usedTicks=30,TimerSlice=30
userId=0,threadId=0,prio=6,loop:3,lastSwitchTick=290,systemTicks=330,usedTicks=40,TimerSlice=30
userId=0,threadId=0,prio=6,loop:4,lastSwitchTick=290,systemTicks=340,usedTicks=50,TimerSlice=30
No threads ready or runnable, and no pending interrupts.
Assuming the program completed.
Machine halting!
Ticks: total 350, idle 0, system 350, user 0
Disk I/O: reads 0, writes 0
Console I/O: reads 0, writes 0
Paging: faults 0
Network I/O: packets received 0, sent 0
Cleaning up...
root@yangyu-ubuntu-32:/mnt/nachos-3.4-Lab/nachos-3.4/threads#
可实现“时间片轮转算法”、“多级队列反馈调度算法”,或将Linux或Windows采用的调度算法应用到Nachos上。
**思路:**nachos启动时在system.cc中会new一个timer类,每隔一个TimerTicks大小触发时钟中断,从而让时钟向前走,时间片的大下定义在stats.h中。同时在stats.h中定义一个时间片大小变量TimerSlice,每个线程运行时间只要大于等于TimerSlice,立即放弃CPU。
stats.h:
……
// nachos执行每条用户指令的时间为1Tick
#define UserTick 1
// 系统态无法进行指令计算,
// 所以nachos系统态的一次中断调用或其他需要进行时间计算的单位设置为10Tick
#define SystemTick 10
// 磁头寻找超过一个扇区的时间
#define RotationTime 500
// 磁头寻找超过一个磁道的时间
#define SeekTime 500
#define ConsoleTime 100 // time to read or write one character
#define NetworkTime 100 // time to send or receive one packet
// 时钟中断间隔
#define TimerTicks 5 // (average) time between timer interrupts
// 时间片轮转算法一个时间片大小
#define TimerSlice 10
……
threadtest.cc:
void SimpleThread(int which)
{
for (int num = 0; num < 5; num++) {
int ticks = stats->systemTicks - scheduler->getLastSwitchTick();
printf("userId=%d,threadId=%d,prio=%d,loop:%d,lastSwitchTick=%d,systemTicks=%d,usedTicks=%d,TimerSlice=%d\n",currentThread->getUserId(),currentThread->getThreadId(),currentThread->getPriority(),num,scheduler->getLastSwitchTick(),stats->systemTicks,ticks,TimerSlice);
// 时间片轮转算法,判断时间片是否用完
// 如果用完主动让出cpu,针对nachos内核线程算法
if(ticks >= TimerSlice){
printf("threadId=%d Yield\n",currentThread->getThreadId());
currentThread->Yield();
}
// 非抢占模式下,多个线程同时执行该接口的话,会交替执行,交替让出cpu
// currentThread->Yield();
}
}
threadtest.cc:
// 创建四个线程,加上主线程共五个,时间片轮转调度策略,不可抢占
void ThreadPriorityTest()
{
Thread* t1 = new Thread("forkThread1", 1);
printf("-->name=%s,threadId=%d\n",t1->getName(),t1->getThreadId());
t1->Fork(SimpleThread, (void*)1);
Thread* t2 = new Thread("forkThread2", 2);
printf("-->name=%s,threadId=%d\n",t2->getName(),t2->getThreadId());
t2->Fork(SimpleThread, (void*)2);
Thread* t3 = new Thread("forkThread3", 3);
printf("-->name=%s,threadId=%d\n",t3->getName(),t3->getThreadId());
t3->Fork(SimpleThread, (void*)3);
Thread* t4 = new Thread("forkThread4", 4);
printf("-->name=%s,threadId=%d\n",t4->getName(),t4->getThreadId());
t4->Fork(SimpleThread, (void*)4);
currentThread->Yield();
SimpleThread(0);
}
// 运行结果,可看到userTicks >= TimserSlice时都让出cpu
// 并且线程执行顺序为1 2 3 4 0,直到结束
root@yangyu-ubuntu-32:/mnt/nachos-3.4-Lab/nachos-3.4/threads#
root@yangyu-ubuntu-32:/mnt/nachos-3.4-Lab/nachos-3.4/threads# ./nachos -q 3
-->name=forkThread1,threadId=1
-->name=forkThread2,threadId=2
-->name=forkThread3,threadId=3
-->name=forkThread4,threadId=4
userId=0,threadId=1,prio=1,loop:0,lastSwitchTick=50,systemTicks=60,usedTicks=10,TimerSlice=10
threadId=1 Yield
userId=0,threadId=2,prio=2,loop:0,lastSwitchTick=60,systemTicks=70,usedTicks=10,TimerSlice=10
threadId=2 Yield
userId=0,threadId=3,prio=3,loop:0,lastSwitchTick=70,systemTicks=80,usedTicks=10,TimerSlice=10
threadId=3 Yield
userId=0,threadId=4,prio=4,loop:0,lastSwitchTick=80,systemTicks=90,usedTicks=10,TimerSlice=10
threadId=4 Yield
userId=0,threadId=0,prio=6,loop:0,lastSwitchTick=90,systemTicks=100,usedTicks=10,TimerSlice=10
threadId=0 Yield
userId=0,threadId=1,prio=1,loop:1,lastSwitchTick=100,systemTicks=110,usedTicks=10,TimerSlice=10
threadId=1 Yield
userId=0,threadId=2,prio=2,loop:1,lastSwitchTick=110,systemTicks=120,usedTicks=10,TimerSlice=10
threadId=2 Yield
userId=0,threadId=3,prio=3,loop:1,lastSwitchTick=120,systemTicks=130,usedTicks=10,TimerSlice=10
threadId=3 Yield
userId=0,threadId=4,prio=4,loop:1,lastSwitchTick=130,systemTicks=140,usedTicks=10,TimerSlice=10
threadId=4 Yield
userId=0,threadId=0,prio=6,loop:1,lastSwitchTick=140,systemTicks=150,usedTicks=10,TimerSlice=10
threadId=0 Yield
userId=0,threadId=1,prio=1,loop:2,lastSwitchTick=150,systemTicks=160,usedTicks=10,TimerSlice=10
threadId=1 Yield
userId=0,threadId=2,prio=2,loop:2,lastSwitchTick=160,systemTicks=170,usedTicks=10,TimerSlice=10
threadId=2 Yield
userId=0,threadId=3,prio=3,loop:2,lastSwitchTick=170,systemTicks=180,usedTicks=10,TimerSlice=10
threadId=3 Yield
userId=0,threadId=4,prio=4,loop:2,lastSwitchTick=180,systemTicks=190,usedTicks=10,TimerSlice=10
threadId=4 Yield
userId=0,threadId=0,prio=6,loop:2,lastSwitchTick=190,systemTicks=200,usedTicks=10,TimerSlice=10
threadId=0 Yield
userId=0,threadId=1,prio=1,loop:3,lastSwitchTick=200,systemTicks=210,usedTicks=10,TimerSlice=10
threadId=1 Yield
userId=0,threadId=2,prio=2,loop:3,lastSwitchTick=210,systemTicks=220,usedTicks=10,TimerSlice=10
threadId=2 Yield
userId=0,threadId=3,prio=3,loop:3,lastSwitchTick=220,systemTicks=230,usedTicks=10,TimerSlice=10
threadId=3 Yield
userId=0,threadId=4,prio=4,loop:3,lastSwitchTick=230,systemTicks=240,usedTicks=10,TimerSlice=10
threadId=4 Yield
userId=0,threadId=0,prio=6,loop:3,lastSwitchTick=240,systemTicks=250,usedTicks=10,TimerSlice=10
threadId=0 Yield
userId=0,threadId=1,prio=1,loop:4,lastSwitchTick=250,systemTicks=260,usedTicks=10,TimerSlice=10
threadId=1 Yield
userId=0,threadId=2,prio=2,loop:4,lastSwitchTick=260,systemTicks=270,usedTicks=10,TimerSlice=10
threadId=2 Yield
userId=0,threadId=3,prio=3,loop:4,lastSwitchTick=270,systemTicks=280,usedTicks=10,TimerSlice=10
threadId=3 Yield
userId=0,threadId=4,prio=4,loop:4,lastSwitchTick=280,systemTicks=290,usedTicks=10,TimerSlice=10
threadId=4 Yield
userId=0,threadId=0,prio=6,loop:4,lastSwitchTick=290,systemTicks=300,usedTicks=10,TimerSlice=10
threadId=0 Yield
No threads ready or runnable, and no pending interrupts.
Assuming the program completed.
Machine halting!
Ticks: total 350, idle 0, system 350, user 0
Disk I/O: reads 0, writes 0
Console I/O: reads 0, writes 0
Paging: faults 0
Network I/O: packets received 0, sent 0
Cleaning up...
root@yangyu-ubuntu-32:/mnt/nachos-3.4-Lab/nachos-3.4/threads#
切换线程过程中,产生段错误,通过定位,误把销毁的线程挂入就绪对了所致。
自己动手实现后,发现时间片轮转算法,线程调度,FIFO,时钟中断等其实并不陌生。一切只要你不懒和肯付出实际行动的难题都是纸老虎。
暂无。
暂无。