实验八:文件系统
一、 实验目的
• 了解基本的文件系统系统调用的实现方法;
• 了解一个基于索引节点组织方式的Simple FS文件系统的设计与实现;
• 了解文件系统抽象层-VFS的设计与实现;
二、 实验任务
实验七完成了在内核中的同步互斥实验。
本次实验涉及的是文件系统,通过分析了解ucore文件系统的总体架构设计,完善读写文件操作,从新实现基于文件系统的执行程序机制(即改写do_execve
),从而可以完成执行存储在磁盘上的文件和实现文件读写等功能。
三、 实验准备
1. 文件系统架构
- 通用文件系统访问接口层:该层提供了一个从用户空间到文件系统的标准访问接口。这一层访问接口让应用程序能够通过一个简单的接口获得ucore内核的文件系统服务。
- 文件系统抽象层:向上提供一个一致的接口给内核其他部分(文件系统相关的系统调用实现模块和其他内核功能模块)访问。向下提供一个同样的抽象函数指针列表和数据结构屏蔽不同文件系统的实现细节。
- Simple FS文件系统层:一个基于索引方式的简单文件系统实例。向上通过各种具体函数实现以对应文件系统抽象层提出的抽象函数。向下访问外设接口
- 外设接口层:向上提供device访问接口屏蔽不同硬件细节。向下实现访问各种具体设备驱动的接口,比如disk设备接口/串口设备接口/键盘设备接口等。
应用程序操作文件(打开/创建/删除/读写),首先需要通过文件系统的通用文件系统访问接口层给用户空间提供的访问接口进入文件系统内部,接着由文件系统抽象层把访问请求转发给某一具体文件系统(比如SFS文件系统),具体文件系统(Simple FS文件系统层)把应用程序的访问请求转化为对磁盘上的block的处理请求,并通过外设接口层交给磁盘驱动例程来完成具体的磁盘操作。
这里我们可以通过下图可以比较好的理解这四个部分的关系:
(图片引自实验指导书)
2. 数据结构
从ucore操作系统不同的角度来看,ucore中的文件系统架构包含四类主要的数据结构, 它们分别是:
- 超级块(SuperBlock),它主要从文件系统的全局角度描述特定文件系统的全局信息。它的作用范围是整个OS空间。
- 索引节点(inode):它主要从文件系统的单个文件的角度它描述了文件的各种属性和数据所在位置。它的作用范围是整个OS空间。
- 目录项(dentry):它主要从文件系统的文件路径的角度描述了文件路径中的特定目录。它的作用范围是整个OS空间。
- 文件(file),它主要从进程的角度描述了一个进程在访问文件时需要了解的文件标识,文件读写的位置,文件引用情况等信息。它的作用范围是某一具体进程。
3. 相关函数改进
在实验指导书的练习0中提到了注意点如下:
注意:为了能够正确执行lab6的测试应用程序,可能需对已完成的实验1/2/3/4/5/6/7的代码进行进一步改进。
根据试验要求,我们需要对部分代码进行改进,这个实验的改进的地方的提示(update)一直没有找到,后来上网上看了学长的博客发现没有需要改进的,直接用就行,需要做的就是练习1、2的编程部分。
四、 实验步骤
(一) 练习0:填写已有实验
lab8会依赖lab1lab7,我们需要把做的lab1lab7的代码填到lab8中缺失的位置上面。练习0 就是—个工具的利用。这里我使用的是linux的Meld工具。和lab7操作流程—样,我们只需要将已经完成的lab7与待完成的lab8分别导入进来,然后点击compare即可,详细细节不再赘述。需要修改的主要是以下六个文件:proc.c、default_pmm.c、pmm.c、swap_fifo.c 、vmm.c、trap.c、sche.c
练习0提到的对已做实验的改进已经在上面的准备部分完成。
(二) 练习1: 完成读文件操作的实现
要求是首先了解打开文件的处理流程,然后参考本实验后续的文件读写操作的过程分析,编写在sfs_inode.c 中 sfs_io_nolock
读文件中数据的实现代码。
1. 打开文件处理大致流程
上面已经提到了ucore操作系统的四类主要数据结构和文件系统架构,下面根据它们详细的观察打开文件处理流程。
文件系统,会将磁盘上的文件(程序)读取到内存里面来,在用户空间里面变成进程去进—步执行或其他操作。通过—系列系统调用完成这个过程。
ucore 文件系统中,是这样处理读写硬盘操作的:
(图片截取自清华大学操作系统课程学堂在线)
① 首先是应用程序发出请求,请求硬盘中写数据或读数据,应用程序通过 FS syscall 接口执行系统调用,获得 ucore操作系统关于文件的—些服务;
② 之后,—旦操作系统内系统调用得到了请求,就会到达 VFS层面(虚拟文件系统),包含很多部分比如文件接口、目录接口等,是—个抽象层面,它屏蔽底层具体的文件系统;
③ VFS 如果得到了处理,那么VFS 会将这个 iNode 传递给 SimpleFS,注意,此时,VFS 中的iNode 还是—个抽象的结构,在 SimpleFS中会转化为—个具体的 iNode;
④ 通过该 iNode 经过 IO 接口对千磁盘进行读写。
而硬盘中的布局信息存在SFS中,如图所示:
在本实验中,第三个磁盘(即disk0,前两个磁盘分别是 ucore.img 和 swap.img)用于存放一个SFS文件系统(Simple Filesystem)。通常文件系统中,磁盘的使用是以扇区(Sector)为单位的,但是为了实现简便,SFS 中以 block (4K,与内存 page 大小相等)为基本单位。
2. 基于ucore代码具体分析
2.1. 磁盘文件布局
SFS文件系统定义在kern/fs/sfs/sfs.h
:
SFS 的前 3 项对应的就是硬盘文件布局的全局信息
,对它们进行分析:
-
第0个块(4K)是超级块(superblock),它包含了关于文件系统的所有关键参数,
当计算机被启动或文件系统被首次接触时,超级块的内容就会被装入内存。其定义如下:
包含一个成员变量魔数magic,其值为0x2f8dbe2a,内核通过它来检查磁盘镜像是否是合法的 SFS img;成员变量blocks记录了SFS中所有block的数量,即 img 的大小;成员变量unused_block记录了SFS中还没有被使用的block的数量;成员变量info包含了字符串"simple file system"。
-
第1个块放了一个root-dir的inode,用来记录根目录的相关信息,分析根目录结构:
inode 是从文件系统的单个文件的角度它描述了文件的各种属性和数据所在位置,相当于—个索引,而 root_dir 是—个根目录索引,根目录表示,我们—开始访问这个文件系统可以看到的目录信息。主要关注 direct 和 indirect,代表根目录下的直接索引和间接索引。
-
从第2个块开始,根据SFS中所有块的数量,用1个bit来表示一个块的占用和未被占用
的情况。这个区域称为SFS的freemap区域,这将占用若干个块空间。最后在剩余的磁盘空间中,存放了所有其他目录和文件的inode信息和内容数据信息。
目录项 entry,kern/fs/sfs/sfs.h
:
数组中存放的是文件的名字,ino 是该文件的 inode 值。
至此硬盘文件布局已经清楚了,SFS的inode(存储在list_entry_t双向链表中)结点就是对于硬件的实际索引。
2.2. 虚拟文件系统VFS
文件系统抽象层是把不同文件统的对外共性接口提取出来,形成—个函数指针数组,这样,通用文件系统访问接口层只需访问文件系统抽象层,而不需关心具体文件系统的实现细节和接口。
SFS 是—个在硬盘之上的抽象,它还需要传递上—层过来的索引值INODE。
在 VFS 层中,我们需要对虚拟的 iNode,和下—层的 SFS 的 iNode 进行对接。
file&dir接口层定义了进程在内核中直接访问的文件相关信息,这定义在file数据结构中,具体描述如下:
(1) 文件结构,kern/vfs/file.c
在 file 基础之上还有—个管理所有 file 的数据结构 file_struct:
(2) VFS的inode结构
inode 数据结构是位于内存的索引节点,把不同文件系统的特定索引节点信息(甚至不能算是—个索引节点)统—封装起来,避免了进程直接访问具体文件系统。
我们看到在 VFS 层面的 iNode 值,包含了 SFS 和硬件设备 device 的情况。
(3) inode的操作函数指针
仅截取前四个open、close、read、write;
inode_ops 是对常规文件、目录、设备文件所有操作的—个抽象函数表示。对某—具体的文件系统中的文件或目录,只需实现相关的函数,就可以被用户进程访问具体的文件了,且用户进程无需了解具体的实现细节。
3. 函数编码
接下来通过分析文件打开的步骤来分析我们需要编码的sfs_io_nolock函数;
假定用户进程需要打开的文件已经存在在硬盘上。
以 user/sfs_filetest1.c 为例,首先用户进程会调用在 main 函数中的如下语句:
int fd1 = safe_open("/test/testfile", O_RDWR | O_TRUNC);
(1) 通用文件访问接口层处理:
首先进入通用文件访问接口层的处理流程,即进—步调用如下用户态函数:open->sys_open->syscall
,从而引起系统调用进入到内核态。到了内核态后,通过中断处理例程,会调用到 sys_open内核函数,并进—步调用 sysfile_open 内核函数。到了这里,需要把位千用户空间的字符串”/test/testfile” 拷贝到内核空间中的字符串 path 中,并进入到文件系统抽象层的处理流程完成进—步的打开文件操作中。
(图片取自学堂在线ucore教学)
(2) 文件系统VFS处理过程
-
分配—个空闲的 file 数据结构变量 file 在文件系统抽象层的处理中,首先调用的是 file_open 函数, 它要给这个即将打开的文件分配—个 file 数据结构的变量,这个变量其实是当前进程的打开文件数组current->fs_struct->filemap[] 中的—个空闲元素(即还没用于—个打开的文件),而这个元素的索引值就是最终要返回到用户进程并赋值给变量 fd1。到了这—步还仅仅是给当前用户进程分配了—个 file 数据结构的变量,还没有找到对应的文件索引节点。
-
为此需要进—步调用 vfs_open 函数来找到 path 指出的文件所对应的基于 inode 数据结构的 VFS 索引节点 inode。 vfs_open 函数需要完成两件事情:通过 vfs_lookup 找到 path 对应文件的 inode;调用vop_open 函数打开文件。
-
找到文件设备的根目录/的索引节点需要注意,这里的 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)执行时获得了赋值。
-
找到根目录/下的test子目录对应的索引节点,在找到根目录对应的inode后,通过调用vop_lookup函数来查找/和test这两层目录下的文件testfile所对应的索引节点,如果找到就返回此索引节点。
-
把 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。自此完成了打开文件操作。
在此需要进一步分析sfs_lookup
函数:
看到函数传入的三个参数,其中 node 是根目录 “/” 所对应的 inode 节点;path 是文件的绝对路径(例如 “/test/file”),而 node_store 是经过查找获得的file所对应的inode节点。
函数以 “/” 为分割符,从左至右逐—分解path获得各个子目录和最终文件对应的 inode 节点。在本例中是分解出 “test” 子目录,并调用sfs_lookup_once函数获得“test”子目录对应的 inode 节点 subnode, 然后循环进—步调用 sfs_lookup_once 查找以 “test” 子目录下的文件 “testfile1” 所对应的 inode 节点。当无法分解 path 后,就意昧着找到了 testfile1 对应的 inode 节点,就可顺利返回了。
而sfs_lookup_once函数就调用了我们需要补充的函数sfs_io_nolock,下面对它进行分析填充;
(3) sfs_io_nolock函数填充
每次通过 sfs_bmap_load_nolock 函数获取文件索引编号,然后调用 sfs_buf_op 完成实际的文件读写操作。blkno 就是文件开始块的位置,nblks 是文件的大小。
4. 问题简述
给出设计实现”UNIX的PIPE机制“的概要设方案,鼓励给出详细设计方案
管道(pipe):
管道可用于具有亲缘关系进程间的通信,有名管道克服了管道没有名字的限制,因此,除具有管道所具有的功能外,它还允许无亲缘关系进程间的通信;
管道机制:
管道是由内核管理的一个缓冲区,相当于我们放入内存中的一个纸条。管道的一端连接一个进程的输出。这个进程会向管道中放入信息。管道的另一端连接一个进程的输入,这个进程取出被放入管道的信息。一个缓冲区不需要很大,它被设计成为环形的数据结构,以便管道可以被循环利用。当管道中没有信息的话,从管道中读取的进程会等待,直到另一端的进程放入信息。当管道被放满信息的时候,尝试放入信息的进程会等待,直到另一端的进程取出信息。当两个进程都终结的时候,管道也自动消失。
主要函数:
管道读函数pipe_read()和管道写函数pipe_wrtie()
管道写函数通过将字节复制到 VFS 索引节点指向的物理内存而写入数据,而管道读函数则通过复制物理内存中的字节而读出数据。当然,内核必须利用一定的机制同步对管道的访问,为此,内核需要使用锁、等待队列和信号。
实现:
管道的实现并没有使用专门的数据结构,而是借助了文件系统的file结构和VFS的索引节点inode。通过将两个 file 结构指向同一个临时的 VFS 索引节点,而这个 VFS 索引节点又指向一个物理页面而实现,如下图:
当写进程向管道中写入时,它利用标准的库函数write(),系统根据库函数传递的文件描述符,可找到该文件的 file 结构。file 结构中指定了用来进行写操作的函数(即写入函数)地址,于是,内核调用该函数完成写操作。写入函数在向内存中写入数据之前,必须首先检查 VFS 索引节点中的信息,进行实际的内存复制工作;
写入函数首先锁定内存,然后从写进程的地址空间中复制数据到内存。否则,写入进程就休眠在 VFS 索引节点的等待队列中,接下来,内核将调用调度程序,而调度程序会选择其他进程运行。写入进程实际处于可中断的等待状态,当内存中有足够的空间可以容纳写入数据,或内存被解锁时,读取进程会唤醒写入进程,这时,写入进程将接收到信号。当数据写入内存之后,内存被解锁,而所有休眠在索引节点的读取进程会被唤醒。
管道的读取过程和写入过程类似。
(三) 练习2: 完成基于文件系统的执行程序机制的实现
改写proc.c中的load_icode函数和其他相关函数,实现基千文件系统的执行程序机制。执行:make qemu。如果能看看到sh用户程序的执行界面,则基本成功了。如果在sh用户界面上可以执行”ls”,”hello”等其他放置在sfs文件系统中的其他执行程序,则可以认为本实验基本成功。
1. 改写相关函数
在 Lab 7 的基础上进行修改,读 elf 文件变成从磁盘上读,而不是直接在内存中读。
(1) alloc_proc
函数
在 proc.c 中,根据注释我们需要先初始化 fs 中的进程控制结构,即在 alloc_proc 函数中我们需要做—下修改,加上—句 proc->filesp = NULL; 从而完成初始化。—个文件需要在 VFS 中变为—个进程才能被执行。
(2) load_icode
函数
主要修改第(3)部分,通过load_icode_read进行特定文件elf头读取;
其余的读取elf的部分不需要改变,因为elf的组成不变。第3部分的将文件逐个段加载到内存中,这里要注意设置虚拟地址与物理地址之间的映射。
2. 问题分析
请在实验报告中给出设计实现基千“UNIX的硬链接和软链接机制”的概要设方案,鼓励给出详细设计方案;
2.1. UNIX的硬链接和软链接机制:
简单的说连接就是可以指向文件系统中其他位置的一个快捷方式,它非常有用,可以避免键入很长的路径名或cd深入到多个文件夹中,用过Windows系统桌面的程序图标其实就是一个快捷方式,大家也可以这么理解,不过两者却存在某些重大的差别,在unix中的连接形式分为两种,分别为硬链接和软链接。
unix硬链接:
硬链接是一个目录条目,它指具有同一个i-node(硬盘上的物理位置)的另一个文件。事实上只存在一个文件,指向硬盘上同一个物理数据的只有两虞多个目录条目。UNIX软链接:
UNIX软链接也称符号连接或symlinks,相当于Windows系统中的快捷方式。和硬链接不同的是,软链接是一个独立的文件,在硬件上有属于自己的i-node。软链接只是一个文件,其中包含指向另一个文件的指针。
2.2. 实现方案
观察到保存在磁盘上的 inode 信息均存在—个 nlinks 变量用千表示当前文件的被链接的计数,因而支持实现硬链接和软链接机制;
- 如果在磁盘上创建—个文件 A 的软链接 B,那么将 B 当成正常的文件创建 inode,然后将 TYPE 域设置为链接,然后使用剩余的域中的—个,指向 A 的 inode 位置,然后再额外使用—个位来标记当前的链接是软链接还是硬链接;
- 当访问到文件 B(read,write 等系统调用),判断如果 B 是—个链接,则实际是将对B指向的文件A(已经知道了 A 的 inode 位置)进行操作;
- 当删除—个软链接 B 的时候,直接将其在磁盘上的 inode 删掉即可;
- 如果在磁盘上的文件 A 创建—个硬链接 B,那么在按照软链接的方法创建完 B 之后,还需要将 A中的被链接的计数加 1;
- 访问硬链接的方式与访问软链接是—致的;当删除—个硬链接B的时候,除了需要删除掉 B 的 inode 之外,还需要将 B 指向的文件 A 的被链接计数减 1,如果减到了 0,则需要将 A 删除掉;
3. 运行结果
这是最后一次实验,因此将make qemu的结果大概都截取出来,并且再次分析lab1~lab8的运行结果;
首先,调用堆栈的变化过程如上图的ebp变化所示,这表明lab1中完成kdebug.c中函数print_stackframe的实现基本正确。
因为这里我们将lab1中的每1秒会输出一次”100 ticks”的时钟中断处理的部分,即填写trap函数中处理时钟中断的部分给变成了后续所需要的检查进程的执行次数(或是定时器相关),因此不会像lab1那样输出ticks++;
说明lab1的软件启动过程基本正确;
接着,可以看到check_alloc()、check_pgdir()、check_boot_pgdir()
succeeded!说明first_fit连续物理内存分配算法是正确实现了的与虚拟地址对应的页表项寻找完成、释放虚地址所在页以及映射的取消实现。
说明lab2的物理内存管理基本正确;
其次,中间的kmalloc_init()、check_vma_struct()、check_vmm()
succeeded的输出分别代表了lab3虚拟内存管理中的给未被映射的地址映射上物理页、补充完成基于FIFO的页面替换算法的实现;
说明lab3的虚拟内存管理基本正确;
check_swap()
succeed的输出说明lab4的分配并初始化一个进程控制块、为新创建的内核线程分配分配资源、proc_run
函数的进程切换工作都正常实现。
中间还有很多关于哲学家问题的输出,不再赘述。
这里的”I am No.0 philosopher_sema”、“Iter 1, No.0 philosopher_sema is thinking”的类似输出以及”No.4 philosopher_condvar quit”说明lab7的同步互斥基本上实现成功。
这里的ls指令输出了进程的文件目录,hello指令直接运行了hello文件,说明这里lab8的实现也基本正确了。
五、 实验总结
本次实验主要是完成文件系统在ucore的实现,可以说本次实验是非常困难的,难于理解,ucore将文件系统用一次实验全部实现也挺不合理的,说实话lab8我并没有完全理解,涉及的东西太多了,在网上找了好多资料然后又看了很多的博客,甚至直接到ucore的学堂在线看视频才勉强看懂。
在学期的最后太多的事情要去赶了,所以很遗憾lab8的实验基本上是靠着答案和博客的代码写的,不过多少还算是知道了一些简单的东西,像是ucore的文件系统架构、数据结构啥的,也算是将书本上的文件系统有关知识和ucore的SFS实践相结合了。
无论如何,这学期的操作系统实验就结束了, 收获很大,付出的时间也都有所回报。