Lab5(2): File system, Spawn and Shell

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就艰难地完成了。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值