- 作者: 雪山肥鱼
- 时间:20210628 06:59
- 目的:初探文件系统(简单版)的实现
# 思考方式
# 文件结构总览
# Inode
# Directory
# 空闲空间管理
# 文件的访问
## 读取流程
## 写入流程
# cache and buffer
## 绕过cache 和 buffer的方式
文件系统是纯软件的,并不像CPU和内存有硬件属性。所以文件系统的工作机制不必关心硬件特性。
- 如何实现一个简单的文件系统
- 需要什么样的磁盘结构
- 文件系统需要跟踪什么
- 如何被访问的
思考的方式
- 数据结构
用什么样子的数据结构去保存 文件的 metadata 和 文件的内容 - 访问的方式
open、read、write 函数是如何访问数据的。这些步骤执行的效率如何?
以上的思考要变成一种 mental model.
结构总览
假设我们这个简单的文件系统有 64个 blocks,每个 block 4kb:
8 -> 63 blocks 存储数据 即 data region.
需要用inode去追踪每一个文件。假设每个inode 占256个字节,那么每个block 可以存储16个 inode,即16个文件的信息。那么,依照上图可知,5个 blocks 可以管办理16个 inode。
这个5个blocks 成为 inodetable
那么引出问题,如何查看哪些inode可用,哪些data可用呢?
可以用 free-list 管理,当然最流行的是 bitmap 管理。
- data bimap
-
inode bitmap
位表 ,bit 1 则已经占用,bit 0 则未占用。
超块内容:包含了文件系统的信息,如:
- inode 信息, 可以管理多少个文件
- data block 信息,有多少个data block
- inode table 和 data table 从哪里开始
- 文件系统的魔法数
当挂在文件系统的适合,OS 会首先读 超块去初始化不同的参数。然后将该卷挂在文件系统树上。当卷中的文件被访问时,系统就会知道在哪里查找所需磁盘上的结构。
Inode
文件系统最终要的磁盘结构之一就是 inode, 即 index node. 从名字可以看出来,所有的node都放在一个数组中,取index,即可拿到node。
Inode 是一个 文件的low-level name,当给OS一个inode,OS 就能直接算出来 inode在disk的相应位置。
举例以下结构:
- inodes block: 20KB (12KB -> 32Kb)-> per inode 256bytes -> 80个inode -> 管理80个文件。
- Super block :0-4kb 1个block
- i bmap : 4 kb -> 8kb 1个 block
- d bmap: 8kb -> 12kb 1 个block
我们只要知道一点,如果在这个vsfs 中,我给出inode 32, 那么我就知道这个inode 处于哪个 扇区。从而找到inode 信息就完事儿了。
为了支持更多更大的文件,inode 结构体中会有一个 indirect pointer 的特殊指针。指向包含更多指针的块。(可以理解为一个4kb 的block 全是指针,指向数据。)
目录组织
目录自己在底层的标识 是 {条目名称, inode号}
上图是 dir 在磁盘的数据,即在 data blocks 中的数据,每个条目的inode, 长度,字符串长度等。
万物皆文件,只不过文件系统将目录视为特殊的文件,并非 regular files 啦,而是directory 类型
寻找目录流程:
- 目录必有indoe, 位于inode表中某处
- inode表中 inode, 被标记为directroy 类型。
- 数据区的数据块存储 目录中的结构。
空闲空间管理
在我们上述讨论的vsfs 中,用过两个简单的位图来管理空闲空间。
- 创建文件,分配inode
- 所搜空闲inode位图,分配给该文件。
- 在 inode 位图中打上标记,1
- 更新data bimap,分配数据块
文件的访问: 读取和写入
这一小节让你彻底明白为什么读写文件时相当耗时的。
从磁盘中读取文件
操作,打开/foo/bar,读取它,然后关闭它。
- open("/foo/bar", O_RDONLY); 系统调用,首先要找到inode,那就必须从跟路径开始找
1.1 root inode 是已知的,找inode后 取 root 的 data block里读取数据。
1.2 查到foo的inode - 读foo 的 inode
2.1读foo目录中的 data block 数据,找到 bar的inode
3.读bar 的 inode。 - 因为在step3中已经拿到了bar的inode,通过数据块指针找到对应的block 继续read.
- 因为文件占了 3个 数据块,所以read 3次。
读取不会访问分配结构
整个过程不会用到位图哟,位图的使用只会在分配的适合才会查询。
写文件流程
写文件流程会相对复杂一些,因为涉及目录的update.
创建新文件并写入内容:
写文件会涉及块的分配,不仅要写数据,还要决定哪个块分配给文件。
总体上会涉及5次I/O
- 读data map 哪些bit 可以写数据
- 写data map
- read inode 找到哪个inode可以用
- 写入 inode(新块的位置更新)
- 写数据本身
流程总结如下:
- 通过读写 找到 /root/foo
- 读inode bitmap , 找到空闲inode
- 写 inode bitmap 表示已经被bar文件使用
- 写/root/foo 在 data block 中的 bar inode信息,
- 读bar 的inode,
- 写bar 的 inode(更新 自己的 block信息等)
- 因为创建了 bar, 所以要 update foo 的inode.
- 这时候才真正的写文件,写之前要查data 的 bitmap, 进行三次读写。
由此可以看出,即便是最简单的操作,一会产生大量I/O操作,分散在磁盘上,文件系统可以做些什么来降低执行I/O 的高成本呢?
cache and buffer
由上述可知,I/O 是昂贵的。会引发很多磁盘I/O,是一个很严重的性能问题。大多数文件系统会使用 DRAM来缓存重要的block。
可以对目录层次进行缓存。下次就不用遍历查找啦。也只会在第一次打开文件会产生I/O,随后打开同一个文件会命中缓存。
缓存并不会减少写入的流量。可以将内容先写入缓冲buffer, 通过延迟写入,文件可以将buffer内容放入一组较小的I/O中。由OS决定何时写入。
如果想要直接写入文件呢?方法可以有以下几种
- fsync()
- 绕过缓存,通过 direct I/O 接口直接写, 避开 缓冲
- 使用 原始磁盘 raw disk 接口。 完全避免使用文件系统。