操作系统实验报告
实验4完成了内核线程, 但到目前为止, 所有的运行都在内核态执行。 实验5将创建用户进程, 让用户进程在用户态执行, 且在需要ucore支持时, 可通过系统调用来让ucore提供服务。 为此需要构造出第一个用户进程, 并通过系统调sys_fork / sys_exec / sys_exit / sys_wait 来支持运行不同的应用程序, 完成对用户进程的执行过程的基本管理。
本实验依赖实验1/2/3/4。请把你做的实验1/2/3/4的代码填入本实验中代码中
有 “LAB1”
, “LAB2”
,“LAB3”
,“LAB4”
的注释相应部分。
我使用Meld Diff Viewer
工具,通过对lab4
和lab5
的文件进行对比,将实验4中缺少的代码填写进去。操作与lab4
相似。由于lab5
是基于 lab1
、lab2
、lab3
和 lab4
完成的,所以虽然lab4
需要 lab1
、lab2
、lab3
的代码,但是由于lab4
中已经复制了 lab1
、lab2
、lab3
的代码,因此lab5
中不需要再将 lab1
、lab2
、lab3
进行拷贝。
对于软件的使用,我们这里只需要将文件的导入进行文件夹对比,然后找到两份代码的不同,进行一个个的比较,然后就可以完成练习0
。
本次实验需要进行复制的文件有:
kdebug.c
trap.c
default_pmm.c
pmm.c
swap_fifo.c
vmm.c
proc.c
copy
但lab5
还需要对四个函数进行修改
在原来的实验基础上,新增了 2 行代码:
proc->wait_state = 0;//PCB 进程控制块中新增的条目,初始化进程等待状态
proc->cptr = proc->optr = proc->yptr = NULL;//进程相关指针初始化
copy
改进后的 alloc_proc
函数:
static struct proc_struct *alloc_proc(void) {
struct proc_struct *proc = kmalloc(sizeof(struct proc_struct));
if (proc != NULL) {
proc->state = PROC_UNINIT; //设置进程为未初始化状态
proc->pid = -1; //未初始化的的进程id为-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);//进程名
proc->wait_state = 0;//PCB 进程控制块中新增的条目,初始化进程等待状态
proc->cptr = proc->optr = proc->yptr = NULL;//进程相关指针初始化
}
return proc;
}
copy
在原来的实验基础上,新增了 2 行代码:
assert(current->wait_state == 0); //确保当前进程正在等待
set_links(proc); //将原来简单的计数改成来执行 set_links 函数,从而实现设置进程的相关链接
copy
set_links
函数:
static void set_links(struct proc_struct *proc) {
list_add(&proc_list,&(proc->list_link));//进程加入进程链表
proc->yptr = NULL; //当前进程的 younger sibling 为空
if ((proc->optr = proc->parent->cptr) != NULL) {
proc->optr->yptr = proc; //当前进程的 older sibling 为当前进程
}
proc->parent->cptr = proc; //父进程的子进程为当前进程
nr_process ++; //进程数加一
}
copy
改进后的 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) { //分配进程数大于 4096,返回
goto fork_out;
}
ret = -E_NO_MEM; //因内存不足而分配失败
if ((proc = alloc_proc()) == NULL) { //调用 alloc_proc() 函数申请内存块,如果失败,直接返回处理
goto fork_out;
}
proc->parent = current; //将子进程的父节点设置为当前进程
assert(current->wait_state == 0); //确保当前进程正在等待
if (setup_kstack(proc) != 0) { //调用 setup_stack() 函数为进程分配一个内核栈
goto bad_fork_cleanup_proc;
}
if (copy_mm(clone_flags, proc) != 0) { //调用 copy_mm() 函数复制父进程的内存信息到子进程
goto bad_fork_cleanup_kstack;
}
copy_thread(proc, stack, tf); //调用 copy_thread() 函数复制父进程的中断帧和上下文信息
//将新进程添加到进程的 hash 列表中
bool intr_flag;
local_intr_save(intr_flag); //屏蔽中断,intr_flag 置为 1
{
proc->pid = get_pid(); //获取当前进程 PID
hash_proc(proc); //建立 hash 映射
set_links(proc);//将原来简单的计数改成来执行set_links函数,从而实现设置进程的相关链接
}
local_intr_restore(intr_flag); //恢复中断
wakeup_proc(proc); //一切就绪,唤醒子进程
ret = proc->pid; //返回子进程的 pid
fork_out: //已分配进程数大于 4096
return ret;
bad_fork_cleanup_kstack: //分配内核栈失败
put_kstack(proc);
bad_fork_cleanup_proc:
kfree(proc);
goto fork_out;
}
copy
在原来的实验基础上,新增了 1 行代码:
SETGATE(idt[T_SYSCALL], 1, GD_KTEXT, __vectors[T_SYSCALL], DPL_USER);
//设置相应的中断门
copy
改进后的 idt_init
函数:
void idt_init(void) {
extern uintptr_t __vectors[];
int i;
for (i = 0; i < sizeof(idt) / sizeof(struct gatedesc); i ++) {
SETGATE(idt[i], 0, GD_KTEXT, __vectors[i], DPL_KERNEL);
}
SETGATE(idt[T_SYSCALL], 1, GD_KTEXT, __vectors[T_SYSCALL], DPL_USER);
//设置相应的中断门
lidt(&idt_pd);
}
copy
在原来的实验基础上,新增了 1 行代码:
current->need_resched = 1;//时间片用完设置为需要调度
copy
改进后的 trap_dispatch
函数如下:
ticks ++;
if (ticks % TICK_NUM == 0) {
assert(current != NULL);
current->need_resched = 1;//时间片用完设置为需要调度
}
copy
do_execv
函数调用load_icode
( 位于kern/process/proc.c
中)来加载并解析一个处于内存中的ELF执行文件格式的应用程序,建立相应的用户内存空间来放置应用程序的代码段、 数据段等,且要设置好proc_struct
结构中的成员变量trapframe
中的内容,确保在执行此进程后,能够从应用程序设定的起始执行地址开始执行。需设置正确的trapframe
内容。
请在实验报告中简要说明你的设计实现过程。
请在实验报告中描述当创建一个用户态进程并加载了应用程序后,CPU是如何让这个应用程序最终在用户态执行起来的。即这个用户态进程被ucore选择占用CPU执行( RUNNING
态)到具体执行应用程序第一条指令的整个经过。
load_icode函数的主要工作就是给用户进程建立一个能够让用户进程正常运行的用户环境。 此函数有一百多行, 完成了如下重要工作:
do_execve
函数:
int
do_execve(const char *name, size_t len, unsigned char *binary, size_t size) {
struct mm_struct *mm = current->mm;
if (!user_mem_check(mm, (uintptr_t)name, len, 0)) {
return -E_INVAL;//获取当前进程的内存地址
}
if (len > PROC_NAME_LEN) {
len = PROC_NAME_LEN;
}
char local_name[PROC_NAME_LEN + 1];
memset(local_name, 0, sizeof(local_name));
memcpy(local_name, name, len);
//为加载新的执行码做好用户态内存空间清空准备
if (mm != NULL) {
lcr3(boot_cr3);//设置页表为内核空间页表
if (mm_count_dec(mm) == 0) {//如果没有进程再需要此进程所占用的内存空间
exit_mmap(mm);//释放进程所占用户空间内存和进程页表本身所占空间
put_pgdir(mm);
mm_destroy(mm);
}
current->mm = NULL;//把当前进程的 mm 内存管理指针为空
}
int ret;
// 加载应用程序执行码到当前进程的新创建的用户态虚拟空间中。这里涉及到读 ELF 格式的文件,申请内存空间,建立用户态虚存空间,加载应用程序执行码等。load_icode 函数完成了整个复杂的工作。
if ((ret = load_icode(binary, size)) != 0) {
goto execve_exit;
}
set_proc_name(current, local_name);
return 0;
execve_exit:
do_exit(ret);
panic("already exit: %e.\n", ret);
}
copy
load_icode
函数:
static int
load_icode(unsigned char *binary, size_t size) {
if (current->mm != NULL) {//当前进程的内存为空
panic("load_icode: current->mm must be empty.\n");
}
int ret = -E_NO_MEM;//记录错误信息:未分配内存
struct mm_struct *mm;
//(1) create a new mm for current process
if ((mm = mm_create()) == NULL) {//分配内存
goto bad_mm;
}
//(2) create a new PDT, and mm->pgdir= kernel virtual addr of PDT
if (setup_pgdir(mm) != 0) {//申请一个页目录表所需的空间
goto bad_pgdir_cleanup_mm;
}
//(3) copy TEXT/DATA section, build BSS parts in binary to memory space of process
struct Page *page;
//(3.1) get the file header of the bianry program (ELF format)
struct elfhdr *elf = (struct elfhdr *)binary;
//(3.2) get the entry of the program section headers of the bianry program (ELF format)
struct proghdr *ph = (struct proghdr *)(binary + elf->e_phoff);//获取段头部表的地址
//(3.3) This program is valid?
if (elf->e_magic != ELF_MAGIC) {//读取的 ELF 文件不合法
ret = -E_INVAL_ELF;//ELF 文件不合法错误
goto bad_elf_cleanup_pgdir;
}
uint32_t vm_flags, perm;
struct proghdr *ph_end = ph + elf->e_phnum;//段入口数目
for (; ph < ph_end; ph ++) {//遍历每一个程序段
//(3.4) find every program section headers
if (ph->p_type != ELF_PT_LOAD) {//当前段不能被加载
continue ;
}
if (ph->p_filesz > ph->p_memsz) {//虚拟地址空间大小大于分配的物理地址空间
ret = -E_INVAL_ELF;
goto bad_cleanup_mmap;
}
if (ph->p_filesz == 0) {//当前段大小为 0
continue ;
}
//(3.5) call mm_map fun to setup the new vma ( ph->p_va, ph->p_memsz)
vm_flags = 0, perm = PTE_U;
if (ph->p_flags & ELF_PF_X) vm_flags |= VM_EXEC;
if (ph->p_flags & ELF_PF_W) vm_flags |= VM_WRITE;
if (ph->p_flags & ELF_PF_R) vm_flags |= VM_READ;
if (vm_flags & VM_WRITE) perm |= PTE_W;
if ((ret = mm_map(mm, ph->p_va, ph->p_memsz, vm_flags, NULL)) != 0) {
goto bad_cleanup_mmap;
}
unsigned char *from = binary + ph->p_offset;
size_t off, size;
uintptr_t start = ph->p_va, end, la = ROUNDDOWN(start, PGSIZE);
ret = -E_NO_MEM;
//(3.6) alloc memory, and copy the contents of every program section (from, from+end) to process's memory (la, la+end)
end = ph->p_va + ph->p_filesz;
//(3.6.1) copy TEXT/DATA section of bianry program
while (start < end) {
if ((page = pgdir_alloc_page(mm->pgdir, la, perm)) == NULL) {
goto bad_cleanup_mmap;
}
off = start - la, size = PGSIZE - off, la += PGSIZE;
if (end < la) {
size -= la - end;
}
memcpy(page2kva(page) + off, from, size);
start += size, from += size;
}
//(3.6.2) build BSS section of binary program
end = ph->p_va + ph->p_memsz;
if (start < la) {
/* ph->p_memsz == ph->p_filesz */
if (start == end) {
continue ;
}
off = start + PGSIZE - la, size = PGSIZE - off;
if (end < la) {
size -= la - end;
}
memset(page2kva(page) + off, 0, size);
start += size;
assert((end < la && start == end) || (end >= la && start == la));
}
while (start < end) {
if ((page = pgdir_alloc_page(mm->pgdir, la, perm)) == NULL) {
goto bad_cleanup_mmap;
}
off = start - la, size = PGSIZE - off, la += PGSIZE;
if (end < la) {
size -= la - end;
}
memset(page2kva(page) + off, 0, size);
start += size;
}
}
//(4) build user stack memory
vm_flags = VM_READ | VM_WRITE | VM_STACK;
if ((ret = mm_map(mm, USTACKTOP - USTACKSIZE, USTACKSIZE, vm_flags, NULL)) != 0) {
goto bad_cleanup_mmap;
}
assert(pgdir_alloc_page(mm->pgdir, USTACKTOP-PGSIZE , PTE_USER) != NULL);
assert(pgdir_alloc_page(mm->pgdir, USTACKTOP-2*PGSIZE , PTE_USER) != NULL);
assert(pgdir_alloc_page(mm->pgdir, USTACKTOP-3*PGSIZE , PTE_USER) != NULL);
assert(pgdir_alloc_page(mm->pgdir, USTACKTOP-4*PGSIZE , PTE_USER) != NULL);
//(5) set current process's mm, sr3, and set CR3 reg = physical addr of Page Directory
mm_count_inc(mm);
current->mm = mm;
current->cr3 = PADDR(mm->pgdir);
lcr3(PADDR(mm->pgdir));
//(6) setup trapframe for user environment
struct trapframe *tf = current->tf;
memset(tf, 0, sizeof(struct trapframe));
/* LAB5:EXERCISE1 YOUR CODE
* should set tf_cs,tf_ds,tf_es,tf_ss,tf_esp,tf_eip,tf_eflags
* NOTICE: If we set trapframe correctly, then the user level process can return to USER MODE from kernel. So
* tf_cs should be USER_CS segment (see memlayout.h)
* tf_ds=tf_es=tf_ss should be USER_DS segment
* tf_esp should be the top addr of user stack (USTACKTOP)
* tf_eip should be the entry point of this binary program (elf->e_entry)
* tf_eflags should be set to enable computer to produce Interrupt
*/
tf->tf_cs = USER_CS;//将 trapframe 的代码段设为 USER_CS。
tf->tf_ds = tf->tf_es = tf->tf_ss = USER_DS;//将 trapframe 的数据段、附加段、堆栈段设为 USER_DS。
tf->tf_esp = USTACKTOP;//将 trapframe 的栈顶指针设为 USTACKTOP。
tf->tf_eip = elf->e_entry;//将 trapframe 的代码段指针设为 ELF 的入口地址 elf->e_entry。
tf->tf_eflags = FL_IF;//将 trapframe 中 EFLAGS 的 IF 置为 1
ret = 0;
out:
return ret;
bad_cleanup_mmap:
exit_mmap(mm);
bad_elf_cleanup_pgdir:
put_pgdir(mm);
bad_pgdir_cleanup_mm:
mm_destroy(mm);
bad_mm:
goto out;
}
copy
请在实验报告中描述当创建一个用户态进程并加载了应用程序后,CPU是如何让这个应用程序最终在用户态执行起来的。即这个用户态进程被ucore选择占用CPU执行(
RUNNING
态)到具体执行应用程序第一条指令的整个经过。
答:按照load_icode
函数的工作流程:
创建子进程的函数do_fork
在执行中将拷贝当前进程( 即父进程) 的用户内存地址空间中的合法内容到新进程中( 子进程) , 完成内存资源的复制。 具体是通过copy_range
函数( 位于kern/mm/pmm.c
中) 实现的, 请补充copy_range
的实现, 确保能够正确执行。
请在实验报告中简要说明如何设计实现”Copy on Write 机制“, 给出概要设计, 鼓励给出详细设计。
do_fork
函数,它完成的工作主要如下:
1、分配并初始化进程控制块( alloc_proc 函数);
2、分配并初始化内核栈,为内核进程(线程)建立栈空间( setup_stack 函数);
3、根据 clone_flag 标志复制或共享进程内存管理结构( copy_mm 函数);
4、设置进程在内核(将来也包括用户态)正常运行和调度所需的中断帧和执行上下文 ( copy_thread 函数);
5、为进程分配一个 PID( get_pid() 函数);
6、把设置好的进程控制块放入 hash_list 和 proc_list 两个全局进程链表中;
7、自此,进程已经准备好执行了,把进程状态设置为“就绪”态;
8、设置返回码为子进程的 PID 号。
do_fork
函数中调用了copy_mm
函数,copy_mm
函数调用dup_mmap
函数,dup_mmap
函数调用copy_range
函数。
int
copy_range(pde_t *to, pde_t *from, uintptr_t start, uintptr_t end, bool share) {
assert(start % PGSIZE == 0 && end % PGSIZE == 0);
assert(USER_ACCESS(start, end));
// copy content by page unit.
do {
//call get_pte to find process A's pte according to the addr start
pte_t *ptep = get_pte(from, start, 0), *nptep;
if (ptep == NULL) {
start = ROUNDDOWN(start + PTSIZE, PTSIZE);
continue ;
}
//call get_pte to find process B's pte according to the addr start. If pte is NULL, just alloc a PT
if (*ptep & PTE_P) {
if ((nptep = get_pte(to, start, 1)) == NULL) {
return -E_NO_MEM;
}
uint32_t perm = (*ptep & PTE_USER);
//get page from ptep
struct Page *page = pte2page(*ptep);
// alloc a page for process B
struct Page *npage=alloc_page();
assert(page!=NULL);
assert(npage!=NULL);
int ret=0;
/* LAB5:EXERCISE2 YOUR CODE
* replicate content of page to npage, build the map of phy addr of nage with the linear addr start
*
* Some Useful MACROs and DEFINEs, you can use them in below implementation.
* MACROs or Functions:
* page2kva(struct Page *page): return the kernel vritual addr of memory which page managed (SEE pmm.h)
* page_insert: build the map of phy addr of an Page with the linear addr la
* memcpy: typical memory copy function
*
* (1) find src_kvaddr: the kernel virtual address of page
* (2) find dst_kvaddr: the kernel virtual address of npage
* (3) memory copy from src_kvaddr to dst_kvaddr, size is PGSIZE
* (4) build the map of phy addr of nage with the linear addr start
*/
void * kva_src = page2kva(page);
//找到父进程需要复制的物理页在内核地址空间中的虚拟地址,这是由于这个函数执行的时候使用的时内核的地址空间
void * kva_dst = page2kva(npage);
//找到子进程需要被填充的物理页的内核虚拟地址
memcpy(kva_dst, kva_src, PGSIZE);
//将父进程的物理页的内容复制到子进程中去
ret = page_insert(to, npage, start, perm);
//建立子进程的物理页与虚拟页的映射关系
assert(ret == 0);
}
start += PGSIZE;
} while (start != 0 && start < end);
return 0;
}
copy
请在实验报告中简要说明如何设计实现”Copy on Write 机制“, 给出概要设计, 鼓励给出详细设计。
答:在创建子进程时,将父进程的PDE直接赋值给子进程的PDE,但是需要将允许写入的标志位置0;当子进程需要进行写操作时,再次出发中断调用do_pgfault()
,此时应给子进程新建PTE,并取代原先PDE中的项,然后才能写入。
请在实验报告中简要说明你对 fork/exec/wait/exit函数的分析。 并回答如下问题:
执行: make grade。 如果所显示的应用程序检测都输出ok, 则基本正确。
在本实验中, 与进程相关的各个系统调用属性如下所示:
系统调用名 | 含义 | 具体完成服务的函数 |
---|---|---|
SYS_exit | process exit | do_exit |
SYS_fork | create child process, dup mm | do_fork->wakeup_proc |
SYS_wait | wait process | do_wait |
SYS_exec | after fork, process execute a program | load a program and refresh the mm |
SYS_clone | create child thread | do_fork->wakeup_proc |
SYS_yield | process flag itself need resecheduling | proc->need_sched=1, then scheduler will rescheule this process |
SYS_sleep | process sleep | do_sleep |
SYS_kill | kill process | do_kill->proc->flags |= PF_EXITING->wakeup_proc->do_wait->do_exit |
SYS_getpid | get the process's pid |
通过这些系统调用, 可方便地完成从进程/线程创建到退出的整个运行过程。
fork->SYS_fork->do_fork+wakeup_proc
copy
```c 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: create a proc struct and init fields (lab4:exercise1) * setup_kstack: alloc pages with size KSTACKPAGE as process kernel stack * copy_mm: process "proc" duplicate OR share process "current"'s mm according clone_flags * if clone_flags & CLONE_VM, then "share" ; else "duplicate" * copy_thread: setup the trapframe on the process's kernel stack top and * setup the kernel entry point and stack of process * hash_proc: add proc into proc hash_list * get_pid: alloc a unique pid for process * 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
// 2. call setup_kstack to allocate a kernel stack for child process
// 3. call copy_mm to dup OR share mm according clone_flag
// 4. call copy_thread to setup tf & context in proc_struct
// 5. insert proc_struct into hash_list && proc_list
// 6. call wakeup_proc to make the new child
copy
学习时间 86分钟
操作时间 12分钟
按键次数 214次
实验次数 4次
报告字数 20480字
是否完成 完成