完结撒花,感谢陪伴(?)

Exam - 打开相对路径文件

题目背景

在 Lab5 的课下内容中,我们实现了通过绝对路径,也即相对于磁盘根目录而言的路径,打开文件,为其获取 struct Openstruct File 的函数—— open

int open(const char *path, int mode);

现在,为了拓展打开文件的方式,我们计划额外实现一个通过指定目录 + 相对路径打开指定文件的函数:openat

int openat(int dirfd, const char *path, int mode);

在这里,我们保证:

  • 目录代表的文件已经在文件管理系统中(占据 Open)与当前进程中(占据 Fd)被打开,即可以通过其 fdnum 对应的 struct Fd 获取信息
  • 调用 openopenat 的所有目录/文件均存在,且 openat 只会打开普通文件(非目录)
  • 调用 openat 打开的路径均为相对路径,即不会以 / 开头

题目要求

我们可以按照通过新建 fsipc 的种类以实现目标功能。那对于实现新 fsipc_* 的全过程,有必要进行总结

  • user/include/fsreq.h
    • 增加一个对于文件服务进程的请求类型 FSREQ_OPENAT 和请求结构体 struct Fsreq_openat完成基础的数据结构准备
  • user/lib/file.c
    • 仿照 open 函数实现 openat 函数;供用户程序直接启动 openat 流程
  • user/lib/fsipc.c
    • 仿照 fsipc_open 实现 fsipc_openat;完成对 Fsreq_openat 各个字段的赋值;使其能发送 openat 的请求
  • fs/serv.c
    • 修改 serve 函数,使其能转发 FSREQ_OPENAT 请求
    • 仿照 serve_open 实现 serve_openat 函数;实现 openat 整体的功能
  • fs/fs.c
    • 仿照 walk_path 实现 walk_path_at从文件层面实现按相对路径 path 查找文件的功能
    • 仿照 file_open 实现 file_openat ,类似地调用 walk_path_at 函数;封装功能,供文件服务进程调用
  • 头文件:
    • user/include/lib.h:增加 openatfsipc_openat 声明
    • fs/serv.h:增加 file_openat 声明

过程中仍然保留了 openat 字样,实际可以根据情况自由调整。除了 fs.c 中功能具体实现的方式不同之外,基本上面几个文件的修改都是有规律的,和 Lab4 添加系统调用类似。

一种可行的做法

因为比较繁琐,所以就把提示塞代码里了,顺序是按照刚才介绍的顺序来的

user/include/fsreq.h

没什么好说的,直接写就完了

#define FSREQ_OPENAT 8         // serve 函数分发时的标准
/*-----------------------------------------------------------*/
struct Fsreq_openat {
u_int dir_fileid; // 相对的 '根目录' 打开的文件 id
char req_path[MAXPATHLEN]; // 打开的文件的相对路径
u_int req_omode; // 打开文件的模式
};

user/lib/file.c

供用户进程调用的 openat 函数

int openat(int dirfd, const char *path, int mode) {
struct Fd *dir;
struct Fd *filefd;
int r;
/* Step 1: 获取相对目录在进程中的 fd,申请待打开的文件的 fd */
fd_lookup(dirfd, &dir);
if ((r = fd_alloc(&filefd)) != 0) { return r; }

/* Step 2: 根据目录在文件服务进程中的 fileid,发送请求 */
int dir_fileid = ((struct Filefd *)dir)->f_fileid;
if ((r = fsipc_openat(dir_fileid, path, mode, filefd)) < 0) { return r; }

char *va;
struct Filefd *ffd;
u_int size, fileid;

/* Step 3: 获取到文件的 fd,对 fd 字段进行赋值,把内容 map 到 data 区域 */
va = fd2data(filefd);
ffd = (struct Filefd *)filefd;
size = ffd->f_file.f_size;
fileid = ffd->f_fileid;

for (int i = 0; i < size; i += BY2BLK) {
if ((r = fsipc_map(fileid, i, (void *)(va + i))) != 0) { return r; }
}
/* Step 4: 返回文件的 fd 号 */
return fd2num(filefd);
}

user/lib/fsipc.c

向文件服务进程发送请求的 fsipc_* 函数:

int fsipc_openat(u_int dir_fileid, const char *path, u_int omode, struct Fd *fd) {
struct Fsreq_openat *req; // 注意这里的结构体类型为 openat
u_int perm;
req = (struct Fsreq_openat *)fsipcbuf;
if (strlen(path) >= MAXPATHLEN) {
return -E_BAD_PATH;
}

req->req_omode = omode;
req->dir_fileid = dir_fileid;
strcpy((char *)req->req_path, path);

return fsipc(FSREQ_OPENAT, req, fd, &perm);
}

fs/serv.c

类似于 serve_open 的接收请求的 serve_openat 函数,同时也别忘了改 serve 函数

void serve_openat(u_int envid, struct Fsreq_openat *rq) {
struct Open *pOpen;
struct Open *o;
struct File *f;
struct Filefd *ff;
int r;

if ((r = open_lookup(envid, rq->dir_fileid, &pOpen)) < 0) {
ipc_send(envid, r, 0, 0);
return;
}
struct File *dir = pOpen->o_file;

if ((r = file_openat(dir, rq->req_path, &f)) < 0) {
ipc_send(envid, r, 0, 0);
return;
}

if ((r = open_alloc(&o)) < 0) {
ipc_send(envid, r, 0, 0);
return;
}

o->o_file = f;

ff = (struct Filefd *)o->o_ff;
ff->f_file = *f;
ff->f_fileid = o->o_fileid;
o->o_mode = rq->req_omode;
ff->f_fd.fd_omode = o->o_mode;
ff->f_fd.fd_dev_id = devfile.dev_id;

ipc_send(envid, 0, o->o_ff, PTE_D | PTE_LIBRARY);
}

fs/fs.c

底层直接执行文件打开的函数,类似于 walk_path & file_open

int walk_path_at(struct File *par_dir, char *path, struct File **pdir,
struct File **pfile, char *lastelem) {
char *p;
char name[MAXNAMELEN];
struct File *dir, *file;
int r;

// start at the par_dir
file = par_dir;
dir = 0;
name[0] = 0;

if (pdir) {
*pdir = 0;
}

*pfile = 0;

// find the target file by name recursively.
while (*path != '\0') {
dir = file;
p = path;

while (*path != '/' && *path != '\0') { path++; }

if (path - p >= MAXNAMELEN) { return -E_BAD_PATH; }

memcpy(name, p, path - p);
name[path - p] = '\0';
path = skip_slash(path);
if (dir->f_type != FTYPE_DIR) { return -E_NOT_FOUND; }

if ((r = dir_lookup(dir, name, &file)) < 0) {
if (r == -E_NOT_FOUND && *path == '\0') {
if (pdir) { *pdir = dir; }

if (lastelem) { strcpy(lastelem, name); }

*pfile = 0;
}

return r;
}
}

if (pdir) { *pdir = dir; }

*pfile = file;
return 0;
}

int file_openat(struct File *dir, char *path, struct File **file) {
return walk_path_at(dir, path, 0, file, 0);
}

思考

这个题考察了如何添加新的文件服务请求的种类,这个信息的流动通路是文件系统中的很重要一环。实现文件的相对路径打开也是比较实用的功能,可以参考这一点实现 Lab6 挑战性任务中的支持相对路径功能

Extra - 实现符号链接文件

题目背景

在 MOS 中我们只实现了两种文件类型:常规文件 & 目录文件。现在我们需要实现一种新的文件类型:符号链接文件。这类文件会指向一个存在于文件系统中的文件(也可能是符号链接文件),当使用 open 访问这些文件时,会等效于访问了他们所指向的文件,类似于 Windows 中的快捷方式

小示例

同学们可以在 Linux 终端使用 ln -s <目标文件> <链接文件的路径> 命令来创建一个符号链接。同学们可以在开发机中依次执行下面的命令来了解符号链接的概念

$ mkdir ~/test
$ cd ~/test
$ echo 2023 > target # 创建名为 target 的文件,内容为 2023
$ ln -s target symlink # 创建一个符号链接 symlink,指向 target
$ ls -l # 可以展示当前目录下链接文件的相关信息,可以看到输出显示了 symlink -> target,这表明 symlink 指向 target
$ cat symlink # 看看命令输出了什么

继续在这个目录下创建一个名为 test_open.c 的文件,写入下面的代码。

#include <fcntl.h>
#include <unistd.h>
#include <stdio.h>

int main() {
int fd = open("symlink", 0);
char buf[8];
int len = read(fd, buf, sizeof(buf) - 1);
buf[len] = '\0';
printf("%s", buf);
return 0;
}

使用命令 gcc test_open.c && ./a.out 来编译运行该程序,可以看到输出为2023,即 target 文件的内容。

题目要求

  • 符号链接指向的文件一定是使用绝对路径表示,并且文件一定存在;
  • 符号链接文件只会指向普通文件和符号链接文件(也就是不会指向目录文件)
    • 链接不会出现环
    • 文件名长度合法

一种可行的做法

  • user/include/fs.h 中,定义新的文件类型
  • 修改 tools/fsformat.c,使得 fsformat 工具在写入文件时根据传入的各个文件的类型来决定写入磁盘镜像的文件的类型(FTYPE_LNKFTYPE_DIRFTYPE_REG)。以下是一种修改的思路:
    • main() 函数中调用的 stat() 函数改为 lstat()(参数不用改变)。stat()lstat() 的作用都是将一个文件的相关信息储存在 statbuf 结构体中。它们的区别在于:stat() 函数会跟踪链接,会解析链接最终指向的文件;而lstat() 不会跟踪链接,直接解析链接文件本身。而我们需要读取链接文件本身的信息,所以需要使用 lstat() 函数替换 stat() 函数(直接改函数名即可,这两个函数都是 Linux 的库函数)。
    • 修改 main() 函数,在 if else 结构中增加一个分支,调用 write_symlink() 函数(下面自行编写),使其不仅支持目录和普通文件的读取,还可以支持符号链接文件的读取。可以使用 S_ISLNK(stat_buf.stmode) 判断命令行参数对应的文件是否为符号链接。
    • 修改 write_directory()函数,在 if else 结构中增加一个分支,调用你编写的 write_symlink() 函数,使其不仅支持目录和普通文件的读取,还可以支持符号链接文件的读取。结构体 dirent 的成员变量 d_type 可能会用到以下取值:DT_DIR(目录)、DT_LNK(符号链接)、DT_REG(普通文件)。
    • 可以仿照 write_file() 函数编写 write_symlink() 函数,实现向磁盘镜像写入符号链接文件的功能。你可以调用 Linux 库函数 int readlink(char *pathname, char *buf, int bufsiz) 来读取一个链接文件指向的目标路径(这个函数将路径 pathname 处的符号链接指向的目标路径写入 buf 指向的缓冲区,最多写入 bufsiz 个字节,返回值是写入的字节数量。详细说明可以在开发机中使用 man 2 readlink 命令查阅)。一种可能的实现框架如下所示:
void write_symlink(struct File *dirf, const char *path) {
struct File *target = create_file(dirf);
// Your code here: 使用 readlink() 函数读取链接文件指向的路径,将其写入到下一个可用的磁盘块

const char *fname = strrchr(path, '/');
if (fname) {
fname++;
} else {
fname = path;
}
// Your code here: 设置链接文件的文件名、大小(指向路径的字符串的长度)、类型属性

save_block_link(target, 0, next_block(BLOCK_DATA));
}
  • 修改文件系统的实现,使其满足:用户程序使用 open() 函数打开一个符号链接文件的时候,实际上打开的是其最终指向的文件,返回最终指向的文件的文件描述符。被打开的文件可以正常地读写,就像直接打开了最终指向的文件一样。

看起来可能步骤很多,实际上就是:

  • 实现一个新的文件种类,文件的内容存放的是它指向的实际文件路径(字符串)
  • fsformat.c 烧录磁盘文件时需要给这个链接文件分配合适大小的空间,写入合适的内容
    • 在 Lab5-Probe 里可能写了:我们的磁盘是通过 fsformat.c 通过数组的形式进行保存的,最后再把这个数组烧录为一个 .img 文件,分配空间、些内容也都是对这个数组进行操作
  • 在 MOS 中需要对链接文件进行处理,获取它内容中(也就是指向的文件)的文件控制块并返回

user/include/fs.h

新建一个文件种类的定义

// File types

#define FTYPE_LNK 2 // Symbolic link file

user/lib/file.c

修改 open 函数打开链接文件时的处理方式

while (((struct Filefd *)fd)->f_file.f_type == FTYPE_LNK) {

va = fd2data(fd);
ffd = (struct Filefd *)fd;
size = ffd->f_file.f_size;
fileid = ffd->f_fileid;
for (int i = 0; i < size; i += BY2PG) {
if ((r = fsipc_map(fileid, i, (void *)(va + i))) != 0) {
return r;
}
}
// debugf("type = %d\n", ((struct Filefd *)fd)->f_file.f_type);
// debugf("name = %s\n", ((struct Filefd *)fd)->f_file.f_name);
// debugf("content = %s\n", (char *)va);
fd_alloc(&nfd);
fd = nfd;

fsipc_open((char *)va, mode, fd); // va 存放是文件的内容,也就是符号链接指向的路径名,直接通过 fsipc 打开
}

tools/fsformat.c

文件烧录的过程

main: 修改 stat & if-else 分支

int main(int argc, char **argv) {

for (int i = 2; i < argc; i++) {
char *name = argv[i];
struct stat stat_buf;
int r = lstat(name, &stat_buf); // stat 修改在此处
assert(r == 0);
if (S_ISDIR(stat_buf.st_mode)) {
printf("writing directory '%s' recursively into disk\n", name);
write_directory(&super.s_root, name);
} else if (S_ISREG(stat_buf.st_mode)) {
printf("writing regular file '%s' into disk\n", name);
write_file(&super.s_root, name);
} else if (S_ISLNK(stat_buf.st_mode)) { // 在这里为 LNK 类型提供支持
printf("writing symlinking file '%s' into disk\n", name);
write_symlink(&super.s_root, name);
} else {
fprintf(stderr, "'%s' has illegal file mode %o\n", name,
stat_buf.st_mode);
exit(2);
}
}

}

在所给模板的基础上填写内容

void write_symlink(struct File *dirf, const char *path) {
struct File *target = create_file(dirf);
// Your code here: 使用 readlink()
// 函数读取链接文件指向的路径,将其写入到下一个可用的磁盘块
char target_name[1025];
int name_length = readlink(path, target_name, 1024);
target_name[name_length] = '\0'; // 注意写 ''\0' 的位置,处理覆写问题
int bno = next_block(BLOCK_FILE); // 标定所使用的 blcok 的属性
strcpy((void *)disk[nextbno].data, (const void *)target_name);

const char *fname = strrchr(path, '/');
if (fname) {
fname++;
} else {
fname = path;
}

// Your code here:
// 设置链接文件的文件名、大小(指向路径的字符串的长度)、类型属性
strcpy(target->f_name, fname); // 设置文件基础信息
target->f_type = FTYPE_LNK;
target->f_size = strlen(target_name);

save_block_link(target, 0, next_block(BLOCK_DATA));
}

两处都是添加新的 if-else 分支,这就不再去写了

思考

这次 Extra 没从 MOS 的实现出发,而是从文件烧录上下了功夫,从其他的角度考察了对于文件系统逻辑的把控。但说实话感觉有点偏门()