Lab 5: File system, Spawn and Shell

tags: mit-6.828 os


概述

本lab将实现JOS的文件系统,只要包括如下四部分:

  1. 引入一个文件系统进程(FS进程)的特殊进程,该进程提供文件操作的接口。
  2. 建立RPC机制,客户端进程向FS进程发送请求,FS进程真正执行文件操作,并将数据返回给客户端进程。
  3. 更高级的抽象,引入文件描述符。通过文件描述符这一层抽象就可以将控制台,pipe,普通文件,统统按照文件来对待。(文件描述符和pipe实现原理)
  4. 支持从磁盘加载程序并运行。

File system preliminaries

我们将要实现的文件系统会比真正的文件系统要简单,但是能满足基本的创建,读,写,删除文件的功能。但是不支持链接,符号链接,时间戳等特性。

On-Disk File System Structure

JOS的文件系统不使用inodes,所有文件的元数据都被存储在directory entry中。

文件和目录逻辑上都是由一系列数据blocks组成,这些blocks分散在磁盘中,文件系统屏蔽blocks分布的细节,提供一个可以顺序读写文件的接口。JOS文件系统允许用户读目录元数据,这就意味着用户可以扫描目录来像实现ls这种程序,UNIX没有采用这种方式的原因是,这种方式使得应用程序过度依赖目录元数据格式。

Sectors and Blocks

大部分磁盘都是以Sector为粒度进行读写,JOS中Sectors为512字节。文件系统以block为单位分配和使用磁盘。注意区别,sector size是磁盘的属性,block size是操作系统使用磁盘的粒度。JOS的文件系统的block size被定为4096字节。

Superblocks

文件系统使用一些特殊的block保存文件系统属性元数据,比如block size, disk size, 根目录位置等。这些特殊的block叫做superblocks。

我们的文件系统使用一个superblock,位于磁盘的block 1。block 0被用来保存boot loader和分区表。很多文件系统维护多个superblock,这样当一个损坏时,依然可以正常运行。

磁盘结构如下:

Super结构如下:

  1. struct Super {
  2.     uint32_t s_magic;       // Magic number: FS_MAGIC
  3.     uint32_t s_nblocks;     // Total number of blocks on disk
  4.     struct File s_root;     // Root directory node
  5. };

File Meta-data

我们的文件系统使用struct File结构描述文件,该结构包含文件名,大小,类型,保存文件内容的block号。struct File结构的f_direct数组保存前NDIRECT(10)个block号,这样对于10*4096=40KB的文件不需要额外的空间来记录内容block号。对于更大的文件我们分配一个额外的block来保存4096/4=1024 block号。所以我们的文件系统允许文件最多拥有1034个block。File结构如下:

File结构定义在inc/fs.h中:

  1. struct File {
  2.     char f_name[MAXNAMELEN];    // filename
  3.     off_t f_size;           // file size in bytes
  4.     uint32_t f_type;        // file type
  5.     // Block pointers.
  6.     // A block is allocated iff its value is != 0.
  7.     uint32_t f_direct[NDIRECT]; // direct blocks
  8.     uint32_t f_indirect;        // indirect block
  9.     // Pad out to 256 bytes; must do arithmetic in case we're compiling
  10.     // fsformat on a 64-bit machine.
  11.     uint8_t f_pad[256 - MAXNAMELEN - 8 - 4*NDIRECT - 4];
  12. } __attribute__((packed));  // required only on some 64-bit machines

Directories versus Regular Files

File结构既能代表文件也能代表目录,由type字段区分,文件系统以相同的方式管理文件和目录,只是目录文件的内容是一系列File结构,这些File结构描述了在该目录下的文件或者子目录。

超级块中包含一个File结构,代表文件系统的根目录。

The File System

Disk Access

到目前为止内核还没有访问磁盘的能力。JOS不像其他操作系统一样在内核添加磁盘驱动,然后提供系统调用。我们实现一个文件系统进程来作为磁盘驱动。

x86处理器使用EFLAGS寄存器的IOPL为来控制保护模式下代码是否能执行设备IO指令,比如in和out。我们希望文件系统进程能访问IO空间,其他进程不能。

Exercise 1

文件系统进程的type为ENV_TYPE_FS,需要修改env_create(),如果type是ENV_TYPE_FS,需要给该进程IO权限。

在env_create()中添加如下代码:

  1. if (type == ENV_TYPE_FS) {
  2. e->env_tf.tf_eflags |= FL_IOPL_MASK;
  3. }

The Block Cache

我们的文件系统最大支持3GB,文件系统进程保留从0x10000000 (DISKMAP)到0xD0000000 (DISKMAP+DISKMAX)固定3GB的内存空间作为磁盘的缓存。比如block 0被映射到虚拟地址0x10000000,block 1被映射到虚拟地址0x10001000以此类推。

如果将整个磁盘全部读到内存将非常耗时,所以我们将实现按需加载,只有当访问某个bolck对应的内存地址时出现页错误,才将该block从磁盘加载到对应的内存区域,然后重新执行内存访问指令。

Exercise 2

实现bc_pgfault()和flush_block()。

bc_pgfault()是FS进程缺页处理函数,负责将数据从磁盘读取到对应的内存。可以回顾下lab4。

  1. bc_pgfault(struct UTrapframe *utf)
  2. {
  3. void *addr = (void *) utf->utf_fault_va;
  4. uint32_t blockno = ((uint32_t)addr - DISKMAP) / BLKSIZE;
  5. int r;
  6. // Check that the fault was within the block cache region
  7. if (addr < (void*)DISKMAP || addr >= (void*)(DISKMAP + DISKSIZE))
  8. panic("page fault in FS: eip %08x, va %08x, err %04x",
  9. utf->utf_eip, addr, utf->utf_err);
  10. // Sanity check the block number.
  11. if (super && blockno >= super->s_nblocks)
  12. panic("reading non-existent block %08x\n", blockno);
  13. // Allocate a page in the disk map region, read the contents
  14. // of the block from the disk into that page.
  15. // Hint: first round addr to page boundary. fs/ide.c has code to read
  16. // the disk.
  17. //
  18. // LAB 5: you code here:
  19. addr = ROUNDDOWN(addr, PGSIZE);
  20. sys_page_alloc(0, addr, PTE_W|PTE_U|PTE_P);
  21. if ((r = ide_read(blockno * BLKSECTS, addr, BLKSECTS)) < 0)
  22. panic("ide_read: %e", r);
  23. // Clear the dirty bit for the disk block page since we just read the
  24. // block from disk
  25. if ((r = sys_page_map(0, addr, 0, addr, uvpt[PGNUM(addr)] & PTE_SYSCALL)) < 0)
  26. panic("in bc_pgfault, sys_page_map: %e", r);
  27. // Check that the block we read was allocated. (exercise for
  28. // the reader: why do we do this *after* reading the block
  29. // in?)
  30. if (bitmap && block_is_free(blockno))
  31. panic("reading free block %08x\n", blockno);
  32. }

flush_block()将一个block写入磁盘。flush_block()不需要做任何操作,如果block没有在内存或者block没有被写过。可以通过PTE的PTE_D位判断该block有没有被写过。

  1. void
  2. flush_block(void *addr)
  3. {
  4. uint32_t blockno = ((uint32_t)addr - DISKMAP) / BLKSIZE;
  5. int r;
  6. if (addr < (void*)DISKMAP || addr >= (void*)(DISKMAP + DISKSIZE))
  7. panic("flush_block of bad va %08x", addr);
  8. // LAB 5: Your code here.
  9. addr = ROUNDDOWN(addr, PGSIZE);
  10. if (!va_is_mapped(addr) || !va_is_dirty(addr)) { //如果addr还没有映射过或者该页载入到内存后还没有被写过,不用做任何事
  11. return;
  12. }
  13. if ((r = ide_write(blockno * BLKSECTS, addr, BLKSECTS)) < 0) { //写回到磁盘
  14. panic("in flush_block, ide_write(): %e", r);
  15. }
  16. if ((r = sys_page_map(0, addr, 0, addr, uvpt[PGNUM(addr)] & PTE_SYSCALL)) < 0) //清空PTE_D位
  17. panic("in bc_pgfault, sys_page_map: %e", r);
  18. }

fs/fs.c中的fs_init()将会初始化super和bitmap全局指针变量。至此对于文件系统进程只要访问虚拟内存[DISKMAP, DISKMAP+DISKMAX]范围中的地址addr,就会访问到磁盘((uint32_t)addr - DISKMAP) / BLKSIZE block中的数据。如果block数据还没复制到内存物理页,bc_pgfault()缺页处理函数会将数据从磁盘拷贝到某个物理页,并且将addr映射到该物理页。这样FS进程只需要访问虚拟地址空间[DISKMAP, DISKMAP+DISKMAX]就能访问磁盘了。

The Block Bitmap

fs_init()中已经初始化了bitmap,我们能通过bitmap访问磁盘的block 1,也就是位数组,每一位代表一个block,1表示该block未被使用,0表示已被使用。我们实现一系列管理函数来管理这个位数组。

Exercise 3

实现fs/fs.c中的alloc_block(),该函数搜索bitmap位数组,返回一个未使用的block,并将其标记为已使用。

  1. alloc_block(void)
  2. {
  3. // The bitmap consists of one or more blocks. A single bitmap block
  4. // contains the in-use bits for BLKBITSIZE blocks. There are
  5. // super->s_nblocks blocks in the disk altogether.
  6. // LAB 5: Your code here.
  7. uint32_t bmpblock_start = 2;
  8. for (uint32_t blockno = 0; blockno < super->s_nblocks; blockno++) {
  9. if (block_is_free(blockno)) { //搜索free的block
  10. bitmap[blockno / 32] &= ~(1 << (blockno % 32)); //标记为已使用
  11. flush_block(diskaddr(bmpblock_start + (blockno / 32) / NINDIRECT)); //将刚刚修改的bitmap block写到磁盘中
  12. return blockno;
  13. }
  14. }
  15. return -E_NO_DISK;
  16. }

File Operations

fs/fs.c文件提供了一系列函数用于管理File结构,扫描和管理目录文件,解析绝对路径。

基本的文件系统操作:

  1. file_block_walk(struct File *f, uint32_t filebno, uint32_t **ppdiskbno, bool alloc):查找f指向文件结构的第filebno个block的存储地址,保存到ppdiskbno中。如果f->f_indirect还没有分配,且alloc为真,那么将分配要给新的block作为该文件的f->f_indirect。类比页表管理的pgdir_walk()。
  2. file_get_block(struct File *f, uint32_t filebno, char **blk):该函数查找文件第filebno个block对应的虚拟地址addr,将其保存到blk地址处。
  3. walk_path(const char *path, struct File **pdir, struct File **pf, char *lastelem):解析路径path,填充pdir和pf地址处的File结构。比如/aa/bb/cc.c那么pdir指向代表bb目录的File结构,pf指向代表cc.c文件的File结构。又比如/aa/bb/cc.c,但是cc.c此时还不存在,那么pdir依旧指向代表bb目录的File结构,但是pf地址处应该为0,lastelem指向的字符串应该是cc.c。
  4. dir_lookup(struct File *dir, const char *name, struct File **file):该函数查找dir指向的文件内容,寻找File.name为name的File结构,并保存到file地址处。
  5. dir_alloc_file(struct File *dir, struct File **file):在dir目录文件的内容中寻找一个未被使用的File结构,将其地址保存到file的地址处。

文件操作:

  1. file_create(const char *path, struct File **pf):创建path,如果创建成功pf指向新创建的File指针。
  2. file_open(const char *path, struct File **pf):寻找path对应的File结构地址,保存到pf地址处。
  3. file_read(struct File *f, void *buf, size_t count, off_t offset):从文件f中的offset字节处读取count字节到buf处。
  4. file_write(struct File *f, const void *buf, size_t count, off_t offset):将buf处的count字节写到文件f的offset开始的位置。

Exercise 4

实现file_block_walk()和file_get_block()。

file_block_walk():

  1. static int
  2. file_block_walk(struct File *f, uint32_t filebno, uint32_t **ppdiskbno, bool alloc)
  3. {
  4. // LAB 5: Your code here.
  5. int bn;
  6. uint32_t *indirects;
  7. if (filebno >= NDIRECT + NINDIRECT)
  8. return -E_INVAL;
  9. if (filebno < NDIRECT) {
  10. *ppdiskbno = &(f->f_direct[filebno]);
  11. } else {
  12. if (f->f_indirect) {
  13. indirects = diskaddr(f->f_indirect);
  14. *ppdiskbno = &(indirects[filebno - NDIRECT]);
  15. } else {
  16. if (!alloc)
  17. return -E_NOT_FOUND;
  18. if ((bn = alloc_block()) < 0)
  19. return bn;
  20. f->f_indirect = bn;
  21. flush_block(diskaddr(bn));
  22. indirects = diskaddr(bn);
  23. *ppdiskbno = &(indirects[filebno - NDIRECT]);
  24. }
  25. }
  26. return 0;
  27. }

file_get_block():

  1. int
  2. file_get_block(struct File *f, uint32_t filebno, char **blk)
  3. {
  4. // LAB 5: Your code here.
  5. int r;
  6. uint32_t *pdiskbno;
  7. if ((r = file_block_walk(f, filebno, &pdiskbno, true)) < 0) {
  8. return r;
  9. }
  10. int bn;
  11. if (*pdiskbno == 0) { //此时*pdiskbno保存着文件f第filebno块block的索引
  12. if ((bn = alloc_block()) < 0) {
  13. return bn;
  14. }
  15. *pdiskbno = bn;
  16. flush_block(diskaddr(bn));
  17. }
  18. *blk = diskaddr(*pdiskbno);
  19. return 0;
  20. }

踩坑记录

包括后面的Exercise 10都遇到相同的问题。

写完Exercise4后执行make grade,无法通过测试,提示"file_get_block returned wrong data"。在实验目录下搜索该字符串,发现是在fs/test.c文件中,

  1. if ((r = file_open("/newmotd", &f)) < 0)
  2. panic("file_open /newmotd: %e", r);
  3. if ((r = file_get_block(f, 0, &blk)) < 0)
  4. panic("file_get_block: %e", r);
  5. if (strcmp(blk, msg) != 0)
  6. panic("file_get_block returned wrong data");

也就是说只有当blk和msg指向的字符串不一样时才会报这个错,msg定义在fs/test.c中static char *msg = "This is the NEW message of the day!\n\n"。blk指向/newmotd文件的开头。/newmotd文件在fs/newmotd中,打开后发现内容也是"This is the NEW message of the day!"。照理来说应该没有问题啊。但是通过xxd fs/newmotd指令查看文件二进制发现如下:

  1. 1. 00000000: 5468 6973 2069 7320 7468 6520 4e45 5720 This is the NEW
  2. 2. 00000010: 6d65 7373 6167 6520 6f66 2074 6865 2064 message of the d
  3. 3. 00000020: 6179 210d 0a0d 0a ay!....

最后的两个换行符是0d0a 0d0a,也就是\r\n\r\n。但是msg中末尾却是\n\n。\r\n应该是windows上的换行符,不知道为什么fs/newmotd中的换行符居然是windows上的换行符。找到问题了所在,我们用vim打开fs/newmotd,然后使用命令set ff=unix,保存退出。现在再用xxd fs/newmotd指令查看文件二进制发现,换行符已经变成了\n(0x0a)。这样就可以通过该实验了。在Exercise 10中同样需要将fs文件夹下的lorem,script,testshell.sh文件中的换行符转成UNIX下的。

The file system interface

到目前为止,文件系统进程已经能提供各种操作文件的功能了,但是其他用户进程不能直接调用这些函数。我们通过进程间函数调用(RPC)对其它进程提供文件系统服务。RPC机制原理如下:

  1. Regular env FS env
  2. +---------------+ +---------------+
  3. | read | | file_read |
  4. | (lib/fd.c) | | (fs/fs.c) |
  5. ...|.......|.......|...|.......^.......|...............
  6. | v | | | | RPC mechanism
  7. | devfile_read | | serve_read |
  8. | (lib/file.c) | | (fs/serv.c) |
  9. | | | | ^ |
  10. | v | | | |
  11. | fsipc | | serve |
  12. | (lib/file.c) | | (fs/serv.c) |
  13. | | | | ^ |
  14. | v | | | |
  15. | ipc_send | | ipc_recv |
  16. | | | | ^ |
  17. +-------|-------+ +-------|-------+
  18. | |
  19. +-------------------+

本质上RPC还是借助IPC机制实现的,普通进程通过IPC向FS进程间发送具体操作和操作数据,然后FS进程执行文件操作,最后又将结果通过IPC返回给普通进程。从上图中可以看到客户端的代码在lib/fd.c和lib/file.c两个文件中。服务端的代码在fs/fs.c和fs/serv.c两个文件中。

相关数据结构之间的关系可用下图来表示:

文件系统服务端代码在fs/serv.c中,serve()中有一个无限循环,接收IPC请求,将对应的请求分配到对应的处理函数,然后将结果通过IPC发送回去。

对于客户端来说:发送一个32位的值作为请求类型,发送一个Fsipc结构作为请求参数,该数据结构通过IPC的页共享发给FS进程,在FS进程可以通过访问fsreq(0x0ffff000)来访问客户进程发来的Fsipc结构。

对于服务端来说:FS进程返回一个32位的值作为返回码,对于FSREQ_READ和FSREQ_STAT这两种请求类型,还额外通过IPC返回一些数据。

Exercise 5

实现fs/serv.c中的serve_read()。这是服务端也就是FS进程中的函数。直接调用更底层的fs/fs.c中的函数来实现。

  1. int
  2. serve_read(envid_t envid, union Fsipc *ipc)
  3. {
  4. struct Fsreq_read *req = &ipc->read;
  5. struct Fsret_read *ret = &ipc->readRet;
  6. if (debug)
  7. cprintf("serve_read %08x %08x %08x\n", envid, req->req_fileid, req->req_n);
  8. // Lab 5: Your code here:
  9. struct OpenFile *o;
  10. int r;
  11. r = openfile_lookup(envid, req->req_fileid, &o);
  12. if (r < 0) //通过fileid找到Openfile结构
  13. return r;
  14. if ((r = file_read(o->o_file, ret->ret_buf, req->req_n, o->o_fd->fd_offset)) < 0) //调用fs.c中函数进行真正的读操作
  15. return r;
  16. o->o_fd->fd_offset += r;
  17. return r;
  18. }

Exercise 6

实现fs/serv.c中的serve_write()和lib/file.c中的devfile_write()。

serve_write():

  1. int
  2. serve_write(envid_t envid, struct Fsreq_write *req)
  3. {
  4. if (debug)
  5. cprintf("serve_write %08x %08x %08x\n", envid, req->req_fileid, req->req_n);
  6. // LAB 5: Your code here.
  7. struct OpenFile *o;
  8. int r;
  9. if ((r = openfile_lookup(envid, req->req_fileid, &o)) < 0) {
  10. return r;
  11. }
  12. int total = 0;
  13. while (1) {
  14. r = file_write(o->o_file, req->req_buf, req->req_n, o->o_fd->fd_offset);
  15. if (r < 0) return r;
  16. total += r;
  17. o->o_fd->fd_offset += r;
  18. if (req->req_n <= total)
  19. break;
  20. }
  21. return total;
  22. }

devfile_write():客户端进程函数,包装一下参数,直接调用fsipc()将参数发送给FS进程处理。

  1. static ssize_t
  2. devfile_write(struct Fd *fd, const void *buf, size_t n)
  3. {
  4. // Make an FSREQ_WRITE request to the file system server. Be
  5. // careful: fsipcbuf.write.req_buf is only so large, but
  6. // remember that write is always allowed to write *fewer*
  7. // bytes than requested.
  8. // LAB 5: Your code here
  9. int r;
  10. fsipcbuf.write.req_fileid = fd->fd_file.id;
  11. fsipcbuf.write.req_n = n;
  12. memmove(fsipcbuf.write.req_buf, buf, n);
  13. return fsipc(FSREQ_WRITE, NULL);
  14. }

库函数open()实现

以打开一个文件为例,看下整体过程,read(), write()类似。open()在linux中也要实现定义在头文件<fcntl.h>中,原型如下:

  1. int open(const char *pathname, int flags);

在JOS中open()实现在lib/file.c中,

  1. int
  2. open(const char *path, int mode)
  3. {
  4.     // Find an unused file descriptor page using fd_alloc.
  5.     // Then send a file-open request to the file server.
  6.     // Include 'path' and 'omode' in request,
  7.     // and map the returned file descriptor page
  8.     // at the appropriate fd address.
  9.     // FSREQ_OPEN returns 0 on success, < 0 on failure.
  10.     //
  11.     // (fd_alloc does not allocate a page, it just returns an
  12.     // unused fd address. Do you need to allocate a page?)
  13.     //
  14.     // Return the file descriptor index.
  15.     // If any step after fd_alloc fails, use fd_close to free the
  16.     // file descriptor.
  17.     int r;
  18.     struct Fd *fd;
  19.     if (strlen(path) >= MAXPATHLEN) //文件名不能超过指定长度
  20.         return -E_BAD_PATH;
  21.     if ((r = fd_alloc(&fd)) < 0) //搜索当前进程未被分配的文件描述符
  22.         return r;
  23.     strcpy(fsipcbuf.open.req_path, path);
  24.     fsipcbuf.open.req_omode = mode;
  25.     if ((r = fsipc(FSREQ_OPEN, fd)) < 0) { //通过fsipc()向FS进程发起RPC调用
  26.         fd_close(fd, 0);
  27.         return r;
  28.     }
  29.     return fd2num(fd);
  30. }
  31. static int
  32. fsipc(unsigned type, void *dstva)       //type, fsipcbuf是发送给fs进程的数据。dstava和fsipc()的返回值是从fs进程接收的值
  33. {
  34.     static envid_t fsenv;
  35.     if (fsenv == 0)
  36.         fsenv = ipc_find_env(ENV_TYPE_FS);
  37.     static_assert(sizeof(fsipcbuf) == PGSIZE);
  38.     ipc_send(fsenv, type, &fsipcbuf, PTE_P | PTE_W | PTE_U); //向FS进程发送数据
  39.     return ipc_recv(NULL, dstva, NULL); //接收FS进程发送回来的数据
  40. }

其中fd_alloc()定义在lib/fd.c中,

  1. int
  2. fd_alloc(struct Fd **fd_store)
  3. {
  4.     int i;
  5.     struct Fd *fd;
  6.     for (i = 0; i < MAXFD; i++) { //从当前最小的未分配描述符开始
  7.         fd = INDEX2FD(i);
  8.         if ((uvpd[PDX(fd)] & PTE_P) == 0 || (uvpt[PGNUM(fd)] & PTE_P) == 0) {
  9.             *fd_store = fd;
  10.             return 0;
  11.         }
  12.     }
  13.     *fd_store = 0;
  14.     return -E_MAX_OPEN;
  15. }

每个进程从虚拟地址0xD0000000开始,每一页对应一个Fd结构,也就是说文件描述符0对应的Fd结构地址为0xD0000000,文件描述符1对应的Fd描述符结构地址为0xD0000000+PGSIZE(被定义为4096),以此类推,。可以通过检查某个Fd结构的虚拟地址是否已经分配,来判断这个文件描述符是否被分配。如果一个文件描述符被分配了,那么该文件描述符对应的Fd结构开始的一页将被映射到和FS进程相同的物理地址处。

FS进程收到FSREQ_OPEN请求后,将调用serve_open(),该函数定义在fs/serv.c中。

  1. int
  2. serve_open(envid_t envid, struct Fsreq_open *req,
  3.      void **pg_store, int *perm_store)
  4. {
  5.     char path[MAXPATHLEN];
  6.     struct File *f;
  7.     int fileid;
  8.     int r;
  9.     struct OpenFile *o;
  10.     if (debug)
  11.         cprintf("serve_open %08x %s 0x%x\n", envid, req->req_path, req->req_omode);
  12.     // Copy in the path, making sure it's null-terminated
  13.     memmove(path, req->req_path, MAXPATHLEN);
  14.     path[MAXPATHLEN-1] = 0;
  15.     // Find an open file ID
  16.     if ((r = openfile_alloc(&o)) < 0) {                 //从opentab数组中分配一个OpenFile结构
  17.         if (debug)
  18.             cprintf("openfile_alloc failed: %e", r);
  19.         return r;
  20.     }
  21.     fileid = r;
  22.     // Open the file
  23.     if (req->req_omode & O_CREAT) {
  24.         if ((r = file_create(path, &f)) < 0) {          //根据path分配一个File结构
  25.             if (!(req->req_omode & O_EXCL) && r == -E_FILE_EXISTS)
  26.                 goto try_open;
  27.             if (debug)
  28.                 cprintf("file_create failed: %e", r);
  29.             return r;
  30.         }
  31.     } else {
  32. try_open:
  33.         if ((r = file_open(path, &f)) < 0) {
  34.             if (debug)
  35.                 cprintf("file_open failed: %e", r);
  36.             return r;
  37.         }
  38.     }
  39.     // Truncate
  40.     if (req->req_omode & O_TRUNC) {
  41.         if ((r = file_set_size(f, 0)) < 0) {
  42.             if (debug)
  43.                 cprintf("file_set_size failed: %e", r);
  44.             return r;
  45.         }
  46.     }
  47.     if ((r = file_open(path, &f)) < 0) {
  48.         if (debug)
  49.             cprintf("file_open failed: %e", r);
  50.         return r;
  51.     }
  52.     // Save the file pointer
  53.     o->o_file = f;                                      //保存File结构到OpenFile结构
  54.     // Fill out the Fd structure
  55.     o->o_fd->fd_file.id = o->o_fileid;
  56.     o->o_fd->fd_omode = req->req_omode & O_ACCMODE;
  57.     o->o_fd->fd_dev_id = devfile.dev_id;
  58.     o->o_mode = req->req_omode;
  59.     if (debug)
  60.         cprintf("sending success, page %08x\n", (uintptr_t) o->o_fd);
  61.     // Share the FD page with the caller by setting *pg_store,
  62.     // store its permission in *perm_store
  63.     *pg_store = o->o_fd;
  64.     *perm_store = PTE_P|PTE_U|PTE_W|PTE_SHARE;
  65.     return 0;
  66. }

该函数首先从opentab这个OpenFile数组中寻找一个未被使用的OpenFile结构,上图中假设找到数据第一个OpenFile结构就是未使用的。如果open()中参数mode设置了O_CREAT选项,那么会调用fs/fs.c中的file_create()根据路径创建一个新的File结构,并保存到OpenFile结构的o_file字段中。

结束后,serve()会将OpenFile结构对应的Fd起始地址发送个客户端进程,所以客户进程从open()返回后,新分配的Fd和FS进程Fd共享相同的物理页。

Spawning Processes

lib/spawn.c中的spawn()创建一个新的进程,从文件系统加载用户程序,然后启动该进程来运行这个程序。spawn()就像UNIX中的fork()后面马上跟着exec()。

spawn(const char *prog, const char **argv)做如下一系列动作:

  1. 从文件系统打开prog程序文件
  2. 调用系统调用sys_exofork()创建一个新的Env结构
  3. 调用系统调用sys_env_set_trapframe(),设置新的Env结构的Trapframe字段(该字段包含寄存器信息)。
  4. 根据ELF文件中program herder,将用户程序以Segment读入内存,并映射到指定的线性地址处。
  5. 调用系统调用sys_env_set_status()设置新的Env结构状态为ENV_RUNNABLE。

Exercise 7

实现sys_env_set_trapframe()系统调用。

  1. static int
  2. sys_env_set_trapframe(envid_t envid, struct Trapframe *tf)
  3. {
  4. // LAB 5: Your code here.
  5. // Remember to check whether the user has supplied us with a good
  6. // address!
  7. int r;
  8. struct Env *e;
  9. if ((r = envid2env(envid, &e, 1)) < 0) {
  10. return r;
  11. }
  12. tf->tf_eflags = FL_IF;
  13. tf->tf_eflags &= ~FL_IOPL_MASK; //普通进程不能有IO权限
  14. tf->tf_cs = GD_UT | 3;
  15. e->env_tf = *tf;
  16. return 0;
  17. }

Sharing library state across fork and spawn

UNIX文件描述符是一个大的概念,包含pipe,控制台I/O。在JOS中每种设备对应一个struct Dev结构,该结构包含函数指针,指向真正实现读写操作的函数。

lib/fd.c文件实现了UNIX文件描述符接口,但大部分函数都是简单对struct Dev结构指向的函数的包装。

我们希望共享文件描述符,JOS中定义PTE新的标志位PTE_SHARE,如果有个页表条目的PTE_SHARE标志位为1,那么这个PTE在fork()和spawn()中将被直接拷贝到子进程页表,从而让父进程和子进程共享相同的页映射关系,从而达到父子进程共享文件描述符的目的。

Exercise 8

修改lib/fork.c中的duppage(),使之正确处理有PTE_SHARE标志的页表条目。同时实现lib/spawn.c中的copy_shared_pages()。

  1. static int
  2. duppage(envid_t envid, unsigned pn)
  3. {
  4. int r;
  5. // LAB 4: Your code here.
  6. void *addr = (void*) (pn * PGSIZE);
  7. if (uvpt[pn] & PTE_SHARE) {
  8. sys_page_map(0, addr, envid, addr, PTE_SYSCALL); //对于标识为PTE_SHARE的页,拷贝映射关系,并且两个进程都有读写权限
  9. } else if ((uvpt[pn] & PTE_W) || (uvpt[pn] & PTE_COW)) { //对于UTOP以下的可写的或者写时拷贝的页,拷贝映射关系的同时,需要同时标记当前进程和子进程的页表项为PTE_COW
  10. if ((r = sys_page_map(0, addr, envid, addr, PTE_COW|PTE_U|PTE_P)) < 0)
  11. panic("sys_page_map:%e", r);
  12. if ((r = sys_page_map(0, addr, 0, addr, PTE_COW|PTE_U|PTE_P)) < 0)
  13. panic("sys_page_map:%e", r);
  14. } else {
  15. sys_page_map(0, addr, envid, addr, PTE_U|PTE_P); //对于只读的页,只需要拷贝映射关系即可
  16. }
  17. return 0;
  18. }

copy_shared_pages()

  1. static int
  2. copy_shared_pages(envid_t child)
  3. {
  4. // LAB 5: Your code here.
  5. uintptr_t addr;
  6. for (addr = 0; addr < UTOP; addr += PGSIZE) {
  7. if ((uvpd[PDX(addr)] & PTE_P) && (uvpt[PGNUM(addr)] & PTE_P) &&
  8. (uvpt[PGNUM(addr)] & PTE_U) && (uvpt[PGNUM(addr)] & PTE_SHARE)) {
  9. sys_page_map(0, (void*)addr, child, (void*)addr, (uvpt[PGNUM(addr)] & PTE_SYSCALL));
  10. }
  11. }
  12. return 0;
  13. }

The Shell

运行make run-icode,将会执行user/icode,user/icode又会执行inti,然后会spawn sh。然后就能运行如下指令:

  1. echo hello world | cat
  2. cat lorem |cat
  3. cat lorem |num
  4. cat lorem |num |num |num |num |num
  5. lsfd

Exercise 10

目前shell还不支持IO重定向,修改user/sh.c,增加IO该功能。

  1. runcmd(char* s) {
  2. ...
  3. if ((fd = open(t, O_RDONLY)) < 0) {
  4. cprintf("open %s for write: %e", t, fd);
  5. exit();
  6. }
  7. if (fd != 0) {
  8. dup(fd, 0);
  9. close(fd);
  10. }
  11. ...
  12. }

总结回顾

  1. 构建文件系统

    1. 引入一个文件系统进程的特殊进程,该进程提供文件操作的接口。具体实现在fs/bc.c,fs/fs.c,fs/serv.c中。
    2. 建立RPC机制,客户端进程向FS进程发送请求,FS进程真正执行文件操作。客户端进程的实现在lib/file.c,lib/fd.c中。客户端进程和FS进程交互可总结为下图:
    3. 更高级的抽象,引入文件描述符。通过文件描述符这一层抽象就可以将控制台,pipe,普通文件,统统按照文件来对待。文件描述符和pipe的原理总结如下:
  2. 支持从磁盘加载程序并运行。实现spawn(),该函数创建一个新的进程,并从磁盘加载程序运行,类似UNIX中的fork()后执行exec()。

具体代码在:https://github.com/gatsbyd/mit_6.828_jos

如有错误,欢迎指正(_):

15313676365

MIT-6.828-JOS-lab5:File system, Spawn and Shell的更多相关文章

  1. MIT6.828 La5 File system, Spawn and Shell

    Lab 5: File system, Spawn and Shell 1. File system preliminaries 在lab中我们要使用的文件系统比大多数"真实"文件 ...

  2. MIT 6.828 JOS学习笔记2. Lab 1 Part 1.2: PC bootstrap

    Lab 1 Part 1: PC bootstrap 我们继续~ PC机的物理地址空间 这一节我们将深入的探究到底PC是如何启动的.首先我们看一下通常一个PC的物理地址空间是如何布局的:        ...

  3. MIT 6.828 | JOS | 关于虚拟空间和物理空间的总结

    Question: 做lab过程中越来越迷糊,为什么一会儿虚拟地址是4G 物理地址也是4G ,那这有什么作用呢? 解决途径: 停下来,根据当前lab的进展,再回头看上学期操作系统的ppt & ...

  4. MIT 6.828 JOS学习笔记0. 写在前面的话

    0. 简介 操作系统是计算机科学中十分重要的一门基础学科,是一名计算机专业毕业生必须要具备的基础知识.但是在学习这门课时,如果仅仅把目光停留在课本上一些关于操作系统概念上的叙述,并不能对操作系统有着深 ...

  5. MIT 6.828 JOS学习笔记18. Lab 3.2 Part B: Page Faults, Breakpoints Exceptions, and System Calls

    现在你的操作系统内核已经具备一定的异常处理能力了,在这部分实验中,我们将会进一步完善它,使它能够处理不同类型的中断/异常. Handling Page Fault 缺页中断是一个非常重要的中断,因为我 ...

  6. MIT 6.828 JOS学习笔记17. Lab 3.1 Part A User Environments

    Introduction 在这个实验中,我们将实现操作系统的一些基本功能,来实现用户环境下的进程的正常运行.你将会加强JOS内核的功能,为它增添一些重要的数据结构,用来记录用户进程环境的一些信息:创建 ...

  7. MIT 6.828 JOS学习笔记7. Lab 1 Part 2.2: The Boot Loader

    Lab 1 Part 2 The Boot Loader Loading the Kernel 我们现在可以进一步的讨论一下boot loader中的C语言的部分,即boot/main.c.但是在我们 ...

  8. MIT 6.828 JOS学习笔记16. Lab 2.2

    Part 3 Kernel Address Space JOS把32位线性地址虚拟空间划分成两个部分.其中用户环境(进程运行环境)通常占据低地址的那部分,叫用户地址空间.而操作系统内核总是占据高地址的 ...

  9. MIT 6.828 JOS学习笔记15. Lab 2.1

    Lab 2: Memory Management lab2中多出来的几个文件: inc/memlayout.h kern/pmap.c kern/pmap.h kern/kclock.h kern/k ...

随机推荐

  1. maven 整合支付宝,导入alipay-sdk-java包到本地仓库

    maven 整合支付宝,导入alipay-sdk-java包到本地仓库   1.环境变量添加: MAVEN_HOME:(maven位置) M2_HOME:(maven位置) PATH:%M2_HOME ...

  2. std::bind常见的坑

    http://note.youdao.com/noteshare?id=bce9cdea8e94501186b5ba3026af685f

  3. OpenStack 计算服务 Nova计算节点部署(八)

    如果使用vmware虚拟机进行部署,需要开启虚拟化:如果是服务器需要在bios上开启. nova计算节点IP是192.168.137.12 环境准备 安装时间同步 yum install ntpdat ...

  4. [六字真言]2.嘛.异常定制和通用.md

    幻世当空 恩怨休怀 舍悟离迷 六尘不改 且怒且悲且狂哉! 最近一直在循环的一首歌! 丰富自己,比取悦他人更有力量.种下梧桐树,引得凤凰来.你若盛开,蝴蝶自来! 言归正传! 言归正传! 不要去大包大揽 ...

  5. Training (deep) Neural Networks Part: 1

    Training (deep) Neural Networks Part: 1 Nowadays training deep learning models have become extremely ...

  6. [软件]Xcode查找系统framework所在路径

    有的时候, 我们不小心改了头文件, 导致Xcode系统库被修改(改回去也不行) 假设我改的是UIKit.framework类库里面的一个文件, 那么你只需要从另一个好使的电脑上, 在这个路径找到UIK ...

  7. (32位)本体学习程序(ontoEnrich)系统使用说明文档

    系统运行:文件夹system下,可执行文件ontoEnrichment --------------------------------------------------------1.简单概念学习 ...

  8. Red Pen - 快速高效的获取设计项目的反馈

    Red Pen 让设计师能够快速,高效的从你的同事和客户获取反馈.只需要简单的拖放图像到 Red Pen 主页,然后把生成的链接分享给你的同事或者客户.他们打开链接就能看到设计稿,并给予实时的反馈,所 ...

  9. Linux 静态库与动态库

    静态库(.a) 一个deal.c  usedeal.c 重点 1. gcc -c deal.c 生成 deal.o 2. ar -rsv libdeal.a  deal.o 生成 libdeal.a ...

  10. 在一台win10上启动多个mysql

    1.因为项目需要用一个已经有数据的mysql,而我之前已经安装了一个mysql(之前的mysql上面也是有东西,不想删除)  想办法.... mysqld.exe --defaults-file=D: ...