【操作系统】GeekOS完成记录(五)project3:调度算法及信号量实现进程间通信

1,586 阅读4分钟

本文正在参加「金石计划」

项目概述

project3本身已有一个时间片轮转调度算法(round robin),我们要实现一个多级反馈调度算法,让geekos要在这两种算法间选择和切换;同时,还要通过信号量来保证进程间的同步。

项目要求

多级反馈队列

image.png

项目中原有一个队列要打造多级队列,就要有多个队列,这里设置为4个,且队列有优先级之分,分别为0-3。其中0的优先级最高。

image.png

总结一下上图,要明确的信息有:

  • 多级反馈队列算法本身如何实现
  • 如何在多级队列和时间片轮转这两种算法间切换
  • 如何得到下一个线程 具体用何种算法对该步是透明的,也就是传入的参数里不要包括具体的调度算法
  • 最后别忘了在系统调用层面加入新添加的函数

接下来我们将一一解决这些问题。

多级反馈队列算法本身如何实现

思路:最高层队列依次向下查找本层队列中最靠近队首的线程,如果找到则不再向下继续查找

        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

image.png

至于文档中提到的MAX_TICKS,我并没有找到,而是看到一个和它有点类似的值,但感觉没必要改。

image.png image.png

信号量

信号量(semaphore)涉及我们经常提到的P-V操作。

为什么会有这样一对名词呢?其实它们是由迪杰斯特拉在论文中所使用的词,我曾在《现代操作系统》一书中看到过对这对词的解释:

image.png

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);  /* 连接信号链表的指针域 */
};

接下来我们看看项目关于信号量的要求:

  • 创建

image.png

由此,我们可以写出如下代码:

/* 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;
}
  • 销毁

image.png

/*
 * 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);
}

image.png

  • V操作 V操作同理,只是return的内容换成了V(sid)。

以下是V(sid)的内容:

image.png

计时器

照着文档提示可以很顺利地完成任务: image.png

static int Sys_GetTimeOfDay(struct Interrupt_State *state)
{
    // TODO("GetTimeOfDay system call");
    return g_numTicks;
}

image.png 上图介绍了Get_Time_Of_Day()的用法,比如,在long.c中,就用到了这种方式: image.png

同时,它也指出,通过这种方式计算的时间,是会把中途其他进程抢占cpu的执行时间算在内的(也就是墙钟时间或虚拟时间)

代码测试及验证

image.png

其中第二个参数是所使用的调度算法,第三个参数是时间片长度,这里就不贴图了,读者可以自行测试和验证。


补充:关于信号量部分的测试与验证:

  • 输入semtest1,这是一个合法的创建信号量请求,所以会看到成功的提示;
  • 输入semtest2,这是一个不合法的创建信号量请求,但是操作系统能抛出异常并及时终止。