浙江大学实验报告
[!ABSTRACT]
课程名称:操作系统 实验类型:综合型/设计型
实验项目名称:实验 6: VFS & FAT32 文件系统
学生姓名:俞仲炜 专业:计算机科学与技术 学号:3220104929
队友:无 专业:无 学号:无
电子邮件地址:zhongweiy@zju.edu.cn 实验日期:2024.12.27
[!CAUTION]
本人只完成了第一部分,即为用户态的 Shell 提供
read
和write
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 结构体添加一个指向文件表的指针:
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 为例:
perms
表示文件的权限,设置FILE_READABLE
来允许进程访问 stdinseek
指向文件定位函数,初始化为 NULLwrite
指向写函数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
函数
可以看到输出了两个 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_lo
和 base_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:
在 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
系统调用是否功能正常:
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 拜拜啦