EXER2: 为新创建的内核线程分配资源*
创建一个内核线程需要分配和设置好很多资源。kernel_thread 函数通过调用 do_fork 函数完成具体内核线程的创建工作。do_fork 的作用是,创建当前内核线程的一个副本,它们的执行上下文、代码、数据都一样,但是存储位置不同。在这个过程中,需要给新内核线程分配资源,并且复制原进程的状态。在 preview 中已经了解了 do_fork 流程如下:
- alloc_proc 函数,分配并初始化进程控制块;
- setup_stack 函数,分配并初始化内核栈;
- copy_mm 函数,根据 clone_flag 标志复制或共享进程内存管理结果;
- copy_thread 函数,设置进程在内核(将来也包括用户态)正常运行和调度所需的中断帧和执行上下文;
- 把设置好的进程控制块放入到 hash_list 和 proc_list 两个全局变量中;
- 进程已准备好,设置为就绪态;
- 返回子进程的 pid。
参考注释实现的代码如下:
/* do_fork - parent process for a new child process
* @clone_flags: used to guide how to clone the child process
* @stack: the parent's user stack pointer. if stack==0, It means to fork a kernel thread.
* @tf: the trapframe info, which will be copied to child process's proc->tf
*/
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
/*
* Some Useful MACROs, Functions and DEFINEs, you can use them in below implementation.
* MACROs or Functions:
* alloc_proc: 练习 1 实现的创建并初始化一个进程控制块
* setup_kstack: 分配并初始化内核栈
* copy_mm: 根据 clone_flag 标志复制或共享进程内存管理结果
* if clone_flags & CLONE_VM, then "share" ; else "duplicate"
* copy_thread: 设置进程在内核(将来也包括用户态)正常运行和调度所需的中断帧和执行上下文
* hash_proc: 基于 pid 将进程插入哈希表
* get_pid: 分配一个唯一的 pid (命名为 generate_pid 不是更好吗。。。
* wakeup_proc: set proc->state = PROC_RUNNABLE
* VARIABLES:
* proc_list: the process set's list
* nr_process: the number of process set
*/
// 1. call alloc_proc to allocate a proc_struct
if((proc = alloc_proc()) == NULL) {
goto fork_out;
}
proc->parent = current; // 设置父进程
// 2. call setup_kstack to allocate a kernel stack for child process
if(setup_kstack(proc) != 0) {
goto bad_fork_cleanup_proc;
}
// 3. call copy_mm to dup OR share mm according clone_flag
if(copy_mm(clone_flags, proc) != 0) {
goto bad_fork_cleanup_kstack;
}
// 4. call copy_thread to setup tf & context in proc_struct
copy_thread(proc, stack, tf);
// 5. insert proc_struct into hash_list && proc_list
proc->pid = get_pid();
hash_proc(proc);
list_add(&proc_list, &(proc->list_link));
nr_process ++;
// 6. call wakeup_proc to make the new child process RUNNABLE
wakeup_proc(proc);
// 7. set ret vaule using child proc's pid
ret = proc->pid;
fork_out:
return ret;
bad_fork_cleanup_kstack:
put_kstack(proc);
bad_fork_cleanup_proc:
kfree(proc);
goto fork_out;
}
需要注意的点就是在 preview 里也提到过了,如果前三步没有执行成功则需要做错误处理,把相关已经占有的内存释放。还有就是进程分配之后设置父进程为当前进程。
比较有趣的点就是在给出的参考代码中用到了如下的结构:
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);
这里似乎是关闭了中断确保这一段操作的原子性,不会发生竞争。
Q: 请说明 ucore 是否做到给每个新 fork 的线程一个唯一的 id?请说明你的分析和理由。*
可以做到,首先分析以下 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) { // 循环分配,超出了上限就到 1,0 应该是 idleproc
last_pid = 1;
goto inside; // 归 1 后就无法保证 last_pid 到 next_safe 区间内的 pid 未被占用,直接跳过 if 进入内部
}
if (last_pid >= next_safe) { // 为了保证 pid 的唯一性,下面要逐个检查
inside:
next_safe = MAX_PID;
repeat:
le = list;
while ((le = list_next(le)) != list) {
proc = le2proc(le, list_link);
if (proc->pid == last_pid) { // 如果 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;
}
通过 next_safe 划定一个不会发生冲突的区间,可以提高一点效率。反正就是用一段很长的代码保证了分配一个唯一的 pid。同时结合前面提到的,调用 get_pid 时关闭了中断保证原子性,不会发生竞争,进一步保证了 pid 的唯一性。
最后更新: November 26, 2020