首页 > 技术文章 > 【操作系统/Linux】Linux文件系统(转载)

nntzhc 2021-05-20 22:23 原文

深度剖析 Linux cp 的秘密 (qq.com)

使用 25 张图,深度剖析 Linux 的 3 种“拷贝”命令 (qq.com)

存储基础 — 文件描述符 fd 究竟是什么? (qq.com)

 

struct task_struct 是进程的抽象封装,标识一个进程,在 Linux 里面的进程各种抽象视角,都是这个结构体给到你的。当创建一个进程,其实也就是 new 一个 struct task_struct 出来。这个结构体中,有file_struct结构体的成员。

file_struct 本质上是用来管理所有打开的文件的,内部的核心是由一个静态数组动态数组管理结构实现。

文件描述符 fd 就是这个数组的索引,也就是数组的槽位编号而已。 通过非负数 fd 就能拿到对应的 struct file 结构体的地址。

struct file 结构体里面有一个 inode 的指针,也就自然引出了 inode 的概念。这个指向的 inode 并没有直接指向具体文件系统的 inode ,而是操作系统抽象出来的一层虚拟文件系统,叫做 VFS ( Virtual File System ),然后在 VFS 之下才是真正的文件系统,比如 ext4 之类的。

完整架构图如下:

 

为什么会有这一层封装呢?

其实很容里理解,就是解耦。如果让 struct file 直接和 struct ext4_inode 这样的文件系统对接,那么会导致 struct file 的处理逻辑非常复杂,因为每对接一个具体的文件系统,就要考虑一种实现。所以操作系统必须把底下文件系统屏蔽掉,对外提供统一的 inode 概念,对下定义好接口进行回调注册。这样让 inode 的概念得以统一,Unix 一切皆文件的基础就来源于此。

 简单梳理下:

  1. 进程结构 task_struct :表征进程实体,每一个进程都和一个 task_struct 结构体对应,其中 task_struct.files 指向一个管理打开文件的结构体 fiels_struct ;

  2. 文件表项管理结构 files_struct :用于管理进程打开的 open 文件列表,内部以数组的方式实现(静态数组和动态数组结合)。返回给用户的 fd 就是这个数组的编号索引而已,索引元素为 file 结构;

    • files_struct 只从属于某进程;
  3. 文件 file 结构:表征一个打开的文件,内部包含关键的字段有:当前文件偏移,inode 结构地址

    • 该结构虽然由进程触发创建,但是 file  结构可以在进程间共享;
  4. vfs inode 结构体:文件 file 结构指向 的是 vfs 的 inode ,这个是操作系统抽象出来的一层,用于屏蔽后端各种各样的文件系统的 inode 差异;

    • inode 这个具体进程无关,是文件系统级别的资源;
  5. ext4 inode 结构体(指代具体文件系统 inode ):后端文件系统的 inode 结构,不同文件系统自定义的结构体,ext2 有 ext2_inode_info,ext4 有ext4_inode_info,minix 有 minix_inode_info,这些结构里都是内嵌了一个 vfs inode 结构体,原理相同;

     

inode/block 概念

  1. 磁盘空间是按照 Block 粒度来划分空间的,存储数据的区域全都是 Block,我们叫做数据区域;
  2. 文件存储不再连续存储在磁盘上,所以需要记录元数据,这个我们叫做 inode;

文件系统中,一个 inode 唯一对应一个文件,inode 的个数则是在文件系统格式化的时候就确定好了的,换言之,一个 local 文件系统支持的文件数是天然就有上限的

block 固定大小,每个 4k(大部分文件系统都是,这里不做纠结),block 意图存储打散的用户数据。

无论是 inode 区,还是 block 区,本质上都是在线性的磁盘空间上。文件系统的空间层次如下:

 

 一个文件的对应一个 inode,这个文件需要按照 Block 切分存储在磁盘上,存储的位置则由 inode 记录起来,通过 inode 则能找到 block,也就获取到用户数据。

存储一个文件的时候,需要取一个空闲的 inode,然后把数据切分成 4k 大小存储到空闲的 block 上。

划重点:空闲的inode,空闲的 block。 这个很关键,已经存储了数据的地方不能再让写,不然会把别人的数据覆盖掉。

inode 区和 block 区分别需要另一张表,用来表示 inode 是否在用,block 是否在用,这个表的名字我们叫做 bitmap 表。bitmap 是一个 bit 数组,用 0 表示空闲,1 表示在用,如下:

 

bitmap 什么时候用呢?自然是写的时候,也就是分配 inode 或者 block 的时候,因为只有分配的时候,你才需要找空闲的空间。

小结一下

  1. bitmap 本质是个 bit 数组,占用空间极其少,用 0 来表示空闲,1 表示在用。使用时机是在创建文件,或者写数据的时候;
  2. inode 则对应一个文件,里面存储的是元数据,主要是数据 block 的位置信息;
  3. block 里面存储的是用户数据,用户数据按照 block 大小(4k)切分,离散的分布在磁盘上。读的时候只有依赖于 inode 里面记录的位置才能恢复出完整的文件;
  4. inode 和 block 的总个数在文件系统格式化的时候就确定了,所以文件数和文件大小都是有上限的;

问题:inode结构只能表示很小的文件,如何解决?

既然问题在于浪费内存,inode 内存分配不灵活,那就可以看把 inode->i_block 下放到磁盘。 

因为磁盘的空间比内存大了不止一个量级。100M 对内存来说很大,对磁盘来说很小。换句话说,用把用户数据所在的 block 编号存到磁盘上去,这个也需要物理空间,使用的也是 block 来存储,只不过这种 block 存储的是 block 编号信息,而不是用户数据。

那么我们怎么通过 inode 找到用户数据呢?

因为这个 block 本身也有编号,我们则需要把这个存储用户 block 编号的 block 所在块的编号存储在 inode->i_block[15] 里,当读数据的时候,我们需要先找到这个存储编号的 block,然后再通过里面存储的用户数据所在的 block 编号找到用户所在的 block ,去读数据。

这个存储用户 block 编号的 block 所在块的编号我们叫做间接索引,然后我们根据跳转的次数可以分类成一级索引,二级索引,三级索引。顾名思义,一级索引就是跳转 1 次就能定位到用户数据,二级索引就是跳转 2 次,三级索引就是跳转 3 次才能定位到用户数据。那么 inode->i_block[15]  里面存储的可以直接定位到用户数据的 block 就是直接索引

终于可以说回 ext2 的使用了,ext2 的 inode->i_block[15] 数组。知识点来了,按照约定,这 15 个槽位分作 4 个不同类别来用:

  1. 前 12 个槽位(也就是 0 - 11 )我们成为直接索引
  2. 第 13 个位置,我们称为 1 级索引
  3. 第 14 个位置,我们称为 2 级索引
  4. 第 15 个位置,我们称为 3 级索引

 

 

 

直接索引:能存 12 个 block 编号,每个 block 4K,就是 48K,也就是说,48K 以内的文件,只需要用到 inode->i_block[15]  前 12 个槽位存储编号就能完全 hold 住。

一级索引

inode->i_block[12] 这个位置存储的是一个一级索引,也就是说这里存储的编号指向的 block 里面存储的也是 block 编号,里面的编号指向用户数据。一个 block 4K,每个元素 4 字节,也就是有 1024 个编号位置可以存储。

所以,一级索引能寻址 4M(1024 * 4K)空间 。

二级索引

二级索引是在一级索引的基础上多了一级而已,换算下来,有了 4M 的空间用来存储用户数据的编号。所以二级索引能寻址 4G (4M/4 * 4K) 的空间。

三级索引

三级索引是在二级索引的基础上又多了一级,也就是说,有了 4G 的空间来存储用户数据的 block 编号。所以二级索引能寻址 4T (4G/4 * 4K) 的空间。

最后,看一眼完整的表示图:

 

 所以,在我们 ext2 的文件系统上,通过这种间接块索引的方式,最大能支撑的文件大小 = 48K + 4M + 4G + 4T ,约等于 4 T。文件系统最大支撑 16T 空间,因为 4 Byte 的整形最大数就是 2^32=4294967296 , 乘以 4K 就等于 16 T。

ext2 文件系统支持的最大单文件大小和文件系统最大容量就是这么算出来的(温馨提示:ext4 文件系统不仅兼容间接块的实现,还使用的是 extent 模式来管理的空间,最大支持单文件 16 TB ,文件系统最大 1 EB)。

思考:这种多级索引寻址性能表现怎么样?

在不超过 12 个数据块的小文件的寻址是最快的,访问文件中的任意数据理论只需要两次读盘,一次读 inode,一次读数据块。访问大文件中的数据则需要最多五次读盘操作:inode、一级间接寻址块、二级间接寻址块、三级间接寻址块、数据块。

知识点总结

  1. 文件系统对外提供文件语义,本质只是管理磁盘空间的软件而已;
  2. 经典的文件系统主要划分 3 大块 superblock 区,inode 区,block 区(块描述区,bitmap区这里暂不介绍)。一个文件在文件系统的内部形态由一个 inode 记录元数据加上 block 存储用户存储用户数据样子;superblock 区中的数据其实就是文件卷的控制信息部分,也可以说它是卷资源表,有关文件卷的大部分信息都保存在这里。例如:硬盘分区中每个block的大小、硬盘分区上一共有多少个block group、以及每个block group中有多少个inode。
  3. 文件系统的 size 是文件大小,是逻辑空间大小,文件大小 size 和真实的物理空间并不是一个概念
  4. 稀疏语义是文件系统提供的一种特性,根本用途是用来更有效的利用磁盘空间;
  5. 后分配空间是空间利用最有效的方式,公有云的云盘靠什么赚钱?就是后分配,你买了 2T 的云盘,在没有写入数据的时候,一个字节都没给你分配,你却是付出 2T 的价格;
  6. stat 命令能够查看物理空间占用,Blocks 表示的是扇区(512字节)个数;
  7. 稀疏文件的空洞和用户真正的全 0 数据是无法区分的,因为对外表现是一样的(这点非常重要);
  8. cp 命令通过调用 ioctl(fiemap)系统调用,可以获取到文件空洞的分布情况,cp 过程中跳过这些空洞,极大的提高了效率(100G 的源文件,cp 只做了十几次 io 搞定了,所以 1 秒足以);
  9. cp 的 sparse 参数从速度最快,空间最省,数据最拷贝最多,各有特点,小小的 cp 命令出来的目标文件,其实和源文件并不相同,只不过你没注意到;
  10. 预分配和 punch hole 其实都是fallocate 调用,只是参数不同而已,调用的时候,注意要 4k 对齐才能达到目的
  11. 稀疏文件的 punch hole 应用有很多场景,通常是用来快速释放空间,比如镜像文件;

Linux 的文件和目录

文件系统中其实有两种文件类型,分为:

  • 普通文件(这里把链接文件包含在普通文件以内)
  • 目录文件

可以通过 inode->i_mode 字段,使用 S_ISREGS_ISDIR 这两个宏来判断是哪个类型。普通文件很容易理解,就是普通的数据文件,inode 里面存储元数据,inode 可以索引到 block,block 里面存储用户的数据。目录文件 inode 存储元数据,block 里面存储的是目录条目。

举个形象的例子:在当前 testdir 目录下,有 dir1,dir2,dir3 这三个文件。假设 dir1 的 inode 编号是 1024,dir2 是 1025,dir3 是 1026。

那么现实是这样的:

  1. testdir 这个目录首先会对应有一个 inode,inode->i_mode 的类型是目录,并且还会有 block 块,通过 inode->i_blocks 能索引到这些 block;
  2. block 里面存储的内容很简单,是一个个目录条目,内核的名字缩写为 dirent,每一个 dirent 本质就是一个 文件名字 到 inode 编号的映射,所以,testdir 这个目录文件的 block 里存了 3 条记录 [dir1, 1024],[dir2, 1025],[dir3, 1026];

 

 

所以,目录到底是什么呢?就存储形态而已,目录也是文件,存储的是 名字 到 inode number 的映射表。dirent 其实就是 directory entry 的缩写。 

其实已经讲了一半了,树形结构的数据结构基础已经有了,就是目录文件和 dirent 的实现。假设叶子结点的为普通文件。

磁盘上存储了 3 个目录文件

 

 

这个时候,读者朋友你是不是都可以用笔画出一个树形结构了,内存的树形结构也是这么来的。通过磁盘的映射数据构造出来。在内存中,这个树形结构的节点用 dentry 来表示(通常翻译成目录项,但是笔者认为这个翻译很容易让人误解)。

以下是笔者从内核精简出来的 dentry 结构体,通过这个总结到几个信息:

  1. dentry 绑定到唯一一个 inode 结构体;
  2. dentry 有父,子,兄弟的索引路径,有这个就足够在内存中构建一个树了,并且事实也确实如此;

目录文件类型为树形结构提供了存储到磁盘持久化的一种形态,是一种 map 表项的形态,每一个表项我们叫做 dirent 。文件树的结构在内存中以 dentry 结构体体现。

ln 命令

                                          示例文件结构图:

ln 可以用来创建一个链接文件,有趣的是,链接文件有两个不同的类别:

  • 软链接文件
  • 硬链接文件

软链接文件:

  • 软链接文件是一个全新的文件,有独立的 inode,有自己的 block ,而这个文件类型是“链接文件”的类型而已;
  • 这个软链接文件的内容是一段 path 路径,这个路径直接指向源文件;

软链接文件就是一个文件而已,文件里面存储的是一个路径字符串。所以软链接文件可以非常灵活,链接文件本身和源解耦,只通过一段路径字符串寻路。

所以,软链接文件是可以跨文件系统创建的

 

 如图,软连接在目录中创建了真正的文件。

 

硬链接文件

  1. 硬链接文件其实并没有新建文件(也就是说,没有消耗 inode 和 文件所需的 block 块);
  2. 硬链接其实是修改了当前目录所在的目录文件,加了一个 dirent 而已,这个 dirent 用一个新的 name 名字指向原来的 inode number;

 

 对于示例文件结构图,有三个目录文件:

 

如图,硬链接只是在目录文件中进行了修改。

由于新旧两个 dirent 都是指向同一个 inode,那么就导致了一个限制:不能跨文件系统。因为,不同文件系统的 inode 管理都是独立的。

 mv 命令

 1   源 和 目的 在同一个文件系统

mv 命令的核心操作是系统调用 rename ,rename 从内核实现来说只涉及到元数据的操作,只涉及到 dirent 的增删(当然不同的文件系统可能略有不同,但是大致如是)。通常操作是删除源文件所在目录文件中的 dirent,在目标目录文件中添加一个新的 dirent 项。

划重点:inode number 不变,inode 不变,不增不减,还是原来的 inode 结构体,所以数据完全没有拷贝。

 2   源 和 目的 在不同的文件系统

 

这个时候操作分成两步走,先 copy ,后 remove 。

  1. 第一步:走不了 rename ,那么就退化成 copy ,也就是真正的拷贝。读取源文件,写入目标位置,生成一个全新的目标文件副本;
    • 这里调用的 copy_reg 的函数封装(要知道这个函数是 cp 命令的核心函数,在 深度剖析 Linux cp 的秘密 有深入剖析过 );
    • lnmvcp 是在 coreutils 库里的命令,公用函数本身就是可以复用的;
  2. 第二步:删除源文件,使用 rm 函数删除;

思考问题:mv 跨文件系统的时候,如果第一步成功了,第二步失败了(比如没有删除权限)会怎么样?

会导致垃圾。也就是说,目标处创建了一个新文件,源文件并没有删除。

总结

  1. 目录文件是一种特殊的文件,可以理解成存储的是 dirent 列表。dirent 只是名字到 inode 的映射,这个是树形结构的基础;
  2. 常说目录树在内存中确实是一个树的结构,每个节点由 dentry 结构体表示;
  3. ln -s 创建软链接文件,软链接文件是一个独立的新文件,有一个新的 inode ,有新的 dentry,文件类型为 link,文件内容就是一条指向源的路径,所以软链的创建可以无视文件系统,跨越山河
  4. ln 默认创建硬连接,硬链接文件只在目录文件里添加了一个新 dirent 项 <新name:原inode>,文件 inode 还是和原文件同一个,所以硬链接不能跨文件系统(因为不同的文件系统是独立的一套 inode 管理方式,不同的文件系统实例对 inode number 的解释各有不同)
  5. ln 命令貌似创建出了新文件,但其实不然,ln 只跟元数据相关,涉及到 dirent  的变动,不涉及到数据的拷贝,起不到数据备份的目的;
  6. mv 其实是调用 rename 调用,在同一个文件系统中不涉及到数据拷贝,只涉及到元数据变更( dirent 的增删 ),所以速度也很快。但如果 mv 的源和目的在不同的文件系统,那么就会退化成真正的 copy ,会涉及到数据拷贝,这个时候速度相对慢一些,慢成什么样子?就跟 cp 命令一样;
  7. cp 命令才是真正的数据拷贝命令,速度可能相对慢一些,但是 cp 命令有 --spare 可以优化拷贝速度,针对空洞和全 0 数据,可以跳过,从而针对稀疏文件可以节省大量磁盘 IO

 

  1. 进程结构 task_struct :表征进程实体,每一个进程都和一个 task_struct 结构体对应,其中 task_struct.files 指向一个管理打开文件的结构体 fiels_struct ;

  2. 文件表项管理结构 files_struct :用于管理进程打开的 open 文件列表,内部以数组的方式实现(静态数组和动态数组结合)。返回给用户的 fd 就是这个数组的编号索引而已,索引元素为 file 结构;

    • files_struct 只从属于某进程;
  3. 文件 file 结构:表征一个打开的文件,内部包含关键的字段有:当前文件偏移,inode 结构地址

    • 该结构虽然由进程触发创建,但是 file  结构可以在进程间共享;
  4. vfs inode 结构体:文件 file 结构指向 的是 vfs 的 inode ,这个是操作系统抽象出来的一层,用于屏蔽后端各种各样的文件系统的 inode 差异;

    • inode 这个具体进程无关,是文件系统级别的资源;
  5. ext4 inode 结构体(指代具体文件系统 inode ):后端文件系统的 inode 结构,不同文件系统自定义的结构体,ext2 有 ext2_inode_info,ext4 有ext4_inode_info,minix 有 minix_inode_info,这些结构里都是内嵌了一个 vfs inode 结构体,原理相同;

推荐阅读