Appearance
文件的操作
2026 大纲 四(一)3 文件的操作,补充说明点名六个动作:建立、删除、打开、关闭、读、写。
为什么读文件之前非得先"打开"
前两节把静态的东西讲完了:文件是什么、元数据装在哪。这一节讲怎么用。
大纲点名的六个动作里,create、delete、read、write 都很直白, 但中间那两个——open 和 close——常被当成一句"打开文件"和"用完记得关"带过。 它们其实是这一节的重点,因为它们的存在本身就需要解释:为什么不能直接读?
想一想不打开会怎样。每次 read("/usr/local/bin/foo", ...) 都要从根目录开始, 一级一级把目录文件读进内存、逐条比对文件名,找到 inode 号再去读 inode—— 路径深度为
open 干的事就是把这一次昂贵的目录检索结果,缓存成一个便宜的整数下标。 之后每次 read/write 直接拿这个整数去查表,一次磁盘 I/O 都不用。 close 则是这笔缓存的归还。
而"缓存在哪"这个问题又立刻分岔:两个进程打开同一个文件, 有些东西必须各看各的(比如各自读到哪了),有些东西必须全系统只有一份 (比如这个文件被几个进程开着)。一张表满足不了这两个相反的要求, 所以有了两级打开文件表——这一节最值得理解的结构。
剩下的几个动作也各藏一处反直觉:建立时通常不分配任何数据块、 删除必须先回收空间再删目录项、write 返回不代表数据已经落盘、 seek 一次磁盘都不碰。每一处都能推出理由,不必硬记。
一、文件操作概览
二、创建与删除
建立 create(filename, mode):① 在目录中检查文件名是否已存在 ② 为新文件分配一个 FCB(或 inode),填入类型、权限、所有者、时间戳,块指针全部置空 ③ 在目录中添加目录项 (filename, inode号) ④ 分配必要的外存空间——通常不在这一步做,而是延迟到首次写入:建立时长度未知、大量文件建了就没写过(锁文件、临时文件)、块指针本来就允许为空,而延迟之后系统已知道要写多少字节,能一次要一批连续块。
所以顺序是:元数据先建(文件立刻"存在"了),数据块后分(第一次 write 时才向空闲空间管理要块)。
删除 delete(filename):① 查目录项取出 inode 号 ② 回收该文件占用的外存空间 ③ 删除该目录项 ④ 链接计数减 1,为 0 时回收 inode。第 ②③ 步的顺序不能颠倒:反过来在两步之间掉电,目录里已经没有这个文件、再也找不到它的 inode,而它的数据块仍标记为已占用,就成了只能靠全盘扫描捡回的孤儿块;正序的中间状态虽然也是错的,但目录结构完整、可被遍历、可被检查程序发现。真实文件系统进一步用日志(journal)把这一串修改打包成原子操作,从根上消掉中间状态。
三个"删"要分清
| 动作 | 目录项 | inode | 数据块 |
|---|---|---|---|
delete / unlink | 删除 | 计数减 1,为 0 才回收 | 计数为 0 时回收 |
truncate | 保留 | 保留(大小改为 0) | 全部回收 |
| 删除一个硬链接 | 删除这一个 | 计数减 1,通常不为 0 | 不回收 |
判据是"名字没了"还是"内容没了":delete 去掉的是名字,truncate 去掉的是内容。详见硬链接和软链接。
三、打开与关闭
打开 fd = open(filename, mode):
- 在目录中搜索文件名,找到对应的 FCB/inode
- 检查访问权限(mode 是否与文件的权限位相容)
- 在系统打开文件表中查找该文件是否已被打开:已打开则打开计数 +1;未打开则把 FCB/inode 信息复制进去
- 在进程打开文件表中分配一个空闲表项,指向系统打开文件表中对应条目
- 返回进程打开文件表的索引,即文件描述符 fd
进程 A 的打开文件表 系统打开文件表
┌────┬──────────┐ ┌────┬──────────┬───────┐
│ fd │ 系统表索引│ │ 索引│ FCB 信息 │打开计数│
├────┼──────────┤ ├────┼──────────┼───────┤
│ 0 │ → │──────►│ 3 │ file.txt │ 2 │
│ 1 │ → │──┐ │ 5 │ data.bin │ 1 │
└────┴──────────┘ │ └────┴──────────┴───────┘
│ ▲
进程 B 的打开文件表 │ │
┌────┬──────────┐ │ │
│ 0 │ → │──┘ │
│ 1 │ → │────────────┘
└────┴──────────┘| 表 | 作用 | 包含字段 |
|---|---|---|
| 进程打开文件表 | 每个进程私有 | fd、系统表索引、读写指针、访问模式 |
| 系统打开文件表 | 全系统共享 | FCB/inode 信息、打开计数、文件锁 |
关闭 close(fd):清除进程打开文件表中 fd 对应的表项 → 系统表对应项的打开计数 −1 → 计数为 0 时回收该系统表项,必要时把修改过的 FCB 写回磁盘。
"忘了 close"在单次运行里通常不出事(进程退出时内核会兜底),但在长期运行的服务里会耗尽进程打开文件表——fd 数量有上限。
四、读与写
读写是六个动作里唯一真正搬运数据的两个,也是与缓冲区管理、空闲空间管理、文件的物理结构三处都要打交道的两个。
读 read(fd, buffer, size):由 fd 找到表项与内存 inode → 由读指针算出这是第几个逻辑块、块内偏移多少 → 按物理结构把逻辑块号翻译成物理盘块号 → 先查内核缓冲区(磁盘高速缓存),命中就直接拷贝、未命中才真正读盘并把该块留在缓冲区 → 拷到用户 buffer → 读指针前移实际读到的字节数。
短读的三种成因:
| 情形 | 为什么会短读 | 返回值 |
|---|---|---|
| 读到文件末尾 | 从读指针到文件尾不足 size 字节 | 剩余字节数;再读一次返回 0(0 才表示 EOF) |
| 管道 / 终端等字符设备 | 对方还没写那么多,内核不会替你等满 | 当前可得的字节数 |
| 被信号打断 | 已经搬了一部分数据 | 已搬的字节数 |
写 write(fd, buffer, size):定位到内存 inode 与写指针 → 算出逻辑块号,若该逻辑块还没有对应的物理块就向空闲空间管理申请新块并把块号填进 inode 的地址项 → 把用户数据拷进内核缓冲区 → 延迟写 → 更新写指针,若写过了原文件末尾还要更新 inode 中的文件大小与占用块数并标记为脏。
写与空闲空间管理的协作,是本章几篇的交汇点:
- 写指针落在文件已有范围内 → 逻辑块已有物理块 → 不需要分配,直接覆盖写。
- 写指针超出文件当前长度 → 向空闲空间管理要块:位示图法找一个为 0 的位并置 1,成组链接法从超级块的空闲盘块号栈弹出一个块号。
- 拿到物理块号后填哪一项由物理结构决定——落在直接地址项内就直接填,超出了要先分配一个索引块再把数据块号填进去。所以"追加一个块"有时会连带分配 2 个甚至 3 个块。
- 空闲块数变了 → 超级块的空闲计数要改 → 超级块也被标记为脏,同样走延迟写。
五、定位与截断
文件指针(读写位置)存放在进程打开文件表中,每个进程独立维护;seek(fd, offset, whence) 只修改这个整数,不产生任何磁盘 I/O。
读写指针到底记在哪一级表:两种教材口径的对照(题面出现"系统打开文件表含读写位置"、或题目考到 fork 后父子共享位置时展开)
上面的说法(读写指针在进程打开文件表)是通行的教学口径,也是本站其余各篇统一采用的口径。
但孙钟秀、费翔林《操作系统教程》按 Linux 的实际结构描述:读写位置记在"系统打开文件表"里,进程侧那张表只保存指向它的指针;两个各自独立打开同一文件的进程会各有一个系统打开文件表项,因此位置照样互不干扰;而 fork 出的子进程与父进程共用同一个系统打开文件表项,于是共享同一个读写位置。
两种说法并不矛盾,分歧只在"把哪一级叫做进程表":
- 通行口径把"每次打开各自一份、含读写位置"的那张表叫进程打开文件表;
- Linux 口径把它叫系统打开文件表(对应 VFS 的
file对象),另设一张更薄的表做 fd → 表项的映射。
真正不变的结论只有两条(无论按哪种叫法都成立):
- 读写位置属于"一次打开",不属于"文件本体"——绝不会存进 FCB/inode。
- 两个独立打开同一文件的进程,读写位置互不干扰;而
fork出的父子进程共享同一个表项,因而共享读写位置。
做题时按题面给出的表名走;本站正文一律用通行口径,虚拟文件系统一篇讲 VFS 的 file 对象时用的是 Linux 的对象名。
截断 truncate(filename, len) 把文件长度改成指定值(通常是 0)但保留文件本身:找到 inode → 顺着地址项遍历全部数据块(含各级索引块)全部还给空闲空间管理 → 地址项清空、大小置 0、修改时间更新 → 目录项不动。
它存在的理由很直接:很多场景要的是"清空重来"而不是"删了再建"。日志文件想丢弃旧内容但要保持权限和已打开的 fd 有效;下载失败要重新开始但不想让别的程序看到文件消失一瞬间。若用 delete + create 替代,inode 号会变、已打开的 fd 会失效、权限要重设——这三样正是截断要避免的。
truncate | delete | |
|---|---|---|
| 目录项 | 保留 | 删除 |
| inode 与 inode 号 | 保留(大小归 0) | 计数减 1,为 0 则回收 |
| 数据块 | 全部回收 | 计数为 0 时回收 |
| 已打开的 fd | 仍然有效 | 仍指向旧 inode,直到关闭才真正回收 |
| 权限、所有者 | 不变 | 随文件消失 |
考点速记
- 六个动作按碰不碰磁盘分层:
seek不碰,open/close碰元数据,read/write碰数据块,create/delete两样都碰。 - 建立:查重名 → 分配并填写 FCB/inode(块指针全部置空)→ 加目录项 → 分配外存空间。⚠️外存空间通常延迟到首次写入才分配——建立时长度未知、大量文件建了就没写过、块指针本来就允许为空、延迟后能一次要一批连续块。
- 删除必须先回收空间、后删目录项。反过来在两步之间掉电,目录里已经没有这个文件、再也找不到它的 inode,而数据块仍标记为已占用 ⇒ 孤儿块(存储泄漏)。一般原则:任何中间状态崩溃后,剩下的结构仍要可被遍历、可被检查程序发现。
- 打开的实质是把一次昂贵的目录检索结果缓存成一个便宜的整数下标。没有
open,每次读写都要从根重走路径解析(深度要 次磁盘 I/O)。⚠️按名查找发生在 open里,read/write/close都只认 fd。 - 首次打开时 OS 做的是把 FCB(inode)读进内存——不是把文件内容读进内存,也不修改权限,返回给用户的是 fd 这个整数而不是缓冲区指针。
- 两级打开文件表的分界线:允许两个进程看到不同值的字段 → 进程(用户)打开文件表(fd、系统表索引、读写指针、访问模式);必须全系统只有一份的字段 → 系统打开文件表(FCB/inode 信息、打开计数、文件锁)。
- 合成一级必坏:只留全系统一张表 ⇒ 两个进程共用一个读写指针,"各读各的"直接失效;只留每进程一张表 ⇒ 同一文件有多份 inode 副本互相看不见,且没人知道还有几个人开着它,无法判断何时可安全回写释放,也没法实现文件锁。
- fd 是进程打开文件表的下标,只在本进程内有意义;不是 inode 号,也不是系统表下标。同一文件在两个进程里 fd 可以不同。
- 关闭:打开计数减 1,减到 0 才回收系统表项、释放内存 inode 所占空间并把改过的 FCB 写回磁盘。⚠️ 关闭不删目录项、不释放外存 inode、不减磁盘上的链接计数——那三样是
delete干的。 read返回值小于请求长度是正常的(读到末尾、管道暂时只有这么多、被信号打断),返回 0 才表示 EOF;读指针按实际读到的字节数前移。write返回 ≠ 数据已落盘——延迟写只保证进了内核缓冲区。对称的读优化叫提前读。seek只改内存里的一个整数:不产生任何磁盘 I/O、不预读、不分配盘块。truncate与delete的唯一分界是目录项动不动:两者都回收数据块,truncate保留目录项与文件名权限、已打开的 fd 仍然有效。
这一节在真题里被考过的形式:
四道题分成两组:两道问 open/close 各做了什么, 两道问多进程共享时那两张表怎么分工。判据都在速记第四到九条里。
- 问首次打开文件时 OS 需要做的是什么(2014-29)。答将文件控制块读到内存中。⚠️ 另三项各错一处:"将文件内容读到内存"——
open不读任何数据,读数据是read的事;"修改 FCB 中的读写权限"——打开只是检查权限够不够,不改权限;"返回数据缓冲区首指针"——返回的是 fd 这个整数,用户拿不到内核缓冲区的地址。 - 问哪个系统调用包含按名查找功能(2024-29)。答
open()。⚠️ 这道题正是速记第四条的直接考法——按名查找只在open做一次,之后read/write/close全部认 fd,再不碰文件名。 - 问多个进程共享同一文件时哪句话正确(2020-23)。答在系统打开文件表中仅有一个表项包含 F 的属性。⚠️ 另三项分别踩了三个坑:"只能用读方式打开"——共享不等于只读;"各进程用户打开文件表中的表项内容相同"——恰恰相反,读写指针是各看各的,这正是要分两级的原因;"进程关闭 F 时系统删除系统表中的表项"——要打开计数减到 0 才删。
- 问文件仅被进程 P 打开、P 关闭它时文件系统要做什么(2023-31)。答释放 F 的索引节点所占的内存空间。⚠️ 另三项全是
delete才做的事:删目录项、释放外存 inode、把磁盘链接计数减 1。关闭动的是内存里的那份,删除才动磁盘上的那份——这一对分界是本节最稳定的考点。
复习优先级:必须拿满。 全部四道题只考两件事:open/close 各碰什么、 两级表怎么分工。把速记第六条那条分界线(允不允许两个进程看到不同的值)记住, 再把"关闭动内存、删除动磁盘"这一对钉死,四道题都是送分。 第二、三条(延迟分配、先回收后删目录项)目前没单独考过,但推理链很短,读懂即可。
易错:认为
open会把文件内容读进内存。它只把 FCB/inode 读进内存。
易错:认为
open返回的是缓冲区指针。返回的是 fd——进程打开文件表的下标。
易错:认为
read/write也要按文件名查找。按名查找只在open做一次。
易错:认为各进程用户打开文件表中同一文件的表项内容相同。读写指针各看各的,这正是分两级的原因。
易错:认为进程一关闭文件,系统打开文件表的表项就被删除。要打开计数减到 0。
易错:把关闭要做的事和删除要做的事搞混。关闭释放的是内存 inode;删目录项、释放外存 inode、减链接计数都是
delete。
易错:认为
read返回值小于请求长度就是出错。返回 0 才是 EOF,读满 N 字节必须循环。
易错:认为
write返回就说明数据落盘了。延迟写只保证进了内核缓冲区。
易错:认为
seek会预读或分配盘块。它只改内存里的一个整数。
教材出处
- 汤小丹《计算机操作系统》:提前读与延迟写的定义与作用,见印刷版 p265(8.3 提高磁盘 I/O 速度的途径)——延迟写"并不立即将该缓冲区中的数据写入磁盘,而是将它挂在空闲缓冲区队列的末尾……当再有进程申请到该缓冲区时,才将该缓冲区中的数据写入磁盘"。
- 孙钟秀、费翔林《操作系统教程》(第 6 版):打开操作"将该文件有关的目录信息复制到指定的主存区域,并将有关信息填入活动文件表",关闭操作"切断用户进程和该文件的联系,若该目录已被修改过,则应更新辅存中对应的文件目录",见印刷版 p176–p177;同书 p178 说明 Linux 中
fork出的子进程与父进程通过同一个系统打开文件表共享相同的读写位置。
相关知识
文件元数据与索引节点|文件的保护|目录|外存空闲空间管理|缓冲区管理|虚拟文件系统