课程主页:https://www.xuetangx.com/course/THU08091000267/5883104?channel=i.area.learn_title
实验指导书:https://objectkuan.gitbooks.io/ucore-docs/content/
github:https://github.com/chyyuu/ucore_os_lab (master 分支)
前置知识
设备驱动程序、文件系统、虚拟文件系统。
改动点
相比于 lab7
源代码,lab8
主要做了如下改动:
proc.h
扩展struct proc_struct
成员属性1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23struct proc_struct {
... // 同以往结构的属性
struct files_struct *filesp; // 当前进程的文件集信息
};
struct files_struct {
struct inode *pwd; // 当前进程所在工作目录的 inode
struct file *fd_array; // 打开文件表
int files_count; // 共享此 files_struct 的进程个数
semaphore_t files_sem; // 用于互斥访问 files_struct
};
struct file {
enum {
FD_NONE, FD_INIT, FD_OPENED, FD_CLOSED,
} status; // 文件状态 (无效、初始态、打开态、关闭态)
bool readable; // 可读
bool writable; // 可写
int fd; // 对应的文件描述符
off_t pos; // 目前的访问位置
struct inode *node; // 对应的 inode
int open_count; // 此文件打开的次数 (此实验中,该字段似乎没什么用。然而,对于 Linux 系统而言,该字段是具有意义的:父子进程共享文件描述符,它们会对应至相同的文件表项,该表项的 open_count 取值会增加)
};在 Linux 系统中,每个打开的文件对应三种数据结构:文件描述符表、打开文件表、
inode
表,其中前一者为进程级数据结构,后两者为系统级数据结构。https://blog.csdn.net/ai2000ai/article/details/79738422在
ucore
中,每个打开的文件仅对应两种数据结构:打开文件表 (包含文件描述符表的信息)、inode
表,其中前者为进程级数据结构,后者为系统级数据结构。iobuf.[ch]
提供数据读写的内核缓冲区1
2
3
4
5
6struct iobuf {
void *io_base;
off_t io_offset;
size_t io_len;
size_t io_resid;
};dev.h
规范设备抽象 (只要设备实现此结构所需内容,该系统便可应用此设备,用于屏蔽底层设备的不同)1
2
3
4
5
6
7
8struct device {
size_t d_blocks;
size_t d_blocksize;
int (*d_open)(struct device *dev, uint32_t open_flags);
int (*d_close)(struct device *dev);
int (*d_io)(struct device *dev, struct iobuf *iob, bool write);
int (*d_ioctl)(struct device *dev, int op, void *data);
};值得一说的是,借助于上述内容,我们可以实现如下函数:
1
2
3
4
5
6
7
8
9
10
11
12static const struct inode_ops dev_node_ops = {
.vop_magic = VOP_MAGIC,
.vop_open = dev_open,
.vop_close = dev_close,
.vop_read = dev_read,
.vop_write = dev_write,
.vop_fstat = dev_fstat,
.vop_ioctl = dev_ioctl,
.vop_gettype = dev_gettype,
.vop_tryseek = dev_tryseek,
.vop_lookup = dev_lookup,
};inode.h
规范 VFS 层级的inode
结构1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40struct inode {
// inode 对应的实际类型所存放的信息 (若是设备,则存放设备信息;若是特定 FS,则存放其详细的 inode 信息)
union {
struct device __device_info;
struct sfs_inode __sfs_inode_info;
} in_info;
// inode 对应的实际类型 (特定设备、特定文件系统)
enum {
inode_type_device_info = 0x1234,
inode_type_sfs_inode_info,
} in_type;
// 此 inode 的引用计数
int ref_count;
// 打开此 inode 的文件个数
int open_count;
// inode 对应的抽象文件系统
struct fs *in_fs;
// 抽象 inode 的操作集
const struct inode_ops *in_ops;
};
// VFS 层级,针对 inode 操作的众多定义,具体实现基于实际的文件系统
struct inode_ops {
unsigned long vop_magic;
int (*vop_open)(struct inode *node, uint32_t open_flags);
int (*vop_close)(struct inode *node);
int (*vop_read)(struct inode *node, struct iobuf *iob);
int (*vop_write)(struct inode *node, struct iobuf *iob);
int (*vop_fstat)(struct inode *node, struct stat *stat);
int (*vop_fsync)(struct inode *node);
int (*vop_namefile)(struct inode *node, struct iobuf *iob);
int (*vop_getdirentry)(struct inode *node, struct iobuf *iob);
int (*vop_reclaim)(struct inode *node);
int (*vop_gettype)(struct inode *node, uint32_t *type_store);
int (*vop_tryseek)(struct inode *node, off_t pos);
int (*vop_truncate)(struct inode *node, off_t len);
int (*vop_create)(struct inode *node, const char *name, bool excl, struct inode **node_store);
int (*vop_lookup)(struct inode *node, char *path, struct inode **node_store);
int (*vop_ioctl)(struct inode *node, int op, void *data);
};vfs.h
规范 VFS 层级的fs
结构1
2
3
4
5
6
7
8
9
10
11
12
13
14
15struct fs {
// 具体文件系统的信息 (此实验仅涉及 sfs)。
union {
struct sfs_fs __sfs_info;
} fs_info;
// fs 对应的实际文件系统类型
enum {
fs_type_sfs_info,
} fs_type;
// 针对 fs 的四大操作
int (*fs_sync)(struct fs *fs);
struct inode *(*fs_get_root)(struct fs *fs);
int (*fs_unmount)(struct fs *fs);
void (*fs_cleanup)(struct fs *fs);
};vfs.h
提供 VFS 层级的,针对文件、路径等内容的众多操作,它们进一步会调用inode_op->xxx
完成具体功能。sysfile.[ch]
提供关于文件系统调用的内核级封装这部分提供的系统调用会进一步调用 VFS 层级的函数,从而实现相关功能。
vfsdev.c
提供vfs_dev_t
结构1
2
3
4
5
6
7
8
9
10
11
12
13
14// ucore 将设备也视为一种文件,因此也将其集成至 VFS。
// VFS 将设备表示为 vfs_dev_t,并将其串接为一个链表,以方便后续操作。
typedef struct {
// 设备名称
const char *devname;
// 设备对应的 inode 信息 (十分重要,借助于它,ucore 才能统一设备与文件系统的操作)
struct inode *devnode;
// 设备所挂载的文件系统
struct fs *fs;
// 设备是否可挂载
bool mountable;
// 链接只用
list_entry_t vdev_link;
} vfs_dev_t;sfs.h
提供简易文件系统 SFS 的各种数据结构下图为 SFS 的物理布局:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50// sfs superblock 内容的具体结构
struct sfs_super {
uint32_t magic; // 唯一标记 sfs
uint32_t blocks; // sfs 的总块数
uint32_t unused_blocks; // sfs 尚未使用的块数
char info[SFS_MAX_INFO_LEN + 1]; // sfs 简介信息
};
// sfs 的 硬盘inode 内容的具体结构
struct sfs_disk_inode {
uint32_t size; // 文件大小 (字节单位)
uint16_t type; // 文件类型 (文件、目录、链接)
uint16_t nlinks; // 硬链接数目
uint32_t blocks; // 文件内容所占块数
uint32_t direct[SFS_NDIRECT]; // 块索引的直接索引
uint32_t indirect; // 块索引的一级索引
};
// sfs 目录文件内部一项的具体结构
struct sfs_disk_entry {
uint32_t ino; // 文件/目录的索引节点所占数据块索引值
char name[SFS_MAX_FNAME_LEN + 1]; // 文件/目录的名称
};
// 上述均为数据在硬盘中的组织形式,下述则是数据在内存中的组织形式
// sfs 的 内存inode 内容的具体结构 (之所以区分硬盘和内存,一则额外信息需要保存,二则方便某些操作)
struct sfs_inode {
struct sfs_disk_inode *din; // 硬盘 inode 的具体信息
uint32_t ino; // inode 号
bool dirty; // 此 inode 是否被修改
int reclaim_count; // 此 inode 待回收数,若其值为 0,需将其写回硬盘
semaphore_t sem; // 用于互斥访问 sfs_disk_inode
list_entry_t inode_link; // 链接之用
list_entry_t hash_link;
};
// sfs 文件系统的具体结构
struct sfs_fs {
struct sfs_super super; // superblock 信息
struct device *dev; // 所挂载的设备
struct bitmap *freemap; // freemap 表示的空闲块信息
bool super_dirty; // superblock/freemap 是否被修改
void *sfs_buffer; // 用于从硬盘获取非对齐块信息,以此作为缓冲,并复制给其他缓冲区
semaphore_t fs_sem; // 用于互斥访问 fs
semaphore_t io_sem; // 用于互斥访问 io
semaphore_t mutex_sem; /* semaphore for link/unlink and rename */
list_entry_t inode_list; // sfs 所管部分 inode 的链表组织形式
list_entry_t *hash_list; // sfs 所管部分 inode 的 hash 表组织形式
};sfs_inode.c
提供 SFS 关于inode
操作的实现1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27// 目录节点的操作
static const struct inode_ops sfs_node_dirops = {
.vop_magic = VOP_MAGIC,
.vop_open = sfs_opendir,
.vop_close = sfs_close,
.vop_fstat = sfs_fstat,
.vop_fsync = sfs_fsync,
.vop_namefile = sfs_namefile,
.vop_getdirentry = sfs_getdirentry,
.vop_reclaim = sfs_reclaim,
.vop_gettype = sfs_gettype,
.vop_lookup = sfs_lookup,
};
// 文件节点的操作
static const struct inode_ops sfs_node_fileops = {
.vop_magic = VOP_MAGIC,
.vop_open = sfs_openfile,
.vop_close = sfs_close,
.vop_read = sfs_read,
.vop_write = sfs_write,
.vop_fstat = sfs_fstat,
.vop_fsync = sfs_fsync,
.vop_reclaim = sfs_reclaim,
.vop_gettype = sfs_gettype,
.vop_tryseek = sfs_tryseek,
.vop_truncate = sfs_truncfile,
};
练习零
该练习用于了解 ucore
文件系统的实现机制与运行流程。
文件系统的实现机制
ucore
文件系统的实现机制详见 fs_init()
,我们对其进行简要分析:
vfs_init()
初始化 VFSVFS 主要记录两大数据:
1
2
3
4
5
6
7// 设备信息
static list_entry_t vdev_list;
static semaphore_t vdev_list_sem;
// 根目录的 inode 信息
static struct inode *bootfs_node = NULL;
static semaphore_t bootfs_sem;因此,初始化上述变量即是
vfs_init()
的工作。dev_init()
初始化相关设备此处设备指代
stdin
、stdout
、disk0
,在此仅以stdin
的初始化进行说明。stdin
初始化工作主要在于构建vfs_dev_t
,完成相关初始化,并将其加入至vdev_list
。vfs_dev_t
的详细信息具体如下 (可以看到:各字段均已填充完毕):1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31typedef struct {
const char *devname; --> "stdin"
struct inode *devnode; --> 下述的 struct inode
struct fs *fs; --> NULL
bool mountable; --> false (设备不同,选项不同。对于 disk0,其选择即为 true)
list_entry_t vdev_link;
} vfs_dev_t;
struct inode {
union {
struct device __device_info;
struct sfs_inode __sfs_inode_info;
} in_info; --> 下述的 struct device
enum {
inode_type_device_info = 0x1234,
inode_type_sfs_inode_info,
} in_type; --> inode_type_device_info
int ref_count; --> 1
int open_count; --> 0
struct fs *in_fs; --> NULL
const struct inode_ops *in_ops; --> 在dev.c中声明的dev_node_ops (重申一次,这些操作具体由 struct device 所定义的四个函数实现)
};
struct device {
size_t d_blocks; --> 0
size_t d_blocksize; --> 1
int (*d_open)(struct device *dev, uint32_t open_flags); --> stdin_open
int (*d_close)(struct device *dev); --> stdin_close
int (*d_io)(struct device *dev, struct iobuf *iob, bool write); --> stdin_io
int (*d_ioctl)(struct device *dev, int op, void *data); --> stdin_ioctl
};sfs_init()
初始化 sfssfs_init()
的工作在于:挂载 sfs 至disk0
,并使用disk0
硬盘信息初始化fs
。(把此实例文件系统挂在虚拟文件系统中,从而让ucore的其他部分能够通过访问虚拟文件系统的接口来进一步访问到SFS实例文件系统).fs
的详细信息具体如下:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30// disk0 所指代的 vfs_dev_t->fs = 下述 struct fs
struct fs {
// 具体文件系统的信息 (此实验仅涉及 sfs)。
union {
struct sfs_fs __sfs_info;
} fs_info; --> 下述的 struct sfs_fs
// fs 对应的实际文件系统类型
enum {
fs_type_sfs_info,
} fs_type; --> fs_type_sfs_info
// 针对 fs 的四大操作
int (*fs_sync)(struct fs *fs); --> sfs_sync (此四者为 sfs 的具体实现)
struct inode *(*fs_get_root)(struct fs *fs); --> sfs_get_root
int (*fs_unmount)(struct fs *fs); --> sfs_unmount
void (*fs_cleanup)(struct fs *fs); --> sfs_cleanup
};
struct sfs_fs {
struct sfs_super super; --> disk0 硬盘所存的 superblock
struct device *dev; --> disk0 所指代的 struct device
struct bitmap *freemap; --> 新建 freemap,并使用 disk0 硬盘所存信息进行初始化
bool super_dirty; --> false
void *sfs_buffer; --> 新建的缓冲区
semaphore_t fs_sem; --> 初始化若干信号量
semaphore_t io_sem;
semaphore_t mutex_sem;
list_entry_t inode_list; --> 初始化链表
list_entry_t *hash_list; --> 初始化 hash 表
};注:第二个内核线程
init
的主体实现init_main()
会设置disk0
的根目录inode
为bootfs_node
。
至此,ucore
文件系统已然实现。
文件系统的运行流程
使用若干文件操作说明 ucore
文件系统的运行流程:
SYS_open
1
2
3
4
5
6
7
81. 系统调用 SYS_open 陷入中断,经获取 path/open_flags 参数后,调用 sysfile_open() 进行处理。
2. sysfile_open() 进一步调用 file_open(),它首先从当前进程的文件集中分配 struct file,并调用 vfs_open() 进行处理。
3. vfs_open() 进一步调用 vfs_lookup() 去寻找 path 对应文件的 inode。
1. vfs_lookup() 借助于 get_device() 获取 path 最初目录的 inode (对于路径 "device:xxx" 而言,即是 device 对应的 inode,它可通过遍历 vdev_list 找到;对于路径 "/xxx" 而言,即是 / 对应的 inode,它可通过访问 bootfs_node 得到;对于路径 "xxx",即是工作目录对应的 inode`,它可通过访问 pwd 得到)。
2. 随后,借助于 vop_lookup() 寻找该目录下对应文件的 inode (该目录的 inode 已知,则可直接调用其具体的 inode_ops)。
4. 如果没有找到对应文件的 inode,而 open_flags 允许新建,则新建一个 inode (该目录是已知的,则其通过 inode_ops 调用的新建流程会自动设置,新建 inode 的 inode_ops 为该文件系统所允许的 inode_ops),并完成相应的初始化工作。
5. 使用上述得到的 inode 以及 open_flags 填充 struct file。
6. 返回 file->fd 给用户进程。SYS_close
1
2
3
41. 系统调用 SYS_close 陷入中断,经获取 fd 参数后,调用 sysfile_close() 进行处理。
2. sysfile_close() 进一步调用 file_close(),它首先从当前进程的文件集中获取此 fd 对应的 struct file,并调用 fd_array_close() 进行处理。
3. fd_array_close() 将 file->open_count 减一,如果此时为 0,则调用 fd_array_free() 进行清理。
4. fd_array_free() 进一步调用 vfs_close(),它会依据 node->ref_count/open_count 是否为 0,进一步调用 vop_close()/vop_reclaim() 完成善后工作 (由于 inode 已知,同样可直接调用特定文件系统的 inode_ops )。SYS_read
1
2
3
4
51. 系统调用 SYS_read 陷入中断,经获取 fd/base/len 参数后,调用 sysfile_read() 进行处理。
2. sysfile_read() 执行若干预处理操作 (判断是否存在该 fd、分配内核缓冲区),使用 file_read() 进行读取。
3. file_read() 找到该 fd 对应的 struct file,经过权限是否允许的判断后,使用 vop_read() 读取相关内容至内核缓冲区。
4. vop_read() 会基于 file-> pos 找到相应的物理块,并进一步调用设备的 dev_node_ops 完成读取操作。
5. 层层返回,将内核缓冲区的数据拷贝至用户空间 (可能由于待读取的数据很多,它会多次重复执行 3/4/5)。SYS_write
1
2
3
4
5
61. 系统调用 SYS_write 陷入中断,经获取 fd/base/len 参数后,调用 sysfile_write() 进行处理。
2. sysfile_write() 执行若干预处理操作 (判断是否存在该 fd、分配内核缓冲区、)。
3. 随后,sysfile_write() 拷贝用户空间数据至内核缓冲区,并使用 file_write() 进行写入。
4. file_write() 找到该 fd 对应的 struct file,经过权限是否允许的判断后,使用 vop_write() 将内核缓冲区内容写入至相关设备。
5. vop_write() 会基于 file-> pos 找到相应的物理块,并进一步调用设备的 dev_node_ops 完成写入操作。
// 可能由于待写入的数据很多,它会多次重复执行 3/4/5。
以用户态写文件函数write的整个执行过程为例,器流程图如下所示:
练习一
该练习用于实现文件读写的核心函数 sfs_io_nolock()
。
用户程序读文件需要使用系统调用。在用户程序执行
read
操作时会调用sys_read
系统调用。根据ucore的中断机制实现,系统调用将通过trap_dispatch
分发给syscall
,随后分发给读的系统调用sys_read
内核函数。
sys_read
内核函数需要进一步调用sysfile_read
内核函数,进入到文件系统抽象层处理流程完成进一步的读文件操作。sysfile_read
函数调用file_read
函数,file_read
函数调用vop_read
函数接口进入到文件系统实例的读操作接口。
vop_read
函数实际上是对sfs_read
的包装。sfs_read函数调用sfs_io函数。它有三个参数,node是对应文件的inode,iob是缓存,write表示是读还是写的布尔值( 0表示读,1表示写) ,这里是0。函数先找到inode对应sfs和sin,然后调用sfs_io_nolock函数进行读取文件操作,最后调用iobuf_skip函数调整iobuf的指针。
sfs_io_nolock
函数主要用来将磁盘中的一段数据读入到内存中或者将内存中的一段数据写入磁盘,其补充完整后的实现如下所示( 完整代码见源代码 ,这里只展示补充的部分):
1 | static int sfs_io_nolock(struct sfs_fs *sfs, struct sfs_inode *sin, void *buf, off_t offset, size_t *alenp, bool write) { |
问答
如何实现 UNIX 的 PIPE 机制?
简单来说,PIPE 用于两个进程通信,前者输出放至管道,后者输入取自管道,输入输出并不同步。那么,我么可以新建一个临时文件,再分别让这两个进程打开,各自构建出一个
struct file
,即文件描述符。两个进程应当对应不同的struct file
,但是对应相同的struct inode
(其对应的实际数据应当直接存放于内核之中)。另外,对于每个进程而言,其
fd_array[0,1,2]
分别指代输入、输出、错误输出。因此,应当修改前者的输出fd_array[1]
和后者的输入fd_array[0]
为上述的struct file
。
练习二
该练习用于实现程序加载的核心函数 load_icode()
。
load_icode()
实现具体见源代码 (可类比 lab7 实现此函数):
1 | static int load_icode(int fd, int argc, char **kargv) { |