Theory · OS · File System & Page Cache

文件系统与 Page Cache

VFS 四大对象 · inode · 硬链接与软链接 · ext4 日志 · write 成功 ≠ 数据落盘 —— 从 fd 到磁盘块的完整链路

抽象层

VFS 用四个对象(superblock/inode/dentry/file)把 ext4/xfs/tmpfs 抹平成同一套 open/read/write

寻址层

文件 = inode(元数据)+ 数据块;硬链接是"多个名字同一个 inode",软链接是"存路径的独立 inode"

缓存层

Page Cache 让读写都先过内存——write 返回只进缓存,fsync 才真正落盘(MySQL redo log 的存在理由)

定位:OS 系列第十篇,文件与 I/O 三部曲之首。主线:fd → VFS → inode → 磁块的寻址链 + page cache 的持久性陷阱(write ≠ 落盘)。

Why File Systems Matter

先看事故:write 明明返回成功,重启后日志却没了

// 一个再普通不过的写日志函数
func onOrder(pay []byte) {
    n, err := f.Write(pay)
    if err != nil { return }  // 一切正常
    ack()                     // 已回客户端"成功"
}
// 30 秒后机房掉电。重启后:
// 最后 30 秒的订单日志 —— 一条都没有。
// 而且没有任何一行代码报错。
关键观察:write 的返回值只承诺"内核收下了",不承诺"磁盘写完了"。数据此刻躺在内存的 Page Cache 里,等后台线程延后落盘——掉电一来,内存里的东西全部归零。

没有文件系统这一层会怎样

如果没有 VFS 与 inode,每个程序都得自己算磁盘扇区、管空闲块、记哪个文件在哪些块上——换一块盘、换一种文件系统就要重写。VFS 把 ext4 / xfs / tmpfs / /proc 全部抹平成了同一套 open / read / write,程序只认路径。

没有 inode 与目录会怎样

文件名和文件内容必须分开存:名字(dentry)随便改、可以多个,内容(数据块)由 inode 统一管位置、权限、时间戳。正因为拆开了,重命名、硬链接、权限修改才成了"改一行元数据"的廉价操作。

没有 Page Cache / fsync 这对旋钮会怎样

磁盘比内存慢 5~6 个数量级。要么每次写都同步等盘(安全但慢到不能用),要么永远异步(快但会丢)。OS 的答案是给两个旋钮:默认 write-back 保性能,要安全就自己 fsync——面试问"write 成功数据在哪"就是问你知不知道这个旋钮。

本 deck 的路线

先看清抽象层(VFS 四对象)→ 再走一遍寻址链(inode / 链接 / 目录)→ 再看磁盘侧怎么保证崩溃一致性(ext4 日志)→ 最后回到内存侧的缓存与持久化(Page Cache / fsync)——这条链走完,开头那个事故的原因与解法都会自己浮出来。

动机页:用"write 返回成功但掉电丢数据"这个能立刻在脑子里跑起来的事故开场,先讲清没有文件系统抽象层会怎样,再给出本 deck 的四段路线。

Prerequisites & Glossary

先把词认全:下面每一页都会用到它们

术语一句话理解(先记住这个,细节后面展开)
文件描述符 fd进程打开一个文件后拿到的整数编号,后续 read/write 全用它指代这个文件
VFS 虚拟文件系统内核里的统一接口层,让 ext4 / xfs / tmpfs 对上呈现同一套 open/read/write
inode 索引节点一个文件的身份证:权限、大小、时间戳、数据块位置——唯独不含文件名
dentry 目录项路径里的"一段名字",作用是把名字映射到 inode
目录一种特殊文件,内容就是一张 dentry 表(名字 → inode 号)
硬链接 / 软链接硬链接 = 同一 inode 的多个名字;软链接 = 一个存着路径字符串的独立文件
元数据 metadata"描述文件的数据":权限、属主、大小、时间戳——不是文件内容本身
Page Cache 页缓存文件数据在内存里的副本,读写都先过它,避免每次都访盘
脏页 / fsync脏页 = 被改过但还没落盘的缓存页;fsync = 强制把它刷到磁盘的系统调用
日志 journal"先记下要做哪些修改,再去改"的崩溃恢复机制,与数据库 WAL 同源

如果你还不确定下面两件事

先读这两篇再回来,本 deck 默认你已经知道它们:
操作系统总览 → 系统调用怎么从用户态进内核态
虚拟内存 → "页"是什么、mmap 与缺页是怎么回事

最小心智模型(后面所有内容都是它的展开)

一个文件 = 名字(dentry)→ 身份证(inode)→ 财产(数据块)。删文件是摘名字,不是烧财产;改名是换名字,不动身份证;读文件是"按名字找到身份证、按身份证找到财产",而这一切的中间站是内存里的 Page Cache。

一个提前建立的直觉

整条链路上只有一个地方会真丢数据:数据在内存(Page Cache)里、还没到盘上的那段窗口。后面 ext4 日志、fsync、目录 fsync,全部是在回答"怎么把这个窗口关掉或者缩短"。

阅读提示:术语不用背,忘了回来查这一页就行;真正需要背的是"寻址链 fd→file→dentry→inode"和最后那张速查页。
前置页:十个术语先定义再使用。最小心智模型"名字→身份证→财产"把 inode/dentry/链接/删除语义统一成一个动作;提前点出"只有内存到磁盘这一段会真丢数据"。

Virtual File System

VFS:一套 API,万种文件系统

开场那个"write 成功却丢数据"的事故,要回答它必须先看清一次 write 到底穿过了哪几层——这就是 VFS 的层次图。

VFS 四大对象与从 fd 到磁盘的层次 层次图:应用持有文件描述符 fd,fd 指向内核的 file 对象(记录打开模式与读写偏移),file 指向 dentry 目录项(路径解析与缓存),dentry 指向 inode(文件元数据与数据块位置),inode 属于具体的文件系统如 ext4 或 xfs,最终落在块设备磁盘上。右上注明 VFS 四大对象的职责。 应用:read(fd, buf, n) fd 只是"打开文件表"的下标 进程 fd 表 0/1/2 = stdin/out/err file 对象(打开实例) 读写偏移 offset · 打开模式 · 引用计数 dentry 目录项 路径解析的中间层 · dcache 缓存 inode(文件真身) 元数据 + 数据块指针 · 唯一 inode 号 · 链接计数 具体文件系统:ext4 / xfs / btrfs / tmpfs 各自实现 VFS 定义的操作函数表 块设备 / 磁盘 VFS 四大对象(面试必背) superblock 文件系统全局信息(块大小、inode 总量、挂载状态) inode 一个文件的全部元数据 + 数据块位置(不含文件名!) dentry 路径的一个组成部分(文件名 → inode 的映射) file 一次打开的实例(offset、mode)——进程私有视角 关系速记:fd → file(打开态)→ dentry(名字)→ inode(内容)。一个 inode 可挂多个 dentry (= 硬链接);一个 file 每打开一次新建一个。
四对象最容易混的点:inode 不含文件名(名字在 dentry 里)、file 是打开实例(同文件多次打开各有 offset)。右链 fd→file→dentry→inode→FS→盘 是全篇骨架。

Inode Internals

inode:文件的身份证与数据地图

inode 里存什么

文件类型与权限、属主 uid/gid、大小、时间戳(atime/mtime/ctime)、链接计数数据块指针。注意两件"不在":文件名(在 dentry)和文件内容(在数据块)。

数据块怎么找:ext2/3 的间接指针

经典方案:15 个指针 = 12 直接 + 1 一级间接 + 1 二级间接 + 1 三级间接。直接指针管小文件(零间接访问),间接层按需扩容——大文件要经过多层查表。

ext4 的升级:extent(区段树)

ext4 改用 extent:一个条目记录"起始块 + 连续长度"——1 个条目描述 128MB 连续空间(4KB 块),大文件从"成千上万指针"变"几层区段树",既省 inode 空间又对顺序 I/O 极友好。

inode 资源是有限的

格式化时定死 inode 总量(mkfs -i 可调)。inode 耗尽 = 磁盘明明有空间却写不了文件(海量小文件场景),df -i 查看——运维经典事故。

方案机制特点
间接指针(ext2/3)12 直接 + 3 级间接稳定可预测;大文件多层查表
extent(ext4)连续区段 + 区段树大文件友好、省元数据
B-tree(xfs/btrfs)扩展 B 树 / CoW B 树海量文件与快照能力强
stat 命令验证:stat file 能看到 inode 号、链接数、三个时间戳。ctime 是元数据变更时间(不是创建时间!),chmod 也会动它——高频混淆点。
面试金句:"文件 = inode(身份证)+ dentry(户口上的名字)+ 数据块(财产)。删文件删的是名字,inode 在链接计数归零且无人打开时才真正回收。"
三种寻址方案按演进讲:间接指针 → extent → B 树。ctime ≠ 创建时间与 inode 耗尽(df -i)是两个高频陷阱。"删名字不删内容"为下一页硬链接铺垫。

Hard Link vs Symbolic Link

硬链接与软链接:两种"别名"的本质区别

硬链接与软链接的结构对比 左侧硬链接:两个目录项 a.txt 和 b.txt 指向同一个 inode,链接计数为 2,删除任一名字后另一个仍可访问。右侧软链接:link 是一个独立 inode,内容存放目标路径字符串,指向 a.txt 的目录项;删除 a.txt 后软链接悬空失效。 硬链接 ln a.txt b.txt dentry "a.txt" 目录里的名字 ① dentry "b.txt" 目录里的名字 ② inode #131072 links = 2 · 数据块在这 两个名字 = 同一个文件的户口本两行 删一个名字:links-1,另一个照常访问 限制:不能跨文件系统 · 不能给目录建(防环) 软链接 ln -s a.txt link dentry "link" 自己的名字 独立 inode 内容 = "a.txt" 路径串 目标文件 inode #98304 快捷方式:存的是路径字符串 目标被删 → 悬空(dangling);可跨 FS、可指目录 访问多一次路径解析 · ls -l 显示 l 类型 判别口诀:硬链接是"同一个 inode 的多个名字"(副本级等价);软链接是"存路径的独立文件"(指针级引用)
图解两条路:硬链接两名字一 inode(links 计数);软链接独立 inode 存路径(悬空风险)。经典追问:为什么不能给目录建硬链接——防环 + 让 .. 语义唯一。

Directory · dentry cache

目录也是文件,路径解析靠缓存

目录的本质

目录是一张特殊文件,内容是 dentry 列表:名字 → inode 号。所以"创建文件" = 目录里加一行 + 分配 inode;"删除" = 摘一行 + inode 链接计数减一。同一目录下名字唯一——就是这张表的键约束。

路径解析:一层层查

/a/b/c.txt 的解析:从根 / 的 inode 找到 a 的 dentry → 装入 a 的 inode 与内容 → 找 b → … 每一级都可能是一次磁盘读。真实系统 dcache(dentry cache)缓存"名字→inode"结果,热路径全内存命中。

硬链接 vs 目录项的数量错觉

目录里看到 1000 个文件 = 1000 行 dentry + 至多 1000 个 inode;若 999 个是硬链接,实际只有 1 份内容——文件数 ≠ 数据量,海量小文件的成本主要在元数据(inode/dentry)。

相对路径与 ".." 的实现

.. 不是魔法,是目录里的一行 dentry(指向父目录 inode)——这也是"目录不能建硬链接"的另一个理由:否则环上会画出多个自洽的父路径

目录文件的内容:名字到 inode 的映射表 目录文件内容示意:dentry 表列出 .、..、a.txt、b.txt 四行,分别映射到自身目录、父目录和两个文件的 inode 号;右侧标注同 inode 硬链接计数。 目录 /home/app 的内容(dentry 表) . → inode #65536(自己) .. → inode #32768(父目录) a.txt → inode #131072 b.txt → inode #131072(硬链接) a.txt 与 b.txt 指向同一 inode → links=2 目录"内容"就是这张表——cat 它需要特权,ls/stat 是它的安全视图
面试金句:"路径解析 = 沿 dentry 表逐级跳 inode;dcache 是把跳表结果缓存起来——所以深路径不一定慢(热),浅路径不一定快(冷)。"
目录 = dentry 表的直白解法(图)消除"目录魔法感"。dcache 与海量小文件成本是运维向加分点;".." 的实现解释了目录硬链接禁令。

ext4 Layout · Journal

ext4:块组布局与崩溃一致性(日志)

块组布局

磁盘切成多个 块组(block group),每组自带一份元数据副本:超级块、块位图(哪些数据块可用)、inode 位图、inode 表、数据块——把元数据摊近数据,既分散热点又减少寻道。数据在 inode 邻近分配(预分配策略),顺序写更快。

为什么需要日志(journal)

写一个文件要改多处(inode、位图、目录、数据块)——掉电只写了一半,元数据就撕裂了(fsck 扫全盘也未必救回)。日志的思想:先把"要做哪些修改"记到 journal(顺序追加,快),再正式落盘;崩溃后重放日志即可恢复到一致点。

三种日志模式

journal:元数据+数据都进日志(最安全,最慢);ordered(默认):只记元数据,但保证数据先于元数据落盘;writeback:只记元数据,数据随意(最快,崩溃后可能看到新元数据+旧数据)。与 MySQL redo log / WAL 是同一思想:顺序日志换随机写一致性。

日志模式的写盘顺序对比 三行对比:journal 模式先写日志再写数据最后提交;ordered 模式数据先落盘、元数据记日志、再提交;writeback 模式元数据记日志提交,数据写盘时机随意。 journal 日志(数据+元) 数据落盘 元数据 commit ordered ★ 数据先写 日志(仅元数据) commit writeback 日志(仅元数据) commit 数据随意写 安全性 journal > ordered > writeback · 性能反之 与 WAL/redo log 同源:顺序日志换崩溃一致性
面试金句:"ext4 日志回答的问题是'崩溃的瞬间文件系统在哪一步'——先记意图再执行,重放即可恢复;MySQL 的 redo log 是同一答案在数据库层的重述。"
块组布局一句带过(重点是"元数据就近"的动机),日志三模式是本页核心:ordered 默认 + 与 WAL 同源。崩溃一致性是文件系统和存储中间件的公共考题。

Page Cache · Write-back

Page Cache:磁盘的内存影子

读路径

read() 先查 Page Cache(以 4KB 页为单位的文件缓存):命中直接拷给用户(不访盘);未命中读盘并预读(readahead,赌空间局部性——顺序读的吞吐法宝)。

写路径:write-back

write() 只写 Page Cache 就返回(页标脏),刷盘线程(per-BDI writeback)延后批量落盘——触发条件:脏页比例超 dirty_ratio(约 20%,开始节流写入者)、脏页老化超 dirty_expire_centisecs(默认 30s)、用户显式 sync。

一致性影响(必考)

write 返回成功 ≠ 数据在盘上——掉电丢最多 30s 的写入,"写文件崩溃丢数据吗"的答案全在这;② 多进程读同一文件天然共享缓存(高效);③ mmap 写与 read 的视图一致(同一缓存),但与直接设备写有窗口期不一致

O_DIRECT 什么时候用

自带缓存的应用(数据库)用 O_DIRECT 绕过 Page Cache,避免双重缓存与缓存污染——MySQL InnoDB 就是代表;普通应用老实用 Page Cache(它有预读与全局复用)。

控制点默认含义
dirty_ratio~20%脏页超此比例,写入者被节流(同步落盘)
dirty_background_ratio~10%超过则唤醒后台刷盘线程
dirty_expire_centisecs3000脏页 30s 后必须落盘
readahead128KB 起顺序读预读窗口,可按文件调
观测:free -h 的 buff/cache(Page Cache 大小)、cat /proc/meminfo | egrep 'Dirty|Writeback'(正在积累/正在刷的脏页)、iostat 的 w/s 与 wMB/s(落盘速率)。
面试金句:"Page Cache 把磁盘当慢速内存的延伸:读赌局部性,写赌不崩——write-back 的性能与 fsync 的安全是一对旋钮,应用必须知道自己要哪个。"
Page Cache 四件事:读命中/预读、write-back 刷盘三触发、"write 成功≠落盘"、O_DIRECT 的存在理由。脏页参数表给足运维弹药;fsync 是下一页主角。

Durability · fsync / O_DIRECT

持久性阶梯:从 write 到 fsync 到 O_DIRECT

持久性四级(代价递增)

write:进 Page Cache 即返回——最快,掉电可能丢;② fsync(fd):该文件的脏页 + 元数据全部落盘;fdatasync:只刷数据与必要元数据(大小变了才刷)——省一次元数据写;③ sync():全系统脏页(运维用,应用别用);④ O_DIRECT + 自管缓存:完全绕开内核缓存,自己决定持久化。

元数据陷阱(经典事故)

新文件 write + fsync 数据 ≠ 安全:目录项还没落盘,掉电后文件整个消失。正确姿势:fsync 文件 → fsync 所在目录。SQLite/LevelDB 的文档都强调这一步——"持久化边界是整条元数据链"。

fsync 的性能账

一次 fsync ≈ 一次磁盘寻道 + 刷写,HDD 毫秒级、SSD 也要百微秒级。高频小写入系统(消息队列、WAL)靠三招摊薄:组提交(攒一批一次 fsync)、顺序追加文件、journal 预留区——MySQL redo log 与 Kafka 的 log 就这么干。

// 正确的"安全写一个小文件"
fd = open(path, O_WRONLY|O_CREAT, 0644);
write(fd, data, len);
fsync(fd);          // 数据+该文件元数据落盘
close(fd);
// 关键一步(经常被忘):
dfd = open(dirpath, O_RDONLY|O_DIRECTORY);
fsync(dfd);         // 把目录项也落盘!
close(dfd);

// Go 对应:
// f.Sync()  → fsync
// 目录 fsync 需要自开目录 fd
与 MySQL 的对账(跨册联动):InnoDB 每事务 redo fsync + 组提交、binlog 两阶段提交——全部建立在本页语义上:三大日志 deck 的 crash-safe 结论 = 本页机制 × 数据库策略。
面试金句:"持久化的单位不是'写'而是'fsync 边界':数据、元数据、目录项——漏掉任何一环,掉电都可能让整次写入蒸发。"
四级阶梯 + 目录 fsync 陷阱(最高频的错误)+ 组提交的性能出路。与 MySQL log deck 的对账让跨领域链接有了具体含义。

Tooling Cheat Sheet

文件系统排障工具箱

命令看什么
stat finode 号、链接数、三个时间戳
df -h / df -i容量 vs inode 余量(海量小文件事故)
lsof | grep deleted"已删除但仍被打开"的文件(空间不释放)
du -sh * vs df -hdu 小 df 大 = 有 deleted-but-open 文件
/proc/meminfo: Cached/DirtyPage Cache 与脏页水位
iostat -x 1设备 util、w_await(落盘压力)
经典案:磁盘满了但 du 找不到大文件——进程还开着已删除的大日志(fd 引用着 inode),lsof | grep deleted 找到后重启/截断该进程即可释放。原理正是"删名字不删 inode"。

三个故障 → 三条路径

写不了文件但 df 有空间 → df -i 看 inode 耗尽(海量小文件/坏目录);② 删了日志磁盘不释放 → lsof deleted(fd 引用);③ 写入毛刺 → 看 Dirty 水位(dirty_ratio 节流)与 iostat util(撞上 journal 刷盘)。

vmtouch(冷知识加分)

vmtouch file 显示文件有多少页在 Page Cache 里;vmtouch -t 预热(把热数据搬进缓存)——低延迟服务的启动预热手段。

文件数与性能

单目录百万文件:dcache 压力 + 目录操作变慢(线性扫 dentry)。工程上限思维:分桶目录(hash 前缀两级),这也是图床/对象存储的通用布局。

命令表按"故障→工具"组织。deleted-but-open 与 inode 耗尽是两个最高频的真实事故;vmtouch 是冷门加分项,分桶目录是工程模式。

Go × File I/O

Go 的文件 I/O:直薄 syscall + bufio 习惯

os.File 的层次

os.Open/os.Create → 持有 fd 的 os.File;Read/Write 直发 read/write 系统调用(无内置缓冲!)。所以小数据高频写必须包 bufio.Writer(默认 4KB,把 N 次 write 合并成 1 次)——syscall 成本意识直接来自第 1 篇。

持久化三件套

f.Sync() = fsync;目录 fsync 要自己 open 目录 fd 再 Sync;需要"追加且顺序"的日志型写入用 O_APPEND(原子追加,多进程安全)——Go 写 WAL/日志文件的标准组合。

mmap 与大文件读取

标准库无 mmap,golang.org/x/exp/mmap 提供只读映射;随机访问大文件(索引文件、只读字典)mmap 省一次拷贝且享受 Page Cache——与下一篇 zero-copy 呼应。

并发写文件

os.File 的 Write 并发安全(内部锁),但多 goroutine 交错写会打乱逻辑顺序——协议类输出仍建议单写者 + channel 聚合(与 ipc deck 的 CSP 结论一致)。

// 高性能追加日志的标准骨架
f, _ := os.OpenFile("app.log",
    os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0644)
w := bufio.NewWriterSize(f, 64<<10)

func onLog(line []byte) {
    w.Write(line)          // 进 64KB 用户态缓冲
}
func onFlush() {
    w.Flush()              // 批量 write(一次 syscall)
    f.Sync()               // 需要落盘时才 fsync
}
// 语义分层:bufio 管成本,
// Sync 管安全——两者独立控制
面试金句:"Go 的 os.File 是无缓冲薄封装——把'缓冲还是直写'的决定权留给应用:性能(bufio 合并 syscall)与安全(Sync 落盘)是两个独立旋钮。"
联动:Kafka 的顺序写 + 刷盘策略(producer acks / log.flush)在 Kafka 底层 deck 有完整对账——同样的 Page Cache 玩法。
Go 岗三件:os.File 无缓冲直薄 syscall(bufio 的存在理由)、Sync/目录 fsync/O_APPEND 三件套、mmap 只读场景。"性能与安全双旋钮"是本页记忆锚。

Cheat Sheet

一页带走:寻址链、链接、持久化、排障

① 寻址链(读一个文件到底走过哪几层)

fd → file(打开实例:offset/mode)→ dentry(名字→inode,走 dcache)→ inode(元数据+数据块位置)→ 具体文件系统(ext4/xfs)→ 块设备。
· inode 不含文件名;file 是"一次打开",同文件打开两次各有一个 offset
· 中途先查 Page Cache:命中直接拷贝返回,不访盘

② VFS 四大对象

superblock整个文件系统的全局信息(块大小、inode 总量)
inode一个文件的元数据 + 数据块位置
dentry路径的一段,名字 → inode 的映射(可缓存)
file一次打开的实例,持有 offset 与模式

③ 硬链接 vs 软链接

硬链接同一 inode 的多个名字;不能跨 FS、不能指目录;删一个名字 links-1
软链接独立 inode 存路径串;可跨 FS、可指目录;目标删除后悬空

④ 持久性四级(代价递增)

write只进 Page Cache 即返回,掉电可丢(默认最多 30s)
fdatasync刷数据 + 必需的元数据(大小变了才刷 inode)
fsync刷数据 + 该文件全部元数据
O_DIRECT绕开 Page Cache,应用自管缓存(数据库专用)

⑤ 最容易忘的一步

新建文件:fsync(文件) 之后还要 fsync(所在目录)。
持久化的单位不是"写",而是整条元数据链——数据、inode、目录项,漏一环掉电就全丢。

⑥ 三个故障 → 三条命令

有空间却写不进df -i(inode 耗尽)
删了日志空间不释放lsof | grep deleted(fd 还引用着)
写入毛刺 / 卡顿/proc/meminfo 的 Dirty + iostat -x 1
一句话背下来:文件 = 名字 + 身份证 + 财产;读写先过内存,落盘要显式 fsync,且必须连目录一起 fsync
速查页:六块按使用顺序排——寻址链、四对象、两链接、持久性四级、目录 fsync 陷阱、故障→命令。回看时只看这一页。

Interview QA · Part 1

高频追问:结构与链接

先盖住答案自己答一遍,再展开对照——想不起来比看得顺眼记得牢;答不出的直接翻回速查页。

1 · 描述一下读文件的完整链路。

fd→file→dentry→inode→块

read(fd) → 进程 fd 表找到 file(offset)→ dcache/dentry 定位 inode → 先查 Page Cache:命中拷贝返回;未命中经块层读盘(带预读)→ 拷贝到用户缓冲。全程两次拷贝(盘→缓存→用户)。

2 · inode 是什么?为什么删除文件后空间没释放?

fd 引用

inode 是文件元数据 + 数据块位置,文件名在 dentry。删除只摘 dentry,inode 链接计数减一;还有进程开着 fd 就不回收——lsof | grep deleted 找到元凶(日志未切割场景)。

3 · 硬链接和软链接的区别?(必考)

inode 级 vs 路径级

硬链接:多个 dentry 指同一 inode(等价副本,不能跨文件系统/不能指目录);软链接:独立 inode 存路径字符串(可跨 FS/指目录,目标删除后悬空,多一次解析)。

4 · 为什么不能给目录建硬链接?

防环

目录树要求唯一父路径:目录硬链接可成环(a→b→a),find/遍历死循环、.. 语义崩坏。系统只保留 . 和 .. 两个受控"目录链接",用户级一律用软链接。

5 · 软链接和文件复制有什么区别?

引用 vs 拷贝

软链接只存路径(几十字节、独立 inode、目标变则内容"变");复制是完整新 inode + 新数据块(空间全量、与源解耦)。硬链接介于两者:零拷贝但真等价。

6 · stat 里三个时间 atime/mtime/ctime 分别是什么?

ctime≠创建

atime 读访问(Noatime 挂载可关以省性能)、mtime 内容修改、ctime inode 元数据变更(chmod/mv/链接数变化都触发)。没有"创建时间"字段(部分 FS 扩展有 birth time)。

结构组六题:读链路、inode 与 deleted、硬软链接、目录禁令、复制对比、时间戳。第 4 题的"唯一父路径"论证比"防环"三个字高一档。

Interview QA · Part 2

高频追问:缓存与持久化

同样建议先自答。这一页的题都需要"结论 + 一句代价/边界"才完整——只答结论会被追问。

7 · write 返回成功,数据一定在磁盘上吗?

write-back

不一定:write 只进 Page Cache(脏页),后台线程延迟批量刷盘(比例触发/30s 老化)。要持久必须 fsync;要连目录项一起持久还要 fsync 目录——掉电窗口的来源。

8 · fsync 和 fdatasync 的区别?

元数据范围

fsync 刷数据 + 全部元数据(mtime、inode);fdatasync 只刷数据 + 持久化数据所必需的元数据(文件大小变了才刷)——追加写场景 fdatasync 明显省一次 inode 写,数据库 WAL 常用。

9 · 什么时候用 O_DIRECT?

双重缓存

应用自带缓存(InnoDB Buffer Pool)时绕过 Page Cache:省内存双份、避免缓存污染、自己控制刷盘节奏。代价:放弃预读与全局复用,一般要对齐 I/O。普通应用别用——Page Cache 的预读很值钱。

10 · 什么是预读?为什么顺序读这么重要?

readahead

内核检测到顺序访问模式就提前把后续页读进 Page Cache——把"每次读都等磁盘"变成"磁盘一直在跑"。这就是数据库强调顺序 I/O、日志型存储(LSM/Kafka)碾压随机写的物理原因。

11 · ext4 的日志解决什么问题?和 redo log 什么关系?

崩溃一致性

一次文件写涉及多处元数据,崩溃会撕裂。日志先把修改意图顺序记下再执行,重放恢复一致点——WAL 思想。MySQL redo log 是同一思想在事务层的实现(区别:redo 面向页、带 LSN、配合两阶段提交)。

12 · Page Cache 太大把内存吃光了怎么办?

它是可回收的

Page Cache 是"可回收内存"——内存紧张时自动让位(第 7 篇回收流水线的第一顺位),不需要人工清理。真要限:cgroup v2 的 memory.high 限业务进程,或 drop_caches 应急(生产慎用,之后冷启动更慢)。

缓存与持久组六题:write-back、fsync 家族、O_DIRECT、预读、日志与 WAL、Page Cache 回收。第 12 题纠正"cache 占内存=问题"的常见误解。

Related & References

相关知识点与参考

OS 系列(本分类)

I/O 模型与 epoll →("等数据就绪"的另一半故事)
零拷贝 →(把本篇的两次拷贝再省掉)
虚拟内存 →(mmap/缺页是文件映射的地基)
内存布局 →(Page Cache 与用户堆共用物理内存)

跨领域联动

三大日志与 crash-safe →(WAL 的数据库版)
Kafka 高吞吐 →(顺序写 + Page Cache + 零拷贝)

参考来源(本 deck 结论可溯源至下列一手材料)

OSTEP ch.39–43(文件与目录 / 崩溃一致性)inode、目录实现、日志(journaling)的理论框架
CSAPP ch.10(系统级 I/O)fd / file 表 / v-node 的层次与共享语义
man 2 fsync / open(O_DIRECT)/ stat持久化语义、直读模式、元数据字段的一手定义
ext4 wiki(kernel.org)+ xfs docsextent、三种日志模式、块组布局
MySQL 8.0 Manual(innodb_flush_method / doublewrite)O_DIRECT 的数据库实践与崩溃一致性补强
xiaolincoding.com《图解系统》file_system / pagecacheinode 全家桶与脏页丢失实验的中文叙述
收尾:OSTEP 的崩溃一致性两章与 CSAPP ch10 是双主源。总页数 13。下一篇:I/O 模型与 epoll——从"文件"转向"网络就绪事件"。