## 第六章.(实验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设置为x1(ra)寄存器的值,还记得我们在copy_thread中层将ra设置为forkret嘛?现在程序将从forkret继续执行: ``` 160 static void 161 forkret(void) { 162 extern elf_info current; 163 load_elf(current.file_name,¤t); 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,程序就此由内核切换至用户程序执行!! ​