前置知识 进程、虚拟存储、中断机制、系统调用。
改动点 相比于 lab4
源代码,lab5
主要做了如下改动:
memlayout.h
增加用户空间的图形表示和宏定义
宏定义 USERTOP/USERBASE
等指明用户虚拟地址空间范围,图形表示显示哪些用户空间属于合法空间。
pmm.[ch]
添加若干与页表项、页面相关的拷贝函数
vmm.[ch]
添加若干与 mm/vma/pgdir
相关的函数
值得注意的是: struct mm_struct
属性内容发生变化:
1 2 3 4 5 6 7 8 9 struct mm_struct { list_entry_t mmap_list; struct vma_struct *mmap_cache ; pde_t *pgdir; int map_count; void *sm_priv; int mm_count; lock_t mm_lock; };
proc.h
扩展 proc_struct
数据结构
proc_struct
的扩展点具体如下:
1 2 3 4 5 6 7 struct proc_struct { ... int exit_code; uint32_t wait_state; struct proc_struct *cptr , *yptr , *optr ; };
proc.c
增加若干与 SYSCALL
相关的实现函数
在 ucore
中,进程与线程采用同一套管理机制,它们间的区别可能仅在于:进程之间不共享地址空间,而线程间共享某进程的地址空间 (通过共享 mm
结构即可),因此两个术语基本可以混用。
练习一 该练习用于了解 do_exec()
的具体实现。
在 lab4 中,我们已经知道:如何创建一个内核线程,并通过调度使得其正常执行。那么我们首先看一下 lab5 中所涉的内核线程:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 --- int pid = kernel_thread(init_main, NULL , 0 ); --- --- static int init_main (void *arg) { size_t nr_free_pages_store = nr_free_pages(); size_t kernel_allocated_store = kallocated(); int pid = kernel_thread(user_main, NULL , 0 ); if (pid <= 0 ) { panic("create user_main failed.\n" ); } while (do_wait(0 , NULL ) == 0 ) { schedule(); } return 0 ; } --- --- static int user_main (void *arg) {#ifdef TEST KERNEL_EXECVE2(TEST, TESTSTART, TESTSIZE); #else KERNEL_EXECVE(exit ); #endif panic("user_main execve failed.\n" ); } ---
接下来,我们主要看看 do_exec()
的实现细节 (do_exec
等价于实际系统中的 exec()
):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 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 ; } 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); } struct trapframe *tf = current->tf;memset (tf, 0 , sizeof (struct trapframe));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;
练习二 该练习用于了解 do_fork()->copy_mm()
的具体实现。
copy_mm()
具体指代复制当前进程的 mm
给新建 proc
的 mm
结构,具体实现细节直接看源代码即可,也没什么好说的。
练习三 该练习用于理解各系统调用的实现机理。
首先看用户空间的系统调用:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 static inline int syscall (int num, ...) { va_list ap; va_start(ap, num); uint32_t a[MAX_ARGS]; int i, ret; for (i = 0 ; i < MAX_ARGS; i ++) { a[i] = va_arg(ap, uint32_t ); } va_end(ap); asm volatile ( "int %1;" : "=a" (ret) : "i" (T_SYSCALL), "a" (num), "d" (a[0 ]), "c" (a[1 ]), "b" (a[2 ]), "D" (a[3 ]), "S" (a[4 ]) : "cc" , "memory" ) ; return ret; } int sys_exit (int error_code) { return syscall(SYS_exit, error_code); } int sys_fork (void ) { return syscall(SYS_fork); } int sys_wait (int pid, int *store) { return syscall(SYS_wait, pid, store); } int sys_yield (void ) { return syscall(SYS_yield); } int sys_kill (int pid) { return syscall(SYS_kill, pid); } int sys_getpid (void ) { return syscall(SYS_getpid); } int sys_putc (int c) { return syscall(SYS_putc, c); } int sys_pgdir (void ) { return syscall(SYS_pgdir); }
接下来,硬件得到 T_SYSCALL
中断号,并调用相关中断处理程序进行处理:
1 2 3 case T_SYSCALL: syscall(); break ;
接下来,看看内核程序 syscall()
的具体实现:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 static int sys_exit (uint32_t arg[]) { int error_code = (int )arg[0 ]; return do_exit(error_code); } static int sys_fork (uint32_t arg[]) { struct trapframe *tf = current->tf; uintptr_t stack = tf->tf_esp; return do_fork(0 , stack , tf); } static int sys_wait (uint32_t arg[]) { int pid = (int )arg[0 ]; int *store = (int *)arg[1 ]; return do_wait(pid, store); } static int sys_exec (uint32_t arg[]) { const char *name = (const char *)arg[0 ]; size_t len = (size_t )arg[1 ]; unsigned char *binary = (unsigned char *)arg[2 ]; size_t size = (size_t )arg[3 ]; return do_execve(name, len, binary, size); } static int sys_yield (uint32_t arg[]) { return do_yield(); } static int sys_kill (uint32_t arg[]) { int pid = (int )arg[0 ]; return do_kill(pid); } static int sys_getpid (uint32_t arg[]) { return current->pid; } static int sys_putc (uint32_t arg[]) { int c = (int )arg[0 ]; cputchar(c); return 0 ; } static int sys_pgdir (uint32_t arg[]) { print_pgdir(); return 0 ; } static int (*syscalls[]) (uint32_t arg[]) = { [SYS_exit] sys_exit, [SYS_fork] sys_fork, [SYS_wait] sys_wait, [SYS_exec] sys_exec, [SYS_yield] sys_yield, [SYS_kill] sys_kill, [SYS_getpid] sys_getpid, [SYS_putc] sys_putc, [SYS_pgdir] sys_pgdir, }; #define NUM_SYSCALLS ((sizeof(syscalls)) / (sizeof(syscalls[0]))) void 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); }
对于上述 sys_xxx
,简要做如下分析:
do_fork(0, stack, tf)
第一个参数为 0,表明其会完全复制当前进程的地址空间,第二三参数,表明其会完全复制当前进程的用户栈及中断帧。如此实现,含义与实际 fork()
相同。
由于之前早已说明 do_fork()
的实现细节,在此不再赘述。
do_execve(name, len, binary, size)
练习一已经说明其含义,在此不再赘述。
do_exit(error_code)
do_exit()
用于回收该进程的内存资源,并设置 PROC_ZOMBIE
状态以等待父进程处理。如果当前进程存在子进程,需要将它们置为 initproc
的子进程。
do_wait(pid, store)
do_wait()
用于等待子进程处于 PROC_ZOMBIE
状态,并回收其剩余资源 (指代内核栈、PCB)。
某进程资源的回收工作具体分为两步:do_exit()
自主回收内存资源,do_wait()
由父进程回收内核资源。之所以如此实现,原因在于:进程回收内核资源,则其必定在使用内核栈,而内核资源包括内核栈,因此存在一个矛盾。另外,父进程可通过回收资源过程,获取其子进程的运行结果,然后动态施行相关操作。
do_yield()
do_yield()
用于释放 CPU,并让其调度处理其他进程。
do_kill(pid)
do_kill()
用于设置特定进程的 flags |= PF_EXITING;
。
sys_getpid/sys_putc/sys_pgdir
此三者都比较简单,没什么可说的。
需要注意:do_yield()
和 do_kill()
功能实现的部分代码在函数内部,部分代码位于 trap()
内部,trap()
内部的实现如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 void trap (struct trapframe *tf) { if (current == NULL ) { trap_dispatch(tf); } else { struct trapframe *otf = current->tf; current->tf = tf; bool in_kernel = trap_in_kernel(tf); trap_dispatch(tf); current->tf = otf; if (!in_kernel) { if (current->flags & PF_EXITING) { do_exit(-E_KILLED); } if (current->need_resched) { schedule(); } } } }
扩展练习一 该练习用于实现 Copy on Write(COW)
机制。
COW
机制实现比较复杂,并没有实现。在此简要说明其思想。
当执行 do_fork(0, stack, tf) -> copy_mm()
时,复制 vma
集合、页目录表等内容,共享当前进程的物理页,并设置相关条目为只读 (而非原先的复制物理页)。
当新旧进程需要写共享物理页时,触发中断,执行 do_pgfault()
。如果该页面为只读,且其共享数大于 1,则表明存在新旧进程在共享物理页,因此复制该物理页给当前进程,并设置相关条目为读写。
如何创建用户线程? 借助于 do_fork()/do_execve()
,我们成功创建用户进程 (也可称作该进程对应的主线程),它所对应的栈直接位于虚拟地址空间中的栈区。
用户进程的栈大小默认为 8M。当栈所占实际空间小于 8M 时,它会因缺页中断而动态增长;当栈所占实际空间大于 8M 时,便会触发 栈溢出 。
对于用户线程而言,它们需要共享用户进程资源 (例如:地址空间、打开的文件列表),而独占栈空间、寄存器组等资源。
经过上面的实现,共享进程资源比较简单,无非就是设置相关指针以指向相同结构体;独占寄存器组也比较简单,使用 proc.context
保存即可;唯一难点在于:如何独占栈空间的同时,实现共享该进程的地址空间?
Linux 系统的解决方案是这样的:创建用户线程之时,在当前地址空间的堆区直接分配栈空间,以此作为该线程的栈。
用户线程的栈大小默认为 16K,最大为 2M。
注:Linux 的解决方案中,线程栈空间直接分配,它不会动态增长。