课程主页:https://www.xuetangx.com/course/THU08091000267/5883104?channel=i.area.learn_title
实验指导书:https://objectkuan.gitbooks.io/ucore-docs/content/
github:https://github.com/chyyuu/ucore_os_lab (master 分支)
前置知识
线程、状态转换。
改动点
相比于 lab3 源代码,lab4 主要是添加代码 process/* 以实现进程/线程管理功能。
练习一
此练习用于认识进程/线程控制块 PCB。
在 ucore 中,struct proc_struct 用于描述进程/线程:
| 12
 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
 
 | struct proc_struct {enum proc_state state;
 int pid;
 int runs;
 uintptr_t kstack;
 volatile bool need_resched;
 struct proc_struct *parent;
 struct mm_struct *mm;
 struct context context;
 struct trapframe *tf;
 uintptr_t cr3;
 uint32_t flags;
 char name[PROC_NAME_LEN + 1];
 list_entry_t list_link;
 list_entry_t hash_link;
 };
 
 
 enum proc_state {
 PROC_UNINIT = 0,
 PROC_SLEEPING,
 PROC_RUNNABLE,
 PROC_ZOMBIE,
 };
 
 
 struct context {
 uint32_t eip;
 uint32_t esp;
 uint32_t ebx;
 uint32_t ecx;
 uint32_t edx;
 uint32_t esi;
 uint32_t edi;
 uint32_t ebp;
 };
 
 | 
鉴于对于 PCB 的上述理解,我们应当在 alloc_proc() 中,如此初始化新建的 PCB:
| 12
 3
 4
 5
 6
 7
 8
 9
 10
 11
 12
 13
 14
 15
 16
 17
 
 | if (proc != NULL) {
 proc->state = PROC_UNINIT;
 proc->pid = -1;
 proc->runs = 0;
 proc->kstack = 0;
 proc->need_resched = 0;
 proc->parent = NULL;
 proc->mm = NULL;
 memset(&proc->context, 0 , sizeof(struct context));
 proc->tf = NULL;
 
 proc->cr3 = boot_cr3;
 proc->flags = 0;
 memset(&(proc->name), 0, PROC_NAME_LEN);
 
 }
 
 | 
练习二
该练习用于实现 do_fork() 以为新建进程/线程分配资源 (其含义等价于实际系统中的 fork())。
对于进程/线程而言,其所需资源包括 (就目前 lab 而言):PCB、内核栈、内存资源。
在 do_fork() 函数内部,一一分配这些资源即可。具体源代码如下示:
| 12
 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
 
 | int do_fork(uint32_t clone_flags, uintptr_t stack, struct trapframe *tf) {int ret = -E_NO_FREE_PROC;
 struct proc_struct *proc;
 if (nr_process >= MAX_PROCESS) {
 goto fork_out;
 }
 ret = -E_NO_MEM;
 
 
 if ((proc = alloc_proc()) == NULL)
 {
 goto fork_out;
 }
 
 
 proc->parent = current;
 
 
 if (setup_kstack(proc) != 0)
 {
 goto bad_fork_cleanup_proc;
 }
 
 
 if (copy_mm(clone_flags, proc) != 0)
 {
 goto bad_fork_cleanup_kstack;
 }
 
 
 copy_thread(proc, stack, tf);
 
 
 bool intr_flag;
 local_intr_save(intr_flag);
 {
 proc->pid = get_pid();
 list_add(&proc_list, &(proc->list_link));
 hash_proc(proc);
 nr_process++;
 }
 
 
 wakeup_proc(proc);
 
 ret = proc->pid;
 
 fork_out:
 return ret;
 bad_fork_cleanup_kstack:
 put_kstack(proc);
 bad_fork_cleanup_proc:
 kfree(proc);
 goto fork_out;
 }
 
 | 
练习三
该练习用于详细了解 ucore 内部如何创建并执行内核线程(其中包括进程,在这里指等同于内核线程,切换的实现)。
do_fork() 函数完成后,新建进程的各种资源已经分配完成,此时我们简单看看 PCB 的部分内容:
| 12
 3
 4
 5
 6
 7
 8
 9
 10
 11
 12
 13
 
 | tf.tf_cs = KERNEL_CS;
 tf.tf_ds = tf.tf_es = tf.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;
 tf.tf_regs.reg_eax = 0;
 tf.tf_esp = esp;
 tf.tf_eflags |= FL_IF;
 
 
 context.eip = (uintptr_t)forkret;
 context.esp = (uintptr_t)(proc->tf);
 
 | 
在uCore执行完proc_init函数后,就创建好了两个内核线程:idleproc和initproc,这时uCore当前的执行现场就是idleproc,等到执行到init函数的最后一个函数cpu_idle之前,uCore的所有初始化工作就结束了,idleproc将通过执行cpu_idle函数让出CPU,给其它内核线程执行,具体过程如下:
| 12
 3
 4
 5
 6
 7
 
 | void
 cpu_idle(void) {
 while (1) {
 if (current->need_resched) {
 schedule();
 ……
 
 | 
uCore在实验四中只实现了一个最简单的FIFO调度器,其核心就是schedule函数。它的执行逻辑很简单:
1.设置当前内核线程current->need_resched为0; 
2.在proc_list队列中查找下一个处于“就绪”态的线程或进程next; 
3.找到这样的进程后,就调用proc_run函数,保存当前进程current的执行现场(进程上下文),恢复新进程的执行现场,完成进程切换。
接下来,我们看看,切换进程时具体执行的 proc_run() 函数:
| 12
 3
 4
 5
 6
 7
 8
 9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 
 | void proc_run(struct proc_struct *proc) {if (proc != current) {
 bool intr_flag;
 struct proc_struct *prev = current, *next = proc;
 local_intr_save(intr_flag);
 {
 
 current = proc;
 
 
 load_esp0(next->kstack + KSTACKSIZE);
 
 lcr3(next->cr3);
 
 switch_to(&(prev->context), &(next->context));
 }
 local_intr_restore(intr_flag);
 }
 }
 
 | 
| 12
 
 | 两个内核线程 一个为 idle_proc 为 第 0 个内核线程 完成内核中的初始化 然后调度执行其他进程或线程另一个为 init_proc 本次实验的内核线程 只用来打印字符串
 
 | 
- 语句local_intr_save(intr_flag);….local_intr_restore(intr_flag);在这里有何作用?请说明理由
| 1
 | 关闭中断 避免进程切换的中途 再被中断(其他进程再进行调度)
 | 
进一步,追看 switch_to() 的具体实现:
| 12
 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
 
 | switch_to:                      # 保存原有进程的 context 信息。
 # 调用 switch_to 后栈的情况  |     To    esp + 8 |
 #                          |   From   esp + 4  |
 #                          |  Ret Addr <- esp  |
 movl 4(%esp), %eax          # eax points to from
 # 指代将 switch_to 的返回地址 pop 作为原先进程的 context.eip
 popl 0(%eax)
 movl %esp, 4(%eax)
 movl %ebx, 8(%eax)
 movl %ecx, 12(%eax)
 movl %edx, 16(%eax)
 movl %esi, 20(%eax)
 movl %edi, 24(%eax)
 movl %ebp, 28(%eax)
 
 # 恢复新进程的 context 信息。
 movl 4(%esp), %eax          # eax points to to
 
 movl 28(%eax), %ebp
 movl 24(%eax), %edi
 movl 20(%eax), %esi
 movl 16(%eax), %edx
 movl 12(%eax), %ecx
 movl 8(%eax), %ebx
 movl 4(%eax), %esp
 
 # 设置函数的返回地址为 to 所指代的 eip (具体指代 forkret),那么 switch_to 返回后,便会执行 forkret 函数。
 pushl 0(%eax)
 
 ret
 
 | 
由上述汇编代码可知,switch_to 函数返回后会执行forkret函数,而forkret会调用位于kern/trap/trapentry.S中的forkrets函数执行
进一步,追看 forkret() -> forkrets() 的具体实现:
| 12
 3
 4
 5
 6
 7
 8
 9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 
 | __trapret: # restore registers from stack (从栈中弹出所有通用寄存器的值)
 popal
 
 # restore %gs, %fs, %ds and %es
 popl %gs
 popl %fs
 popl %es
 popl %ds
 # get rid of the trap number and error code
 addl $0x8, %esp
 # 跳转到kernel_thread_entry (此时esp指向了current->tf.tf_eip,而如果此时执行的是initproc,则current->
 # tf.tf_eip=kernel_thread_entry)
 iret
 
 .globl forkrets
 forkrets:
 # 把esp指向当前进程的中断帧
 movl 4(%esp), %esp
 # 此部分为中断处理的后半部分,弹出栈顶的一系列元素,返回执行中断前的指令,具体指代 tf.eip 所指。
 # 对于新进程而言,其指代 kernel_thread_entry (它会调用 fn 和 arg,开始真正执行新进程的指令)。
 jmp __trapret
 
 | 
至此,完成分析 “进程切换” 的完整步骤。
虽然本 lab 仅涉及内核线程的切换,但是其进程切换方式是比较特殊的:借助于中断实现。该种实现方式允许特权级切换,从而可以构建用户进程,从而为 lab5 打下基础。
kernel_thread_entry是entry.S中实现的汇编函数,它做的事情很简单:
| 12
 3
 4
 5
 
 | kernel_thread_entry: # void kernel_thread(void)pushl %edx # push arg
 call *%ebx # call fn (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函数退出线程执行。