课程主页: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
用于描述进程/线程:
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
| 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:
1 2 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()
函数内部,一一分配这些资源即可。具体源代码如下示:
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
| 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 的部分内容:
1 2 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,给其它内核线程执行,具体过程如下:
1 2 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()
函数:
1 2 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); } }
|
1 2
| 两个内核线程 一个为 idle_proc 为 第 0 个内核线程 完成内核中的初始化 然后调度执行其他进程或线程 另一个为 init_proc 本次实验的内核线程 只用来打印字符串
|
- 语句local_intr_save(intr_flag);….local_intr_restore(intr_flag);在这里有何作用?请说明理由
1
| 关闭中断 避免进程切换的中途 再被中断(其他进程再进行调度)
|
进一步,追看 switch_to()
的具体实现:
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
| 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()
的具体实现:
1 2 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中实现的汇编函数,它做的事情很简单:
1 2 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函数退出线程执行。