03.07 文件与目录:从聊天日志轮转理解名字与打开对象
返回第三卷 · 进程与系统调用前置:03.02 · 目录与 FD 前置:03.03 · 分配与映射前置:03.06
03.07 文件与目录:从聊天日志轮转理解名字与打开对象
DeepTutor 初稿经技术与教学审阅后的静态课程。
/logs、文件编号、偏移和消息内容均为纸上教学数据;没有执行文件操作、Go、Linux 观测、IM 服务、基准或站点。本章以 Linux 普通文件为主要语境;名称变更与打开对象的行为按 Linux 手册核对,不把路径操作直接说成崩溃后持久保证。
一、路径名换了,旧写入为什么还去旧日志?
设想虚构 IM 网关把 c-a 的日志写到 /logs/chat.log。轮转时,运维动作把它改名为 /logs/chat.log.1,再创建一个新的 /logs/chat.log。如果网关仍握着改名前打开的文件,它继续写哪一份?删除 .1 后目录里看不到它,为何空间可能仍被占用?
要回答,先把“文件”拆成三层:目录里的名字,保存文件元数据与内容身份的 inode,以及进程已经打开的引用。固定纸上初态:旧日志的 inode 记作 I1,内容恰好是 m-a\nm-b\n,其中 \n 各占一个字节,所以两行分别 4 字节、总共 8 字节。进程以可读写方式打开旧日志,得到文件描述符 fd7。后续的新日志 inode 记作 I2。I1/I2 和 fd7 是教学符号,不代表系统实际分配编号。
| 第一遍 | 第二遍 | 交付物 |
|---|---|---|
| 路径、目录项、inode、fd、打开文件描述与偏移 | dup、重新打开、rename、unlink、短读写与耐久边界 | 一张三层引用图、一张偏移表、一张轮转状态表 |
03.02 已讲系统调用与文件读取,03.03 已认识工作目录、权限与 FD。这里把“名字怎么找文件”和“已打开的对象怎么继续用”连起来;03.08 才处理 fsync、目录项持久化和崩溃点。
二、目录项与 inode:名字是入口,inode 才标识对象
目录把一个名字关联到某个文件对象。在 Linux 文件系统的入门模型中,/logs 目录里的条目 chat.log → I1,而 I1 保存类型、权限、大小、时间、硬链接数等元数据,并关联文件数据。路径 /logs/chat.log 是从根目录逐段解析到这个条目的查找方法;它不是 I1 本身。
目录 /logs
chat.log ───→ inode I1 ───→ 数据:m-a\nm-b\n一个 inode 可以由多个硬链接名字指向。若 chat-copy.log → I1 也存在,删掉 chat.log 只减少一个名字,不会使另一个名字失效。inode 号只保证在同一个文件系统内标识文件;跨文件系统比对还需要设备身份,因此“看到相同 inode 数字”本身不足以证明是同一对象。符号链接又是另一种机制:它存放要解析的目标路径,可能成为悬空链接;本章轮转先不用它,避免把两种链接混成一种。
目录项和 inode 的分离使改名成为可能:把 chat.log 改成 chat.log.1,是改变目录名称关系,不要求把日志内容全量复制一遍。对名称、权限和硬链接的具体操作还要检查调用是否成功;磁盘空间、跨文件系统操作和并发其他进程都可能让实际结果不同于纸图。
三、FD、打开文件描述与偏移:两个整数不一定有两份位置
Linux 的 open 在成功时给进程一个文件描述符(FD),它是进程 FD 表中的索引;同时形成一个打开文件描述(open file description),记录当前偏移和文件状态标志,并关联 I1。名字在打开时帮助找到文件,之后 fd7 对应的打开对象不会因路径改名而自动切换。
进程 FD 表: fd7 ──→ 打开文件描述 O1(偏移 0)──→ inode I1
目录 /logs:chat.log ─────────────────────────────→ inode I1现在假定每次请求的字节数都能完整读到、没有错误与并发修改,手算两个 read(4):第一次 fd7 读得 m-a\n,O1 偏移由 0 变成 4。接着将 fd7 复制为 fd8;这是两个 FD 指向同一个 O1,不是再打开一次文件。fd8 再读 4 字节得 m-b\n,O1 偏移变成 8,fd7 看见的也是偏移 8。若另行按路径打开得到 fd9→O2→I1,它有独立的偏移 0,再读 4 字节得 m-a\n、O2 偏移变成 4。本章随即关闭 fd9,后续轮转只保留 fd7/fd8。
| 操作后 | fd7/fd8 指向 O1 的偏移 | fd9 指向 O2 的偏移 | 读到的数据 |
|---|---|---|---|
初始 fd7 打开 | 0 | 尚无 O2 | — |
fd7 读 4 字节 | 4 | 尚无 O2 | m-a\n |
dup(fd7) 得 fd8,再由 fd8 读 4 字节 | 8 | 尚无 O2 | m-b\n |
单独 open 得 fd9,由它读 4 字节 | 8 | 4 | m-a\n |
关闭 fd9 | 8 | O2 关闭 | — |
偏移属于打开文件描述,而非路径名或 inode 只有一份。 dup 共享偏移,独立 open 创建独立偏移;同一 inode 可以同时有多个打开文件描述。真实 read 可能少于请求的 4 字节,返回 0 对普通文件表示当前到达 EOF,错误需要单独处理。上表的完整 4 字节是为教偏移而写明的假设,不是文件读取 API 的通用保证。
四、重命名与新建:旧 FD 不会“追随名字”
现在执行一组纸上成功的同文件系统操作。先把 /logs/chat.log 改名为 /logs/chat.log.1;此时 .1→I1,而 fd7/fd8→O1→I1 不变。随后创建新 /logs/chat.log→I2,新打开的 fd10→O3→I2。若旧服务未重新打开日志,仍用 fd7 写入 m-c\n,它从 O1 偏移 8 写入旧 I1,得到旧文件 12 字节、O1 偏移 12;新 I2 仍为空。这里假定那次写完整 4 字节且无错误。
| 时刻 | /logs/chat.log | /logs/chat.log.1 | 旧 fd7/fd8 | 新 fd10 |
|---|---|---|---|---|
| 初态 | I1 | 不存在 | O1→I1 | 尚无 |
rename 后 | 暂不存在 | I1 | O1→I1 | 尚无 |
| 创建并打开新日志后 | I2 | I1 | O1→I1 | O3→I2 |
旧 fd7 写 m-c\n 后 | I2,仍空 | I1,现 12 字节 | O1→I1,偏移 12 | O3→I2,偏移 0 |
注意表中的空档:rename 旧名和“创建新 chat.log”是两步,期间原路径不存在。Linux rename 在其自身操作范围内改变名字或替换目标有原子性语义,不会自动把这两步合成一个无空档事务;跨文件系统移动也不具备本章所假设的同文件系统成功路径。业务若要求轮转期间写入永不丢失,应制定“谁切换 FD、旧新文件如何交接、失败如何恢复”的规则。
os.OpenFile 可以作为 Go 程序获取 *os.File 的入口,但本章没有提供真实轮转实现。Go 的 os.Rename 在不同操作系统和文件系统上的保证需要单独核对;本表陈述的是 Linux 手册下的单步概念模型。改名成功也不等于这些目录更新在突然掉电后一定保留,持久化将在 03.08 说明。
五、unlink:目录里没名字,不等于打开对象立刻消失
再设删除 /logs/chat.log.1,且它是 I1 的最后一个硬链接名字。目录里再也找不到 I1;但 fd7/fd8 仍指向 O1→I1,因此进程仍可通过这些 FD 使用旧对象,I1 的数据空间也还不能按“已经完全无人使用”回收。先关闭 fd7,fd8 还在,O1 仍被引用;再关闭 fd8,在没有其他 FD、映射等引用的纸上假设下,旧 I1 才具备释放条件。新 /logs/chat.log→I2 从始至终是另一对象。
unlink(.1) 后:目录中没有 I1 的名字;fd7、fd8 → O1 → I1
close(fd7) 后:fd8 → O1 → I1
close(fd8) 后:旧对象不再由这些 FD 持有,可由文件系统回收这是“rm 看不到日志,磁盘占用仍高”的一个可能解释,还要确认是否有别的硬链接、映射或进程引用,以及文件系统实际占用。不能据此断定每次空间不回落都由删除后未关闭的 FD 造成;也不能把 close(fd7) 误认为所有复制过的 FD 都关闭。若要只读核对,后续可将进程 FD 列表、文件设备号/inode 和文件系统空间变化放在同一时间线,而不要先删更多文件尝试“修复”。
六、路径解析、权限与 Go 文件对象
相对路径 logs/chat.log 从进程当前工作目录(cwd)解析;如果 cwd 从 /srv/im 变到 /tmp,同一字符串不再指向同一处。目录需要允许穿越/搜索,文件打开还需满足相应访问权限;路径组件可能包含符号链接。03.03 已用 PID/cwd/权限定位进程现场,本章强调成功打开前的路径查找与成功打开后的对象引用分开。
在 Go 中,os.Stat(path)按当前路径名询问对象,f.Stat()询问已经打开的 *os.File。轮转以后,对 /logs/chat.log 做路径查询可见 I2,而旧 f 仍对应 I1。对比时应结合设备号与 inode 等系统信息,不要只比较文件大小:两份不同文件可能同样大小,且名字随时可能被并发改动。用户输入若能影响文件路径,还需要规范化与权限边界,09.08 已从应用安全角度讨论输入;这里不展开安全实现。
| 问题 | 看路径名 | 看已打开文件 |
|---|---|---|
“此刻 /logs/chat.log 是谁?” | 解析目录,轮转后是 I2 | 旧 fd 不回答新路径是什么 |
| “旧日志处理器继续写谁?” | 路径可能已换 | 旧 fd 保持 O1→I1 |
| “轮转后还要写新日志?” | 需按新路径打开并检查成功 | 明确切换到指向 I2 的新 FD |
打开文件也占用有限 FD 资源,错误路径必须关好自己拥有的 FD。FD 泄漏会让进程最终难以继续打开文件或 socket;而“关闭过一个整数”不能证明另一个复制 FD 或其他进程的引用已经释放。03.10 再学习把 FD 数量、文件身份和进程症状组成诊断证据。
七、字节读取与写入:边界要由程序维护,落盘另有合同
read 和 write 处理的是字节数量,返回实际处理的字节数;一次调用可能少于请求量,不能把“请求 4 字节”写成“必然得到一条 m-a\n”。如果应用把一条消息定义为一行,就要在程序层处理短读、EOF、换行边界和编码,不能把文件 API 当消息协议。写入也可能部分成功或失败;重试剩余字节必须依据实际返回的 n 和错误,避免重复前缀或漏掉后缀。
对普通文件,打开时指定 O_APPEND 可使单次 write先定位到当前文件末尾并写入;它不保证多次 write 组成的业务消息完整原子,也不把远端/网络文件系统的行为统一成这张纸图。更关键的是,普通缓冲写可能先进入页缓存:write 返回说明这次调用的字节操作结果,不等于文件内容和目录名字都具备掉电后恢复保证。03.08 会把 write、fsync、目录同步、rename 的崩溃点拆开。
| 失败或变化 | 本章必须检查 | 不可直接声称 |
|---|---|---|
read 返回少于预期 | n、错误、是否 EOF 与是否继续读 | “一调用就是一条完整消息” |
write 部分成功 | 已写前缀 n 与剩余字节、错误 | “整条日志都已写入” |
| 改名后继续写旧 fd | fd 关联的打开描述/inode | “名字变了所以写入自动切换” |
write 返回成功 | 已接受的字节与进程状态 | “掉电后文件和目录项都能恢复” |
八、交付日志轮转纸图与分层练习
交付三张可核查的纸:第一张画“名字→inode”和“FD→打开文件描述→inode”两条链;第二张逐步写 read/dup/独立 open 的偏移;第三张写 rename→创建新文件→旧 fd 写入→unlink→逐个 close 的状态与可能失败点。全部数值和结果为明确假设下的推演,不是已执行的 Linux/Go 测试。
分层练习:先答,再展开反馈
1. `/logs/chat.log` 是 inode 本身吗?
不是。它是经目录解析找到文件对象的路径名。
2. inode 通常保存哪几类信息?
文件类型、权限、大小、时间、硬链接数等元数据及数据定位信息。
3. 不同文件系统里的相同 inode 数字能证明同一文件吗?
不能。inode 号只在所属文件系统内有标识意义,还要看设备身份。
4. 两个硬链接名字指向同一 inode,删一个名字会删除另一个吗?
不会。另一个目录项仍指向这个 inode。
5. 符号链接和硬链接等同吗?
不等同。符号链接按保存的目标路径另行解析,可成为悬空链接。
6. FD 是路径字符串吗?
不是。FD 是进程 FD 表中的整数索引,指向打开文件描述。
7. 打开文件描述主要保存什么状态?
当前文件偏移、状态标志及关联的打开文件对象等。
8. 旧日志的 `m-a\n` 和 `m-b\n` 各多少字节?
按本章 ASCII 教学数据,m-a\n 和 m-b\n 各 4 字节,共 8 字节。
9. `fd7` 从偏移 0 完整读 4 字节后,O1 偏移是多少?
4,读到 m-a\n。
10. `dup(fd7)` 得 fd8 后,fd8 是否从 0 开始?
不是。它与 fd7 共享 O1,开始时偏移为 4。
11. fd8 再完整读 4 字节后,fd7 所见偏移是多少?
8;两 FD 共享 O1 的偏移。
12. 独立 `open` 同一路径得 fd9,初始偏移是多少?
0;它创建独立的 O2,即使仍指向 I1。
13. `rename chat.log→chat.log.1` 后,fd7 指向哪个 inode?
仍是 I1,改名不会重定向已打开的 FD。
14. 新建 `chat.log→I2` 后,旧 fd7 写入会写到 I2 吗?
不会。fd7 仍沿 O1 写 I1;要写 I2 需打开并切换新 FD。
15. 旧 fd7 从偏移 8 完整写 `m-c\n` 后,I1 的长度是多少?
12 字节,O1 偏移变成 12;这是没有并发修改与错误的纸上假设。
16. `rename` 旧名再创建新名,两步之间原路径一直存在吗?
不一定。本章顺序中两步之间 /logs/chat.log 暂不存在。
17. `unlink(chat.log.1)` 后,fd7/fd8 会立即失效吗?
不会。它们仍通过 O1 引用 I1。
18. 只关闭 fd7 后 I1 一定可回收吗?
不能;fd8 还引用 O1,其他链接或引用也需核对。
19. 相对路径按什么位置解析?
按当前工作目录等路径解析规则;同一字符串在不同 cwd 下可指向不同对象。
20. `read(4)` 成功就必然返回 4 字节吗?
不必然。应检查实际 n,少读可能由 EOF 等原因造成。
21. `O_APPEND` 能保证由三次 write 组成的一条消息整体原子吗?
不能。它只涉及每次 write 的末尾定位与写入语义,业务记录边界仍需设计。
22. `write` 成功和 `rename` 成功能直接证明掉电后日志完整吗?
不能。数据和目录项的同步、崩溃点与文件系统条件在 03.08 才系统说明。
来源与下一步
- Linux
open(2)、dup(2)与lseek(2):FD、打开文件描述、共享与独立偏移。 - Linux
inode(7)、rename(2)与unlink(2):名字、链接、重命名和打开对象的生命周期。 - Linux
read(2)、write(2)与Goos包:实际字节数和 Go 文件对象入口。
按学习路线,下一章03.08 持久化机制用同一类历史保存场景推演 write/fsync/rename 之间的崩溃点。离开本章前,应能解释路径名、inode、打开文件描述与 FD 的各自责任,旧 FD 为何不追随新名字,以及为什么删除名字和数据持久性都要单独判断。