练习1: 加载应用程序并执行(需要编码)
execve函数
为了将一个新程序读进内存中执行,进程需要系统调用SYS_exec
,该系统调用实际会调用do_execve
:
// do_execve - call exit_mmap(mm)&put_pgdir(mm) to reclaim memory space of current process
// - call load_icode to setup new memory space accroding binary prog.
// 调用exit_mmap和put_pgdir来取回正在运行的进程的空间
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);
// 先清除掉进程的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;
}
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);
}
execve
主要清除了进程原本的用户内存空间,然后他调用了load_icode
,它的工作是将二进制文件加载到进程中。因为这时ucore还没有文件系统,一个程序的elf文件已经随内核一同加载到内存中,这时只要将它读取到进程中,就可以执行。load_icode
代码如下:
/* load_icode - load the content of binary program(ELF format) as the new content of current process
* 加载二进制文件到进程中
* @binary: the memory addr of the content of binary program
* @size: the size of the content of binary program
*/
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
//调用 mm_create 函数来申请进程的内存管理数据结构 mm 所需内存空间,并对 mm 进行初始化;
if ((mm = mm_create()) == NULL) {
goto bad_mm;
}
//(2) create a new PDT, and mm->pgdir= kernel virtual addr of PDT
// 申请一个页目录表所需的一个页大小的内存空间
// 并把描述ucore内核虚空间映射的内核页表(boot_pgdir所指)的内容拷贝到此新目录表中
// 最后让mm->pgdir指向此页目录表,这就是进程新的页目录表了,且能够正确映射内核虚空间
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
// 根据应用程序执行码的起始位置来解析此ELF格式的执行程序
// 并调用 mm_map 函数根据ELF格式执行程序的各个段(代码段、数据段、BSS段等)的起始位置和大小建立对应的vma结构,
// 并把 vma 插入到 mm 结构中,表明这些是用户进程的合法用户态虚拟地址空间
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) {
ret = -E_INVAL_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)
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,sr3,设置cr3寄存器的内容为页目录表的地址
mm_count_inc(mm);
current->mm = mm;
current->cr3 = PADDR(mm->pgdir);
lcr3(PADDR(mm->pgdir));
//(6) setup trapframe for user environment
// 先清空进程的中断帧,再重新设置进程的中断帧,
// 使得在执行中断返回指令iret后,能够让 CPU转到用户态特权级,并回到用户态内存空间,
// 使用用户态的代码段、数据段和堆栈,且能够跳转到用户进程的第一条指令执行,并确保在用户态能够响应中断
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;
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;
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;
}
这里主要关注中断帧的设置:
//(6) setup trapframe for user environment
// 先清空进程的中断帧,再重新设置进程的中断帧,
// 使得在执行中断返回指令iret后,能够让CPU转到用户态特权级,并回到用户态内存空间,
// 使用用户态的代码段、数据段和堆栈,且能够跳转到用户进程的第一条指令执行,并确保在用户态能够响应中断
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;
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;
ret = 0;
进程的虚拟内存空间的分布图:
4G -------------------------> +---------------------------------+
* | |
* | Empty Memory (*) |
* | |
* +---------------------------------+ 0xFB000000
* | Cur. Page Table (Kern, RW) | RW/-- PTSIZE
* VPT -----------------> +---------------------------------+ 0xFAC00000
* | Invalid Memory (*) | --/--
* KERNTOP -------------> +---------------------------------+ 0xF8000000
* | |
* | Remapped Physical Memory | RW/-- KMEMSIZE
* | |
* KERNBASE ------------> +---------------------------------+ 0xC0000000
* | Invalid Memory (*) | --/--
* USERTOP -------------> +---------------------------------+ 0xB0000000
* | User stack |
* +---------------------------------+
* | |
* : :
* | ~~~~~~~~~~~~~~~~ |
* : :
* | |
* ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
* | User Program & Heap |
* UTEXT ---------------> +---------------------------------+ 0x00800000
* | Invalid Memory (*) | --/--
* | - - - - - - - - - - - - - - - |
* | User STAB Data (optional) |
* USERBASE, USTAB------> +---------------------------------+ 0x00200000
* | Invalid Memory (*) | --/--
* 0 -------------------> +---------------------------------+ 0x00000000
练习2: 父进程复制自己的内存空间给子进程(需要编码)
函数调用的流程:
do_fork()---->copy_mm()---->dup_mmap()---->copy_range()
其中copy_mm
主要创建了进程的mm
结构和页目录项,dup_mmap
创建了vma
结构,并通过copy_range
将父进程页面的内容复制到子进程中。
fork函数
进程利用系统调用SYS_fork
来创建一个新进程,系统调用会引起一个中断,最终由trap_dispatch
根据中断号调用kern/syscall/syscall.c
下的sys_fork
:
static int
sys_fork(uint32_t arg[]) {
struct trapframe *tf = current->tf;
uintptr_t stack = tf->tf_esp;
return do_fork(0, stack, tf);
}
sys_fork
实际会调用do_fork
:
/* 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;
// 分配并初始化进程控制块(alloc_proc 函数)
if ((proc = alloc_proc()) == NULL) {
goto fork_out;
}
proc->parent = current;
assert(current->wait_state == 0); // 确保当前进程正在等待
// 分配并初始化内核栈(setup_stack 函数)
if (setup_kstack(proc) != 0) {
goto bad_fork_cleanup_proc;
}
// 根据 clone_flag标志复制或共享进程内存管理结构(copy_mm 函数)
if (copy_mm(clone_flags, proc) != 0) {
goto bad_fork_cleanup_kstack;
}
// 设置进程在内核(将来也包括用户态)正常运行和调度所需的中断帧和执行上下文(copy_thread 函数)
copy_thread(proc, stack, tf);
// 把设置好的进程控制块放入hash_list 和 proc_list 两个全局进程链表中
bool intr_flag;
local_intr_save(intr_flag);
{
proc->pid = get_pid();
hash_proc(proc);
set_links(proc); //将原来简单的计数改成来执行set_links函数,从而实现设置进程的相关链接
}
local_intr_restore(intr_flag);
// 进程已经准备好执行了,把进程状态设置为“就绪”态
wakeup_proc(proc);
//设置返回码为子进程的 id 号
ret = proc->pid;
fork_out:
return ret;
bad_fork_cleanup_kstack:
put_kstack(proc);
bad_fork_cleanup_proc:
kfree(proc);
goto fork_out;
}
do_fork
进行一系列初始化工作后,进程就创建完成了,这时新创建的进程是父进程的副本。
总结一下,进程复制fork
的步骤如下:
分配新的proc --> 分配kernel stack --> 复制父进程的内存 -->
设置trapframe&context --> 其他house-keeping work --> 返回新进程的pid
COW机制
COW机制是指copy on write机制,在进程请求系统调用SYS_fork
时,父进程将得到内存空间和他完全相同的子进程:
fork is an operation whereby a process creates a copy of itself.
COW机制可以让父子进程共享内存空间,从而节省内存。在进程A创建进程B时,B不直接开辟新的内存空间并复制父进程的内存,而是让进程B直接使用A的内存页面,但将进程B的页目录下的项设为只读,当进程B对页面进行写操作时,将产生page fault,这时才真正请求新的物理页,并复制进程A中需要写的那块内存页面到进程B。
练习3: 阅读分析源代码,理解进程执行 fork/exec/wait/exit 的实现,以及系统调用的实现(不需要编码)
fork
首先当程序执行fork
时,fork使用了系统调用SYS_fork
,而系统调用SYS_fork
则主要是由do_fork
和wakeup_proc
来完成的。do_fork
完成的工作在lab4的时候已经做过详细介绍,这里再简单说一下,主要是完成了以下工作:
1、分配并初始化进程控制块(alloc_proc
函数);
2、分配并初始化内核栈(setup_stack
函数);
3、根据 clone_flag标志复制或共享进程内存管理结构(copy_mm
函数);
4、设置进程在内核(将来也包括用户态)正常运行和调度所需的中断帧和执行上下文(copy_thread
函数);
5、把设置好的进程控制块放入hash_list
和 proc_list
两个全局进程链表中;
6、自此,进程已经准备好执行了,把进程状态设置为“就绪”态;
7、设置返回码为子进程的 id 号。
而wakeup_proc
函数主要是将进程的状态设置为等待,即proc->wait_state = 0
,此处不赘述。
exec
当应用程序执行的时候,会调用SYS_exec
系统调用,而当ucore收到此系统调用的时候,则会使用do_execve()
函数来实现,因此这里我们主要介绍do_execve()
函数的功能,函数主要时完成用户进程的创建工作,同时使用户进程进入执行。
主要工作如下:
1、首先为加载新的执行码做好用户态内存空间清空准备。如果mm
不为NULL,则设置页表为内核空间页表,且进一步判断mm
的引用计数减1后是否为0,如果为0,则表明没有进程再需要此进程所占用的内存空间,为此将根据mm中的记录,释放进程所占用户空间内存和进程页表本身所占空间。最后把当前进程的mm
内存管理指针为空。
2、接下来是加载应用程序执行码到当前进程的新创建的用户态虚拟空间中。之后就是调用load_icode
从而使之准备好执行。(具体load_icode
的功能在练习1已经介绍的很详细了,这里不赘述了)
wait
当执行wait
功能的时候,会调用系统调用SYS_wait
,而该系统调用的功能则主要由do_wait
函数实现,主要工作就是父进程如何完成对子进程的最后回收工作,具体的功能实现如下:
1、 如果 pid!=0
,表示只找一个进程 id 号为 pid
的退出状态的子进程,否则找任意一个处于退出状态的子进程;
2、 如果此子进程的执行状态不为PROC_ZOMBIE
,表明此子进程还没有退出,则当前进程设置执行状态为PROC_SLEEPING
(睡眠),睡眠原因为WT_CHILD
(即等待子进程退出),调用schedule()
函数选择新的进程执行,自己睡眠等待,如果被唤醒,则重复跳回步骤 1 处执行;
3、 如果此子进程的执行状态为 PROC_ZOMBIE
,表明此子进程处于退出状态,需要当前进程(即子进程的父进程)完成对子进程的最终回收工作,即首先把子进程控制块从两个进程队列proc_list
和hash_list
中删除,并释放子进程的内核堆栈和进程控制块。自此,子进程才彻底地结束了它的执行过程,它所占用的所有资源均已释放。
exit
当执行exit
功能的时候,会调用系统调用SYS_exit
,而该系统调用的功能主要是由do_exit
函数实现。具体过程如下:
1、先判断是否是用户进程,如果是,则开始回收此用户进程所占用的用户态虚拟内存空间;(具体的回收过程不作详细说明)
2、设置当前进程的中hi性状态为PROC_ZOMBIE
,然后设置当前进程的退出码为error_code
。表明此时这个进程已经无法再被调度了,只能等待父进程来完成最后的回收工作(主要是回收该子进程的内核栈、进程控制块)
3、如果当前父进程已经处于等待子进程的状态,即父进程的wait_state
被置为WT_CHILD
,则此时就可以唤醒父进程,让父进程来帮子进程完成最后的资源回收工作。
4、如果当前进程还有子进程,则需要把这些子进程的父进程指针设置为内核线程init
,且各个子进程指针需要插入到init
的子进程链表中。如果某个子进程的执行状态是 PROC_ZOMBIE
,则需要唤醒init
来完成对此子进程的最后回收工作。
5、执行schedule()
调度函数,选择新的进程执行。
所以说该函数的功能简单的说就是,回收当前进程所占的大部分内存资源,并通知父进程完成最后的回收工作。
关于系统调用
首先罗列下目前ucore所有的系统调用如下表:
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
一般来说,用户进程只能执行一般的指令,无法执行特权指令。采用系统调用机制为用户进程提供一个获得操作系统服务的统一接口层,简化用户进程的实现。
根据之前的分析,应用程序调用的exit/fork/wait/getpid
等库函数最终都会调用 syscall 函数,只是调用的参数不同而已(分别是 SYS_exit
/ SYS_fork
/ SYS_wait
/ SYS_getid
)
当应用程序调用系统函数时,一般执行INT T_SYSCALL
指令后,CPU 根据操作系统建立的系统调用中断描述符,转入内核态,然后开始了操作系统系统调用的执行过程,在内核函数执行之前,会保留软件执行系统调用前的执行现场,然后保存当前进程的tf
结构体中,之后操作系统就可以开始完成具体的系统调用服务,完成服务后,调用IRET
返回用户态,并恢复现场。这样整个系统调用就执行完毕了。