进程切换

在这一节中我们来看一看进程切换的全过程。

在ucore完成初始化后,ucore内核就没事做了,于是进入了“idle”的状态(这也是我们给第0个进程起名叫idle的原因)。这是,它会陷入死循环,不断检查自己是否需要调度:

void
cpu_idle(void) {
    while (1) {
        if (current->need_resched) {
            schedule();
            ......

因为我们之前在初始化中把idle进程的need_resched设为了1,所以其总会调用schedule函数来检查是否有进程可以调度。我们已经初始化了另外一个内核进程,所以调度器发现了这个进程,并且准备调度到这个进程。

我们实现的schedule函数非常的简单:当需要调度的时候,把当前的进程放在队尾,从队列中取出第一个可以运行的进程,切换到它运行。这就是FIFO调度算法。schedule函数会调用proc_run来唤醒选定的进程。proc_run函数内部如下:

void
proc_run(struct proc_struct *proc) {
    if (proc != current) {
        bool intr_flag;
        struct proc_struct *prev = current, *next = proc;
        local_intr_save(intr_flag);
        {
            current = proc;
            lcr3(next->cr3);
            switch_to(&(prev->context), &(next->context));
        }
        local_intr_restore(intr_flag);
    }
}

函数中主要进行了三个操作:

  1. 将当前运行的进程设置为要切换过去的进程

  2. 将页表换成新进程的页表

  3. 使用switch_to切换到新进程

switch_to函数如下:

.text
# void switch_to(struct proc_struct* from, struct proc_struct* to)
.globl switch_to
switch_to:
    # save from's registers
    STORE ra, 0*REGBYTES(a0)
    STORE sp, 1*REGBYTES(a0)
    STORE s0, 2*REGBYTES(a0)
    STORE s1, 3*REGBYTES(a0)
    STORE s2, 4*REGBYTES(a0)
    STORE s3, 5*REGBYTES(a0)
    STORE s4, 6*REGBYTES(a0)
    STORE s5, 7*REGBYTES(a0)
    STORE s6, 8*REGBYTES(a0)
    STORE s7, 9*REGBYTES(a0)
    STORE s8, 10*REGBYTES(a0)
    STORE s9, 11*REGBYTES(a0)
    STORE s10, 12*REGBYTES(a0)
    STORE s11, 13*REGBYTES(a0)

    # restore to's registers
    LOAD ra, 0*REGBYTES(a1)
    LOAD sp, 1*REGBYTES(a1)
    LOAD s0, 2*REGBYTES(a1)
    LOAD s1, 3*REGBYTES(a1)
    LOAD s2, 4*REGBYTES(a1)
    LOAD s3, 5*REGBYTES(a1)
    LOAD s4, 6*REGBYTES(a1)
    LOAD s5, 7*REGBYTES(a1)
    LOAD s6, 8*REGBYTES(a1)
    LOAD s7, 9*REGBYTES(a1)
    LOAD s8, 10*REGBYTES(a1)
    LOAD s9, 11*REGBYTES(a1)
    LOAD s10, 12*REGBYTES(a1)
    LOAD s11, 13*REGBYTES(a1)

    ret

可以看出来这段代码就是将需要保存的寄存器进行保存和调换。在之前我们也已经谈到过了,这里只需要调换被调用者保存寄存器即可。由于我们在初始化时把上下文的ra寄存器设定成了forkret函数的入口,所以这里会返回到forkret函数,进一步进入到forkretsforkrets函数很短:

    .globl forkrets
forkrets:
    # set stack to this new process's trapframe
    move sp, a0
    j __trapret

这里把传进来的参数,也就是进程的中断帧放在了sp,这样在__trapret中就可以直接从中断帧里面恢复所有的寄存器啦!我们在初始化的时候对于中断帧做了一点手脚,epc寄存器指向的是kernel_thread_entrys0寄存器里放的是新进程要执行的函数,s1寄存器里放的是传给函数的参数。在kernel_thread_entry函数中:

.text
.globl kernel_thread_entry
kernel_thread_entry:        # void kernel_thread(void)
    move a0, s1
    jalr s0

    jal do_exit

我们把参数放在了a0寄存器,并跳转到s0执行我们指定的函数!这样,一个进程的初始化就完成了。至此,我们实现了基本的进程管理,并且成功创建并切换到了我们的第一个内核进程。

lab4的代码可以在这里找到

最后更新于