文件系统及硬链接

来自Wired
跳转到导航 跳转到搜索

  要说硬链接,最好从数据存储的原理上说起,但知道基础即可无需过于深入,我也提供了其他人的博客文章[1]供感兴趣的访客深入了解。

File:文件系统与硬链接.png.png
我画了一张拓扑图供您整合本章涉及的知识点所用。

硬盘中的最小存储单位:扇区、块[2]

  • 硬盘上最小的存储单位是“扇区”(扇形的一段区域),这是物理上实际存在的;
    • 过去生产的硬盘每个扇区大小通常是512B,现代生产的硬盘则在慢慢普及4KB(4096B),通常有“512e/4Kn”的标志,代表硬盘物理扇区大小为4KB,但可以通过技术手段兼容只支持512B模式的旧系统;
    • 每个扇区只能存储一份文件,哪怕这份文件未超过扇区大小也会占据一个扇区的空间。
  • 为了提高效率与便于管理,文件系统在扇区之上虚构了“块”的概念,一个块通常包括多个扇区(正整数倍个),文件系统以“块”为基础管理文件,“块”并非物理上存在的,是逻辑上的最小存储单位,也称为“逻辑块”;
    • “块”的大小是在格式化硬盘时设置的,除了它是虚构产物外 ,与扇区具有相同的性质,每个块只能存储一份文件,哪怕这份文件未超过所设置的块大小;
      • 若创建1kb的文档,实际文件体积及显示的体积均为1kb,但实际占用4kb空间;
      • 若创建4.1kb的文档,文件体积为4.1kb,但占用2个快,及8kb空间。
    • 所以若无特殊需要一般都设置为4096B(4KB),大多数现代文件系统亦用此作为默认大小,通常无需变动。但若某硬盘只有备份用途、以大体积的压缩包或视频为主,则可将块大小设为1MB甚至更高,更少的块意味着在管理块及对应的扇区上花费更少的资源,每次 I/O 操作能读写更多数据意味着更少的寻址及读写次数,性能自然会提升。代价便是每个文件都最小占用1MB(所设置的块大小)空间,如果这块硬盘要用于其他用途,则不一定适用,更改块大小又只能格式化硬盘。

 

记录数据块的索引节点:inode[3]

  数据存储在文件系统虚拟的块中,块由物理上存在的扇区构成,系统又怎么知道某份文件的数据存储在哪些块呢?简单来说,就靠inode,它存储的是文件的元数据信息及数据块的位置。

 

使用stat命令查询目录(值得注意的是:“万物皆文件”[4]是linux系统中很重要的理念,对新手来说“文件夹(目录)也是文件[5]”是很反常识的,但它确是如此)的元数据信息,其中包括了如下内容:

anon@anon:~/音乐/Ichiko Aoba/2012 - Ichiko Aoba - うたびこ$ stat .
  文件:.
  大小:4096      	块:8          IO 块大小:4096   目录
设备:259,6	Inode: 24254874    硬链接:2
权限:(0755/drwxr-xr-x)  Uid: ( 1000/    anon)   Gid: ( 1000/    anon)
访问时间:2024-09-25 00:47:28.225927473 +0800
修改时间:2024-09-25 00:47:03.857699427 +0800
变更时间:2024-09-25 00:47:03.857699427 +0800
创建时间:2024-09-21 00:02:39.657964154 +0800
  • 直接运行sata命令得出的结果更倾向于显示文件的元数据信息,而非inode所记录的,例如第零行“文件: XXXX”它只是表示stat命令所读取的文件(.为当前目录即它自身),块大小及设备信息等是文件系统的记录/设置;
  • 第一行:
    • 文件大小:文件占用的大小,如果小于一个块的大小,那也会占据一个块[6]
    • 块:情况稍微特殊些,尽管您设置的文件系统块大小[7]为4KB,但它仍旧以512B为基础计算[8]
    • IO 块大小:通过:sudo fdisk -l /dev/硬盘编号 查询I/O操作大小,是文件系统用于读写文件的首选字节数而不是唯一标准[9]
    • 文件属性。
  • 第二行:
    • 设备编号(文件所在硬盘的编号[10]);
    • inode 编号;
    • 硬链接数量:指向该 inode 的文件数量;
      • 目录特殊点(注意这里是目录表(见下一小节)的知识点):
        • 新建的目录会有两个特殊条目,一个指向自身(.)的inode、一个指向父目录(..)的inode[11],若创建子目录则还会包括指向子目录(XXX)inode的条目;
        • 例如假设三个空文件夹如此排列 /home/anon/音乐,其中“音乐”有来自于它自己父目录的两个硬链接;而“anon”有来自于父目录它自己子目录指向的父目录的三个硬链接;此时创建 /home/anon/电影/ 目录 则“anon”的硬链接+1(来自于新文件夹指向的父目录..)[12]
  • 其他便是文件权限(读/写/执行权限)、拥有者及用户组的ID;及变动文件[13]相关的时间戳。

 

在文件系统调试器(debugfs)内使用stat命令读取文件的inode信息:

/* 使用 < df 文件名 > 命令查询文件存储于哪块 */
anon@anon:~/下载$ df Disk-structure2.svg.png 

文件系统          1K的块      已用      可用 已用% 挂载点
/dev/nvme0n1p3 944431232 375957760 520425244   42% /

/* 使用 < sudo debugfs /dev/硬盘编号 > 进入对应硬盘的调试终端 */
anon@anon:~$ sudo debugfs /dev/nvme0n1p3
debugfs 1.47.0 (5-Feb-2023)
debugfs:  

/* 使用 < stat /路径/文件 > 查询文件的inode信息 */
debugfs:  stat /home/anon/下载/Disk-structure2.svg.png

Inode: 23734514   Type: regular    Mode:  0664   Flags: 0x80000
Generation: 1837483936    Version: 0x00000000:00000002
User:  1000   Group:  1000   Project:     0   Size: 268191
File ACL: 0
Links: 1   Blockcount: 528
Fragment:  Address: 0    Number: 0    Size: 0
 ctime: 0x66f2c2e7:34d18f38 -- Tue Sep 24 21:47:19 2024
 atime: 0x66f2c344:43b1f34c -- Tue Sep 24 21:48:52 2024
 mtime: 0x66f2c2e7:349486a4 -- Tue Sep 24 21:47:19 2024
crtime: 0x66f2c2e7:1d7446b4 -- Tue Sep 24 21:47:19 2024
Size of extra inode fields: 32
Inode checksum: 0x3993049a
EXTENTS:
(0-65):225714571-225714636

/* 使用 < stat /路径/文件 > 查询目录的inode信息 */
debugfs:  stat /home/anon/下载/

Inode: 23724069   Type: directory    Mode:  0755   Flags: 0x81000
Generation: 2202907076    Version: 0x00000000:0000098e
User:  1000   Group:  1000   Project:     0   Size: 12288
File ACL: 0
Links: 5   Blockcount: 24
Fragment:  Address: 0    Number: 0    Size: 0
 ctime: 0x66f40f70:38538adc -- Wed Sep 25 21:26:08 2024
 atime: 0x66f41f00:0a6fcb00 -- Wed Sep 25 22:32:32 2024
 mtime: 0x66f40f70:38538adc -- Wed Sep 25 21:26:08 2024
crtime: 0x66ba8b19:4bc4d184 -- Tue Aug 13 06:22:17 2024
Size of extra inode fields: 32
Inode checksum: 0x5f763fea
EXTENTS:
(0):94904370, (1):94910121, (2):94912946
(END)
  • 其中前半部分仍旧是inode编号、权限、属性等信息。
  • Fragment:表示数据是被连续存储的,非连续存储就涉及到碎片化的知识,一般linux系统不用太过在意,会有碎片但不至于有严重影响[14],通常在windows(NTFS文件系统)中比较常见,官方也提供了整理碎片的功能。
  • 文件时间戳[13]
  • Inode checksum:校验和,根据固定算法生成,用于校验inode数据的完整性。
  • EXTENTS:数据块信息。
    • (0-65):225714571-225714636表示文件数据共有66份被存储到225714571-225714636这段连续的数据块中;
    • (0):94904370, (1):94910121, (2):94912946 表示文件被分割为三份存储到三个不连续的块中,其中第0块存储在编号为94904370的块中,以此类推,严格来说这就有碎片化的问题,但通常无大碍[14],固态硬盘更不需要整理碎片化[15]

 

“统筹一切”的目录表(directory entry)

  简单来说,当我们保存编辑的文档时,数据经由文件系统写入到文件对应的inode记录的数据块的位置,便将文档保存在了硬盘的扇区内。可是inode本身只包括文件内容相关的元数据信息及存储的数据块编号,并不记录文件名、子文件/夹等与文件内容无关的信息,那我们怎么知道某个文件夹内包含哪些文件、文件的名字呢?轮到目录表出场了。

  与目录表相近的另一个概念是“inode 表”,inode 表是实际存在于分区中的一个专门的区域,用于记录inode信息,而目录表没有这种区域。简单来说,对于普通文件,inode指向的数据块存储的就是文件数据本身;对于目录文件,inode指向的数据块存储的便是该目录下的文件名及inode号的对应关系——即目录表。

简单展开说说:

  • 从终端运行 nano /home/anon/文档/test.md (只看文件系统涉及的几个知识点的作用,下同),文件系统会从“/(根目录)开始,因为根目录对应的inode号是预分配的[16],文件系统打开其对应的数据块并查找 home[17]对应的inode号,如此逐级查找……
  • 从终端运行nano ./tese.md 时,则是直接查找当前目录的目录表中该文件名对应的inode号,因为当前目录的inode号在进程控制块(Process Control Block)中保存,无需再从根目录逐级查找;
  • 正如在解释stat命令输出的硬链接数量[11]时的注释般,也可以运行nano ../test.md命令,它会查找当前目录的目录表记录的父目录的inode号,并从父目录中查找该文件名对应的inode号。

基本可以说索引文件的方式就这三种,当前目录、父目录、从根目录开始。

  • 还有一种特殊的 nano ~/test.md ,但~只是表示当前登陆用户的目录/home/用户名/,这跟windows的%APPDATA%表示C:\Users\username\AppData\Roaming一样,都是通过“环境变量”的设置决定的,也可以自行设置短语/符号与对应的路径或命令[18]。shell会将其自动解析为/home/用户名/再处理命令,本质仍旧是从根目录逐级访问。

但它无法被直接访问,可以在debugfs内使用ls命令读取文件夹包含的文件/子文件夹及对应的inode编号:

/* 使用 < sudo debugfs /dev/硬盘编号 > 进入对应硬盘的调试终端 */
anon@anon:~$ sudo debugfs /dev/nvme1n1p3

/* 使用 < ls /路径/ > 查询文件夹包含的子文件夹/文件 */
debugfs:  ls "/home/anon/下载/[BDMV][240925][UMXK-9037][GIRLS BAND CRY][Vol.4]/"

 25183407  (12) .    23724069  (12) ..    25183410  (20) UMXX_1085   
 25183350  (16) SCANS    25183426  (4024) Bonus CD   
 (END)
 
 /* 使用 < ls /路径/ > 查询文件夹包含的子文件夹/文件 */
debugfs:  ls "/home/anon/下载/[BDMV][240925][UMXK-9037][GIRLS BAND CRY][Vol.4]/SCANS"

 25183350  (12) .    25183407  (12) ..    25183420  (16) 12.png   
 25183488  (16) 01.png    25183489  (16) 02.png    25183490  (16) 03.png   
 25183495  (16) 04.png    25183520  (16) 05.png    25183521  (16) 06.png   
 25183522  (16) 08.png    25183523  (16) 07.png    25183524  (16) 09.png   
 25183525  (16) 10.png    25183526  (16) 11.png    25183527  (16) 13.png   
 25183528  (16) 14.png    25183529  (16) 15.png    25183530  (16) 16.png   
 25183531  (16) 17.png    25183532  (16) 18.png    25183533  (16) 19.png   
 25183534  (16) 20.png    25183535  (16) 21.png    25183536  (16) 22.png   
 25183537  (16) 23.png    25183538  (16) 24.png    25183539  (16) 25.png   
 25183549  (3660) 26.png   
(END)

 /* debugfs内的ls不支持递归搜索、>等符号,可以使用 < find "/使用find递归搜索的目录/" -exec stat --format='%n %i' {} \; > /使用stat获取所有文件的目录及inode编号信息并输出到此文件内 > 实现 */
anon@anon:~$ find "/home/anon/下载/[BDMV][240925][UMXK-9037][GIRLS BAND CRY][Vol.4]" -exec stat --format='%n %i' {} \; > /home/anon/下载/tree.txt
  • 左为inode编号;中为st_mode的十进制表达(与POSIX标准一致),表示文件类型和权限信息;右为对应的文件夹或文件名。

 

硬链接[19]

  到此便是文件系统及数据存储的基本逻辑[20]。简而言之,硬链接便是为某个inode新增一个指向它的指针、在目录表中新增一个条目使文件名指向已经存在的某个inode,无论您怎么理解和描述,这就是它的作用,不一样的路径、文件名,但指向同一个文件。

  有什么好处呢?硬链接只是新建了指向已经存在的inode的条目,所以它不会占用空间[21];因为是同一个inode的两个不同入口,所以删除其中任意一个并不会导致文件从物理上删除,直至没有文件指向该inode才会从系统中删除。

  有什么限制呢?只有支持硬链接的文件系统才支持硬链接[22];硬链接仅限于文件,而不能作用于目录[23];因为硬链接本质是引用相同的inode,inode基于文件系统,所以硬链接无法跨越文件系统[24]

  有什么具体使用场景呢?当多个地方需要同一个文件、当该文件需要不同的文件名或位于不同的路径时便可通过硬链接实现。例如媒体库,通过bt下载某部动漫后,即想遵循人人为我我为人人的精神持续保种(下载/保种文件夹)、又想将其存档(收藏/归档文件夹)、还想导入到流媒体库中(媒体库文件夹),下载的原始文件的命名方式、收藏时遵循/喜欢的命名格式、媒体库要求的命名规范各不相同,同时存在三份相同的数据太浪费资源,于是便可以通过硬链接无伤的满足需求;例如传统的文件结构分类中有一份文件同时满足多个文件夹的标准,拿捏不住具体放哪个文件夹中便可以硬链接。

  具体来说,硬链接方式:ln 原始文件 新文件

 

软链接

  相比于硬链接,软链接更容易理解,便是windows中的“快捷方式”。

  有哪些优点?不限制文件系统,硬链接是多个文件指向同一个inode,而软链接有自己的inode,存储的内容是目标文件的路径。

  有哪些缺点?因为存储的是路径,所以如果目标改变了文件名或路径便会失效;流媒体库等无法正常加载内容(其一是软链接指向路径而不是具体的数据,其二是检索时或许会忽略软链接属性的文件)。

  适用场景?只是需要一个快捷方式或快捷访问某个跨盘的目录。

  具体来说,软链接方式:ln -s 原始文件 新文件

 

参考信息

  1. 一口气搞懂「文件系统」,就靠这 25 张图了图解 | 原来这就是文件系统简直不要太硬了!一文带你彻底理解文件系统Linux的文件系统及文件缓存知识点整理Disk Organisation
  2. 参考:存储基础知识:扇区与块/簇硬盘分区、寻址和系统启动过程Ext4 磁盘布局
  3. 本文基于ext4文件系统,详细参考:inode 索引节点理解inode索引节点 - Linux 内核文档Inode Data Structure
  4. 驱动除外,感兴趣可以见:https://unix.stackexchange.com/a/141020
  5. linux系统中文件夹/目录也是被视为文件的,只是它们的属性不同导致作用不同,但他们共属于文件,文件名是不能冲突的,例如文件夹内已经有了名为1的文件夹,是无法创建名为 1 的文件的,反之亦然。
  6. 通过:sudo blockdev --getbsz /dev/硬盘编号 查询硬盘设置的块大小。
  7. 通过:sudo blockdev --getbsz /dev/硬盘编号 查询。
  8. 复杂一些可以简单理解为历史遗留因素,参考:How does stat command calculate the blocks of a file?POSIX.1-2008
  9. 详见:What is the meaning of IO Block and how is it calculated?
  10. 详见:Device number in stat command output
  11. 11.0 11.1 这也是为什么在命令行中可以使用 ../ 访问父目录、./访问当前目录,而没有.../等方式,因为该目录的目录表中记录了这两项。
  12. 参考:Why does a new directory have a hard link count of 2 before anything is added to it?
  13. 13.0 13.1 详见:timestamp, modification time, and created time of a file
  14. 14.0 14.1 详见:Is there any disk defragmenting GUI like piriform's Defraggler for Linux?Why Linux Doesn't Need DefragmentingSparse file
  15. 参考:Should I defrag an SSD drive?
  16. 具体取决于文件系统,如ext4为2:Why do inode numbers start from 1 and not 0?
  17. 没有父目录及斜线前后缀,那是文件系统加载时自己加的。
  18. Linux 的export与alias命令
  19. 硬链接与软链接硬链接和软链接
  20. 其中当然还有非常多的细节及技术,我说的也并不尽然全对,因为只将基础简化为这几个部分是有很多东西无法解释的,但请您自行探索。
  21. 当然部分软件,尤其是windows等不常用硬链接功能系统上的工具或许会错误统计占用空间,这是工具问题。
  22. 废话,目前常用的文件系统格式如linux常用的etx4、Btrfs;NAS常用的zfs、xfs;windows常用的ntfs都支持硬链接。但win的早期(xp时代)流行的FAT32和exFAT(现在多用于u盘等移动存储设备)格式便不支持硬链接。
  23. 主要原因是循环问题,最简单的比如 /home/anon/下载 硬链接 /home/anon/下载/图片 就会导致两者相互包括的无限循环,如ext4文件系统在设计时便禁止了这项操作。详细参考:Directory hardlinks break the filesystem in multiple waysWhy are hard links to directories not allowed in UNIX/Linux?
  24. 非常容易陷入误解的地方,但很多文档/文章都是这么描述,以我的技术水平也不好随意修改,不过你可以换一种方式理解:硬链接不可以跨分区,哪怕是同一块硬盘内,因为inode数据是存储在硬盘分区中的专门一块区域:inode 表,不同分区有自己的 inode 表。所以组raid的多块硬盘因为共用同一个inode表,也可以硬链接(本身组raid后也不分是哪块硬盘了)。