清华大学操作系统Lab4实验报告
课程主页:http://os.cs.tsinghua.edu.cn/oscourse/OS2018spring
实验指导书:https://chyyuu.gitbooks.io/ucore_os_docs/content/
github:https://github.com/chyyuu/ucore_os_lab
实验目的
- 了解内核线程创建/执行的管理过程
- 了解内核线程的切换和基本调度过程
实验内容
实验二、三完成了无力和虚拟内存管理,这给创建内核线程(内核线程是一种特殊的进程)打下了提供内存管理的基础。当一个程序加载到内存中运行时,首先通过ucore OS的内存管理子系统分配合适的空间,然后就需要考虑如何分时使用CPU来“并发”执行多个程序,让每个运行的程序(这里用线程或进程表示)“感到”它们各自拥有“自己”的CPU。
本次实验将首先接触的是内核线程的管理。内核线程是一种特殊的进程,内核线程与用户进程的区别有两个:
- 内核线程只运行在内核态
- 用户进程会在用户态和内核态交替运行
- 所有内核线程共用ucore内核内存空间,不需为每个内核线程维护单独的内存空间
- 而用户线程需要维护各自的用户内存空间
练习1:分配并初始化一个进程控制块
在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 2015011345
proc -> state = PROC_UNINIT;
proc -> pid = -1;
proc -> cr3 = boot_cr3; // 内核线程,页表使用boot_cr3
proc -> mm = NULL;
proc -> runs = 0;
proc -> kstack = 0;
proc -> need_resched = 0;
proc -> parent = NULL;
memset(&(proc -> context), 0, sizeof(struct context));
proc -> tf = NULL;
proc -> flags = 0;
memset(proc -> name, 0, PROC_NAME_LEN);
}
return proc;
}
请说明proc_struct中struct context context
和struct trapframe *tf
成员变量含义和在本实验中的作用。
proc_struct
中的context
:进程的上下文,用于进程切换(switch.S中)。在uCore中,所有的进程在内核中也是相对独立的(例如独立的内核堆栈和上下文等等)。使用context保存寄存器的目的就在于在内核态中能够进行上下文之间的切换。实际利用context进行上下文切换的函数是在kern/process/switch.S
中的switch_to
。proc_struct
中的tf
:中断帧的指针。总是指向内核栈的某个位置:当进程从用户空间跳到内核空间时,中断帧记录了进程在被中断前的状态。当内核需要跳回用户空间时,需要调整中断帧以恢复让进程继续执行的各寄存器值。除此之外,uCore内核允许嵌套中断。因此为了保证嵌套中断发生时tf总是能够指向当前的trapframe,uCore在内核栈上维护了tf的链,在trap.c::trap
中。
练习2:为新创建的内核线程分配资源
/* do_fork - parent process for a new child process
* @clone_flags: used to guide how to clone the child process
* @stack: the parent's user stack pointer. if stack==0, It means to fork a kernel thread.
* @tf: the trapframe info, which will be copied to child process's proc->tf
*/
int
do_fork(uint32_t clone_flags, uintptr_t stack, struct trapframe *tf) {
int ret = -E_NO_FREE_PROC;
struct proc_struct *proc;
if (nr_process >= MAX_PROCESS) {
goto fork_out;
}
ret = -E_NO_MEM;
// 1. call alloc_proc to allocate a proc_struct
proc = alloc_proc();
if (proc == NULL) {
goto fork_out;
}
proc -> parent = current;
// 2. call setup_kstack to allocate a kernel stack for child process
int kstack_success = setup_kstack(proc);
if (kstack_success != 0) {
goto bad_fork_cleanup_proc;
}
// 3. call copy_mm to dup OR share mm according clone_flag
int copy_success = copy_mm(clone_flags, proc);
if (copy_success != 0) {
goto bad_fork_cleanup_kstack;
}
// 4. call copy_thread to setup tf & context in proc_struct
copy_thread(proc, stack, tf);
bool intr_flag;
local_intr_save(intr_flag);
proc -> pid = get_pid();
// 5. insert proc_struct into hash_list && proc_list
hash_proc(proc);
list_add(&proc_list, &(proc -> list_link));
nr_process++;
local_intr_restore(intr_flag);
// 6. call wakeup_proc to make the new child process RUNNABLE
wakeup_proc(proc);
// 7. set ret vaule using child proc's pid
ret = proc -> pid;
fork_out:
return ret;
bad_fork_cleanup_kstack:
put_kstack(proc);
bad_fork_cleanup_proc:
kfree(proc);
goto fork_out;
}
请说明ucore是否做到给每个新fork的线程一个唯一的id?请说明你的分析和理由。
可以保证每个线程的id唯一。具体可见proc.c::get_pid
函数:
// get_pid - alloc a unique pid for process
static int
get_pid(void) {
static_assert(MAX_PID > MAX_PROCESS);
struct proc_struct *proc;
list_entry_t *list = &proc_list, *le;
static int next_safe = MAX_PID, last_pid = MAX_PID;
if (++ last_pid >= MAX_PID) {
last_pid = 1;
goto inside;
}
if (last_pid >= next_safe) {
inside:
next_safe = MAX_PID;
repeat:
le = list;
while ((le = list_next(le)) != list) {
proc = le2proc(le, list_link);
if (proc->pid == last_pid) {
if (++ last_pid >= next_safe) {
if (last_pid >= MAX_PID) {
last_pid = 1;
}
next_safe = MAX_PID;
goto repeat;
}
}
else if (proc->pid > last_pid && next_safe > proc->pid) {
next_safe = proc->pid;
}
}
}
return last_pid;
}
首先,第一句assert可以保证进程数一定不会多于可以分配的进程标识号的数目。
接下来,函数将扫描所有的进程,找到一个当前没被使用的进程号,存储在last_pid中,作为新进程的进程号。具体来说,循环扫描每一个当前进程:当一个现有的进程号和last_pid相等时,则将last_pid+1;当现有的进程号大于last_pid时,这意味着在已经扫描的进程中[last_pid, min(next_safe, proc->pid)]这段进程号尚未被占用,继续扫描。
这样可以保证返回的新进程号一定没有被占用,即具有唯一的id。
练习3:阅读代码,理解proc_run函数和它调用的函数如何完成进程切换的。
// proc_run - make process "proc" running on cpu
// NOTE: before call switch_to, should load base addr of "proc"'s new PDT
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;
// 加载待调度进程的内核栈基地址和页表基地址
load_esp0(next->kstack + KSTACKSIZE);
lcr3(next->cr3);
// 保存原线程的寄存器并恢复待调度线程的寄存器
switch_to(&(prev->context), &(next->context));
}
// 恢复中断
local_intr_restore(intr_flag);
}
}
保存寄存器和恢复待调度进程的寄存器部分代码在switch_to中,如下:
switch_to: # switch_to(from, to)
# save from's registers
movl 4(%esp), %eax # eax points to from
popl 0(%eax) # save eip !popl
movl %esp, 4(%eax) # save esp::context of from
movl %ebx, 8(%eax) # save ebx::context of from
movl %ecx, 12(%eax) # save ecx::context of from
movl %edx, 16(%eax) # save edx::context of from
movl %esi, 20(%eax) # save esi::context of from
movl %edi, 24(%eax) # save edi::context of from
movl %ebp, 28(%eax) # save ebp::context of from
# restore to's registers
movl 4(%esp), %eax # not 8(%esp): popped return address already
# eax now points to to
movl 28(%eax), %ebp # restore ebp::context of to
movl 24(%eax), %edi # restore edi::context of to
movl 20(%eax), %esi # restore esi::context of to
movl 16(%eax), %edx # restore edx::context of to
movl 12(%eax), %ecx # restore ecx::context of to
movl 8(%eax), %ebx # restore ebx::context of to
movl 4(%eax), %esp # restore esp::context of to
pushl 0(%eax) # push eip
ret
本实验在执行过程中,创建且运行了几个内核线程
共两个线程:
- idleproc:ucore的第一个内核线程,完成内核中各个子系统的初始化,之后立即调度,执行其他进程。
- initproc:“hello world”线程。
语句 local_intr_save(intr_flag);....local_intr_restore(intr_flag);
在这里有何作用?请说明理由。
在进程调度开始前关中断,在结束进程调度后开中断。这是为了防止在进程调度过程中产生中断导致进程调度的嵌套。
覆盖的知识点
- 内核线程的创建和切换。
与参考答案的区别
- 练习1:只写了一部分初始化,参考答案后发现没有写完整。
- 练习2:没有使用
local_intr_save(intr_flag);....local_intr_restore(intr_flag);
,后参考答案后不上。
总结
感谢期中考试让我有机会从头开始搞清楚了各种原理和ucore的前几个lab的很多实现细节。
尽管这次在实验开始前观看了mooc并认真阅读了实验指导书,但是在实现过程中还是有很多问题,有待进一步研究。