1.exec系统调用原型
在linux
系统下系统调用exec
是以新的进程去代替原来的进程,但进程的PID保持不变。因此,可以这样认为,exec系统调用并没有创建新的进程,只是替换了原来进程上下文的内容。原进程的代码段,数据段,堆栈段被新的进程所代替。在我们的内核中,如果仅有 fork
的话,那么所有的进程都只能和用户初始进程一样执行同样的代码段,这显然是远远不够的。于是我们还需要引入 exec
系统调用来执行不同的可执行文件:
#define __NR_execve 221 int sys_exec(char* name) { return syscall(__NR_execve,0,name,0); }
|
上面这段代码是定义在用户态的app.c
中,name
为传递给sys_exec
函数的参数,代表了要加载的可执行文件的名字。实际上一般来说可执行文件都有参数,因此参数也需要通过sys_exec
传递进去,这里先默认无参数。
用户在用户态调用此函数来加载执行一个可执行文件,内核通过系统调用分发,因此在内核的sys_call.c
中做了如下修改:
uint64_t __sys_exec(const char* name) { char* app_name = translated_byte_buffer(name); printk("exec app_name:%s\n",app_name); exec(app_name); return 0; }
|
和之前同理用户态传进来的字符串的参数由于地址空间的不同因此需要先去调用translated_byte_buffer
转换一下,然后去调用exec
函数,此函数会用来根据传入的app
的名字进行加载执行
在实现exec
函数之前,先来精简一下loader.c
中的代码:
void load_app(size_t app_id) { AppMetadata metadata = get_app_data(app_id + 1); elf64_ehdr_t *ehdr = metadata.start; elf_check(ehdr); TaskControlBlock* proc = task_create_pt(app_id); load_segment(ehdr,proc); proc->entry = (u64)ehdr->e_entry; proc_ustack(proc); }
|
对load_app
此函数进行了简化,封装了三个函数,一个是elf_check(elf64_ehdr_t *ehdr)
,用于检查传入的elf
文件的魔数,另一个是load_segment(elf64_ehdr_t *ehdr,struct TaskControlBlock* proc)
,用于加载应用程序的数据并映射,第三个是proc_ustack(struct TaskControlBlock *p)
,用于映射应用程序的用户栈。
void elf_check(elf64_ehdr_t *ehdr) { assert(*(u32 *)ehdr==ELFMAG); if (ehdr->e_machine != EM_RISCV || ehdr->e_ident[EI_CLASS] != ELFCLASS64) { panic("only riscv64 elf file is supported"); } }
|
void load_segment(elf64_ehdr_t *ehdr,struct TaskControlBlock* proc) { elf64_phdr_t *phdr; for (size_t i = 0; i < ehdr->e_phnum; i++) { phdr =(u64) (ehdr->e_phoff + ehdr->e_phentsize * i + (u64)ehdr); if(phdr->p_type == PT_LOAD) { u64 start_va = phdr->p_vaddr; proc->ustack = start_va + phdr->p_memsz; u8 map_perm = PTE_U | flags_to_mmap_prot(phdr->p_flags); u64 map_size = PGROUNDUP(phdr->p_memsz); for (size_t j = 0; j < map_size; j+= PAGE_SIZE) { PhysPageNum ppn = kalloc(); u64 paddr = phys_addr_from_phys_page_num(ppn).value; memcpy(paddr, (u64)ehdr + phdr->p_offset + j, PAGE_SIZE); PageTable_map(&proc->pagetable,virt_addr_from_size_t(start_va + j), \ phys_addr_from_size_t(paddr), PAGE_SIZE , map_perm); } } } proc->ustack = 2 * PAGE_SIZE + PGROUNDUP(proc->ustack); proc->base_size=proc->ustack; }
|
void proc_ustack(struct TaskControlBlock *p) { PhysPageNum ppn = kalloc(); u64 paddr = phys_addr_from_phys_page_num(ppn).value; PageTable_map(&p->pagetable,virt_addr_from_size_t(p->ustack - PAGE_SIZE),phys_addr_from_size_t(paddr), \ PAGE_SIZE, PTE_R | PTE_W | PTE_U); }
|
然后是修复一个bug
:
这个根据程序名获取应用程序的函数,这里应该是get_app_data(i+1)
。
2. 解除映射与释放内存
一个进程通过exec
系统调用来加载另外一个应用程序来进行执行,那么在这个新的应用程序开始执行的时候,原有进程的地址空间生命周期就可以结束了,里面包含的全部物理页帧都会被回收,新的应用程序需要为其分配新的页表,然后映射新的地址空间。首先同样回忆一下一个应用程序包含了哪些内存地址空间:
每个应用程序依次映射了:跳板页,trap
上下文页,用户栈页,应用程序页。销毁一个应用程序的地址空间首先就是要解除映射关系,然后将内核分配给此应用程序的物理内存释放掉。由于所有程序的跳板页都是映射到同一页物理地址,这一页是不能是不能释放物理内存的,只需要解除映射关系即可。
在address.c
中定义了一个函数用于销毁一个应用程序的地址空间:
void proc_freepagetable(PageTable* pagetable, u64 sz) { uvmunmap(pagetable, floor_virts(virt_addr_from_size_t(TRAMPOLINE)), 1, 0); uvmunmap(pagetable, floor_virts(virt_addr_from_size_t(TRAPFRAME)), 1, 1); uvmfree(pagetable, sz); }
|
此函数的参数为应用程序的根页表和应用程序的大小base_size
,此函数首先是
- 调用
uvmunmap
函数将TRAMPOLINE
页解除了映射关系,并未释放此页内存
- 调用
uvmunmap
函数将TRAPFRAME
页解除了映射关系,并且释放了此页内存
- 调用
uvmfree
函数将应用程序从0x10000
开始到base_size
之间的内存页解除了映射关系,并且释放掉了对应的物理内存。释放掉了页表所占的三页内存
首先来看uvmunmap
函数:
void uvmunmap(PageTable* pt, VirtPageNum vpn, u64 npages, int do_free) { PageTableEntry* pte; u64 a; for (a = vpn.value; a < vpn.value + npages; a++) { pte = find_pte(pt,virt_page_num_from_size_t(a)); if(pte !=0 ) { if(do_free) { u64 phyaddr = PTE2PA(pte->bits); PhysPageNum ppn = floor_phys(phys_addr_from_size_t(phyaddr)); kfree(ppn); } *pte = PageTableEntry_empty(); } } }
|
- 传入参数为应用程序的根页表,需要取消映射的虚拟地址的起始地址,需要取消映射的页数,是否释放内存的标志位
- 从
vpn
开始,通过find_pte
函数去查看此页是否被映射,如果被映射了,则取消映射即将第三级的页表项赋值为空
- 如果
do_free
成立,则去释放掉对应的物理内存
然后来看uvmfree
函数:
void uvmfree(PageTable* pt , u64 sz) { if(sz > 0) { uvmunmap(pt,floor_virts(virt_addr_from_size_t(0)),sz/PAGE_SIZE,1); } freewalk(pt->root_ppn); }
|
- 首先是调用
uvmunmap
函数,将应用程序虚拟地址从0~base_sz
之间的的映射关系取消掉,同时释放掉对应的物理内存
- 然后是调用
freewalk
函数将此地址空间页表占用的物理空间全部释放掉
从根页表开始映射,一个应用程序的映射关系如上图所示,每个页表页有512个页表项,因此需要递归搜寻去释放内存:
void freewalk(PhysPageNum ppn) { for (int i = 0; i < 512; i++) { PageTableEntry* pte = &get_pte_array(ppn)[i]; if((pte->bits & PTE_V) && (pte->bits & (PTE_R|PTE_W|PTE_X)) == 0) {
PhysPageNum child_ppn = PageTableEntry_ppn(pte); freewalk(child_ppn); *pte = PageTableEntry_empty(); } else if(pte->bits & PTE_V) { panic("freewalk: leaf"); } } kfree(ppn); }
|
由于freewalk
函数是在解除映射关系后才调用的,因此在上述的映射关系中,二级页表的所有页表项都是空的,if((pte->bits & PTE_V) && (pte->bits & (PTE_R|PTE_W|PTE_X)) == 0)
这个判断条件只在根页表和一级页表才成立,此时才会去向下搜寻下一级页表知道三级页表搜寻完毕。
3.exec系统调用的实现
void exec(const char* name) {
AppMetadata metadata = get_app_data_by_name(name); elf64_ehdr_t *ehdr = metadata.start; elf_check(ehdr); struct TaskControlBlock* proc = current_proc(); PageTable old_pagetable = proc->pagetable; u64 oldsz = proc->base_size; proc_pagetable(proc); load_segment(ehdr,proc); proc_ustack(proc);
TrapContext* cx_ptr = proc->trap_cx_ppn; cx_ptr->sepc = (u64)ehdr->e_entry; cx_ptr->sp = proc->ustack; reg_t sstatus = r_sstatus(); sstatus &= (0U << 8); w_sstatus(sstatus); cx_ptr->sstatus = sstatus; cx_ptr->kernel_satp = kernel_satp; cx_ptr->kernel_sp = proc->kstack; cx_ptr->trap_handler = (u64)trap_handler;
proc_freepagetable(&old_pagetable,oldsz); }
|
这里无需对任务上下文进行处理,因为这个进程本身已经在执行了,而只有被暂停的应用才需要在内核栈上保留一个任务上下文。
4. 系统调用后重新获取 Trap 上下文
过去的 trap_handler
实现是这样处理系统调用的:
这里的 cx
是当前应用的 Trap 上下文的指针,我们需要通过查页表找到它具体被放在哪个物理页帧上,并构造相同的虚拟地址来在内核中访问它。对于系统调用 sys_exec
来说,一旦调用它之后,我们会发现 trap_handler
原来上下文中的 cx
失效了——因为它是用来访问之前地址空间中 Trap 上下文被保存在的那个物理页帧的,而现在它已经被回收掉了。因此,为了能够处理类似的这种情况,我们在 syscall
分发函数返回之后需要重新获取 cx
,目前的实现如下:
5. 测试
在user
目录下新建一个应用程序xec.c
:
#include <timeros/types.h> #include <timeros/syscall.h> #include <timeros/string.h>
int main() { while (1) { printf("exec!\n"); } return 0; }
|
我们让write.c
通过sys_exec
系统调用来执行此程序:
#include <timeros/types.h> #include <timeros/syscall.h> #include <timeros/string.h> int main() { sys_exec("xec"); return 0; }
|
我修改了一下user
目录下的Makefile
使得通过一个make
命令就可编译所用的应用程序
CROSS_COMPILE = riscv64-unknown-elf- CFLAGS = -nostdlib -fno-builtin -mcmodel=medany
CC = ${CROSS_COMPILE}gcc OBJCOPY = ${CROSS_COMPILE}objcopy OBJDUMP = ${CROSS_COMPILE}objdump INCLUDE:=-I../include
LIB = ../lib
all: time write xec
write: write.c $(LIB)/*.c ${CC} ${CFLAGS} $(INCLUDE) -T user.ld -Wl,-Map=write.map -o bin/write $^
time: time.c $(LIB)/*.c ${CC} ${CFLAGS} $(INCLUDE) -T user.ld -Wl,-Map=time.map -o bin/time $^
xec: xec.c $(LIB)/*.c ${CC} ${CFLAGS} $(INCLUDE) -T user.ld -o bin/xec $^
debug: objdump_time objdump_write objdump_xec
objdump_time: ${OBJDUMP} -d bin/time > time.txt objdump_write: ${OBJDUMP} -d bin/write > write.txt objdump_xec: ${OBJDUMP} -d bin/xec > xec.txt
|
编译运行,现在os/user
目录执行make
命令生成应用程序,然后到quard-star
目录编译运行,结果如下:
可以看见xec
被成功执行了!!
参考链接