前置知识
线程、状态转换。
改动点
相比于 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);
|
接下来,我们看看,切换进程时具体执行的 proc_run()
函数:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24
| 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); } }
|
进一步,追看 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
| switch_to: # 保存原有进程的 context 信息。 movl 4(%esp), %eax # eax points to from # 指代将 switch_to 的返回地址 pop 作为原先进程的 context.eip # (这里似乎有些不对,如果下次 to 进程切换至 from 进程,切换结果便是 proc_run 中 switch_to 的下一条指令,明显不符合所学内容)。 # 目前可能只是权宜之计,后续会有所改动。 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
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
|
进一步,追看 forkret() -> forkrets()
的具体实现:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| __trapret: popal
popl %gs popl %fs popl %es popl %ds
addl $0x8, %esp iret
.globl forkrets forkrets: # 最初新进程的 esp 指向内核栈的栈顶,其中存放 tf。上下文切换完成后,凡是函数调用,消耗的都是新进程的 esp,因此其栈顶会发生变化。如此调整,以保证 esp 仍指向原先存放的 tf。 movl 4(%esp), %esp # 此部分为中断处理的后半部分,弹出栈顶的一系列元素,返回执行中断前的指令,具体指代 tf.eip 所指。 # 对于新进程而言,其指代 kernel_thread_entry (它会调用 fn 和 arg,开始真正执行新进程的指令)。 jmp __trapret
|
至此,完成分析 “进程切换” 的完整步骤。
虽然本 lab 仅涉及内核线程的切换,但是其进程切换方式是比较特殊的:借助于中断实现。该种实现方式允许特权级切换,从而可以构建用户进程,从而为 lab5 打下基础。