在 Linux 内核层面,“增删改查”实际上是文件系统(File System)的核心操作
- 引言:界定范围(VFS 与 具体文件系统如 Ext4),核心概念(用户态到内核态的转换)。
- 核心数据结构:介绍 Inode, Dentry, Superblock, File Object。
- 架构全景:VFS 抽象层的作用。
- 深度解析 CRUD:
- Create (创建):Inodes 分配与目录项链接。
- Read (读取):页缓存(Page Cache)与缺页中断。
- Update (写入):脏页(Dirty Page)、回写机制与日志(Journaling)。
- Delete (删除):硬链接计数与资源释放。
- 总结:一张图概括流程。
一、 核心概念与数据结构 (The Anatomy)
在深入 CRUD 之前,必须先理解 Linux 文件系统的“四大金刚”。Linux 使用 VFS (Virtual File System) 来屏蔽底层不同文件系统(Ext4, XFS, NTFS)的差异。
- Superblock (超级块):
- 作用:存储整个文件系统的元数据(如文件系统大小、块大小、空闲块数量)。
- 类比:停车场的总控室,知道整个停车场有多少车位,剩下多少车位。
- Inode (索引节点):
- 作用:这是文件的灵魂。存储文件的元数据(权限、所有者、大小、时间戳)以及数据块的指针(即数据在磁盘哪个位置)。注意:Inode 不包含文件名。
- 类比:车位上的详细档案卡,记录了这辆车是谁的、停了多久,但没写车牌号(文件名)。
- Dentry (目录项):
- 作用:连接文件名和 Inode。Linux 内核维护一个 Dentry Cache (dcache) 来加速路径查找。
- 类比:停车场系统的查找表,记录了“车牌号(文件名)”对应“档案卡编号(Inode号)”。
- Data Block (数据块):
- 作用:真正存储文件内容的地方。
二、 架构层级 (The Architecture)
用户发起的操作流程如下:
用户程序 (User Space) -> 标准库 (glibc) -> 系统调用 (System Call) -> VFS 层 -> 具体文件系统 (如 Ext4) -> 通用块设备层 -> 磁盘驱动 -> 物理磁盘。
三、 增 (Create):从无到有的过程
场景:用户执行 touch myfile.txt 或代码调用 open("myfile.txt", O_CREAT)。
底层原理:
- 路径解析:内核通过 VFS 的
path_lookup逐级解析路径。 - 寻找父目录:找到目标目录的 Inode。
- 分配 Inode:
- 内核在Inode Bitmap(Inode 位图)中查找一个空闲位。
- 分配一个新的 Inode 结构体,初始化元数据(权限、时间、UID/GID)。
- 创建 Dentry:
- 在内存中创建一个新的 Dentry 对象,将文件名 "myfile.txt" 与新申请的 Inode 绑定。
- 写入父目录:
- 将这个 Dentry 信息写入父目录的数据块中(因为目录本质上也是一个文件,里面存的是文件名到 Inode 号的映射列表)。
- 持久化 (Journaling):
- 为了防止断电导致数据不一致,Ext4 等日志文件系统会先将上述元数据变更写入 Journal (日志区),成功后再刷入磁盘的实际位置。
核心点:创建文件主要是在操作元数据(Inode 和 Dentry),此时通常还没分配实际存储数据的数据块(Data Block)。
四、 查 (Read):缓存为王
场景:用户执行 cat myfile.txt 或代码调用 read(fd, buf, size)。
底层原理:
- 查找文件对象:通过文件描述符(fd)找到内核中的
struct file对象,进而找到 Dentry 和 Inode。 - 页缓存 (Page Cache) 检查:
- Linux 不会直接从磁盘读数据。它先检查内核的 Page Cache(内存)中是否已经缓存了该文件的对应页。
- 命中 (Hit):直接将内存中的数据拷贝到用户态 buffer,速度极快(纳秒级)。
- 未命中 (Miss):触发缺页异常 (Page Fault) 或者内核启动预读机制。
- 磁盘寻址:
- 文件系统驱动(如 Ext4)根据 Inode 中的Extent Tree(或旧式的块指针)计算逻辑偏移量对应的物理磁盘块号。
- DMA 传输:
- CPU 发指令给磁盘控制器,通过 DMA(直接内存访问)将数据从磁盘搬运到内核的 Page Cache 中。
- 拷贝给用户:
- 数据进入 Page Cache 后,内核再将其
copy_to_user到用户程序的内存空间。
- 数据进入 Page Cache 后,内核再将其
核心点:读取操作是“内存优先”的。此时内存是磁盘的缓存。
五、 改 (Update):延迟写入与脏页
场景:代码调用 write(fd, buf, size)。
底层原理:
- 写入 Page Cache:
- 同样,数据不是直接写到磁盘的。
- 内核将用户数据拷贝到 Page Cache 对应的页中。
- 将该页标记为 Dirty (脏页)。此时
write系统调用通常就会返回成功(这叫 Write-back 策略)。
- 延迟分配 (Delayed Allocation):
- 为了优化磁盘碎片,Ext4 可能不会立即决定数据存放在磁盘哪个块,而是等到真正要刷盘时,再一次性分配连续的磁盘块。
- 回写 (Writeback):
- 脏页会在内存中停留几十秒(默认 30秒)。
- 由内核线程
pdflush或kworker周期性地,或者在内存不足时,将脏页刷入磁盘。
- 日志保障 (Journaling):
- 如果是
data=ordered模式(默认),内核会确保数据块先落盘,再更新元数据(Inode),最后更新日志。这保证了崩溃重启后不会看到垃圾数据。
- 如果是
核心点:写入操作极其依赖 Page Cache 和 Journal。用户以为写完了,其实还在内存里飘着。
六、 删 (Delete):只是断开了链接
场景:用户执行 rm myfile.txt 或代码调用 unlink("myfile.txt")。
底层原理:
- 检查权限:检查用户是否有权限修改父目录(因为删除文件本质是修改父目录的内容)。
- 删除 Dentry:
- 从内核的 Dcache 中移除该条目。
- 从父目录的数据块中移除文件名到 Inode 的映射记录。
- 减少链接计数 (i_nlink):
- 将该文件的 Inode 结构中的
i_nlink计数器减 1。
- 将该文件的 Inode 结构中的
- 真正的释放:
- 内核检查
i_nlink是否为 0。 - 并且检查该文件的打开引用计数(Open file count)是否为 0(即是否有进程正打开着这个文件)。
- 如果都为 0:
- 释放数据块:在 Block Bitmap 中将对应位标记为空闲。
- 释放 Inode:在 Inode Bitmap 中将对应位标记为空闲。
- 注意:磁盘上的实际数据内容通常不会被抹去(除非使用
shred等工具),只是标记为“可覆盖”。
- 内核检查
核心点:
rm只是删除了目录项(名字)。只有当所有硬链接都被删除且没有进程打开该文件时,空间才会被释放。
七、 总结图表
| 操作 | 关键系统调用 | 涉及的核心组件 | 关键机制 |
|---|---|---|---|
| Create | open, mkdir |
Inode Bitmap, Dentry | 元数据操作,先写日志,再写磁盘 |
| Read | read, mmap |
Page Cache | 缓存命中,未命中则 DMA 读取,零拷贝优化 |
| Update | write |
Page Cache, Dirty Bit | Write-back (回写),延迟分配,脏页刷盘 |
| Delete | unlink |
i_nlink, Superblock | 引用计数,软删除,仅仅修改位图状态 |
浙公网安备 33010602011771号