内核线程管理
实验执行流程 概述
lab2和lab3完成了对内存的虚拟化 ,但整个控制留还是一条线串行执行。lab4将在此基础上进行CPU的虚拟化,即让ucore实现分时共享CPU,实现多条控制流能够并发执行。
**从某种程度看,我们可以把控制流看做是一个内核线程。**内核线程是一种特殊的进程,内核线程和用户进程的区别有两个:
- 内核线程只运行在内核态,而用户进程会在用户态和内核态交替运行
- 所有内核线程直接使用共同的ucore内核的内存空间,不需要为每个内核线程维护单独的内存空间,而用户进程需要维护各自的用户内存空间
因此,从内存空间占用情况这个角度上看,我们可以把线程看做是一种共享内存空间的轻量级进程。
为了实现内核线程,需要设计管理线程的数据结构,包括进程控制块(在这里也可以叫做线程控制块)、进程控制块链表、调度器(scheduler)等。
在kern_init函数中,当完成虚拟内存的初始化工作后,就可以调用proc_init函数,这个函数完成了idleproc内核线程和initproc内核线程的复制或创建工作。
idleproc内核线程的工作就是不停的查询,看是否有其他内核线程可以执行了。如果有,马上让调度器选择那个内核线程执行。所有idleproc内核线程是在ucore操作系统没有其他内核线程可执行的情况下才会被调用。接着就是调用kernel_thread函数来创建initproc内核线程。initproc内核线程的工作就是显示“Hello World”,表明自己存在且能正常工作了。
设计关键数据结构 – 进程控制块
在lab4中,进程管理信息用struct proc_struct来表示。在kern/process/proc.h中定义如下:
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
};
其中的几个比较重要的成员变量如下:
- mm:内存管理的信息,包括内存映射列表、页表指针等。mm成员变量在lab3中用于虚存管理。但在实际OS中,内核线程常驻内存,因此不需要考虑页面换入换出的swap page问题,在lab5中涉及到了用户进程,才考虑进程用户内存空间的swap page问题,mm才会发挥作用。所有在lab4中mm对于内核线程就没有用了,在内核线程的proc_struct的成员变量mm = NULL。mm里有个很重要的项pgdir,记录的是该进程使用的一级页表的物理地址。由于mm = NULL,所以在proc_struct数据结构中需要有一个代替pgdir项来记录页表起始地址,这就是proc_struct数据结构中的cr3成员变量。
// the control struct for a set of vma using the same PDT
struct mm_struct {
list_entry_t mmap_list; // linear list link which sorted by start addr of vma
struct vma_struct *mmap_cache; // current accessed vma, used for speed purpose
pde_t *pgdir; // the PDT of these vma
int map_count; // the count of these vma
void *sm_priv; // the private data for swap manager
};
- state:该进程所处的状态。
// process's state in his life cycle
enum proc_state {
PROC_UNINIT = 0, // uninitialized
PROC_SLEEPING, // sleeping
PROC_RUNNABLE, // runnable(maybe running)
PROC_ZOMBIE, // almost dead, and wait parent proc to reclaim his resource
};
- parent:指向用户进程的父进程(创建它的进程)的指针。在所有进程中,只有一个进程没有父进程,就是内核创建的第一个内核线程idleproc。内核根据这个父子关系建立一个树形结构,用于维护一些特殊操作,例如确定某个进程是否可以对另外一个进程进行某种操作等等。
- context:进程的上下文,用于进程切换(参见switch.S)。在ucore中,所有的进程在内核中也是相对独立的(例如独立的内核堆栈以及上下文等)。使用context保存寄存器的目的就在于在内核态中能够进行上下文之间的切换。实际利用context进行上下文切换的函数是在kern/process/switch.S中定义的switch_to。
// Saved registers for kernel context switches.
// Don't need to save all the %fs etc. segment registers,
// because they are constant across kernel contexts.
// Save all the regular registers so we don't need to care
// which are caller save, but not the return register %eax.
// (Not saving %eax just simplifies the switching code.)
// The layout of context must match code in switch.S.
struct context {
uint32_t eip;
uint32_t esp;
uint32_t ebx;
uint32_t ecx;
uint32_t edx;
uint32_t esi;
uint32_t edi;
uint32_t ebp;
};
- tf:中断帧的指针,总是指向内核栈的某个位置:当进程从用户控件跳到内核空间时,中断帧记录了进程在被中断前的状态。当内核需要跳回用户空间时,需要调整中断帧以恢复让进程继续执行的各寄存器的值。此外,ucore内核允许嵌套中断。因此为了保证嵌套中断发生时tf总是能指向当前的trapframe,ucore在内核栈上维护了tf的链,可以参考trap.c::trap函数做进一步的了解。
struct trapframe {
struct pushregs tf_regs;
uint16_t tf_gs;
uint16_t tf_padding0;
uint16_t tf_fs;
uint16_t tf_padding1;
uint16_t tf_es;
uint16_t tf_padding2;
uint16_t tf_ds;
uint16_t tf_padding3;
uint32_t tf_trapno;
/* below here defined by x86 hardware */
uint32_t tf_err;
uintptr_t tf_eip;
uint16_t tf_cs;
uint16_t tf_padding4;
uint32_t tf_eflags;
/* below here only when crossing rings, such as from user to kernel */
uintptr_t tf_esp;
uint16_t tf_ss;
uint16_t tf_padding5;
} __attribute__((packed));
- cr3:cr3保存页表的物理地址,目的就是进程切换的时候方便直接使用lcr3实现页表切换,避免每次都根据mm来计算cr3。mm数据结构是用来实现用户空间的虚存管理的,但是内核线程没有用户空间,它执行的只是内核中的一小段代码(通常是一小段函数),所以它没有mm结构。当某个进程是一个普通用户态进程的时候,PCB中的cr3就是mm中页表(pgdir)的物理地址;而当它是内核线程的时候,cr3等于boot_cr3。而boot_cr3指向了ucore启动时建立好的内核虚拟空间的页目录表首地址。
- kstack:每个线程都有一个内核栈,并且位于内核地址空间的不同位置。对于内核线程,该栈就是运行时的程序使用的栈;而对于普通进程,该栈是发生了特权级改变的时候,用来保存被打断的硬件信息用的栈。ucore在创建进程时分配了2个连续的物理页(在memlayout.h中KSTACKSIZE的定义)作为内核栈的空间。这个栈很小,所以在内核中的代码应该尽可能的紧凑,并且避免在栈上分配大的数据结构,以免栈溢出,导致系统崩溃。kstack记录了分配给该进程/线程的内核栈的位置。主要作用有以下几点:1、当内核准备从一个进程切换到另一个的时候,需要根据kstack的值正确的设置好tss,以便在进程切换以后再发生中断时能够使用正确的栈;2、内核栈位于内核地址空间,并且是不共享的(每个线程都有自己的栈),因此不受到mm的管理,当进程退出的时候,内核能够根据kstack的值决定快速定位栈的位置并进行回收。
#define KSTACKPAGE 2 // # of pages in kernel stack
#define KSTACKSIZE (KSTACKPAGE * PGSIZE) // sizeof kernel stack
另一方面,为了管理系统中所有的进程控制块,ucore维护了如下全局变量(位于kern/process/proc.c):
- static struct proc *current:当前占用CPU且处于“运行”状态进程控制块指针。通常这个变量是只读的,只有在进程切换的时候才能进行修改,并且整个切换和修改过程需要保证操作的原子性,目前至少需要屏蔽中断。可以参考switch_to的实现。
- static struct proc *initproc:本实验中,指向一个内核线程。本实验以后,指向第一个用户态进程。
- static list_entry_t hash_list[HASH_LIST_SIZE]:所有进程控制块的哈希表,proc_struct中的成员变量hash_link将基于pid链接如这个哈希表中。
- static list_entry_t proc_list:所有进程控制块的双向线性列表,proc_struct中的成员变量list_link将链接到这个链表中。
创建并执行内核线程
建立进程控制块(proc.c中的alloc_proc函数)后,就可以通过进程控制块来创建具体的进程/线程了。
// alloc_proc - alloc a proc_struct and init all fields of proc_struct
static struct proc_struct *
alloc_proc(void) {
struct proc_struct *proc = kmalloc(sizeof(struct proc_struct));
if (proc != NULL) {
//LAB4:EXERCISE1 YOUR CODE
/*
* below fields in proc_struct need to be initialized
* enum proc_state state; // Process state
* int pid; // Process ID
* int runs; // the running times of Proces
* uintptr_t kstack; // Process kernel st