Linux磁盘级文件/文件系统理解
1. 磁盘的物理结构
磁盘的核心是一个利用磁性介质和机械运动进行数据读写的、非易失性的存储设备。
1.1 盘片
盘片是传统机械硬盘中最核心的部件,它是数据存储的物理载体。盘片是一个坚硬的、表面极度光滑的圆形碟片,被安装在硬盘内部的主轴上。多个盘片会叠放在一起,共同绕主轴旋转。
盘片的上下两个表面都涂有一层非常薄的磁性材料,这层涂层是数据真正被记录的地方。数据就通过改变这层材料上微小区域的磁场方向来记录(代表 0 和 1)。
1.2 主轴(马达)
主轴用于固定并旋转所有盘片。转速越高,盘片转动越快,磁头就能越快地访问到目标数据,读写速度也越快,但同时功耗、发热和噪音也更大。
1.3 磁道(Track)
盘片旋转时,磁头会保持在一个固定位置,这样就会在盘片上画出一个圆形路径,这个圆形就称为磁道。整个磁盘是由一圈一圈的磁道组成的。
1.4 磁头(Head)
负责读取和写入数据的部件。每个盘片的上下表面都对应一个独立的磁头。
在读写操作时,磁头会悬浮在盘片表面上方一个极小的距离(现代硬盘这个距离只有几纳米,比灰尘还小得多)。它永远不会接触盘片表面,否则会导致磁头碰撞(Head Crash),划伤磁性涂层,造成数据丢失。
所有磁头臂都连接在同一个 传动器 上,因此所有磁头总是同步移动。
1.5 柱面(Cylinder)
所有盘片上半径相同的磁道组成了一个柱面。因为所有磁头是同步移动的,当第一个盘片的第N磁道被访问时,其他盘片上的第N磁道也可以被同时访问。柱面是操作系统进行磁盘分区和空间分配时的一个重要逻辑概念。
1.6 扇区(Sector)
将一条磁道划分成若干个弧段,每一个弧段就称为一个扇区。它是磁盘进行数据读写的最小物理单位。传统硬盘的一个扇区大小是512字节,而现代高级格式硬盘(Advanced Format)则采用4096字节(4K) 为一个扇区。
1.7 块(Block)
这是操作系统层面的概念。操作系统为了管理方便,会将一个或多个连续的扇区组合起来(通常为8个扇区4KB),块(在Linux/Ext文件系统中)。它是操作系统进行文件存储和读写的最小逻辑单位。
1.8 外壳
硬盘的所有精密组件都被密封在一个金属外壳中。外壳内部是无尘的,任何微小的灰尘都可能损坏盘片和磁头。
1.9 图示
1.10 CHS寻址
-
扇区是从磁盘读出和写⼊信息的最⼩单位,通常⼤⼩为 512 字节。
-
磁头(head)数:每个盘⽚⼀般有上下两⾯,分别对应1个磁头,共2个磁头。
-
磁道(track)数:磁道是从盘⽚外圈往内圈编号0磁道,1磁道…,靠近主轴的同⼼圆⽤于停靠磁头,不存储数据。
-
柱⾯(cylinder)数:磁道构成柱⾯,数量上等同于磁道个数。
-
扇区(sector)数:每个磁道都被切分成很多扇形区域,每道的扇区数量相同。
-
圆盘(platter)数:就是盘⽚的数量。
-
磁盘容量 = 磁头数 × 磁道(柱⾯)数 × 每道扇区数 × 每扇区字节数。
CHS是三个参数的缩写,代表了一个扇区在磁盘上的物理位置:
-
C - Cylinder(柱面):所有盘片上半径相同的磁道组成的集合。可以想象为以主轴为圆心的一系列同心圆柱面。柱面号确定了磁头臂的径向位置。
-
H - Head(磁头):指定哪个磁头来读写。因为每个盘面都有一个磁头,所以磁头号实际上就指定了要使用哪一个盘面。
-
S - Sector(扇区):在确定的磁道上,指定从哪个扇区开始读写。扇区是磁盘最小的物理存储单元(通常为512字节)。
CHS寻址 就是通过给出 (C, H, S) 这三个坐标值,来唯一确定硬盘上的一个扇区。
2. Linux文件系统
2.1 概念
首先,传动臂上的磁头是共进退的,柱面是一个逻辑上的概念,本质是每一面,相同半径的磁道逻辑上构成柱面,逻辑上,磁盘是由柱面卷起来的。
-
某一盘面上的磁道按扇区展开(一维数组)。
-
整个磁盘所有盘面的同一个磁道,即柱面展开(二维数组)。
- 柱面上的每个磁道,扇区数是一样的。
-
一个磁盘是由N个柱面组成的(三维数组)。
-
三维数组本质也是一维数组,所以每一个扇区都有一个对应的数组下标,称为LBA(Logical Block Address),所以在操作系统文件系统概念中只有LBA地址,LBA地址向CHS地址的转换由硬件自己完成。
-
所以磁盘逻辑结构是由一个一个柱面展开并连接在一起的,形成一个一维数组。
2.2 CHS和LBA地址
2.2.1 CHS转LBA
- LBA=柱⾯号C∗单个柱⾯的扇区总数+磁头号H∗每磁道扇区数+扇区号S−1LBA = 柱⾯号C*单个柱⾯的扇区总数 + 磁头号H*每磁道扇区数 + 扇区号S - 1LBA=柱⾯号C∗单个柱⾯的扇区总数+磁头号H∗每磁道扇区数+扇区号S−1
2.2.2 LBA转CHS
-
柱⾯号C=LBA//(磁头数∗每磁道扇区数)【就是单个柱⾯的扇区总数】柱⾯号C = LBA // (磁头数*每磁道扇区数)【就是单个柱⾯的扇区总数】柱⾯号C=LBA//(磁头数∗每磁道扇区数)【就是单个柱⾯的扇区总数】
-
磁头号H=(LBA%(磁头数∗每磁道扇区数))//每磁道扇区数磁头号H = (LBA \% (磁头数*每磁道扇区数)) // 每磁道扇区数磁头号H=(LBA%(磁头数∗每磁道扇区数))//每磁道扇区数
-
扇区号S=(LBA%每磁道扇区数)+1扇区号S = (LBA \% 每磁道扇区数) + 1扇区号S=(LBA%每磁道扇区数)+1
//: 表⽰除取整。
LBA地址是从0开始,CHS地址是从1开始。
2.3 分块
操作系统为了管理方便,会将一个或多个连续的扇区组合起来(通常为8个扇区4KB)
为什么要分块?
-
减少管理开销。
-
一次性读写一个较大的块,比多次读写小单元更能减少IO次数。
2.4 分区
文件系统分区是指将物理存储设备(如硬盘、SSD)划分为多个独立的、逻辑上隔离的区块,每个区块单独格式化并挂载到 Linux 目录树的特定位置(如/
、/home
),从而实现对存储资源的有序管理。
为什么要分区?
- 将存储资源拆分,降低管理复杂度。
2.5 分组
2.5.1 为什么要分组?
想象一下,如果没有分组,整个硬盘就像一个巨大的、没有分隔的房间。所有文件的数据块(data blocks)和文件属性集合(inodes)都混杂在一起。磁头需要在整个盘片上来回大幅度移动来读写文件,这非常低效(产生大量寻道时间),并且容易产生碎片。
因此,磁盘被先分区,再分组,每个组相对独立,拥有自己的内部结构。管理的精髓在于可复制性。成功管理一个组,便掌握了管理所有组的核心能力;能管理好所有组,便意味着能管理好一个分区;
2.5.2 理解inode
-
Linux下文件的存储是属性和内容分离的。
-
Linux下,保存文件属性的集合叫做inode,一个文件,一个inode,通过inode编号唯一标识。
一个 inode 主要包含以下元信息,也就是文件属性(可以使用 stat
filename 命令查看):
-
inode 编号:每个 inode 在它所在的文件系统中都有唯一的编号。
-
文件数据的磁盘块地址:指向存储文件内容的那些数据块(Data Blocks)的指针。这是 inode 最关键的作用。
-
文件大小:字节数。
-
文件的拥有者 User ID (UID):哪个用户拥有这个文件。
-
文件的所属组 Group ID (GID):哪个用户组拥有这个文件。
- 文件的访问权限:读、写、执行的权限(rwx for user, group, others)。
-
文件的时间戳:
-
访问时间 (Access):最后一次读取文件内容的时间。
-
修改时间 (Modify):最后一次修改文件内容的时间。
-
改变时间 (Change):最后一次改变文件属性(如权限、所有者) 的时间。
-
-
链接数:有多少个文件名指向这个 inode(后面硬链接会讲到,可以理解为引用计数)。
-
其他信息:如设备文件(/dev/)的主设备号和次设备号等。
特别注意:
-
inode 里面唯独不包含文件的名称! 文件名是存放在目录文件里的。
-
任何文件的内容大小可以不同,但是属性大小一定是相同的(通常是128或256字节)。
2.5.3 目录和文件名在哪里?
理解了 inode 不存文件名后,自然会有这个疑问。答案是:在目录里。
目录也是文件,但是磁盘上没有目录的概念,只有文件属性 + 文件内容的概念。
目录(Directory)本身也是一个文件,它也有自己的 inode 和数据块。它的数据块里的内容非常简单,就是一个 文件名 和 inode 映射表:
文件名 (Filename) | inode 编号 (inode number) |
---|---|
report.txt | 256790 |
cat.jpg | 256791 |
music.mp3 | 256792 |
所以,访问文件,必须打开当前目录,根据文件名,获取对应的inode号,然后进行文件的访问。
当你执行 ls -l
时,系统是这样做的:
-
读取当前目录文件的内容,得到一系列
文件名
和对应的inode 号
。 -
根据每个
inode 号
去找到文件的inode
信息。 -
从
inode
信息中读出文件的权限、所有者、大小等,并和文件名一起显示给你。
2.5.4 Linux路径解析
当你访问 /home/user/report.txt
时,系统会:
-
在根目录
/
的映射表里找到home
对应的 inode 号。 -
根据 home 文件名的 inode 号找到
/home
目录的数据块,在里面找到user
文件名对应的 inode 号。 -
再根据 user 文件名的 inode 号找到
/home/user
目录的数据块,在里面找到report.txt
文件名对应的 inode 号(比如 256790)。 -
最后,根据 inode 号 256790 找到文件的 inode 信息,再根据 inode 信息里的指针找到文件的数据块,读取内容。
2.5.5 Linux dentry(历史路径缓存)
这是解决路径解析性能问题的最关键武器。
-
dentry
(directory entry)是内核在内存中构建的一个数据结构,用于表示路径中的一个组成部分(如home
,user
,report.txt
)。 -
它建立了文件名到 inode 的映射关系。
-
它本身不存储文件数据,甚至不存储完整的元数据,只是一个快速的文件名 -> inode 的查找结构。
-
每个文件都要有对应的dentry结构,在内存中形成整个树形结构。
-
整个树形节点也同时会⾪属于LRU(Least Recently Used,最近最少使⽤)结构中,进⾏节点淘汰。
-
整个树形节点也同时会⾪属于Hash,⽅便快速查找。
扩展:树的构建过程(以 /home/alice/file.txt
为例)
让我们看看这棵树是如何一步步在内存中建立起来的。假设系统启动后,首次访问这个路径。
第0步:树的根
内核启动后,会为根目录 /
创建一个 dentry 对象(我们称之为 dentry_root
)。它是这棵树的根,它的 d_parent
指向它自己。
第1步:解析 /home
-
进程请求打开
/home/alice/file.txt
。 -
解析器从根
dentry_root
开始。 -
它查看
dentry_root
的 inode,并读取/
目录的内容(从磁盘或缓存)。 -
它在目录内容中查找字符串
"home"
。 -
找到后,内核为
home
创建一个新的 dentry 对象(dentry_home
)。-
设置
dentry_home->d_parent = dentry_root
(home
的父目录是/
)。 -
将
dentry_home
添加到dentry_root
的子目录链表(d_subdirs
)中。 -
将
"home"
这个名称和dentry_home
的映射关系,存入一个高效的哈希表中以便快速查找。
-
-
现在树的结构是:
dentry_root(name:/) -> dentry_home(name:home)
第2步:解析 alice
-
解析器现在位于
dentry_home
。 -
它查看
dentry_home
的 inode,并读取/home
目录的内容。 -
在内容中查找字符串
"alice"
。 -
找到后,内核为
alice
创建一个新的 dentry 对象(dentry_alice
)。-
设置
dentry_alice->d_parent = dentry_home
(alice
的父目录是home
)。 -
将
dentry_alice
添加到dentry_home
的子目录链表中。 -
在哈希表中记录
"alice"
到dentry_alice
的映射。
-
-
现在的树结构是:
dentry_root(name:/) -> dentry_home(name:home) -> dentry_alice(name:alice)
第3步:解析 file.txt
-
解析器现在位于
dentry_alice
。 -
它读取
/home/alice
目录的内容。 -
在内容中查找字符串
"file.txt"
。 -
找到后,内核为
file.txt
创建一个新的 dentry 对象(dentry_file
)。-
设置
dentry_file->d_parent = dentry_alice
(file.txt
的父目录是alice
)。 -
将
dentry_file
添加到dentry_alice
的子目录链表中。 -
在哈希表中记录
"file.txt"
到dentry_file
的映射。
-
-
最终的树结构形成:
dentry_root(name:/) -> dentry_home(name:home) -> dentry_alice(name:alice) -> dentry_file(name:file.txt)
树的查询过程(第二次访问)
现在,另一个进程请求打开 /home/alice/file.txt
。此时,完整的路径树已经在 dentry cache 中存在。
解析过程不再是昂贵的磁盘I/O遍历,而是变成了高效的内存树遍历:
-
解析器从
dentry_root
开始。 -
它不会去读磁盘上的
/
目录,而是直接查看dentry_root
的子目录链表(或者更常见的是,直接查询哈希表:“home”
对应的 dentry 是什么?)。 -
哈希表查找:内核使用一个全局的哈希表。它计算字符串
"home"
的哈希值,并在哈希桶中找到dentry_home
。命中! -
解析器跳到
dentry_home
。 -
同样,它计算
“alice”
的哈希值,在哈希表中查找,直接找到dentry_alice
。再次命中! -
解析器跳到
dentry_alice
。 -
计算
“file.txt”
的哈希值,在哈希表中找到dentry_file
。最终命中! -
通过
dentry_file
找到对应的 inode,完成路径解析。
2.5.4 为什么需要 inode?/ inode 的好处?
-
解耦文件名与数据:
- 使用 inode 的核心目的是将文件的元数据与文件的数据本身分离开来。这种设计带来了巨大的灵活性和强大的功能。想象一下如果没有 inode,文件名、权限、数据位置等信息都必须和文件数据紧耦合地放在一起,会非常难以管理。
- 硬链接(Hard Link):创建硬链接就是在另一个目录里增加一个不同的文件名,但指向同一个 inode 号。两个文件名完全平等,删除其中一个,另一个依然能访问数据(只要 inode 的链接数不为 0,本质上就是引用计数)。
-
权限和访问控制:所有权限信息都存储在 inode 里,与文件名分离,安全且统一。
-
高效遍历:系统通过 inode 号来查找文件,比通过冗长的路径名查找要快得多。
2.6 文件系统块组的内部构成
这是文件系统设计中最精妙的部分。Linux文件系统将整个分区划分为多个块组 (Block Groups)。每个块组是自包含的,拥有自己管理文件和数据所需的元数据。
2.6.1 超级块(Super Block)
存放⽂件系统本⾝的结构信息,描述整个分区的⽂件系统信息。记录的信息主要有:Data Bolcks 和 inode 的总量,未使⽤的 Data Blocks 和 inode 的数量(标识的是整个分区的),⼀个 Data Blocks 和 inode 的⼤⼩,最近⼀次挂载的时间,最近⼀次写⼊数据的时间,最近⼀次检验磁盘的时间等其他⽂件系统的相关信息。Super Block的信息被破坏,可以说整个⽂件系统结构就被破坏了。
并非每个组中都存在一个完整的超级块,但是会在多个块组中备份,为了保证因为硬件出现物理问题时,导致的 Super Block 损坏,还能继续正常使用。
2.6.2 块组(Group Descriptor Table)
块组描述符表,描述块组属性信息,整个分区分成多个块组就对应有多少个块组描述符。每个块组描述符存储⼀个块组的描述信息,如在这个块组中从哪⾥开始是inode Table,从哪⾥开始是Data Blocks,空闲的inode和数据块还有多少个等等(标识的是当前这组的)。块组描述符在每个块组的开头都有⼀份拷⻉。
2.6.3 块位图(Block Bitmap)
Block Bitmap中每一位(bit)对应本组内的一个数据块(Data Blocks)。1
表示该块已分配使用,0
表示空闲。
2.6.4 inode位图(inode Bitmap)
inode Bitmap中每一位(bit)对应本组内的一个inode。
2.6.5 inode表(inode Table)
存放⽂件属性 如 ⽂件⼤⼩,所有者,最近修改时间和当前分组所有inode属性的集合。inode编号以分区为单位,不可跨分区。
-
最重要的:指向文件数据块的指针。
-
12直接块指针:直接指向12个数据块(12∗4KB12 * 4KB12∗4KB),适合小文件。
-
3间接指针:指向一个块,该块本身不存数据,而是存储指向数据块的指针。
-
一级间接块索引指针:1024∗4KB1024 * 4KB1024∗4KB
-
二级间接块索引指针:1024∗1024∗4KB1024 * 1024 * 4KB1024∗1024∗4KB
-
三级间接块索引指针:1024∗1024∗1024∗4KB1024 * 1024 * 1024 * 4KB1024∗1024∗1024∗4KB
-
-
以上按照一个块4KB,指针大小4Byte计算的。
2.6.6 数据块(Data Blocks)
真正存储文件内容和目录结构的地方。目录文件的数据块里并不直接存储文件,而是存储一个类似<inode编号> <文件名>
的列表(映射关系)。这就实现了文件名与文件本身的解耦。
Data Blocks编号以分区为单位,不可跨分区。
2.7 挂载分区
假设你有一块新硬盘,分了区(比如 sdb1
),并格式化了文件系统(比如 EXT4)。现在你想用它来存放你的文件。
创建挂载点:你需要一“入口目录。通常我们会在 /mnt
或 /media
下创建。
sudo mkdir /mnt/dir_name
挂载:使用 mount
命令将分区 sdb1
挂载到刚创建的目录上。
sudo mount /dev/sdb1 /mnt/dir_name
访问:现在,当你进入 /mnt/dir_name
目录时,你看到的就是分区 sdb1
里的内容。你在这里新建一个 file.txt
文件,这个文件实际就写在了 sdb1
这个物理硬盘分区上。
卸载:当你不用时,可以卸载它。卸载后,目录 /mnt/dir_name
又变回一个普通的空目录。
sudo umount /mnt/my_movies
结论:
-
分区写⼊⽂件系统,⽆法直接使⽤,需要和指定的⽬录关联,进⾏挂载才能使⽤。
-
所以,可以根据访问⽬标⽂件的路径前缀准确判断我在哪⼀个分区。