Linux file system


Linux文件系统就像是整个操作系统的地基和骨架——所有数据都靠它来组织和存取。要理解它,需要同时把它当作一棵“倒挂的树”和一套“抽象层”

整体视角:一棵单一的大树(统一的命名空间)

在Linux里,不管有多少硬盘、分区、U盘,统统被挂到一个统一的目录树下

  • 根目录/是起点,类似于Windows的C:\
  • 其他设备(分区、外部存储)不会像Windows那样显示成D:\ E:\,而是通过挂载点(mount point)嵌入到树中,比如/mnt/usb, /home, /media/cdrom
  • 一切皆文件:设备、套接字、管道甚至进程接口(如/proc)都表现为文件
    • 这不仅是一种技术更是一种哲学。它意味着几乎所有资源和操作都可以通过一套统一的open, read, write, close, ioctl接口来访问和管理,把资源抽象为可流动的字节流

Linux常见目录结构(FHS标准)

熟悉目录相当于熟悉操作系统的器官功能

  • /bin:用户最基本的二进制可执行文件,比如ls, cp, mv
  • /sbin:系统管理工具(ifconfig, mount等)
  • /etc:配置文件,整个系统的“设置中心”
  • /dev:设备文件,例如/dev/sda硬盘、/dev/tty终端
  • /proc:伪文件系统,实时反映内和和进程状态。比如/proc/cpuinfo, /proc/meminfo
  • /sys:sysfs,和内核设备树挂钩的接口
  • /var:可变数据,日志,缓存,邮件队列
  • /usr:用户级别程序和库,体量最大
  • /home:用户主目录

/dev

/dev/null

dev/null是Unix/Linux系统中的一个特殊设备文件,通常被称为“空设备(null device)”
从工程角度讲,它的语义非常明确:写入它的数据会被立即丢弃;从它读取永远得到EOF
它是一个字符设备文件,由内核实现,位于虚拟设备文件系统(/dev),它不是普通文件,不占磁盘空间,不保存任何数据

行为模型

写入/dev/null

echo "hello" > /dev/null 

结果

  • 写成功
  • 数据直接消失
  • 不报错

内核行为等价于

write(fd, buf, len); // 返回 len,但什么都没保存

/dev/null读取

cat /dev/null 

结果

  • 立即EOF
  • 无任何输出

等价于

read(fd, buf, len); // 立即返回 0 
存在意义

Unix的一条核心哲学是:一切皆文件;既然标准输入/输出/错误都是“文件描述符”,那就需要一个合法的“什么都不做”的端点。/dev/null就是这个“黑洞”

常见用途
  1. 丢弃输出
make > /dev/null 

只关心返回码,不关心输出

  1. 丢弃错误输出
cmd 2> /dev/null 

忽略所有错误输出

  1. 同时丢弃stdout + stderr
cmd > /dev/null 2>&1 
  1. 禁用程序读取输入
cmd < /dev/null 

告诉程序:不要等用户输入

设计哲学

/dev/null的意义在于:提供一个“合法但无副作用”的IO终点
这让

  • shell重定向
  • 管道
  • 守护进程
  • 构建系统
  • 编译工具

都可以不加分支地工作

底层机制:inode与block

这是Linux文件系统的灵魂

  • block(块):存储数据的基本单位,通常4KB
  • inode(索引节点):描述文件的元数据,存储文件大小、权限、所有者、时间戳、数据块位置
  • 每个文件都有一个inode号,可以通过ls-i查看
  • 目录本质上是“文件名 -> inode号”的映射表
  • 文件名并不是文件本身,inode才是核心身份
    • 这就是为什么硬连接(hard link)可以让一个文件有多个名字——它们指向同一个inode

inode结构

inode(索引节点)是Linux文件系统的灵魂。一个文件对应一个inode,里面存的是“元数据”,而不是文件名
典型的ext4 inode结构里包含:

  • 文件类型和权限(普通文件、目录、设备文件)
  • 所有者(UID)、组(GID)
  • 时间戳:创建、修改、访问时间(ext4支持纳秒级)
  • 文件大小
  • 指针数组(block pointers):记录数据存放的位置

指针机制是关键:

  • 12个直接指针(直接指向数据块)
  • 1个间接指针(指向“数据块指针的块”)
  • 1个双重间接指针(指向“指针块的指针块”)
  • 1个三重间接指针(三级寻址,能管理超大文件)

这就是为什么一个inode本身不存放数据,而是存放“数据的地图”

inode本质

inode是文件系统设计的分水岭。早期系统(如FAT)只有“文件名 + 数据块列表”,无法支持硬链接或权限模型
inode的发明,让“名字”和“实体”解耦:

  • 目录项(dentry)存储名字
  • inode存储实体信息
  • 数据块存储内容

这种分离让Linux能支持:

  • 同一个文件多个名字(硬链接)
  • 删除名字但文件仍然存在(进程持有fd时)
  • 稳定的权限与时间戳机制
  • 高级缓存与写回控制

文件名与目录

文件名其实不在inode里

  • 目录文件是一个特殊的文件,内容就是一张表:文件名 -> inode号
  • ls时,系统就是先读目录文件,找到inode. 再通过inode找到数据块
  • 这也就是为什么文件名可以删掉(目录项消失),但inode和数据块还在(硬链接依旧存在)

虚拟文件系统(VFS)

Linux内核引入了一个抽象层叫VFS(Virtual File System),程序调用open/read/write系统调用时,不用管底层是ext4还是XFS,VFS会帮忙翻译。它就像一个“通用接口”,底下可以换不同实现,这就是Linux能挂载不同文件系统的原因。VFS在内核中定义了一组所有文件都必须实现的通用接口(如inode_operations, file_operations)。当用户程序执行文件操作时,调用先到VFS,再由VFS根据文件所在的路径,路由到具体的文件系统实现去处理

Linux文件系统的三层抽象

真正理解文件系统,需要看到三层“视角”

  1. 用户空间(User Space) 程序看到的只是路径和文件描述符(fd),并不直接感知inode或block。所有访问都要经过系统调用接口,比如open(), read(), write()
  2. 内核抽象层(VFS层) VFS是统一接口层,它让ext4, xfs, btrfs, procfs这些完全不同的实现都能“看起来像文件系统”
    内核中,每种文件系统驱动都要注册一组操作函数(super_operations, inode_operations, file_operations
    这就像面向对象编程中的“虚函数表”:VFS只关心接口,不关心底层实现
  3. 存储层(Block Layer) VFS最终调用具体的文件系统驱动(如ext4),它通过块设备层(Block I/O Layer)与物理设备交互
    在这里出现的关键词包括:页缓存(Page Cache)、缓冲区(Buffer Head)、I/O调度器(Elevator Algorithm)、DMA(直接内存访问)

这三层结构保证了可替换性与高性能。Linux可以在不改动应用程序的前提下挂载任意文件系统,也能用同一套机制管理网络文件、虚拟内存文件、甚至设备节点

不同的Linux文件系统类型

Linux有多个文件系统实现,各有优劣:

  • ext2/ext3/ext4:最经典的文件系统,ext4最常用,支持journaling(日志机制)防止崩溃时丢数据
  • XFS:高性能文件系统,擅长处理超大文件,常用于服务器
  • Btrfs:新一代文件系统,支持快照、压缩、子卷,类似于ZFS的野心
  • tmpfs:内存中的临时文件系统,用于/tmp,掉电即失
  • procfs/sysfs:伪文件系统,数据来自内核而不是硬盘

ext4文件系统的大布局

当在磁盘上格式化一个ext4分区时,它会分成几大区域

[ Boot Block][ Superblock ][ Block Group 0 ][ Block Group 1 ]...
  • Boot Block:通常占用前1KB,可以存放引导程序(但一般给GRUB用)
  • Superblock(超级块):文件系统的“身份证”,存储文件系统大小、inode数量、block大小、挂载次数等全局信息
  • Block Groups(块组):整个文件系统被划分为一个个块组(类似分区里的“格子”)。每个块组内部都有自己的元数据和数据存储区

Block Group的内部结构

每个块组都包含以下部分

[ Superblock副本 ][ Group Descriptor ][ Block Bitmap ][ Inode Bitmap ]
[ Inode Table ][ Data Blocks ]
  • Superblock副本:为了防止损坏,ext4会在不同块组里保存超级块的备份
  • Group Descriptor:描述当前块组的元信息,比如这个块组的空闲inode/块数量
  • Block Bitmap:记录哪些块被占用,哪些是空闲的
  • Inode Bitmap:记录哪些inode被占用
  • Inode Table:存放inode结构体数组
  • Data Blocks:真正存放文件内容的地方

ext4改进机制

相比ext2/ext3, ext4加了很多现代特性

  • Extents:替代传统的“每块一指针”,用一个“范围”来表示连续的数据块,大幅减少大文件的寻址开销
  • Journal(日志):写操作先写入日志,再写到数据区,保证崩溃后能恢复一致性
  • 延迟分配(delayed allocation):写文件时先缓存,等缓冲区满了再写盘,提高性能
  • 大文件/大分区支持:单文件最大16TB,分区最大1EB(1 exabyte = 1024 PB)

现代文件系统的趋势

Linux文件系统的演进方向,正在从“可靠 + 通用”转向“可验证 + 可回滚”

  1. ext4:稳定成熟,但本质是20世纪的架构
  2. Btrfs/ZFS:Copy-on-Write(COW)架构,内置校验、快照、压缩。数据安全性高,但对写入延迟敏感
  3. OverlayFS/UnionFS:容器时代的利器,可将多层文件系统叠加成一个视图(如Docker的镜像层)
  4. FUSE(Filesystem in Userspace):允许用户空间实现文件系统,典型如sshfs. AppImage, rclone 这体现了Linux哲学中的“机制优于策略”——内核只提供通用机制,不规定策略

当代Linux的存储体系已经超越了传统磁盘概念:

  • 页缓存(Page Cache)和块缓存(Buffer Cache)是内核层的“第二层文件系统”
  • tmpfs是“以内存为磁盘”的反向结构
  • over layfs是“虚拟叠加”的组合
  • procfs / sysfs是“状态即文件”的抽象

换句话说,Linux的文件系统并不仅仅是管理数据的,而是管理抽象和语义的

数据读写流程:从抽象到具体

结合VFS和底层实现,一个read操作的大致流程如下

  1. 系统调用:用户程序调用read(fd, buf, count)
  2. VFS层:内核通过文件描述符fd找到对应的file结构体,其中包含来指向VFS通用操作的指针
  3. 路由到具体文件系统:VFS根据文件所在的挂载点,确定其文件系统类型(如ext4),并调用ext4注册的read操作
  4. 页缓存(Page Cache):如果缓存未命中,ext4开始工作。它根据VFS传来的偏移量,通过文件的inode中的指针(或Extents)计算出数据所在的块(block)号
  5. 块设备层:ext4向块设备层发起I/O请求,读取这些块
  6. I/O调度:块设备层的I/O调度器会对多个请求进行合并和排序(如电梯算法),以优化磁盘磁头的移动路径
  7. 设备驱动:最终,请求被发送到具体的硬盘驱动程序,由驱动通过DMA等方式将数据从磁盘读入页缓存
  8. 完成:数据从页缓存复制到用户空间的buf,系统调用返回

写入(write)操作也是类似的路径,但更复杂,涉及回写(Writeback):数据通常先写到页缓存,内核线程再异步地将脏页刷回磁盘。这提升了性能,但也带来了数据一致性的考虑,这就是Journaling(日志)发挥作用的地方

日志(Journaling)机制的工作流程

日志是防止文件系统在崩溃后陷入不一致状态的关键技术(如ext3/ext4的data=ordered模式):

  1. 记录日志:在真正向数据块写入用户内容之前,先将本次写入的元数据(如要写入哪些块、inode如何更新)作为一个“事务”追加到日志区域
  2. 提交日志:将日志事务标记为提交,确保日志记录是完整的
  3. 写入数据:开始真正的数据写入(Commit)
  4. 清理日志:数据写入成功后,清理日志中对应的记录

崩溃恢复

  • 如果崩溃发生在步骤1-2,日志中的事务不完整,直接忽略
  • 如果崩溃发生在步骤3-4,重启后文件系统检查日志,发现有一个已提交但未完成的事务,它会重放(replay)这个事务,确保元数据的一致性。这比传统的fsck扫描整个磁盘要快几个数量级

现代特性:Copy-on-Write(COW)

这是Btrfs和ZFS等新一代文件系统的核心思想,与传统的日志式文件系统(如ext4)有根本不同:

  • 原理:当需要修改一个数据块时,并不直接在原位置覆盖写入,而是将块复制到一个新位置,在新位置进行修改,最后更新指针指向块
  • 优势:
    • 几乎即时的快照:快照只需记录当前的数据块指针,创建成本极低
    • 数据一致性:避免了崩溃时“半新半旧”的块的问题
    • 潜在的性能提升:适合并行写入