练习1:分配并初始化一个进程控制块
任务:
alloc_proc函数(位于kern/process/proc.c中)负责分配并返回一个新的struct proc_struct结构,用于存储新建立的内核线程的管理信息。ucore需要对这个结构进行最基本的初始化,你需要完成这个初始化过程。
static struct proc_struct * alloc_proc(void) {
struct proc_struct *proc = kmalloc(sizeof(struct proc_struct));
if (proc != NULL) {
//LAB4:EXERCISE1 YOUR CODE
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);
}
return proc;
}
问题:
请说明proc_struct中struct context context和struct trapframe tf成员变量含义和在本实验中的作用是什么?
struct context context:储存进程当前状态,用于进程切换中上下文的保存与恢复。
struct trapframe tf:内核态中的线程返回用户态所加载的上下文,中断返回时,新进程会恢复保存的trapframe信息至各个寄存器中,然后开始执行用户代码。
练习2:为新创建的内核线程分配资源
任务:
你需要完成在kern/process/proc.c中的do_fork函数中的处理过程。
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;
//LAB4:EXERCISE2 YOUR CODE
// 首先分配一个PCB
if ((proc = alloc_proc()) == NULL)
goto fork_out;
// fork肯定存在父进程,所以设置子进程的父进程
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);
// 将子进程的PCB添加进hash list或者list
// 需要注意的是,不能让中断处理程序打断这一步操作
bool intr_flag;
local_intr_save(intr_flag);
{
proc->pid = get_pid();
hash_proc(proc);
list_add(&proc_list, &(proc->list_link));
nr_process ++;
}
local_intr_restore(intr_flag);
// 设置新的子进程可执行
wakeup_proc(proc);
// 返回子进程的pid
ret = proc->pid;
fork_out:
return ret;
bad_fork_cleanup_kstack:
put_kstack(proc);
bad_fork_cleanup_proc:
kfree(proc);
goto fork_out;
}
// copy_thread - setup the trapframe on the process’s kernel stack top and
// - setup the kernel entry point and stack of process
static void
copy_thread(struct proc_struct *proc, uintptr_t esp, struct trapframe *tf) {
proc->tf = (struct trapframe *)(proc->kstack + KSTACKSIZE) - 1;
*(proc->tf) = *tf;
proc->tf->tf_regs.reg_eax = 0;
proc->tf->tf_esp = esp;
proc->tf->tf_eflags |= FL_IF;
proc->context.eip = (uintptr_t)forkret;
proc->context.esp = (uintptr_t)(proc->tf);
}
// hash_proc - add proc into proc hash_list
static void
hash_proc(struct proc_struct *proc) {
list_add(hash_list + pid_hashfn(proc->pid), &(proc->hash_link));
}
/* *
问题:
请说明ucore是否做到给每个新fork的线程一个唯一的id?请说明你的分析和理由。
是。
(last_pid,nextsafe]这个区间是空闲的。
在函数get_pid中,如果静态成员last_pid小于next_safe,则当前分配的last_pid一定是安全的,即唯一的PID。
但如果last_pid大于等于next_safe,或者last_pid的值超过MAX_PID,则当前的last_pid就不一定是唯一的PID,此时就需要遍历proc_list,重新对last_pid和next_safe进行设置,为下一次的get_pid调用打下基础。
// get_pid - alloc a unique pid for process
static int
get_pid(void) {
static_assert(MAX_PID > MAX_PROCESS);
struct proc_struct *proc;
list_entry_t *list = &proc_list, *le;
static int next_safe = MAX_PID, last_pid = MAX_PID;
if (++ last_pid >= MAX_PID) {
last_pid = 1;
goto inside;
}
if (last_pid >= next_safe) {
inside:
next_safe = MAX_PID;
repeat:
le = list;
while ((le = list_next(le)) != list) {
proc = le2proc(le, list_link);
if (proc->pid == last_pid) {
if (++ last_pid >= next_safe) {
if (last_pid >= MAX_PID)
last_pid = 1;
next_safe = MAX_PID;
goto repeat;
}
}
else if (proc->pid > last_pid && next_safe > proc->pid)
next_safe = proc->pid;
}
}
return last_pid;
}
练习3:阅读代码,理解 proc_run 函数和它调用的函数如何完成进程切换的。
proc_run函数会设置内核栈地址,同时还会加载页目录表的地址。等到这些前置操作完成后,最后执行上下文切换。
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);//开锁
}
}
/* *
static inline void
lcr3(uintptr_t cr3) {
asm volatile (“mov %0, %%cr3” :: “r” (cr3) : “memory”);
}
.text
.globl switch_to
switch_to: # switch_to(from, to)
# save from’s registers
movl 4(%esp), %eax # 获取当前进程的context结构地址
popl 0(%eax) # 将eip保存至当前进程的context结构
movl %esp, 4(%eax) # 将esp保存至当前进程的context结构
movl %ebx, 8(%eax) # 将ebx保存至当前进程的context结构
movl %ecx, 12(%eax) # 将ecx保存至当前进程的context结构
movl %edx, 16(%eax) # 将edx保存至当前进程的context结构
movl %esi, 20(%eax) # 将esi保存至当前进程的context结构
movl %edi, 24(%eax) # 将edi保存至当前进程的context结构
movl %ebp, 28(%eax) # 将ebp保存至当前进程的context结构
# restore to's registers
movl 4(%esp), %eax # 获取下一个进程的context结构地址
# 需要注意的是,其地址不是8(%esp),因为之前已经pop过一次栈。
movl 28(%eax), %ebp # 恢复ebp至下一个进程的context结构
movl 24(%eax), %edi # 恢复edi至下一个进程的context结构
movl 20(%eax), %esi # 恢复esi至下一个进程的context结构
movl 16(%eax), %edx # 恢复edx至下一个进程的context结构
movl 12(%eax), %ecx # 恢复ecx至下一个进程的context结构
movl 8(%eax), %ebx # 恢复ebx至下一个进程的context结构
movl 4(%eax), %esp # 恢复esp至下一个进程的context结构
pushl 0(%eax) # 插入下一个进程的eip,以便于ret到下个进程的代码位置。
ret
问题:
1)在本实验的执行过程中,创建且运行了几个内核线程?
两个内核线程,分别是idleproc和initproc。
2)语句local_intr_save(intr_flag);…local_intr_restore(intr_flag);在这里有何作用?
这两句代码的作用分别是阻塞中断和解除中断的阻塞。
理由:
#define local_intr_save(x) do { x = __intr_save(); } while (0)
static inline bool
__intr_save(void) {
if (read_eflags() & FL_IF) {
intr_disable();
return 1;
}
return 0;
}
#define FL_IF 0x00000200 // Interrupt Flag
static inline uint32_t
read_eflags(void) {
uint32_t eflags;
asm volatile (“pushfl; popl %0” : “=r” (eflags));
return eflags;
}
/* intr_disable - disable irq interrupt /
void
intr_disable(void) {
cli();
}
static inline void
cli(void) {
asm volatile (“cli” ::: “memory”);
}
CLI禁止中断发生
#define local_intr_restore(x) __intr_restore(x);
static inline void
__intr_restore(bool flag) {
if (flag) {
intr_enable();
}
}
/ intr_enable - enable irq interrupt */
void
intr_enable(void) {
sti();
}
static inline void
sti(void) {
asm volatile (“sti”);
}
STI允许中断发生