MIT S6.828 Lab5(实验记录五)

157 阅读6分钟

持续创作,加速成长!这是我参与「掘金日新计划 · 10 月更文挑战」的第29天,点击查看活动详情 

前言

接着上篇文章,上篇文章中讲了文件操作,以及相关的的function实现,这篇文章来接着讲讲文件系统接口,来完善一下知识体系。注:内容相对枯燥,就是我个人学习过程中的技术笔记与总结思考

文件系统接口

现在,我们已经在文件系统环境本身中拥有了必要的功能,我们需要使希望使用文件系统的其他环境能够访问它。由于其他环境不能直接调用文件系统环境中的函数,因此我们将通过基于JOS的IPC机制构建的远程过程调用或RPC抽象来公开对文件系统环境的访问。如下图,是对文件系统服务器的调用(以read为例):

      Regular env           FS env
   +---------------+   +---------------+
   |      read     |   |   file_read   |
   |   (lib/fd.c)  |   |   (fs/fs.c)   |
...|.......|.......|...|.......^.......|...............
   |       v       |   |       |       | RPC mechanism
   |  devfile_read |   |  serve_read   |
   |  (lib/file.c) |   |  (fs/serv.c)  |
   |       |       |   |       ^       |
   |       v       |   |       |       |
   |     fsipc     |   |     serve     |
   |  (lib/file.c) |   |  (fs/serv.c)  |
   |       |       |   |       ^       |
   |       v       |   |       |       |
   |   ipc_send    |   |   ipc_recv    |
   |       |       |   |       ^       |
   +-------|-------+   +-------|-------+
           |                   |
           +-------------------+

虚线以下的所有内容只是将读取请求从常规环境获取到文件系统环境的机制。在开始时,read()(我们提供)工作于任何文件描述符,并且只是调度到适当的设备读取功能,在这个给例子中会调度到devfile_read()(我们可以有更多的设备类型,如:管道)。 devfile_read() 实现专门用于磁盘上文件的read()。它和lib/file.c中其他的devfile_* 函数一起实现FS操作的客户端,并且都以大致相同的方式工作,将参数捆绑在请求结构中,调用fsipc() 以发送IPC请求,并解压缩并返回结果。fsipc() 函数仅处理向服务器发送请求和接收回复的常见详细信息。

文件系统服务器代码可以在 fs/serv.c 中找到。它在serve() 函数中循环,通过IPC无休止地接收请求,将请求分派给相应的处理程序函数,并通过IPC发送回结果。在read() 读取示例中,serve()将调度到serve_read() ,它将负责特定于读取请求的 IPC 详细信息,例如解压缩请求结构并最终调用file_read() 以实际执行文件读取。

回想一下,JOS 的 IPC 机制允许环境发送一个 32 位数字,并选择性地共享页面。要将请求从客户端发送到服务器,我们使用 32 位数字作为请求类型(文件系统服务器 的RPC编号,就像 syscalls 的编号一样),并将请求的参数存储在通过 IPC 共享的页面上的 union Fsipc中。在客户端,我们总是在fsipcbuf 处共享页面;在服务器端,我们将传入的请求页面映射到 fsreq(0x0ffff000)。

服务器还通过 IPC 将响应发送回去。我们使用 32 位数字作为函数的返回代码。对于大多数 RPC 来说,这就是他们返回的全部内容。FSREQ_READ 和FSREQ_STAT 也会返回数据,它们只是将其写入客户端发送其请求的页面。无需在响应 IPC 中发送此页面,因为客户端首先与文件系统服务器共享此页面。此外,在其响应中,FSREQ_OPEN与客户共享一个新的“Fd页面”。我们稍后将返回到文件描述符页面。

Exercise 5. Implement serve_read() in fs/serv.c.

serve_read()'s heavy lifting will be done by the already-implemented file_read() in fs/fs.c (which, in turn, is just a bunch of calls to file_get_block() ). serve_read() just has to provide the RPC interface for file reading. Look at the comments and code in serve_set_size() to get a general idea of how the server functions should be structured.

在完成这个Exercise之前,首先看一些在serve.c中的重要的信息:

首先是Fsipc结构体:它规定的是客户端和服务端通信的数据格式,里面的每一个结构体成员对应的是一种文件系统的操作。每次从客户端发来的请求都会把参数放入一个union Fsipc映射的物理页到服务端(req过程),然后服务端再把处理之后的结果放入union Fsipc中返回给客户端(ret过程)。

union Fsipc {
        struct Fsreq_open {
                char req_path[MAXPATHLEN];
                int req_omode;
        } open;
        struct Fsreq_set_size {
                int req_fileid;
                off_t req_size;
        } set_size;
        struct Fsreq_read {
                int req_fileid;
                size_t req_n;
        } read;
        struct Fsret_read {
                char ret_buf[PGSIZE];
        } readRet;
        struct Fsreq_write {
                int req_fileid;
                size_t req_n;
                char req_buf[PGSIZE - (sizeof(int) + sizeof(size_t))];
        } write;
        struct Fsreq_stat {
                int req_fileid;
        } stat;
        struct Fsret_stat {
                char ret_name[MAXNAMELEN];
                off_t ret_size;
                int ret_isdir;
        } statRet;
        struct Fsreq_flush {
                int req_fileid;
        } flush;
        struct Fsreq_remove {
                char req_path[MAXPATHLEN];
        } remove;

        // Ensure Fsipc is one page
        char _pad[PGSIZE];
};

之后是OpenFile结构体:维护了服务端的一个映射,把真实文件的File结构体和客户端打开的Fd结构体建立了一个映射关系。每个文件对应的Fd都会被映射到FILEVA之上的一个物理页中,服务端和打开这个文件的客户端共享这个物理页。在客户端和服务端通信时使用o_fileid来指定共享的文件。这里的映射上限就是MAXOPEN个物理页。

struct OpenFile {
	uint32_t o_fileid;	// file id
	struct File *o_file;	// mapped descriptor for open file
	int o_mode;		// open mode
	struct Fd *o_fd;	// Fd page
};

File结构体不多赘述,Fd结构体是一个服务端的抽象层,一个OpenFile中会建立一组映射关系,用来抽象文件并记录文件的信息。

struct Fd {
        int fd_dev_id;
        off_t fd_offset;
        int fd_omode;
        union {
                // File server files
                struct FdFile fd_file;
        };
};

再补充一个客户端的设备处理的结构体Dev,注册了设备的id和名字以及一系列处理函数

struct Dev {
	int dev_id;
	const char *dev_name;
	ssize_t (*dev_read)(struct Fd *fd, void *buf, size_t len);
	ssize_t (*dev_write)(struct Fd *fd, const void *buf, size_t len);
	int (*dev_close)(struct Fd *fd);
	int (*dev_stat)(struct Fd *fd, struct Stat *stat);
	int (*dev_trunc)(struct Fd *fd, off_t length);
};

最后,我们再梳理一下服务端的操作流程:

  1. 通过IPC从客户端接受一个请求req和一个数据页fsreq
  2. 根据req去执行对应的服务端的请求处理函数
  3. 将处理函数的执行结果再通过IPC返回给客户端的调用者
  4. 取消fsreq的映射关系

总结这套流程核心就是请求与返回,客户端发送请求给服务端处理,处理之后再把结果返回给客户端。其中通信实现的核心机制就是IPC,这里更多地我们要注意服务端的处理细节,serve.c中为我们提供了请求-处理的注册表如下:

typedef int (*fshandler)(envid_t envid, union Fsipc *req);

fshandler handlers[] = {
	// Open is handled specially because it passes pages
	/* [FSREQ_OPEN] =	(fshandler)serve_open, */
	[FSREQ_READ] =		serve_read,
	[FSREQ_STAT] =		serve_stat,
	[FSREQ_FLUSH] =		(fshandler)serve_flush,
	[FSREQ_WRITE] =		(fshandler)serve_write,
	[FSREQ_SET_SIZE] =	(fshandler)serve_set_size,
	[FSREQ_SYNC] =		serve_sync
};

// Definitions for requests from clients to file system
enum {
	FSREQ_OPEN = 1,
	FSREQ_SET_SIZE,
	// Read returns a Fsret_read on the request page
	FSREQ_READ,
	FSREQ_WRITE,
	// Stat returns a Fsret_stat on the request page
	FSREQ_STAT,
	FSREQ_FLUSH,
	FSREQ_REMOVE,
	FSREQ_SYNC
};

即通过请求号来调用注册表中的对应函数,那么对我们需要的读操作,接下来就来实现其对应的处理函数。

int
serve_read(envid_t envid, union Fsipc *ipc)
{
	struct Fsreq_read *req = &ipc->read;
	struct Fsret_read *ret = &ipc->readRet;

	if (debug)
		cprintf("serve_read %08x %08x %08x\n", envid, req->req_fileid, req->req_n);

	// Lab 5: Your code here:
	struct OpenFile *o;
	int r;
	if ((r = openfile_lookup(envid, req->req_fileid, &o)) < 0)
		return r;
	if ((r = file_read(o->o_file, ret->ret_buf, req->req_n, o->o_fd->fd_offset)) < 0)
		return r;
	o->o_fd->fd_offset += r;
    
	return r;
}

根据Exercise的提示,我们可以了解到其执行过程为:先从Fsipc中获取到读请求和读返回的结构体,然后在Openfile中查找到fileid对应的结构体信息,之后从openfile的o_file中读取内容,并把读到的内容保存在读返回的ret_buf中,之后改变文件偏移量。