Lab4 - Preemptive Multitasking需要在多个同时激活的用户模式环境中实现抢占式多任务。
实验分为三个部分。第一部分要求为JOS添加多处理器支持,并实现论询调度以及基本的环境管理系统调用;第二部分需要实现一个Unix-like的fork函数,使得用户环境可以创建一份自己的拷贝;第三部分需要实现进程间通讯功能,允许不同的用户环境显式地互相通信和同步。同时还需要实现对于硬件时钟中断和抢占的支持。
2018年3月2日,完成了实验并写完了报告。
实验准备
根据官网,切换到分支lab4并合并分支lab3。在合并的过程中,发生了冲突。
1 | user% git checkout -b lab3 origin/lab3 |
根据提示,是conf/lab.mk
中发生了冲突。打开后可以发现是其中记录的时间和实验数发生了变化,直接采用分支lab4的版本即可,然后提交,分支合并完成。
实验四包括如下的新文件:
- kern/cpu.h 内核私有的关于多处理器的支持
- kern/mpconfig.c 读取多处理器配置的代码
- kern/lapic.c 内核驱动每个处理的的APIC(高级可编程中断控制器)的代码
- kern/mpentry.S 非引导CPU的汇编入口代码
- kern/spinlock.h 内核私有的自旋锁定义,包括大内核锁
- kern/spinlock.c 内核实现自旋锁的代码
- kern/sched.c 需要实现的调度器的代码框架
实验过程
第一部分 Multiprocessor Support and Cooperative Multitasking - 多处理器支和写作式多任务
在第一部分中,需要使JOS运行在一个多处理器系统上,并且实现新的JOS内核系统调用去允许用户级别的环境创建额外的新环境。还需要实现协同式的论询调度,允许内核当旧的用户环境自愿放弃CPU或退出时切换至一个新的用户环境。
多处理器支持
需要使JOS支持“对称多处理”(Symmetric Multiprocessing),在该模型下,所有的CPU都有着对系统资源(内存、IO总线等)的平等访问权。尽管在SMP下所有的CPU在功能上均等价,在启动时仍然分为两种类型——引导处理器(Bootstrap Processor或是BSP)负责初始化系统并且引导操作系统;应用处理器(Application Processors或是APs)仅在系统启动运行后被BSP激活。BSP处理器由硬件和BIOS决定。在此刻,所有已有的代码已经运行在了BSP上。
在SMP中,每一个CPU都有一个相伴的本地APIC(LAPIC)单元。LAPIC单元负责在整个系统中传递中断。LAPIC为相连的CPU提供了一个唯一标识。在本实验中,将利用LAPIC的一下功能(在kern/lapic.c
中):
- 读取LAPIC标识(APIC ID)以告知CPU代码运行在哪一个CPU上(参考
cpunum()
) - 从BSP向APs发送STARTUP处理器间中断(Interprocessor interrupt或IPI)以唤醒其他CPU(参考
lapic_startup()
) - 在第三部分中,通过编程LAPIC内置的计时器去引发时钟中断以实现抢占式多任务(参考
apic_init()
处理器通过内存映射IO(Memory-mapped I/O 或是MMIO)访问LAPIC。在MMIO中,一部分物理内存被硬连接至某些IO设备的寄存器。所以相同的访问内存的load/store
指令可以被用来访问设备寄存器。你可能已经见过了存在于物理地址0xA0000
中IO洞(以此来写VGA显示缓存)。LAPIC存在于物理地址从0xFE000000
(4064M)开始的洞中。该地址太高以至于无法在KERNBASE的直接映射访问。JOS的虚拟内存映射在MMIOBASE
处留下了4MB的空隙。在之后的实验中会引入更多的MMIO
区域,所以应当写一个简单的函数从该区域分配空间并映射内存。
练习1的答案如下,仅供参考:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17// In kern/pmap.c, mmio_map_region():
// Your code here:
size = ROUNDUP(size, PGSIZE);
if (base + size > MMIOLIM) {
// reservation overflog MMIOLIM
panic("reservation bytes overflows!");
}
// use boot_map_region to map [pa, pa + size) to [base, base + size)
boot_map_region(kern_pgdir, base, size, pa, PTE_W | PTE_PCD | PTE_PWT);
// update base and return
uintptr_t saved_base = base;
base += size;
return (char *)saved_base;
// panic("mmio_map_region not implemented");
应用处理器引导
BSP在引导APs之前应当首先收集多处理器系统的信息,如总CPU数,APIC IDs以及LAPIC单元的内存映射IO地址等。kern/mpconfig.c
中的mp_init()
函数通过读取BIOS内存中的MP配置表获取相应的信息。
在kern/init.c
中的boot_aps()
函数驱动AP引导过程。APs在实模式中启动,所以boot_aps()
从kern/mpentry.S
中拷贝AP入口代码到一个实模式可寻址的位置。可以在一定程度上控制AP执行开始代码的位置。在本实验中,将入口代码拷贝至0x7000
(MPENTRY_PADDR),但实际上任何640KB以下的、页对齐的和未使用的物理地址均可使用。
然后,boot_aps()
通过向对应AP的LAPIC单元发送STARTUP处理器间中断和初始的CS:IP地址(本实验中为MPENTRY_PADDR),依次激活APs。在简单的设置后,将AP启动分页,使得AP进入保护模式,然后调用启动例程mp_main()
(在kern/init.c
中。boot_aps()
在唤醒下一个AP之前先等待当前AP在struct CpuInfo
的cpu_status
域中发送一个CPU_STARTED
标记。
AP引导的汇编代码和C代码同实验一BSP的引导代码相似,可以比对异同。
练习2的代码如下,仅供参考:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19// In kern/pmap.c, page_init():
// code MODIFIED
size_t i;
// initialize from page 1 to page npages_basemem - 1
// set pp_ref to 0, set pp_link to last page_free_list
// and then update page_free_list
// Lab 4: remove page at MPENTRY_PADDR
size_t mp_entry_page = PGNUM(MPENTRY_PADDR);
for(i = 1 ; i < npages_basemem ; i++) {
if (i == mp_entry_page) {
continue;
}
pages[i].pp_ref = 0;
pages[i].pp_link = page_free_list;
page_free_list = &pages[i];
}
问题1:
宏MPBOOTPHYS
作用是将给定的内核虚拟地址(在mpentry.S
中)转换成相应的加载后的物理地址。这么做的原因是在mpentry.S
中,保护模式与分页机制尚未开启,设置分段等需要知道相应的物理地址。
每CPU状态和初始化
当编写多处理器操作系统时,区分对于每个处理器而言私有的每CPU状态以及整个系统共享的全局状态是很重要的。kern/cpu.h
定义了大部分每CPU状态,包括了存储了每CPU变量的struct CpuInfo
。cpunum()
总是返回调用它的CPU ID,能用来索引例如cpus
的数组。宏thiscpu
是当前CPU的struct CpuInfo
的简写。
以下是你需要注意的每CPU状态:
- 每CPU内核栈 - 多个CPU可能会同时陷入内核,因此每个处理器需要独立的内核栈以避免互相干扰。
percpu_kstacks[NCPU][KSTKSIZE]
为NCPU
个内核栈预留空间。在实验二中,将bootstack
指向的物理内存作为BSP的栈映射在了KSTACKTOP下面。相似地,本实验中,需要将每个CPU的内核栈映射到该区域同时分配保护页作为它们之间的缓冲。CPU 0的栈将从KSTACKTOP开始向下增长;CPU 1的栈将从CPU 0栈下方间隔KSTKGAP处开始增长。inc/memlayout.h
展示了映射约束。 - 每CPU的TSS和TSS描述符 - 每CPU的任务状态段同样用来致命每个CPU内核栈的位置。CPU i的TSS被存储于
cpus[i].cpu_ts
中,相应的TSS描述符在GDT入口gdt[(GD_TSS0 >> 3) + i]
处被定义。kern/trap.c
中定义的ts
将不再有效。 - 每CPU的当前环境指针 - 由于每个CPU都能同步地执行不同的用户环境。重新将
curenv
定义为指向当前CPU(当前代码正在执行的CPU)正在执行的环境的cpus[cpunum()].cpu_env
(或是thiscpu->cpu_env
)。 - 每CPU的系统寄存器 - 包括系统寄存器在内的所有寄存器都属于CPU私有。因此初始化这些寄存器的指令如
lcr3
,ltr
, ‘lgdt’等,必须在每个CPU上都被执行。函数env_init_percpu()
以及trap_init_percpu
正是为此而被定义。
除此之外任何额外的用来CPU初始化的所有每CPU状态都应该在每个CPU处重复。
练习3中遇到的问题:
- 在使用boot_map_region映射内存时忘记将kstacktop_i减去KSTKSIZE,导致未通过检查
练习3的代码如下,仅供参考:1
2
3
4
5
6
7
8
9
10// LAB 4: Your code here:
size_t i;
for (i = 0; i < NCPU; i++) {
// traverser through 0 to NCPU to use boot_map_region to map
// per-CPU's kernel stack to corresponding va
uintptr_t kstacktop_i = KSTACKTOP - i * (KSTKSIZE + KSTKGAP);
boot_map_region(kern_pgdir, kstacktop_i - KSTKSIZE, KSTKSIZE,
PADDR(percpu_kstacks[i]), PTE_W | PTE_P);
}
练习4中遇到的问题:
- 使用ltr加载任务段选择子时,每个CPU应当使用不同的选择子,
inc/memlayout.c
中的GD_TSS0为CPU0的任务段选择子,应该加上i << 3的偏移 - 完成
trap_init_percpu()
后,旧的使用ts
的代码应当被注释
练习4的代码如下,仅供参考:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22// LAB 4: Your code here:
// trap_init_percpu() is called by all CPUs
// set esp0 and ss0 of task state segment to provide
// per-CPU's kernel stack access
thiscpu->cpu_ts.ts_esp0 = (uintptr_t)percpu_kstacks[cpunum()];
thiscpu->cpu_ts.ts_ss0 = GD_KD;
// set IO map base address to prevent unauthorized environments
// this line works together as we'll set TSS segment limit later
// so all ports in address space have no corresponding IOPB
thiscpu->cpu_ts.ts_iomb = sizeof(struct Taskstate);
// set TSS in gdt
// 0 means RPL and sd_s = 0 means system segment
gdt[(GD_TSS0 >> 3) + cpunum()] = SEG16(STS_T32A, (uint32_t)(&(thiscpu->cpu_ts)),
sizeof(struct Taskstate) - 1, 0);
gdt[(GD_TSS0 >> 3) + cpunum()].sd_s = 0;
// Load TSS selector
ltr(GD_TSS0 + (cpunum() << 3));
// load the IDT
lidt(&idt_pd);
同步锁
当前的代码在mp_main()
中初始化完AP之后忙等待。在继续下一步之前,首先得解决多个CPU同时执行内核代码时的竞争条件。最简答的方式是使用一个“大”内核锁,大内核锁在从用户模式进入内核模式时被获取,在从环境返回用户模式时被释放。在这种模型下,用户模式环境可以在任意多个CPU上运行,但同时只能有一个环境能在内核中运行,其他想在内核中运行的环境被强制等待。
kern/spinlock.h
声明了内核锁,同时提供了lock_kernel
提供加锁的功能;unlock_kernel
提供解锁的功能,你应当在如下4个地方应用内核锁:
- 在
i386_init()
中,在BSP唤醒其他CPU之前加锁 - 在
mp_main()
中,在初始化AP后加锁,然后调用sched_yield()
去在当前AP上执行环境 - 在
trap()
中,当从用户模式陷入的时候加锁,通过检查tf_cs的低位判断陷阱发生于用户模式还是内核模式 - 在
env_run()
中,在“刚好”切换回用户模式之前解锁。太早或太晚解锁会导致严重的竞争和死锁
练习5的代码如下,仅供参考:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30// In i386_init():
// Acquire the big kernel lock before waking up APs
// Your code here:
lock_kernel();
// In mp_main():
// Now that we have finished some basic setup, call sched_yield()
// to start running processes on this CPU. But make sure that
// only one CPU can enter the scheduler at a time!
//
// Your code here:
// lock the kernel and start running enviroments
lock_kernel();
sched_yield();
// In trap():
// Trapped from user mode.
// Acquire the big kernel lock before doing any
// serious kernel work.
// LAB 4: Your code here.
lock_kernel();
// In env_run():
// address space switch
// reference from inc/x86.h
lcr3(PADDR(e->env_pgdir));
// release kernel lock here
unlock_kernel(); // newly added code
// drop into user mode
env_pop_tf(&(e->env_tf));
问题2:
即使受到内核锁的保护,CPU之间仍然需要独立的内核栈。假设CPU0因中断陷入内核并在内核栈中保留了相关的信息,此时若CPU1也发生中断而陷入内核,在同一个内核栈的情况下,CPU0中的信息将会被覆盖从而导致出现错误。
论询调度
下一个任务是改变JOS内核使得其能按照“论询”的方式在多个环境中切换。在JOS中,论询调度按如下方式工作:
- ‘kern/sched.c’中的
sched_yield()
负责选择一个新环境执行。其循环按顺序遍历envs
数组,从上一次运行的环境(如果没有之前运行的环境,则从第一个环境)开始,找到第一个具有状态ENV_RUNNABLE的环境,调用env_run()
执行。 sched_yield()
绝对不能同时在两个CPU上运行相同的环境。其会告知环境当前已经正在运行在某CPU上(很可能是当前环境),因为该环境的状态将为ENV_RUNNING。- 将实现一个新的系统调用
sys_yield()
,用户环境可以通过该系统调用唤醒sched_yield()
函数以主动放弃CPU。
练习6中遇到的问题:
- 在实现
sched_yield()
的时候没有检查thiscpu->cpu_env
是否为空,对空指针的访问导致了缺页错误 - 一开始的实现弄错了获取env_index和自增的顺序,参考代码中第一个
if
处的else
分支的注释 - 练习6结束后需要将
init.c
中mp_main()
的最后一行注释
练习6的代码如下,仅供参考:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69// In kern/sched.c, sched_yield():
// LAB 4: Your code here.
// get index from current CPU's env
size_t env_index;
// set curenv_flag to default true
int curenv_flag = true;
size_t i;
if (thiscpu->cpu_env == NULL) {
// no previous running environment
// start at beginning of envs array
i = 0;
env_index = NENV - 1;
// mark curenv_flag as true
curenv_flag = false;
} else {
// start at previous running environment
env_index = ENVX(thiscpu->cpu_env->env_id);
// NB: don't mess with code order here, must first retrieve
// env_index then increment
i = (env_index == NENV - 1) ? 0 : env_index + 1;
}
// traverse through envs list to find first ENV_RUNNABLE
// env
for (; i != env_index; i = ((i == NENV - 1) ? 0 : i + 1)) {
if (envs[i].env_status == ENV_RUNNABLE) {
// found, set idle and break from loop
idle = &envs[i];
break;
}
}
// if idle is NULL and curenv_flag is true,
// means no envs are runnable and last previous running env is ENV_RUNNING
// check if last previous environment is ENV_RUNNING,
// if so, choose it.
if (!idle && curenv_flag && envs[env_index].env_status == ENV_RUNNING) {
idle = &envs[env_index];
}
if (idle) {
// idle env choosed, run it directly
env_run(idle);
} else {
// failed to choose idle env, halt CPU
// sched_halt never returns
sched_halt();
}
// In kern/syscall.c, syscall():
// code added:
case SYS_yield:
// call sys_yield
sys_yield();
break;
// In kern/init.c, i386_init():
// code modified:
// Don't touch -- used by grading script!
ENV_CREATE(TEST, ENV_TYPE_USER);
// Touch all you want.
// create three user_yield
// code MODIFIED here
ENV_CREATE(user_yield, ENV_TYPE_USER);
ENV_CREATE(user_yield, ENV_TYPE_USER);
ENV_CREATE(user_yield, ENV_TYPE_USER);
问题3:
地址切换前后的页表中,e
指向的虚拟地址都被同一块物理页映射。出现这种情况的原因在于env
的env_pgdir
是以kern_pgdir
为原型产生的,e
出于UTOP之上的地址,而UTOP以上的地址的映射关系在两个页表中是一样的。
问题4:
当发生地址转换时一定是从用户陷入内核之后,无论以何种方式陷入内核,必须要经过kern/trap.c
中的trap()
函数。观察该函数,可以发现,当从用户模式陷入内核时,代码将内核栈中的tf
(包括页表和寄存器等)拷贝至内核间共享的对应的env
中,所以之后寄存器状态才能恢复。
环境创建的系统调用
尽管内核已经可以运行并在不同环境间切换了,但内核任然被限制了只能执行内核初始设置的用户环境。
需要实现必要的系统调用使得JOS可以允许用户环境创建和启动其他的用户环境。
Unix提供了fork()
作为它的进程创建原语。Unix的fork()
拷贝调用进程(父进程)的整个地址空间以创建子进程。
从用户空间来看,父子进程唯一可观察的差异就是它们的进程ID和父进程ID(通过gitpid()
和getppid()
返回。在父进程中,fork()
返回子进程ID;在子进程中,fork()
返回0。默认情况下,每个进程均获得其私有的地址空间,并且任意一个进程对于内存的修改对于其他进程都是不可见的。
将要实现一个不同的、更加原始的JOS系统调用原语集合去创建新的用户模式环境,通过这些系统调用,除了其他类型的环境创建以外,将能够在用户空间实现一个完整的类Unixfork()
系统调用。需要实现的系统调用为:
sys_exofork
- 创建一个几乎空白的新环境:在地址空间没有任何的用户映射,并且也无法运行。新环境将会有和父环境在执行sys_exofork
系统调用时完全一致的寄存器状态。在父进程中,sys_exofork
会返回新创建环境的envid_t
(若环境创建错误则返回一个错误码);子进程则会返回0(因为子进程最初被标记为不可运行,直到父进程通过显式标记子进程可运行之后,sys_exofork
返回。sys_env_set_status
- 设置指定的环境的状态为ENV_RUNNABLE或是ENV_NOT_RUNNABLE。该系统调用通常在一个新环境的地址空间和寄存器状态完全初始化之后标记其为可运行。sys_page_alloc
- 分配一页的物理内存并将其映射到给定环境地址空间的给定虚拟地址。sys_page_alloc
- 将一页映射(而不是实际的页内容)从一个环境的地址空间拷贝至另一个,共享内存使得新映射和旧映射指向同一页物理内存。sys_page_unmap
- 将给定环境的给定虚拟地址的页面解除映射。
上述所有的系统调用接受环境ID,内核支持将0到“当前环境”转换,在kern/env.c
中的envid2env()
实现。
已经在user/dumpfork.c
中提供了非常原始的类Unix的fork()
实现。测试程序用上述系统调用创建并运行一个当前地址空间拷贝的子进程,然后两个环境使用sys_yield()
来回切换。父进程在10次迭代后退出;子进程在20次迭代后退出。
练习7的代码如下,仅供参考:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269// In sys_exofork():
// Allocate a new environment.
// Returns envid of new environment, or < 0 on error. Errors are:
// -E_NO_FREE_ENV if no free environment is available.
// -E_NO_MEM on memory exhaustion.
static envid_t sys_exofork(void) {
// Create the new environment with env_alloc(), from kern/env.c.
// It should be left as env_alloc created it, except that
// status is set to ENV_NOT_RUNNABLE, and the register set is copied
// from the current environment -- but tweaked so sys_exofork
// will appear to return 0.
// LAB 4: Your code here.
// get env_id of current environment
envid_t parent_id = thiscpu->cpu_env->env_id;
// use env_alloc to create new environment and
// do some basic setup(status and register set)
struct Env *env = NULL;
if (env_alloc(&env, parent_id) < 0) { panic("sys_exofork failed!"); }
env->env_status = ENV_NOT_RUNNABLE;
env->env_tf = thiscpu->cpu_env->env_tf;
// do the trick to set eax register of newly
// alloc environment to 0
env->env_tf.tf_regs.reg_eax = 0;
// return new environment's ID
return env->env_id;
// panic("sys_exofork not implemented");
}
// In sys_env_set_status():
// Set envid's env_status to status, which must be ENV_RUNNABLE
// or ENV_NOT_RUNNABLE.
//
// Returns 0 on success, < 0 on error. Errors are:
// -E_BAD_ENV if environment envid doesn't currently exist,
// or the caller doesn't have permission to change envid.
// -E_INVAL if status is not a valid status for an environment.
static int sys_env_set_status(envid_t envid, int status) {
// Hint: Use the 'envid2env' function from kern/env.c to translate an
// envid to a struct Env.
// You should set envid2env's third argument to 1, which will
// check whether the current environment has permission to set
// envid's status.
// LAB 4: Your code here.
// check status passed in
if (status != ENV_RUNNABLE && status != ENV_NOT_RUNNABLE) {
// invalid status
return -E_INVAL;
}
struct Env *env;
// call envid2env to translate envid passed in
if (envid2env(envid, &env, true) < 0) {
// bad environment
return -E_BAD_ENV;
}
// set status
env->env_status = status;
// panic("sys_env_set_status not implemented");
return 0;
}
// In sys_page_alloc():
// Allocate a page of memory and map it at 'va' with permission
// 'perm' in the address space of 'envid'.
// The page's contents are set to 0.
// If a page is already mapped at 'va', that page is unmapped as a
// side effect.
//
// perm -- PTE_U | PTE_P must be set, PTE_AVAIL | PTE_W may or may not be set,
// but no other bits may be set. See PTE_SYSCALL in inc/mmu.h.
//
// Return 0 on success, < 0 on error. Errors are:
// -E_BAD_ENV if environment envid doesn't currently exist,
// or the caller doesn't have permission to change envid.
// -E_INVAL if va >= UTOP, or va is not page-aligned.
// -E_INVAL if perm is inappropriate (see above).
// -E_NO_MEM if there's no memory to allocate the new page,
// or to allocate any necessary page tables.
static int sys_page_alloc(envid_t envid, void *va, int perm) {
// Hint: This function is a wrapper around page_alloc() and
// page_insert() from kern/pmap.c.
// Most of the new code you write should be to check the
// parameters for correctness.
// If page_insert() fails, remember to free the page you
// allocated!
// LAB 4: Your code here.
struct Env *env;
// check and get env
if (envid2env(envid, &env, true) < 0) {
// envid not exist or permission error
return -E_BAD_ENV;
}
// check va
if ((uintptr_t)va >= UTOP || PGOFF(va) != 0) {
// va above UTOP or va is not page-aligned
return -E_INVAL;
}
// check perm
if ((perm & (~PTE_SYSCALL)) || !(perm & PTE_U) || !(perm & PTE_P)) {
// invalid perm
return -E_INVAL;
}
// allocate page and then insert it
// check if out of memory
struct PageInfo *page;
if ((page = page_alloc(ALLOC_ZERO)) == NULL) {
// failed to allocate page
return -E_NO_MEM;
}
if (page_insert(env->env_pgdir, page, va, perm) < 0) {
// page table couldn't be allocated
// free ununsed page
page_free(page);
return -E_NO_MEM;
}
// panic("sys_page_alloc not implemented");
// page successfully allocated
return 0;
}
// In sys_page_map():
// Map the page of memory at 'srcva' in srcenvid's address space
// at 'dstva' in dstenvid's address space with permission 'perm'.
// Perm has the same restrictions as in sys_page_alloc, except
// that it also must not grant write access to a read-only
// page.
//
// Return 0 on success, < 0 on error. Errors are:
// -E_BAD_ENV if srcenvid and/or dstenvid doesn't currently exist,
// or the caller doesn't have permission to change one of them.
// -E_INVAL if srcva >= UTOP or srcva is not page-aligned,
// or dstva >= UTOP or dstva is not page-aligned.
// -E_INVAL is srcva is not mapped in srcenvid's address space.
// -E_INVAL if perm is inappropriate (see sys_page_alloc).
// -E_INVAL if (perm & PTE_W), but srcva is read-only in srcenvid's
// address space.
// -E_NO_MEM if there's no memory to allocate any necessary page tables.
static int sys_page_map(envid_t srcenvid, void *srcva, envid_t dstenvid,
void *dstva, int perm) {
// Hint: This function is a wrapper around page_lookup() and
// page_insert() from kern/pmap.c.
// Again, most of the new code you write should be to check the
// parameters for correctness.
// Use the third argument to page_lookup() to
// check the current permissions on the page.
// LAB 4: Your code here.
struct Env *srcenv, *dstenv;
// check environment
if ((envid2env(srcenvid, &srcenv, true) < 0) ||
(envid2env(dstenvid, &dstenv, true) < 0)) {
// srcenvid or dstenvid doesn't exist or
// caller doesn't have permissions
return -E_BAD_ENV;
}
// check srcva and dstva about address and page-aligned
// get srcenv and dstenv
if (((uintptr_t)srcva >= UTOP) || ((uintptr_t)dstva >= UTOP) ||
(PGOFF(srcva) != 0) || (PGOFF(dstva) != 0)) {
// addresses above UTOP or addresses not page_aligned
return -E_INVAL;
}
struct PageInfo *srcpage;
pte_t * scrpte_ptr;
// use page look up to get source page and corresponding pte_t *
if ((srcpage = page_lookup(srcenv->env_pgdir, srcva, &scrpte_ptr)) ==
NULL) {
// srcva not mapped in srcenvid's address space
return -E_INVAL;
}
// check perm passed in
if ((perm & (~PTE_SYSCALL)) || !(perm & PTE_U) || !(perm & PTE_P)) {
// invalid perm
return -E_INVAL;
}
// check if srcva is writable if perm has PTE_W
if ((perm & PTE_W) && (!((*scrpte_ptr) & PTE_W))) {
// perm has PTE_W while srcva ISN'T writable
return -E_INVAL;
}
// insert source page into dstenv's pgdir with perm
// check if out of memory
if (page_insert(dstenv->env_pgdir, srcpage, dstva, perm) < 0) {
// out of memory to allocate page table
return -E_NO_MEM;
}
// panic("sys_page_map not implemented");
// page successfully mapped
return 0;
}
// In sys_page_unmap():
// Unmap the page of memory at 'va' in the address space of 'envid'.
// If no page is mapped, the function silently succeeds.
//
// Return 0 on success, < 0 on error. Errors are:
// -E_BAD_ENV if environment envid doesn't currently exist,
// or the caller doesn't have permission to change envid.
// -E_INVAL if va >= UTOP, or va is not page-aligned.
static int sys_page_unmap(envid_t envid, void *va) {
// Hint: This function is a wrapper around page_remove().
// LAB 4: Your code here.
struct Env *env;
// check and get env
if (envid2env(envid, &env, true) < 0) {
// envid not exist or permission error
return -E_BAD_ENV;
}
// check va
if ((uintptr_t)va >= UTOP || PGOFF(va) != 0) {
// va above UTOP or va is not page-aligned
return -E_INVAL;
}
// call page_remove to unmap page
page_remove(env->env_pgdir, va);
// panic("sys_page_unmap not implemented");
// page successfully unmapped
return 0;
}
// In syscall():
// new code added
case SYS_exofork:
// call sys_exofork
return sys_exofork();
break;
case SYS_env_set_status:
// call sys_env_set_status
return sys_env_set_status((envid_t)a1, (int)a2);
break;
case SYS_page_alloc:
// call sys_page_alloc
return sys_page_alloc((envid_t)a1, (void *)a2, (int)a3);
break;
case SYS_page_map:
// call sys_page_map
return sys_page_map((envid_t)a1, (void *)a2, (envid_t)a3, (void *)a4,
(int)a5);
break;
case SYS_page_unmap:
// call sys_page_unmap
return sys_page_unmap((envid_t)a1, (void *)a2);
break;
第二部分 Copy-on-Write Fork - 写时复制Fork
Unix提供了一个fork()
系统调用作为其原始的进程创建原语。fork()
系统调用将调用进程的整个地址空间拷贝以创建子进程。
xv6通过将父进程页中的所有数据复制到子进程中的新页中来实现fork()
,这本质上就是dumbfowk()
采用的方法。对于父进程地址空间的拷贝是整个fork()
操作中最“贵”的部分。
然而,对于fork()的调用通常紧跟着一个对子进程的exec()
系统调用,该系统调用用一个新程序替换子进程的内存,这就是一个典型的shell所做的。在这种情况下,花费的用于复制父进程的地址空间的时间是被浪费的,因为子进程在调用exec()
前只会使用很少的内存。
出于以上的原因,Unix之后的版本利用了虚拟内存硬件去允许父进程和子进程共享映射到各自地址空间的内存,直到其中一个进程实际修改为止。该技术又被称作写时复制。
为了实现写时复制,在fork()
中内核仅仅从父进程拷贝地址的映射而非实际映射的页面到子进程,并且在同时将共享的页面标记为只读。
当两个进程之一尝试写入其中一个共享页面时,该过程触发页面错误。
内核意识到该页面实际上是一个“虚拟”或是“写时复制”页面,所以其会创建一个错误页面的新的,私有的,可写的页副本。
通过这种方式,直到实际写入时,独立页面的内容才被复制。
该过程使得紧接着exec()
的fork()
调用更加节约:子进程在调用exec()
很可能只会复制一页(栈的当前页)。
本实验的下一个部分要求完成一个合适的类Unix的fork()
的写时复制的实现(作为用户空间库例程)。在用户空间实现fork()
并且支持写时复制使得内核能保留相对简洁,因此更不容易出现严重的错误。这同样可以允许独立的用户模式程序实现自己的fork()
语义。
用户级别的页错误处理
用户级别的写时复制fork()
实现需要知道写保护页面的页错误。写时复制只是用户级别的页错误处理的诸多可能用处之一。
通常的做法是设置一个地址空间以便页错误指示何时需要采取某些行动。大多数Unix内核通常只会为新进程的栈区域分配一页,随着进程栈逐渐增长直至访问到了还未被映射的栈地址,触发页错误,然后内核会“按需”分配更多的栈页面。
典型的Unix内核必须追踪进程的空间中的不同区域发生页错误时所采取的行动。栈区域的页错误通常会导致新页的分配和映射。BSS区域的页错误通常会导致分配一个新页,填充0,并映射。对于具有按需分页的可执行文件的系统,text段的页错误会导致内核从磁盘上的二进制文件读取相应的页面并映射。
内核需要追踪大量的信息。与传统的Unix方法不同,本实验需要用户决定如何处理用户空间中的每个页面错误,而这些错误的损害通常不大。这种设计还为程序定义存储区域带来了极大的灵活性。之后将会应用用户级别的错误处理程序来映射和访问基于磁盘的文件系统上的文件。
设置页错误处理程序
为了处理用户环境自己的页错误,用户环境必须向JOS内核注册一个页错误处理程序入口。用户环境通过sys_env_set_pgfault_upcall()
的“上行”系统调用注册自己的错误处理程序入口。已经向struct Env
添加了新的域env_pgfault_upcall
来记录该信息。
练习8的代码如下,仅供参考:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32// In sys_env_set_pgfault_upcall():
// Set the page fault upcall for 'envid' by modifying the corresponding struct
// Env's 'env_pgfault_upcall' field. When 'envid' causes a page fault, the
// kernel will push a fault record onto the exception stack, then branch to
// 'func'.
//
// Returns 0 on success, < 0 on error. Errors are:
// -E_BAD_ENV if environment envid doesn't currently exist,
// or the caller doesn't have permission to change envid.
static int sys_env_set_pgfault_upcall(envid_t envid, void *func) {
// LAB 4: Your code here.
struct Env *env;
// check and translate the envid
if (envid2env(envid, &env, true) < 0) {
// envid not exist or permission error
return -E_BAD_ENV;
}
// set func
env->env_pgfault_upcall = func;
// panic("sys_env_set_pgfault_upcall not implemented");
return 0;
}
// In syscall():
// new code added
case SYS_env_set_pgfault_upcall:
// call sys_env_set_pgfault_upcall
return sys_env_set_pgfault_upcall((envid_t)a1, (void *)a2);
break;
用户环境中的正常栈和异常栈
在正常执行时,JOS中的用户环境会在正常栈中执行:正常栈的ESP寄存器指向USTACKTOP,并且压入的栈数据会存放于USTACKTOP - PGSIZE到USTACKTOP - 1的闭区域。
然而,当一个页错误在用户模式发生时,内核将重启用户环境,在一个另外的栈上运行一个特定的用户级别的页错误处理程序,该栈被称为用户异常栈。
实际上,将使JOS代表用户环境自动执行“栈切换”,就像x86处理器在用户模式转换到内核模式时已经代表JOS实现了栈切换一样。
JOS用户异常栈的大小也为一个页面大,栈顶被定义为指向虚拟地址UXSTACKTOP,用户异常栈的有效字节为UXSTACKTOP - PGSIZE到UXSTACKTOP - 1的闭区域。
当运行于异常栈上时,用户级别的页错误处理程序可以使用JOS的常规系统调用映射新的页或是调整页映射以修复任何可能导致页错误的问题。
然后用户级别的页错误处理程序通过汇编语言存根返回至原始栈上的错误代码处。
每一个想要支持用户级别页错误处理程序的用户环境需要自己为其异常栈通过sys_page_alloc()
分配内存。
唤醒用户页错误处理程序
现在需要改变kern/trap.c
中的页错误处理程序按特定的方式处理用户模式的页错误。
将错误时的用户环境状态叫做陷阱时状态。
若是没有页面错误处理函数被注册,则JOS内核会和之前一样,摧毁用户环境并输出一条消息。否则,内核会在异常栈设置在inc/trap.h
中如struct UTrapFrame
定义的陷阱帧。
然后内核会安排用户环境以页错误处理程序以上述栈帧在异常栈上恢复执行。fault_va
是导致也错误的虚拟地址。
如果当异常发生时用户环境已经在用户异常栈上运行,则错误处理程序自身发生了错误。在这种情况下,你应当在当前的tf->tf_esp
中而非UXSTACKTOP中重新启动栈帧。你应当首先压入一个4字节的字,然后一个struct UTrapFrame
。
检查tf->tf_esp是否在UXSTACKTOP - PGSIZE到UXSTACKTOP - 1的闭区间内来判断其是否已经在用户异常栈上。
练习9中遇到的问题:
curenv
已经被重新定义为thiscpu->cpu_env
,因此可以使用- 判断tf->tf_esp的if语句中,将与写成了或
练习9的代码如下,仅供参考:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43// In trap.c, page_fault_handler():
// LAB 4: Your code here.
struct UTrapframe *utf;
uintptr_t utf_addr;
// check tf->tf_esp's location to calculate utf's address
if ((tf->tf_esp >= UXSTACKTOP - PGSIZE) && (tf->tf_esp < UXSTACKTOP)) {
// recursive case
// reason why here needs to substract a 4-byte word
// is to reserve space for reset eip/esp
utf_addr = tf->tf_esp - sizeof(struct UTrapframe) - 4;
} else {
// non-recursive case, set utf_addr to the top of
// user exception stack
utf_addr = UXSTACKTOP - sizeof(struct UTrapframe);
}
if (curenv->env_pgfault_upcall) {
// page fault upcall exist
// use mem assert to check environment allocates the exception
// stack and has write permission to it, and stack ISN'T overflow
// combine three case with only one user_mem_assert to check utf_addr
// NB: curenv HAS been redefined as thiscpu->cpu_env
user_mem_assert(curenv, (void *)utf_addr, sizeof(struct UTrapframe),
PTE_U | PTE_W | PTE_P);
// set user stack frame
utf = (struct UTrapframe *)utf_addr;
utf->utf_fault_va = fault_va;
utf->utf_err = tf->tf_err;
utf->utf_regs = tf->tf_regs;
utf->utf_eip = tf->tf_eip;
utf->utf_eflags = tf->tf_eflags;
utf->utf_esp = tf->tf_esp;
// modify stack frame to set entry for env_pgfault_upcall
// and set address for user exception stack
tf->tf_eip = (uintptr_t)curenv->env_pgfault_upcall;
tf->tf_esp = utf_addr;
env_run(curenv);
}
用户模式页错误入口
然后需要实现汇编例程,该例程调用C页错误处理程序,然后在恢复在原始的错误指令处的执行。该例程就是被sys_env_set_pgfault_upcall()
注册的例程。
最后需要在C用户库实现用户模式的页错误处理机制。
练习10中遇到的问题:
- 需要想清楚思路,即先将eip“压入”陷阱时栈中,使用一个sub后接一个mov指令来模拟该压栈过程,若为递归过程,则会将eip填入之前留下的空白。然后依次恢复寄存器以及eflags。最后pop %esp以切换栈并调用ret返回
练习10的代码如下,仅供参考:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32// In pfentry.S:
// LAB 4: Your code here.
// push trap-time %eip to trap-time stack
// mov utf_eip to %eax
movl 0x28(%esp), %eax
// use sub and mov to *simulate* push
subl $0x4, 0x30(%esp)
movl 0x30(%esp), %ebp
movl %eax, (%ebp)
// pop the unused fault_va and err
popl %eax
popl %eax
// Restore the trap-time registers. After you do this, you
// can no longer modify any general-purpose registers.
// LAB 4: Your code here.
popal
// Restore eflags from the stack. After you do this, you can
// no longer use arithmetic operations or anything else that
// modifies eflags.
// LAB 4: Your code here.
addl $0x4, %esp
popfl
// Switch back to the adjusted trap-time stack.
// LAB 4: Your code here.
popl %esp
// Return to re-execute the instruction that faulted.
// LAB 4: Your code here.
ret
练习11中遇到的问题:
- 注意描述,要使用
_pgfault_upcall()
(汇编例程包装)来注册用户页处理函数而非传入的那个函数
练习11的代码如下,仅供参考:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16// In set_pgfault_handler():
if (_pgfault_handler == 0) {
// First time through!
// LAB 4: Your code here.
// alloc one page for user exception stack
// and register user-level page fault upcall
if (sys_page_alloc(thisenv->env_id, (void *)(UXSTACKTOP - PGSIZE),
PTE_U | PTE_W | PTE_P) < 0) {
panic("failed to alloc page for user exception stack!");
}
if (sys_env_set_pgfault_upcall(thisenv->env_id, _pgfault_upcall) < 0) {
panic("failed to register page fault upcall!");
}
// panic("set_pgfault_handler not implemented");
}
faultalloc能成功而faultallocbad失败的原因在于faultallocbad直接使用系统调用输出,该输出会使用先使用内存检查而失败;而faultalloc使用了用户库中的cprintf
,该函数在使用系统调用输出之前已经尝试对错误地址进行了解引用,触发了用户级别的缺页错误,在实际执行系统调用时,错误的地址已经被分配了页面,因此能通过内存检查。
实现写时复制
已经拥有了在用户空间中完整实现写时复制fork()
的内核设施基础。
现在已经在lib/fork.c
中给出了fork()
的框架。同dumbfork()
相似,fork()
首先创建了一个新环境,然后扫描父进程的地址空间并在子进程中建立相同的页映射。核心区别在于dumbfork()
复制页,而fork()
只实现页映射的复制。fork()
直到某环境尝试写时在复制对应的页。
fork()
的基本控制流为:
- 父进程设置
pgfault()
作为C级别的页错误处理程序,使用实现了的set_pgfault_handler()
函数 - 父进程调用
sys_exofork()
去创建子环境 - 对于地址空间中每一个在UTOP下的可写的或是写时复制的页面,父进程调用
duppage()
,该函数将该页面以写时复制映射进子进程然后在父进程的地址空间中重新映射该页面为写时复制(顺序很重要)。duppage()
同时设置PTE,所以页面将不可写。通过avail域的PTE_COW区分写时复制页面和真正的只读页面。然而,异常栈却不按该方式重映射。需要为子进程分配一个新页面作为异常栈。由于页处理程序实际执行了页复制并且页处理程序运行于异常栈上,异常栈不能写时复制。fork()
也需要处理存在但既不是写时复制也不是可写的页面。 - 父进程为子进程设置用户页错误入口。
- 子进程已经准备好运行了,有父进程将其标记为可运行。
每一次环境尝试向一个还不可写的写时复制的页面写入时,将出现页错误,用户页错误处理程序的控制流为:
- 内核将页错误传递给页错误上行调用,即调用了
fork()
的页错误处理程序。 pgfault()
检查错误为写入导致并且相应页面的PTE被标记为写时复制,否则使内核恐慌。pgfault()
分配一个新的页面,映射至一个临时位置,然后复制错误页面的内容到新分配的页面,最后将新页面以读写权限映射到合适的地址,替代旧的只读页。
用户级别的lib/fork.c
代码必须查询环境的页表(如查询某页面的PTE被标记为写时复制)。内核将环境的页表映射在UVPT正是为了这个目的。内核通过将页目录的指针指向自己的映射技巧使得可以为用户代码快速查找PTEs,lib/entry.S
已经设置了uvpd
和uvpd
,你可以快速在lib/fork.c
中查找页表信息。
练习12中遇到的问题:
- 注意在
pgfault()
和duppage()
中,必须使用0而不是thisenv->env_id
,原因已经在代码注释中标出 - 在
fork()
中需要显式地判断用户异常页并跳过duppage()
- 阅读给出的uvpt技巧,需要在代码中使用
- 为了简化错误处理,避免重复代码,使用了
goto
- 假定先将父进程的用户栈标记为COW,然后再尝试将子进程的用户栈标记为COW之前,可能会出现父进程的用户栈已经被写入的情况而触发缺页错误,因此
fork()
会立刻分配新页,拷贝内容并重新映射。子进程被标记COW时复制的页映射已经不是原来的页映射了,因此,后来对父进程做的改动会反映在子进程上,这显然是错误的 - fork后必须修改thisenv的值以保证用户程序行为正确
练习12的代码如下,仅供参考:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211// In lib/fork.c, fork():
//
// User-level fork with copy-on-write.
// Set up our page fault handler appropriately.
// Create a child.
// Copy our address space and page fault handler setup to the child.
// Then mark the child as runnable and return.
//
// Returns: child's envid to the parent, 0 to the child, < 0 on error.
// It is also OK to panic on error.
//
// Hint:
// Use uvpd, uvpt, and duppage.
// Remember to fix "thisenv" in the child process.
// Neither user exception stack should ever be marked copy-on-write,
// so you must allocate a new page for the child's user exception stack.
//
envid_t fork(void) {
// LAB 4: Your code here.
int r;
// use set_pgfault_handler to install pgfault() as page fault handler
set_pgfault_handler(pgfault);
// use system call to create new blank child environment
envid_t child_env_id = sys_exofork();
if (child_env_id == 0) {
// must update thisenv to make sure user program behave
// normally
thisenv = envs + ENVX(sys_getenvid());
}
if (child_env_id < 0) {
// error when create new environment, simply return
return child_env_id;
}
// traverse through parent's address space and use duppage
// to copy address mappings
uintptr_t addr;
for (addr = 0; addr < UTOP; addr += PGSIZE) {
if ((((pde_t *)uvpd)[PDX(addr)] & PTE_P) &&
(((pte_t *)uvpt)[PGNUM(addr)] & PTE_P)) {
// if both page directory entry and page table entry
// exist for one address(page-aligned), then call duppage
if (addr == (UXSTACKTOP - PGSIZE)) {
// ignore user exception stack
// for we will map a page for user exception later
continue;
}
if ((r = duppage(child_env_id, PGNUM(addr))) < 0) {
// duppage failed
// destroy child environment and return
goto error_handle;
}
}
}
// child must have its own exception stack
// so alloc page and map it for child here
if ((r = sys_page_alloc(child_env_id, (void *)(UXSTACKTOP - PGSIZE),
PTE_P | PTE_U | PTE_W)) < 0) {
// failed to set child environment's exception stack
// destroy child environment and return
goto error_handle;
}
// set child's page fault handler so that parent
// and child looks the same
// use sys_env_set_pgfault_upcall system call
extern void _pgfault_upcall(void);
if ((r = sys_env_set_pgfault_upcall(child_env_id, _pgfault_upcall)) < 0) {
// failed to set child's page fault upcall
// destroy child environment and return
goto error_handle;
}
// now child is ready to run, set child to ENV_RUNNABLE
// use sys_env_set_status system call
if ((r = sys_env_set_status(child_env_id, ENV_RUNNABLE)) < 0) {
// failed to change child environment's running status
// destroy child environment and return
goto error_handle;
}
// panic("fork not implemented");
return child_env_id;
// use goto to avoid meaningless error_handle code
// copy and paste everywhere
error_handle:
if (sys_env_destroy(child_env_id) < 0) {
// failed either, panic then
panic("failed to destroy child environment!");
}
return r;
}
// In lib/fork.c, duppage():
//
// Map our virtual page pn (address pn*PGSIZE) into the target envid
// at the same virtual address. If the page is writable or copy-on-write,
// the new mapping must be created copy-on-write, and then our mapping must be
// marked copy-on-write as well. (Exercise: Why do we need to mark ours
// copy-on-write again if it was already copy-on-write at the beginning of
// this function?)
//
// Returns: 0 on success, < 0 on error.
// It is also OK to panic on error.
//
static int duppage(envid_t envid, unsigned pn) {
int r;
// LAB 4: Your code here.
int perm = PTE_P | PTE_U;
if ((((pte_t *)uvpt)[pn] & (PTE_W | PTE_COW))) {
// check for writable or copy-on-write page
// and add perm with PTE_COW
perm |= PTE_COW;
// NB: we also use 0 to replace thisenv->env_id
// for in unix-like fork() implementation, we will copy
// address space from parent process to child process
// at current time, thisenv->env_id IS ABSOLUTELY WRONG
// map from parent environment to child environment
if (sys_page_map(0, (void *)(pn * PGSIZE), envid, (void *)(pn * PGSIZE),
perm) < 0) {
panic("failed to map page from parent to child!");
}
// remap parent's environment
if (sys_page_map(0, (void *)(pn * PGSIZE), 0, (void *)(pn * PGSIZE),
perm) < 0) {
panic("failed to remap page in parent!");
}
} else {
// for other pages, simply map from
// parent environment to child environment
if (sys_page_map(0, (void *)(pn * PGSIZE), envid, (void *)(pn * PGSIZE),
perm) < 0) {
panic("failed to map page from parent to child!");
}
}
// panic("duppage not implemented");
return 0;
}
// In lib/fork.c, pgfault():
//
// Custom page fault handler - if faulting page is copy-on-write,
// map in our own private writable copy.
//
static void pgfault(struct UTrapframe *utf) {
void * addr = (void *)utf->utf_fault_va;
uint32_t err = utf->utf_err;
int r;
// Check that the faulting access was (1) a write, and (2) to a
// copy-on-write page. If not, panic.
// Hint:
// Use the read-only page table mappings at uvpt
// (see <inc/memlayout.h>).
// LAB 4: Your code here.
// use uvpt to get PTE and check premissions
if (!(err & FEC_WR) || !(((pte_t *)uvpt)[PGNUM(addr)] & PTE_COW)) {
panic("fault isn't write or PTE is not marked as PTE_COW or both!");
}
// Allocate a new page, map it at a temporary location (PFTEMP),
// copy the data from the old page to the new page, then move the new
// page to the old page's address.
// Hint:
// You should make three system calls.
// LAB 4: Your code here.
// allocate a page and map it at PFTEMP
// use sys_page_alloc system call
// NB: use 0 instead of thisenv->env_id
// for normally when child environment scheduled by JOS
// first it tries to store 0(return value of fork()) to
// local variable child_env_id on the stack
// which will take a page fault and kernel will trasfer control
// to pgfault here
// thisenv->env_id IS ABSOLUTELY WRONG here, so use 0
// and let kernel convert from 0 to correct env_id
if ((r = sys_page_alloc(0, (void *)PFTEMP, PTE_P | PTE_U | PTE_W)) < 0) {
panic("failed to allocate page at temp location - %e, %x!", r,
thisenv->env_id);
}
addr = ROUNDDOWN(addr, PGSIZE);
// use memcpy to copy from fault address's page to PFTEMP's newly
// allocated page
memcpy((void *)PFTEMP, addr, PGSIZE);
// use sys_page_map system call to map newly allocated
// page at PFTEMP at addr with read/write permissions
if ((r = sys_page_map(0, (void *)PFTEMP, 0, addr, PTE_P | PTE_U | PTE_W)) <
0) {
panic("failed to map new page - %e!", r);
}
// use sys_page_unmap system call to unmap page at
// temp location PFTEMP
if ((r = sys_page_unmap(0, (void *)PFTEMP)) < 0) {
panic("failed to unmap page at PFTEMP - %e!", r);
}
// panic("pgfault not implemented");
}
第三部分 - Preemptive Multitasking and Inter-Process communication (IPC) 抢占式多任务和进程间通信
最后一部分要求你修改内核,实现抢占式非协作环境以及允许环境显式传递信息。
时钟中断和抢占
运行user/spin
测试程序,该测试程序fork
一个用户环境,生成的子环境一旦获得了CPU,就会执行一个无限循环,无论是父环境还是内核都无法重新获得CPU。
对于从错误和恶意代码中保护系统而言,这不是一个理想的情况,因为任何用户环境都可以通过一个无限循环以及永不放弃使得整个系统停机。为了允许内核抢占一个用户环境并强制从该环境处获取CPU的控制,需要扩展JOS内核以支持时钟硬件的外部硬件中断。
中断规则
外部中断(如设备中断)被称为IRQs(Interrupt request)。总共有16种可能的IRQs,分别从0编号到15。从IRQ号到IDT入口的映射并不固定。picirq.c
中的pic_init()
将IRQs从0到15映射到了IRQ_OFFSET到IRQ_OFFSET + 15。
在inc/trap.h
中,IRQ_OFFSET被定义为32,因此IDT入口32-47对应于IRQ的0-15。例如,始终中断是IRQ 0。因此,IDT[IRQ_OFFSET + 0]包含了内核中时钟中断处理程序的地址。IRQ_OFFSET设置为32后,设备中断将不会和处理器异常号重叠。
同xv6 Unix相比,JOS内核提供了一个关键性简化。在内核中时,外部设备中断永远是被禁止的(但是在用户环境中是被允许的)。外部中断被%eflags寄存器的FL_IF标记控制。当该bit被设置时,外部中断被允许。尽管有若干种方式可以修改该bit位,将在进入和退出用户模式时通过保存和恢复%eflags寄存器的过程来处理它。
必须确保在用户环境中FL_IF标记被设置,这样当中断到达后,并且将中断传递给处理器,并由内核中的中断处理程序处理。否则中断会被屏蔽和忽略之道中断重新被允许。在引导程序的第一条指定就屏蔽了外部设备中断,并且直到目前为止,还没有重新启用。
练习13中遇到的问题:
- 注意到简化规则要求中说明必须在陷入内核时将IF置0,所以必须修改之前的代码,将所有门都改为中断门
练习13的代码如下,仅供参考:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57// In kern/trapentry.S:
// externel interrupts
TRAPHANDLER_NOEC(irq_0_handler, IRQ_OFFSET + 0);
TRAPHANDLER_NOEC(irq_1_handler, IRQ_OFFSET + 1);
TRAPHANDLER_NOEC(irq_2_handler, IRQ_OFFSET + 2);
TRAPHANDLER_NOEC(irq_3_handler, IRQ_OFFSET + 3);
TRAPHANDLER_NOEC(irq_4_handler, IRQ_OFFSET + 4);
TRAPHANDLER_NOEC(irq_5_handler, IRQ_OFFSET + 5);
TRAPHANDLER_NOEC(irq_6_handler, IRQ_OFFSET + 6);
TRAPHANDLER_NOEC(irq_7_handler, IRQ_OFFSET + 7);
TRAPHANDLER_NOEC(irq_8_handler, IRQ_OFFSET + 8);
TRAPHANDLER_NOEC(irq_9_handler, IRQ_OFFSET + 9);
TRAPHANDLER_NOEC(irq_10_handler, IRQ_OFFSET + 10);
TRAPHANDLER_NOEC(irq_11_handler, IRQ_OFFSET + 11);
TRAPHANDLER_NOEC(irq_12_handler, IRQ_OFFSET + 12);
TRAPHANDLER_NOEC(irq_13_handler, IRQ_OFFSET + 13);
TRAPHANDLER_NOEC(irq_14_handler, IRQ_OFFSET + 14);
TRAPHANDLER_NOEC(irq_15_handler, IRQ_OFFSET + 15);
// In kern/trap.c, trap_init():
// set up interupt gate descriptor
SETGATE(idt[IRQ_OFFSET + 0], 0, GD_KT, irq_0_handler, 0);
SETGATE(idt[IRQ_OFFSET + 1], 0, GD_KT, irq_1_handler, 0);
SETGATE(idt[IRQ_OFFSET + 2], 0, GD_KT, irq_2_handler, 0);
SETGATE(idt[IRQ_OFFSET + 3], 0, GD_KT, irq_3_handler, 0);
SETGATE(idt[IRQ_OFFSET + 4], 0, GD_KT, irq_4_handler, 0);
SETGATE(idt[IRQ_OFFSET + 5], 0, GD_KT, irq_5_handler, 0);
SETGATE(idt[IRQ_OFFSET + 6], 0, GD_KT, irq_6_handler, 0);
SETGATE(idt[IRQ_OFFSET + 7], 0, GD_KT, irq_7_handler, 0);
SETGATE(idt[IRQ_OFFSET + 8], 0, GD_KT, irq_8_handler, 0);
SETGATE(idt[IRQ_OFFSET + 9], 0, GD_KT, irq_9_handler, 0);
SETGATE(idt[IRQ_OFFSET + 10], 0, GD_KT, irq_10_handler, 0);
SETGATE(idt[IRQ_OFFSET + 11], 0, GD_KT, irq_11_handler, 0);
SETGATE(idt[IRQ_OFFSET + 12], 0, GD_KT, irq_12_handler, 0);
SETGATE(idt[IRQ_OFFSET + 13], 0, GD_KT, irq_13_handler, 0);
SETGATE(idt[IRQ_OFFSET + 14], 0, GD_KT, irq_14_handler, 0);
SETGATE(idt[IRQ_OFFSET + 15], 0, GD_KT, irq_15_handler, 0);
// In kern/env.c, env_alloc():
// Enable interrupts while in user mode.
// LAB 4: Your code here.
// simply use bit or to set FL_IF
e->env_tf.tf_eflags |= FL_IF;
// In kern/sched.c, sched_halt():
// Reset stack pointer, enable interrupts and then halt.
asm volatile (
"movl $0, %%ebp\n"
"movl %0, %%esp\n"
"pushl $0\n"
"pushl $0\n"
// Uncomment the following line after completing exercise 13
"sti\n"
"1:\n"
"hlt\n"
"jmp 1b\n"
: : "a" (thiscpu->cpu_ts.ts_esp0));
处理时钟中断
在user/spin
程序中,在子环境第一次运行后,会陷入无限循环,而内核将无法得到控制权,需要对硬件编程以定时产生时钟中断,该终端会将控制权强行切回CPU,然后可以将控制权交给另一个用户环境。
对于lapic_init()
和pic_init()
的调用已经写好,它们会设置时钟并且终端控制器会定时生成中断。需要完成代码处理这些中断。
练习14中遇到的问题:
- 首先需要调用
lapic_eoi()
以确认接收到了中断,否则会一直卡住
练习14的代码如下,仅供参考:1
2
3
4
5
6
7
8
9
10// in kern/trap.c, trap_dispatch():
// new code added
if (tf->tf_trapno == IRQ_OFFSET + IRQ_TIMER) {
// dispatch clock interupts
// call sched_yield() to find and run a different environment
// NB: should first call lapic_eoi() to ACKNOWLEDGE interupt
lapic_eoi();
sched_yield();
// sched_yield() might not return
}
进程间通讯
到目前位置实验一直关注于操作系统的独立部分,操作系统通过虚拟化提供了一种每个环境都独占整个机器的“错觉”。然而操作系统的另一项服务是允许程序在必要的时候通讯,该功能非常强大,Unix管道就是一个典型的例子。
有许多进程间通讯的模型,即使是现在也有许多关于哪一种模型是最好的争论。我们不参与争论。相反,我们将实现一个简单的IPC模型。
JOS中的IPC
你将实现一些额外的JOS内核系统调用,这些调用共同提供一个简单的进程间通讯机制。具体的系统调用为sys_ipc_recv()
和sys_ipc_try_send()
,然后你将实现两个库包装ipc_recv()
和ipc_send()
。
用户环境通过JOS的IPC机制向其他环境互相发送的信息包括两部分,一个32-bit的值和一个可选的单页映射。允许环境传递页映射提供了一个比传递一个32-bit的整数更有效的数据交换方式,这也可以较为方便的设置共享内存。
发送和接收消息
环境调用sys_ipc_recv()
去接受消息。该系统调用取消调度当前环境并且直到一条消息被收到后才会被继续运行。当一个环境等待接收消息时,任何其他的环境可以向其发送一条消息 - 并不限于特定的环境,也不限于父子进程间传递消息,换言之,你在第一部分中实现的权限检查将不适用于IPC,因为IPC系统调用经过精心设计以保证安全,环境不会通过简单地发送消息而导致其他环境出现错误(除非目标环境也有错误)。
环境调用sys_ipc_try_send()
去尝试发送一个值(并附带上接受者的环境ID),如果指定的环境实际上正在接受该值(已调用sys_ipc_recv()
并且尚未获得值),则值成功发送并返回0。否则,返回-E_IPC_NOT_RECV表示目标环境当前不期望接收值。
用户空间的库函数ipc_recv()
将负责调用sys_ipc_recv()
并在当前环境的struct Env
中查找接收值的信息。
用户空间的库函数ipc_send()
将负责反复调用sys_ipc_try_send()
直到发送成功。
传输页
当一个环境以一个有效的dstva
参数(小于UTOP)调用sys_ipc_recv()
时,环境改变状态以表明想要接受一个页映射。如果发送者发送了一个页,那么该页将会被映射到接收者地址空间的dstva
处。如果接收者已经在dstva
映射了一页,那么之前的页会被解映射。
当一个环境以一个有效的srcva
参数(小于UTOP)调用sys_ipc_try_send()
时,表明发送者想要将当前映射在srcva
的页发送给接收者,并且带有权限perm
。在一次成功的IPC之后,发送者在其地址空间保留在srcva
处原有的页映射,但是接收者也持有最开始由发送者指定的相同的物理页面在dstva
的映射。最终,该页面在发送者和接收者之间共享。
如果发送者和接收者两者之一没有表明要传输页面,那么将不会有页面被传输。在任意一次IPC之后将接收者的struct Env
中新的域env_ipc_perm
设置为接受页面的权限,如果没有收到页面,则为0。
实现IPC
练习15中遇到的问题:
- 在
sys_ipc_try_send()
和sys_ipc_recv()
中,均不将超过UTOP的地址标记为错误,超过UTOP的地址是无效的,但并不会导致系统调用失败 - 在
sys_ipc_recv()
中在接收到消息前将环境状态置为不可运行,那么在收到消息后,应当恢复到可运行的状态 - 在
sys_ipc_try_send()
中调用page_lookup()
时,要记住检查的是源地址的映射,因此应当使用当前环境的页表而非接收者环境的页表
练习15的代码如下,仅供参考:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248// In kern/syscall.c, sys_ipc_recv():
// Block until a value is ready. Record that you want to receive
// using the env_ipc_recving and env_ipc_dstva fields of struct Env,
// mark yourself not runnable, and then give up the CPU.
//
// If 'dstva' is < UTOP, then you are willing to receive a page of data.
// 'dstva' is the virtual address at which the sent page should be mapped.
//
// This function only returns on error, but the system call will eventually
// return 0 on success.
// Return < 0 on error. Errors are:
// -E_INVAL if dstva < UTOP but dstva is not page-aligned.
static int sys_ipc_recv(void *dstva) {
// LAB 4: Your code here.
// similar to sys_ipc_try_send, will not mark an error
// if dstva >= UTOP
if ((uintptr_t)dstva < UTOP && PGOFF(dstva) != 0) {
// if dstva < UTOP but dstva is not page-aligned
// NB: && rather than ||, we won't report error
// when dstva >= UTOP
return -E_INVAL;
}
// mark calling environment as not runnable
curenv->env_status = ENV_NOT_RUNNABLE;
// update status to mark receiver is willing to receive message
curenv->env_ipc_recving = true;
curenv->env_ipc_dstva = dstva;
// give up cpu
sys_yield();
// panic("sys_ipc_recv not implemented");
// ipc succeeds
return 0;
}
// In kern/syscall.c, sys_ipc_try_send():
// Try to send 'value' to the target env 'envid'.
// If srcva < UTOP, then also send page currently mapped at 'srcva',
// so that receiver gets a duplicate mapping of the same page.
//
// The send fails with a return value of -E_IPC_NOT_RECV if the
// target is not blocked, waiting for an IPC.
//
// The send also can fail for the other reasons listed below.
//
// Otherwise, the send succeeds, and the target's ipc fields are
// updated as follows:
// env_ipc_recving is set to 0 to block future sends;
// env_ipc_from is set to the sending envid;
// env_ipc_value is set to the 'value' parameter;
// env_ipc_perm is set to 'perm' if a page was transferred, 0 otherwise.
// The target environment is marked runnable again, returning 0
// from the paused sys_ipc_recv system call. (Hint: does the
// sys_ipc_recv function ever actually return?)
//
// If the sender wants to send a page but the receiver isn't asking for one,
// then no page mapping is transferred, but no error occurs.
// The ipc only happens when no errors occur.
//
// Returns 0 on success, < 0 on error.
// Errors are:
// -E_BAD_ENV if environment envid doesn't currently exist.
// (No need to check permissions.)
// -E_IPC_NOT_RECV if envid is not currently blocked in sys_ipc_recv,
// or another environment managed to send first.
// -E_INVAL if srcva < UTOP but srcva is not page-aligned.
// -E_INVAL if srcva < UTOP and perm is inappropriate
// (see sys_page_alloc).
// -E_INVAL if srcva < UTOP but srcva is not mapped in the caller's
// address space.
// -E_INVAL if (perm & PTE_W), but srcva is read-only in the
// current environment's address space.
// -E_NO_MEM if there's not enough memory to map srcva in envid's
// address space.
static int sys_ipc_try_send(envid_t envid, uint32_t value, void *srcva,
unsigned perm) {
// LAB 4: Your code here.
// check for existence of env with envid
struct Env *env;
if (envid2env(envid, &env, false) < 0) {
// environment envid doesn't currently exist
return -E_BAD_ENV;
}
// check receiver's status
if (!env->env_ipc_recving) {
// envid is not currently blocked in sys_ipc_recv
// or another environment managed to send first
return -E_IPC_NOT_RECV;
}
if ((uintptr_t)srcva < UTOP) {
// srcva is valid
// check srcva is page-aligned
if (PGOFF(srcva) != 0) {
// srcva is not page-aligned
return -E_INVAL;
}
// check perm passed in
if ((perm & (~PTE_SYSCALL)) || !(perm & PTE_U) || !(perm & PTE_P)) {
// invalid perm
return -E_INVAL;
}
// check srcva actually be mapped
pte_t * pgtable_entry;
struct PageInfo *page;
if ((page = page_lookup(curenv->env_pgdir, srcva, &pgtable_entry)) ==
NULL) {
// NB: check for SRCVA here, so use curenv->env_pgdir
// srcva is not mapped at caller's address space
return -E_INVAL;
}
// check PTE_W between sender and receiver
if ((perm & PTE_W) && (!((*pgtable_entry) & PTE_W))) {
// if perm has PTE_W while srcva is read-only in
// current environment's address space
return -E_INVAL;
}
if ((uintptr_t)(env->env_ipc_dstva) < UTOP) {
// receiver also expects page to be transferred
// call page_insert to actually transer the page
if (page_insert(env->env_pgdir, page, env->env_ipc_dstva, perm) <
0) {
// out of memory when try to insert page
return -E_NO_MEM;
}
// page successfully transferred
env->env_ipc_perm = perm;
}
} else {
// srcva >= UTOP
// invalid srcva, but we won't return error here
// ipc_send will panic if we do so
}
// update receiver's information
env->env_ipc_recving = false;
env->env_ipc_from = curenv->env_id;
env->env_ipc_value = value;
// mark receiver to be runnable to match what we do in sys_ipc_recv
env->env_status = ENV_RUNNABLE;
// modify trapframe's eax register to `update' return value
env->env_tf.tf_regs.reg_eax = 0;
// panic("sys_ipc_try_send not implemented");
// ipc succeeds
return 0;
}
// In lib/ipc.c, ipc_recv():
// Receive a value via IPC and return it.
// If 'pg' is nonnull, then any page sent by the sender will be mapped at
// that address.
// If 'from_env_store' is nonnull, then store the IPC sender's envid in
// *from_env_store.
// If 'perm_store' is nonnull, then store the IPC sender's page permission
// in *perm_store (this is nonzero if a page was successfully
// transferred to 'pg').
// If the system call fails, then store 0 in *fromenv and *perm (if
// they're nonnull) and return the error.
// Otherwise, return the value sent by the sender
//
// Hint:
// Use 'thisenv' to discover the value and who sent it.
// If 'pg' is null, pass sys_ipc_recv a value that it will understand
// as meaning "no page". (Zero is not the right value, since that's
// a perfectly valid place to map a page.)
int32_t ipc_recv(envid_t *from_env_store, void *pg, int *perm_store) {
// LAB 4: Your code here.
// check pg, if pg is null, then should set it to UTOP,
// for only if dstva is below UTOP, sender knows that
// receiver wants a page to be transferred
if (!pg) { pg = (void *)UTOP; }
int r;
if ((r = sys_ipc_recv(pg)) < 0) {
// system call fails
// store from env and perm if corresponding pointer is not null
if (from_env_store) { *from_env_store = 0; }
if (perm_store) { *perm_store = 0; }
// return the error
return r;
}
// system call succeeds
// extract information from thisenv's IPC fields
if (from_env_store) {
// store IPC sender's envid into *from_env_store
*from_env_store = thisenv->env_ipc_from;
}
if (perm_store) {
// store IPC sender's page permission
*perm_store = thisenv->env_ipc_perm;
}
// panic("ipc_recv not implemented");
// return the value send by the sender
return thisenv->env_ipc_value;
}
// In kern/syscall.c, syscall():
// new code added
case SYS_ipc_recv:
// call sys_ipc_recv
return sys_ipc_recv((void *)a1);
break;
case SYS_ipc_try_send:
// call sys_ipc_try_send
return sys_ipc_try_send((envid_t)a1, (uint32_t)a2, (void *)a3,
(unsigned)a4);
// In lib/ipc.c, ipc_send():
// Send 'val' (and 'pg' with 'perm', if 'pg' is nonnull) to 'toenv'.
// This function keeps trying until it succeeds.
// It should panic() on any error other than -E_IPC_NOT_RECV.
//
// Hint:
// Use sys_yield() to be CPU-friendly.
// If 'pg' is null, pass sys_ipc_try_send a value that it will understand
// as meaning "no page". (Zero is not the right value.)
void ipc_send(envid_t to_env, uint32_t val, void *pg, int perm) {
// LAB 4: Your code here.
// check pg, if pg is null, then should set it to UTOP(invalid value
// but won't result in panic)
if (!pg) { pg = (void *)UTOP; }
int r;
while (true) {
// keeps trying until succeeds or panic
r = sys_ipc_try_send(to_env, val, pg, perm);
if (r == 0) {
// message successfully sent, return
return;
}
if (r != -E_IPC_NOT_RECV) {
// error when sending, should panic
panic("failed to send messages - %e!", r);
}
// use sys_yield to avoid waste on CPU
sys_yield();
}
// panic("ipc_send not implemented");
}
实验小结
最终执行make grade
,评分脚本的输出如下:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52dumbfork:
$ make run-dumbfork-nox-gdb QEMUEXTRA+=-snapshot
OK (1.6s)
Part A score: 5/5
faultread:
$ make run-faultread-nox-gdb QEMUEXTRA+=-snapshot
OK (1.0s)
faultwrite:
$ make run-faultwrite-nox-gdb QEMUEXTRA+=-snapshot
OK (1.0s)
faultdie:
$ make run-faultdie-nox-gdb QEMUEXTRA+=-snapshot
OK (1.0s)
faultregs:
$ make run-faultregs-nox-gdb QEMUEXTRA+=-snapshot
OK (1.1s)
faultalloc:
$ make run-faultalloc-nox-gdb QEMUEXTRA+=-snapshot
OK (4.6s)
faultallocbad:
$ make run-faultallocbad-nox-gdb QEMUEXTRA+=-snapshot
OK (1.3s)
faultnostack:
$ make run-faultnostack-nox-gdb QEMUEXTRA+=-snapshot
OK (1.8s)
faultbadhandler:
$ make run-faultbadhandler-nox-gdb QEMUEXTRA+=-snapshot
OK (1.2s)
faultevilhandler:
$ make run-faultevilhandler-nox-gdb QEMUEXTRA+=-snapshot
OK (1.0s)
forktree:
$ make run-forktree-nox-gdb QEMUEXTRA+=-snapshot
OK (1.4s)
Part B score: 50/50
spin:
$ make run-spin-nox-gdb QEMUEXTRA+=-snapshot
OK (0.7s)
stresssched:
$ make run-stresssched-nox-gdb CPUS=4 QEMUEXTRA+=-snapshot
OK (3.0s)
sendpage:
$ make run-sendpage-nox-gdb CPUS=2 QEMUEXTRA+=-snapshot
OK (1.7s)
pingpong:
$ make run-pingpong-nox-gdb CPUS=4 QEMUEXTRA+=-snapshot
OK (2.1s)
primes:
$ make run-primes-nox-gdb CPUS=4 QEMUEXTRA+=-snapshot
OK (4.5s)
Part C score: 25/25
Score: 80/80
至此,实验四结束。
第四个实验是第一到第四个实验中难度最大、代码量最多的一个实验,在前三个实验中任何的代码错误(尽管可能在当时的实验评分中没有反映出来)都会导致实验四出现难以调试的错误。
实验四中每个练习遇到的问题我都已经在报告中总结,此外,也在代码中以NB标明。其中大部分都是一些小错误,还有少部分是没有将问题考虑全面导致的。尽管如此,几乎每一个错误我都花费了至少几十分钟去调试。
操作系统的调试不仅仅困难(Triple Fault会导致不打补丁的qemu重启),任何一个小错误都会导致系统崩溃。现代操作系统如Linux的鲁棒性可见一斑。
本学期的事情比起上学期只增不减,加上课程设计只要求完成四个实验,实验四结束后,MIT-6.828系列就告一段落了。
希望我的这四篇实验报告能对正在写实验的你产生些许的启发,也欢迎找我交流相关的问题。