内核必须懂(二): 文件系统初探

3,067 阅读8分钟

目录

  • 前言
  • 文件系统结构
  • 新建文件和inode
  • 文件创建过程
  • inode解析
  • 打开文件
  • 参考
  • 最后

前言

这次来说文件系统. 文件系统是非常重要的, 提高磁盘使用率, 减小磁盘磨损等等都是文件系统要解决的问题. 市面上的文件系统也是数不胜数, 比较常用的像ext4, xfs以及ntfs等等, 国内的像鹅厂的tfs, 然后还有sun号称"last word in file system"的ZFS, 学习ZFS而来的btrfs.

下面上一张Linux文件系统组件的体系结构图, 是我整合了多方文献并结合自己的经验画出来的. 可以看出, 最重要的就是vfs, 正是因为它, 才让Linux可以同时支持多种的文件系统. 举个例子, 比如你装了双系统mint+windows, 在mint中, 你可以看到windows的ntfs磁盘, 但是返回了windows, 你就看不到mint的磁盘了.

Linux文件系统组件的体系结构图

那Linux支持哪些文件系统呢? 来到源码的fs文件夹, Linux支持的文件系统可多了去了, 注意看蓝色的.

Linux支持文件系统


文件系统结构

磁盘扇区什么的就不多说了. 也许会出一篇谈存储介质的文章, 说说ssd结构啥的. 直接跳过硬件从文件系统结构开始. 注意, 我说的是通用模型, 每个fs的具体实现有差异, 而且差异蛮大的. ext家族是Linux默认的fs了, 事实上ext2/ext3和ext4差异也很大.

  • superblock: 记录此fs的整体信息, 包括inode/block的总量、使用量、剩余量, 以及文件系统的格式与相关信息等;
  • inode table: superblock之后就是inode table, 存储了全部inode.
  • data block: inode table之后就是data block. 文件的内容保存在这个区域. 磁盘上所有块的大小都一样.
  • inode: 记录文件的属性, 同时记录此文件的数据所在的block号码. 每个inode对应一个文件/目录的结构, 这个结构它包含了一个文件的长度、创建及修改时间、权限、所属关系、磁盘中的位置等信息.
  • block: 实际记录文件的内容. 一个较大的文件很容易分布上千个独产的磁盘块中, 而且, 一般都不连续, 太散会导致读写性能急剧下降.

好, 我猜你和我一样是右脑思维, 上图就好:

文件系统结构

可以看出来, 这是多层索引结构的文件系统. 用b+树是最佳解决方案, 比如btrfs. inode table指向inode, inode指向一个或者多个block, 注意, 图中还是直接指向, 后面还会讲述多层指向. 最怕的就是inode指向的block太散. 一个比较好的解决办法就是在文件末尾不断添加数据, 而不是新建文件.


新建文件和inode

新建一个文件和文件夹, 用stat指令查看文件信息.

touch hello
stat hello
mkdir hellodir
stat hellodir

新建文件

可以看到一些信息. 例如一个目录初始大小就是4KB, 8个block, 一个扇区就是512B, 一个io block是4KB, 对应第一幅图的General Block Device Layer层. 这些其实不看也知道, 前提是这是常规的fs.


文件创建过程

创建成功一个文件有4步:

  • 存储属性: 也就是文件属性的存储, 内核先找到一块空的inode. 例如, 之前的1049143. 内核把文件的信息记录其中. 如文件的大小、文件所有者、和创建时间等, 用stat指令都可以看到.
  • 存储数据: 即文件内容的存储, 比方建立一个1B的文件, 那一个block, 8个扇区, 内核把数据放到一个空闲逻辑块中也就是空闲block中. 很明显, 碎片化的问题已经呈现在这里了. 1B它也要用4K对吧.
  • 记录分配情况: 假如数据保存到了3个block中, 位置要记录到inode的磁盘序号列表中. 这3个编号分别放在最开始的3个位置. 然后读的时候会一次性读, 可以看我的第二张图. 当然了fat就没有inode, 它在一个块中放了下一个块的位置, 形成链, u盘就是这种fs.
  • 添加文件名到目录: 文件名和inode之间的对应关系将文件名和文件以及文件的内容属性连接起来, 找到文件名就找到文件的inode,通过inode就能找到文件的属性和内容. 换句话说, 就是机器看的和人看的做衔接, 例如网址和ip. 当然, 如果你看inode就能区分文件, 第四步可以不要(手动滑稽).

目录的话, 就是多了.文件(指向自己), ..文件(指向上级目录). 然后添加自己的inode到上级目录. 看图就秒懂了.

stat

文件创建过程

inode解析

用df指令可以看inode的总数和使用量.

df -i

dumpe2fs打开指定磁盘可以看inode的大小, 这里是256.

指令

inode如何记录文件并且最大是多少呢? inode记录block号码的区域定义为12个直接, 一个间接, 一个双间接与一个三间接记录区. 一个inode是4B, 这样用4K的block可以有1K的inode.

  • 直接: 12 * 4K
  • 间接: (4K / 4) * 4K
  • 双间接: (4K / 4) * (4K / 4) * 4K
  • 三间接: (4K / 4) * (4K / 4) * (4K / 4) * 4K

所以的话, 4T, are you OK? 算归算, fs在不断发展, 这是过时的大小了. ext4的话单个文件可以到达16TB, fs可达1EB. 但是注意, ext4的作者都说了, ext4只是过渡, btrfs会更棒, 那事实上, cent os用的xfs也很很棒.


打开文件

创建之后当然要打开了, 打开文件也是有一系列过程的. 先来看看两个指令:

sysctl -a | grep fs.file-max
ulimit -n

打开文件最大数

  • 第一个指令查看os最大打开数, 这是系统级限制.
  • 第二个指令查看单进程最大打开数, 这是用户级限制.
  • 进程描述符(task_struct):
  • 为了管理进程, 操作系统要对每个进程所做的事情进行清楚地描述, 为此, 操作系统使用数据结构来代表处理不同的实体, 这个数据结构就是通常所说的进程描述符进程控制块(PCB). 通俗来讲就是操作系统中描述进程的结构体叫做PCB.
  • Linux内核通过一个被称为进程描述符task_struct结构体来管理进程, 这个结构体包含了一个进程所需的所有信息. 它定义在include/linux/sched.h文件中. 这不是这次的重点, 但是这个task_struct结构体确实很重要, 也很复杂.
  • 每个进程都会被分配一个task_struct结构, 它包含了这个进程的所有信息, 在任何时候操作系统都能跟踪这个结构的信息.
  • 文件描述符表(file_struct): 该表记录进程打开的文件. 它的表项里面有一个指针, 指向存放在内核空间的文件表中的一个表项. 它向用户提供一个简单的文件描述符(fd), 使得用户可以通过方便地访问一个文件. 例如, 当进程使用open打开一个文件时, 内核就会在这个表中添加一个表项. 如果对同一个文件打开多次, 那么将有多个表项. 使用dup时, 也会增加一个表项.

  • 文件表: 文件表保存了进程对文件读写的偏移量. 该表还保存了进程对文件的存取权限等等. 比如, 进程以O_RDONLY方式打开文件, 这将记录到对应的文件表表项中. 然后每个表有一个指向inode table中inode的指针. 结合之前的图片看, 所有结构就联系起来了, 所以inode是核心点.

上图上图:

打开文件

  • 在进程A中, 文件描述符1和2都指向了同一个打开的文件表A. 这可能是通过调用dup()、dup2()、fcntl()或者对同一个文件多次调用了open()函数而形成的.
  • 进程A的文件描述符0和进程B的文件描述符2都指向了同一个打开的文件表A. 这种情形可能是在调用fork()后出现的(即, 进程A、B是父子进程关系), 或者当某进程通过UNIX域套接字将一个打开的文件描述符传递给另一个进程时, 也会发生. 再者是不同的进程独自去调用open函数打开了同一个文件, 此时进程内部的描述符正好分配到与其他进程打开该文件的描述符一样.
  • 此外, 进程A的描述符0和进程B的描述符255分别指向不同的打开文件表, 但这些文件表均指向inode table的相同条目(假设), 也就是指向同一个文件. 发生这种情况是因为每个进程各自对同一个文件发起了open()调用。同一个进程两次打开同一个文件, 也会发生类似情况.

为什么要说这些情况呢? 因为如果没有理解清楚这些, 在做多进程多线程read和write的时候很有可能会导致读取和写入混乱.


参考

看了非常多很棒的文章, 这里也分享给大家.


最后

这次从结构上逐步往内解剖文件系统, inode是核心点. 当然还有两篇甚至更多的后续文章, 最后会写个简单的用户态文件系统, 喜欢记得点个赞或者关注我哦~