该文章参考宋宝华老师的视频课程,详细可以去听阅码场宋老师的课程。
文件系统的架构
- 一切都是文件:VFS
- 字符设备文件、块设备文件
- 超级块、目录、inode
- 符号链接与硬链接
- 目录的组织
- icache和dcache,slab shrink
- 块映射
- 发现并读取/usr/bin/xxx的全流程
- 用户空间的文件系统:FUSE
最近大家都在谈的虚拟化技术,就是要充分利用系统的资源,这里充分利用系统资源的意思就是:
CPU不闲着,IO也不闲着,CPU不等IO, IO不等CPU,它们之间没有相互等的情况。
在linux系统中,我们访问一个硬盘设备,我们有两个方法:
1、裸设备访问 。file_operations 在 fs/block_dev.c 中实现。
2、挂载后以文件系统访问。file_operations 在 fs/ext4/file.c 中实现, 文件系统会把你的读写转化为硬盘的哪个扇区、磁道等,所以我们在写硬盘驱动的时候就不用写
file_operations了,因为这一层已经被文件系统实现了。
我们知道一个文件 1.txt 它不仅仅是文件的具体内容,它还有一些元数据,比如文件的创建者,创建时间,文件大小,权限等等信息。那这些
信息都要存储到硬盘里来的,我们知道硬盘里最小的存储单位是扇区,一般来说一个扇区是 512个字节,那一个 10M大小的文件,要存到硬盘里,
可能文件的内容部分就需要20480个扇区来存储,那每个扇区的位置也要存储起来,文件的元数据也要存储起来,那这些东西都是文件系统来做的。
经典的文件系统结构如下:
superblock 主要存储
- inode bitmap 和 data blocks bitmap 的大小 和 位置。
- inode 的位置和数量
- data blocks 的位置和数量
- root inode 的索引
linux系统中,everything is a file , 每个文件都有一个身份证号,这个身份证号就是 inode , 这个 身份证号是唯一的,如果有两个文件身份证号是一样的,那就是它们两个
是完全一样的,比如我们建立一个硬链接,ln 1.txt 2.txt 。
inode bitmap 就是用每一个bit 来表示 inode 是否是空闲的,有没有被占用。在创建文件的时候,在 inode bitmap 中就会有一个bit 被占用,当你删除的时候,这个bit 就会被标记为空闲。
接下来是 datablock bitmap , 假如我们要存储的文件大小是 10M , 假设每一个data block 是 4k , 那每一个bit 就代表哪些4k的block是空闲的。我们需要记录 data block 中哪些 block 是否被用掉,datablock bitmap 就是来记录这个的。比如我们要给一个文件的末尾增加16k的内容,文件系统就会在 datablock bitmap 中寻找4个空闲的bit, 然后标记成1。
inode table 不再是一个bitmap, 而是相同大小的一个等分块,假设每个 inode table 是 128byte ,当然不同的文件系统的 inode table的大小不一样,有的64byte ,有的256byte,这里以 128byte为例,这128byte里面存储了 文件的元数据,大小、时间、作者等,还有最最重要的就是 这个文件的每个数据块的位置。这个用指针来描述的。
下面来描述一下,当我们创建一个文件 xxx.c 的全过程。
touch xxx.c ,首先会在 inode bitmap 中为这个文件,寻找一个身份证号,比如 inode=11 , bitmap的第11位就会写成1。
比如这个文件的大小是 16k , 接下来需要在 datablock bitmap 中找到 4个 空闲的比特位,然后写成1,这4个比特位代表了 datablock中哪4个4k的block是空闲的,可以用的。
接下来就会在 inode table 中找到 inode=11 的 那个 inode table , 然后把文件的元数据写进去,最终要的是要把 用到的那4个 datablock的位置,用指针写进去。那这里就需要4个指针,32个byte 就可以解决。
到这里整个文件就创建完成了。
但是我们知道,每一个 inode table 就只有 128 byte ,就算都拿来存储指针,也能存储的文件大小也是有限的,那到底该如何解决呢?
直接映射的方法,存储的文件是有限的。可以采用间接映射的方法,一个指针指向一个数据块,这个4k的数据块里存储的1024个指针,这就是2级的映射,可以最大存储 4M的文件,当然也有3级的映射,最大可以存储 4GB 的大小。
Ext4 中有个 extents 的概念,对于 地址连续存储的数据库,有更节省空间的做法。
硬盘里不可能上来就直接管理 n 个 block ,就像一个公司就一个部门,这样管理起来就特别麻烦了,文件系统会分为几个group ,每个group
还是咱们上面说的文件系统的组织结构。
Linux 中 目录是一个特殊的文件,everything is a file , 它也有 inode 编号,跟普通文件一样,特殊的地方就是它的内容,它的内容是 文件的名字 和 inode的映射关系。
比如我们要检索 /usr/bin/emacs 这个文件,首先会 查 / 目录的 inode = 1 , 然后根据 第1个 inode table 中的 指针信息 找到 / 的数据内容,然后查到 /usr 目录的inode 编号2,根据这个编号,可以查到 /usr 的inode table 中指针信息,查到它的数据内容,进而查到 /usr/bin的inode 编号为11 , 再查inode = 11的 inode table 的指针信息得到 /usr/bin/的数据内容,也就得到了 /usr/bin/emacs的inode 编号为119 ,于是我们查到了 emac这个文件在硬盘中存放的位置。
硬链接的概念就更好理解了
作为一个linux 工程师,理解的时候就不能像 m.txt 是 a.txt 的硬链接这么简单,要理解成 m.txt a.txt 在硬盘里的存储和实现,是完全对等的,唯一不同的地方就是
在一个数据表项里面它们的名称不一样而已,要知道 inode 编号才是文件唯一的身份证号!把硬链接就理解成是一个别名,地位上是完全对等的。
但是删除m.txt ,a.txt 还是存在的。硬链接的本质就是 在一个目录表项里多了一条和 inode的映射关系。把 m.txt 删除了,只是删除了这一条映射关系。
ln -a a b 软连接是不一样的,b 也是有自己的inode编号的,也有自己的inode table , 不过它的内容是指向原原件的。
----------
之前我们举的例子,检索 /usr/bin/emacs 这个文件出来,这个过程是比较漫长的,在linux 中,每一个路径都对应有一个 dentry , 如果建立一个硬链接,ln /usr/bin/emacs /usr/bin/emacs-ln , 这两个就是不同的dentry , 每一个dentry被读过之后,linux 就会给它简建立一个dentry cache中,每一个inode 的inode table 被读过一遍之后,linux 就会给它建立一个 inode cache , 这就是我们在linux 中常说的 icache 和 dcache , 这两个cache 是文件系统级别的cache 和 CPU中的cache不一样。
上图中的标注:SLAB_RECLAIM_ACCOUNT 说明是可以会回收的。
这两个cache都是slab级别申请的,是可以自动回收的。这里和 kmalloc 和 kfree不一样,kmalloc 虽然也是slab级别的申请,但是需要自己去回收释放。
当这个值我们把它加大的时候,系统在做内存回收的时候,就会尽量帮我们回收 icache dcache 。
回收的时候用的是LRU的算法。大部分是LRU的算法。
如果想 清掉 系统的 pagecache :
如果想 清掉 系统的 icache 和 dcache :
如果想 清掉 系统的 pagecache , icache 和 dcache :
一个inode 对应一个 pagecache
比如1.txt 2.txt 是硬链接的关系,有三个进程,分别打开 这两个文件,同一个路径对应一个dentry , 但实际上linux 是以 inode 来区分文件的,所以这三个进程打开的文件,都会对应一个inode,
注意:
1、每个进程运行,都对应一个 task_struct , 每打开一个文件,都对应一个 file 的结构体,file里的 file_operations其实是 inode->i_fops 来填充的。
2、一个inode 对应一个 pagecache ,比如打开的是一个 1Mbyte 的文件,每次读写这个文件,都会在缓存在内存里,叫 pagecache ,它在内存里是如何的结构呢?其实是以一个 radix tree 基数树来组织的。
每一个小块就是一个4k的页,比如要读这个文件的 第16k 的位置,如果 radix tree 里没有,会在radix tree 中申请要读写大小的page , 然后从 磁盘里 通过 address_space_operations() 来读写到radis tree中 申请的pages中。
i_mmap 是管理地址空间的,一般包括两部分,它的关联的数据结构就是 address_space , 地址空间包含两部分,第一部分就是 radix tree 管理的Pages 在不在,基数的意思大概是文件的偏移地址,根据这个基数去在tree上查,然后查到 要读的偏移地址的页的位置,如果pagecache 不命中,就会通过a_ops-> address_space_operations 从硬盘里去读。
如果我们自己要实现一个文件系统,要做社么事情呢?
首先 申请一个 file_system_type , 实现 mount umount
填充 super block , 如何申请 和 销毁 inode , 置空 可以分配为系统通用的接口。
填充 inode_operations , 主要是 如何创建、查找 inode , 如何 mkdir
file 结构体中的 file_operations
可以自己写一个文件系统
GitHub - 21cnbao/simplefs: A simple, kernel-space, on-disk filesystem from the scratch
主要包含两部分,一个是内核模块 mkfs-simplefs.ko ,一个格式化工具mkfs-simplefs
一种用户态 文件系统 FUSE