Skip to content

浙江大学实验报告

:material-circle-edit-outline: 约 1154 个字 :fontawesome-solid-code: 122 行代码 :material-clock-time-two-outline: 预计阅读时间 5 分钟

[!ABSTRACT]

课程名称:操作系统 实验类型:综合型/设计型

实验项目名称:实验 6: VFS & FAT32 文件系统

学生姓名:俞仲炜 专业:计算机科学与技术 学号:3220104929

队友:无 专业:无 学号:无

电子邮件地址:zhongweiy@zju.edu.cn 实验日期:2024.12.27

[!CAUTION]

本人只完成了第一部分,即为用户态的 Shell 提供 readwrite syscall 的实现

本实验基于 LAB5 的框架

实验内容或步骤

准备工作

从仓库同步文件,主要是新增了文件系统相关的 fs 目录及相应的头文件,并更改了 user 目录部分文件以测试文件系统相关功能

在初始化时只创建一个用户态进程,因为是基于 LAB5 的代码,故无需任何更改

加了一个根目录下的 fs 文件夹,所以需要在 arch/riscv/Makefile 里面添加相关编译产物来进行链接:

all:
    ${MAKE} -C kernel all
    ${LD} -T kernel/vmlinux.lds kernel/*.o ../../init/*.o ../../lib/*.o ../../fs/*.o ../../user/uapp.o -o ../../vmlinux 
    $(shell test -d boot || mkdir -p boot)
    ...

[!NOTE]

根目录的 Makefile 也需要修改,以在编译内核前编译 fs 模块,注意要在编译内核前

Shell: 与内核进行交互

文件系统抽象

修改 proc.h,为进程 task_struct 结构体添加一个指向文件表的指针:

struct task_struct {
    ...
    struct mm_struct mm;
    struct files_struct *files; // ADD
};

stdout/err/in 初始化

在 proc.c 中的 task_init 函数中为每个进程调用 file_init,初始化每个进程的文件表:

void task_init() {
    ...
    for(int i = 1; i < nr_tasks; i++){
        ...
        /* LAB 6 */
        task[i]->files = file_init();
    }
    ...
}

初始化的文件表中至少包含 stdin、stdout 与 stderr 三个标准输入输出流,所以我们需要在初始化文件表时预先初始化为这三项,根据其含义分别进行相应的赋值,且保证其余项的 opened 字段初始值为 0:

struct files_struct *file_init() {
    /* LAB6 4.2.2 */
    // todo: alloc pages for files_struct, and initialize stdin, stdout, stderr
    struct files_struct *ret = (struct fs_struct *)alloc_page();
    memset(ret, 0, PGSIZE);

    // stdin
    ret->fd_array[0].opened = 1;
    ret->fd_array[0].perms = FILE_READABLE;
    ret->fd_array[0].cfo = 0;
    ret->fd_array[0].lseek = NULL;
    ret->fd_array[0].write = NULL;
    ret->fd_array[0].read = stdin_read;

    // stdout
    ret->fd_array[1].opened = 1;
    ret->fd_array[1].perms = FILE_WRITABLE;
    ret->fd_array[1].cfo = 0;
    ret->fd_array[1].lseek = NULL;
    ret->fd_array[1].write = stdout_write;
    ret->fd_array[1].read = NULL;

    // stderr
    ret->fd_array[2].opened = 1;
    ret->fd_array[2].perms = FILE_WRITABLE;
    ret->fd_array[2].cfo = 0;
    ret->fd_array[2].lseek = NULL;
    ret->fd_array[2].write = stderr_write;
    ret->fd_array[2].read = NULL;

    return ret;
}

以 stdin 为例:

  1. perms 表示文件的权限,设置 FILE_READABLE 来允许进程访问 stdin
  2. seek 指向文件定位函数,初始化为 NULL
  3. write 指向写函数
  4. read 指向读函数

处理 stdout/err 的写入

捕获 write 的 syscall,然后查找对应的 fd,并通过对应的 write 函数调用来进行输出, sys_write 主要检查 write 请求是否有效,有效则调用文件的写函数:

int64_t sys_write(uint64_t fd, const char *buf, uint64_t len) {
    // int64_t ret;
    struct file *file = &(current->files->fd_array[fd]);
    if (file->opened == 0) {
        printk("[FS] file %d not opened\n", fd);
        return ERROR_FILE_NOT_OPEN;
    } else {
        // check perms and call write function of file
        if(!(file->perms & FILE_WRITABLE) || file->write == NULL) {
            printk("[FS] file %d not writable\n", fd);
            return ERROR_FILE_NOT_OPEN;
        }
    }
    return file->write(file, buf, len);
}

stdout_write 在实验框架中已给出,待实现的是 stderr_write,两者功能上等价,所以直接参考 stdout_write 即可:

int64_t stderr_write(struct file *file, const void *buf, uint64_t len) {
    // todo
    char to_print[len + 1];
    for (int i = 0; i < len; i++) {
        to_print[i] = ((const char *)buf)[i];
    }
    to_print[len] = 0;
    return printk(buf);   
}

然后可检查 write 系统调用是否功能正常:

[!NOTE]

sbi_debug_console_read 尚未实现,需要先注释掉 uart_getchar 函数

image-20241227134346320

可以看到输出了两个 hello ... 说明两者功能正常

处理 stdin 的读取

stdin 是从键盘获得输 ⼊,需要借助 sbi,先在 arch/riscv/include/sbi.h 中添加函数声明:

struct sbiret sbi_debug_console_read(uint64_t num_bytes, uint64_t base_addr_lo, uint64_t base_addr_hi);

并在 sbi.c 中进行实现(eid 和 console_write_byte 一样为 0x4442434e,fid 为 1)

其中参数 num_bytes 为读取的字节数,base_addr_lobase_addr_hi 为写入的目的地址(base_addr_hi 在 64 位架构中不会用到):

struct sbiret sbi_debug_console_read(uint64_t num_bytes, uint64_t base_addr_lo, uint64_t base_addr_hi){
    return sbi_ecall(0x4442434e, 1, num_bytes, base_addr_lo, base_addr_hi, 0, 0, 0);
}

在 vfs.c 中已实现读取单个字符的函数 uart_getchar()

因为 sbi_debug_console_read 是非阻塞的,我们需要另一个函数来不断进行读取,直到读到了有效字符,然后在 stdin_read 中只需要这样读取 len 个字符就好了:

int64_t stdin_read(struct file *file, void *buf, uint64_t len) {
    // todo: use uart_getchar() to get `len` chars
    for(int i = 0; i < len; i++) ((char *)buf)[i] = uart_getchar();
    return len;
}

然后在 syscall.h 增加系统调用号 63:

#define SYS_READ    63

trap_handler 中进行该系统调用的捕获:

void trap_handler(uint64_t scause, uint64_t sepc, struct pt_regs *regs) {
    ...
    if(exc == 8){
        ...
        if(regs->reg[16] == SYS_READ) {
            regs->reg[9] = sys_read(regs->reg[9], (const char*)regs->reg[10], regs->reg[11]);
            regs->sepc += 4;
        } 
        ...
    }
    ...
}

实现 read 的系统调用,与 write 相似:

int64_t sys_read(uint64_t fd, const char *buf, uint64_t len) {
    struct file *file = &(current->files->fd_array[fd]);
    if(file->opened == 0) {
        printk("[*] file %d not opened\n", fd);
        return ERROR_FILE_NOT_OPEN;
    } else {
        if(!(file->perms & FILE_READABLE) || file->read == NULL) {
            printk("[*] file %d not readable\n", fd);
            return ERROR_FILE_NOT_OPEN;
        }
    }

    return file->read(file, buf, len);
}

然后可检查 read 系统调用是否功能正常:

image-20241227140154854

FAT32:持久存储

没做这部分

心得体会

很震惊这么简单的前半部分会给 60% 的分数,老师和助教真的尽可能捞大家了(哭死)

因为只写了前半部分,十分顺利地完成了,甚至没有进行调试就能跑了(哭死)

实验文档里提到了 sbi_ecall 有可能引发问题,查阅了 GCC 关于内联汇编的文档,发现只需要在在 Clobber 部分添加需要使用的寄存器即可

LAB1 实验文档提供的示例有写这部分,我这部分当时已写上,故本次实验没有遇到这里引发的问题:

struct sbiret sbi_ecall(...) 
{
    ...
    asm volatile (
        ...
        : [error] "=r" (error), [value] "=r" (value)
        : [ext] "r" (ext), [fid] "r" (fid), [arg0] "r" (arg0), [arg1] "r" (arg1), [arg2] "r" (arg2), [arg3] "r" (arg3), [arg4] "r" (arg4), [arg5] "r" (arg5)
        : "memory", "a0", "a1", "a2", "a3", "a4", "a5", "a6", "a7"
    );
    ...
}

终于结束了 😭 不用再写操作系统了,写得最累的一套实验

不过这一套实验做下来确实感觉能力提升了很多,对 C 编程和操作系统底层逻辑的理解直接上升了一个档次(或者说因为之前太菜了 hhhh)

以及得感谢两位助教,线上问的问题都会很快地得到回应,最后都能得到解决,感觉专业课的体验和助教关系太大了,庆幸自己很幸运

2024 ZJU Operating System 拜拜啦