哈喽,我是子牙老师。今天咱们从ext4文件系统源码层面聊聊Linux内核是如何创建目录的,创建文件也差不多原理
看这篇文章之前,建议你看下我之前写的《手写文件系统》,不然怕你看不懂
当我们创建目录的时候,差不多这两种方式。本文会从这两个大方向进行分析
本文会讲到以下这些:
- 在当前目录创建与指定目录创建,源码层面,有什么不一样
- 创建的目录,在硬盘中是如何存储的
- 创建目录的时候,内核怎么知道操作的是哪块硬盘哪个扇区
- 创建目录的时候,会产生哪些数据
- 创建目录的时候,会写几次盘
- 挂载分区,到底是什么意思,背后到底干了什么
- vfs与ext4是如何关联起来的
- 如何单步调试ext4文件系统源码?
以下,enjoy
目录在硬盘中是如何存储的
借用一下上篇文章用过的图
创建目录是一件非常复杂的事情,而且必须要原子性完成,否则会造成数据错乱。如何理解这个复杂呢?要操作图中列出的所有数据块,就意味着要多次写盘,IO自古以来都是性能瓶颈,EXT是如何解决的呢?做了哪些优化呢?听我娓娓道来
先从创建流程及写盘开始说吧。首先得判断当前块组inode是否已用光,一个块组占一个4K扇区,意味着一个块组最大支持32768个inode,如果没用光,拿到inode,比如是17,更新inode位图块(第一次写盘)
如果用光了呢?需要查找当前分区可用的块组,对此,EXT实现了两套算法
如果查不到可用的块组呢?那这意思还不明显吗?当前分区用光了,创建不了了
假设我们拿到了inode,值为17,这时候就需要找到inode=17的inode信息存储在inode列表哪个扇区,并完成初始化。上篇文章算过,硬盘扇区4K为例,一个inode节点占256B,一个4K扇区能够存16个inode节点,inode=17,存储在inode列表区域的第二个扇区中的第一个(第二次写盘),如图
写的什么信息?ext4_inode结构体
第三次写盘,将目录ziya的信息写入父目录的数据区,目录信息对应的结构体是ext4_dir_entry,如图
这样就完成了目录的创建,但是还不完整。注意到图中黄色区域没,每个目录在创建的时候,ext文件系统需要为其创建两个默认目录。这时候又要去获取inode,更新inode位图,更新inode列表,又要写好几次盘,我算不下去了……
这时候还有为新创建的目录申请数据块,即操作块位图块,然后将目录.或…的目录信息写入。
当这些事情全部做完,需要统一更新块组描述符表、超级块信息。至此,创建目录所有工作完成,细思极恐!
总结一下吧,当创建一个目录的时候,底层要干这些活:
- 去问当前块组的inode位图:你还有没有空位置啊?有?那留一个!没有,那你想想办法,给我找一个
- 有位置了,我把我的信息告诉你,你填进去
- 告诉父目录我的信息,让父目录知道我的存在,知道我的inode信息存储在哪
- 给我分个存放数据的地方,我要放两个目录.与…
- 统一更新超级块、块组描述符表
如果现实中,对创建目录不做一点点的优化,那硬盘是不是就跑疯了?对于写盘,做了如下策略
恭喜你!知道创建一个目录,底层都干了些什么。不知道在看的过程中,你的脑海中有没有生成一些问题,比如内核是如何知道要写哪个盘、哪个扇区
当前目录创建
在当前目录创建,与在指定目录创建,不同点就是如何知道是哪个盘哪个分区
先说下哪个盘哪个分区的信息存储在哪呢?如果运行起来,就存储在vfs中的super_block中
super_block中的这些信息从哪来的呢?来自用户mount时的传入及文件系统本身的超级块,比如ext4的ext4_super_block,比如 mount /dev/sda1 /
如果是当前目录创建,怎么得到super_block呢?当前进程结构体task_struct中有
在当前目录创建文件或目录,按顺序来说就是:
- 从当前进程的结构体task_struct中获取属性struct fs_struct fs
- 从fs总获取struct path pwd
- 从pwd中获取当前目录的挂载信息struct vfsmount *mnt
- 从mnt中获取super_block
- 从super_block中,你就知道要操作哪个盘、文件系统
- 根据super_block.s_fs_info就能拿到ext4_super_block及各种operations,比如目录操作
从流程中你是否能感受到什么叫挂载?挂载就是将磁盘分区,以合适的文件系统,与vfs关联起来,就是这么简单
结合上一趴,你就算知道了,在当前目录下创建文件或目录,底层工作的完整流程
不知道你看完后是否有这样的疑问:当前目录struct path pwd中的信息是怎么来的
来解开此谜底!
指定目录创建
在指定目录下创建目录,这里就假设在根目录下吧,多层目录,只是把等下要说的事情重复做多遍而已
比如我们通过mkdir在根目录下创建目录ziya
我们先不说内核是怎么做的,我们先想一想,如果这个事情要我们来做,我们会怎么做:
- 你得知道要写哪个盘吧
- 你得知道要写哪个扇区吧
- 你得知道当前块组是哪个吧,剩下的前面都讲过了,pass
如何知道根目录挂载的是哪个盘的哪个扇区呢?这个就比较复杂了,跟mnt namespace关联起来了
通过遍历进程的mnt namespace,就可以得到。我也写了一个内核程序,演示给你看。目前国内我所知的教Linux内核的,实话实说,还没看到对Linux内核的理解,在我之上的,大多数都是理论派、照本宣科派
每个进程的mnt namespace怎么来的呢?继承自始祖进程init_task。前提是你不通过clone函数创建新的mnt namespace
你会发现:始祖进程的mnt namespace最开始是空的。为什么会这样呢?之前写过一篇文章《如果你想手写Linux系统》中讲过,始祖进程是开发内核的大神写死的,不是通过程序创建出来的,而根目录挂载只有运行时才能去做,所以这里是空的。那什么时候赋值的呢?init_mount_tree
这个函数中间经历过几个调用链,大家都不认识,我就不提了。我就提它的入口:start_kernel,对Linux内核略知的应该知道这个函数吧,Linux内核由汇编进入C语言世界的入口函数
init_mount_tree挂载的是临时根文件系统rootfs,是内核启动时临时用的,完全进入内核后,需要重新挂载,类似这样,完成第一块盘的第一个分区与根目录的关联
当我们在任何目录中创建文件或目录,这里我们举例用的是根目录,会先去查根目录的挂载信息,就是通过读进程实例task_struct中的命名空间mnt namespace获得挂载信息
拿到了vfsmount.super_block,剩下的工作就跟前面讲过的一样了
至此,你就算完整的知道了,Linux内核是如何与文件系统配合,完成目录或文件的创建。肿么样?这样的硬核文章看起来是否酣畅淋漓……
当我们通过cd切换目录的时候,内核就会将目录信息dentry,目录的挂载信息vfsmount封装成path,赋值给进程中的task_struct.fs.pwd。
Linux内核,在这个人的手中,竞如此简单!你是不是有这样的感叹?借用卖油翁的:无他,唯手熟尔!