进程模块初始化

在这一小节中,我们来看一看如何进行进程模块的初始化。

进程初始化的函数定义在文件kern/process/proc.c中的proc_init。进程模块的初始化主要分为两步,首先创建第0个内核进程,idle

// kern/process/proc.c
// proc_init - set up the first kernel thread idleproc "idle" by itself and 
//           - create the second kernel thread init_main
void
proc_init(void) {
    int i;

    list_init(&proc_list);//进程链表
    for (i = 0; i < HASH_LIST_SIZE; i ++) {
        list_init(hash_list + i);
    }

    if ((idleproc = alloc_proc()) == NULL) { //分配"第0个"进程 idle
        panic("cannot alloc idleproc.\n");
    }

    idleproc->pid = 0;
    idleproc->state = PROC_RUNNABLE;
    idleproc->kstack = (uintptr_t)bootstack;
    idleproc->need_resched = 1;
    set_proc_name(idleproc, "idle");
    nr_process ++;
    //全局变量current保存当前正在执行的进程
    current = idleproc;

    int pid = kernel_thread(init_main, "Hello world!!", 0);
    if (pid <= 0) {
        panic("create init_main failed.\n");
    }

    initproc = find_proc(pid);
    set_proc_name(initproc, "init");

    assert(idleproc != NULL && idleproc->pid == 0);
    assert(initproc != NULL && initproc->pid == 1);
}

在进程模块初始化时,首先需要初始化进程链表。进程链表就是把所有进程控制块串联起来的数据结构,可以记录和追踪每一个进程。然后,调用proc_alloc函数来为第一个进程分配其进程控制块。当我们的操作系统开始运行的时候,其实它已经可以被视作一个进程了。但是我们还没有为他设计好进程控制块,也就没法进行管理。proc_alloc函数会使用kmalloc分配一段空间来保存进程控制块,并且设定一些初值告诉我们这个进程目前还在初始化中。

在分配完空间后,我们对于idle进程的控制块进行一定的初始化:

idleproc->pid = 0;
idleproc->state = PROC_RUNNABLE;
idleproc->kstack = (uintptr_t)bootstack;
idleproc->need_resched = 1;
set_proc_name(idleproc, "idle");
nr_process ++;

从这里开始,idle进程具有了合法的进程编号,0。我们把idle进程的状态设置为RUNNABLE,表示其可以执行。因为这是第一个内核进程,所以我们可以直接将ucore的启动栈分配给他。需要注意的是,后面再分配新进程时我们需要为其分配一个栈,而不能再使用启动栈了。我们再把idle进程标志为需要调度,这样一旦idle进程开始执行,马上就可以让调度器调度另一个进程进行执行。

接下来我们对于第一个真正的内核进程进行初始化(因为idle进程仅仅算是“继承了”ucore的运行)。我们的目标是使用新的内核进程进行一下内核初始化的工作,但在这章我们先仅仅让它输出一个Hello World,证明我们的内核进程实现的没有问题。下面是创建内核进程的代码:

int
kernel_thread(int (*fn)(void *), void *arg, uint32_t clone_flags) {
    struct trapframe tf;
    memset(&tf, 0, sizeof(struct trapframe));

    tf.gpr.s0 = (uintptr_t)fn;
    tf.gpr.s1 = (uintptr_t)arg;
    tf.status = (read_csr(sstatus) | SSTATUS_SPP | SSTATUS_SPIE) & ~SSTATUS_SIE;
    tf.epc = (uintptr_t)kernel_thread_entry;
    return do_fork(clone_flags | CLONE_VM, 0, &tf);
}

我们将寄存器s0s1分别设置为需要进程执行的函数和相关参数列表,之后设置了status寄存器使得进程切换后处于中断使能的状态。我们还设置了epc使其指向kernel_thread_entry,这是进程执行的入口函数。最后,调用do_fork函数把当前的进程复制一份。

do_fork函数内部主要进行了如下操作:

  1. 分配并初始化进程控制块(alloc_proc函数)

  2. 分配并初始化内核栈(setup_stack函数)

  3. 根据clone_flags决定是复制还是共享内存管理系统(copy_mm函数)

  4. 设置进程的中断帧和上下文(copy_thread函数)

  5. 把设置好的进程加入链表

  6. 将新建的进程设为就绪态

  7. 将返回值设为线程id

如果执行失败,则需要调用相应的错误处理函数释放空间。更多的实现细节可以参考代码,在练习中也会有更多的涉及。

在这里我们需要尤其关注copy_thread函数:

static void
copy_thread(struct proc_struct *proc, uintptr_t esp, struct trapframe *tf) {
    proc->tf = (struct trapframe *)(proc->kstack + KSTACKSIZE - sizeof(struct trapframe));
    *(proc->tf) = *tf;

    // Set a0 to 0 so a child process knows it's just forked
    proc->tf->gpr.a0 = 0;
    proc->tf->gpr.sp = (esp == 0) ? (uintptr_t)proc->tf : esp;

    proc->context.ra = (uintptr_t)forkret;
    proc->context.sp = (uintptr_t)(proc->tf);
}

在这里我们首先在上面分配的内核栈上分配出一片空间来保存trapframe。然后,我们将trapframe中的a0寄存器(返回值)设置为0,说明这个进程是一个子进程。之后我们将上下文中的ra设置为了forkret函数的入口,并且把trapframe放在上下文的栈顶。在下一个小节,我们会看到这么做之后ucore是如何完成进程切换的。

最后更新于