1.概述
从某种程度上,我们可以把控制流看作是一个内核线程。内核线程是一种特殊的进程,内核线程与用户进程的区别有两个:内核线程只运行在内核态而用户进程会在用户态和内核态交替运行;所有内核线程直接使用共同的ucore内核内存空间,不需为每个内核线程维护单独的内存空间而用户进程需要维护各自的用户内存空间。
从内存空间占用情况这个角度上看,我们可以把线程看作是一种共享内存空间的轻量级进程。
为了实现内核线程,需要设计管理线程的数据结构,即进程控制块(也叫做线程控制块)。如果要让内核线程运行,我们首先要创建内核线程对应的进程控制块,还需把这些进程控制块通过链表链接在一起,便于随时进行插入,删除和查找操作等进程管理事务。这个链表就是进程控制块链表。然后再通过调度器来让不同的内核线程在不同的时间段占用CPU执行,实现对CPU的分时共享。
2.设计关键数据结构--进程控制块
struct proc_struct {
enum proc_state state; // Process state
int pid; // Process ID
int runs; // the running times of Proces
uintptr_t kstack; // Process kernel stack
volatile bool need_resched; // bool value: need to be rescheduled to release CPU?
struct proc_struct *parent; // the parent process
struct mm_struct *mm; // Process's memory management field
struct context context; // Switch here to run process
struct trapframe *tf; // Trap frame for current interrupt
uintptr_t cr3; // CR3 register: the base addr of Page Directroy Table(PDT)
uint32_t flags; // Process flag
char name[PROC_NAME_LEN + 1]; // Process name
list_entry_t list_link; // Process link list
list_entry_t hash_link; // Process hash list
};
state:进程所处的状态。
parent:用户进程的父进程(创建它的进程)。
context:进程的上下文,用于进程切换。
tf:中断帧的指针,总是指向内核栈的某个位置:当进程从用户空间跳到内核空间时,中断帧记录了进程在被中断前的状态。
cr3:cr3保存页表的物理地址,目的就是切换的时候方便直接使用lcr3实现页表切换,避免每次都根据mm来计算cr3.
kstack:每个线程都有一个内核栈,并且位于内核地址空间的不同位置。对于内核线程,该栈就是运行时的程序使用的栈;而对于普通进程,该栈是发生特权级改变的时候使保存被打断的硬件信息用的栈。kstack记录了分配给该进程/线程的内核栈的位置。
3.创建并执行内核进程
建立进程控制块后,现在就可以通过进程控制块来创建具体的进程了。
(1)创建第0个内核线程idleproc
(2)创建第1个内核线程initproc
第0个内核线程主要工作是完成内核中各个子系统的初始化,然后就通过执行函数开始过退休生活了。
通过kernel_thread函数创建一个内核线程。
// kernel_thread - create a kernel thread using "fn" function
// NOTE: the contents of temp trapframe tf will be copied to
// proc->tf in do_fork-->copy_thread function
int
kernel_thread(int (*fn)(void *), void *arg, uint32_t clone_flags) {
struct trapframe tf;
memset(&tf, 0, sizeof(struct trapframe));
tf.tf_cs = KERNEL_CS;
tf.tf_ds = tf.tf_es = tf.tf_ss = KERNEL_DS;
tf.tf_regs.reg_ebx = (uint32_t)fn;
tf.tf_regs.reg_edx = (uint32_t)arg;
tf.tf_eip = (uint32_t)kernel_thread_entry;
return do_fork(clone_flags | CLONE_VM, 0, &tf);
}
该函数采用了局部变量tf来放置保存内核线程的临时中断帧,并把中断帧的指针传递给do_fork函数,然后do_fork会调用copy_thread函数来在新创建的进程内核栈上专门给进程的中断帧分配一块空间。
do_fork是创建线程的主要函数。kernel_thread函数通过调用do_fork函数最终完成了内核线程的创建工作。do_fork函数主要做了以下6件事情:
(1)分配并初始化进程控制块;
(2)分配并初始化内核栈;
(3)根据clone_flag标志复制或共享进程内存管理结构;
(4)设置进程在内核正常运行和调度所需的中断帧和执行上下文;
(5)把设置好的进程控制块放入hash_list和proc_list两个全局进程链表中;
(6)自此,进程已经准备好执行了,把进程状态设置为“就绪”态;
(7)设置返回码为子进程的id号。
(3)调度并执行内核线程initproc