Linux 系统 | 文件系统

Linux 操作系统之文件系统

1 概述

1.1 文件系统在内核中的位置

操作系统内核可分为若干功能子系统:进程调度、进程间通信、内存管理、网络接口、文件系统。文件系统是其中负责管理持久化数据的子系统。

进程间通信的常见方式包括:

  • 管道:匿名管道与命名管道(FIFO);
  • 消息队列;
  • 信号(signal);
  • 信号量;
  • 共享内存;
  • 套接字(socket):包括 Unix domain socket 与网络 socket。

其中:

  • 共享内存:内核在物理内存中预留一块区域,需要访问的进程将其映射到自己的地址空间。多个进程直接读写同一块内存,无须复制数据,效率较高。
  • 信号量:本质上是一个计数器,配合共享内存使用,用于控制多进程对共享数据的访问顺序,实现进程间同步。

文件系统知识框架如下图所示:

1.2 文件系统的定义与作用

文件系统是操作系统中负责管理持久化数据的子系统。它将用户文件存放在磁盘等持久化存储介质中,即使计算机关机断电,数据也不会丢失。

文件系统是管理文件与目录的一套机制。不同的组织方式形成不同的文件系统类型。文件系统的基本数据单位是文件,主要完成对磁盘上文件的创建、删除、修改、移动、重命名等组织与管理工作。

文件系统类型对单个文件与文件系统本身的大小存在上限约束,例如:

  • ext3:单个文件不超过 2 TiB;
  • ext4:单个文件最大 16 TiB(4 KiB 块时),文件系统最大 1 EiB;
  • FAT32:单个文件不超过 4 GiB。

1.3 一切皆文件

在 Linux 中,一切皆文件。普通文件、目录、块设备、管道、socket 等,都由文件系统统一管理。用户可以使用统一的系统调用接口(open、read、write、close 等)操作它们。

这一设计继承自 UNIX 哲学:把一切资源抽象为文件,用户以读写文件的方式访问硬件设备,这类特殊文件称为设备文件。

2 存储硬件基础

2.1 磁盘、硬盘与其他存储设备

磁盘(disk)是利用磁记录技术存储数据的存储器;硬盘(hard disk drive, HDD)是计算机最主要的存储设备,属于磁盘的一种,采用温彻斯特结构。软盘也属于磁盘,但已被淘汰;U 盘、固态硬盘(SSD)与磁盘的存储原理不同。

磁盘通过磁性进行读写,断电后数据不丢失;内存的存储载体与磁盘不同,断电后数据即丢失。

2.2 硬盘的物理结构

硬盘由涂覆磁性介质的金属盘片构成,每个盘片有两个盘面,每个面对应一个磁头(head)。盘片绕主轴高速旋转,磁头沿半径方向移动。关键物理概念:

概念 英文 说明
磁头 head 每个盘片一般有两个磁头,分别对应上下两个盘面
磁道 track 盘片表面以盘片中心为圆心的同心圆;从外圈向内圈编号为 0 磁道、1 磁道......靠近主轴的同心圆用于停靠磁头,不存储数据
柱面 cylinder 不同盘面上半径相同的所有磁道逻辑上构成一个柱面;柱面数等于磁道数
扇区 sector 每个磁道被切分为若干扇形区域,是磁盘读写的最小物理单位,传统大小为 512 字节
盘片 platter 圆盘数量即盘片数量

扇区是磁盘存储数据的基本单位,传统硬盘每个扇区 512 字节。磁头、柱面、扇区三者可唯一定位一个扇区,即 CHS(Cylinder-Head-Sector)寻址方式。磁盘容量计算公式为:

存储容量 = 磁头数 × 柱面数 × 每道扇区数 × 每扇区字节数 \text{存储容量} = \text{磁头数} \times \text{柱面数} \times \text{每道扇区数} \times \text{每扇区字节数} 存储容量=磁头数×柱面数×每道扇区数×每扇区字节数

信息记录可表示为:某柱面(磁道)、某磁头、某扇区。

传统 CHS 寻址用 8 bit 存储磁头地址、10 bit 存储柱面地址、6 bit 存储扇区地址,可寻址的最大容量为:

256 × 1024 × 64 × 512 B = 8 G i B 256 \times 1024 \times 64 \times 512\ \mathrm{B} = 8\ \mathrm{GiB} 256×1024×64×512 B=8 GiB

CHS 模式支持的硬盘容量有限,无法满足大容量硬盘的需求,因此现代硬盘普遍采用逻辑块寻址方式。

2.3 从 CHS 到 LBA:磁盘的逻辑结构

可以把磁盘类比为卷在一起的磁带:盘面上的同心圆磁道展开后是一条一维带,所有盘面的同一磁道(柱面)再依次展开,整个磁盘就等价于一个一维扇区数组。每个扇区获得一个线性下标,称为 LBA(Logical Block Address,逻辑块地址)。

LBA 与 CHS 可以相互转换。设每磁道扇区数为 S P T SPT SPT,磁头数为 H max ⁡ H_{\max} Hmax,则单个柱面的扇区总数为 H max ⁡ × S P T H_{\max} \times SPT Hmax×SPT。CHS 转换为 LBA(扇区号 S S S 从 1 开始,柱面号 C C C、磁头号 H H H 从 0 开始):

L B A = C × ( H max ⁡ × S P T ) + H × S P T + ( S − 1 ) C = ⌊ L B A H max ⁡ × S P T ⌋ H = ⌊ L B A   m o d   ( H max ⁡ × S P T ) S P T ⌋ S = ( L B A   m o d   S P T ) + 1 \begin{aligned} LBA &= C \times \left( H_{\max} \times SPT \right) + H \times SPT + \left( S - 1 \right) \\ C &= \left\lfloor \frac{LBA}{H_{\max} \times SPT} \right\rfloor \\ H &= \left\lfloor \frac{LBA \bmod \left( H_{\max} \times SPT \right)}{SPT} \right\rfloor \\ S &= \left( LBA \bmod SPT \right) + 1 \end{aligned} LBACHS=C×(Hmax×SPT)+H×SPT+(S−1)=⌊Hmax×SPTLBA⌋=⌊SPTLBAmod(Hmax×SPT)⌋=(LBAmodSPT)+1

磁盘内部自行维护上述参数与转换关系。对操作系统而言,磁盘就是一个以扇区为元素的一维数组,数组下标即 LBA,用一个整数即可访问任意扇区,无须关心物理位置。

2.4 块:文件系统的读写单位

磁盘是典型的块设备。操作系统读写硬盘时不会逐个扇区读取,而是一次性连续读取多个扇区,即读取一个"块"(block)。块大小在格式化时确定且不可更改,最常见为 4 KiB,即连续 8 个扇区组成一个块;块是文件存取的最小单位。

扇区与块的地址换算关系为:

块号 = ⌊ L B A 8 ⌋ L B A = 块号 × 8 + n ( n = 0 , 1 , ... , 7 ) \begin{aligned} \text{块号} &= \left\lfloor \frac{LBA}{8} \right\rfloor \\ LBA &= \text{块号} \times 8 + n \qquad ( n = 0, 1, \ldots, 7 ) \end{aligned} 块号LBA=⌊8LBA⌋=块号×8+n(n=0,1,...,7)

其中 n n n 为块内 n n n 个扇区。

2.5 硬盘的类型

按存储介质与原理,硬盘分为两类:

  • 固态硬盘(SSD):以闪存芯片为存储介质,属于半导体存储,不存在机械动作;由控制芯片、缓存芯片和闪存芯片组成 PCB。特点是读写速度快、抗震、功耗低,单位容量价格较高。
  • 机械硬盘(HDD):通过磁性介质读写,属于电磁存储,存在机械运转部件;特点是容量大、单位价格低,但读写速度低于 SSD,且怕震动。

磁盘是计算机中主要的存储介质,容量大、价格便宜但速度慢;机械磁盘是计算机中唯一的机械设备。

2.6 硬盘接口

硬盘接口是硬盘与主机系统之间的连接部件,负责在硬盘缓存与主机内存之间传输数据,直接影响系统的整体性能。常见接口:

接口 说明
IDE(PATA) 早期并行接口,已淘汰
SCSI 小型计算机系统接口,常用于服务器
SATA 串行 ATA,SATA 3.0 速率约 600 MB/s
SAS 串行 SCSI,用于企业级存储,向下兼容 SATA
NVMe 基于 PCIe 总线,面向 SSD,远高于 SATA 的带宽与时延性能
光纤通道 用于存储区域网络(SAN)

2.7 分区与分区表

一块磁盘可以划分为多个分区(partition),分区实质上是对硬盘的一种格式化。柱面(在 LBA 设备上等价于连续块区间)是分区的最小单位,分区本质上是设置每个分区的起始与结束位置。柱面大小一致、每柱面扇区数一致,因此只要记录起始与结束位置即可确定分区大小及其块范围。

分区表主要有两种格式:

2.7.1 MBR

MBR(Master Boot Record)分区表中,第一个扇区最为重要,包含:

  • 主引导记录(boot code),占 446 字节;
  • 分区表(partition table),占 64 字节,分为 4 个 16 字节的分区项;
  • 结束标志 0x55AA,占 2 字节。

分区表仅有 64 字节,最多记录 4 个分区,分为主分区(primary)与扩展分区(extended)。扩展分区只有一个,需再划分为若干逻辑分区(logical partition)才能使用;逻辑分区编号从 5 开始。MBR 最大支持 2 TiB 磁盘( 2 32 × 512 B 2^{32} \times 512\ \mathrm{B} 232×512 B)。

2.7.2 GPT

GPT(GUID Partition Table)为兼容不同扇区大小的磁盘,采用逻辑块地址(LBA)统一编址:

  • LBA 0:保护性 MBR,用于兼容旧工具;
  • LBA 1:GPT 头,记录分区表的位置、大小、备份位置及校验码(CRC32);
  • LBA 2 至 LBA 33:共 32 个块记录分区项,默认每块 4 个分区项,共 128 个分区;
  • 磁盘末尾 33 个块:分区表与 GPT 头的备份。

GPT 没有扩展分区的概念,全部为主分区;若分区表损坏,可用末尾的备份恢复。GPT 最大支持约 8 ZiB( 2 64 × 512 B 2^{64} \times 512\ \mathrm{B} 264×512 B)的磁盘。

2.8 Linux 中的设备命名

Linux 把硬盘、分区等设备都表示为文件,位于 /dev 目录下。分区文件的命名方式为磁盘设备名加编号:

  • IDE 设备以 hd 开头,如 hda1 表示第一块 IDE 硬盘的第一个分区;
  • SCSI、SATA、SAS 及 USB 设备以 sd 开头,如 sda1、sdb2;
  • 虚拟化环境中的虚拟磁盘以 vd 开头,如 vda1;
  • 回环设备(loop device)以 loop 开头,如 loop0,用于将镜像文件挂载为块设备;
  • 设备映射(LVM、dm-crypt 等)以 dm- 开头,如 dm-1。

编号 1 至 4 保留给主分区与扩展分区,逻辑分区从 5 开始。同一块硬盘上,主分区与扩展分区合计最多 4 个。

3 文件系统的逻辑结构

3.1 文件 = 属性 + 内容

文件由属性(元数据)与内容(数据)两部分组成,二者在磁盘上分离存储。ls -l 输出的每一行包含 7 列:模式(文件类型与权限)、硬链接数、文件所有者、所属组、大小、最后修改时间、文件名。

bash 复制代码
$ ls -l
total 12
-rw-rw-r--  1 sjx sjx  827 Jul 20 18:04 install.sh   # 普通文件
drwxrwxr-x 12 sjx sjx 4096 Sep 17 22:00 linux        # 目录
-rw-rw-r--  1 sjx sjx  625 Sep 10 20:18 Makefile      # 普通文件

stat 命令可以查看更完整的元数据:

bash 复制代码
$ stat Makefile
  File: 'Makefile'
  Size: 625       	Blocks: 8          IO Block: 4096   regular file
Device: fd01h/64769d	Inode: 658165      Links: 1
Access: (0664/-rw-rw-r--)  Uid: ( 1001/     sjx)   Gid: ( 1001/     sjx)
Access: 2026-09-13 21:08:03.034928673 +0800   # 最后访问时间
Modify: 2026-09-10 20:18:12.216482861 +0800   # 内容最后修改时间
Change: 2026-09-10 20:18:12.216482861 +0800   # 属性最后修改时间
 Birth: -

存储文件元信息(创建者、日期、大小等)的区域称为 inode(索引节点)。每个文件对应一个 inode,inode 内包含唯一的 inode 编号。

3.2 格式化与磁盘布局

磁盘格式化后,存储空间被划分为两个区域:一个区域存放文件的元数据(inode),一个区域存放文件的实际内容(block)。初始化时还会创建超级块(super block),其中保存整个文件系统的信息(如 dumpe2fs 命令查看到的信息即来自超级块),并在多个位置备份。

Linux 正统文件系统(如 ext2、ext3、ext4)将硬盘分区划分为三个区域:

  1. 超级块(super block):记录块与 inode 的总量、已用量与剩余量、块大小、最近一次挂载/写入/校验时间等文件系统自身结构信息;
  2. inode 表:每个 inode 对应一个文件或目录,记录文件长度、创建与修改时间、权限、所属关系、数据块位置等;
  3. 数据块区:存放文件内容。

3.3 ext2 文件系统:块组(Block Group)布局

要在硬盘上存储文件,必须先把硬盘格式化为某种文件系统。ext2 系列文件系统将整个分区划分为若干大小相同的块组(Block Group),每个块组具有相同的内部结构;只要能管理一个块组,就能管理整个分区乃至整个磁盘。

启动块(Boot Block)大小固定为 1 KiB,由 PC 标准规定,存储磁盘分区与启动信息,任何文件系统不得修改。启动块之后才是文件系统的数据。每个块组的构成:

  1. 超级块(Super Block):每个块组的开头都保存一份超级块拷贝(第一个块组必须有,其余块组可没有),彼此保持一致。这样即使部分扇区出现物理损坏,文件系统仍能正常工作;
  2. 块组描述符表(GDT,Group Descriptor Table):描述每个块组的属性,如块位图、inode 位图、inode 表的位置,空闲块与空闲 inode 数量等,每个块组开头同样保留一份拷贝;
  3. 块位图(Block Bitmap):记录数据块区中哪些块已被占用、哪些空闲;
  4. inode 位图(Inode Bitmap):每个比特表示一个 inode 是否空闲;
  5. inode 表(Inode Table):存放本组全部文件的属性。inode 编号以分区为单位整体划分,不可跨分区;
  6. 数据块区(Data Blocks):存放文件内容。block 按分区划分,不可跨分区。目录的数据块中存放该目录下的文件名与 inode 编号映射。

3.4 inode 数据结构

ext2 的磁盘级 inode 结构定义(节选自内核源码):

c 复制代码
struct ext2_inode {
    __le16 i_mode;                 /* 文件模式(类型与权限) */
    __le16 i_uid;                  /* 所有者 UID 低 16 位 */
    __le32 i_size;                 /* 文件大小(字节) */
    __le32 i_atime;                /* 访问时间 */
    __le32 i_ctime;                /* 创建时间 */
    __le32 i_mtime;                /* 修改时间 */
    __le32 i_dtime;                /* 删除时间 */
    __le16 i_gid;                  /* 组 GID 低 16 位 */
    __le16 i_links_count;          /* 硬链接计数 */
    __le32 i_blocks;               /* 占用块数 */
    __le32 i_flags;                /* 文件标志 */
    union { ... } osd1;            /* 操作系统相关字段 */
    __le32 i_block[EXT2_N_BLOCKS]; /* 指向数据块的指针数组 */
    __le32 i_generation;           /* 文件版本号(用于 NFS) */
    __le32 i_file_acl;             /* 文件 ACL */
    __le32 i_dir_acl;              /* 目录 ACL */
    __le32 i_faddr;                /* 碎片地址 */
    union { ... } osd2;            /* 操作系统相关字段 */
};

#define EXT2_NDIR_BLOCKS  12       /* 12 个直接块指针 */
#define EXT2_IND_BLOCK    EXT2_NDIR_BLOCKS
#define EXT2_DIND_BLOCK   (EXT2_IND_BLOCK + 1)
#define EXT2_TIND_BLOCK   (EXT2_DIND_BLOCK + 1)
#define EXT2_N_BLOCKS     (EXT2_TIND_BLOCK + 1)   /* 共 15 个指针 */

要点:

  • 文件名不存放在 inode 中,而存放在目录的数据块中;
  • inode 的大小一般为 128 字节或 256 字节;
  • 文件内容大小可以不同,但属性大小固定;
  • i_block[15] 中,下标 0 至 11 为直接块指针,12 为一级间接指针,13 为二级间接指针,14 为三级间接指针。

3.5 inode 与数据块的映射

inode 内部通过 i_block[EXT2_N_BLOCKS] 完成 inode 与数据块的映射。以 4 KiB 块为例,各寻址级别可达容量为:

寻址方式 指针数量 可寻址空间
12 个直接块 12 12 × 4 K i B = 48 K i B 12 \times 4\ \mathrm{KiB} = 48\ \mathrm{KiB} 12×4 KiB=48 KiB
一级间接块 1024 1024 1024 4 M i B 4\ \mathrm{MiB} 4 MiB
二级间接块 1024 2 1024^2 10242 4 G i B 4\ \mathrm{GiB} 4 GiB
三级间接块 1024 3 1024^3 10243 4 T i B 4\ \mathrm{TiB} 4 TiB

由此,文件 = 属性 + 内容 都可以定位:通过 inode 找到属性与数据块位置,通过数据块读取内容。

3.6 创建文件的过程

以 touch abc 为例,创建一个新文件依次执行以下 4 个操作:

  1. 存储属性:内核在 inode 位图中找到一个空闲 inode(假设编号为 263466),将文件大小、所有者、创建时间等属性写入其中;
  2. 存储数据:文件内容需要若干个数据块,内核在块位图中找到空闲块(如 300、500、800),将内核缓冲区中的数据依次复制到这些块;
  3. 记录分配情况 :内核在 inode 的磁盘分布区(i_block 数组)中按顺序记录上述块号;
  4. 添加文件名到目录 :内核将入口 (inode 编号, 文件名) 添加到所在目录的数据块中,建立文件名与 inode 的映射关系。

3.7 目录的本质与文件名映射

目录也是文件,磁盘上不存在独立的"目录"概念,只有文件属性与文件内容。目录的数据块中保存的是 文件名与 inode 编号的映射列表,其中至少包含两个特殊条目:

  • .:指向目录自身;
  • ..:指向父目录。

新建目录的 . 与 .. 通过 inode 编号关联;文件系统创建时,根目录的父目录被设置为指向自身,因此根目录下 . 与 .. 指向同一个 inode。

可以通过程序读取目录内容验证文件名与 inode 的映射关系:

c 复制代码
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <dirent.h>

int main(int argc, 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); }

    struct dirent *entry;
    while ((entry = readdir(dir)) != NULL) {     /* 逐个读取目录项 */
        if (strcmp(entry->d_name, ".") == 0 ||
            strcmp(entry->d_name, "..") == 0)    /* 跳过 . 与 .. */
            continue;
        printf("Filename: %s, Inode: %lu\n",
               entry->d_name, (unsigned long)entry->d_ino);
    }
    closedir(dir);
    return 0;
}

用户访问文件时使用文件名而非 inode 编号。查找文件时,先通过路径找到对应目录项,获得 inode 编号;再在 inode 表中找到 inode;最后通过 inode 中的块指针找到数据块。

3.8 路径解析

目录本身也是文件,访问目录同样需要其 inode 编号。访问路径 /home/sjx/code/test.c 时,必须从根目录开始,依次打开路径上的每个目录,逐层查找下一级目录项,直到定位目标文件,这一过程称为路径解析。

  • 访问文件必须提供"目录 + 文件名"即完整路径;
  • 根目录的文件名与 inode 编号固定,系统启动后必须已知,无须查找;
  • 路径由进程提供:进程拥有当前工作目录(CWD),用户执行指令或调用 open 时提供路径;
  • 系统中的目录结构由系统与用户共同构建:新建的任何文件都位于某个已存在的目录之下,从而形成天然的树状路径结构。

3.9 目录项与路径缓存

每次访问文件都从根目录逐级解析效率太低,因此内核会将已解析的目录缓存在内存中,这一机制的数据结构是目录项(dentry)。

  • 磁盘上没有 dentry;dentry 是内核维护的数据结构,缓存在内存中,包括文件名、inode 指针及与其他目录项的层级关系;
  • 磁盘上的目录文件是持久化的,目录项则由内核在内存中构建;
  • dentry 结构使所有已打开文件在内存中形成树状结构,同时隶属于 LRU(最近最少使用)链表进行节点淘汰,并加入哈希表以便快速查找;
  • 打开任何文件时,内核先在该树中按路径查找:命中则直接返回 inode 与内容;未命中则从磁盘加载路径并缓存新的 dentry,构成 Linux 的路径缓存。
c 复制代码
struct dentry {
    atomic_t d_count;                    /* 引用计数 */
    unsigned int d_flags;
    spinlock_t d_lock;
    struct inode *d_inode;               /* 关联的 inode,负目录项为 NULL */
    struct hlist_node d_hash;            /* 哈希链表节点 */
    struct dentry *d_parent;             /* 父目录项 */
    struct qstr d_name;                  /* 文件名 */
    struct list_head d_lru;              /* LRU 链表节点 */
    union {
        struct list_head d_child;        /* 父目录的子项链表 */
        struct rcu_head d_rcu;
    } d_u;
    struct list_head d_subdirs;          /* 子目录项链表 */
    struct list_head d_alias;            /* inode 别名链表 */
    unsigned long d_time;
    struct dentry_operations *d_op;
    struct super_block *d_sb;            /* dentry 树的根(超级块) */
    void *d_fsdata;
    int d_mounted;
    unsigned char d_iname[DNAME_INLINE_LEN_MIN];  /* 内联短文件名 */
};

总结三者关系:文件 = 目录项(dentry)+ inode + 数据块(block)。目录项提供文件名到 inode 的映射,inode 提供属性与数据块位置,数据块存放内容。

3.10 挂载

inode 编号不可跨分区,而 Linux 系统中可以同时存在多个分区。分区写入文件系统后并不能直接使用,必须与某个目录关联起来,即挂载(mount)之后才能使用。挂载点就是访问该分区的入口。

bash 复制代码
$ dd if=/dev/zero of=./disk.img bs=1M count=5      # 制作 5 MiB 镜像文件,模拟一个分区
$ mkfs.ext4 disk.img                                # 格式化并写入 ext4 文件系统
$ mkdir mydisk                                      # 建立空目录作为挂载点
$ sudo mount -t ext4 ./disk.img ./mydisk            # 将镜像挂载到挂载点
$ df -h                                             # 确认挂载成功
/dev/loop0      3.9M   53K  3.5M   2% /home/sjx/mydisk
$ sudo umount mydisk                                # 卸载分区

其中 /dev/loop0 表示第一个回环(loop)设备,是一种伪设备,允许将普通文件当作块设备使用。

挂载后,根据访问路径的前缀即可判断文件位于哪个分区。手动挂载的信息必须写入 /etc/fstab,否则下次开机需要重新挂载。

4 内核分层架构与虚拟文件系统

4.1 文件系统的分层结构

从物理磁盘到用户进程,文件访问经过如下层次:

  1. 硬盘驱动(Device Driver) :不同硬盘(PATA、SATA、AHCI、NVMe 等)提供各自的 I/O 接口,驱动模块一般位于内核源码 drivers/ata 目录,通用驱动可直接编译进内核,可通过 /boot/config-xxx 确认,如 CONFIG_SATA_AHCI=y;
  2. 通用块设备层(General Block Device Layer):将杂乱的驱动接口抽象为统一的块设备接口,对上层屏蔽硬盘与驱动的差异;该层的修改会影响所有文件系统;
  3. 具体文件系统 :由 mkfs.xxx 命令创建,如 mkfs.ext4、mkfs.xfs;内核支持的文件系统类型见内核源码 fs 目录;
  4. 虚拟文件系统(VFS):将不同文件系统的 API 抽象为统一接口;
  5. 系统调用层(System Call Interface):用户空间经系统调用操作文件系统。

4.2 虚拟文件系统(VFS)

文件系统种类繁多,操作系统为向用户提供统一接口,在用户层与具体文件系统层之间引入中间层,即虚拟文件系统。VFS 定义了一组所有文件系统都支持的数据结构与标准接口,用户与应用程序只需面对统一接口,无须关心底层文件系统的差异。文件系统挂载后,其元数据被加载到内存,VFS 仅存在于内存中。

VFS 主要定义四类对象:

  1. 超级块对象(super block) :记录文件系统的总体信息,如 inode 总量、block 总量;VFS 的超级块最终落地到具体文件系统(ext4、xfs 等)的超级块;
  2. 索引节点对象(inode):包含文件的元信息与数据块位置;
  3. 目录项对象(dentry):记录文件名与层级关系;
  4. 文件对象(file):记录进程与打开文件的交互状态。

用户空间的访问路径为:用户程序 → 系统调用(open、read、write、close、mount、umount、mkdir 等)→ VFS → 具体文件系统 → 通用块设备层 → 设备驱动 → 磁盘。

4.3 文件描述符

进程通过系统调用使用文件:open 打开文件并返回文件描述符,read/write 通过文件描述符读写,close 关闭文件。

c 复制代码
int fd = open(name, flag);   /* 打开文件,参数为路径名与标志 */
write(fd, ...);              /* 按文件描述符写入 */
close(fd);                   /* 关闭文件,避免资源泄漏 */

open 的参数包含文件的路径名与文件名;后续 read、write 均使用 open 返回的文件描述符,不再使用文件名。

文件描述符(file descriptor, fd) 是进程打开文件表中的表项编号,是打开文件的标识。传统默认配置下每个进程最多打开 1024 个文件,文件描述符取值范围为 0 至 1023;该限制可通过 ulimit -n 或 /etc/security/limits.conf 调整,现代内核的全局上限由 fs.nr_open 控制。

4.4 进程

进程(process) 是程序关于某数据集合的一次运行活动,是系统进行资源分配与调度的基本单位。进程的组成:

  • PCB(Process Control Block,进程控制块):包含 UID、PID、状态、地址空间等信息,是进程存在的唯一标识;
  • 代码段(code):程序的机器指令;
  • 数据段(data):程序运行过程中产生的数据。

操作系统为每个进程维护一张打开文件表,表中的每一项对应一个文件描述符。进程通过文件描述符直接访问文件时,文件描述符中已记录了文件的访问上下文,无须再经路径逐级查找。

对文件执行移动操作时,只改变目录项(文件名与 inode 的映射)所属的目录,inode 与数据块在磁盘上的位置不变,因此同分区移动文件速度很快。

5 常见文件系统类型

Linux 文件系统可以粗略分为:根文件系统(rootfs)、虚拟文件系统(VFS)、真实文件系统(存在于持久化存储设备)、伪文件系统(存在于内存)、网络文件系统(NFS)。

5.1 本地磁盘文件系统

  • ext4:第四代扩展文件系统,带日志功能,ext3/ext4 在 ext2 基础上增强但布局设计保持一致;是许多发行版的默认文件系统。最大文件 16 TiB(4 KiB 块),最大文件系统 1 EiB;
  • xfs:由 SGI 开发的 64 位日志文件系统,RHEL 7 / CentOS 7 及之后版本的默认文件系统。特点如下:
    • 全 64 位体系,支持上百万太字节级存储空间;
    • 对特大文件与海量小文件、超大目录的支持表现出色;
    • 最大单文件 2 63 − 1 2^{63} - 1 263−1 字节(约 8 EiB),最大文件系统 16 EiB;
    • 支持大文件与大分区,读写吞吐高,可扩展性优于 ext4。

ext4 与 xfs 的对比:

对比项 ext4 xfs
单个文件大小 16 GiB 至 16 TiB 16 TiB 至约 8 EiB
最大文件系统 1 EiB 16 EiB
日志 支持 支持,且元数据与数据日志可分离
适用场景 中小规模、通用场景 大文件、大分区、海量小文件场景

/boot 分区容量小、文件数量少且以读取为主,使用 ext4 可获得更快的启动阶段驱动加载速度。

5.2 交换分区(swap)

交换分区是磁盘上划出的、用作内存扩展的区域。当物理内存不足时,内核将部分内存页换出到交换分区,以临时存放数据。由于磁盘访问速度远低于内存,频繁换页会导致程序运行变慢,因此生产环境中常按业务需要谨慎配置。现代内存容量较大的场景下,交换分区一般不再按物理内存 2 倍配置,而是取较小值(如 2 至 4 GiB)或按需设置。

常用操作:

bash 复制代码
$ cat /proc/swaps                    # 查看交换分区状态
Filename        Type       Size   Used  Priority
/dev/dm-1       partition  2097148   0      -2

$ swapon -a                          # 开启所有交换分区
$ swapoff -a                         # 关闭所有交换分区
$ swapon /dev/sdd2                   # 开启指定交换分区
$ swapoff /dev/sdd2                  # 关闭指定交换分区
$ free -m                            # 查看内存与交换空间使用情况
5.2.1 内核参数 swappiness

内核参数 vm.swappiness 控制换页倾向,取值范围 0 至 100:数值越大,内核越倾向于回收匿名页(换出到交换分区);数值越小,越倾向于回收文件页(页缓存)。默认值因发行版而异(常见为 30 或 60)。

bash 复制代码
$ cat /proc/sys/vm/swappiness        # 查看当前值(运行时生效,重启失效)
30
$ vim /etc/sysctl.conf               # 永久修改:写入内核参数配置文件
vm.swappiness = 10
$ sysctl -p                          # 使配置文件中的参数立即生效
5.2.2 开机自动挂载与交换分区的启停

磁盘手动挂载后,必须将挂载信息写入 /etc/fstab,否则下次开机需要重新挂载。示例配置:

fstab 复制代码
/dev/mapper/centos-root /                       xfs    defaults        0 0
UUID=0615b8e8-3e85-45eb-9b7d-4f7e08a00eac /boot xfs    defaults        0 0
/dev/mapper/centos-swap swap                    swap   defaults        0 0

临时关闭交换分区:swapoff /dev/mapper/centos-swap;再次开启:swapon -a。永久关闭:注释掉 /etc/fstab 中的 swap 行并重启,或执行 swapoff -a 后同步修改配置文件。

/etc/sysctl.conf 存放内核参数;/proc/sys 目录下的子系统(abi、crypto、debug、dev、fs、kernel、net、user、vm)与运行中的内核参数一一对应,修改对应文件即修改内核运行参数。

5.3 伪文件系统

伪文件系统不将数据存储在硬盘上,而是占用内存空间,以文件形式向用户提供系统信息与设备管理手段,常用于 /proc、/sys、/dev。

5.3.1 procfs

procfs(进程文件系统)挂载于 /proc,由内核在启动时动态生成,用于通过文件接口访问进程与系统信息。它不占磁盘空间,仅占用少量内存。/proc 下以数字命名的目录对应各进程,目录名即进程 PID。

bash 复制代码
$ cat /proc/cpuinfo     # CPU 信息
$ cat /proc/meminfo     # 内存信息
$ cat /proc/swaps       # 交换分区信息
$ ls /proc/self         # 当前进程的目录

查看内核支持的文件系统类型:cat /proc/filesystems。

/proc 节点的创建以内核源码 fs/proc/cpuinfo.c 中的实现为例:文件操作结构体注册读写方法,proc_create 函数在 /proc 下创建节点,fs_initcall 宏使初始化函数在内核启动阶段执行。

c 复制代码
// SPDX-License-Identifier: GPL-2.0
#include <linux/cpufreq.h>
#include <linux/fs.h>
#include <linux/init.h>
#include <linux/proc_fs.h>
#include <linux/seq_file.h>

__weak void arch_freq_prepare_all(void) { }          /* 架构相关准备函数 */

extern const struct seq_operations cpuinfo_op;

static int cpuinfo_open(struct inode *inode, struct file *file)
{
    arch_freq_prepare_all();
    return seq_open(file, &cpuinfo_op);              /* 以 seq_file 方式输出 */
}

static const struct file_operations proc_cpuinfo_operations = {
    .open    = cpuinfo_open,
    .read    = seq_read,
    .llseek  = seq_lseek,
    .release = seq_release,
};

static int __init proc_cpuinfo_init(void)
{
    proc_create("cpuinfo", 0, NULL, &proc_cpuinfo_operations);  /* 创建节点 */
    return 0;
}
fs_initcall(proc_cpuinfo_init);                      /* 内核启动时自动初始化 */

用户态亦可编写内核模块创建自定义 proc 节点。驱动源码(基于 Ubuntu 本地内核头文件编译):

c 复制代码
#include <linux/fs.h>
#include <linux/init.h>
#include <linux/proc_fs.h>
#include <linux/module.h>
#include <linux/uaccess.h>

static char kernel_buf[1024];

#define MIN(a, b) ((a) < (b) ? (a) : (b))

static int myproc_open(struct inode *node, struct file *file)
{
    printk("[%s %d][%s]\n", __FILE__, __LINE__, __func__);
    return 0;
}

static ssize_t myproc_read(struct file *file, char __user *buf,
                           size_t size, loff_t *offset)
{
    printk("[%s %d][%s]\n", __FILE__, __LINE__, __func__);
    copy_to_user(buf, kernel_buf, MIN(1024, size));  /* 向用户空间复制数据 */
    return MIN(1024, size);
}

static ssize_t myproc_write(struct file *file, const char __user *buf,
                            size_t size, loff_t *offset)
{
    printk("[%s %d][%s]\n", __FILE__, __LINE__, __func__);
    copy_from_user(kernel_buf, buf, MIN(1024, size)); /* 从用户空间复制数据 */
    return MIN(1024, size);
}

static int myproc_close(struct inode *node, struct file *file)
{
    printk("[%s %d][%s]\n", __FILE__, __LINE__, __func__);
    return 0;
}

static const struct file_operations myproc_operations = {
    .owner   = THIS_MODULE,
    .open    = myproc_open,
    .read    = myproc_read,
    .write   = myproc_write,
    .release = myproc_close,
};

static int __init myproc_init(void)
{
    proc_create("myproc", 0, NULL, &myproc_operations);  /* 创建 /proc/myproc */
    return 0;
}

static void __exit myproc_exit(void)
{
    remove_proc_entry("myproc", NULL);                   /* 删除节点 */
}

module_init(myproc_init);
module_exit(myproc_exit);
MODULE_DESCRIPTION("proc test");
MODULE_LICENSE("GPL");

配套 Makefile:

makefile 复制代码
KERNELDIR := /usr/src/linux-headers-5.4.0-149-generic   # 内核头文件路径
CURRENT_PATH := $(shell pwd)
CONFIG_MODULE_SIG = n                                    # 禁用模块签名

obj-m := proc.o

build: kernel_modules

kernel_modules:
	$(MAKE) -C $(KERNELDIR) M=$(CURRENT_PATH) modules   # 调用内核构建系统

clean:
	$(MAKE) -C $(KERNELDIR) M=$(CURRENT_PATH) clean

编译、加载与测试流程:

bash 复制代码
$ make                              # 编译模块,生成 proc.ko
$ sudo insmod proc.ko               # 加载模块
$ lsmod | grep proc                 # 查看已加载模块
$ sudo chmod 777 /proc/myproc       # 赋予节点访问权限
$ echo "hello" > /proc/myproc       # 向模块写入数据
$ dmesg                             # 查看内核打印
$ cat /proc/myproc                  # 从模块读取数据
$ sudo rmmod proc                   # 卸载模块
5.3.2 devtmpfs

devtmpfs 在 Linux 内核启动早期自动建立初步的 /dev 目录,使一般启动程序无须等待 udev(设备管理器,负责维护 /dev 下的设备节点)即可使用设备文件,从而缩短开机时间。设备节点是外观与普通文件相同的特殊文件,通过设备文件与设备驱动程序交互,可使用 ls、cat 等常规命令操作。

5.3.3 sysfs

sysfs 是基于内存的虚拟文件系统,挂载于 /sys,用于以文件形式向用户程序提供内核信息,便于管理系统中的设备。与 /dev 提供设备本身的访问接口不同,/sys 用于查看和管理设备:包含系统中几乎所有设备的详细信息,如制造商与型号、设备的总线位置、设备状态、设备层级关系等。/sys 下的文件不是设备节点,不与设备直接交互。

5.4 临时文件系统 tmpfs

tmpfs 完全驻留于内存(必要时可使用交换分区),数据在重启或卸载后丢失,典型挂载点为 /dev/shm、/run。适合存放临时数据、共享内存段等需要高速访问的场景。

5.5 内存文件系统的用途小结

/proc、/sys 类文件系统的共同特点是读写这些文件实际上是读写内核中的数据:读写 /proc/sys 下的文件等价于读写对应的内核参数;/proc/cpuinfo、/proc/meminfo 反映硬件与运行状态。

5.6 网络文件系统(NFS)

NFS(Network File System)采用客户端/服务器架构,使客户端访问远程文件如同访问本地文件。服务器将本地文件系统导出(export),客户端挂载(mount)后使用;文件通常以块为单位传输。典型应用:在嵌入式 Linux 开发中,将主机上的目标文件系统或可执行文件共享给目标板,便于目标板直接运行程序。

5.7 光盘文件系统 iso9660

iso9660 是光盘标准文件系统,使用前需将光盘放入光驱(或将 ISO 镜像通过回环设备挂载):

bash 复制代码
$ mount -t iso9660 /dev/sr0 /mnt          # 挂载光盘
$ mount -o loop,ro -t iso9660 image.iso /mnt  # 挂载 ISO 镜像

5.8 Windows 文件系统

  • FAT32:已淘汰,单文件上限 4 GiB;
  • NTFS(New Technology File System):微软专利技术,当前主流。

5.9 嵌入式与其他文件系统

  • 基于闪存的文件系统:jffs2、yaffs、ubifs;
  • 只读压缩文件系统:cramfs、squashfs、romfs;
  • 内存文件系统:ramdisk、ramfs/tmpfs;
  • 网络文件系统:NFS;
  • 分布式与专用文件系统:GFS(Google File System)、HDFS(Hadoop Distributed File System)、TFS(淘宝文件系统,面向海量小图片与文件)。

6 文件操作语义

6.1 读取文件的数据通路

文件系统读取文件的完整流程:

  1. 根据文件名,在目录的数据块中查到对应的 inode 编号;
  2. 在 inode 表中定位该 inode;
  3. 通过 inode 中的块指针找到数据块,读取内容。

inode 表与块位图用于空间管理:inode 位图用比特标注哪些 inode 已用(1)、哪些空闲(0),块位图同理。df -i 可查看各文件系统的 inode 使用情况:

bash 复制代码
$ df -i /boot
文件系统                   Inode  已用(I)  可用(I) 已用(I)%  挂载点

进程通过文件描述符持续读写文件时,描述符中已记录文件的访问上下文,无须每次经路径解析与目录项查找。

6.2 删除文件与数据恢复

删除文件时,文件系统执行以下操作:

  1. 在目录的数据块中删除该文件名对应的目录项;
  2. 在 inode 位图中将该 inode 标记为未使用;
  3. 在块位图中将该文件占用的数据块标记为未使用。

inode 与数据块本身的内容并未立即清除。inode 数量与块数量按比例分配,inode 通常少于块;若系统中大量小文件耗尽 inode,即使块仍有剩余空间,也会无法创建新文件。

因此:

  • 删除小文件比删除大文件快得多:两者都要删除目录项并更新位图,但大文件占用的数据块数量多;
  • 删除后的数据可以恢复:误删文件后应立即停止写入新数据(最好关机),使用恢复软件扫描残留的 inode 与数据块进行恢复;
  • 现代文件系统带有日志(journal),记录磁盘操作元数据,可用于恢复目录项等信息。

6.3 硬链接与软链接

真正定位磁盘文件的是 inode 而非文件名。Linux 允许多个文件名对应同一个 inode,即硬链接(hard link):

bash 复制代码
$ touch test
$ ln test hard                  # 创建硬链接
$ ls -li
656163 -rw-rw-r--  2 sjx sjx 0 Sep 29 17:19 hard
656163 -rw-rw-r--  2 sjx sjx 0 Sep 29 17:19 test

test 与 hard 的 inode 相同,内核记录的硬链接数为 2。删除文件时:删除目录中对应记录,并将硬链接计数减 1,计数归零时才释放 inode 与数据块。

软链接(符号链接,symbolic link)通过文件名引用另一个文件,自身是一个独立文件,拥有独立的 inode,其数据块中存放目标路径字符串,类似于快捷方式:

bash 复制代码
$ ln -s a b                     # 创建软链接
$ ls -li
656163 -rw-rw-r-- 1 sjx sjx 0 Sep 29 17:31 a
667405 lrwxrwxrwx 1 sjx sjx 1 Sep 29 17:31 b -> a

对比与用途:

对比项 硬链接 软链接
inode 与目标文件相同 独立 inode
本质 文件名与 inode 的映射关系 独立文件,内容为目标路径
跨分区 不支持 支持
链接目录 不支持(防止环) 支持
典型用途 文件备份、. 与 .. 即目录的硬链接 版本管理、路径重定向、快捷方式

6.4 文件的时间属性

每个文件维护三个时间:

  • Access:最后访问时间;
  • Modify:文件内容最后修改时间;
  • Change:文件属性(inode 元数据)最后修改时间。

修改文件内容通常同时更新 Modify 与 Change;仅修改权限等属性只更新 Change。

7 分区规划与挂载配置

7.1 经典分区方案

标准方案(以 CentOS 为例):

挂载点 建议大小 说明
/ 剩余全部空间 根分区
/boot 约 1 GiB,ext4 存放内核、启动配置与 GRUB 等启动必需文件
swap 视内存与业务而定 交换分区
/home 按需单独划分 用户数据
/var 按需单独划分 日志与频繁变动数据
/mysql_data 等 按业务划分,xfs 数据库等专用数据分区,便于管理与扩容

自定义方案示例:/ 使用 xfs 占剩余空间;/boot 使用 ext4 约 1 GiB;swap 按实际需求设置;数据库数据使用独立分区存放。/lib 存放共享库,/proc 存放运行中的进程与内核信息(进程以 PID 为目录名)。

7.2 开机自动挂载

手动挂载的分区必须在 /etc/fstab 中登记才能开机自动挂载。/etc/fstab 每行格式为:

fstab 复制代码
<设备或 UUID>  <挂载点>  <文件系统类型>  <挂载选项>  <dump>  <fsck>

示例:

fstab 复制代码
UUID=0615b8e8-3e85-45eb-9b7d-4f7e08a00eac /boot xfs   defaults 0 0
UUID=e0b23637-b723-4680-8d8c-a6f4238095e8 /mount  xfs   defaults 0 0
UUID=7d44e927-00d2-43b6-823a-89d9e6e30478 /mount1/hyrz ext4 defaults 0 0

使用 UUID 标识设备可避免设备名变化导致的挂载错误。

7.3 开机自启服务管理(systemd)

bash 复制代码
$ systemctl enable firewalld      # 设置开机自启(建立指向 /etc/systemd/system/ 的符号链接)
$ systemctl disable firewalld     # 关闭开机自启(删除对应的符号链接)

enable 操作会在 /etc/systemd/system/multi-user.target.wants 等目录下创建指向 /usr/lib/systemd/system/ 中服务单元的符号链接。

8 常用命令速查

8.1 文件系统信息查看

bash 复制代码
$ dumpe2fs /dev/sdb2 | less       # 查看 ext2/3/4 文件系统信息(超级块来自此)
$ xfs_info /data                  # 查看 xfs 文件系统信息
$ stat a.txt                      # 查看文件元数据
$ df -h                           # 查看磁盘空间使用情况
$ df -i                           # 查看 inode 使用情况
$ lsblk                           # 查看块设备与分区拓扑
$ fdisk -l                        # 查看分区表(MBR)
$ parted -l                       # 查看分区表(GPT/MBR)
$ cat /proc/filesystems           # 查看内核支持的文件系统类型
$ cat /proc/cpuinfo               # 查看 CPU 参数

8.2 文件与进程

bash 复制代码
$ lsof -i :80                     # 查看占用 80 端口的进程
$ lsof -p 1055                    # 查看指定进程打开了哪些文件
$ lsof                            # 列举系统中所有打开的文件

8.3 分区与文件系统创建

bash 复制代码
$ fdisk /dev/sdb                  # MBR 分区交互工具
$ parted /dev/sdb                 # GPT/MBR 分区工具
$ mkfs.ext4 /dev/sdb1             # 创建 ext4 文件系统
$ mkfs.xfs  /dev/sdb2             # 创建 xfs 文件系统
$ mkswap /dev/sdb3                # 创建交换分区
$ mount /dev/sdb1 /data           # 挂载分区
$ umount /data                    # 卸载分区
$ blkid                           # 查看设备 UUID 与文件系统类型

8.4 链接操作

bash 复制代码
$ ln target linkname              # 创建硬链接
$ ln -s target linkname           # 创建软链接

注:本文为"Linux 系统 | 文件系统"相关合辑。

略作重排,图片清晰度受引文原图所限。

如有内容异常,请看原文。


Reference

  1. linux磁盘分区详解 - 6点32分 - 博客园
    https://www.cnblogs.com/sangmu/p/6629594.html
  2. https://cyc2018.github.io/CS-Notes/#/notes/Linux
  3. https://www.cnblogs.com/alantu2018/p/8461749.html
  4. https://www.cnblogs.com/brianzhu/p/6398923.html