用户态定义与接口

在之前的分析中,我们已经完成了文件管理进程中的函数实现。现在我们来分析用户态中用户直接可用的函数接口与数据结构定义

我们还是从距离文件系统最近的函数与文件开始:user/lib/fsipc.cuser/include/fsreq.h

首先明确用户进程和文件管理进程使用 IPC 通信进行交互。在文件管理进程的运行的核心、分发函数 serve 中,我们通过 ipc_recv 从用户进程获取到了一片虚拟地址 REQVA,并且针对不同的信息,将 REQVA 转换成了不同的数据结构,进行处理。这些数据结构就定义在 fsreq.h 文件中。

#define FSREQ_OPEN 1
#define FSREQ_MAP 2
#define FSREQ_SET_SIZE 3
#define FSREQ_CLOSE 4
#define FSREQ_DIRTY 5
#define FSREQ_REMOVE 6
#define FSREQ_SYNC 7

struct Fsreq_open {
char req_path[MAXPATHLEN];
u_int req_omode;
};

struct Fsreq_map {
int req_fileid;
u_int req_offset;
};
// CONTINUE...

篇幅所限不再全部排列。

那这片 va 到底是谁发送来的呢?在用户态中存在一个专门的分发函数:fsipc

fsipc.c - 文件请求服务函数

  • 我们规定在 MOS 系统中,文件管理进程必须为第二个进程(envs[1]),以保证传输正确性
  • 可以看出,本接口通过 fsreq 参数传输页面,type 参数传输申请的服务类型
// Overview:
// Send an IPC request to the file server, and wait for a reply.
static int fsipc(u_int type, void *fsreq, void *dstva, u_int *perm) {
u_int whom;
// Our file system server must be the 2nd env.
ipc_send(envs[1].env_id, type, fsreq, PTE_D);
return ipc_recv(&whom, dstva, perm);
}

在每一个用户态的每一个请求服务函数中,即 fsipc_* 函数,都会用到一个临时的页面用来传输数据:

u_char fsipcbuf[BY2PG] __attribute__((aligned(BY2PG)));

通过修改页面内的数据,搭配不同的服务函数,让文件管理进程以不同的方式去解析这片地址空间,以实现传输相同页面却能实现不同功能的效果。例如我们要实现的函数 fsipc_remove

// Overview:
// 请求文件系统移除某文件
int fsipc_remove(const char *path) {
/* Step 1: 检查文件名长度 */
/* Exercise 5.12: Your code here. (1/3) */
if (strlen(path) > MAXPATHLEN || strlen(path) == 0) { return -E_BAD_PATH; }

/* Step 2: 把预留的临时地址 **视作** Fsreq_remove 结构 */
struct Fsreq_remove *req = (struct Fsreq_remove *)fsipcbuf;

/* Step 3: 修改传输页面的数据,以实现 remove 功能 */
/* Exercise 5.12: Your code here. (2/3) */
strcpy((char *) req->req_path, path);

/* Step 4: 发送操作类型 FSREQ_REMOVE 和所需数据 req ,启动服务 */
/* Exercise 5.12: Your code here. (3/3) */
return fsipc(FSREQ_REMOVE, req, 0, 0);
}

实际上每个函数中,对 fsipc_req 这片第地址的类型转换都不同,也会写入不同的信息。

file.c - 纯文件操作函数

再向上一层,调用这些用户态中的请求服务函数 fsipc_* 的函数位于 user/lib/file.c 中。

它们是用户态中能直接执行特定文件操作的函数。

// Returns the file descriptor, 也就是 Fd 号
int open(const char *path, int mode) {
int r;

// Step 1: 申请一个 Fd
struct Fd *fd;
/* Exercise 5.9: Your code here. (1/5) */
if ((r = fd_alloc(&fd)) != 0) {
return r;
}
// Step 2: 调用服务函数 fsipc_open,同时指定 Fd 的工作模式
/* Exercise 5.9: Your code here. (2/5) */
if ((r = fsipc_open(path, mode, fd)) != 0) {
return r;
}
// Step 3: Set 'va' to the address of the page where the 'fd''s data is cached, using
// 'fd2data'. Set 'size' and 'fileid' correctly with the value in 'fd' as a 'Filefd'.
char *va;
struct Filefd *ffd;
u_int size, fileid;
/* Exercise 5.9: Your code here. (3/5) */
va = fd2data(fd);
ffd = (struct Filefd *) fd;
size = ffd->f_file.f_size;
fileid = ffd->f_fileid;
// Step 4: Alloc pages and map the file content using 'fsipc_map'.
for (int i = 0; i < size; i += BY2PG) {
/* Exercise 5.9: Your code here. (4/5) */
if ((r = fsipc_map(fileid, i, (void *) (va + i))) != 0) {
return r;
}
}

// Step 5: 返回执行函数用到的 Fd 对应的 Fd 号
/* Exercise 5.9: Your code here. (5/5) */
return fd2num(fd);
}
int remove(const char *path) {
// Your code here.
// Call fsipc_remove.

/* Exercise 5.13: Your code here. */
return fsipc_remove(path);
}

但我们在用户程序中通常不使用这些函数,具体的原因在下一个文件中说。

fsipc.c & file.c

user_lib_in_lab5

fd.c - 文件系统顶层函数

struct Dev

对于整个文件系统而言,总共能操作的设备共有三种:(纯)文件设备、控制台和管道。而我们在 file.c ,甚至 Lab5 之前部分中实现的都是对文件设备的操作,而剩余两个部分是 Lab6 的内容了。

其实本来是没有括号这个“纯”字的,但是个人觉得比较好区分“操作 fd “和”操作 file “两个层次,于是就瞎分了分

上面提到的这三类设备,都是 dev (即 file device)的一种,我们在 fd.c 的最顶层函数中也是直接对 dev 进行对应的操作,表层并无法分辨“文件”这一概念。所以应该对每个操作涉及到的不同设备类型进行分发,让每个设备对应一个执行任务的函数,再把分发函数封装在执行功能的函数内。

这三类设备平等,并且在文件系统中视角来看,都属于 Fd ,即 file descriptor 的一种(继承?)。每类设备拥有自己的功能函数以处理对应的请求,结构体定义中的 3-7 字段就是存放功能函数的指针。三种设备的信息统一存放在一个 Dev 数组内,设备具体又分别定义在各自功能函数的头文件内。

// fd.h
struct Dev {
int dev_id;
char *dev_name;
int (*dev_read)(struct Fd *, void *, u_int, u_int);
int (*dev_write)(struct Fd *, const void *, u_int, u_int);
int (*dev_close)(struct Fd *);
int (*dev_stat)(struct Fd *, struct Stat *);
int (*dev_seek)(struct Fd *, u_int);
};
static struct Dev *devtab[] = {&devfile, &devcons, &devpipe}; // 纯文件、控制台、管道

struct Fd

其次是文件描述符 Fd每一个用户进程可以同时操控多个设备,为了对这些设备加以区分,就引入了文件描述符的定义。通过访问对应设备的文件描述符,就能得知它的设备种类(dev_lookup),因此我们只需要传入文件描述符,就能自动地获得 Dev 中存放的函数指针并执行。这也许是一种面向对象的处理方式。

同时,每个进程的文件描述符上限为 32 个,每个描述符单独占用一个虚拟页,并且还对应一个 PDMAP 大小的 data 空间处理该设备打开后处理的内容。

// file descriptor
struct Fd {
u_int fd_dev_id; // 此设备对应的 Dev 类型
u_int fd_offset; // 目前的文件指针距离起始的偏移量,类似 ftell 的文件指针
u_int fd_omode; // 该设备的读写模式,只读、读写等
};

我们对文件描述符的访问,通过它在空间中的顺序(下标)实现。对外界,文件描述符相当于一个整数,服务函数 fd_lookupfd2data 等和宏定义通过“翻译”这个整数,获得文件描述符的地址和它对应的 data 空间。存放的地址如下:

fd_in_memory

为了我们更容易实现对纯文本的操作,所以又再次对 Fd 结构进行了封装,实现了一个专门为 file 使用的 FileFd 结构,它包含了更多的信息,能够更容易地处理 file 类的操作。

// file descriptor + file
struct Filefd {
struct Fd f_fd;
u_int f_fileid;
struct File f_file;
};

实际上可以直接把 FileFd 当作普通的 Fd 用,因为字段原因,所以内存上读起来是这样的效果

fd_and_filefd

我们之前提到, Open 结构体类似于一个文件服务进程使用的“窗口”,实际上文件描述符 Fd类似一个在用户态中的窗口,只不过它的范围更广,可以包含除了 File 以外的其他设备,但同时它也只为自己的进程而服务。

如何根据 Fd 执行功能函数?

  • 根据 Fd 号找到对应的 Fd 数据
  • 判断其 Dev 种类,获取执行该操作的函数指针
  • 直接调用函数指针,实现该 Dev 自行实现的底层函数,完成操作。

我们以 read 一个设备(Fd)为例,分析以上过程

// Overview:
// Read at most 'n' bytes from 'fd' at the current seek position into 'buf'.
// read(fdnum, (char *)buf + tot, n - tot);
int read(int fdnum, void *buf, u_int n) {
int r;
struct Dev *dev;
struct Fd *fd;

// Step 1: 根据 fdnum 获取 Fd 结构体和 Dev 种类
/* Exercise 5.10: Your code here. (1/4) */
if ((r = fd_lookup(fdnum, &fd)) < 0 || (r = dev_lookup(fd->fd_dev_id, &dev)) < 0) { return r; }

// Step 2: 检查 fd 的开启模式
/* Exercise 5.10: Your code here. (2/4) */
if ((fd->fd_omode & O_ACCMODE) == O_WRONLY) { return -E_INVAL; }

// Step 3: 调用该 dev 提供的 dev_read 函数
/* Exercise 5.10: Your code here. (3/4) */
r = dev->dev_read(fd, buf, n, fd->fd_offset);

// Step 4: 更新文件指针的偏移
/* Exercise 5.10: Your code here. (4/4) */
if (r > 0) { fd->fd_offset += r; }

return r;
}

大致思路如上,所以我们实际上只需要准备好这个 dev_* 函数即可完成功能。

接着以定义在 file.c 中的 devfile 为例,由于 .dev_read = file_read,所以我们之前编写的读 file 的函数 file_read 就会在顶层的 read 中被直接执行。

struct Dev devfile = {
.dev_id = 'f',
.dev_name = "file",
.dev_read = file_read,
.dev_write = file_write,
.dev_close = file_close,
.dev_stat = file_stat,
};

终于,我们在用户态直接对某个设备调用顶层的功能函数,就能直接获得响应了。

一言

纵观 Lab5,MOS 在其中为我们构造了一个通配的文件系统。

它通过统一的结构体 Fd 层次化地管理所用可用的设备。向上,在 fd.c 中为用户提供统一的操作函数;向下,不同设备通过 implements 实现 Dev 的功能函数,达到同一调用的效果。

在此之下,file.cconsole.cpipe.c 三文件实现了 Dev 中的”抽象函数“。为了优化函数实现的效果,我们建立了一个为所有进程管理纯文件 file 的文件服务进程 serv.c,并通过 fsipc.c 借助 进程间 IPC 通信机制实现数据传输。

对文件服务进程而言,它通过 IPC 通信与用户进程(请求方)通信,利用文件级、磁盘块级的交互函数 fs.c 与磁盘进行交互,并利用块缓存的机制对磁盘块进行管理,以满足请求者对指定文件的特定文件块的访问。

再细化到最后一步,所有的磁盘交互函数都是利用系统调用访问 KSEG1 段实现与外设的直接交互,并在最底层的函数中实现了以块、甚至扇区为单位的读写,即 ide.c

See Part1 at here

See Part2 at here