ucore_lab5
练习0:
修改proc.c , default_pmm.c , pmm.c , swap_fifo.c , vmm.c , trap.c文件中的代码即可。
练习1:
加载应用程序并执行(需要编码)
题目概述:
do_execv函数调用load_icode(位于kern/process/proc.c中)来加载并解析一个处于内存中的ELF执行文件格式的应用程序,建立相应的用户内存空间来放置应用程序的代码段、数据段等,且要设置好proc_struct结构中的成员变量trapframe中的内容,确保在执行此进程后,能够从应用程序设定的起始执行地址开始执行。需设置正确的trapframe内容。
实现思路:
load_icode函数主要用来被do_execve调用,将执行程序加载到进程空间(执行程序本身已从磁盘读取到内存中),这涉及到修改页表、分配用户栈等工作。给用户进程建立一个能够让用户进程正常运行的用户环境。我们按照代码中给出的提示完成初始化就可以了。
代码实现:
/* 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)eip是程序的入口,elf类的e_entry函数直接声明了,直接使用。
* tf_eflags should be set to enable computer to produce InterruptFL_IF打开中断
*/
tf->tf_cs = USER_CS;
tf->tf_ds = tf->tf_es = tf->tf_ss = USER_DS;
tf->tf_esp = USTACKTOP;
tf->tf_eip = elf->e_entry;
tf->tf_eflags = FL_IF;
memlayout.h:
#ifndef __KERN_MM_MEMLAYOUT_H__
#define __KERN_MM_MEMLAYOUT_H__
/* This file contains the definitions for memory management in our OS. */
/* global segment number */
#define SEG_KTEXT 1
#define SEG_KDATA 2
#define SEG_UTEXT 3
#define SEG_UDATA 4
#define SEG_TSS 5
/* global descrptor numbers */
#define GD_KTEXT ((SEG_KTEXT) << 3) // kernel text
#define GD_KDATA ((SEG_KDATA) << 3) // kernel data
#define GD_UTEXT ((SEG_UTEXT) << 3) // user text
#define GD_UDATA ((SEG_UDATA) << 3) // user data
#define GD_TSS ((SEG_TSS) << 3) // task segment selector
#define DPL_KERNEL (0)
#define DPL_USER (3)
#define KERNEL_CS ((GD_KTEXT) | DPL_KERNEL)
#define KERNEL_DS ((GD_KDATA) | DPL_KERNEL)
#define USER_CS ((GD_UTEXT) | DPL_USER)
#define USER_DS ((GD_UDATA) | DPL_USER)
load_icode:
- 调用mm_create函数来申请进程的内存管理数据结构mm所需内存空间,并对mm进行初始化;
- 调用setup_pgdir来申请一个页目录表所需的一个页大小的内存空间,并把描述ucore内核虚空间映射的内核页表(boot_pgdir所指)的内容拷贝到此新目录表中,最后让mm->pgdir指向此页目录表,这就是进程新的页目录表了,且能够正确映射内核虚空间;
- 根据应用程序执行码的起始位置来解析此ELF格式的执行程序,并调用mm_map函数根据ELF格式的执行程序说明的各个段(代码段、数据段、BSS段等)的起始位置和大小建立对应的vma结构,并把vma插入到mm结构中,从而表明了用户进程的合法用户态虚拟地址空间;
- 调用根据执行程序各个段的大小分配物理内存空间,并根据执行程序各个段的起始位置确定虚拟地址,并在页表中建立好物理地址和虚拟地址的映射关系,然后把执行程序各个段的内容拷贝到相应的内核虚拟地址中,至此应用程序执行码和数据已经根据编译时设定地址放置到虚拟内存中了;
- 需要给用户进程设置用户栈,为此调用mm_mmap函数建立用户栈的vma结构,明确用户栈的位置在用户虚空间的顶端,大小为256个页,即1MB,并分配一定数量的物理内存且建立好栈的虚地址<—>物理地址映射关系;
- 至此,进程内的内存管理vma和mm数据结构已经建立完成,于是把mm->pgdir赋值到cr3寄存器中,即更新了用户进程的虚拟内存空间,此时的initproc已经被hello的代码和数据覆盖,成为了第一个用户进程,但此时这个用户进程的执行现场还没建立好;
- 先清空进程的中断帧,再重新设置进程的中断帧,使得在执行中断返回指令“iret”后,能够让CPU转到用户态特权级,并回到用户态内存空间,使用用户态的代码段、数据段和堆栈,且能够跳转到用户进程的第一条指令执行,并确保在用户态能够响应中断;
static int load_icode(unsigned char *binary, size_t size) {
if (current->mm != NULL) {
panic("load_icode: current->mm must be empty.\n");
}
//准备部分:当前进程必须为空,这样才能加载到内存。
(在调用它的do_exceve函数中,如果没有bug,那么已经清空了)
int ret = -E_NO_MEM;
struct mm_struct *mm; //声明了一个页表
//第1步:为当前的进程创建一块内存
if ((mm = mm_create()) == NULL) {//申请内存
goto bad_mm;
}
这里调用了一个函数mm_create(kern/mm/vmm.c,43——60行):
struct mm_struct * mm_create(void) {
struct mm_struct *mm = kmalloc(sizeof(struct mm_struct));
if (mm != NULL) {
list_init(&(mm->mmap_list));
mm->mmap_cache = NULL;
mm->pgdir = NULL;
mm->map_count = 0;
if (swap_init_ok) swap_init_mm(mm);
else mm->sm_priv = NULL;
set_mm_count(mm, 0);
lock_init(&(mm->mm_lock));
}
return mm;
}
可以看到这个函数首先申请了一块内存空间,如果内存空间申请成功了,那么就会把这个内存空间返回给外面调用它的mm变量,如果申请失败,那么新开辟的空间都不存在,即为NULL,且会返回它自己,因此外部的判断条件是mm不能等于NULL,如果等于NULL,说明创建空间失败了,否则,就能够说明创建成功。
//(2) create a new PDT, and mm->pgdir= kernel virtual addr of PDT
第2步:调用 setup_pgdir来申请一个页目录表所需的一个页大小的内存空间
if (setup_pgdir(mm) != 0) {//申请页表
goto bad_pgdir_cleanup_mm;
}
这里调用了一个函数setup_pgdir(kern/process/proc.c,288——299行)
static int setup_pgdir(struct mm_struct *mm) {
struct Page *page;
if ((page = alloc_page()) == NULL) {
return -E_NO_MEM;
}
pde_t *pgdir = page2kva(page);
memcpy(pgdir, boot_pgdir, PGSIZE);
pgdir[PDX(VPT)] = PADDR(pgdir) | PTE_P | PTE_W;
mm->pgdir = pgdir;
return 0;
}
如果没有返回0,那么分配页目录表失败,因此程序需要判断为0的情况,到一个错误的状态。
//第3步:读取ELF格式的文件,在内存中复制该进程所需要的代码段等信息
//(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; //获取ELF格式文件的表头
在bootloader启动的过程中,已经将ucore内核和用户代码全部加载到内存,因为没有文件管理系统,我们只需要关注这个代码在内存中的哪里,找到了开头就能根据它找到数据段。
//(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) {
continue ;
}
//这个地方获取的是文件的各个段,包括代码段、数据段等。
//(3.5) call mm_map fun to setup the new vma ( ph->p_va, ph->p_memsz)
根据获取的各个段的开头,以及虚拟地址创建VMA(管理进程所认为的合法空间)
一开始给各个段赋予了一些属性:
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;
}
//使用mm_map函数建立合法空间(kern/mm/vmm.c,159——165行)
int mm_map(struct mm_struct *mm, uintptr_t addr, size_t len, uint32_t vm_flags,
struct vma_struct **vma_store) {
uintptr_t start = ROUNDDOWN(addr, PGSIZE), end = ROUNDUP(addr + len, PGSIZE);
if (!USER_ACCESS(start, end)) {
return -E_INVAL;
}
//关于mm_map的解释是:
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
//这里是拷贝内容,memcpy是拷贝函数
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
//执行程序的BSS段需要清空,这里全部设置为0
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);//设置为0
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;
}
//重新建立mm_map堆栈
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
*/
//完成一个优先级的转变,从内核态切换到用户态(特权级从0到3)实现部分
tf->tf_cs = USER_CS;
tf->tf_ds = tf->tf_es = tf->tf_ss = USER_DS;
tf->tf_esp = USTACKTOP;
tf->tf_eip = elf->e_entry;
tf->tf_eflags = FL_IF;
*tf是一个是中断帧的指针,总是指向内核栈的某个位置:当进程从用户空间跳到内核空间时,中断帧记录了进程在被中断前的状态。当内核需要跳回用户空间时,需要调整中断帧以恢复让进程继续执行的各寄存器值。
其定义在(kern/trap/trap.h,60——82行)。
1、将tf_cs设置为用户态,这个定义在(kern/mm/memlayout.h,第21行),有一个宏定义已经定义了用户态和内核态。
2、tf_ds=tf_es=tf_ss也需要设置为用户态:定义在(kern/mm/memlayout.h,第26行)
3、需要将esp设置为用户栈的栈顶,直接使用之前建立用户栈时的参数USTACKTOP就可以。
4、eip是程序的入口,elf类的e_entry函数直接声明了,直接使用。
5、FL_IF打开中断。
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;
}
1-1:用户进程执行
问题概述:
请在实验报告中描述当创建一个用户态进程并加载了应用程序后,CPU是如何让这个应用程序最终在用户态执行起来的。即这个用户态进程被ucore选择占用CPU执行(RUNNING态)到具体执行应用程序第一条指令的整个经过。
解答::
- 调用schedule函数,调度器占用了CPU的资源之后,用户态进程调用了exec系统调用,从而转入到了系统调用的处理例程;
- 之后进行正常的中断处理例程,然后控制权转移到了syscall.c中的syscall函数,然后根据系统调用号转移给了sys_exec函数,在该函数中调用了do_execve函数来完成指定应用程序的加载;
- 在do_execve中进行了若干设置,包括推出当前进程的页表,换用内核的PDT,调用load_icode函数完成对整个用户线程内存空间的初始化,包括堆栈的设置以及将ELF可执行文件的加载,之后通过current->tf指针修改了当前系统调用的trapframe,使得最终中断返回的时候能够切换到用户态,并且同时可以正确地将控制权转移到应用程序的入口处;
- 在完成了do_exec函数之后,进行正常的中断返回的流程,由于中断处理例程的栈上面的eip已经被修改成了应用程序的入口处,而CS上的CPL是用户态,因此iret进行中断返回的时候会将堆栈切换到用户的栈,并且完成特权级的切换,并且跳转到要求的应用程序的入口处;
- 开始执行应用程序的第一条代码
练习2:
父进程复制自己的内存空间给子进程(需要编码)
题目概述:
创建子进程的函数do_fork在执行中将拷贝当前进程(即父进程)的用户内存地址空间中的合法内容到新进程中(子进程),完成内存资源的复制。具体是通过copy_range函数(位于kern/mm/pmm.c中)实现的,请补充copy_range的实现,确保能够正确执行。
函数功能:
copy_range()将实际的代码段和数据段搬到新的子进程里面去,再设置好页表的相关内容,使得子进程有自己的内存管理架构。
实现思路:
代码实现:
//将实际的代码段和数据段搬到新的子进程里面去,再设置好页表的相关内容
int copy_range(pde_t *to, pde_t *from, uintptr_t start, uintptr_t end, bool share) {
//确保start和end可以整除PGSIZE
assert(start % PGSIZE == 0 && end % PGSIZE == 0);
assert(USER_ACCESS(start, end));
//以页为单位进行复制
do {
//得到A&B的pte地址
pte_t *ptep = get_pte(from, start, 0), *nptep;
if (ptep == NULL)
{
start = ROUNDDOWN(start + PTSIZE, PTSIZE);
continue ;
}
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);
//为B分一个页的空间
struct Page *npage=alloc_page();
assert(page!=NULL);
assert(npage!=NULL);
int ret=0;
//##########################修改部分########################################
/* LAB5:EXERCISE2 YOUR CODE
* (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
*/
//1.找寻父进程的内核虚拟页地址
void * kva_src = page2kva(page);
//2.找寻子进程的内核虚拟页地址
void * kva_dst = page2kva(npage);
//3.复制父进程内容到子进程
memcpy(kva_dst, kva_src, PGSIZE);
//4.建立物理地址与子进程的页地址起始位置的映射关系
ret = page_insert(to, npage, start, perm);
//##########################修改部分########################################
assert(ret == 0);
}
start += PGSIZE;
} while (start != 0 && start < end);
return 0;
}
page2kva:
**函数功能:**返回一个内核页的虚拟地址,返回值是void指针
static inline void*page2kva(struct Page* page) {
return KADDR(page2pa(page));
}
page_insert:
**函数功能:**插入一个新的页表页并建立映射关系
int page_insert(pde_t * pgdir, struct Page* page, uintptr_t la, uint32_t perm) {
pte_t* ptep = get_pte(pgdir, la, 1);
if (ptep == NULL) {
return -E_NO_MEM;
}
page_ref_inc(page);
if (*ptep & PTE_P) {
struct Page* p = pte2page(*ptep);
if (p == page) {
page_ref_dec(page);
}
else {
page_remove_pte(pgdir, la, ptep);
}
}
*ptep = page2pa(page) | PTE_P | perm;
tlb_invalidate(pgdir, la);
return 0;
}
2-1、Copy on Write 机制
首先,Copy on Write 是在复制一个对象的时候并不是真正的把原先的对象复制到内存的另外一个位置上,而是在新对象的内存映射表中设置一个指针,指向源对象的位置,并把那块内存的Copy-On-Write位设置为1。通俗来说一下这样做的好处:如果复制的对象只是对内容进行"读"操作,那其实不需要真正复制,这个指向源对象的指针就能完成任务,这样便节省了复制的时间并且节省了内存。但是问题在于,如果复制的对象需要对内容进行写的话,单单一个指针可能满足不了要求,因为这样对内容的修改会影响其他进程的正确执行,所以就需要将这块区域复制一下,当然不需要全部复制,只需要将需要修改的部分区域复制即可,这样做大大节约了内存并提高效率。
因为如果设置原先的内容为只可读,则在对这段内容进行写操作时候便会引发Page Fault,这时候我们便知道这段内容是需要去写的,在Page Fault中进行相应处理即可。也就是说利用Page Fault来实现权限的判断,或者说是真正复制的标志。
基于原理和之前的用户进程创建、复制、运行等机制进行分析,设计思想:
- 设置一个标记位,用来标记某块内存是否共享,实际上dup_mmap函数中有对share的设置,因此首先需要将share设为1,表示可以共享。
- 在pmm.c中为copy_range添加对共享页的处理,如果share为1,那么将子进程的页面映射到父进程的页面即可。由于两个进程共享一个页面之后,无论任何一个进程修改页面,都会影响另外一个页面,所以需要子进程和父进程对于这个共享页面都保持只读。
- 当程序尝试修改只读的内存页面的时候,将触发Page Fault中断,这时候我们可以检测出是超出权限访问导致的中断,说明进程访问了共享的页面且要进行修改,因此内核此时需要重新为进程分配页面、拷贝页面内容、建立映射关系
练习3:
阅读分析源代码,理解进程执行 fork/exec/wait/exit 的实现,以及系统调用的实现(不需要编码)
1. do_fork
- 调用alloc_proc,首先获得一块用户信息块。
- 调用setup_kstack为进程分配一个内核栈。
- 调用copy_mm复制原进程的内存管理信息到新进程(但内核线程不必做此事)
- 调用copy_thread复制原进程上下文到新进程
- 调用hash_proc和set_links将新进程添加到进程列表hash_list和proc_list中
- 调用wakeup_proc唤醒新进程
- 返回新进程号ret(proc->id)
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;
proc = alloc_proc(); //调用alloc_proc,首先获得一块用户信息块
assert(proc->wait_state == 0);
if (setup_kstack(proc) != 0) //调用setup_kstack为进程分配一个内核栈。
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复制原进程上下文到新进程
proc->parent = current;
bool intr_flag;
local_intr_save(intr_flag);
{
proc->pid = get_pid();
hash_proc(proc);
//调用hash_proc和set_links将新进程添加到进程列表hash_list和proc_list中
//nr_process++;
set_links(proc);
}
local_intr_restore(intr_flag);
wakeup_proc(proc); //调用wakeup_proc唤醒新进程
ret = proc->pid; //返回新进程号ret(proc->id)
fork_out:
return ret;
bad_fork_cleanup_kstack:
put_kstack(proc);
bad_fork_cleanup_proc:
kfree(proc);
goto fork_out;
}
2. do_execve
-
do_execve
函数做的事请比较简单- 检查当前进程所分配的内存区域是否存在异常。
- 回收当前进程的所有资源,包括已分配的内存空间/页目录表等等。
- 读取可执行文件,并根据
ELFheader
分配特定位置的虚拟内存,并加载代码与数据至特定的内存地址,最后分配堆栈并设置trapframe
属性。 - 设置新进程名称。
-
该函数几乎释放原进程所有的资源,除了PCB。也就是说,
do_execve
保留了原进程的PID、原进程的属性、原进程与其他进程之间的关系等等。 -
该函数的具体实现如下
COPYint 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); // 删除该内存管理所对应的PDT put_pgdir(mm); mm_destroy(mm); } current->mm = NULL; } // 加载可执行文件代码,重设mm_struct,以及重置trapframe int ret; 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); }
3. do_wait
-
do_wait
程序会使某个进程一直等待,直到(特定)子进程退出后,该进程才会回收该子进程的资源并函数返回。该函数的具体操作如下:-
检查当前进程所分配的内存区域是否存在异常。
-
查找特定/所有子进程中是否存在某个等待父进程回收的子进程(
PROC_ZOMBIE
)。
- 如果有,则回收该进程并函数返回。
- 如果没有,则设置当前进程状态为
PROC_SLEEPING
并执行schedule
调度其他进程运行。当该进程的某个子进程结束运行后,当前进程会被唤醒,并在do_wait
函数中回收子进程的PCB内存资源。
-
-
该函数的具体实现如下:
COPYint do_wait(int pid, int *code_store) { struct mm_struct *mm = current->mm; if (code_store != NULL) { if (!user_mem_check(mm, (uintptr_t)code_store, sizeof(int), 1)) { return -E_INVAL; } } struct proc_struct *proc; bool intr_flag, haskid; repeat: haskid = 0; if (pid != 0) { proc = find_proc(pid); if (proc != NULL && proc->parent == current) { haskid = 1; if (proc->state == PROC_ZOMBIE) goto found; } } else { proc = current->cptr; for (; proc != NULL; proc = proc->optr) { haskid = 1; if (proc->state == PROC_ZOMBIE) goto found; } } if (haskid) { current->state = PROC_SLEEPING; current->wait_state = WT_CHILD; schedule(); if (current->flags & PF_EXITING) do_exit(-E_KILLED); goto repeat; } return -E_BAD_PROC; found: if (proc == idleproc || proc == initproc) panic("wait idleproc or initproc.\n"); if (code_store != NULL) *code_store = proc->exit_code; local_intr_save(intr_flag); { unhash_proc(proc); remove_links(proc); } local_intr_restore(intr_flag); put_kstack(proc); kfree(proc); return 0; }
4. do_exit
-
该函数与
do_execve/do_wait
函数中的进程回收代码类似,但又有所不同。其具体操作如下:-
·回收所有内存(除了PCB,该结构只能由父进程回收)
-
设置当前的进程状态为
PROC_ZOMBIE
-
设置当前进程的退出值
current->exit_code
。 -
如果有父进程,则唤醒父进程,使其准备回收该进程的PCB。
正常情况下,除了
initproc
和idleproc
以外,其他进程一定存在父进程。 -
如果当前进程存在子进程,则设置所有子进程的父进程为
initproc
。这样倘若这些子进程进入结束状态,则initproc
可以代为回收资源。 -
执行进程调度。一旦调度到当前进程的父进程,则可以马上回收该终止进程的
PCB
。
-
-
该函数的具体实现如下
COPYint do_exit(int error_code) { if (current == idleproc) panic("idleproc exit.\n"); if (current == initproc) panic("initproc exit.\n"); // 释放所有内存空间 struct mm_struct *mm = current->mm; if (mm != NULL) { lcr3(boot_cr3); if (mm_count_dec(mm) == 0) { exit_mmap(mm); put_pgdir(mm); mm_destroy(mm); } current->mm = NULL; } // 设置当前进程状态 current->state = PROC_ZOMBIE; current->exit_code = error_code; // 请求父进程回收剩余资源 bool intr_flag; struct proc_struct *proc; local_intr_save(intr_flag); { proc = current->parent; // 唤醒父进程。父进程准备回收该进程的PCB资源。 if (proc->wait_state == WT_CHILD) wakeup_proc(proc); // 如果当前进程存在子进程,则设置所有子进程的父进程为init。 while (current->cptr != NULL) { proc = current->cptr; current->cptr = proc->optr; proc->yptr = NULL; if ((proc->optr = initproc->cptr) != NULL) initproc->cptr->yptr = proc; proc->parent = initproc; initproc->cptr = proc; if (proc->state == PROC_ZOMBIE) { if (initproc->wait_state == WT_CHILD) wakeup_proc(initproc); } } } local_intr_restore(intr_flag); // 该进程的生命周期即将结束,调度其他进程执行。 schedule(); panic("do_exit will not return!! %d.\n", current->pid); }
5. syscall系统调用
-
syscall
是内核程序为用户程序提供内核服务的一种方式。 -
在中断处理例程中,程序会根据中断号,执行
syscall
函数(注意该syscall函数为内核代码,非用户库中的syscall函数)。内核syscall函数会一一取出六个寄存器的值,并根据系统调用号来执行不同的系统调用。而那些系统调用的实质就是其他内核函数的wrapper。以下为syscall
函数实现的代码:COPYvoid syscall(void) { struct trapframe *tf = current->tf; uint32_t arg[5]; int num = tf->tf_regs.reg_eax; if (num >= 0 && num < NUM_SYSCALLS) { if (syscalls[num] != NULL) { arg[0] = tf->tf_regs.reg_edx; arg[1] = tf->tf_regs.reg_ecx; arg[2] = tf->tf_regs.reg_ebx; arg[3] = tf->tf_regs.reg_edi; arg[4] = tf->tf_regs.reg_esi; tf->tf_regs.reg_eax = syscalls[num](arg); return ; } } print_trapframe(tf); panic("undefined syscall %d, pid = %d, name = %s.\n", num, current->pid, current->name); }
-
等相应的内核函数结束后,程序通过之前保留的
trapframe
返回用户态。一次系统调用结束。
3-1、问题一
请分析fork/exec/wait/exit在实现中是如何影响进程的执行状态的?
- fork会修改其子进程的状态为
PROC_RUNNABLE
,而当前进程状态不变。 - exec不修改当前进程的状态,但会替换内存空间里所有的数据与代码。
- wait会先检测是否存在子进程。如果存在进入
PROC_ZONBIE
的子进程,则回收该进程并函数返回。但若存在尚处于PROC_RUNNABLE
的子进程,则当前进程会进入PROC_SLEEPING
状态,并等待子进程唤醒。 - exit会将当前进程状态设置为
PROC_ZONBIE
,并唤醒父进程,使其处于PROC_RUNNABLE
的状态,之后主动让出CPU。
3-2、问题二
- 请给出ucore中一个用户态进程的执行状态生命周期图(包执行状态,执行状态之间的变换关系,以及产生变换的事件或函数调用)。(字符方式画即可)
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-bOIjsSGK-1646988884970)(E:\Typore图片\image-20211219140841102.png)]
扩展练习
实现 Copy on Write 机制
给出实现源码和设计报告。
这个扩展练习涉及到本实验和上一个实验“虚拟内存管理”。在ucore操作系统中,当一个用户父进程创建自己的子进程时,父进程会把其申请的用户空间设置为只读,子进程可共享父进程占用的用户内存空间中的页面(这就是一个共享的资源)。当其中任何一个进程修改此用户内存空间中的某页面时,ucore会通过page fault异常获知该操作,并完成拷贝内存页面,使得两个进程都有各自的内存页面。这样一个进程所做的修改不会被另外一个进程可见了。请在ucore中实现这样的COW机制。
实现思路:
当一个用户父进程创建自己的子进程时,父进程会把其申请的用户空间设置为只读,子进程可共享父进程占用的用户内存空间中的页面。当其中任何一个进程修改此用户内存空间中的某页面时,ucore会通过page fault异常获知该操作,并完成拷贝内存页面,使得两个进程都有各自的内存页面。这样一个进程所做的修改不会被另外一个进程可见了。
- 设置一个标记位,用来标记某块内存是否共享,dup_mmap函数中有对share的设置,因此首先需要将share设为1,表示可以共享。
- 在pmm.c中为copy_range添加对共享页的处理,如果share为1,那么将子进程的页面映射到父进程的页面。由于两个进程共享一个页面之后,无论任何一个进程修改页面,都会影响另外一个页面,所以需要子进程和父进程对于这个共享页面都保持只读。
- 当程序尝试修改只读的内存页面的时候,将触发Page Fault中断,这时候我们可以检测出是超出权限访问导致的中断,进程访问了共享的页面且要进行修改,因此内核此时需要重新为进程分配页面、拷贝页面内容、建立映射关系.
代码实现:
copy_range:
当进行内存访问时,CPU会根据PTE上的读写位PTE_P
、PTE_W
来确定当前内存操作是否允许,如果不允许,则缺页中断。我们可以在copy_range
函数中,将父进程中所有PTE中的PTE_W
置为0,这样便可以将父进程中所有空间都设置为只读。然后使子进程的PTE全部指向父进程中PTE存放的物理地址,这样便可以达到内存共享的目的。
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);
int ret = 0;
// 如果启用写时复制
if(share)
{
cprintf("Sharing the page 0x%x\n", page2kva(page));
// 物理页面共享,并设置两个PTE上的标志位为只读
page_insert(from, page, start, perm & ~PTE_W);
ret = page_insert(to, page, start, perm & ~PTE_W);
}
// 完整拷贝内存
else
{
// alloc a page for process B
// 目标页面地址
struct Page *npage = alloc_page();
assert(page!=NULL);
assert(npage!=NULL);
cprintf("alloc a new page 0x%x\n", page2kva(npage));
void * kva_src = page2kva(page);
void * kva_dst = page2kva(npage);
memcpy(kva_dst, kva_src, PGSIZE);
// 将目标页面地址设置到PTE中
ret = page_insert(to, npage, start, perm);
}
assert(ret == 0);
}
start += PGSIZE;
} while (start != 0 && start < end);
return 0;
}
do_pgfault:
当某个进程想写入一个共享内存时,由于PTE上的PTE_W
为0,所以会触发缺页中断处理程序。此时进程需要在缺页中断处理程序中复制该页内存,并设置该页内存所对应的PTE_W
为1。
int do_pgfault(struct mm_struct *mm, uint32_t error_code, uintptr_t addr) {
// ........
// 查找当前虚拟地址所对应的页表项
if ((ptep = get_pte(mm->pgdir, addr, 1)) == NULL) {
cprintf("get_pte in do_pgfault failed\n");
goto failed;
}
// 如果这个页表项所对应的物理页不存在,则
if (*ptep == 0) {
// 分配一块物理页,并设置页表项
if (pgdir_alloc_page(mm->pgdir, addr, perm) == NULL) {
cprintf("pgdir_alloc_page in do_pgfault failed\n");
goto failed;
}
}
else {
struct Page *page=NULL;
// 如果当前页错误的原因是写入了只读页面
if (*ptep & PTE_P) {
// 写时复制:复制一块内存给当前进程
cprintf("\n\nCOW: ptep 0x%x, pte 0x%x\n",ptep, *ptep);
// 原先所使用的只读物理页
page = pte2page(*ptep);
// 如果该物理页面被多个进程引用
if(page_ref(page) > 1)
{
// 释放当前PTE的引用并分配一个新物理页
struct Page* newPage = pgdir_alloc_page(mm->pgdir, addr, perm);
void * kva_src = page2kva(page);
void * kva_dst = page2kva(newPage);
// 拷贝数据
memcpy(kva_dst, kva_src, PGSIZE);
}
// 如果该物理页面只被当前进程所引用,即page_ref等1
else
// 则可以直接执行page_insert,保留当前物理页并重设其PTE权限。
page_insert(mm->pgdir, page, addr, perm);
}
else
{
// 如果swap已经初始化完成
if(swap_init_ok) {
// 将目标数据加载到某块新的物理页中。
// 该物理页可能是尚未分配的物理页,也可能是从别的已分配物理页中取的
if ((ret = swap_in(mm, addr, &page)) != 0) {
cprintf("swap_in in do_pgfault failed\n");
goto failed;
}
// 将该物理页与对应的虚拟地址关联,同时设置页表。
page_insert(mm->pgdir, page, addr, perm);
}
else {
cprintf("no swap_init_ok but ptep is %x, failed\n",*ptep);
goto failed;
}
}
// 当前缺失的页已经加载回内存中,所以设置当前页为可swap。
swap_map_swappable(mm, addr, page, 1);
page->pra_vaddr = addr;
}
ret = 0;
failed:
return ret;
}