记录从上层用户操作到底层文件系统之间的调用流程以及数据流。
simplefs 实战
[simplefs][https://github.com/psankar/simplefs] 用最少的代码实现了文件系统的基本操作。
基本体验
创建一个硬盘
目前使用 dd
命令创建一个块大小为 4096字节,共100个块的硬盘文件。
1 | bs 指定一个块的大小 |
格式化并挂载
1 | make |
查看内容
接下来就是以root的身份进入到 mount
文件夹,便可以查看其文件及文件内容。
格式化代码分析(mkfs-simplefs.c)
其格式化的步骤为:
- 写入superblock 的内容
- 写根目录inode
- 写文件inode
- 写根目录block
- 写文件block
写 superblock
此函数将一个block来保存superblock的信息。
1 |
|
写根文件inode
根文件的inode紧接着superblock 往后填充,也就是在第2个block中存储inode.
1 | struct simplefs_inode { |
写文件inode
通过此函数可以看出:所有的inode都存储在一个block中,而一个inode大小为 28
字节。
也就是说,此文件系统最多支持文件和文件夹的总数为 4096 / 28 = 146
1 |
|
写根目录block
写根目录block就是写文件名以及其inode的索引,一个名称对的大小为 264
字节,
也就是说一个目录最多可以存储的名称对为 4096 / 264 = 15
个,也就是说一个目录
最多存储15个文件或目录名。
1 |
|
写文件block
写文件block就是把文件内容写进去即可。
1 | int write_block(int fd, char *block, size_t len) |
文件系统的结构
根据上面的格式化代码,可以知道其结构如下图:
可以看出此文件系统的确是足够的简单:
- superblock描述极为简单
- 并不具备block bitmap 和 inode bitmap
- 最多支持的文件和文件夹总数为146个(因为仅用了一个block来存储inode)
- 一个文件夹中可以存储的文件和文件夹总数为15个
- 一个文件的内容不能超过一个block
文件系统操作逻辑
根据以上简单结构的分析,可以猜测出其基本的文件操作逻辑:
- 新建文件夹
- 从inode table 中填充一个文件夹类型的inode并获取其索引
- 为此索引的inode分配一个block并写入对应的inode
- 将新建文件夹的名称和inode索引对应存储在当前文件夹的block中
- 更新 superblock 中的inode计数
- 新建文件
- 从inode table 中填充一个文件类型的inode并获取其索引
- 为此索引的inode分配一个block并写入对应的inode
- 将文件内容写入其block中
- 将新建文件的名称和inode索引对应存储在当前文件夹的block中
- 更新 superblock 中的inode计数
- 删除文件或文件夹
- 去除当前文件夹中对应此文件或文件夹的描述字符串
- 更新 superblock 中的inode计数
- 建立硬链接
- 在当前文件夹下拷贝一份目标文件所在的文件夹中对于此文件的描述字符串
- 建立符号链接
- 首先新建一个文件
- 然后新建文件的内容指向目标文件所在的文件夹的inode
基于这些猜测,接下来分析其文件系统操作代码。
操作代码分析(simple.c)
挂载
在载入模块时,会首先使用函数 ·kmem_cache_create· ,用于为文件系统的inode申请缓存以便达到快速访问的目的。
1 | sfs_inode_cachep = kmem_cache_create("sfs_inode_cache", |
在挂载文件时,会调用函数 simplefs_fill_super
函数,此函数的主要目的就是填充 super_block
结构体
1 | /* This function, as the name implies, Makes the super_block valid and |
其数据填充结果如下图:
在 super.h
中有以下两个操作,对照上图就可以看出其意义:
1 | //获取 simplefs_super_block 结构体地址 |
读取文件夹内容
为了获取文件夹的内容得先从目录inode找到其对应的block。
当在 mount
文件夹下使用命令 ls
时,其执行路径依次为:
- simplefs_iterate : 用于扫描目录中的文件或文件夹名称以及其对应的inode
1 |
|
- simplefs_lookup : 得到文件或文件夹的inode内容并初始化系统的 inode结构体
1 | struct dentry *simplefs_lookup(struct inode *parent_inode, |
- simplefs_get_inode : 得到请求的inode号码的内容
1 | /* This functions returns a simplefs_inode with the given inode_no |
- simplefs_iterate
可以看出其基本思路是:
- 通过文件夹的inode获取其block
- 扫描block有哪些文件或文件夹
- 获取这些扫描到的文件或文件夹的inode内容,为其操作做好准备
读取文件内容
可以猜测为了读取文件内容,首先要获取其inode才能找到其block.
当执行 cat vanakkam
时,执行的函数依次是:
- simplefs_iterate : 重复执行了8次
- simplefs_read
1 | ssize_t simplefs_read(struct file * filp, char __user * buf, size_t len, |
- simplefs_read
可以看出其思路为:
- 从目录inode获取目录block,进而获取到文件的inode
- 所以当你对一个目录都没有读权限时,是无法通过其inode来获取文件内容的
- 从文件inode找到其对应block再读取其内容
写文件内容
可以猜测其与读文件内容的思路是一样的:
- 从目录inode获取目录block,进而获取到文件的inode
- 从文件inode找到其对应block再写入对应的内容
- 更新inode描述(因为inode中具有文件信息)
执行 echo “Hello world!” > vanakkam 其执行路径为:
- simplefs_iterate : 重复执行了12次,没看懂为什么
- simplefs_write : 写入数据并同步
1 | ssize_t simplefs_write(struct file * filp, const char __user * buf, size_t len, |
- simplefs_inode_save : 更新inode
1 | int simplefs_inode_save(struct super_block *sb, struct simplefs_inode *sfs_inode) |
- simplefs_inode_search : 从inode table 中找到对应序列的inode
新建文件夹
先来猜测新建文件夹的步骤:
- 根据文件夹inode找到其block
- 为新建的文件夹在inode table 中获取一个inode
- 为新建的文件夹分配一个block
- 将新申请到的文件夹名称以及其inode号写入父文件夹的block中
- 更新父文件夹inode
- 与硬盘同步
执行命令 mkdir hello
其调用函数依次为:
- simplefs_iterate : 浏览目录获取其档案及对应inode
- simplefs_lookup : 查看当前目录是否已有此档案名
- simplefs_mkdir : 新建文件夹
- simplefs_create_fs_object : 新建档案
1 | static int simplefs_create_fs_object(struct inode *dir, struct dentry *dentry, |
- simplefs_sb_get_object_count : 获取当前super block 中记录的inode数量
- simplefs_sb_get_a_freeblock : 获取空闲block
1 | int simplefs_sb_get_a_freeblock(struct super_block *vsb, uint64_t * out) |
- simplefs_sb_sync : 同步super block 与硬盘
- simplefs_inode_add : 获取一个inode
1 | void simplefs_inode_add(struct super_block *vsb, struct simplefs_inode *inode) |
- simplefs_sb_sync
- simplefs_inode_save
- simplefs_inode_search
新建文件
还是先来猜测一下新建文件的步骤:
- 根据文件夹inode找到对应的block
- 从inode table 中为新建文件获取一个inode
- 从block 中为新建文件获取一个block,并填充其内容
- 更新文件的inode
- 更新文件夹的inode,以及block
- 更新super block 的 inode
下面执行 echo "hello world!" > hello/hello.txt
其条用函数依次为:
- simplefs_iterate : 首先通过根目录扫描其所包含的条目
- simplefs_iterate : 然后通过扫描 =hello= 目录扫描其所包含的条目
- simplefs_lookup : 查找是否存在 =hello.txt= 的inode
- simplefs_create_fs_object: 新建文件
- simplefs_sb_get_object_count : 获取当前super block 中记录的inode数量
- simplefs_sb_get_a_freeblock : 获取空闲block
- simplefs_sb_sync : 同步super block 与硬盘
- simplefs_inode_add : 获取一个inode
- simplefs_sb_sync
- simplefs_inode_save
- simplefs_inode_search
- simplefs_write : 写入文件内容
- simplefs_inode_save : 更新inode
- simplefs_inode_search
功能实现
通过查看其代码可以发现,此文件系统还有以下功能未能实现:
- 删除文件
- 删除文件夹
- 建立符号链接
- 建立硬链接
下面来尝试一一实现:
删除文件
前面已经新建了文件 /hello/hello.txt
,下面尝试将它删除。
根据已有的知识先来猜测一下如何以最简单的方式删除一个文件,
为了能够使得操作步骤尽量的少,其实没有必要去擦除文件block的内容,而只需要对其inode操作即可。
也就是说涉及以下几个部分:
- 文件夹block的字符串和inode对擦除
- 文件夹inode中的描述修改
- inode table 修改
- super block 修改
通过 strace rm -f hello.txt
观察到有这么一行输出:
1 | unlinkat(AT_FDCWD, "hello.txt", 0) = -1 EPERM (Operation not permitted) |
对应驱动的调用接口应该是 struct inode_operations
下的 unlink
1 | int (*unlink) (struct inode *,struct dentry *); |
- 在实现的过程中发现,其在增加inode 和 dir content 时是直接简单粗暴的在尾部增加,很明显在删除文件时会产生漏洞,所以此bug也需要修复。
FUSE
如上图所示,FUSE仅仅在内核中实现了一个简单的模块,用于接口VFS和用户空间,文件系统的操作细节则存在于用户空间中。
- 这种方式导致操作效率低但便于调试
比较重要的数据结构
1 | /** |
inode Tab 存在于硬盘中,如果每次CPU从硬盘中读取那么效率会比较低下,
所以内核会为inode Table 申请一段内存以作为缓存,称为 对应文件系统的 inode cache.
1 | static int __init init_inodecache(void) |
同样在VFS层面上,也会对抽象出来的 inode 和 路径进行缓存(dentry), 分别称为 icache 和 dcache.
1 | static void __init dcache_init(void) |
最终这些申请的缓存都是内核通过LRU算法进行回收的(内核通过 shrink方法来回收slab内存)
- shrink 方法需要驱动编写者来主动实现