ARTICLE DETAIL

建站实战干货

来自一线的建站与推广经验沉淀,每一条都经过真实交付验证。

【Linux】磁盘与文件系统

2026/8/16 7:42:31 拓冰建站 浏览量
【Linux】磁盘与文件系统

1、理解硬件

1.1 磁盘

这里的磁盘指的是机械磁盘,并不是笔记本上的SSD固态硬盘

机械磁盘是计算机中唯一的一个机械设备,磁盘属于外设,速度慢

磁盘的物理结构:

磁盘有两个面,磁头一面一个,磁头与高速旋转的盘面不接触,磁盘存储数据的原理是磁盘表面分布数以亿计的微型磁铁,每个磁畴都有固定的南北两极,北极朝上代表二进制1,南极朝上代表二进制0,磁盘存储0、1的本质,就是控制磁畴的磁极朝向。

盘片的正反两面都可以存储数据,磁头相当于读写笔,负责读取盘片数据,修改盘片数据,工作时,盘片高速旋转,磁头悬浮摆动,二者配合完成读写,通过电磁感应改变盘片磁畴的磁极方向,完成数据写入;通过感应磁极状态,完成数据读取

1.2 磁盘的存储结构

每一个磁盘由多个磁道构成,每一个磁道有多个扇区组成,扇区是磁盘读写的最小物理单位,标准大小为512字节,如果仅需修改扇区内少量数据,需先将512字节扇区数据读取到内存,修改完成后,再整体写回磁盘。因此磁盘被称为块设备

物理形态上,磁盘内圈扇区弧长更短、外圈扇区弧长更长,但存储容量完全一致(均为512字节)

柱面:柱面是磁盘多盘面堆叠形成的逻辑概念:所有盘面中,半径完全相同的磁道上下堆叠,形成一个圆柱形空间,这个整体就称为柱面,磁头运动目的是定位磁道或柱面,盘面旋转目的是定位扇区,所有的磁头都连在一根机械臂杆上,所有磁头共进退,所有、同步摆动,所有盘面同半径磁道可同时被精准定位

2、磁盘的抽象逻辑

如何定位一个扇区?

  • 可以先定位磁头(header)
  • 确定磁头要访问哪⼀个柱面(磁道)(cylinder)
  • 定位⼀个扇区(sector)
  • CHS地址定位

磁盘容量=磁头数 × 磁道(柱面)数 × 每道扇区数 × 每扇区字节数

磁盘的逻辑存储结构可以类似于:

这样每一个扇区就有了一个线性地址,这种地址叫做LBA,LBA本质就是数组下标

但真实情况是,磁盘在进行访问时,先确定我们要访问哪一个柱面

由于磁盘整体是由“柱面”卷起来的,所以,磁盘真实的情况是:

磁道:某一盘面的某一磁道展开:

即一维数组

柱面:整个磁盘所有盘面的同一个磁道,即柱面展开:

整个磁盘所有盘面的同一个磁道,柱面上的每个磁道,扇区个数是一样的,即二维数组

整盘

整张磁盘就是多张二维数组的扇区数组表组成的三维数组表

所以寻址一个扇区,先找到哪一个柱面(Cylinder),再确定柱面在哪一个磁道(磁头位置Head),再确定扇区(Sector),所以就有了CHS

CHS && LBA地址

LBA转成CHS地址:

  • 柱面号C=LBA//(磁头数*每磁道扇区数)【就是单个柱面的扇区总数】
  • 磁头号H = (LBA % (磁头数*每磁道扇区数)) // 每磁道扇区数
  • 扇区号S = (LBA % 每磁道扇区数) + 1
  • “//”表示除取整

eg:

磁盘参数:四个磁头,每个磁道四个扇区,共两个柱面,目标LBA地址=18

步骤1:单柱面扇区总数 = 4 × 4 = 16

步骤2:柱面号 C = 18 // 16 = 1(目标扇区在1号柱面)

步骤3:柱面内偏移 = 18 % 16 = 2

步骤4:磁头号 H = 2 // 4 = 0(目标扇区在0号磁头对应盘面)

步骤5:扇区号 S = 2 % 4 + 1 = 3(目标扇区为3号扇区)
最终转换结果:LBA=18 → CHS(1,0,3)

CHS转成LBA:

  • 磁头数*每磁道扇区数 = 单个柱面的扇区总数
  • LBA = 柱面号C单个柱面的扇区总数 + 磁头号H每磁道扇区数 + 扇区号S - 1
  • 即:LBA = 柱面号C*(磁头数每磁道扇区数) + 磁头号H每磁道扇区数 + 扇区号S - 1
  • 扇区号通常是从1开始的,而在LBA中,地址是从0开始的
  • 柱面和磁道都是从0开始编号的
  • 总柱面,磁道个数,扇区总数等信息,在磁盘内部会自动维护,上层开机的时候,会获取到这些参数

3、文件系统

3.1 块

硬盘是典型的“块”设备,操作系统读取硬盘数据的时候,其实是不会一个个扇区地读取,这样效率太低,而是一次性连续读取多个扇区,即一次性读取一个”块”

数据块是文件系统进行IO操作的最小逻辑单位,标准大小为4KB,即8个512字节的扇区

块号和LBA地址可以相互转换,知道LBA:块号 = LBA/8,知道块号:LAB=块号*8 + n.(n是块内第几个扇区)

分区

为了方便对磁盘进行管理,操作系统首先将其划分成若干个逻辑分区(如C盘、D盘),类似于将一个大国划分为多个省份进行治理

分组

在Linux文件系统中,每个分区会被进一步划分成多个块组(Block Group),这是一种分而治之的管理策略,每个组块内部结构相同,包含独立的元数据和数据区域,只要掌握了一个块组的管理方法,就可以通过复制(Ctrl+C, Ctrl+V)的方式管理所有块组,进而管理整个分区和磁盘

在完成分区和分组后,向每个块组中写入文件系统所需的管理信息的过程叫格式化

块组的结构:

Super Block:存放文件系统(File System)全局的信息,记录的信息主要有:bolck 和 inode的总量,未使用的block和inode的数量,一个block和inode的大小,最近一次挂载的时间,最近一次写入数据的时间,最近一次检验磁盘的时间等其他文件系统的相关信息,注意:不是每一个组都有Super Block,为了防止Super Block被破坏,Super Block被备份存入了不同分组中

GDT:块组描述符表,描述块组属性信息,整个分区分成多个块组就对应有多少个块组描述符。每个块组描述符存储一个块组的描述信息,如在这个块组中从哪里开始是inode Table,从哪⾥开始是Data Blocks,空闲的inode和数据块还有多少个等等

Block Bitmap:位图,记录Data Block中哪个位置的数据块被占用,值为1表示已分配,0表示空闲

Inode Bitmap:位图,用于跟踪Inode Table区域中每个inode的使用情况。比特位的位置对应inode编号,值为1表示已分配,0表示空闲。

Inode:Inode是一个固定大小(通常为128字节)的结构体,其中包含文件类型、权限、拥有者、大小、时间戳等有关文件属性的信息,注意:文件名不属于文件inode的属性

Inode Table:inode表,每一个inode存放单个文件的属性,每个incode有全局唯一incode编号,一个 4KB 块可以存放 32 个inode

最前面的数字就是inode编号

Data Block:存放文件内容,文件内容会根据其大小被分割并存储在一个或多个4KB的数据块中,数据块占据了块组中绝大部分的物理空间,每个块都有唯一的块号

所以,文件 = 文件内容 + 文件属性,文件内容存放在Data Block中,文件属性存放在Inode中

3.2 文件的创建机制

  • 创建空文件时,内核在内存中构建inode结构体,填充属性(不含文件名)
  • 将inode bitmap载入内存,遍历bit位,查找bit=0(空闲)的位置,分配inode编号
  • 将内存中的inode写入inode table对应位置
  • 如果想文件中写入数据,则在block bitmap中申请数据块,bit值为1,块号记录到inode映射表中,将用户数据写入这个data block

3.3 删除文件机制

删除文件并非真正删除数据,而是将文件对应的inode位(Inode Bitmap)和数据块位(Block Bitmap)从1置为0。这个过程非常快,因为只是修改了少量数据

3.4 查找文件和查看文件

  • ls -l:通过文件名找到inode编号 → 查inode table获取属性 → 打印
  • cat:通过文件名找到inode编号 → 读取inode中的块号列表 → 依次读取data block内容并输出

3.5 目录

上面的操作全部都基于inode编号,但用户操作一般来讲全部写文件名,不用inode号,而且inode结构体不存储文件名,原因是什么?

因为Linux下一切皆文件,所以目录也是文件,拥有自己的inode和data block,文件 = 文件内容 + 文件属性,目录文件的属性储存在inode中,目录文件的data block中存储的是一张映射表,以“文件名 ↔ inode编号”的映射对形式存在,所以文件名保存在它的父目录的data block中,不保存在自己的inode中

利用代码查看文件名与inode映射关系:

#include<stdio.h>#include<string.h>#include<stdlib.h>#include<dirent.h>#include<sys/types.h>#include<unistd.h>intmain(intargc,char*argv[]){if(argc!=2){fprintf(stderr,"Usage: %s <directory>\n",argv[0]);exit(EXIT_FAILURE);}DIR*dir=opendir(argv[1]);// 系统调用,自行查阅if(!dir){perror("opendir");exit(EXIT_FAILURE);}structdirent*entry;while((entry=readdir(dir))!=NULL){// 系统调用,自行查阅// Skip the "." and ".." directory entriesif(strcmp(entry->d_name,".")==0||strcmp(entry->d_name,"..")==0){continue;}printf("Filename: %s, Inode: %lu\n",entry->d_name,(unsignedlong)entry->d_ino);}closedir(dir);return0;}

4、重新理解inode编号和块号

ext 文件系统磁盘会划分多个块组 (block group),包含:super block、GDT、inode bitmap、block bitmap、inode table、data block

  1. inode编号和块号不是组内有效,而是整个分区全局有效且唯一,但不能跨分区
  2. 在一个分区内部,一个文件系统内部,有多少inode,有多少数据块,都是固定的,都是提前设计好的,已知inode编号,可以找到数据块号找到对应的group

在磁盘空间充足的条件下,可能也无法创建新文件,因为如果1个inode对应多个Data Block数据块(1:200或1:300),当inode还有空余时,数据块可能已经被用完,所以有时Inode Bitmap还有空余的位置,但Data Block已经用完,不能创建新文件

5、路径解析和缓存机制

5.1 路径解析

如果我想访问/home/me/lesson11/code.c,code.c的文件名保存在lesson11中,要拿到 test.c 的 inode,必须先打开lesson11目录,lesson11的目录要保存在父目录me的数据块中,要拿到 lesson11 的 inode,必须先打开me的目录,要拿到 me 的 inode,必须先打开 home 的目录,home的目录保存在根目录/,根目录会被Linux系统自动打开

所以,当我们要访问任何文件的时候,Linux内核都要为我们做从根目录/开始的路径解析,所以,访问文件必须要有路径

5.2 缓存机制

但如果每次访问文件都路径解析、全部读磁盘 IO,性能很差。内核引入dcache(dentry cache) 目录项缓存

同时,Linux要管理用户访问过的,打开过的路径节点,管理需要先描述,再组织,所以引入struct dentry管理

为避免每次访问都进行磁盘 I/O,Linux 内核维护一棵 dentry 多叉树缓存已访问过的路径节点

struct dentry为内核缓存路径,每一个被访问的文件/目录,在内核中都会生成一个dentry对象

dentry的成员:

  • d_name:文件名
  • d_inode:指针,指向内存中的 inode 对象
  • d_parent:指向父 dentry
  • 链表:兄弟节点链表;LRU 链表

这棵多叉树会动态变化,文件创建,删除等操作会是dentry增加或删除节点

不是只有目录才有dentry,每一个访问的文件都要有dentry,普通文件是 dentry 树的叶子节点

搜索的时候,第一次访问会比较慢,第二次访问会比较快,因为第一次打开文件时,内核要从根开始,逐级在磁盘查找每一级路径,磁盘 IO 慢,卡顿,同时把每一级路径全部生成dentry 放到内存缓存,第二次访问同一个路径会直接在内存dentry缓存树中查找,不需要读磁盘

注意dentry 缓存的目的是:把路径名→inode 的解析结果缓存,减少磁盘访问

解析相对路径时,操作系统会以进程当前工作目录的 dentry 作为查找起点,再向下遍历 dentry 树

5.3 用户调用open的过程

调用open("/home/me/test.c", ...)系统调用完整流程

  1. 从根目录(或者相对路径的 cwd)开始,遍历 dcache;缺失节点就读磁盘目录块,生成 dentry,加入 dcache 缓
  2. 得到目标文件对应的struct dentry
  3. 从 dentry 的d_inode拿到内存 inode
  4. 内核创建struct file对象,包含指向目标的dentry指针,当前读写位置等
  5. 在进程的文件描述符表分配一个空闲 fd 下标,指向这个struct file
  6. 返回 fd 给用户

5.4 路径由谁提供

访问任何文件,Linux内核都是先做路径解析和 dentry 目录项缓存,那么路径是由谁提供的?

  • 访问文件,都是指令/工具访问,本质是进程访问,进程有CWD为进程提供路径,而CWD默认是由bash提供,bash来自系统和环境变量的共同作用
  • 用户open文件,提供了路径

可是最开始的路径从哪里来?

  • 所以Linux为什么要有根目录,根目录下为什么要有那么多缺省目录?
  • 用户为什么要有家目录,用户可以新建目录?
  • 上面所有行为:本质就是在磁盘文件系统中,新建目录文件。而用户新建的任何文件,都在用户或者系统指定的目录下新建,这就是天然路径
  • 系统+用户共同构建Linux路径结构

6、存储大文件问题

场景:分区200GB,分为20个块组,每组10GB,如果想存50GB的大文件,单个块组放不下怎么办?

在inode结构体中有数组,一共15个元素,有指针,保存磁盘块号,用于关联文件数据块

前12个元素(0~11):直接指针,直接指向数据块,支持最多48KB文件

第13个元素(一级间接):该位置保存一个索引块的块号,这个索引块不存业务文件内容,这个 4KB 块里面全部存放其他数据块的块号,可以存储1024个块号,额外容纳4MB文件

第14个元素(二级间接):这个索引块里面存的是一级间接索引块的块号,共可支持 1024 × 4MB ≈ 4GB 文件

第15个元素(三级间接):三层索引嵌套,可存储 1024 × 4GB ≈ 4TB 文件

本质是inode的15项构成的一颗多叉树,索引块类似根节点,通过索引块间接扩展,支持巨型文件

7、硬链接

创建命令:

ln原文件 硬链接文件名#示例 ln log.txt log_hard

硬链接后,两个文件的inode一样,2表示硬链接数,记录指向该inode的文件名数量,硬链接的本质是在当前目录的数据块中增加一个“文件名 → inode”的映射关系,不创建新的inode,多个不同文件名,可以指向同一个 inode、同一套磁盘数据块

硬链接应用场景

  • 轻量备份,不需要拷贝完整文件数据,只新增文件名映射
  • 支撑Linux目录结构...

由上图可以看出lesson12中.的indoe和code.c中..的inode相同,所以.为当前目录的硬链接,..为上级目录的硬链接

限制:用户不能给目录创建硬链接,如果允许用户给目录创建硬链接,会制造环形路径,遍历目录会造成死循环(系统保留的.和…由内核特殊处理

8、软链接

创建命令:

ln-s目标文件路径 软链接名#示例 ln -s test.c test_soft

由上图可以看出,软链接是独立文件,拥有自己全新的 inode、自己的数据块,其内容为目标文件的路径字符串

访问软链接时,系统读取软链接内部保存的目标路径,自动跳转打开目标文件

软链接应用场景

~lesson13/test/dira/code中打印hello world,并编写成可执行程序,但每次访问时都要写入路径,效率低,所以使用软链接可以简化深层路径执行

利用软链接:

此时只使用软链接将复杂的路径~lesson13/test/dira/code变成了code_soft,提高了效率

删除软硬链接可以用指令:

unlink code_soft