在这一小节中,我们来看一看如何进行进程模块的初始化。
进程初始化的函数定义在文件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);
}
我们将寄存器s0
和s1
分别设置为需要进程执行的函数和相关参数列表,之后设置了status
寄存器使得进程切换后处于中断使能的状态。我们还设置了epc
使其指向kernel_thread_entry
,这是进程执行的入口函数。最后,调用do_fork
函数把当前的进程复制一份。
do_fork
函数内部主要进行了如下操作:
分配并初始化进程控制块(alloc_proc
函数)
根据clone_flags
决定是复制还是共享内存管理系统(copy_mm
函数)
设置进程的中断帧和上下文(copy_thread
函数)
如果执行失败,则需要调用相应的错误处理函数释放空间。更多的实现细节可以参考代码,在练习中也会有更多的涉及。
在这里我们需要尤其关注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是如何完成进程切换的。