Spawning Process
我们已经给出了spawn的代码(可见lib/spawn.c)。它创建了一个新环境,从文件系统加载一个程序映像到其中,然后启动运行这个程序的子环境。父环境然后独立于子进程运行。spawn函数实际上就像一个fork,在子进程中马上执行exec.
我们实现spawn而不是一个UNIX-style的exec,是因为spawn更容易从用户空间以“exokernel fashion(外内核方式?)”实现,不需要内核的特殊帮助。为什么在用户空间实现exec更难?
Exercise 7. spawn依赖于新的系统调用sys_env_set_trapframe来初始化新创建环境的状态。在kern/syscall.c中实现sys_env_set_trapframe(不要忘记在syscall()中添加新的系统调用dispatch)。
通过运行在kern/init.c中的user/spawnhello程序来测试代码,它将尝试从文件系统中派生出/hello.
使用make grade测试代码。
粗略阅读了一下spawn.c的代码,大概理解了spawn的作用和工作机制:
spawn它首先创建了一个子环境,然后从磁盘中加载了一个程序代码映像,并在子环境中运行加载,这类似于Unix的fork+exec。不同的是,spawn运行在用户空间。
spawn的流程如下:
1. 打开文件,获取文件描述符Fd。
2. 读取ELF头部,检查ELF文件魔数。
3. 调用sys_exofork()创建一个子环境。
4. 设置child_tf, eip为ELF文件的入口entry, esp为init_stack分配的栈空间。
5. 将ELF文件映射到子环境的地址空间,并根据ELF的读写段来设置读写权限。
6. 拷贝共享页。
7. 调用sys_env_set_trapframe()设置子环境的env_tf位child_tf。
8. 调用sys_env_set_status()设置子进程为RUNNABLE状态。
保存当前环境的trapframe。
static int
sys_env_set_trapframe(envid_t envid, struct Trapframe *tf)
{
// LAB 5: Your code here.
// Remember to check whether the user has supplied us with a good
// address!
//panic("sys_env_set_trapframe not implemented");
struct Env *e;
int r;
if((r=envid2env(envid,&e,true))<0)
return r;
user_mem_assert(e,tf,sizeof(struct Trapframe),PTE_U);
/* 之前看大家都这么写的,但是make grade不对,重看了注释,发现需要更改的是tf
e->env_tf = *tf;
//环境运行在用户模式下
e->env_tf.tf_cs |= 3;
//中断使能
e->env_tf.tf_eflags |= FL_IF;
*/
tf->tf_eflags |= FL_IF;
tf->tf_eflags &= ~FL_IOPL_MASK;
tf->tf_cs |= 3;
e->env_tf = *tf;
return 0;
}
记得在syscall()中添加dispatch。
Sharing library state across fork and spawn
UNIX文件描述符是一种通用概念,它还包含pipe, 控制I/O等等。在JOS中,每种设备类型都有一个对应的struct Dev, 带有指向实现该设备类型读/写等功能的指针。lib/fd.c在struct Dev之上实现了一般的类UNIX文件描述符接口。每个struct Fd表示其设备类型,并且lib/fd.c的大多数函数只是将操作分发到合适的struct Dev中的函数。
lib/fd.c还在每个应用程序环境的地址空间中维护文件描述符表区域,开始于FSTABLE。这个区域为应用程序可以立即打开的每个MAXFD(目前为32个)文件描述符保留一个页面值(4KB)的地址空间。在任何给定时间,当且仅当使用相应的文件描述符时,才映射特定的文件描述符页。每个文件描述符在从FILEDATA开始的区域中也有一个可选的“data page”,如果设备选择的话可以使用它。
我们希望跨fork和spawn共享文件描述符状态,但文件描述符状态保存在用户空间内存中。现在,在fork上,内存将被标记为写时复制(copy on write),因此状态将被复制而不是共享(这意味着环境将不能在他们自己没有打开的文件中查找,pipe将不能通过fork工作。在spawn上,内存将被留下,根本不复制。(实际上,生成的环境在开始时没有打开文件描述符。)
我们将更改fork以了解特定的内存区域是由“库操作系统”使用的,并且应该始终被共享。与在某处硬编码区域列表不同,我们将在页表项中设置一个其他未使用的位(就像我们在fork中对PTE_COW所做的那样)
我们在inc/lib.h中定义了一个新的PTE_SHARE位,该位是三个PTE位之一,在Intel和AMD手册中被标记为“可用于软件使用”。我们将建立这样的约定:如果页表项设置了这个位,那么PTE应该在fork和spawn直接从父目录复制到子目录,注意,这不同于将其标记为“即写即复制”:正如第一段所述,我们确保共享页面的更新。
Exercise 8. 更改lib/fork.c中的duppage以遵循新的约定,如果页表项设置了PTE_SHARE位,那么直接复制映射即可。(应该使用PTE_SYSCALL而不是0xfff来屏蔽页表项中的相关位。0xfff也会拾取被访问的和脏位。)
同样,在lib/spawn.c中实现copy_shared_pages. 它应该循环遍历当前进程中所有页表项(就像fork所做的那样), 将设置了PTE_SHARE位的任何页映射复制到子进程中。
//lib/fork.c:duppage()
void *va = (void *)(pn*PGSIZE);
if(uvpt[pn] & PTE_SHARE){
if((r=sys_page_map(parent_envid,va,envid,va,uvpt[pn]&PTE_SYSCALL))<0)
return r;
}else{
static int
copy_shared_pages(envid_t child)
{
// LAB 5: Your code here.
uintptr_t addr;
int r;
//仿照fork,遍历所有页面
for(addr=(uintptr_t)UTEXT; addr<USTACKTOP;addr+=PGSIZE){
if((uvpd[PDX(addr)]&PTE_P) && (uvpt[PGNUM(addr)]&PTE_P)){
if(uvpt[PGNUM(addr)] & PTE_SHARE){
if((r=sys_page_map(thisenv->env_id,(void *)addr,child,(void *)addr, uvpt[PGNUM(addr)]&PTE_SYSCALL))<0)
return r;
}
}
}
return 0;
}
实验结果
The keyboard interface
要让shell工作,我们需要一种方法来键入它。QEMU一直在显示我们写入到CGA显示器和串行端口的输出,但到目前为止,我们只在内核监视器中接受输入。在QEMU中,在图形化窗口中键入的输入显示为从键盘到JOS的输入,而在控制台中键入的输入显示为串行端口上的字符。kern/console.c已经包含了自lab1以来内核监视器一直使用的键盘和串行驱动程序,但是现在需要将它们附加到系统的其他部分。
Exercise 9. 在kern/trap.c中,调用kbd_intr处理trap IRQ_OFFSET+IRQ_KBD, 调用serial_intr处理IRQ_OFFSET+IRQ_SERIAL.
我们在lib/console.c中实现控制台输入/输出文件类型。kbd_intr和serial_intr用最近读取的输入填充缓冲区,而控制台文件类型耗尽缓冲区 (控制台文件类型默认用于stdin/stdout,除非用户重定向它们)
通过运行make run-testkbd并键入几行代码来测试。系统应该在您写完行之后将您的输入返回给您。如果有可用的控制台和图形窗口,请同时在这两个窗口中输入。
if(tf->tf_trapno == IRQ_OFFSET + IRQ_KBD){
kbd_intr();
return;
}
if(tf->tf_trapno == IRQ_OFFSET + IRQ_SERIAL){
serial_intr();
return;
}
结果
The Shell
运行make run-icode or make run-icode-nox。这将运行内核并启动user/icode。icode执行init,它将把控制台设置为文件描述符0和1(标准输入和标准输出)。然后它会spawn sh,也就是shell。你应该能运行以下命令:
echo hello world | cat
cat lorem |cat
cat lorem |num
cat lorem |num |num |num |num |num
lsfd
Exercise 10. shell不支持I/O重定向。运行sh <script会很好,而不必像上面那样手动在脚本中输入所有命令。将<的I/O重定向添加到user/sh.c.
运行make run-testshell来测试shell.test只是将上面的命令(可以在fs/testshell.sh中找到)提供给shell,然后检查输出是否匹配fs/testshell.key.
if((fd=open(t,O_RDONLY))<0){
cprintf("open file failed\n");
exit();
}
if(fd!=0){
dup(fd,0);
close(fd);
}
break;
这里的代码很简单,但是没想到我却在这里遇到一个惊天大bug! 排查了很久很久,发现是在kern/pmap.c中的page_remove()错了,修改成了这样。诶,虽然知道可能是PTE_X相关错误,但没想到是在这里呀,至于为什么需要加这个判断条件,等回头再看吧,先占个坑。
void
page_remove(pde_t *pgdir, void *va)
{
// Fill this function in
pte_t *pte;
struct PageInfo* pageinfo = page_lookup(pgdir,va,&pte);
//原来没有*pte & PTE_P 判断条件
if(pageinfo && (*pte &PTE_P)){
page_decref(pageinfo);
*pte = 0;
tlb_invalidate(pgdir,va);
}
}
到此,lab5就艰难地完成了。