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.

13 KiB

第六章实验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程序就此由内核切换至用户程序执行