姓名:宋亦婷
学号:22920172204201
- 了解内核线程创建/执行的管理过程
- 了解内核线程的切换和基本调度过程
在实验2/3上完成了物理和虚拟内存管理,这给创造内核线程打下了提供内存管理的基础。当一个程序加载到内存中运行时,首先通过 ucore OS 的内存管理子系统分配合适的空间,然后就需要考虑如何分时使用CPU来并发执行多个程序,让每个运行的程序感到各自拥有自己的CPU。
本实验依赖实验1/2/3,请把你做的实验1/2/3中的代码填入本实验代码中有“LAB1”、"LAB2"、"LAB3"的注释相应部分。其中需要填入代码的部分有:
~/lab3/kern/kdebug.c 部分;
~/lab3/kern/trap.c 部分;
~/lab3/kern/mm/default_pmm.c部分;
~/lab3/kern/mm/pcc.m部分;
~/lab3/kern/mm/swap_fifo.c部分;
~/lab3/kern/mm/vmm.c部分。
可以使用一些工具直接进行文件对比,例如可以用Eclipse,直接使用Compare With将lab3中的代码搬入lab4。
实验要求:
alloc_proc函数(位于kern/process/proc.c中)负责分配并返回一个新的struct proc_struct结构,用于存储新建立的内核线程的管理信息。ucore需要对这个结构进行最基本的初始化,你需要完成这个初始化过程。
【提示】在alloc_proc函数的实现中,需要初始化的proc_struct结构中的成员变量至少包括: state/pid/runs/kstack/need_resched/parent/mm/context/tf/cr3/flags/name。
实验如下:
#Step1:查看所需要填写部分的内容:
#Step2:查看proc.h中定义了进程控制块的结构体 proc_struct:
#Step3:初始化进程控制块如下:
// alloc_proc - alloc a proc_struct and init all fields of proc_struct
static struct proc_struct *
alloc_proc(void) {
struct proc_struct *proc = kmalloc(sizeof(struct proc_struct));
if (proc != NULL) {
//LAB4:EXERCISE1 2015011345
proc -> state = PROC_UNINIT; //设置进程为未初始化状态
proc -> pid = -1; //未初始化进程id=-1
proc -> cr3 = boot_cr3; //内核线程,页表使用boot_cr3
proc -> mm = NULL; //置空虚拟内存
proc -> runs = 0; //初始化时间片
proc -> kstack = 0; //初始化内存栈地址
proc -> need_resched = 0; //调度设为不需要调整
proc -> parent = NULL; //置空父节点
memset(&(proc -> context), 0, sizeof(struct context)); //初始化上下文
proc -> tf = NULL; //置空中断帧指针
proc -> flags = 0; //初始化标志位
memset(proc -> name, 0, PROC_NAME_LEN); //置空进程名
}
return proc;
}
copy
回答以下问题:
请说明proc_struct中struct context context
和struct trapframe *tf
成员变量含义和在本实验中的作用是啥?(提示通过看代码和编程调试可以判断出来)
struct context context
:进程的上下文,用于进行的切换。又因为在系统中,所有的进程在内核中也是相对独立的,即使用context保存寄存器可以使得在内核态中进行上下文间的切换。
struct trapframe *tf
:中断帧的指针,用于保存当进程用用户空间跳到内核空间时,前一个被中断或异常打断的进程的状态信息;当内核空间需要跳回用户空间时,需调整中断帧以恢复让进程继续执行的各寄存器值。在实验系统中,内核允许嵌套中断,即为了保证嵌套中断发生时 *tf 能指向当前的trapftame,系统在内核栈上维护了 *tf的链。
实验要求:
创建一个内核线程需要分配和设置好很多资源。kernel_thread函数通过调用do_fork函数完成具体内核线程的创建工作。do_kernel函数会调用alloc_proc函数来分配并初始化一个进程控制块,但alloc_proc只是找到了一小块内存用以记录进程的必要信息,并没有实际分配这些资源。ucore一般通过do_fork实际创建新的内核线程。do_fork的作用是,创建当前内核线程的一个副本,它们的执行上下文、代码、数据都一样,但是存储位置不同。在这个过程中,需要给新内核线程分配资源,并且复制原进程的状态。你需要完成在kern/process/proc.c中的do_fork函数中的处理过程。它的大致执行步骤包括:
实验如下:
#Step1:查看kernel_thread()部分:
#Step2:查看该部分注释有:
对注释进行分析:
1.分配并初始化进程控制块。
2.分配并初始化内核栈。
3.根据* clone_flag
标志复制或共享进程内存管理结构。
4.设置进程在内核正常运行和调度所需的中断帧和执行上下文。
5.将设置好的进程控制块放入hash_list
和proc_list
两个全局进程链表中。
6.将进程状态设置为就绪。
7.设置返回码为子进程的id号。
#Step3:根据注释和分析补全代码有:
/* 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;
// 1. call alloc_proc to allocate a proc_struct
proc = alloc_proc();
if (proc == NULL) {
goto fork_out;
}
proc -> parent = current;
// 2. call setup_kstack to allocate a kernel stack for child process
int kstack_success = setup_kstack(proc);
if (kstack_success != 0) {
goto bad_fork_cleanup_proc;
}
// 3. call copy_mm to dup OR share mm according clone_flag
int copy_success = copy_mm(clone_flags, proc);
if (copy_success != 0) {
goto bad_fork_cleanup_kstack;
}
// 4. call copy_thread to setup tf & context in proc_struct
copy_thread(proc, stack, tf);
bool intr_flag;
local_intr_save(intr_flag);
proc -> pid = get_pid();
// 5. insert proc_struct into hash_list && proc_list
hash_proc(proc);
list_add(&proc_list, &(proc -> list_link));
nr_process++;
local_intr_restore(intr_flag);
// 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;
}
copy
回答以下问题:
查看get_pid部分函数:
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;
}
copy
分析有:
第一句assert可以保证进程数一定不会多于可以分配的进程标识号的数目。
函数将扫描所有的进程,找到一个当前没被使用的进程号,存储在last_pid
中,作为新进程的进程号。即循环扫描每一个当前进程,当一个现有的进程号和last_pid
相等时,则将last_pid+1
;当现有的进程号大于last_pid
时,这意味着在已经扫描的进程中[last_pid, min(next_safe, proc->pid)]
这段进程号尚未被占用,继续扫描。这样可以保证返回的新进程号一定没有被占用,即具有唯一的id。
实验要求:
请简要说明你对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);
}
}
copy
保存寄存器和恢复待调度进程的寄存器部分代码在switch_to中,如下:
switch_to: # switch_to(from, to)
\# save from's registers
movl 4(%esp), %eax # eax points to from
popl 0(%eax) # save eip !popl
movl %esp, 4(%eax) # save esp::context of from
movl %ebx, 8(%eax) # save ebx::context of from
movl %ecx, 12(%eax) # save ecx::context of from
movl %edx, 16(%eax) # save edx::context of from
movl %esi, 20(%eax) # save esi::context of from
movl %edi, 24(%eax) # save edi::context of from
movl %ebp, 28(%eax) # save ebp::context of from
\# restore to's registers
movl 4(%esp), %eax # not 8(%esp): popped return address already
\# eax now points to to
movl 28(%eax), %ebp # restore ebp::context of to
movl 24(%eax), %edi # restore edi::context of to
movl 20(%eax), %esi # restore esi::context of to
movl 16(%eax), %edx # restore edx::context of to
movl 12(%eax), %ecx # restore ecx::context of to
movl 8(%eax), %ebx # restore ebx::context of to
movl 4(%eax), %esp # restore esp::context of to
pushl 0(%eax) # push eip
ret
copy
回答以下问题:
在本实验的执行过程中,创建且运行了几个内核线程?
创建了两个线程,分别为:
(1)idleproc:ucore的第一个内核线程,完成内核中各个子系统的初始化,之后立即调度,执行其他进程。
(2)initproc:“hello world”线程。
语句local_intr_save(intr_flag);....local_intr_restore(intr_flag);
在这里有何作用?请说明理由。
该语句为屏蔽中断操作语句,在此作用为:在进程调度开始前关中断,在结束进程调度后开中断,即为了防止在进程调度过程中产生中断导致进程调度的嵌套。
本次实验主要了解了内核线程的创建和切换,知道了内核线程直接使用共同的ucore内核内存空间,且内核线程是一种特殊的线程,其与用户线程存在一定的区别。
学习时间 60分钟
操作时间 9分钟
按键次数 6次
实验次数 3次
报告字数 9213字
是否完成 完成