You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

463 lines
13 KiB

This file contains invisible Unicode characters!

This file contains invisible Unicode characters that may be processed differently from what appears below. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to reveal hidden characters.

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

## 第六章实验5进程的封装
### 6.1 实验内容
实验要求在APP里写fork调用其执行过程将fork出一个子进程。在代理内核中实现fork的处理例程trap使其能够支撑APP程序的正确执行。
在本次实验的app4.c文件中将会测试fork函数。代码中170及172系统调用分别对应着sys_fork和sys_getpid系统调用。调用fork函数后将会有两个返回。在父进程中fork返回新创建子进程的进程ID而在子进程中fork返回0。你需要阅读proc.c文件完善相关代码是的app4.c可以正常运行。
**6.1.1 练习一alloc_proc需要编程**
完善"pk/proc.c"中的alloc_proc(),你需要对以下属性进行初始化:
l enum proc_state state;
l int pid;
l int runs;
l uintptr_t kstack;
l volatile bool need_resched;
l struct proc_struct *parent;
l struct mm_struct *mm;
l struct context context;
l struct trapframe *tf;
l uintptr_t cr3;
l uint32_t flags;
l char name[PROC_NAME_LEN + 1];
**6.1.2 练习二do_fork需要编程**
完善"pk/proc.c"中的do_fork函数你需要进行以下工作
l 调用alloc_proc()来为子进程创建进程控制块
l 调用setup_kstack来设置栈空间
l 用copy_mm来拷贝页表
l 调用copy_thread来拷贝进程
l 为子进程设置pid
l 设置子进程状态为就绪
l 将子进程加入到链表中
完成以上代码后,你可以进行如下测试,然后输入如下命令:
`$ riscv64-unknown-elf-gcc ../app/app5.c -o ../app/elf/app5`
`$ spike ./obj/pke app/elf/app5`
预期的输出如下:
```
PKE IS RUNNING
page fault vaddr:0x00000000000100c2
page fault vaddr:0x000000000001e17f
page fault vaddr:0x0000000000018d5a
page fault vaddr:0x000000000001a8ba
page fault vaddr:0x000000000001d218
page fault vaddr:0x000000007f7e8bf0
page fault vaddr:0x0000000000014a68
page fault vaddr:0x00000000000162ce
page fault vaddr:0x000000000001c6e0
page fault vaddr:0x0000000000012572
page fault vaddr:0x0000000000011fa6
page fault vaddr:0x0000000000019064
page fault vaddr:0x0000000000015304
page fault vaddr:0x0000000000017fd4
this is farther process;my pid = 1
sys_exit pid=1
page fault vaddr:0x0000000000010166
page fault vaddr:0x000000000001e160
page fault vaddr:0x000000000001d030
page fault vaddr:0x0000000000014a68
page fault vaddr:0x00000000000162ce
page fault vaddr:0x000000000001c6e0
page fault vaddr:0x0000000000012572
page fault vaddr:0x0000000000011fa6
page fault vaddr:0x0000000000019064
page fault vaddr:0x000000000001abb6
page fault vaddr:0x0000000000015304
page fault vaddr:0x0000000000017fd4
page fault vaddr:0x0000000000018cd4
this is child process;my pid = 2
sys_exit pid=2
```
如果你的app可以正确输出的话那么运行检查的python脚本
`./pke-lab5`
若得到如下输出,那么恭喜你,你已经成功完成了实验六!!!
```
build pk : OK
running app5 : OK
test fork : OK
Score: 20/20
```
### 6.2 基础知识
**6.2.1 进程结构**
在pk/proc.h中我们定义进程的结构如下
```
42 struct proc_struct {
43 enum proc_state state;
44 int pid;
45 int runs;
46 uintptr_t kstack;
47 volatile bool need_resched;
48 struct proc_struct *parent;
50 struct context context;
51 trapframe_t *tf;
52 uintptr_t cr3;
53 uint32_t flags;
54 char name[PROC_NAME_LEN + 1];
55 list_entry_t list_link;
56 list_entry_t hash_link;
57 };
```
可以看到在41行的枚举中我们定义了进程的四种状态其定义如下
```
11 enum proc_state {
12 PROC_UNINIT = 0,
13 PROC_SLEEPING,
14 PROC_RUNNABLE,
15 PROC_ZOMBIE,
16 };
```
四种状态分别为未初始化(PROC_UNINIT)、睡眠PROC_SLEEPING、可运行PROC_RUNNABLE以及僵死PROC_ZOMBIE状态。
除却状态,进程还有以下重要属性:
l pid进程id是进程的标识符
l runs进程已经运行的时间
l kstack进程的内核栈空间
l need_resched是否需要释放CPU
l parent进程的父进程
l context进程的上下文
l tf当前中断的栈帧
l cr3进程的页表地址
l name进程名
除了上述属性可以看到在55、56行还维护了两个进程的链表这是操作系统内进程的组织方式系统维护一个进程链表以组织要管理的进程。
**6.2.2 设置第一个内核进程idleproc**
在"pk/pk.c"的rest_of_boot_loader函数中调用了proc_init来设置第一个内核进程
```
317 void
318 proc_init() {
319 int i;
320 extern uintptr_t kernel_stack_top;
321
322 list_init(&proc_list);
323 for (i = 0; i < HASH_LIST_SIZE; i ++) {
324 list_init(hash_list + i);
325 }
326
327 if ((idleproc = alloc_proc()) == NULL) {
328 panic("cannot alloc idleproc.\n");
329 }
330
331 idleproc->pid = 0;
332 idleproc->state = PROC_RUNNABLE;
333 idleproc->kstack = kernel_stack_top;
334 idleproc->need_resched = 1;
335 set_proc_name(idleproc, "idle");
336 nr_process ++;
337
338 currentproc = idleproc;
339
340 }
```
322行的proc_list是系统所维护的进程链表324行的hash_list是一个大小为1024的list_entry_t的hash数组。在对系统所维护的两个list都初始化完成后系统为idleproc分配进程结构体。然后对idleproc的各个属性进行设置最终将currentproc改为idleproc。
在上述代码中我们只是为idleproc分配了进程控制块但并没有切换到idleproc真正的切换代码在proc_init函数后面的run_loaded_program以及cpu_idle函数中进行。
**6.2.3 do_fork**
在run_loaded_program中有如下代码
```
140 trapframe_t tf;
141 init_tf(&tf, current.entry, stack_top);
142 __clear_cache(0, 0);
143 do_fork(0,stack_top,&tf);
144 write_csr(sscratch, kstack_top);
```
在这里声明了一个trapframe并且将它的gpr[2]sp设置为内核栈指针将它的epc设置为current.entry其中current.entry是elf文件的入口地址也就是app的起始执行位置随即我们调用了do_frok函数其中传入参数stack为0表示我们正在fork一个内核进程。
在do_frok函数中你会调用alloc_proc()来为子进程创建进程控制块、调用setup_kstack来设置栈空间调用copy_mm来拷贝页表调用copy_thread来拷贝进程。现在我们来对以上函数进行分析。
setup_kstack函数代码如下在函数中我们为进程分配栈空间然后返回
```
210 static int
211 setup_kstack(struct proc_struct *proc) {
212 proc->kstack = (uintptr_t)__page_alloc();
213 return 0;
214 }
```
copy_mm k函数代码如下在函数中我们对页表进行拷贝。
```
228 static int
229 copy_mm(uint32_t clone_flags, struct proc_struct *proc) {
230 //assert(currentproc->mm == NULL);
231 /* do nothing in this project */
232 uintptr_t cr3=(uintptr_t)__page_alloc();
233 memcpy((void *)cr3,(void *)proc->cr3,RISCV_PGSIZE);
234 proc->cr3=cr3;
235 return 0;
236 }
```
最后是copy_thread函数
```
240 static void
241 copy_thread(struct proc_struct *proc, uintptr_t esp, trapframe_t *tf) {
242 proc->tf = (trapframe_t *)(proc->kstack + KSTACKSIZE - sizeof(trapframe_t));
243 *(proc->tf) = *tf;
244
245 proc->tf->gpr[10] = 0;
246 proc->tf->gpr[2] = (esp == 0) ? (uintptr_t)proc->tf -4 : esp;
247
248 proc->context.ra = (uintptr_t)forkret;
249 proc->context.sp = (uintptr_t)(proc->tf);
250 }
```
在函数中首先对传入的栈帧进行拷贝并且将上下文中的ra设置为地址forkret将sp设置为该栈帧。
完成以上几步后我们为子进程设置pid将其加入到进程链表当中并且设置其状态为就绪。
**6.2.3 上下文切换**
每个进程都有着自己的上下文,在进程间切换时,需要对上下文一并切换。
在pk/proc.c的cpu_idle中有以下代码
```
374 void
375 cpu_idle(void) {
376 while (1) {
377 if (currentproc->need_resched) {
378 schedule();
379 }
380 }
381 }
```
在当前进程处于need_resched状态时会执行调度算法schedule其代码如下
```
16 void
17 schedule(void) {
18 list_entry_t *le, *last;
19 struct proc_struct *next = NULL;
20 {
21 currentproc->need_resched = 0;
22 last = (currentproc == idleproc) ? &proc_list : &(currentproc->list_link);
23 le = last;
24 do {
25 if ((le = list_next(le)) != &proc_list) {
26 next = le2proc(le, list_link);
27 if (next->state == PROC_RUNNABLE) {
28 break;
29 }
30 }
31 } while (le != last);
32 if (next == NULL || next->state != PROC_RUNNABLE) {
33 next = idleproc;
34 }
35 next->runs ++;
36 if (next != currentproc) {
37 proc_run(next);
38 }
39 }
40 }
```
在schedule函数中找到下一个需要执行的进程并执行执行代码proc_run如下
```
145 void
146 proc_run(struct proc_struct *proc) {
147 if (proc != currentproc) {
148 bool intr_flag;
149 struct proc_struct *prev = currentproc, *next = proc;
150 currentproc = proc;
151 write_csr(sptbr, ((uintptr_t)next->cr3 >> RISCV_PGSHIFT) | SATP_MODE_CHOICE);
152 switch_to(&(prev->context), &(next->context));
153
154 }
155 }
```
当传入的proc不为当前进程时执行切换操作
```
7 switch_to:
8 # save from's registers
9 STORE ra, 0*REGBYTES(a0)
10 STORE sp, 1*REGBYTES(a0)
11 STORE s0, 2*REGBYTES(a0)
12 STORE s1, 3*REGBYTES(a0)
13 STORE s2, 4*REGBYTES(a0)
14 STORE s3, 5*REGBYTES(a0)
15 STORE s4, 6*REGBYTES(a0)
16 STORE s5, 7*REGBYTES(a0)
17 STORE s6, 8*REGBYTES(a0)
18 STORE s7, 9*REGBYTES(a0)
19 STORE s8, 10*REGBYTES(a0)
20 STORE s9, 11*REGBYTES(a0)
21 STORE s10, 12*REGBYTES(a0)
22 STORE s11, 13*REGBYTES(a0)
23
24 # restore to's registers
25 LOAD ra, 0*REGBYTES(a1)
26 LOAD sp, 1*REGBYTES(a1)
27 LOAD s0, 2*REGBYTES(a1)
28 LOAD s1, 3*REGBYTES(a1)
29 LOAD s2, 4*REGBYTES(a1)
30 LOAD s3, 5*REGBYTES(a1)
31 LOAD s4, 6*REGBYTES(a1)
32 LOAD s5, 7*REGBYTES(a1)
33 LOAD s6, 8*REGBYTES(a1)
34 LOAD s7, 9*REGBYTES(a1)
35 LOAD s8, 10*REGBYTES(a1)
36 LOAD s9, 11*REGBYTES(a1)
37 LOAD s10, 12*REGBYTES(a1)
38 LOAD s11, 13*REGBYTES(a1)
39
40 ret
```
可以看到在switch_to中我们正真执行了上一个进程的上下文保存以及下一个进程的上下文加载。在switch_to的最后一行我们执行ret指令该指令是一条从子过程返回的伪指令会将pc设置为x1ra寄存器的值还记得我们在copy_thread中层将ra设置为forkret嘛现在程序将从forkret继续执行
```
160 static void
161 forkret(void) {
162 extern elf_info current;
163 load_elf(current.file_name,&current);
164
165 int pid=currentproc->pid;
166 struct proc_struct * proc=find_proc(pid);
167 write_csr(sscratch, proc->tf);
168 set_csr(sstatus, SSTATUS_SUM | SSTATUS_FS);
169 currentproc->tf->status = (read_csr(sstatus) &~ SSTATUS_SPP &~ SSTATUS_SIE) | SSTATUS_SPIE;
170 forkrets(currentproc->tf);
171 }
```
我们进入forkrets
```
121 forkrets:
122 # set stack to this new process's trapframe
123 move sp, a0
124 addi sp,sp,320
125 csrw sscratch,sp
126 j start_user
```
```
76 .globl start_user
77 start_user:
78 LOAD t0, 32*REGBYTES(a0)
79 LOAD t1, 33*REGBYTES(a0)
80 csrw sstatus, t0
81 csrw sepc, t1
82
83 # restore x registers
84 LOAD x1,1*REGBYTES(a0)
85 LOAD x2,2*REGBYTES(a0)
86 LOAD x3,3*REGBYTES(a0)
87 LOAD x4,4*REGBYTES(a0)
88 LOAD x5,5*REGBYTES(a0)
89 LOAD x6,6*REGBYTES(a0)
90 LOAD x7,7*REGBYTES(a0)
91 LOAD x8,8*REGBYTES(a0)
92 LOAD x9,9*REGBYTES(a0)
93 LOAD x11,11*REGBYTES(a0)
94 LOAD x12,12*REGBYTES(a0)
95 LOAD x13,13*REGBYTES(a0)
96 LOAD x14,14*REGBYTES(a0)
97 LOAD x15,15*REGBYTES(a0)
98 LOAD x16,16*REGBYTES(a0)
99 LOAD x17,17*REGBYTES(a0)
100 LOAD x18,18*REGBYTES(a0)
101 LOAD x19,19*REGBYTES(a0)
102 LOAD x20,20*REGBYTES(a0)
103 LOAD x21,21*REGBYTES(a0)
104 LOAD x22,22*REGBYTES(a0)
105 LOAD x23,23*REGBYTES(a0)
106 LOAD x24,24*REGBYTES(a0)
107 LOAD x25,25*REGBYTES(a0)
108 LOAD x26,26*REGBYTES(a0)
109 LOAD x27,27*REGBYTES(a0)
110 LOAD x28,28*REGBYTES(a0)
111 LOAD x29,29*REGBYTES(a0)
112 LOAD x30,30*REGBYTES(a0)
113 LOAD x31,31*REGBYTES(a0)
114 # restore a0 last
115 LOAD x10,10*REGBYTES(a0)
116
117 # gtfo
118 sret
```
可以看到在forkrets最后执行了sret程序就此由内核切换至用户程序执行