本文正在参加「金石计划」
项目概述
project3本身已有一个时间片轮转调度算法(round robin),我们要实现一个多级反馈调度算法,让geekos要在这两种算法间选择和切换;同时,还要通过信号量来保证进程间的同步。
项目要求
多级反馈队列
项目中原有一个队列要打造多级队列,就要有多个队列,这里设置为4个,且队列有优先级之分,分别为0-3。其中0的优先级最高。
总结一下上图,要明确的信息有:
- 多级反馈队列算法本身如何实现
- 如何在多级队列和时间片轮转这两种算法间切换
- 如何得到下一个线程 具体用何种算法对该步是透明的,也就是传入的参数里不要包括具体的调度算法
- 最后别忘了在系统调用层面加入新添加的函数
接下来我们将一一解决这些问题。
多级反馈队列算法本身如何实现
思路:最高层队列依次向下查找本层队列中最靠近队首的线程,如果找到则不再向下继续查找
int i;
for (i = 0; i < MAX_QUEUE_LEVEL; i++)
{
best = Get_Front_Of_Thread_Queue(&s_runQueue[i]);
if (best != NULL)
{
Remove_Thread(&s_runQueue[i], best);
break;
}
}
在多级队列和时间片轮转这两种算法间切换
思路:
- 在kthread.c下设置一个全局变量,此处设为g_schedulingPolicy。如果传入的策略值和当前的不一致,说明需要切换;之后再判断具体是要切换成哪一种,切换好后重新赋值。
- RR算法转换到多级队列算法可以看做是队列的物理结构不变,只是前者的线程都是在某一个队列中(比如Q0),而后者的线程则分散在Q0~Q3的队列中,有访问优先级之分。
代码实现:
int Chang_Scheduling_Policy(int policy, int quantum)
{
if (policy != g_schedulingPolicy)
{
/* MLF -> RR */
if (policy == ROUND_ROBIN)
{
int i;
for (i = MAX_QUEUE_LEVEL - 1; i > 0; i--)
Append_Thread_Queue(&s_runQueue[i - 1], &s_runQueue[i]);
}
/* RR -> MLF */
else
{
if (Is_Member_Of_Thread_Queue(&s_runQueue[0], IdleThread))
{
Remove_Thread(&s_runQueue[0], IdleThread);
Enqueue_Thread(&s_runQueue[MAX_QUEUE_LEVEL - 1], IdleThread);
}
}
g_schedulingPolicy = policy;
Print("g_schedulingPolicy = %d\n", g_schedulingPolicy);
}
g_Quantum = quantum;
Print("g_Quantum = %d\n", g_Quantum);
return 0;
}
得到下一个线程
虽然不能通过参数获得调度算法的类型,但是依然是要对RR和多级调度分情况讨论的,而获取状态的方式就是通过上一小点提到的g_schedulingPolicy这个全局变量。
具体的代码如下:它会用到上上小点提到的多级队列算法本身的实现,我们把那个函数命名为MultiBack()
struct Kernel_Thread *Get_Next_Runnable(void)
{
/* Find the best thread from the highest-priority run queue */
// TODO("Find a runnable thread from run queues");
KASSERT(g_schedulingPolicy == ROUND_ROBIN ||
g_schedulingPolicy == MULTILEVEL_FEEDBACK);
/* 查找下一个被调度的线程 */
struct Kernel_Thread *best = NULL;
if (g_schedulingPolicy == ROUND_ROBIN)
{
best = Find_Best(&s_runQueue[0]);
if (best != NULL)
Remove_Thread(&s_runQueue[0], best);
}
else
{
MultiBack(best);
}
/* 若没有可执行进程,则寻找空闲线程 */
KASSERT(best != NULL);
return best;
}
系统调用
- 在syscall.c的列表中添加Sys_SetSchedulingPolicy()
- 完成边界处理并调用kthread.c中的Chang_Scheduling_Policy()
static int Sys_SetSchedulingPolicy(struct Interrupt_State *state)
{
// TODO("SetSchedulingPolicy system call");
/* 调度类型必须为0或1,0表示默认的RR,1表示多级反馈 */
if (state->ebx != ROUND_ROBIN && state->ebx != MULTILEVEL_FEEDBACK)
{
Print("Error! Scheduling Policy should be RR or MLF\n");
return -1;
}
/* 时间片必须在1~TICKS_PER_SEC(100)之间 */
if (state->ecx < 1 || state->ecx > TICKS_PER_SEC)
{
Print("Error! Quantum should be in the range of [1, 100]\n");
return -1;
}
int res = Chang_Scheduling_Policy(state->ebx, state->ecx);
return res;
}
至于为什么TICKS_PER_SEC的值被设为100,是因为在timer.c中:
初始值为18,我结合注释改成了100
至于文档中提到的MAX_TICKS,我并没有找到,而是看到一个和它有点类似的值,但感觉没必要改。
信号量
信号量(semaphore)涉及我们经常提到的P-V操作。
为什么会有这样一对名词呢?其实它们是由迪杰斯特拉在论文中所使用的词,我曾在《现代操作系统》一书中看到过对这对词的解释:
P、V是荷兰语的用法;
- p操作代表数量的减少(down)、或者sleep状态;
- v则是代表数量的增加、或者wake up 状态
接下来我们再来看看信号量的组成:
//synch.h
struct Semaphore
{
int semaphoreID;
char semaphoreName[MAX_SEMAPHORE_NAME + 1]; /*+1的原因:字符数组以'\0'结尾 */
int value;
int registeredThreadCount;
struct Kernel_Thread *registeredThreads[MAX_REGISTERED_THREADS];
struct Thread_Queue waitingThreads;
DEFINE_LINK(Semaphore_List, Semaphore); /* 连接信号链表的指针域 */
};
接下来我们看看项目关于信号量的要求:
- 创建
由此,我们可以写出如下代码:
/* Create a semaphore.
* Params:
* state->ebx - user address of name of semaphore
* state->ecx - length of semaphore name
* state->edx - initial semaphore count
* Returns: the global semaphore id
*/
static int Sys_CreateSemaphore(struct Interrupt_State *state)
{
// TODO("CreateSemaphore system call");
int res;
ulong_t userAddr = state->ebx;
ulong_t nameLen = state->ecx;
ulong_t initCount = state->edx;
if (nameLen <= 0 || initCount < 0 || nameLen > MAX_SEMAPHORE_NAME)
{
Print("Error! Semaphore Params incorrect\n");
/*#define EINVALID -12*/
return EINVALID;
}
char *semName = NULL;
/* 从用户空间复制信号量名字符串到内核空间 */
res = Copy_User_String(userAddr, nameLen, MAX_SEMAPHORE_NAME, &semName);
if (res != 0)
{
Print("Error! Cannot copy string from user spcce\n");
return res;
}
/* 判断信号量名的合法性 */
if (strnlen(semName, MAX_SEMAPHORE_NAME) != nameLen)
{
Print("Error! Semaphore Name is Invalid\n");
return EINVALID;
}
/* 创建一个信号量 */
res = Create_Semaphore(semName, nameLen, initCount);
return res;
}
- 销毁
/*
* Destroy a semaphore.
* Params:
* state->ebx - the semaphore id
*
* Returns: 0 if successful, error code (< 0) if unsuccessful
*/
static int Sys_DestroySemaphore(struct Interrupt_State *state)
{
// TODO("DestroySemaphore system call");
int sid = state->ebx;
if (sid <= 0)
{
Print("Error! Semaphore ID is Invalid\n");
return EINVALID;
}
return Destroy_Semaphore(state->ebx);
}
- P操作 其实在geekos中,Sys_P()只是调用P操作,而真正的P操作不需要我们实现:
static int Sys_P(struct Interrupt_State *state)
{
// TODO("P (semaphore acquire) system call");
int sid = state->ebx;
if (sid <= 0)
{
Print("Error! Semaphore ID is Invalid\n");
return EINVALID;
}
return P(sid);
}
- V操作 V操作同理,只是return的内容换成了V(sid)。
以下是V(sid)的内容:
计时器
照着文档提示可以很顺利地完成任务:
static int Sys_GetTimeOfDay(struct Interrupt_State *state)
{
// TODO("GetTimeOfDay system call");
return g_numTicks;
}
上图介绍了
Get_Time_Of_Day()的用法,比如,在long.c中,就用到了这种方式:
同时,它也指出,通过这种方式计算的时间,是会把中途其他进程抢占cpu的执行时间算在内的(也就是墙钟时间或虚拟时间)
代码测试及验证
其中第二个参数是所使用的调度算法,第三个参数是时间片长度,这里就不贴图了,读者可以自行测试和验证。
补充:关于信号量部分的测试与验证:
- 输入
semtest1,这是一个合法的创建信号量请求,所以会看到成功的提示; - 输入
semtest2,这是一个不合法的创建信号量请求,但是操作系统能抛出异常并及时终止。