文件的操作
4.1.1 列出了六个基本操作,这一节讲它们各自在系统内部做了什么。
真正的重点只有一处:打开文件表。这个数据结构是理解”文件描述符是什么""为什么两个进程读同一个文件互不干扰""关闭文件到底关掉了什么”这一整串问题的唯一钥匙。
机制
创建与删除:都是”两件事”
创建文件要做两件事:① 在外存中为文件分配必要的空间;② 在目录中为之创建一个目录项,目录项中记录了文件名、在外存中的地址等信息。
删除文件同样是两件事,恰好是创建的逆操作:① 在目录中找到该文件的目录项,使之成为空项;② 回收该文件占用的存储空间。
这两组动作的对称性值得注意:目录项和数据空间是两份独立的资源,必须成对地分配、成对地回收。 后面讲文件系统一致性时会看到,崩溃恰好发生在两件事之间,就会留下”目录项已建但空间未分配”或”空间已占但无人引用”的不一致状态。
打开与关闭:为什么需要它们
不打开也能读写吗——理论上可以,但代价是每一次读写都要重新走一遍完整的目录检索:解析路径、逐级读目录文件、比对文件名、找到 FCB、检查权限。对一个要读一万次的文件,这一套要做一万遍。
打开操作就是把这套开销一次性付清。 系统在 open 时完成检索与权限检查,把文件的属性从外存复制到内存打开文件表的一个表项中,并将该表项的编号返回给用户。此后的读写只要报这个编号,系统直接查表,不再碰目录。
关闭操作则是把这份连接状态撤销:删除进程打开文件表中的对应表项,并把系统打开文件表中的打开计数减一;计数减到 0 时,才真正回收该表项,必要时把被修改过的属性写回磁盘。
两级打开文件表
打开文件表有两张,分属两个层级,这是本节的核心结构:
系统打开文件表全系统只有一张,每个被打开的文件在其中占一个表项,记录文件号、文件名、外存地址、访问权限、打开计数。
进程打开文件表每个进程各有一张,每次 open 调用在其中占一个表项,记录文件描述符 fd、读写指针、访问权限、以及指向系统表中对应表项的索引号。
graph LR subgraph P1["进程 A 的打开文件表"] A1["fd=3<br/>读写指针=200<br/>→ 系统表 #7"] end subgraph P2["进程 B 的打开文件表"] B1["fd=5<br/>读写指针=0<br/>→ 系统表 #7"] end subgraph SYS["系统打开文件表(全系统唯一)"] S1["#7 file.txt<br/>外存地址<br/>打开计数=2"] end INODE["内存索引节点<br/>物理地址 / 长度 / 权限"] A1 --> S1 B1 --> S1 S1 --> INODE classDef proc fill:#dbeafe,stroke:#2563eb,color:#1e3a8a classDef sys fill:#fef3c7,stroke:#d97706,color:#78350f classDef ino fill:#dcfce7,stroke:#16a34a,color:#14532d class A1,B1 proc class S1 sys class INODE ino
这张图解释了本节几乎所有的边界问题。 两个进程打开同一个文件时,系统表里只有一个表项(打开计数为 2),而进程表里各有一项——共享的是”这个文件本身”,独立的是”我读到哪儿了”。
读文件与写文件
读文件时,进程给出文件描述符和内存中读入数据的位置;系统据此在进程打开文件表中找到表项,取出读写指针与系统表索引,再经由索引节点找到数据块的物理地址,把数据读入内存。
写文件的流程对称,进程给出文件描述符和内存中待写数据的位置。
两者都不需要给出文件名——文件名在 open 时已经用过一次,之后一律用编号。
边界
打开文件不等于把文件读入内存
疑问点:
open系统调用的执行过程
open系统调用具体做了哪些事情。
open 读入的是文件的”属性”,不是文件的”内容”。
完整过程是:解析路径名 → 逐级检索目录,找到该文件的 FCB 或索引节点 → 检查本次打开请求的权限是否被允许 → 把索引节点复制到内存(若已在内存则直接复用内存 inode),在系统打开文件表中建立(或复用)表项 → 在进程打开文件表中建立表项 → 返回文件描述符 fd。
一个 4GB 的文件同样可以被瞬间打开,因为 open 从头到尾只碰了目录和索引节点。真正读数据是后续 read 的事。
文件描述符不是索引节点号
fd 只是当前进程打开文件表的下标,作用域仅限于本进程。 不同进程的 fd=3 通常指向完全不同的文件;同一个文件在不同进程里的 fd 也几乎必然不同。
索引节点号才是文件在文件系统中的全局标识。 两者相差两级:fd → 进程表项 → 系统表项 → inode。
读写指针必须放在进程打开文件表里
这是两级表设计中最能体现”为什么这么分”的一点。
如果把读写指针放进索引节点或系统打开文件表,两个同时读同一文件的进程就会互相干扰——A 读到第 200 字节,B 一读就从 200 继续,双方都得不到正确结果。
放在进程打开文件表里,每次 open 就有一份独立的偏移量,各读各的。同一个进程对同一文件调用两次 open,也会得到两个 fd、两个互不相干的读写指针。
反过来,打开计数必须放在系统打开文件表里:它的语义是”当前还有多少个打开引用指向这个文件”,本就是跨进程的全局信息,放在任何一个进程的表里都无从统计。
一句话概括分工:凡是”这一次打开”的状态放进程表,凡是”这个文件”的状态放系统表。
关闭文件关掉的是打开状态,不是文件
疑问点:关闭文件操作的主要工作
关闭文件操作的主要工作是什么。
close 做四件事:撤销进程打开文件表中的该表项使 fd 失效;把系统打开文件表中的打开计数减一;若计数减为 0 则回收该系统表项;若文件属性或缓存被修改过,必要时写回磁盘。
要点在于引用计数:若一个系统表项被多个 fd 引用,关闭其中一个 fd只会让计数减一,文件对其他持有者依然是打开的。
close 与”删除”毫无关系。 关闭只是断开本进程与文件的连接,文件仍然完好地留在磁盘上。
截断不是删除
截断(truncate)是把一个已有文件的长度截为 0,回收其全部数据块,但保留目录项和全部属性。
删除则连目录项一起清除。 两者的区别正对应了 4.1.3 开头说的”两件事”:截断只做第二件(回收空间),删除两件都做。
需要保留文件名、权限、创建时间等属性而只清空内容时,用的就是截断。
对照速查
| 操作 | 做了什么 |
|---|---|
| 创建 | ① 外存分配空间 ② 目录中建目录项 |
| 删除 | ① 目录项置空 ② 回收存储空间 |
| 截断 | 只做②:长度截为 0,保留目录项与属性 |
| 打开 | 检索目录 → 查权限 → 属性调入内存 → 返回 fd |
| 关闭 | 撤销进程表项 → 打开计数减 1 → 计数为 0 才回收 |
| 读 / 写 | 给 fd 和内存位置,不给文件名 |
| 系统打开文件表 | 进程打开文件表 | |
|---|---|---|
| 数量 | 全系统一张 | 每进程一张 |
| 表项对应 | 一个被打开的文件 | 一次 open 调用 |
| 关键字段 | 文件号、外存地址、打开计数 | fd、读写指针、系统表索引 |
| 判断依据 | 属于”这个文件”的状态 | 属于”这一次打开”的状态 |
| 易混三者 | 是什么 | 作用域 |
|---|---|---|
| fd | 进程打开文件表的下标 | 仅本进程 |
| 系统表索引 | 系统打开文件表的下标 | 全系统 |
| inode 号 | 文件的全局标识 | 整个文件系统 |
考点
- 创建与删除各是两件事(目录项 + 存储空间),且互为逆操作
- 打开只读属性,不读内容;
open的完整五步 - 读写指针在进程打开文件表,打开计数在系统打开文件表——考查”为什么”
- fd 只是进程表的下标,不是 inode 号
close使计数减一,减到 0 才回收表项;关闭 ≠ 删除- 截断只回收数据块,保留目录项
- 读写文件时给的是 fd 而非文件名
链接
- 🏠 返回总览:操作系统第 4 章:文件管理总览
- ⬅️ 上一节:4.1.2 文件控制块和索引节点
- ➡️ 下一节:4.1.4 文件的逻辑结构
- 🔗 权限在
open时怎样检查,见 4.1.6 文件保护 - 🔗 路径解析的完整过程,见 4.2.4 目录实现
- 📖 名词库:第 4 章名词库