10【认识文件系统】
1 认识硬件——磁盘
1.1 物理构成
磁盘是计算机中唯一的机械设备,同时也是一种外部存储设备(外设)。早期的计算机通常配备的是机械硬盘(HDD),依靠磁头和盘片的机械运动来进行数据的读写。但随着用户对计算机速度要求的不断提高,如今大多数个人电脑已普遍采用SSD(固态硬盘)。
相比机械硬盘,SSD没有机械运动部件,具有读写速度快、体积小、低功耗、耐用性好、无噪音等优点,且仍有广阔的技术发展空间。因此,在桌面电脑领域,SSD几乎已经全面取代了机械硬盘。
但在企业级存储领域,机械硬盘仍被大量使用,主要因为其低成本、大容量的特点,非常适合海量数据的长期存储,短期内仍难以被完全淘汰。
1.2 磁盘的工作原理及特点
磁盘的核心工作原理是:二进制序列通过磁头的充放电过程写入盘片。任何硬件本质上只能识别二进制信号(高低电平),磁头通过充电或放电,将电信号转化为磁信号,改变盘片表面磁性区域的排列,进而完成数据的写入。
主要特点:
- 计算机内部信息流动通常以电子或光电信号形式高速传输,而磁盘依靠机械运动,速度相对较慢。
- 磁盘工作时,盘片高速旋转,磁头沿盘面半径方向微调位置,但始终悬浮于盘面之上,不直接接触。
- 磁盘制造必须在无尘密封环境中进行,灰尘若进入磁盘,会划伤盘面,造成数据丢失。
- 内存(RAM)属于易失性存储,掉电后数据丢失,而磁盘则是持久性存储,即使断电,数据仍可保存。
补充:磁盘是有寿命的,在企业级存储中,磁盘接近报废时不会随意丢弃,因为盘内存有大量敏感数据。磁盘密封性强,不易物理销毁,因此大公司通常会通过专业安全部门,采用严格流程销毁磁盘,以防数据泄露风险。
1.3 存储构成
1.3.1 磁盘的基本工作方式
- 磁头的左右摆动:磁头臂在磁盘表面径向移动,定位到特定的磁道(柱面)
- 盘片的旋转:当磁头静止在某个磁道上时,高速旋转的盘片使目标扇区经过磁头下方
1.3.2 存储的最小单位:扇区
磁盘被访问的最基本单位是扇区,传统大小为512字节,现代高级格式磁盘通常采用4KB扇区。基于这一特性,我们可以将磁盘视为由无数个扇区构成的线性存储介质。
1.3.3 三维定位:如何访问一个扇区
- 磁头选择:确定使用哪个盘面(对于多碟片磁盘)
- 磁道定位:在选定盘面上找到具体的磁道(柱面)
- 扇区识别:在旋转的磁道上等待目标扇区经过磁头
1.4 逻辑抽象
1.4.1 磁带的线性存储特性
从物理结构来看,磁带是一种顺序访问的存储介质,数据以线性方式排列在磁带上。如果我们将磁带逻辑上摊开,可以将其抽象为一个连续的线性存储空间。这种抽象允许我们使用逻辑块寻址(LBA, Logical Block Addressing)的方式来组织和管理磁带上的数据。
1.4.2 逻辑扇区与LBA地址
- 每个逻辑扇区(通常为固定大小,如512字节或4KB)对应磁带上的一个存储单元
- 通过LBA地址(即逻辑扇区号,类似于数组下标)唯一标识每个扇区
- 整个磁带可视为一个由LBA地址索引的大数组,支持随机访问的逻辑模型(尽管物理上仍需顺序访问)
问题1:如何通过下标找到我们要写入的扇区?
假设有以下磁盘参数:
- 每个盘面有2w个扇区
- 每个盘面有50个磁道(50个圈)
- 每个磁道有400个扇区(每一圈分了400份)
以LBA地址28888为例:
- 28888 / 20000 = 1
- 28888 % 20000 = 8888
- 8888 / 400 = 22
- 8888 % 400 = 88
- 第一个磁盘(下标从0开始)的第22个磁道的第88个扇区。
- Cylinder Header Sector ==> CHS寻址方式
- C = 22 ;H = 1;S = 88
问题2:为什么扇区大小不均匀,但 LBA 地址是均匀的?
因为磁盘的磁道是从中心向外辐射分布的,靠近中心的磁道周长小,外圈磁道周长大。在早期磁盘设计中,为了简化控制逻辑,通常会采用固定扇区数设计,即每个磁道分配相同数量的扇区。但这样导致内圈扇区物理尺寸小,外圈扇区物理尺寸大,扇区大小在物理空间上是不均匀的。
通过调整数据编码密度(比如让内圈磁道的数据编码更稠密,外圈更稀疏),理论上可以让每个扇区实际存储的数据量更均衡。现在的磁盘也普遍采用了这种区段分区(Zoned Recording)技术,让外圈磁道能存储更多数据,提升存储效率。
至于LBA(Logical Block Addressing)地址,它是逻辑地址,按“块”递增编号,与物理扇区大小无关,映射层可以屏蔽底层磁道结构差异,保证逻辑地址空间连续、均匀,方便操作系统统一管理磁盘数据。
当然,如果对物理扇区大小进行调整,底层算法和控制逻辑也需要相应适配,现有磁盘固件已能很好支持这种变化。
1.4.3 回归硬件
问题:我们究竟是如何和硬件交互的?
虽然大多数人只接触操作系统层面的文件操作(如 read / write),但在底层,其实我们与硬件的交互是通过“寄存器”完成的 —— 不止 CPU 有寄存器,所有外设也都有自己的寄存器集,磁盘也不例外。
当 CPU 向磁盘发起一次 I/O 请求时,交互过程大致如下:
- 控制寄存器(Control Register):CPU 通过它告诉磁盘“我要做什么”——是要读数据,还是要写数据。也可能指定其他控制位,如启动位、复位位等。(告诉磁盘是打算读还是打算写)
- 数据寄存器(Data Register):用于传输数据。如果是写操作,CPU 会将要写入的数据写入磁盘的数据寄存器;如果是读操作,磁盘会将读出的数据放入此寄存器,由 CPU 读取。((告诉磁盘要写入哪些数据,读那些数据)
- 地址寄存器(Address Register):用于告诉磁盘“要读/写哪一块数据”,一般是使用逻辑块地址(LBA)表示。磁盘控制器会把 LBA 转换为实际的 CHS(柱面-磁头-扇区)地址,定位到具体的物理位置。
- 状态/结果寄存器(Status Register):用于反馈 I/O 操作是否成功,或当前磁盘状态(如是否就绪、是否发生错误、空间是否不足等)。CPU 通过轮询、或中断机制读取此寄存器判断操作结果。
2 文件系统
通过逻辑抽象,虽然我们可以把磁盘扇区的地址抽象成线性的 LBA 地址,但实际操作系统要处理很多关键问题,比如:
- 磁盘有多大?
- 已用了多少扇区?
- 哪些扇区还没用?
- 哪些扇区存放的是元数据(如文件属性)?
- 哪些扇区存的是实际内容?
- 新写入的数据该写到哪个扇区?
这些问题决定了操作系统必须设计一套机制,把磁盘空间合理地组织和管理起来!
2.1 磁盘分区
首先,物理磁盘通常会被划分成多个分区,每个分区就像一个逻辑磁盘,独立管理。分区的划分可以灵活配置(如图所示)。
2.2 文件系统结构
每个分区内部,通常会被组织成若干 Block Group,而每个 Block Group 里又由多个区域组成,比如:
Boot Block:文件系统中的特殊块,位于起始位置,存放 Boot Loader 引导信息,作用是启动操作系统。
Block Group:分区内部会被划分为一个个 Block,Block 大小在格式化时确定,通常不可修改。
每个 Block Group 典型结构包括:Super Block、Group Descriptor、Block Bitmap、Inode Bitmap、Inode Table、Data Blocks
2.3 inode机制
- inode 存储文件的全部属性信息(比如权限、所有者、大小、修改时间),但不存储文件名!
- 每个文件都有唯一的 inode 号,inode 号是文件的唯一标识。
- 文件的实际内容存放在 Data Block 区域,inode 里会记录指向这些块的地址。
- 可以通过
ls -li
命令查看文件的 inode 编号。
2.4 Data Block
Data Block:是文件系统中用于存放文件实际内容的区域。数据以块(Block)为单位存储,常见的 Block 大小是 4KB,通常 一个块只存放自己的数据,不会混合多个文件的数据。
问题:一个文件对应多少个 Data Block?
- 每个文件在文件系统中只有一个 inode,inode 记录了文件的属性信息,同时也记录了 该文件的内容存放在哪些数据块里。
- 如果文件很大,可能需要占用很多个 Data Block,因此 inode 里需要有“指向数据块”的信息。
- 具体来说,inode 结构体里有一个 block 数组,用于存储数据块的编号(Block Number)。
问题:如果文件非常大,那 block 数组是不是要非常大?
不是的,block 数组通常设计得很有限,里面分为两种类型的指针:
- 一部分是直接索引(Direct Pointer),直接指向实际的数据块。
- 还有一部分是间接索引(Indirect Pointer):
- 一级间接索引:指向一个“块号列表”块,该块里存储更多数据块的块号;
- 二级间接索引、三级间接索引也是同理,逐层扩展。
- 通过这种直接+间接索引的设计,文件系统可以用很小的 inode 结构,支持非常大的文件,同时又不浪费空间。
block 数组并不需要无限增大,而是通过“多级间接索引”机制来高效管理大文件的数据块映射,兼顾了小文件的访问效率和大文件的存储能力!
2.5 Bitmap
操作系统如何知道磁盘里哪些块/哪些 inode 被用过、哪些还空闲?这就靠 位图(Bitmap) 来管理!
- Block Bitmap 记录着 Data Block 的使用情况:
- 每一个 bit 对应一个 Data Block
- bit=1 表示该块已被占用
- bit=0 表示该块空闲,可用
- 通过 Block Bitmap,文件系统可以快速找到空闲的数据块,分配给新文件或追加的文件内容
- inode Bitmap 记录着 inode 的使用情况:
- 每一个 bit 对应一个 inode
- bit=1 表示该 inode 已被占用(已有文件或目录)
- bit=0 表示该 inode 空闲,可用
- 文件系统创建新文件时,先查 inode Bitmap,找到一个空闲 inode,分配给新文件,建立文件属性信息。
问题1:为什么下载一个文件很久,删除一个文件却很快?
因为 删除文件时并不会真正“擦除”文件内容,操作系统只是在 Block Bitmap 和 inode Bitmap 里把对应 bit 标记为“空闲”,表示可以被新文件覆盖即可,整个过程非常快。相反,下载/写入文件需要实际分配 inode 和数据块,还要写入磁盘数据,过程自然更慢。
问题2:误删了文件,还能恢复吗?
其实误删后,文件内容本身还在磁盘上,只是对应的 Bitmap 标记为“可用”了。如果没有新数据覆盖,理论上是可以恢复的!
恢复过程一般是先找到文件的 inode 编号,读取 inode 里记录的 block 位置,重新拼接数据。通常需要依靠专业工具或人员,因为一旦新文件写入,会覆盖旧数据,恢复难度就变大。
Block Bitmap 和 inode Bitmap 是文件系统中非常关键的机制,通过简单高效的 bit 标记,帮助操作系统快速管理和分配磁盘空间,也是文件删除“秒删”背后的原因!
2.6 GDT (Group Descriptor Table)和超级块(Super Block)
在文件系统中,除了我们常说的 inode 和 Data Block,其实还有一些 “元信息” 结构,来描述整个文件系统的组织和状态,GDT 和超级块就是最核心的两部分:
- GDT(Group Descriptor Table,块组描述符表)
- 该组的数据块使用情况
- 该组的 inode 使用情况
- 该组内各区域的位置(如 Bitmap、inode 表)
- 可以理解为 “块组的小管家”,帮助系统快速定位某个组内资源。
- Super Block(超级块)
- 总共有多少 block / inode
- 当前未用的 block / inode 数量
- block / inode 的大小
- 最近挂载、修改、检查的时间
- 其他文件系统配置信息
- Super Block 是整个文件系统的“核心配置表”,如果它损坏,整个文件系统就无法正常工作。
超级块的备份机制
- 虽然 Super Block 是存在块组内部的,但是它记录的是整个分区的信息,理论上一个 Super Block 足够。
- 但因为 Super Block 极为重要,一旦损坏文件系统就崩,所以文件系统一般会在分区内的多个组中存放 Super Block 的备份。
- 这样即使主 Super Block 损坏,操作系统也可以用备份 Super Block 来恢复文件系统结构。
问题:操作系统怎么找到超级块?它在那么多块里怎么定位?
靠“魔数”,超级块内部有一个固定偏移位置,存放一个特殊值,叫做 魔数(Magic Number),这个魔数是文件系统格式定义的唯一标识。操作系统读取磁盘块时,只要在固定偏移处找到正确的魔数,就知道这个块是超级块,从而完成定位。
GDT 负责管理块组内的资源,超级块负责记录整个文件系统的元信息,魔数帮助操作系统准确定位超级块,保证文件系统的可靠性和稳定性!
2.7 格式化
在一个分区使用之前,必须先格式化 ——也就是提前将一部分文件系统的属性信息(比如超级块、GDT、Bitmap、inode 表等)写入分区对应的位置,这样才能保证后续系统在使用该分区时,知道如何管理磁盘空间,如何存取数据。
另外,格式化也可以让磁盘恢复到“未使用”状态,Bitmap 置零,逻辑上所有空间重新可用,方便新的数据写入。
3 对目录的理解
3.1 新建 & 删除文件,系统背后做了什么
新建文件
- 系统首先会根据路径信息,定位到对应的分区,读取该分区的超级块。
- 超级块里知道当前 block 总数、inode 总数等基础信息。
- 系统再通过 GDT 确认该分区中哪些 block group 状态最好(空间最多、inode 最空闲)。
- 然后通过 inode Bitmap 找到一个空闲 inode,分配给新文件,并填写文件属性信息。
- 如果文件需要写入数据,还会根据内容大小,确定需要多少 数据块,通过 Block Bitmap 查找空闲块,把块号写入 inode 结构体的 block 数组里,最后把文件内容写入这些数据块中。
一句话总结:新建文件,其实就是分配 inode + 分配数据块 + 填写属性 + 写入数据。
删除文件
- 系统同样先根据路径定位分区,找到对应的 inode。
- 然后修改 inode Bitmap 和 Block Bitmap,把对应位置的 bit 置 0,表示 inode 和数据块已释放,可再次使用。
- 文件数据实际上还在磁盘上,直到被新的文件覆盖。
一句话总结:删除文件,只是修改 Bitmap 标志位,数据本身不会立刻被清空。
3.2 目录文件
在 Linux 文件系统中,目录也是一种特殊的文件。它也有自己的 inode,也由 属性 + 内容 两部分组成。但它的内容部分不是普通数据,而是由“文件名 → inode编号” 的映射关系表组成,起到类似键值对的效果。
问题1:为什么一个目录下不能有同名文件?
因为在目录中,文件名是 key,inode 是 value,key 必须唯一。如果有两个文件名相同,系统将无法通过文件名准确查找 inode,因此这是被禁止的。
问题2:为什么没有写权限(w)无法创建文件?
因为创建文件时,系统要把“文件名 → inode”写入当前目录的内容区域。如果目录本身不可写,就无法建立这条映射关系,自然就无法创建新文件。
问题3:为什么没有读权限(r)无法查看目录内容?
因为查看目录其实是读取目录文件的内容,只有具备读权限,系统才能读出文件名 → inode 的映射,进而显示目录下有哪些文件。没有读权限,系统就“看不到”这个映射表,自然就“看不到”目录下的文件。
问题4:为什么没有执行权限(x)无法进入目录?
因为进入一个目录本质上是对该目录执行“路径解析 + 目录跳转”的操作,系统需要“执行”该目录条目,将其 inode 载入并更新工作目录。没有执行权限,系统无法完成这一步,就无法 cd 进入该目录。
问题5:如何找到目录本身的 inode?
目录文件本身也有 inode,但它不会出现在自己内部的映射里。要找到一个目录的 inode,需要从它的父目录读取其文件名对应的 inode 号,再从父目录的父目录……一路递归直到根目录 /,再反向解析回目标目录路径。这也是为什么:访问一个文件必须指定路径(即目录树),因为只有路径才能层层查找到 inode,进而找到目标文件。
3.3 dentry缓存(扩展)
在 Linux 文件系统中,访问一个文件必须先从路径解析出对应的 inode,而路径是分层的,也就是:
/home/user/docs/file.txt → 要先找到 / → 然后找 home → 再找 user → 最后找到 file.txt
而每一级目录都需要读取目录文件,找到对应文件名的 inode,这个过程就涉及到不断查找、不断磁盘 IO,如果每次都从磁盘递归查找,效率会非常低!
所以Linux提供了dentry缓存,将常用文件的inode信息缓存起来!
dentry缓存,简称dcache,是Linux为了提高目录项对象的处理效率而设计的。它是一个slab cache,保存在全局变量dentry_cache中,用于保存目录项的缓存。dentry结构是一种含有指向父节点和子节点指针的双向结构,多个这样的双向结构构成一个内存里面的树状结构,也就是文件系统的目录结构在内存中的缓存了。
4 软硬链接
4.1 软链接
软连接(Symbolic Link),也称为符号链接,本质上是一个独立的文件,拥有自己的 inode 和数据块。它的内容并不是实际的数据,而是一个指向目标文件路径的字符串。可以类比为 Windows 系统中的快捷方式。
由于软链接只是路径的引用,如果原始文件被删除或移动,软链接就会“失效” —— 我们常说的“断链”(broken link),
4.1.1 应用场景举例
当一个可执行程序放在了某个路径很深的目录中(如 /usr/local/lib/xxx/target/bin/run.sh),每次手动执行很麻烦。我们可以在当前目录下创建一个指向它的软链接:
ln -s /usr/local/lib/xxx/target/bin/run.sh ./run
这样以后只要在当前目录下执行 ./run 就可以了,可执行文件的位置再深也无所谓,有软链接就能随时调用,非常适合常用工具的快速访问。
4.2 硬链接
和软链接不同,硬链接不是一个独立的文件,因为它没有独立的 inode。所谓“创建硬链接”,本质上就是:
- 在某个目录的数据块中新增一个“文件名 → inode编号”的映射关系,也就是说,只是给现有 inode 多取了一个“别名”。
- 多个硬链接共享同一个 inode 和同一组数据块,谁被打开其实都等价于访问原始文件。
4.2.1 硬链接下的引用计数机制
每个 inode 都有一个引用计数,表示有多少个目录项指向它。
问题1:为什么一个目录的引用计数是 2
- 因为一个目录创建完成后会包含两个默认的子项:
.
→ 指向自己..
→ 指向父目录
问题2:当目录下再新建一个文件,为什么引用计数变为 3
- 因为新建的文件会指向该目录作为“父目录”,即该目录被
..
多引用了一次,所以引用计数加 1。
补充:
- 某个目录的引用计数 - 2 = 它包含的实际子目录数量
- 删除文件时,系统会先删除该文件名与 inode 的映射,然后 inode 的引用计数减 1,只有当引用计数减为 0,才会真正释放对应的 inode 和数据块(即清空位图)
问题3:为什么 Linux 不允许对目录建立硬链接?
操作系统禁止用户对目录创建硬链接,最主要的原因是:防止目录结构出现“环”!
Linux 的路径解析是递归向上回溯到根目录,再逐层向下查找回来。如果在中间某一层手动创建了一个指向“上级目录”的硬链接,可能会造成无限回溯,形成死循环,文件系统就崩了。
问题4:但是 .
和 ..
不就是对目录的硬链接吗?
.
和 ..
本质上确实是目录的硬链接!
- 但是这两个特殊链接是由操作系统自动创建的,我们无法手动创建;
- 操作系统还强制规定路径查找时不会对
.
和..
做“循环遍历”,所以不会构成环; - 它们的存在是为了支持相对路径的便捷访问,否则用户只能用繁琐的绝对路径定位文件。
硬链接应用场景:通常用来做路径定位!!可以通过硬链接进行目录切换!(不常用)