文件的操作

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 而非文件名

链接