NJU计算机课程基础实验 PA3笔记(二) 2022-08-28
简易文件系统 SDL库的完善——仙剑奇侠传
4、简易文件系统
对用户程序来说, 它怎么知道文件位于ramdisk的哪一个位置呢? 更何况文件会动态地增删, 用户程序并不知情. 这说明, 把ramdisk的读写接口直接提供给用户程序来使用是不可行的. 操作系统还需要在存储介质的驱动程序之上为用户程序提供一种更高级的抽象, 那就是文件.
定义一个简易文件系统sfs(Simple File System):
每个文件的大小是固定的
写文件时不允许超过原有文件的大小
文件的数量是固定的, 不能创建新文件
没有目录(/a/b/c)
既然文件的数量和大小都是固定的, 我们自然可以把每一个文件分别固定在ramdisk中的某一个位置.
为了记录ramdisk中各个文件的名字和大小, 我们还需要一张"文件记录表
nanos-lite/Makefile修改后然后运行
make ARCH=riscv32-nemu update
就会自动编译Navy中的程序, 并把
navy-apps/fsimg/
目录下的所有内容整合成ramdisk镜像navy-apps/build/ramdisk.img
, 同时生成这个ramdisk镜像的文件记录表navy-apps/build/ramdisk.h
, Nanos-lite的Makefile
会通过软连接把它们链接到项目中.如果你修改了Navy中的内容, 请记得通过上述命令来更新镜像文件.
"文件记录表"其实是一个数组, 数组的每个元素都是一个结构体:
typedef struct {
char *name; // 文件名
size_t size; // 文件大小
size_t disk_offset; // 文件在ramdisk中的偏移
} Finfo;
最基本的文件读写操作:
size_t read(const char *filename, void *buf, size_t len);
size_t write(const char *filename, void *buf, size_t len);
操作系统中存在不少"没有名字"的文件(比如管道传输给另一个工具的标准输入,less等). 为了统一管理它们, **通过一个编号来表示文件, 文件描述符(file descriptor). **
一个文件描述符对应一个正在打开的文件, 由操作系统来维护文件描述符到具体文件的映射. 于是我们很自然地通过
open()
系统调用来打开一个文件, 并返回相应的文件描述符int open(const char *pathname, int flags, int mode);
由于sfs的文件数目是固定的, 我们可以简单地把文件记录表的下标作为相应文件的文件描述符返回给用户程序. 在这以后, 所有文件操作都通过文件描述符来标识文件:
size_t read(int fd, void *buf, size_t len);
size_t write(int fd, const void *buf, size_t len);
int close(int fd);
我们为每一个已经打开的文件引入偏移量属性
open_offset
, 来记录目前文件操作的位置. 每次对文件读写了多少个字节, 偏移量就前进多少.偏移量可以通过
lseek()
系统调用来调整, 从而可以对文件中的任意位置进行读写:size_t lseek(int fd, size_t offset, int whence);
为了方便用户程序进行标准输入输出, 操作系统准备了三个默认的文件描述符:
#define FD_STDIN 0
#define FD_STDOUT 1
#define FD_STDERR 2
它们分别对应标准输入
stdin
, 标准输出stdout
和标准错误stderr
. 我们经常使用的printf, 最终会调用write(FD_STDOUT, buf, len)
进行输出;而scanf将会通过调用
read(FD_STDIN, buf, len)
进行读入.
nanos-lite/src/fs.c
中定义的file_table
会包含nanos-lite/src/files.h
, 其中前面还有3个特殊的文件:stdin
,stdout
和stderr
的占位表项, 它们只是为了保证sfs和约定的标准输入输出的文件描述符保持一致, 例如根据约定stdout
的文件描述符是1
, 而我们添加了三个占位表项之后, 文件记录表中的1
号下标也就不会分配给其它的普通文件了.根据以上信息, 我们就可以在文件系统中实现以下的文件操作了:
int fs_open(const char *pathname, int flags, int mode);
size_t fs_read(int fd, void *buf, size_t len);
size_t fs_write(int fd, const void *buf, size_t len);
size_t fs_lseek(int fd, size_t offset, int whence);
int fs_close(int fd);
这些文件操作实际上是相应的系统调用在内核中的实现.
由于sfs中每一个文件都是固定的, 不会产生新文件, 因此"
fs_open()
没有找到pathname
所指示的文件"属于异常情况, 你需要使用assertion终止程序运行.为了简化实现, 我们允许所有用户程序都可以对所有已存在的文件进行读写, 这样以后, 我们在实现
fs_open()
的时候就可以忽略flags
和mode
了.使用
ramdisk_read()
和ramdisk_write()
来进行文件的真正读写.由于文件的大小是固定的, 在实现
fs_read()
,fs_write()
和fs_lseek()
的时候, 注意偏移量不要越过文件的边界.除了写入
stdout
和stderr
之外(用putch()
输出到串口), 其余对于stdin
,stdout
和stderr
这三个特殊文件的操作可以直接忽略.由于sfs