From dde4fe5f1d7331b5b5a1e77f7952826e1340c8d2 Mon Sep 17 00:00:00 2001 From: Object Ho Date: Tue, 14 Oct 2014 19:57:50 +0800 Subject: [PATCH] Finish lab 8 --- SUMMARY.md | 22 + lab3/.DS_Store | Bin 6148 -> 6148 bytes lab8.md | 706 ------------------------- lab8/.DS_Store | Bin 0 -> 12292 bytes lab8/lab8_1_goals.md | 7 + lab8/lab8_2_1_exercises.md | 15 + lab8/lab8_2_2_files.md | 125 +++++ lab8/lab8_2_labs.md | 4 + lab8/lab8_3_1_ucore_fs_introduction.md | 43 ++ lab8/lab8_3_2_fs_interface.md | 24 + lab8/lab8_3_3_1_fs_layout.md | 32 ++ lab8/lab8_3_3_2_inode.md | 96 ++++ lab8/lab8_3_3_sfs.md | 13 + lab8/lab8_3_4_1_file_dir_interface.md | 31 ++ lab8/lab8_3_4_2_inode_interface.md | 40 ++ lab8/lab8_3_4_fs_abstract.md | 4 + lab8/lab8_3_5_1_data_structure.md | 32 ++ lab8/lab8_3_5_2_stdout_dev_file.md | 54 ++ lab8/lab8_3_5_3_stdin_dev_file.md | 82 +++ lab8/lab8_3_5_dev_file_io_layer.md | 4 + lab8/lab8_3_6_labs_steps.md | 13 + lab8/lab8_3_7_1_file_open.md | 36 ++ lab8/lab8_3_7_2_file_read.md | 40 ++ lab8/lab8_3_7_file_op_implement.md | 2 + lab8/lab8_3_fs_design_implement.md | 2 + lab8/lab8_4_lab_requirement.md | 7 + {lab8 => lab8_figs}/image001.png | Bin {lab8 => lab8_figs}/image002.png | Bin {lab8 => lab8_figs}/image003.png | Bin {lab8 => lab8_figs}/image004.png | Bin 30 files changed, 728 insertions(+), 706 deletions(-) create mode 100644 lab8/.DS_Store create mode 100644 lab8/lab8_1_goals.md create mode 100644 lab8/lab8_2_1_exercises.md create mode 100644 lab8/lab8_2_2_files.md create mode 100644 lab8/lab8_2_labs.md create mode 100644 lab8/lab8_3_1_ucore_fs_introduction.md create mode 100644 lab8/lab8_3_2_fs_interface.md create mode 100644 lab8/lab8_3_3_1_fs_layout.md create mode 100644 lab8/lab8_3_3_2_inode.md create mode 100644 lab8/lab8_3_3_sfs.md create mode 100644 lab8/lab8_3_4_1_file_dir_interface.md create mode 100644 lab8/lab8_3_4_2_inode_interface.md create mode 100644 lab8/lab8_3_4_fs_abstract.md create mode 100644 lab8/lab8_3_5_1_data_structure.md create mode 100644 lab8/lab8_3_5_2_stdout_dev_file.md create mode 100644 lab8/lab8_3_5_3_stdin_dev_file.md create mode 100644 lab8/lab8_3_5_dev_file_io_layer.md create mode 100644 lab8/lab8_3_6_labs_steps.md create mode 100644 lab8/lab8_3_7_1_file_open.md create mode 100644 lab8/lab8_3_7_2_file_read.md create mode 100644 lab8/lab8_3_7_file_op_implement.md create mode 100644 lab8/lab8_3_fs_design_implement.md create mode 100644 lab8/lab8_4_lab_requirement.md rename {lab8 => lab8_figs}/image001.png (100%) rename {lab8 => lab8_figs}/image002.png (100%) rename {lab8 => lab8_figs}/image003.png (100%) rename {lab8 => lab8_figs}/image004.png (100%) diff --git a/SUMMARY.md b/SUMMARY.md index 3a1eb58..677bbb8 100644 --- a/SUMMARY.md +++ b/SUMMARY.md @@ -95,4 +95,26 @@ * [实验报告要求](lab7/lab7_4_lab_requirement.md) * [附录](lab7/lab7_5_appendix.md) * [Lab 8](lab8.md) + * [实验目的](lab8/lab8_1_goals.md) + * [实验内容](lab8/lab8_2_labs.md) + * [练习](lab8/lab8_2_1_exercises.md) + * [项目组成](lab8/lab8_2_2_files.md) + * [文件系统设计与实现](lab8/lab8_3_fs_design_implement.md) + * [ucore 文件系统总体介绍](lab8/lab8_3_1_ucore_fs_introduction.md) + * [通用文件系统访问接口](lab8/lab8_3_2_fs_interface.md) + * [Simple FS 文件系统](lab8/lab8_3_3_sfs.md) + * [文件系统的布局](lab8/lab8_3_3_1_fs_layout.md) + * [索引节点](lab8/lab8_3_3_2_inode.md) + * [文件系统抽象层 - VFS](lab8/lab8_3_4_fs_abstract.md) + * [file & dir接口](lab8/lab8_3_4_1_file_dir_interface.md) + * [inode 接口 ](lab8/lab8_3_4_2_inode_interface.md) + * [设备层文件 IO 层](lab8/lab8_3_5_dev_file_io_layer.md) + * [关键数据结构](lab8/lab8_3_5_1_data_structure.md) + * [stdout设备文件](lab8/lab8_3_5_2_stdout_dev_file.md) + * [stdin 设备文件](lab8/lab8_3_5_3_stdin_dev_file.md) + * [实验执行流程概述](lab8/lab8_3_6_labs_steps.md) + * [文件操作实现](lab8/lab8_3_7_file_op_implement.md) + * [打开文件](lab8/lab8_3_7_1_file_open.md) + * [读文件](lab8/lab8_3_7_2_file_read.md) + * [实验报告要求](lab8/lab8_4_lab_requirement.md) diff --git a/lab3/.DS_Store b/lab3/.DS_Store index 23c76111dd923ec66874f0d9ab362e5b30ece4bb..affecaf8a8a17c62f5249e6eb47824354c24e43e 100644 GIT binary patch delta 189 zcmZoMXffDe#lpeBz~BbN8k3z^oIsp2U``K<6_CRi1m>J$F#&OIfH@P`#V0GWngRuw z;=lrNtS%tV6EJ5Ls~?cVoCV@cVlx7blocks 的参数。当 index == inode-\>blocks 时,该函数理解为需要为 inode 增长一个 block。并标记 inode 为 dirty(所有对 inode 数据的修改都要做这样的操作,这样,当 inode 不再使用的时候,sfs 能够保证 inode 数据能够被写回到磁盘)。sfs\_bmap\_load\_nolock 调用的 sfs\_bmap\_get\_nolock 来完成相应的操作,阅读 sfs\_bmap\_get\_nolock,了解他是如何工作的。(sfs\_bmap\_get\_nolock 只由 sfs\_bmap\_load\_nolock 调用) -2. sfs\_bmap\_truncate\_nolock:将多级数据索引表的最后一个 entry 释放掉。他可以认为是 sfs\_bmap\_load\_nolock 中,index == inode-\>blocks 的逆操作。当一个文件或目录被删除时,sfs 会循环调用该函数直到 inode-\>blocks 减为 0,释放所有的数据页。函数通过 sfs\_bmap\_free\_nolock 来实现,他应该是 sfs\_bmap\_get\_nolock 的逆操作。和 sfs\_bmap\_get\_nolock 一样,调用 sfs\_bmap\_free\_nolock 也要格外小心。 -3. sfs\_dirent\_read\_nolock:将目录的第 slot 个 entry 读取到指定的内存空间。他通过上面提到的函数来完成。 -4. sfs\_dirent\_write\_nolock:用指定的 entry 来替换某个目录下的第 slot 个entry。他通过调用 sfs\_bmap\_load\_nolock保证,当第 slot 个entry 不存在时(slot == inode-\>blocks),SFS 会分配一个新的entry,即在目录尾添加了一个 entry。 -5. sfs\_dirent\_search\_nolock:是常用的查找函数。他在目录下查找 name,并且返回相应的搜索结果(文件或文件夹)的 inode 的编号(也是磁盘编号),和相应的 entry 在该目录的 index 编号以及目录下的数据页是否有空闲的 entry。(SFS 实现里文件的数据页是连续的,不存在任何空洞;而对于目录,数据页不是连续的,当某个 entry 删除的时候,SFS 通过设置 entry-\>ino 为0将该 entry 所在的 block 标记为 free,在需要添加新 entry 的时候,SFS 优先使用这些 free 的 entry,其次才会去在数据页尾追加新的 entry。 - -注意,这些后缀为 nolock 的函数,只能在已经获得相应 inode 的semaphore才能调用。 - -**Inode的文件操作函数** - -``` -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, - …… -}; -``` - -上述sfs\_openfile、sfs\_close、sfs\_read和sfs\_write分别对应用户进程发出的open、close、read、write操作。其中sfs\_openfile不用做什么事;sfs\_close需要把对文件的修改内容写回到硬盘上,这样确保硬盘上的文件内容数据是最新的;sfs\_read和sfs\_write函数都调用了一个函数sfs\_io,并最终通过访问硬盘驱动来完成对文件内容数据的读写。 - -**Inode的目录操作函数** - -``` -static const struct inode_ops sfs_node_dirops = { - .vop_magic = VOP_MAGIC, - .vop_open = sfs_opendir, - .vop_close = sfs_close, - .vop_getdirentry = sfs_getdirentry, - .vop_lookup = sfs_lookup, - …… -}; -``` - -对于目录操作而言,由于目录也是一种文件,所以sfs\_opendir、sys\_close对应户进程发出的open、close函数。相对于sfs\_open,sfs\_opendir只是完成一些open函数传递的参数判断,没做其他更多的事情。目录的close操作与文件的close操作完全一致。由于目录的内容数据与文件的内容数据不同,所以读出目录的内容数据的函数是sfs\_getdirentry,其主要工作是获取目录下的文件inode信息。 - -### 3.4 文件系统抽象层 -VFS - -文件系统抽象层是把不同文件系统的对外共性接口提取出来,形成一个函数指针数组,这样,通用文件系统访问接口层只需访问文件系统抽象层,而不需关心具体文件系统的实现细节和接口。 - -#### 3.4.1 file&dir接口 - -file&dir接口层定义了进程在内核中直接访问的文件相关信息,这定义在file数据结构中,具体描述如下: - -``` -struct file { - enum { - FD_NONE, FD_INIT, FD_OPENED, FD_CLOSED, - } status; //访问文件的执行状态 - bool readable; //文件是否可读 - bool writable; //文件是否可写 - int fd; //文件在filemap中的索引值 - off_t pos; //访问文件的当前位置 - struct inode *node; //该文件对应的内存inode指针 - atomic_t open_count; //打开此文件的次数 -}; -``` - -而在kern/process/proc.h中的proc\_struct结构中描述了进程访问文件的数据接口fs\_struct,其数据结构定义如下: - -``` -struct fs_struct { - struct inode *pwd; //进程当前执行目录的内存inode指针 - struct file *filemap; //进程打开文件的数组 - atomic_t fs_count; //访问此文件的线程个数?? - semaphore_t fs_sem; //确保对进程控制块中fs_struct的互斥访问 -}; -``` - -当创建一个进程后,该进程的fs\_struct将会被初始化或复制父进程的fs\_struct。当用户进程打开一个文件时,将从filemap数组中取得一个空闲file项,然后会把此file的成员变量node指针指向一个代表此文件的inode的起始地址。 - -#### 3.4.2 inode 接口 - -index -node是位于内存的索引节点,它是VFS结构中的重要数据结构,因为它实际负责把不同文件系统的特定索引节点信息(甚至不能算是一个索引节点)统一封装起来,避免了进程直接访问具体文件系统。其定义如下: - -``` -struct inode { - union { //包含不同文件系统特定inode信息的union成员变量 - struct device __device_info; //设备文件系统内存inode信息 - struct sfs_inode __sfs_inode_info; //SFS文件系统内存inode信息 - } in_info; - enum { - inode_type_device_info = 0x1234, - inode_type_sfs_inode_info, - } in_type; //此inode所属文件系统类型 - atomic_t ref_count; //此inode的引用计数 - atomic_t open_count; //打开此inode对应文件的个数 - struct fs *in_fs; //抽象的文件系统,包含访问文件系统的函数指针 - const struct inode_ops *in_ops; //抽象的inode操作,包含访问inode的函数指针 -}; -``` - -在inode中,有一成员变量为in\_ops,这是对此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_getdirentry)(struct inode *node, struct iobuf *iob); - 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); -…… - }; -``` - -参照上面对SFS中的索引节点操作函数的说明,可以看出inode\_ops是对常规文件、目录、设备文件所有操作的一个抽象函数表示。对于某一具体的文件系统中的文件或目录,只需实现相关的函数,就可以被用户进程访问具体的文件了,且用户进程无需了解具体文件系统的实现细节。 - -### 3.5 设备层文件 IO 层 - -在本实验中,为了统一地访问设备,我们可以把一个设备看成一个文件,通过访问文件的接口来访问设备。目前实现了stdin设备文件文件、stdout设备文件、disk0设备。stdin设备就是键盘,stdout设备就是CONSOLE(串口、并口和文本显示器),而disk0设备是承载SFS文件系统的磁盘设备。下面我们逐一分析ucore是如何让用户把设备看成文件来访问。 - -#### 3.5.1 关键数据结构 - -为了表示一个设备,需要有对应的数据结构,ucore为此定义了struct device,其描述如下: - -``` -struct 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); //用ioctl方式控制设备的函数指针 -}; -``` - -这个数据结构能够支持对块设备(比如磁盘)、字符设备(比如键盘、串口)的表示,完成对设备的基本操作。ucore虚拟文件系统为了把这些设备链接在一起,还定义了一个设备链表,即双向链表vdev\_list,这样通过访问此链表,可以找到ucore能够访问的所有设备文件。 - -但这个设备描述没有与文件系统以及表示一个文件的inode数据结构建立关系,为此,还需要另外一个数据结构把device和inode联通起来,这就是vfs\_dev\_t数据结构: - -``` -// device info entry in vdev_list -typedef struct { - const char *devname; - struct inode *devnode; - struct fs *fs; - bool mountable; - list_entry_t vdev_link; -} vfs_dev_t; -``` - -利用vfs\_dev\_t数据结构,就可以让文件系统通过一个链接vfs\_dev\_t结构的双向链表找到device对应的inode数据结构,一个inode节点的成员变量in\_type的值是0x1234,则此 inode的成员变量in\_info将成为一个device结构。这样inode就和一个设备建立了联系,这个inode就是一个设备文件。 - -#### 3.5.2 stdout设备文件 - -**初始化** - -既然stdout设备是设备文件系统的文件,自然有自己的inode结构。在系统初始化时,即只需如下处理过程 - -``` -kern_init-->fs_init-->dev_init-->dev_init_stdout --> dev_create_inode - --> stdout_device_init - --> vfs_add_dev -``` - -在dev\_init\_stdout中完成了对stdout设备文件的初始化。即首先创建了一个inode,然后通过stdout\_device\_init完成对inode中的成员变量inode-\>\_\_device\_info进行初始: - -这里的stdout设备文件实际上就是指的console外设(它其实是串口、并口和CGA的组合型外设)。这个设备文件是一个只写设备,如果读这个设备,就会出错。接下来我们看看stdout设备的相关处理过程。 - -**初始化** - -stdout设备文件的初始化过程主要由stdout\_device\_init完成,其具体实现如下: - -``` -static void -stdout_device_init(struct device *dev) { - dev->d_blocks = 0; - dev->d_blocksize = 1; - dev->d_open = stdout_open; - dev->d_close = stdout_close; - dev->d_io = stdout_io; - dev->d_ioctl = stdout_ioctl; -} -``` - -可以看到,stdout\_open函数完成设备文件打开工作,如果发现用户进程调用open函数的参数flags不是只写(O\_WRONLY),则会报错。 - -**访问操作实现** - -stdout\_io函数完成设备的写操作工作,具体实现如下: - -``` -static int -stdout_io(struct device *dev, struct iobuf *iob, bool write) { - if (write) { - char *data = iob->io_base; - for (; iob->io_resid != 0; iob->io_resid --) { - cputchar(*data ++); - } - return 0; - } - return -E_INVAL; -} -``` - -可以看到,要写的数据放在iob-\>io\_base所指的内存区域,一直写到iob-\>io\_resid的值为0为止。每次写操作都是通过cputchar来完成的,此函数最终将通过console外设驱动来完成把数据输出到串口、并口和CGA显示器上过程。另外,也可以注意到,如果用户想执行读操作,则stdout\_io函数直接返回错误值**-**E\_INVAL。 - -#### 3.5.3 stdin 设备文件 - -这里的stdin设备文件实际上就是指的键盘。这个设备文件是一个只读设备,如果写这个设备,就会出错。接下来我们看看stdin设备的相关处理过程。 - -**初始化** - -stdin设备文件的初始化过程主要由stdin\_device\_init完成了主要的初始化工作,具体实现如下: - -``` -static void -stdin_device_init(struct device *dev) { - dev->d_blocks = 0; - dev->d_blocksize = 1; - dev->d_open = stdin_open; - dev->d_close = stdin_close; - dev->d_io = stdin_io; - dev->d_ioctl = stdin_ioctl; - - p_rpos = p_wpos = 0; - wait_queue_init(wait_queue); -} -``` - -相对于stdout的初始化过程,stdin的初始化相对复杂一些,多了一个stdin\_buffer缓冲区,描述缓冲区读写位置的变量p\_rpos、p\_wpos以及用于等待缓冲区的等待队列wait\_queue。在stdin\_device\_init函数的初始化中,也完成了对p\_rpos、p\_wpos和wait\_queue的初始化。 - -**访问操作实现** - -stdin\_io函数负责完成设备的读操作工作,具体实现如下: - -``` -static int -stdin_io(struct device *dev, struct iobuf *iob, bool write) { - if (!write) { - int ret; - if ((ret = dev_stdin_read(iob->io_base, iob->io_resid)) > 0) { - iob->io_resid -= ret; - } - return ret; - } - return -E_INVAL; -} -``` - -可以看到,如果是写操作,则stdin\_io函数直接报错返回。所以这也进一步说明了此设备文件是只读文件。如果此读操作,则此函数进一步调用dev\_stdin\_read函数完成对键盘设备的读入操作。dev\_stdin\_read函数的实现相对复杂一些,主要的流程如下: - -``` -static int -dev_stdin_read(char *buf, size_t len) { - int ret = 0; - bool intr_flag; - local_intr_save(intr_flag); - { - for (; ret < len; ret ++, p_rpos ++) { - try_again: - if (p_rpos < p_wpos) { - *buf ++ = stdin_buffer[p_rpos % stdin_BUFSIZE]; - } - else { - wait_t __wait, *wait = &__wait; - wait_current_set(wait_queue, wait, WT_KBD); - local_intr_restore(intr_flag); - - schedule(); - - local_intr_save(intr_flag); - wait_current_del(wait_queue, wait); - if (wait->wakeup_flags == WT_KBD) { - goto try_again; - } - break; - } - } - } - local_intr_restore(intr_flag); - return ret; -} -``` - -在上述函数中可以看出,如果p\_rpos < p\_wpos,则表示有键盘输入的新字符在stdin\_buffer中,于是就从stdin\_buffer中取出新字符放到iobuf指向的缓冲区中;如果p\_rpos \>=p\_wpos,则表明没有新字符,这样调用read用户态库函数的用户进程就需要采用等待队列的睡眠操作进入睡眠状态,等待键盘输入字符的产生。 - -键盘输入字符后,如何唤醒等待键盘输入的用户进程呢?回顾lab1中的外设中断处理,可以了解到,当用户敲击键盘时,会产生键盘中断,在trap\_dispatch函数中,当识别出中断是键盘中断(中断号为IRQ\_OFFSET + IRQ\_KBD)时,会调用dev\_stdin\_write函数,来把字符写入到stdin\_buffer中,且会通过等待队列的唤醒操作唤醒正在等待键盘输入的用户进程。 - -### 3.6 实验执行流程概述 - -与实验七相比,实验八增加了文件系统,并因此实现了通过文件系统来加载可执行文件到内存中运行的功能,导致对进程管理相关的实现比较大的调整。我们来简单看看文件系统是如何初始化并能在ucore的管理下正常工作的。 - -首先看看kern\_init函数,可以发现与lab7相比增加了对fs\_init函数的调用。fs\_init函数就是文件系统初始化的总控函数,它进一步调用了虚拟文件系统初始化函数vfs\_init,与文件相关的设备初始化函数dev\_init和Simple FS文件系统的初始化函数sfs\_init。这三个初始化函数联合在一起,协同完成了整个虚拟文件系统、SFS文件系统和文件系统对应的设备(键盘、串口、磁盘)的初始化工作。其函数调用关系图如下所示: - -![image](lab8/image004.png) - -文件系统初始化调用关系图 - -参考上图,并结合源码分析,可大致了解到文件系统的整个初始化流程。vfs\_init主要建立了一个device -list双向链表vdev\_list,为后续具体设备(键盘、串口、磁盘)以文件的形式呈现建立查找访问通道。dev\_init函数通过进一步调用disk0/stdin/stdout\_device\_init完成对具体设备的初始化,把它们抽象成一个设备文件,并建立对应的inode数据结构,最后把它们链入到vdev\_list中。这样通过虚拟文件系统就可以方便地以文件的形式访问这些设备了。sfs\_init是完成对Simple FS的初始化工作,并把此实例文件系统挂在虚拟文件系统中,从而让ucore的其他部分能够通过访问虚拟文件系统的接口来进一步访问到SFS实例文件系统。 - -### 3.7 文件操作实现 - -#### 3.7.1 打开文件 - -有了上述分析后,我们可以看看如果一个用户进程打开文件会做哪些事情?首先假定用户进程需要打开的文件已经存在在硬盘上。以user/sfs\_filetest1.c为例,首先用户进程会调用在main函数中的如下语句: - -``` -int fd1 = safe_open("/test/testfile", O_RDWR | O_TRUNC); -``` - -从字面上可以看出,如果ucore能够正常查找到这个文件,就会返回一个代表文件的文件描述符fd1,这样在接下来的读写文件过程中,就直接用这样fd1来代表就可以了。那这个打开文件的过程是如何一步一步实现的呢? - -**通用文件访问接口层的处理流程** - -首先进入通用文件访问接口层的处理流程,即进一步调用如下用户态函数: open-\>sys\_open-\>syscall,从而引起系统调用进入到内核态。到了内核态后,通过中断处理例程,会调用到sys\_open内核函数,并进一步调用sysfile\_open内核函数。到了这里,需要把位于用户空间的字符串"/test/testfile"拷贝到内核空间中的字符串path中,并进入到文件系统抽象层的处理流程完成进一步的打开文件操作中。 - -**文件系统抽象层的处理流程** - -1. 分配一个空闲的file数据结构变量file在文件系统抽象层的处理中,首先调用的是file\_open函数,它要给这个即将打开的文件分配一个file数据结构的变量,这个变量其实是当前进程的打开文件数组current-\>fs\_struct-\>filemap[]中的一个空闲元素(即还没用于一个打开的文件),而这个元素的索引值就是最终要返回到用户进程并赋值给变量fd1。到了这一步还仅仅是给当前用户进程分配了一个file数据结构的变量,还没有找到对应的文件索引节点。 - -为此需要进一步调用vfs\_open函数来找到path指出的文件所对应的基于inode数据结构的VFS索引节点node。vfs\_open函数需要完成两件事情:通过vfs\_lookup找到path对应文件的inode;调用vop\_open函数打开文件。 - -2. 找到文件设备的根目录“/”的索引节点需要注意,这里的vfs\_lookup函数是一个针对目录的操作函数,它会调用vop\_lookup函数来找到SFS文件系统中的“/test”目录下的“testfile”文件。为此,vfs\_lookup函数首先调用get\_device函数,并进一步调用vfs\_get\_bootfs函数(其实调用了)来找到根目录“/”对应的inode。这个inode就是位于vfs.c中的inode变量bootfs\_node。这个变量在init\_main函数(位于kern/process/proc.c)执行时获得了赋值。 - -3. 找到根目录“/”下的“test”子目录对应的索引节点,在找到根目录对应的inode后,通过调用vop\_lookup函数来查找“/”和“test”这两层目录下的文件“testfile”所对应的索引节点,如果找到就返回此索引节点。 - -4. 把file和node建立联系。完成第3步后,将返回到file\_open函数中,通过执行语句“file-\>node=node;”,就把当前进程的current-\>fs\_struct-\>filemap[fd](即file所指变量)的成员变量node指针指向了代表“/test/testfile”文件的索引节点node。这时返回fd。经过重重回退,通过系统调用返回,用户态的syscall-\>sys\_open-\>open-\>safe\_open等用户函数的层层函数返回,最终把把fd赋值给fd1。自此完成了打开文件操作。但这里我们还没有分析第2和第3步是如何进一步调用SFS文件系统提供的函数找位于SFS文件系统上的“/test/testfile”所对应的sfs磁盘inode的过程。下面需要进一步对此进行分析。 - -**SFS文件系统层的处理流程** - -这里需要分析文件系统抽象层中没有彻底分析的vop\_lookup函数到底做了啥。下面我们来看看。在sfs\_inode.c中的sfs\_node\_dirops变量定义了“.vop\_lookup = sfs\_lookup”,所以我们重点分析sfs\_lookup的实现。 - -sfs\_lookup有三个参数:node,path,node\_store。其中node是根目录“/”所对应的inode节点;path是文件“testfile”的绝对路径“/test/testfile”,而node\_store是经过查找获得的“testfile”所对应的inode节点。 - -Sfs\_lookup函数以“/”为分割符,从左至右逐一分解path获得各个子目录和最终文件对应的inode节点。在本例中是分解出“test”子目录,并调用sfs\_lookup\_once函数获得“test”子目录对应的inode节点subnode,然后循环进一步调用sfs\_lookup\_once查找以“test”子目录下的文件“testfile1”所对应的inode节点。当无法分解path后,就意味着找到了testfile1对应的inode节点,就可顺利返回了。 - -当然这里讲得还比较简单,sfs\_lookup\_once将调用sfs\_dirent\_search\_nolock函数来查找与路径名匹配的目录项,如果找到目录项,则根据目录项中记录的inode所处的数据块索引值找到路径名对应的SFS磁盘inode,并读入SFS磁盘inode对的内容,创建SFS内存inode。 - -#### 3.7.2 读文件 - -读文件其实就是读出目录中的目录项,首先假定文件在磁盘上且已经打开。用户进程有如下语句: - -``` -read(fd, data, len); -``` - -即读取fd对应文件,读取长度为len,存入data中。下面来分析一下读文件的实现。 - -**通用文件访问接口层的处理流程** - -先进入通用文件访问接口层的处理流程,即进一步调用如下用户态函数:read-\>sys\_read-\>syscall,从而引起系统调用进入到内核态。到了内核态以后,通过中断处理例程,会调用到sys\_read内核函数,并进一步调用sysfile\_read内核函数,进入到文件系统抽象层处理流程完成进一步读文件的操作。 - -**文件系统抽象层的处理流程** - -1) 检查错误,即检查读取长度是否为0和文件是否可读。 - -2) 分配buffer空间,即调用kmalloc函数分配4096字节的buffer空间。 - -3) 读文件过程 - -[1] 实际读文件 - -循环读取文件,每次读取buffer大小。每次循环中,先检查剩余部分大小,若其小于4096字节,则只读取剩余部分的大小。然后调用file\_read函数(详细分析见后)将文件内容读取到buffer中,alen为实际大小。调用copy\_to\_user函数将读到的内容拷贝到用户的内存空间中,调整各变量以进行下一次循环读取,直至指定长度读取完成。最后函数调用层层返回至用户程序,用户程序收到了读到的文件内容。 - -[2] file\_read函数 - -这个函数是读文件的核心函数。函数有4个参数,fd是文件描述符,base是缓存的基地址,len是要读取的长度,copied\_store存放实际读取的长度。函数首先调用fd2file函数找到对应的file结构,并检查是否可读。调用filemap\_acquire函数使打开这个文件的计数加1。调用vop\_read函数将文件内容读到iob中(详细分析见后)。调整文件指针偏移量pos的值,使其向后移动实际读到的字节数iobuf\_used(iob)。最后调用filemap\_release函数使打开这个文件的计数减1,若打开计数为0,则释放file。 - -**SFS文件系统层的处理流程** - -vop\_read函数实际上是对sfs\_read的包装。在sfs\_inode.c中sfs\_node\_fileops变量定义了.vop\_read = sfs\_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函数中,先计算一些辅助变量,并处理一些特殊情况(比如越界),然后有sfs\_buf\_op = sfs\_rbuf,sfs\_block\_op = sfs\_rblock,设置读取的函数操作。接着进行实际操作,先处理起始的没有对齐到块的部分,再以块为单位循环处理中间的部分,最后处理末尾剩余的部分。每部分中都调用sfs\_bmap\_load\_nolock函数得到blkno对应的inode编号,并调用sfs\_rbuf或sfs\_rblock函数读取数据(中间部分调用sfs\_rblock,起始和末尾部分调用sfs\_rbuf),调整相关变量。完成后如果offset + alen \> din-\>fileinfo.size(写文件时会出现这种情况,读文件时不会出现这种情况,alen为实际读写的长度),则调整文件大小为offset + alen并设置dirty变量。 - -sfs\_bmap\_load\_nolock函数将对应sfs\_inode的第index个索引指向的block的索引值取出存到相应的指针指向的单元(ino\_store)。它调用sfs\_bmap\_get\_nolock来完成相应的操作。sfs\_rbuf和sfs\_rblock函数最终都调用sfs\_rwblock\_nolock函数完成操作,而sfs\_rwblock\_nolock函数调用dop\_io-\>disk0\_io-\>disk0\_read\_blks\_nolock-\>ide\_read\_secs完成对磁盘的操作。 - -## 4. 实验报告要求 - - -从网站上下载lab8.zip后,解压得到本文档和代码目录lab8,完成实验中的各个练习。完成代码编写并检查无误后,在对应目录下执行 make handin 任务,即会自动生成lab8-handin.tar.gz。最后请一定提前或按时提交到网络学堂上。 - -注意有“LAB8”的注释,这是需要主要修改的内容。代码中所有需要完成的地方challenge除外)都有“LAB8”和“YOUR CODE”的注释,请在提交时特别注意保持注释,并将“YOUR CODE”替换为自己的学号,并且将所有标有对应注释的部分填上正确的代码。 diff --git a/lab8/.DS_Store b/lab8/.DS_Store new file mode 100644 index 0000000000000000000000000000000000000000..f9df618baefc107c536c9a7c05b0c7c884913b24 GIT binary patch literal 12292 zcmeHMO>5gg5S?vv2o#!>($s|nls`~NzmFoMho1WboY<0sL9W`!F3nB)$MaFW1ekrwF z^j6!1F<=ZB1IB$d)aw)Zv{AoT(#UIBNb#krr<1ClVCDYrjppHAtZBaq2&MdzQm$kw zbD)%oNY5f9qRoI%B_lkI5Y(?UKaSUQ-v&Qc@SuhtrMINPIG1m-#7r%P@a+;aHRe*7 z8bv#(e>bRPY=%fov0n<_%LuQtdIrA7{@pcfTfxQ@wC4k8w2&Ut+ccZjbBXq_H}EPg z23cgoJ81J4r7jD`uwxFEx;WgGv7-U}O&_;Kj$=W~H4gl;8lN+aY+5W1BRk&*9tvc~ z5}lkf3pE2*cuI>->_2hoKhcViE9AJ8pP21fPsyGki#7H{W)5wKJ7jTeA7fSnpI`em zY`;p%_k)Pz4*`MEQbtPoj^`BbX8(fH7bU7z4(DF<=ZB1IBblocks 的参数。当 index == inode-\>blocks 时,该函数理解为需要为 inode 增长一个 block。并标记 inode 为 dirty(所有对 inode 数据的修改都要做这样的操作,这样,当 inode 不再使用的时候,sfs 能够保证 inode 数据能够被写回到磁盘)。sfs\_bmap\_load\_nolock 调用的 sfs\_bmap\_get\_nolock 来完成相应的操作,阅读 sfs\_bmap\_get\_nolock,了解他是如何工作的。(sfs\_bmap\_get\_nolock 只由 sfs\_bmap\_load\_nolock 调用) +2. sfs\_bmap\_truncate\_nolock:将多级数据索引表的最后一个 entry 释放掉。他可以认为是 sfs\_bmap\_load\_nolock 中,index == inode-\>blocks 的逆操作。当一个文件或目录被删除时,sfs 会循环调用该函数直到 inode-\>blocks 减为 0,释放所有的数据页。函数通过 sfs\_bmap\_free\_nolock 来实现,他应该是 sfs\_bmap\_get\_nolock 的逆操作。和 sfs\_bmap\_get\_nolock 一样,调用 sfs\_bmap\_free\_nolock 也要格外小心。 +3. sfs\_dirent\_read\_nolock:将目录的第 slot 个 entry 读取到指定的内存空间。他通过上面提到的函数来完成。 +4. sfs\_dirent\_write\_nolock:用指定的 entry 来替换某个目录下的第 slot 个entry。他通过调用 sfs\_bmap\_load\_nolock保证,当第 slot 个entry 不存在时(slot == inode-\>blocks),SFS 会分配一个新的entry,即在目录尾添加了一个 entry。 +5. sfs\_dirent\_search\_nolock:是常用的查找函数。他在目录下查找 name,并且返回相应的搜索结果(文件或文件夹)的 inode 的编号(也是磁盘编号),和相应的 entry 在该目录的 index 编号以及目录下的数据页是否有空闲的 entry。(SFS 实现里文件的数据页是连续的,不存在任何空洞;而对于目录,数据页不是连续的,当某个 entry 删除的时候,SFS 通过设置 entry-\>ino 为0将该 entry 所在的 block 标记为 free,在需要添加新 entry 的时候,SFS 优先使用这些 free 的 entry,其次才会去在数据页尾追加新的 entry。 + +注意,这些后缀为 nolock 的函数,只能在已经获得相应 inode 的semaphore才能调用。 + +**Inode的文件操作函数** + +``` +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, + …… +}; +``` + +上述sfs\_openfile、sfs\_close、sfs\_read和sfs\_write分别对应用户进程发出的open、close、read、write操作。其中sfs\_openfile不用做什么事;sfs\_close需要把对文件的修改内容写回到硬盘上,这样确保硬盘上的文件内容数据是最新的;sfs\_read和sfs\_write函数都调用了一个函数sfs\_io,并最终通过访问硬盘驱动来完成对文件内容数据的读写。 + +**Inode的目录操作函数** + +``` +static const struct inode_ops sfs_node_dirops = { + .vop_magic = VOP_MAGIC, + .vop_open = sfs_opendir, + .vop_close = sfs_close, + .vop_getdirentry = sfs_getdirentry, + .vop_lookup = sfs_lookup, + …… +}; +``` + +对于目录操作而言,由于目录也是一种文件,所以sfs\_opendir、sys\_close对应户进程发出的open、close函数。相对于sfs\_open,sfs\_opendir只是完成一些open函数传递的参数判断,没做其他更多的事情。目录的close操作与文件的close操作完全一致。由于目录的内容数据与文件的内容数据不同,所以读出目录的内容数据的函数是sfs\_getdirentry,其主要工作是获取目录下的文件inode信息。 diff --git a/lab8/lab8_3_3_sfs.md b/lab8/lab8_3_3_sfs.md new file mode 100644 index 0000000..5581d1f --- /dev/null +++ b/lab8/lab8_3_3_sfs.md @@ -0,0 +1,13 @@ + +### 3.3 Simple FS 文件系统 + +这里我们没有按照从上到下先讲文件系统抽象层,再讲具体的文件系统。这是由于如果能够理解Simple +FS(简称SFS)文件系统,就可更好地分析文件系统抽象层的设计。即从具体走向抽象。ucore内核把所有文件都看作是字节流,任何内部逻辑结构都是专用的,由应用程序负责解释。但是ucore区分文件的物理结构。ucore目前支持如下几种类型的文件: + +* 常规文件:文件中包括的内容信息是由应用程序输入。SFS文件系统在普通文件上不强加任何内部结构,把其文件内容信息看作为字节。 +* 目录:包含一系列的entry,每个entry包含文件名和指向与之相关联的索引节点(index node)的指针。目录是按层次结构组织的。 +* 链接文件:实际上一个链接文件是一个已经存在的文件的另一个可选择的文件名。 +* 设备文件:不包含数据,但是提供了一个映射物理设备(如串口、键盘等)到一个文件名的机制。可通过设备文件访问外围设备。 +* 管道:管道是进程间通讯的一个基础设施。管道缓存了其输入端所接受的数据,以便在管道输出端读的进程能一个先进先出的方式来接受数据。 + +在lab8中关注的主要是SFS支持的常规文件、目录和链接中的 hardlink 的设计实现。SFS文件系统中目录和常规文件具有共同的属性,而这些属性保存在索引节点中。SFS通过索引节点来管理目录和常规文件,索引节点包含操作系统所需要的关于某个文件的关键信息,比如文件的属性、访问许可权以及其它控制信息都保存在索引节点中。可以有多个文件名可指向一个索引节点。 diff --git a/lab8/lab8_3_4_1_file_dir_interface.md b/lab8/lab8_3_4_1_file_dir_interface.md new file mode 100644 index 0000000..ec9af16 --- /dev/null +++ b/lab8/lab8_3_4_1_file_dir_interface.md @@ -0,0 +1,31 @@ + +#### 3.4.1 file & dir接口 + +file&dir接口层定义了进程在内核中直接访问的文件相关信息,这定义在file数据结构中,具体描述如下: + +``` +struct file { + enum { + FD_NONE, FD_INIT, FD_OPENED, FD_CLOSED, + } status; //访问文件的执行状态 + bool readable; //文件是否可读 + bool writable; //文件是否可写 + int fd; //文件在filemap中的索引值 + off_t pos; //访问文件的当前位置 + struct inode *node; //该文件对应的内存inode指针 + atomic_t open_count; //打开此文件的次数 +}; +``` + +而在kern/process/proc.h中的proc\_struct结构中描述了进程访问文件的数据接口fs\_struct,其数据结构定义如下: + +``` +struct fs_struct { + struct inode *pwd; //进程当前执行目录的内存inode指针 + struct file *filemap; //进程打开文件的数组 + atomic_t fs_count; //访问此文件的线程个数?? + semaphore_t fs_sem; //确保对进程控制块中fs_struct的互斥访问 +}; +``` + +当创建一个进程后,该进程的fs\_struct将会被初始化或复制父进程的fs\_struct。当用户进程打开一个文件时,将从filemap数组中取得一个空闲file项,然后会把此file的成员变量node指针指向一个代表此文件的inode的起始地址。 diff --git a/lab8/lab8_3_4_2_inode_interface.md b/lab8/lab8_3_4_2_inode_interface.md new file mode 100644 index 0000000..7c80aad --- /dev/null +++ b/lab8/lab8_3_4_2_inode_interface.md @@ -0,0 +1,40 @@ + +#### 3.4.2 inode 接口 + +index +node是位于内存的索引节点,它是VFS结构中的重要数据结构,因为它实际负责把不同文件系统的特定索引节点信息(甚至不能算是一个索引节点)统一封装起来,避免了进程直接访问具体文件系统。其定义如下: + +``` +struct inode { + union { //包含不同文件系统特定inode信息的union成员变量 + struct device __device_info; //设备文件系统内存inode信息 + struct sfs_inode __sfs_inode_info; //SFS文件系统内存inode信息 + } in_info; + enum { + inode_type_device_info = 0x1234, + inode_type_sfs_inode_info, + } in_type; //此inode所属文件系统类型 + atomic_t ref_count; //此inode的引用计数 + atomic_t open_count; //打开此inode对应文件的个数 + struct fs *in_fs; //抽象的文件系统,包含访问文件系统的函数指针 + const struct inode_ops *in_ops; //抽象的inode操作,包含访问inode的函数指针 +}; +``` + +在inode中,有一成员变量为in\_ops,这是对此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_getdirentry)(struct inode *node, struct iobuf *iob); + 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); +…… + }; +``` + +参照上面对SFS中的索引节点操作函数的说明,可以看出inode\_ops是对常规文件、目录、设备文件所有操作的一个抽象函数表示。对于某一具体的文件系统中的文件或目录,只需实现相关的函数,就可以被用户进程访问具体的文件了,且用户进程无需了解具体文件系统的实现细节。 diff --git a/lab8/lab8_3_4_fs_abstract.md b/lab8/lab8_3_4_fs_abstract.md new file mode 100644 index 0000000..656bfe3 --- /dev/null +++ b/lab8/lab8_3_4_fs_abstract.md @@ -0,0 +1,4 @@ + +### 3.4 文件系统抽象层 - VFS + +文件系统抽象层是把不同文件系统的对外共性接口提取出来,形成一个函数指针数组,这样,通用文件系统访问接口层只需访问文件系统抽象层,而不需关心具体文件系统的实现细节和接口。 diff --git a/lab8/lab8_3_5_1_data_structure.md b/lab8/lab8_3_5_1_data_structure.md new file mode 100644 index 0000000..9cf49ad --- /dev/null +++ b/lab8/lab8_3_5_1_data_structure.md @@ -0,0 +1,32 @@ + +#### 3.5.1 关键数据结构 + +为了表示一个设备,需要有对应的数据结构,ucore为此定义了struct device,其描述如下: + +``` +struct 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); //用ioctl方式控制设备的函数指针 +}; +``` + +这个数据结构能够支持对块设备(比如磁盘)、字符设备(比如键盘、串口)的表示,完成对设备的基本操作。ucore虚拟文件系统为了把这些设备链接在一起,还定义了一个设备链表,即双向链表vdev\_list,这样通过访问此链表,可以找到ucore能够访问的所有设备文件。 + +但这个设备描述没有与文件系统以及表示一个文件的inode数据结构建立关系,为此,还需要另外一个数据结构把device和inode联通起来,这就是vfs\_dev\_t数据结构: + +``` +// device info entry in vdev_list +typedef struct { + const char *devname; + struct inode *devnode; + struct fs *fs; + bool mountable; + list_entry_t vdev_link; +} vfs_dev_t; +``` + +利用vfs\_dev\_t数据结构,就可以让文件系统通过一个链接vfs\_dev\_t结构的双向链表找到device对应的inode数据结构,一个inode节点的成员变量in\_type的值是0x1234,则此 inode的成员变量in\_info将成为一个device结构。这样inode就和一个设备建立了联系,这个inode就是一个设备文件。 diff --git a/lab8/lab8_3_5_2_stdout_dev_file.md b/lab8/lab8_3_5_2_stdout_dev_file.md new file mode 100644 index 0000000..4c0e92e --- /dev/null +++ b/lab8/lab8_3_5_2_stdout_dev_file.md @@ -0,0 +1,54 @@ + +#### 3.5.2 stdout设备文件 + +**初始化** + +既然stdout设备是设备文件系统的文件,自然有自己的inode结构。在系统初始化时,即只需如下处理过程 + +``` +kern_init-->fs_init-->dev_init-->dev_init_stdout --> dev_create_inode + --> stdout_device_init + --> vfs_add_dev +``` + +在dev\_init\_stdout中完成了对stdout设备文件的初始化。即首先创建了一个inode,然后通过stdout\_device\_init完成对inode中的成员变量inode-\>\_\_device\_info进行初始: + +这里的stdout设备文件实际上就是指的console外设(它其实是串口、并口和CGA的组合型外设)。这个设备文件是一个只写设备,如果读这个设备,就会出错。接下来我们看看stdout设备的相关处理过程。 + +**初始化** + +stdout设备文件的初始化过程主要由stdout\_device\_init完成,其具体实现如下: + +``` +static void +stdout_device_init(struct device *dev) { + dev->d_blocks = 0; + dev->d_blocksize = 1; + dev->d_open = stdout_open; + dev->d_close = stdout_close; + dev->d_io = stdout_io; + dev->d_ioctl = stdout_ioctl; +} +``` + +可以看到,stdout\_open函数完成设备文件打开工作,如果发现用户进程调用open函数的参数flags不是只写(O\_WRONLY),则会报错。 + +**访问操作实现** + +stdout\_io函数完成设备的写操作工作,具体实现如下: + +``` +static int +stdout_io(struct device *dev, struct iobuf *iob, bool write) { + if (write) { + char *data = iob->io_base; + for (; iob->io_resid != 0; iob->io_resid --) { + cputchar(*data ++); + } + return 0; + } + return -E_INVAL; +} +``` + +可以看到,要写的数据放在iob-\>io\_base所指的内存区域,一直写到iob-\>io\_resid的值为0为止。每次写操作都是通过cputchar来完成的,此函数最终将通过console外设驱动来完成把数据输出到串口、并口和CGA显示器上过程。另外,也可以注意到,如果用户想执行读操作,则stdout\_io函数直接返回错误值**-**E\_INVAL。 diff --git a/lab8/lab8_3_5_3_stdin_dev_file.md b/lab8/lab8_3_5_3_stdin_dev_file.md new file mode 100644 index 0000000..e26cf99 --- /dev/null +++ b/lab8/lab8_3_5_3_stdin_dev_file.md @@ -0,0 +1,82 @@ + +#### 3.5.3 stdin 设备文件 + +这里的stdin设备文件实际上就是指的键盘。这个设备文件是一个只读设备,如果写这个设备,就会出错。接下来我们看看stdin设备的相关处理过程。 + +**初始化** + +stdin设备文件的初始化过程主要由stdin\_device\_init完成了主要的初始化工作,具体实现如下: + +``` +static void +stdin_device_init(struct device *dev) { + dev->d_blocks = 0; + dev->d_blocksize = 1; + dev->d_open = stdin_open; + dev->d_close = stdin_close; + dev->d_io = stdin_io; + dev->d_ioctl = stdin_ioctl; + + p_rpos = p_wpos = 0; + wait_queue_init(wait_queue); +} +``` + +相对于stdout的初始化过程,stdin的初始化相对复杂一些,多了一个stdin\_buffer缓冲区,描述缓冲区读写位置的变量p\_rpos、p\_wpos以及用于等待缓冲区的等待队列wait\_queue。在stdin\_device\_init函数的初始化中,也完成了对p\_rpos、p\_wpos和wait\_queue的初始化。 + +**访问操作实现** + +stdin\_io函数负责完成设备的读操作工作,具体实现如下: + +``` +static int +stdin_io(struct device *dev, struct iobuf *iob, bool write) { + if (!write) { + int ret; + if ((ret = dev_stdin_read(iob->io_base, iob->io_resid)) > 0) { + iob->io_resid -= ret; + } + return ret; + } + return -E_INVAL; +} +``` + +可以看到,如果是写操作,则stdin\_io函数直接报错返回。所以这也进一步说明了此设备文件是只读文件。如果此读操作,则此函数进一步调用dev\_stdin\_read函数完成对键盘设备的读入操作。dev\_stdin\_read函数的实现相对复杂一些,主要的流程如下: + +``` +static int +dev_stdin_read(char *buf, size_t len) { + int ret = 0; + bool intr_flag; + local_intr_save(intr_flag); + { + for (; ret < len; ret ++, p_rpos ++) { + try_again: + if (p_rpos < p_wpos) { + *buf ++ = stdin_buffer[p_rpos % stdin_BUFSIZE]; + } + else { + wait_t __wait, *wait = &__wait; + wait_current_set(wait_queue, wait, WT_KBD); + local_intr_restore(intr_flag); + + schedule(); + + local_intr_save(intr_flag); + wait_current_del(wait_queue, wait); + if (wait->wakeup_flags == WT_KBD) { + goto try_again; + } + break; + } + } + } + local_intr_restore(intr_flag); + return ret; +} +``` + +在上述函数中可以看出,如果p\_rpos < p\_wpos,则表示有键盘输入的新字符在stdin\_buffer中,于是就从stdin\_buffer中取出新字符放到iobuf指向的缓冲区中;如果p\_rpos \>=p\_wpos,则表明没有新字符,这样调用read用户态库函数的用户进程就需要采用等待队列的睡眠操作进入睡眠状态,等待键盘输入字符的产生。 + +键盘输入字符后,如何唤醒等待键盘输入的用户进程呢?回顾lab1中的外设中断处理,可以了解到,当用户敲击键盘时,会产生键盘中断,在trap\_dispatch函数中,当识别出中断是键盘中断(中断号为IRQ\_OFFSET + IRQ\_KBD)时,会调用dev\_stdin\_write函数,来把字符写入到stdin\_buffer中,且会通过等待队列的唤醒操作唤醒正在等待键盘输入的用户进程。 diff --git a/lab8/lab8_3_5_dev_file_io_layer.md b/lab8/lab8_3_5_dev_file_io_layer.md new file mode 100644 index 0000000..9659d7d --- /dev/null +++ b/lab8/lab8_3_5_dev_file_io_layer.md @@ -0,0 +1,4 @@ + +### 3.5 设备层文件 IO 层 + +在本实验中,为了统一地访问设备,我们可以把一个设备看成一个文件,通过访问文件的接口来访问设备。目前实现了stdin设备文件文件、stdout设备文件、disk0设备。stdin设备就是键盘,stdout设备就是CONSOLE(串口、并口和文本显示器),而disk0设备是承载SFS文件系统的磁盘设备。下面我们逐一分析ucore是如何让用户把设备看成文件来访问。 diff --git a/lab8/lab8_3_6_labs_steps.md b/lab8/lab8_3_6_labs_steps.md new file mode 100644 index 0000000..df012fc --- /dev/null +++ b/lab8/lab8_3_6_labs_steps.md @@ -0,0 +1,13 @@ + +### 3.6 实验执行流程概述 + +与实验七相比,实验八增加了文件系统,并因此实现了通过文件系统来加载可执行文件到内存中运行的功能,导致对进程管理相关的实现比较大的调整。我们来简单看看文件系统是如何初始化并能在ucore的管理下正常工作的。 + +首先看看kern\_init函数,可以发现与lab7相比增加了对fs\_init函数的调用。fs\_init函数就是文件系统初始化的总控函数,它进一步调用了虚拟文件系统初始化函数vfs\_init,与文件相关的设备初始化函数dev\_init和Simple FS文件系统的初始化函数sfs\_init。这三个初始化函数联合在一起,协同完成了整个虚拟文件系统、SFS文件系统和文件系统对应的设备(键盘、串口、磁盘)的初始化工作。其函数调用关系图如下所示: + +![image](../lab8_figs/image004.png) + +文件系统初始化调用关系图 + +参考上图,并结合源码分析,可大致了解到文件系统的整个初始化流程。vfs\_init主要建立了一个device +list双向链表vdev\_list,为后续具体设备(键盘、串口、磁盘)以文件的形式呈现建立查找访问通道。dev\_init函数通过进一步调用disk0/stdin/stdout\_device\_init完成对具体设备的初始化,把它们抽象成一个设备文件,并建立对应的inode数据结构,最后把它们链入到vdev\_list中。这样通过虚拟文件系统就可以方便地以文件的形式访问这些设备了。sfs\_init是完成对Simple FS的初始化工作,并把此实例文件系统挂在虚拟文件系统中,从而让ucore的其他部分能够通过访问虚拟文件系统的接口来进一步访问到SFS实例文件系统。 diff --git a/lab8/lab8_3_7_1_file_open.md b/lab8/lab8_3_7_1_file_open.md new file mode 100644 index 0000000..bc3191c --- /dev/null +++ b/lab8/lab8_3_7_1_file_open.md @@ -0,0 +1,36 @@ + +#### 3.7.1 打开文件 + +有了上述分析后,我们可以看看如果一个用户进程打开文件会做哪些事情?首先假定用户进程需要打开的文件已经存在在硬盘上。以user/sfs\_filetest1.c为例,首先用户进程会调用在main函数中的如下语句: + +``` +int fd1 = safe_open("/test/testfile", O_RDWR | O_TRUNC); +``` + +从字面上可以看出,如果ucore能够正常查找到这个文件,就会返回一个代表文件的文件描述符fd1,这样在接下来的读写文件过程中,就直接用这样fd1来代表就可以了。那这个打开文件的过程是如何一步一步实现的呢? + +**通用文件访问接口层的处理流程** + +首先进入通用文件访问接口层的处理流程,即进一步调用如下用户态函数: open-\>sys\_open-\>syscall,从而引起系统调用进入到内核态。到了内核态后,通过中断处理例程,会调用到sys\_open内核函数,并进一步调用sysfile\_open内核函数。到了这里,需要把位于用户空间的字符串"/test/testfile"拷贝到内核空间中的字符串path中,并进入到文件系统抽象层的处理流程完成进一步的打开文件操作中。 + +**文件系统抽象层的处理流程** + +1. 分配一个空闲的file数据结构变量file在文件系统抽象层的处理中,首先调用的是file\_open函数,它要给这个即将打开的文件分配一个file数据结构的变量,这个变量其实是当前进程的打开文件数组current-\>fs\_struct-\>filemap[]中的一个空闲元素(即还没用于一个打开的文件),而这个元素的索引值就是最终要返回到用户进程并赋值给变量fd1。到了这一步还仅仅是给当前用户进程分配了一个file数据结构的变量,还没有找到对应的文件索引节点。 + +为此需要进一步调用vfs\_open函数来找到path指出的文件所对应的基于inode数据结构的VFS索引节点node。vfs\_open函数需要完成两件事情:通过vfs\_lookup找到path对应文件的inode;调用vop\_open函数打开文件。 + +2. 找到文件设备的根目录“/”的索引节点需要注意,这里的vfs\_lookup函数是一个针对目录的操作函数,它会调用vop\_lookup函数来找到SFS文件系统中的“/test”目录下的“testfile”文件。为此,vfs\_lookup函数首先调用get\_device函数,并进一步调用vfs\_get\_bootfs函数(其实调用了)来找到根目录“/”对应的inode。这个inode就是位于vfs.c中的inode变量bootfs\_node。这个变量在init\_main函数(位于kern/process/proc.c)执行时获得了赋值。 + +3. 找到根目录“/”下的“test”子目录对应的索引节点,在找到根目录对应的inode后,通过调用vop\_lookup函数来查找“/”和“test”这两层目录下的文件“testfile”所对应的索引节点,如果找到就返回此索引节点。 + +4. 把file和node建立联系。完成第3步后,将返回到file\_open函数中,通过执行语句“file-\>node=node;”,就把当前进程的current-\>fs\_struct-\>filemap[fd](即file所指变量)的成员变量node指针指向了代表“/test/testfile”文件的索引节点node。这时返回fd。经过重重回退,通过系统调用返回,用户态的syscall-\>sys\_open-\>open-\>safe\_open等用户函数的层层函数返回,最终把把fd赋值给fd1。自此完成了打开文件操作。但这里我们还没有分析第2和第3步是如何进一步调用SFS文件系统提供的函数找位于SFS文件系统上的“/test/testfile”所对应的sfs磁盘inode的过程。下面需要进一步对此进行分析。 + +**SFS文件系统层的处理流程** + +这里需要分析文件系统抽象层中没有彻底分析的vop\_lookup函数到底做了啥。下面我们来看看。在sfs\_inode.c中的sfs\_node\_dirops变量定义了“.vop\_lookup = sfs\_lookup”,所以我们重点分析sfs\_lookup的实现。 + +sfs\_lookup有三个参数:node,path,node\_store。其中node是根目录“/”所对应的inode节点;path是文件“testfile”的绝对路径“/test/testfile”,而node\_store是经过查找获得的“testfile”所对应的inode节点。 + +Sfs\_lookup函数以“/”为分割符,从左至右逐一分解path获得各个子目录和最终文件对应的inode节点。在本例中是分解出“test”子目录,并调用sfs\_lookup\_once函数获得“test”子目录对应的inode节点subnode,然后循环进一步调用sfs\_lookup\_once查找以“test”子目录下的文件“testfile1”所对应的inode节点。当无法分解path后,就意味着找到了testfile1对应的inode节点,就可顺利返回了。 + +当然这里讲得还比较简单,sfs\_lookup\_once将调用sfs\_dirent\_search\_nolock函数来查找与路径名匹配的目录项,如果找到目录项,则根据目录项中记录的inode所处的数据块索引值找到路径名对应的SFS磁盘inode,并读入SFS磁盘inode对的内容,创建SFS内存inode。 diff --git a/lab8/lab8_3_7_2_file_read.md b/lab8/lab8_3_7_2_file_read.md new file mode 100644 index 0000000..d15d804 --- /dev/null +++ b/lab8/lab8_3_7_2_file_read.md @@ -0,0 +1,40 @@ + +#### 3.7.2 读文件 + +读文件其实就是读出目录中的目录项,首先假定文件在磁盘上且已经打开。用户进程有如下语句: + +``` +read(fd, data, len); +``` + +即读取fd对应文件,读取长度为len,存入data中。下面来分析一下读文件的实现。 + +**通用文件访问接口层的处理流程** + +先进入通用文件访问接口层的处理流程,即进一步调用如下用户态函数:read-\>sys\_read-\>syscall,从而引起系统调用进入到内核态。到了内核态以后,通过中断处理例程,会调用到sys\_read内核函数,并进一步调用sysfile\_read内核函数,进入到文件系统抽象层处理流程完成进一步读文件的操作。 + +**文件系统抽象层的处理流程** + +1) 检查错误,即检查读取长度是否为0和文件是否可读。 + +2) 分配buffer空间,即调用kmalloc函数分配4096字节的buffer空间。 + +3) 读文件过程 + +[1] 实际读文件 + +循环读取文件,每次读取buffer大小。每次循环中,先检查剩余部分大小,若其小于4096字节,则只读取剩余部分的大小。然后调用file\_read函数(详细分析见后)将文件内容读取到buffer中,alen为实际大小。调用copy\_to\_user函数将读到的内容拷贝到用户的内存空间中,调整各变量以进行下一次循环读取,直至指定长度读取完成。最后函数调用层层返回至用户程序,用户程序收到了读到的文件内容。 + +[2] file\_read函数 + +这个函数是读文件的核心函数。函数有4个参数,fd是文件描述符,base是缓存的基地址,len是要读取的长度,copied\_store存放实际读取的长度。函数首先调用fd2file函数找到对应的file结构,并检查是否可读。调用filemap\_acquire函数使打开这个文件的计数加1。调用vop\_read函数将文件内容读到iob中(详细分析见后)。调整文件指针偏移量pos的值,使其向后移动实际读到的字节数iobuf\_used(iob)。最后调用filemap\_release函数使打开这个文件的计数减1,若打开计数为0,则释放file。 + +**SFS文件系统层的处理流程** + +vop\_read函数实际上是对sfs\_read的包装。在sfs\_inode.c中sfs\_node\_fileops变量定义了.vop\_read = sfs\_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函数中,先计算一些辅助变量,并处理一些特殊情况(比如越界),然后有sfs\_buf\_op = sfs\_rbuf,sfs\_block\_op = sfs\_rblock,设置读取的函数操作。接着进行实际操作,先处理起始的没有对齐到块的部分,再以块为单位循环处理中间的部分,最后处理末尾剩余的部分。每部分中都调用sfs\_bmap\_load\_nolock函数得到blkno对应的inode编号,并调用sfs\_rbuf或sfs\_rblock函数读取数据(中间部分调用sfs\_rblock,起始和末尾部分调用sfs\_rbuf),调整相关变量。完成后如果offset + alen \> din-\>fileinfo.size(写文件时会出现这种情况,读文件时不会出现这种情况,alen为实际读写的长度),则调整文件大小为offset + alen并设置dirty变量。 + +sfs\_bmap\_load\_nolock函数将对应sfs\_inode的第index个索引指向的block的索引值取出存到相应的指针指向的单元(ino\_store)。它调用sfs\_bmap\_get\_nolock来完成相应的操作。sfs\_rbuf和sfs\_rblock函数最终都调用sfs\_rwblock\_nolock函数完成操作,而sfs\_rwblock\_nolock函数调用dop\_io-\>disk0\_io-\>disk0\_read\_blks\_nolock-\>ide\_read\_secs完成对磁盘的操作。 diff --git a/lab8/lab8_3_7_file_op_implement.md b/lab8/lab8_3_7_file_op_implement.md new file mode 100644 index 0000000..c0dfee6 --- /dev/null +++ b/lab8/lab8_3_7_file_op_implement.md @@ -0,0 +1,2 @@ + +### 3.7 文件操作实现 diff --git a/lab8/lab8_3_fs_design_implement.md b/lab8/lab8_3_fs_design_implement.md new file mode 100644 index 0000000..dff1679 --- /dev/null +++ b/lab8/lab8_3_fs_design_implement.md @@ -0,0 +1,2 @@ + +## 3. 文件系统设计与实现 diff --git a/lab8/lab8_4_lab_requirement.md b/lab8/lab8_4_lab_requirement.md new file mode 100644 index 0000000..1d2e9fd --- /dev/null +++ b/lab8/lab8_4_lab_requirement.md @@ -0,0 +1,7 @@ + +## 4. 实验报告要求 + + +从网站上下载lab8.zip后,解压得到本文档和代码目录lab8,完成实验中的各个练习。完成代码编写并检查无误后,在对应目录下执行 make handin 任务,即会自动生成lab8-handin.tar.gz。最后请一定提前或按时提交到网络学堂上。 + +注意有“LAB8”的注释,这是需要主要修改的内容。代码中所有需要完成的地方challenge除外)都有“LAB8”和“YOUR CODE”的注释,请在提交时特别注意保持注释,并将“YOUR CODE”替换为自己的学号,并且将所有标有对应注释的部分填上正确的代码。 diff --git a/lab8/image001.png b/lab8_figs/image001.png similarity index 100% rename from lab8/image001.png rename to lab8_figs/image001.png diff --git a/lab8/image002.png b/lab8_figs/image002.png similarity index 100% rename from lab8/image002.png rename to lab8_figs/image002.png diff --git a/lab8/image003.png b/lab8_figs/image003.png similarity index 100% rename from lab8/image003.png rename to lab8_figs/image003.png diff --git a/lab8/image004.png b/lab8_figs/image004.png similarity index 100% rename from lab8/image004.png rename to lab8_figs/image004.png