调度程序的设计 | 青训营笔记

109 阅读4分钟

这是我参与「第四届青训营 」笔记创作活动的第5天。

本次实践的目的是实现进程调度,实现多进程同时运行,实现一个自己设计的调度程序。具体内容是实现fork,exit,sleep等库函数和它们对应的系统调用。

前置问题

  1. 请简单说说,如果我们想做虚拟内存管理,可以如何进行设计?

    虚拟空间分为如下几个段:文本段、初始化数据、未初始化数据、栈、堆、内核段,除了堆内存外,其它段落空间都是自动填充分配的,用户无法控制这些内存的使用。通过段选择子查找段描述符,在保护模式下 physical address = base address + offset

  2. 假如我们的系统中预留了100个进程,系统中运行了50个进程,其中某些结束了运行。这时我们又有一个进程想要开始运行(即需要分配给它一个空闲的PCB),那么如何能够以O(1)的时间和O(n)的空间快速地找到这样一个空闲PCB呢?

    建立一个队列queue[MAX_PCB_NUM]用来存储空闲PCB的编号,若一个进程想要开始运行,则将一个空闲PCB的编号i出队列,把PCB[i]分配给需要的进程;若一个进程结束运行,所在PCB空闲,则将其对应的PCB编号进队列。

  3. 为什么不同用户进程需要对应不同的内核堆栈?

    因为进程内核栈除了需要保存内核空间过程调用外,还需要保存用户空间栈的数据和返回地址,以便在返回用户空间继续执行。如果不同用户进程需要对应相同的内核堆栈,处于就绪态的进程保存的现场信息可能会被覆盖,导致信息丢失,无法返回用户空间继续执行。

  4. stackTop有什么用?为什么一些地方要取地址赋值给stackTop

    stackTop保存该进程对应内核栈栈顶地址,在一些地方取地址赋值给stackTop便于在进程切换时将esp切换到即将进行进程的内核栈栈顶。

  5. 在中断嵌套发生时,系统是如何运行的?

    系统判断当前中断处理程序是否能够响应中断及新中断事件优先级,若当前中断处理程序能够响应中断并且到来新中断优先级更高则保护被中断的中断处理程序现场,将信息压栈,然后转向处理新中断事件的中断处理程序,以便于在处理结束时可以返回原来的中断处理程序继续进行;否则不允许中断嵌套。在处理完新中断事件后,将被中断的中断处理程序的信息弹栈,继续处理。

  6. 线程为什么要以函数为粒度来执行?

    更大的粒度是进程,更小的粒度是语句块。之间共享同一进程的全局变量和堆区,但是不共享栈区,pc和寄存器。若以进程为粒度执行,共享全局变量时栈区,pc和寄存器等不可避免的被共享;若以语句块为粒度进行,无法实现全局变量的共享。而以函数为粒度来进行,函数的调用和返回实际上就是栈帧的创建和释放,共享同一进程的全局变量和堆区,但是不共享栈区,pc和寄存器是可以实现的。因此线程要以函数为粒度来执行。

调用 syscall 完善库函数

  • 完成fork

    pid_t fork()
    {
        /*TODO:call syscall*/
        return syscall(SYS_FORK, 0, 0, 0, 0, 0);
    }
    
  • 完成exec

    int exec(uint32_t sec_start, uint32_t sec_num)
    {
        /*TODO:call syscall*/
        return syscall(SYS_EXEC, sec_start, sec_num, 0, 0, 0);
    }
    
  • 完成sleep

    int sleep(uint32_t time)
    {
        /*TODO:call syscall*/
        return syscall(SYS_SLEEP, time, 0, 0, 0, 0);
    }
    
  • 完成exit

    int exit()
    {
        /*TODO:call syscall*/
        return syscall(SYS_EXIT, 0, 0, 0, 0, 0);
    }
    

完成时钟中断处理函数

void timerHandle(struct StackFrame *sf)
{
    // TODO 完成进程调度,建议使用时间片轮转,按顺序调度
    for (int i = 0; i < MAX_PCB_NUM; i++)
        if (pcb[i].state == STATE_BLOCKED)
        {
            pcb[i].sleepTime--;
            if (pcb[i].sleepTime == 0)
                pcb[i].state = STATE_RUNNABLE;
        }
    // 遍历pcb,将状态为STATE_BLOCKED的进程的sleepTime减一,如果进程的sleepTime变为0,重新设为STATE_RUNNABLE
    pcb[current].timeCount++;
    if (pcb[current].timeCount >= MAX_TIME_COUNT || pcb[current].state != STATE_RUNNING)
    {
        if (pcb[current].state == STATE_RUNNING)
            pcb[current].state = STATE_RUNNABLE;
        pcb[current].timeCount = 0;
        int i = (current + 1) % MAX_PCB_NUM;
        for (; i != current; i = (i + 1) % MAX_PCB_NUM)
            if (pcb[i].state == STATE_RUNNABLE)
                break;
        current = i;
        pcb[current].state = STATE_RUNNING;
        // 如果时间片用完(timeCount==MAX_TIME_COUNT)且有其它状态为STATE_RUNNABLE的进程,切换,否则继续执行当前进程
        uint32_t tmpStackTop = pcb[current].stackTop;
        pcb[current].stackTop = pcb[current].prevStackTop;
        pcb[current].state = STATE_RUNNING;
        tss.esp0 = (uint32_t) & (pcb[current].stackTop);
        asm volatile("movl %0, %%esp" ::"m"(tmpStackTop));
        asm volatile("popl %gs");
        asm volatile("popl %fs");
        asm volatile("popl %es");
        asm volatile("popl %ds");
        asm volatile("popal");
        asm volatile("addl $8, %esp");
        asm volatile("iret");
        // 进程切换
    }
}

完成系统调用处理函数

  • syscallFork

    void syscallFork(struct StackFrame *sf)
    {
        // TODO 完善它
    ​
        // TODO 查找空闲pcb,如果没有就返回-1
        int i = 0;
        for (; i < MAX_PCB_NUM; i++)
            if (pcb[i].state == STATE_DEAD)
                break;
        if (i == MAX_PCB_NUM)
        {
            pcb[current].regs.eax = -1;
            return;
        }
        // TODO 拷贝地址空间
        memcpy((void *)((i + 1) * 0x100000), (void *)((current + 1) * 0x100000), 0x100000);
        // 拷贝pcb,这部分代码给出了,请注意理解
        memcpy(&pcb[i], &pcb[current], sizeof(ProcessTable));
    ​
        pcb[i].regs.eax = 0;
        pcb[i].regs.cs = USEL(1 + i * 2);
        pcb[i].regs.ds = USEL(2 + i * 2);
        pcb[i].regs.es = USEL(2 + i * 2);
        pcb[i].regs.fs = USEL(2 + i * 2);
        pcb[i].regs.gs = USEL(2 + i * 2);
        pcb[i].regs.ss = USEL(2 + i * 2);
        pcb[i].stackTop = (uint32_t) & (pcb[i].regs);
        pcb[i].prevStackTop = (uint32_t) & (pcb[i].stackTop);
        pcb[i].state = STATE_RUNNABLE;
        pcb[i].timeCount = 0;
        pcb[i].sleepTime = 0;
    }
    
  • syscallExec

    void syscallExec(struct StackFrame *sf)
    {
        // TODO 完成exec
        // hint: 用loadelf,已经封装好了
        // 调用loadelf把新的程序加载到当前用户进程的地址空间,然后返回0即可(用eax装返回值)
        uint32_t entry = 0;
        uint32_t secstart = sf->ecx;
        uint32_t secnum = sf->edx;
        loadelf(secstart, secnum, (current + 1) * 0x100000, &entry);
        pcb[current].regs.eip = entry;
        pcb[current].regs.eax = 0;
    }
    
  • syscallSleep

    void syscallSleep(struct StackFrame *sf)
    {
        // TODO:实现它
        // 如果传入参数合法,将当前的进程的sleepTime设置为传入的参数,将当前进程的状态设置为STATE_BLOCKED
        if (sf->ecx != 0)
        {
            pcb[current].sleepTime = sf->ecx;
            pcb[current].state = STATE_BLOCKED;
            pcb[current].timeCount = MAX_TIME_COUNT;
            asm volatile("int $0x20");
        }
    }
    
  • syscallExit

    void syscallExit(struct StackFrame *sf)
    {
        // TODO 先设置成dead,然后用int 0x20进入调度
        // 将当前进程的状态设置为STATE_DEAD,然后模拟时钟中断进行进程切换。
        pcb[current].state = STATE_DEAD;
        asm volatile("int $0x20");
    }
    

\