update lab4, and some little fix in other labs

This commit is contained in:
yuchen 2015-03-17 20:53:37 +08:00
parent bf8cb2ad0f
commit 3a2fdaab44
18 changed files with 260 additions and 241 deletions

2
.gitignore vendored
View File

@ -1,4 +1,6 @@
node_modules
_book
assets
*.bak
*.org
*~

View File

@ -115,6 +115,9 @@
* [实验执行流程概述](lab4/lab4_3_1_lab_steps.md)
* [设计关键数据结构 -- 进程控制块](lab4/lab4_3_2_pcb.md)
* [创建并执行内核线程](lab4/lab4_3_3_create_exec_kernel_thread.md)
* [创建第0个内核线程idleproc](lab4/lab4_3_3_1_create_kthread_idleproc.md)
* [创建第1个内核线程initproc](lab4/lab4_3_3_2_create_kthread_initproc.md)
* [调度并执行内核线程initproc](lab4/lab4_3_3_3_sched_run_kthread.md)
* [实验报告要求](lab4/lab4_4_labs_requirement.md)
* [附录A实验四的参考输出](lab4/lab4_5_appendix_a.md)
* [附录B【原理】进程的属性与特征解析](lab4/lab4_6_appendix_b.md)

View File

@ -1,6 +1,6 @@
{
"plugins": [
"autocover"
"autocover","grvis"
],
"pluginsConfig": {
"autocover": {
@ -20,4 +20,4 @@
}
}
}
}
}

View File

@ -6,4 +6,4 @@
- 填写各个基本练习中要求完成的报告内容
- 完成实验后请分析ucore_lab中提供的参考答案并请在实验报告中说明你的实现与参考答案的区别
- 列出你认为本实验中重要的知识点以及与对应的OS原理中的知识点并简要说明你对二者的含义关系差异等方面的理解也可能出现实验中的知识点没有对应的原理知识点
- 列出你认为OS原理中重要的知识点,但在实验中没有对应上
- 列出你认为OS原理中重要,但在实验中没有对应上的知识点

View File

@ -7,7 +7,7 @@
- 填写各个基本练习中要求完成的报告内容
- 完成实验后请分析ucore_lab中提供的参考答案并请在实验报告中说明你的实现与参考答案的区别
- 列出你认为本实验中重要的知识点以及与对应的OS原理中的知识点并简要说明你对二者的含义关系差异等方面的理解也可能出现实验中的知识点没有对应的原理知识点
- 列出你认为OS原理中重要的知识点,但在实验中没有对应上
- 列出你认为OS原理中重要,但在实验中没有对应上的知识点
**练习0填写已有实验**

View File

@ -6,7 +6,7 @@
- 填写各个基本练习中要求完成的报告内容
- 完成实验后请分析ucore_lab中提供的参考答案并请在实验报告中说明你的实现与参考答案的区别
- 列出你认为本实验中重要的知识点以及与对应的OS原理中的知识点并简要说明你对二者的含义关系差异等方面的理解也可能出现实验中的知识点没有对应的原理知识点
- 列出你认为OS原理中重要的知识点,但在实验中没有对应上
- 列出你认为OS原理中重要,但在实验中没有对应上的知识点
#### 练习0填写已有实验

View File

@ -6,7 +6,7 @@
- 填写各个基本练习中要求完成的报告内容
- 完成实验后请分析ucore_lab中提供的参考答案并请在实验报告中说明你的实现与参考答案的区别
- 列出你认为本实验中重要的知识点以及与对应的OS原理中的知识点并简要说明你对二者的含义关系差异等方面的理解也可能出现实验中的知识点没有对应的原理知识点
- 列出你认为OS原理中重要的知识点,但在实验中没有对应上
- 列出你认为OS原理中重要,但在实验中没有对应上的知识点
#### 练习0填写已有实验

View File

@ -45,45 +45,31 @@
└── tools
```
相对与实验三,实验四主要增加的文件如上表红色部分所示,主要修改的文件如上表紫色部分所示。主要改动如下:
相对与实验三,实验四主要改动如下:
● kern/process/ (新增进程管理相关文件)
- kern/process/ (新增进程管理相关文件)
- proc.[ch]:新增:实现进程、线程相关功能,包括:创建进程/线程,初始化进程/线程,处理进程/线程退出等功能
- entry.S新增内核线程入口函数kernel\_thread\_entry的实现
- switch.S新增上下文切换利用堆栈保存、恢复进程上下文
proc.[ch]:新增:实现进程、线程相关功能,包括:创建进程/线程,初始化进程/线程,处理进程/线程退出等功能
- kern/init/
- init.c修改完成进程系统初始化并在内核初始化后切入idle进程
entry.S新增内核线程入口函数kernel\_thread\_entry的实现
- kern/mm/ 基本上与本次实验没有太直接的联系了解kmalloc和kfree如何使用即可
- kmalloc.[ch]新增定义和实现了新的kmalloc/kfree函数。具体实现是基于slab分配的简化算法 (只要求会调用这两个函数即可)
- memlayout.h增加slab物理内存分配相关的定义与宏 (可不用理会)。
- pmm.[ch]修改在pmm.c中添加了调用kmalloc\_init函数,取消了老的kmalloc/kfree的实现在pmm.h中取消了老的kmalloc/kfree的定义
- swap.c修改取消了用于check的Line 185的执行
- vmm.c修改调用新的kmalloc/kfree
switch.S新增上下文切换利用堆栈保存、恢复进程上下文
- kern/trap/
- trapentry.S增加了汇编写的函数forkrets用于do\_fork调用的返回处理。
● kern/init/
- kern/schedule/
- sched.[ch]新增实现FIFO策略的进程调度
init.c修改完成进程系统初始化并在内核初始化后切入idle进程
● kern/mm/
基本上与本次实验没有太直接的联系了解kmalloc和kfree如何使用即可
kmalloc.[ch]新增定义和实现了新的kmalloc/kfree函数。具体实现是基于slab分配的简化算法
(只要求会调用这两个函数即可)
memlayout.h增加slab物理内存分配相关的定义与宏 (可不用理会)。
pmm.[ch]修改在pmm.c中添加了调用kmalloc\_init函数,取消了老的kmalloc/kfree的实现在pmm.h中取消了老的kmalloc/kfree的定义
swap.c修改取消了用于check的Line 185的执行
vmm.c修改调用新的kmalloc/kfree
● kern/trap/
trapentry.S增加了汇编写的函数forkrets用于do\_fork调用的返回处理。
● kern/schedule/
sched.[ch]新增实现FIFO策略的进程调度
● kern/libs
rb\_tree.[ch]新增实现红黑树被slab分配的简化算法使用可不用理会
- kern/libs
- rb\_tree.[ch]新增实现红黑树被slab分配的简化算法使用可不用理会
**编译执行**
@ -94,4 +80,26 @@ make
make qemu
```
则可以得到如附录A所示的显示内容仅供参考不是标准答案输出
则可以得到如下的显示内容(仅供参考,不是标准答案输出)
```
(THU.CST) os is loading ...
Special kernel symbols:
entry 0xc010002a (phys)
etext 0xc010a708 (phys)
edata 0xc0127ae0 (phys)
end 0xc012ad58 (phys)
...
++ setup timer interrupts
this initproc, pid = 1, name = "init"
To U: "Hello world!!".
To U: "en.., Bye, Bye. :)"
kernel panic at kern/process/proc.c:354:
process exit!!.
Welcome to the kernel debug monitor!!
Type 'help' for a list of commands.
K> qemu: terminating on signal 2
```

View File

@ -1,6 +1,13 @@
## 2. 实验内容
实验2/3完成了物理和虚拟内存管理这给创建内核线程内核线程是一种特殊的进程打下了提供内存管理的基础。当一个程序加载到内存中运行时首先通过ucore的内存管理分配合适的空间然后就需要考虑如何使用CPU来“并发”执行多个程序。
实验2/3完成了物理和虚拟内存管理这给创建内核线程内核线程是一种特殊的进程打下了提供内存管理的基础。当一个程序加载到内存中运行时首先通过ucore OS的内存管理子系统分配合适的空间,然后就需要考虑如何分时使用CPU来“并发”执行多个程序让每个运行的程序这里用线程或进程表示“感到”它们各自拥有“自己”的CPU
本次实验将首先接触的是内核线程的管理。内核线程是一种特殊的进程内核线程与用户进程的区别有两个内核线程只运行在内核态而用户进程会在在用户态和内核态交替运行所有内核线程直接使用共同的ucore内核内存空间不需为每个内核线程维护单独的内存空间而用户进程需要维护各自的用户内存空间。相关原理介绍可看附录B【原理】进程/线程的属性与特征解析。
本次实验将首先接触的是内核线程的管理。内核线程是一种特殊的进程,内核线程与用户进程的区别有两个:
- 内核线程只运行在内核态
- 用户进程会在在用户态和内核态交替运行
- 所有内核线程共用ucore内核内存空间不需为每个内核线程维护单独的内存空间
- 而用户进程需要维护各自的用户内存空间
相关原理介绍可看附录B【原理】进程/线程的属性与特征解析。

View File

@ -31,15 +31,15 @@ struct proc_struct {
● parent用户进程的父进程创建它的进程。在所有进程中只有一个进程没有父进程就是内核创建的第一个内核线程idleproc。内核根据这个父子关系建立一个树形结构用于维护一些特殊的操作例如确定某个进程是否可以对另外一个进程进行某种操作等等。
● context进程的上下文用于进程切换参见switch.S。在 ucore中所有的进程在内核中也是相对独立的例如独立的内核堆栈以及上下文等等。使用 context 保存寄存器的目的就在于在内核态中能够进行上下文之间的切换。实际利用context进行上下文切换的函数是在*kern/process/switch.S*中定义switch\_to。
● context进程的上下文用于进程切换参见switch.S。在 uCore中所有的进程在内核中也是相对独立的例如独立的内核堆栈以及上下文等等。使用 context 保存寄存器的目的就在于在内核态中能够进行上下文之间的切换。实际利用context进行上下文切换的函数是在*kern/process/switch.S*中定义switch\_to。
● tf中断帧的指针总是指向内核栈的某个位置当进程从用户空间跳到内核空间时中断帧记录了进程在被中断前的状态。当内核需要跳回用户空间时需要调整中断帧以恢复让进程继续执行的各寄存器值。除此之外ucore内核允许嵌套中断。因此为了保证嵌套中断发生时tf 总是能够指向当前的trapframeucore 在内核栈上维护了 tf 的链可以参考trap.c::trap函数做进一步的了解。
● tf中断帧的指针总是指向内核栈的某个位置当进程从用户空间跳到内核空间时中断帧记录了进程在被中断前的状态。当内核需要跳回用户空间时需要调整中断帧以恢复让进程继续执行的各寄存器值。除此之外uCore内核允许嵌套中断。因此为了保证嵌套中断发生时tf 总是能够指向当前的trapframeuCore 在内核栈上维护了 tf 的链可以参考trap.c::trap函数做进一步的了解。
l cr3: cr3 保存页表的物理地址,目的就是进程切换的时候方便直接使用 lcr3实现页表切换避免每次都根据 mm 来计算 cr3。mm数据结构是用来实现用户空间的虚存管理的但是内核线程没有用户空间它执行的只是内核中的一小段代码通常是一小段函数所以它没有mm 结构也就是NULL。当某个进程是一个普通用户态进程的时候PCB 中的 cr3 就是 mm 中页表pgdir的物理地址而当它是内核线程的时候cr3 等于boot\_cr3。而boot\_cr3指向了ucore启动时建立好的饿内核虚拟空间的页目录表首地址。
cr3: cr3 保存页表的物理地址,目的就是进程切换的时候方便直接使用 lcr3实现页表切换避免每次都根据 mm 来计算 cr3。mm数据结构是用来实现用户空间的虚存管理的但是内核线程没有用户空间它执行的只是内核中的一小段代码通常是一小段函数所以它没有mm 结构也就是NULL。当某个进程是一个普通用户态进程的时候PCB 中的 cr3 就是 mm 中页表pgdir的物理地址而当它是内核线程的时候cr3 等于boot\_cr3。而boot\_cr3指向了uCore启动时建立好的饿内核虚拟空间的页目录表首地址。
● kstack: 每个线程都有一个内核栈,并且位于内核地址空间的不同位置。对于内核线程,该栈就是运行时的程序使用的栈;而对于普通进程,该栈是发生特权级改变的时候使保存被打断的硬件信息用的栈。Ucore在创建进程时分配了 2 个连续的物理页参见memlayout.h中KSTACKSIZE的定义作为内核栈的空间。这个栈很小所以内核中的代码应该尽可能的紧凑并且避免在栈上分配大的数据结构以免栈溢出导致系统崩溃。kstack记录了分配给该进程/线程的内核栈的位置。主要作用有以下几点。首先当内核准备从一个进程切换到另一个的时候需要根据kstack 的值正确的设置好 tss (可以回顾一下在实验一中讲述的 tss 在中断处理过程中的作用),以便在进程切换以后再发生中断时能够使用正确的栈。其次,内核栈位于内核地址空间,并且是不共享的(每个线程都拥有自己的内核栈),因此不受到 mm 的管理,当进程退出的时候,内核能够根据 kstack 的值快速定位栈的位置并进行回收。ucore 的这种内核栈的设计借鉴的是 linux 的方法(但由于内存管理实现的差异,它实现的远不如 linux 的灵活),它使得每个线程的内核栈在不同的位置,这样从某种程度上方便调试,但同时也使得内核对栈溢出变得十分不敏感,因为一旦发生溢出,它极可能污染内核中其它的数据使得内核崩溃。如果能够通过页表,将所有进程的内核栈映射到固定的地址上去,能够避免这种问题,但又会使得进程切换过程中对栈的修改变得相当繁琐。感兴趣的同学可以参考 linux kernel 的代码对此进行尝试。
● kstack: 每个线程都有一个内核栈,并且位于内核地址空间的不同位置。对于内核线程,该栈就是运行时的程序使用的栈;而对于普通进程,该栈是发生特权级改变的时候使保存被打断的硬件信息用的栈。uCore在创建进程时分配了 2 个连续的物理页参见memlayout.h中KSTACKSIZE的定义作为内核栈的空间。这个栈很小所以内核中的代码应该尽可能的紧凑并且避免在栈上分配大的数据结构以免栈溢出导致系统崩溃。kstack记录了分配给该进程/线程的内核栈的位置。主要作用有以下几点。首先当内核准备从一个进程切换到另一个的时候需要根据kstack 的值正确的设置好 tss (可以回顾一下在实验一中讲述的 tss 在中断处理过程中的作用),以便在进程切换以后再发生中断时能够使用正确的栈。其次,内核栈位于内核地址空间,并且是不共享的(每个线程都拥有自己的内核栈),因此不受到 mm 的管理,当进程退出的时候,内核能够根据 kstack 的值快速定位栈的位置并进行回收。uCore 的这种内核栈的设计借鉴的是 linux 的方法(但由于内存管理实现的差异,它实现的远不如 linux 的灵活),它使得每个线程的内核栈在不同的位置,这样从某种程度上方便调试,但同时也使得内核对栈溢出变得十分不敏感,因为一旦发生溢出,它极可能污染内核中其它的数据使得内核崩溃。如果能够通过页表,将所有进程的内核栈映射到固定的地址上去,能够避免这种问题,但又会使得进程切换过程中对栈的修改变得相当繁琐。感兴趣的同学可以参考 linux kernel 的代码对此进行尝试。
为了管理系统中所有的进程控制块ucore维护了如下全局变量位于*kern/process/proc.c*
为了管理系统中所有的进程控制块uCore维护了如下全局变量位于*kern/process/proc.c*
● static struct proc \*current当前占用CPU且处于“运行”状态进程控制块指针。通常这个变量是只读的只有在进程切换的时候才进行修改并且整个切换和修改过程需要保证操作的原子性目前至少需要屏蔽中断。可以参考 switch\_to 的实现。

View File

@ -0,0 +1,29 @@
#### 1. 创建第 0 个内核线程 idleproc
在init.c::kern\_init函数调用了proc.c::proc\_init函数。proc\_init函数启动了创建内核线程的步骤。首先当前的执行上下文从kern\_init 启动至今就可以看成是uCore内核也可看做是内核进程中的一个内核线程的上下文。为此uCore通过给当前执行的上下文分配一个进程控制块以及对它进行相应初始化将其打造成第0个内核线程 -- idleproc。具体步骤如下
首先调用alloc\_proc函数来通过kmalloc函数获得proc\_struct结构的一块内存块-作为第0个进程控制块。并把proc进行初步初始化即把proc\_struct中的各个成员变量清零。但有些成员变量设置了特殊的值比如
```
proc->state = PROC_UNINIT; 设置进程为“初始”态
proc->pid = -1; 设置进程pid的未初始化值
proc->cr3 = boot_cr3; 使用内核页目录表的基址
...
```
上述三条语句中,第一条设置了进程的状态为“初始”态,这表示进程已经
“出生”了正在获取资源茁壮成长中第二条语句设置了进程的pid为-1这表示进程的“身份证号”还没有办好第三条语句表明由于该内核线程在内核中运行故采用为uCore内核已经建立的页表即设置为在uCore内核页表的起始地址boot\_cr3。后续实验中可进一步看出所有内核线程的内核虚地址空间也包括物理地址空间是相同的。既然内核线程共用一个映射内核空间的页表这表示内核空间对所有内核线程都是“可见”的所以更精确地说这些内核线程都应该是从属于同一个唯一的“大内核进程”—uCore内核。
接下来proc\_init函数对idleproc内核线程进行进一步初始化
```
idleproc->pid = 0;
idleproc->state = PROC_RUNNABLE;
idleproc->kstack = (uintptr_t)bootstack;
idleproc->need_resched = 1;
set_proc_name(idleproc, "idle");
```
需要注意前4条语句。第一条语句给了idleproc合法的身份证号--0这名正言顺地表明了idleproc是第0个内核线程。通常可以通过pid的赋值来表示线程的创建和身份确定。“0”是第一个的表示方法是计算机领域所特有的比如C语言定义的第一个数组元素的小标也是“0”。第二条语句改变了idleproc的状态使得它从“出生”转到了“准备工作”就差uCore调度它执行了。第三条语句设置了idleproc所使用的内核栈的起始地址。需要注意以后的其他线程的内核栈都需要通过分配获得因为uCore启动时设置的内核栈直接分配给idleproc使用了。第四条很重要因为uCore希望当前CPU应该做更有用的工作而不是运行idleproc这个“无所事事”的内核线程所以把idleproc-\>need\_resched设置为“1”结合idleproc的执行主体--cpu\_idle函数的实现可以清楚看出如果当前idleproc在执行则只要此标志为1马上就调用schedule函数要求调度器切换其他进程执行。

View File

@ -0,0 +1,76 @@
#### 2. 创建第 1 个内核线程 initproc
第0个内核线程主要工作是完成内核中各个子系统的初始化然后就通过执行cpu\_idle函数开始过退休生活了。所以uCore接下来还需创建其他进程来完成各种工作但idleproc内核子线程自己不想做于是就通过调用kernel\_thread函数创建了一个内核线程init\_main。在实验四中这个子内核线程的工作就是输出一些字符串然后就返回了参看init\_main函数。但在后续的实验中init\_main的工作就是创建特定的其他内核线程或用户进程实验五涉及。下面我们来分析一下创建内核线程的函数kernel\_thread
```
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_struct.tf_es = tf_struct.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);
}
```
注意kernel\_thread函数采用了局部变量tf来放置保存内核线程的临时中断帧并把中断帧的指针传递给do\_fork函数而do\_fork函数会调用copy\_thread函数来在新创建的进程内核栈上专门给进程的中断帧分配一块空间。
给中断帧分配完空间后就需要构造新进程的中断帧具体过程是首先给tf进行清零初始化并设置中断帧的代码段tf.tf\_cs和数据段(tf.tf\_ds/tf\_es/tf\_ss)为内核空间的段KERNEL\_CS/KERNEL\_DS这实际上也说明了initproc内核线程在内核空间中执行。而initproc内核线程从哪里开始执行呢tf.tf\_eip的指出了是kernel\_thread\_entry位于kern/process/entry.S中kernel\_thread\_entry是entry.S中实现的汇编函数它做的事情很简单
```
kernel_thread_entry: # void kernel_thread(void)
pushl %edx # push arg
call *%ebx # call fn
pushl %eax # save the return value of fn(arg)
call do_exit # call do_exit to terminate current thread
```
从上可以看出kernel\_thread\_entry函数主要为内核线程的主体fn函数做了一个准备开始和结束运行的“壳”并把函数fn的参数arg保存在edx寄存器中压栈然后调用fn函数把函数返回值eax寄存器内容压栈调用do\_exit函数退出线程执行。
do\_fork是创建线程的主要函数。kernel\_thread函数通过调用do\_fork函数最终完成了内核线程的创建工作。下面我们来分析一下do\_fork函数的实现练习2。do\_fork函数主要做了以下6件事情
1. 分配并初始化进程控制块alloc\_proc函数
2. 分配并初始化内核栈setup\_stack函数
3. 根据clone\_flag标志复制或共享进程内存管理结构copy\_mm函数
4. 设置进程在内核将来也包括用户态正常运行和调度所需的中断帧和执行上下文copy\_thread函数
5. 把设置好的进程控制块放入hash\_list和proc\_list两个全局进程链表中
6. 自此,进程已经准备好执行了,把进程状态设置为“就绪”态;
7. 设置返回码为子进程的id号。
这里需要注意的是如果上述前3步执行没有成功则需要做对应的出错处理把相关已经占有的内存释放掉。copy\_mm函数目前只是把current-\>mm设置为NULL这是由于目前在实验四中只能创建内核线程proc-\>mm描述的是进程用户态空间的情况所以目前mm还用不上。copy\_thread函数做的事情比较多代码如下
```
static void
copy_thread(struct proc_struct *proc, uintptr_t esp, struct trapframe *tf) {
//在内核堆栈的顶部设置中断帧大小的一块栈空间
proc->tf = (struct trapframe *)(proc->kstack + KSTACKSIZE) - 1;
*(proc->tf) = *tf; //拷贝在kernel_thread函数建立的临时中断帧的初始值
proc->tf->tf_regs.reg_eax = 0;
//设置子进程/线程执行完do_fork后的返回值
proc->tf->tf_esp = esp; //设置中断帧中的栈指针esp
proc->tf->tf_eflags |= FL_IF; //使能中断
proc->context.eip = (uintptr_t)forkret;
proc->context.esp = (uintptr_t)(proc->tf);
}
```
此函数首先在内核堆栈的顶部设置中断帧大小的一块栈空间并在此空间中拷贝在kernel\_thread函数建立的临时中断帧的初始值并进一步设置中断帧中的栈指针esp和标志寄存器eflags特别是eflags设置了FL\_IF标志这表示此内核线程在执行过程中能响应中断打断当前的执行。执行到这步后此进程的中断帧就建立好了对于initproc而言它的中断帧如下所示
```
//所在地址位置
initproc->tf= (proc->kstack+KSTACKSIZE) sizeof (struct trapframe);
//具体内容
initproc->tf.tf_cs = KERNEL_CS;
initproc->tf.tf_ds = initproc->tf.tf_es = initproc->tf.tf_ss = KERNEL_DS;
initproc->tf.tf_regs.reg_ebx = (uint32_t)init_main;
initproc->tf.tf_regs.reg_edx = (uint32_t) ADDRESS of "Helloworld!!";
initproc->tf.tf_eip = (uint32_t)kernel_thread_entry;
initproc->tf.tf_regs.reg_eax = 0;
initproc->tf.tf_esp = esp;
initproc->tf.tf_eflags |= FL_IF;
```
设置好中断帧后最后就是设置initproc的进程上下文process context也称执行现场了。只有设置好执行现场后一旦uCore调度器选择了initproc执行就需要根据initproc-\>context中保存的执行现场来恢复initproc的执行。这里设置了initproc的执行现场中主要的两个信息上次停止执行时的下一条指令地址context.eip和上次停止执行时的堆栈地址context.esp。其实initproc还没有执行过所以这其实就是initproc实际执行的第一条指令地址和堆栈指针。可以看出由于initproc的中断帧占用了实际给initproc分配的栈空间的顶部所以initproc就只能把栈顶指针context.esp设置在initproc的中断帧的起始位置。根据context.eip的赋值可以知道initproc实际开始执行的地方在forkret函数主要完成do\_fork函数返回的处理工作处。至此initproc内核线程已经做好准备执行了。

View File

@ -0,0 +1,83 @@
#### 3. 调度并执行内核线程 initproc
在uCore执行完proc\_init函数后就创建好了两个内核线程idleproc和initproc这时uCore当前的执行现场就是idleproc等到执行到init函数的最后一个函数cpu\_idle之前uCore的所有初始化工作就结束了idleproc将通过执行cpu\_idle函数让出CPU给其它内核线程执行具体过程如下
```
void
cpu_idle(void) {
while (1) {
if (current->need_resched) {
schedule();
……
```
首先判断当前内核线程idleproc的need\_resched是否不为0回顾前面“创建第一个内核线程idleproc”中的描述proc\_init函数在初始化idleproc中就把idleproc-\>need\_resched置为1了所以会马上调用schedule函数找其他处于“就绪”态的进程执行。
uCore在实验四中只实现了一个最简单的FIFO调度器其核心就是schedule函数。它的执行逻辑很简单
1设置当前内核线程current-\>need\_resched为0
2在proc\_list队列中查找下一个处于“就绪”态的线程或进程next
3找到这样的进程后就调用proc\_run函数保存当前进程current的执行现场进程上下文恢复新进程的执行现场完成进程切换。
至此新的进程next就开始执行了。由于在proc10中只有两个内核线程且idleproc要让出CPU给initproc执行我们可以看到schedule函数通过查找proc\_list进程队列只能找到一个处于“就绪”态的initproc内核线程。并通过proc\_run和进一步的switch\_to函数完成两个执行现场的切换具体流程如下
1. 让current指向next内核线程initproc
2. 设置任务状态段ts中特权态0下的栈顶指针esp0为next内核线程initproc的内核栈的栈顶即next-\>kstack + KSTACKSIZE
3. 设置CR3寄存器的值为next内核线程initproc的页目录表起始地址next-\>cr3这实际上是完成进程间的页表切换
4. 由switch\_to函数完成具体的两个线程的执行现场切换即切换各个寄存器当switch\_to函数执行完“ret”指令后就切换到initproc执行了。
注意在第二步设置任务状态段ts中特权态0下的栈顶指针esp0的目的是建立好内核线程或将来用户线程在执行特权态切换从特权态0<--\>特权态3或从特权态3<--\>特权态3时能够正确定位处于特权态0时进程的内核栈的栈顶而这个栈顶其实放了一个trapframe结构的内存空间如果是在特权态3发生了中断/异常/系统调用则CPU会从特权态3--\>特权态0且CPU从此栈顶当前被打断进程的内核栈顶开始压栈来保存被中断/异常/系统调用打断的用户态执行现场如果是在特权态0发生了中断/异常/系统调用则CPU会从从当前内核栈指针esp所指的位置开始压栈保存被中断/异常/系统调用打断的内核态执行现场反之当执行完对中断/异常/系统调用打断的处理后最后会执行一个iret指令在执行此指令之前CPU的当前栈指针esp一定指向上次产生中断/异常/系统调用时CPU保存的被打断的指令地址CS和EIP,“iret指令会根据ESP所指的保存的址CS和EIP恢复到上次被打断的地方继续执行
在页表设置方面由于idleproc和initproc都是共用一个内核页表boot\_cr3所以此时第三步其实没用但考虑到以后的进程有各自的页表其起始地址各不相同只有完成页表切换才能确保新的进程能够正常执行。
第四步proc\_run函数调用switch\_to函数参数是前一个进程和后一个进程的执行现场process context。在上一节“设计进程控制块”中描述了context结构包含的要保存和恢复的寄存器。我们再看看switch.S中的switch\_to函数的执行流程
```
.globl switch_to
switch_to: # switch_to(from, to)
# save from's registers
movl 4(%esp), %eax # eax points to from
popl 0(%eax) # esp--> return address, so save return addr in FROMs
context
movl %esp, 4(%eax)
……
movl %ebp, 28(%eax)
# restore to's registers
movl 4(%esp), %eax # not 8(%esp): popped return address already
# eax now points to to
movl 28(%eax), %ebp
……
movl 4(%eax), %esp
pushl 0(%eax) # push TOs contexts eip, so return addr = TOs eip
ret # after ret, eip= TOs eip
```
首先保存前一个进程的执行现场前两条汇编指令如下所示保存了进程在返回switch\_to函数后的指令地址到context.eip中
```
movl 4(%esp), %eax # eax points to from
popl 0(%eax) # esp--> return address, so save return addr in FROMs
context
```
在接下来的7条汇编指令完成了保存前一个进程的其他7个寄存器到context中的相应成员变量中。至此前一个进程的执行现场保存完毕。再往后是恢复向一个进程的执行现场这其实就是上述保存过程的逆执行过程即从context的高地址的成员变量ebp开始逐一把相关成员变量的值赋值给对应的寄存器倒数第二条汇编指令“pushl 0(%eax)”其实把context中保存的下一个进程要执行的指令地址context.eip放到了堆栈顶这样接下来执行最后一条指令“ret”时会把栈顶的内容赋值给EIP寄存器这样就切换到下一个进程执行了即当前进程已经是下一个进程了。uCore会执行进程切换让initproc执行。在对initproc进行初始化时设置了initproc-\>context.eip = (uintptr\_t)forkret这样当执行switch\_to函数并返回后initproc将执行其实际上的执行入口地址forkret。而forkret会调用位于kern/trap/trapentry.S中的forkrets函数执行具体代码如下
```
.globl __trapret
__trapret:
# restore registers from stack
popal
# restore %ds and %es
popl %es
popl %ds
# get rid of the trap number and error code
addl $0x8, %esp
iret
.globl forkrets
forkrets:
# set stack to this new process's trapframe
movl 4(%esp), %esp //把esp指向当前进程的中断帧
jmp __trapret
```
可以看出forkrets函数首先把esp指向当前进程的中断帧从\_trapret开始执行到iret前esp指向了current-\>tf.tf\_eip而如果此时执行的是initproc则current-\>tf.tf\_eip=kernel\_thread\_entryinitproc-\>tf.tf\_cs = KERNEL\_CS所以当执行完iret后就开始在内核中执行kernel\_thread\_entry函数了而initproc-\>tf.tf\_regs.reg\_ebx = init\_main所以在kernl\_thread\_entry中执行“call %ebx”后就开始执行initproc的主体了。Initprocde的主体函数很简单就是输出一段字符串然后就返回到kernel\_tread\_entry函数并进一步调用do\_exit执行退出操作了。本来do\_exit应该完成一些资源回收工作等但这些不是实验四涉及的而是由后续的实验来完成。至此实验四中的主要工作描述完毕。

View File

@ -1,194 +1,5 @@
### 3.3 创建并执行内核线程
建立进程控制块proc.c中的alloc\_proc函数现在就可以通过进程控制块来创建具体的进程了。首先考虑最简单的内核线程它通常只是内核中的一小段代码或者函数没有用户空间。而由于在操作系统启动后,已经对整个核心内存空间进行了管理,通过设置页表建立了核虚拟空间即boot\_cr3指向的二级页表描述的空间。所以内核中的所有线程都不需要再建立各自的页表只需共享这个核虚拟空间就可以访问整个物理内存了。
建立进程控制块proc.c中的alloc\_proc函数现在就可以通过进程控制块来创建具体的进程/线程了。首先,考虑最简单的内核线程,它通常只是内核中的一小段代码或者函数,没有自己的“专属”空间。这是由于在uCore OS启动后已经对整个内核内存空间进行了管理,通过设置页表建立了核虚拟空间即boot\_cr3指向的二级页表描述的空间。所以uCore OS内核中的所有线程都不需要再建立各自的页表,只需共享这个核虚拟空间就可以访问整个物理内存了。从这个角度看内核线程被uCore OS内核这个大“内核进程”所管理。
#### 1. 创建第 0 个内核线程 idleproc
在init.c::kern\_init函数调用了proc.c::proc\_init函数。proc\_init函数启动了创建内核线程的步骤。首先当前的执行上下文从kern\_init 启动至今就可以看成是ucore内核也可看做是内核进程中的一个内核线程的上下文。为此ucore通过给当前执行的上下文分配一个进程控制块以及对它进行相应初始化将其打造成第0个内核线程 -- idleproc。具体步骤如下
首先调用alloc\_proc函数来通过kmalloc函数获得proc\_struct结构的一块内存—proc这就是第0个进程控制块了并把proc进行初步初始化即把proc\_struct中的各个成员变量清零。但有些成员变量设置了特殊的值
```
练习1 //设置进程为“初始”态
练习1 //进程的pid还没设置好
练习1 //进程在内核中使用的内核页表的起始地址
```
上述三条语句中,第一条设置了进程的状态为“初始”态,这表示进程已经
“出生”了正在获取资源茁壮成长中第二条语句设置了进程的pid为-1这表示进程的“身份证号”还没有办好第三条语句表明由于该内核线程在内核中运行故采用为ucore内核已经建立的页表即设置为在ucore内核页表的起始地址boot\_cr3。后续实验中可进一步看出所有进程的内核虚地址空间也包括物理地址空间是相同的。既然内核线程共用一个映射内核空间的页表这表示所有这些内核空间对所有内核线程都是“可见”的所以更精确地说这些内核线程都应该是从属于同一个唯一的内核进程—ucore内核。
接下来proc\_init函数对idleproc内核线程进行进一步初始化
```
idleproc->pid = 0;
idleproc->state = PROC_RUNNABLE;
idleproc->kstack = (uintptr_t)bootstack;
idleproc->need_resched = 1;
set_proc_name(idleproc, "idle");
```
需要注意前4条语句。第一条语句给了idleproc合法的身份证号--0这名正言顺地表明了idleproc是第0个内核线程。通常可以通过pid的赋值来表示线程的创建和身份确定。“0”是第一个的表示方法是计算机领域所特有的比如C语言定义的第一个数组元素的小标也是“0”。第二条语句改变了idleproc的状态使得它从“出生”转到了“准备工作”就差ucore调度它执行了。第三条语句设置了idleproc所使用的内核栈的起始地址。需要注意以后的其他线程的内核栈都需要通过分配获得因为ucore启动时设置的内核栈直接分配给idleproc使用了。第四条很重要因为ucore希望当前CPU应该做更有用的工作而不是运行idleproc这个“无所事事”的内核线程所以把idleproc-\>need\_resched设置为“1”结合idleproc的执行主体--cpu\_idle函数的实现可以清楚看出如果当前idleproc在执行则只要此标志为1马上就调用schedule函数要求调度器切换其他进程执行。
#### 2. 创建第 1 个内核线程 initproc
第0个内核线程主要工作是完成内核中各个子系统的初始化然后就通过执行cpu\_idle函数开始过退休生活了。所以ucore接下来还需创建其他进程来完成各种工作但idleproc内核子线程自己不想做于是就通过调用kernel\_thread函数创建了一个内核线程init\_main。在实验四中这个子内核线程的工作就是输出一些字符串然后就返回了参看init\_main函数。但在后续的实验中init\_main的工作就是创建特定的其他内核线程或用户进程实验五涉及。下面我们来分析一下创建内核线程的函数kernel\_thread
```
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_struct.tf_es = tf_struct.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);
}
```
注意kernel\_thread函数采用了局部变量tf来放置保存内核线程的临时中断帧并把中断帧的指针传递给do\_fork函数而do\_fork函数会调用copy\_thread函数来在新创建的进程内核栈上专门给进程的中断帧分配一块空间。
给中断帧分配完空间后就需要构造新进程的中断帧具体过程是首先给tf进行清零初始化并设置中断帧的代码段tf.tf\_cs和数据段(tf.tf\_ds/tf\_es/tf\_ss)为内核空间的段KERNEL\_CS/KERNEL\_DS这实际上也说明了initproc内核线程在内核空间中执行。而initproc内核线程从哪里开始执行呢tf.tf\_eip的指出了是kernel\_thread\_entry位于kern/process/entry.S中kernel\_thread\_entry是entry.S中实现的汇编函数它做的事情很简单
```
kernel_thread_entry: # void kernel_thread(void)
pushl %edx # push arg
call *%ebx # call fn
pushl %eax # save the return value of fn(arg)
call do_exit # call do_exit to terminate current thread
```
从上可以看出kernel\_thread\_entry函数主要为内核线程的主体fn函数做了一个准备开始和结束运行的“壳”并把函数fn的参数arg保存在edx寄存器中压栈然后调用fn函数把函数返回值eax寄存器内容压栈调用do\_exit函数退出线程执行。
do\_fork是创建线程的主要函数。kernel\_thread函数通过调用do\_fork函数最终完成了内核线程的创建工作。下面我们来分析一下do\_fork函数的实现练习2。do\_fork函数主要做了以下6件事情
1. 分配并初始化进程控制块alloc\_proc函数
2. 分配并初始化内核栈setup\_stack函数
3. 根据clone\_flag标志复制或共享进程内存管理结构copy\_mm函数
4. 设置进程在内核将来也包括用户态正常运行和调度所需的中断帧和执行上下文copy\_thread函数
5. 把设置好的进程控制块放入hash\_list和proc\_list两个全局进程链表中
6. 自此,进程已经准备好执行了,把进程状态设置为“就绪”态;
7. 设置返回码为子进程的id号。
这里需要注意的是如果上述前3步执行没有成功则需要做对应的出错处理把相关已经占有的内存释放掉。copy\_mm函数目前只是把current-\>mm设置为NULL这是由于目前在实验四中只能创建内核线程proc-\>mm描述的是进程用户态空间的情况所以目前mm还用不上。copy\_thread函数做的事情比较多代码如下
```
static void
copy_thread(struct proc_struct *proc, uintptr_t esp, struct trapframe *tf) {
//在内核堆栈的顶部设置中断帧大小的一块栈空间
proc->tf = (struct trapframe *)(proc->kstack + KSTACKSIZE) - 1;
*(proc->tf) = *tf; //拷贝在kernel_thread函数建立的临时中断帧的初始值
proc->tf->tf_regs.reg_eax = 0;
//设置子进程/线程执行完do_fork后的返回值
proc->tf->tf_esp = esp; //设置中断帧中的栈指针esp
proc->tf->tf_eflags |= FL_IF; //使能中断
proc->context.eip = (uintptr_t)forkret;
proc->context.esp = (uintptr_t)(proc->tf);
}
```
此函数首先在内核堆栈的顶部设置中断帧大小的一块栈空间并在此空间中拷贝在kernel\_thread函数建立的临时中断帧的初始值并进一步设置中断帧中的栈指针esp和标志寄存器eflags特别是eflags设置了FL\_IF标志这表示此内核线程在执行过程中能响应中断打断当前的执行。执行到这步后此进程的中断帧就建立好了对于initproc而言它的中断帧如下所示
```
//所在地址位置
initproc->tf= (proc->kstack+KSTACKSIZE) sizeof (struct trapframe);
//具体内容
initproc->tf.tf_cs = KERNEL_CS;
initproc->tf.tf_ds = initproc->tf.tf_es = initproc->tf.tf_ss = KERNEL_DS;
initproc->tf.tf_regs.reg_ebx = (uint32_t)init_main;
initproc->tf.tf_regs.reg_edx = (uint32_t) ADDRESS of "Helloworld!!";
initproc->tf.tf_eip = (uint32_t)kernel_thread_entry;
initproc->tf.tf_regs.reg_eax = 0;
initproc->tf.tf_esp = esp;
initproc->tf.tf_eflags |= FL_IF;
```
设置好中断帧后最后就是设置initproc的进程上下文process context也称执行现场了。只有设置好执行现场后一旦ucore调度器选择了initproc执行就需要根据initproc-\>context中保存的执行现场来恢复initproc的执行。这里设置了initproc的执行现场中主要的两个信息上次停止执行时的下一条指令地址context.eip和上次停止执行时的堆栈地址context.esp。其实initproc还没有执行过所以这其实就是initproc实际执行的第一条指令地址和堆栈指针。可以看出由于initproc的中断帧占用了实际给initproc分配的栈空间的顶部所以initproc就只能把栈顶指针context.esp设置在initproc的中断帧的起始位置。根据context.eip的赋值可以知道initproc实际开始执行的地方在forkret函数主要完成do\_fork函数返回的处理工作处。至此initproc内核线程已经做好准备执行了。
#### 3. 调度并执行内核线程 initproc
在ucore执行完proc\_init函数后就创建好了两个内核线程idleproc和initproc这时ucore当前的执行现场就是idleproc等到执行到init函数的最后一个函数cpu\_idle之前ucore的所有初始化工作就结束了idleproc将通过执行cpu\_idle函数让出CPU给其它内核线程执行具体过程如下
```
void
cpu_idle(void) {
while (1) {
if (current->need_resched) {
schedule();
……
```
首先判断当前内核线程idleproc的need\_resched是否不为0回顾前面“创建第一个内核线程idleproc”中的描述proc\_init函数在初始化idleproc中就把idleproc-\>need\_resched置为1了所以会马上调用schedule函数找其他处于“就绪”态的进程执行。
ucore在实验四中只实现了一个最简单的FIFO调度器其核心就是schedule函数。它的执行逻辑很简单
1设置当前内核线程current-\>need\_resched为0
2在proc\_list队列中查找下一个处于“就绪”态的线程或进程next
3找到这样的进程后就调用proc\_run函数保存当前进程current的执行现场进程上下文恢复新进程的执行现场完成进程切换。
至此新的进程next就开始执行了。由于在proc10中只有两个内核线程且idleproc要让出CPU给initproc执行我们可以看到schedule函数通过查找proc\_list进程队列只能找到一个处于“就绪”态的initproc内核线程。并通过proc\_run和进一步的switch\_to函数完成两个执行现场的切换具体流程如下
1. 让current指向next内核线程initproc
2. 设置任务状态段ts中特权态0下的栈顶指针esp0为next内核线程initproc的内核栈的栈顶即next-\>kstack + KSTACKSIZE
3. 设置CR3寄存器的值为next内核线程initproc的页目录表起始地址next-\>cr3这实际上是完成进程间的页表切换
4. 由switch\_to函数完成具体的两个线程的执行现场切换即切换各个寄存器当switch\_to函数执行完“ret”指令后就切换到initproc执行了。
注意在第二步设置任务状态段ts中特权态0下的栈顶指针esp0的目的是建立好内核线程或将来用户线程在执行特权态切换从特权态0<--\>特权态3或从特权态3<--\>特权态3时能够正确定位处于特权态0时进程的内核栈的栈顶而这个栈顶其实放了一个trapframe结构的内存空间如果是在特权态3发生了中断/异常/系统调用则CPU会从特权态3--\>特权态0且CPU从此栈顶当前被打断进程的内核栈顶开始压栈来保存被中断/异常/系统调用打断的用户态执行现场如果是在特权态0发生了中断/异常/系统调用则CPU会从从当前内核栈指针esp所指的位置开始压栈保存被中断/异常/系统调用打断的内核态执行现场反之当执行完对中断/异常/系统调用打断的处理后最后会执行一个iret指令在执行此指令之前CPU的当前栈指针esp一定指向上次产生中断/异常/系统调用时CPU保存的被打断的指令地址CS和EIP,“iret指令会根据ESP所指的保存的址CS和EIP恢复到上次被打断的地方继续执行
在页表设置方面由于idleproc和initproc都是共用一个内核页表boot\_cr3所以此时第三步其实没用但考虑到以后的进程有各自的页表其起始地址各不相同只有完成页表切换才能确保新的进程能够正常执行。
第四步proc\_run函数调用switch\_to函数参数是前一个进程和后一个进程的执行现场process context。在上一节“设计进程控制块”中描述了context结构包含的要保存和恢复的寄存器。我们再看看switch.S中的switch\_to函数的执行流程
```
.globl switch_to
switch_to: # switch_to(from, to)
# save from's registers
movl 4(%esp), %eax # eax points to from
popl 0(%eax) # esp--> return address, so save return addr in FROMs
context
movl %esp, 4(%eax)
……
movl %ebp, 28(%eax)
# restore to's registers
movl 4(%esp), %eax # not 8(%esp): popped return address already
# eax now points to to
movl 28(%eax), %ebp
……
movl 4(%eax), %esp
pushl 0(%eax) # push TOs contexts eip, so return addr = TOs eip
ret # after ret, eip= TOs eip
```
首先保存前一个进程的执行现场前两条汇编指令如下所示保存了进程在返回switch\_to函数后的指令地址到context.eip中
```
movl 4(%esp), %eax # eax points to from
popl 0(%eax) # esp--> return address, so save return addr in FROMs
context
```
在接下来的7条汇编指令完成了保存前一个进程的其他7个寄存器到context中的相应成员变量中。至此前一个进程的执行现场保存完毕。再往后是恢复向一个进程的执行现场这其实就是上述保存过程的逆执行过程即从context的高地址的成员变量ebp开始逐一把相关成员变量的值赋值给对应的寄存器倒数第二条汇编指令“pushl 0(%eax)”其实把context中保存的下一个进程要执行的指令地址context.eip放到了堆栈顶这样接下来执行最后一条指令“ret”时会把栈顶的内容赋值给EIP寄存器这样就切换到下一个进程执行了即当前进程已经是下一个进程了。ucore会执行进程切换让initproc执行。在对initproc进行初始化时设置了initproc-\>context.eip = (uintptr\_t)forkret这样当执行switch\_to函数并返回后initproc将执行其实际上的执行入口地址forkret。而forkret会调用位于kern/trap/trapentry.S中的forkrets函数执行具体代码如下
```
.globl __trapret
__trapret:
# restore registers from stack
popal
# restore %ds and %es
popl %es
popl %ds
# get rid of the trap number and error code
addl $0x8, %esp
iret
.globl forkrets
forkrets:
# set stack to this new process's trapframe
movl 4(%esp), %esp //把esp指向当前进程的中断帧
jmp __trapret
```
可以看出forkrets函数首先把esp指向当前进程的中断帧从\_trapret开始执行到iret前esp指向了current-\>tf.tf\_eip而如果此时执行的是initproc则current-\>tf.tf\_eip=kernel\_thread\_entryinitproc-\>tf.tf\_cs = KERNEL\_CS所以当执行完iret后就开始在内核中执行kernel\_thread\_entry函数了而initproc-\>tf.tf\_regs.reg\_ebx = init\_main所以在kernl\_thread\_entry中执行“call %ebx”后就开始执行initproc的主体了。Initprocde的主体函数很简单就是输出一段字符串然后就返回到kernel\_tread\_entry函数并进一步调用do\_exit执行退出操作了。本来do\_exit应该完成一些资源回收工作等但这些不是实验四涉及的而是由后续的实验来完成。至此实验四中的主要工作描述完毕。

View File

@ -6,7 +6,7 @@
- 填写各个基本练习中要求完成的报告内容
- 完成实验后请分析ucore_lab中提供的参考答案并请在实验报告中说明你的实现与参考答案的区别
- 列出你认为本实验中重要的知识点以及与对应的OS原理中的知识点并简要说明你对二者的含义关系差异等方面的理解也可能出现实验中的知识点没有对应的原理知识点
- 列出你认为OS原理中重要的知识点,但在实验中没有对应上
- 列出你认为OS原理中重要,但在实验中没有对应上的知识点
#### 练习0填写已有实验

View File

@ -6,7 +6,7 @@
- 填写各个基本练习中要求完成的报告内容
- 完成实验后请分析ucore_lab中提供的参考答案并请在实验报告中说明你的实现与参考答案的区别
- 列出你认为本实验中重要的知识点以及与对应的OS原理中的知识点并简要说明你对二者的含义关系差异等方面的理解也可能出现实验中的知识点没有对应的原理知识点
- 列出你认为OS原理中重要的知识点,但在实验中没有对应上
- 列出你认为OS原理中重要,但在实验中没有对应上的知识点
#### 练习0填写已有实验

View File

@ -6,7 +6,7 @@
- 填写各个基本练习中要求完成的报告内容
- 完成实验后请分析ucore_lab中提供的参考答案并请在实验报告中说明你的实现与参考答案的区别
- 列出你认为本实验中重要的知识点以及与对应的OS原理中的知识点并简要说明你对二者的含义关系差异等方面的理解也可能出现实验中的知识点没有对应的原理知识点
- 列出你认为OS原理中重要的知识点,但在实验中没有对应上
- 列出你认为OS原理中重要,但在实验中没有对应上的知识点
#### 练习0填写已有实验

View File

@ -6,7 +6,7 @@
- 填写各个基本练习中要求完成的报告内容
- 完成实验后请分析ucore_lab中提供的参考答案并请在实验报告中说明你的实现与参考答案的区别
- 列出你认为本实验中重要的知识点以及与对应的OS原理中的知识点并简要说明你对二者的含义关系差异等方面的理解也可能出现实验中的知识点没有对应的原理知识点
- 列出你认为OS原理中重要的知识点,但在实验中没有对应上
- 列出你认为OS原理中重要,但在实验中没有对应上的知识点
#### 练习0填写已有实验