文件基本操作与文件系统布局:从create到close的全流程
文件基本操作与文件系统布局:从 create 到 close 的全流程
📌 核心要点
- 六个文件基本操作(create / delete / open / close / read / write)覆盖了文件的完整生命周期,每个系统调用背后都涉及外存空间分配、目录项修改和内存数据结构更新
- open 系统调用创建的是"打开文件表"中的条目,返回文件描述符(fd)——这是一个整数,后续 read / write 都用它来定位文件,不需要再传路径
- 文件系统在磁盘上的物理布局从左到右依次是:MBR → 分区表 → 引导块 → superblock → 空闲空间管理区 → inode 区 → 数据区,这七个区域各有独立职责
- 文件系统在内存中维护两张表(系统打开文件表 + 进程打开文件表)和一个缓存(目录缓存),这是理解"open 到底做了什么"的核心
六个操作,一个完整生命周期
用户通过图形界面点击"新建文件"、双击打开、编辑、保存,这些动作背后都是操作系统提供的系统调用(System Call)。408 考纲要求掌握的六个文件基本操作是:create, delete, open, close, read, write。
这六个操作可以分成三类理解:
- 元数据操作:create(建目录项 + 分配空间)、delete(删目录项 + 回收空间)
- 文件打开/关闭:open(建立访问通道)、close(销毁访问通道)
- 数据传输:read(磁盘→内存)、write(内存→磁盘)
open 和 close 可以视为 read / write 的前置与后置操作——文件必须先打开才能读写,读写完应当关闭。
create 系统调用:两个核心动作
当用户在 D:/Demo 目录下创建"新建文本文档.txt"时,应用程序实际上发起了 create 系统调用。OS 需要完成两件事:
教材明确指出 create 系统调用的三个主要参数:所需外存空间大小、文件存放路径、文件名。
动作一:分配外存空间。OS 在文件系统的空闲空间中,为该文件找到足够的磁盘块。具体使用什么策略取决于该文件系统采用的空间管理方案——可能是位示图(Bit Map)找一个空闲位、可能是空闲链表(Free List)取一块、也可能是成组链接法(Group Linking)从当前空闲组取一块。
动作二:创建目录项。OS 根据路径 D:/Demo 找到该目录对应的目录文件(目录本身也是一个文件,由记录组成),在其中新增一条目录项,包含文件名和该文件在外存中的存放位置(物理块地址或 inode 编号)。
408 考题中有一个常见陷阱:问"create 系统调用除了创建目录项外还需要做什么?“——不少考生漏掉"分配外存空间”。实际上,如果不分配空间,文件就没有地方存放数据;但也有一些实现是延迟分配的(只在真正写入时才分配),不过考研以教材描述为准。
open 系统调用:打开文件表是如何工作的?
open 是六个操作中最复杂的。用户调用 open 时需要提供路径和打开模式(如只读 / 只写 / 读写),OS 返回一个整数——文件描述符(File Descriptor, fd)——后续所有 read / write 都通过这个整数来指定操作的文件。
open 系统调用背后,OS 要完成以下步骤:
- 路径解析:从路径字符串中找到目标文件的目录项和对应 inode
- 权限检查:根据 inode 中的权限位,判断当前用户是否有对应模式的访问权限
- 打开文件表操作(核心):在系统打开文件表(System-wide Open File Table)和进程打开文件表(Per-process Open File Table)中创建条目
两张表的区别:
- 系统打开文件表:整个系统只有一张,记录所有被打开的文件的信息(包括读写指针位置、打开计数等)
- 进程打开文件表:每个进程各有一张,记录该进程打开的文件,每一项指向系统打开文件表中的对应条目
进程表中的 fd(文件描述符)本质上就是该进程打开文件表的数组下标。这就是为什么 fd 是小的非负整数(0, 1, 2…)——0 通常对应标准输入,1 对应标准输出,2 对应标准错误。
[经验] 理解"两张表 + fd 是下标"这个模型后,很多问题豁然开朗:为什么 fork 后子进程能和父进程共享文件?因为子进程复制了父进程的进程打开文件表,两张表指向同一个系统打开文件表条目。这就是为什么父子进程的读写指针会互相影响。
read / write:数据在内存和外存之间的搬运
[共识] 以 read 调用为例(write 是对称操作,方向相反),需要提供三个参数:文件描述符 fd、内存缓冲区指针 buf、读取字节数 count。
read 的执行流程:
- 通过 fd 定位到进程打开文件表中的条目
- 定位到系统打开文件表中的对应条目,获取当前读写指针位置
- 通过 inode 中的物理块指针,将文件数据从磁盘读入内核缓冲区(块缓存 / 页缓存)
- 从内核缓冲区复制到用户提供的 buf 中
- 更新读写指针
注意步骤 3 中隐含的一个重要事实:数据是先从磁盘读到内核空间缓冲区,再从内核空间复制到用户空间的。这是操作系统"内核态/用户态隔离"的体现——用户进程不能直接访问磁盘(那需要内核态权限),只能通过系统调用间接进行。
教材也指出:read / write 之前必须先 open,读写完成之后应当 close。open 建立了访问通道(填充了打开文件表),read / write 通过这条通道传输数据,close 销毁通道(释放打开文件表条目、必要时将缓冲区数据刷回磁盘)。
文件系统在磁盘上的物理布局:从 MBR 到数据区
一张原始磁盘(出厂状态)经过物理格式化、分区、逻辑格式化三步处理后,最终形成七个功能区域。这是理解"磁盘上到底放了什么"的关键:
┌────────┬────────┬────────┬───────────┬──────────────┬──────────┬──────────┐ │ MBR │ 分区表 │ 引导块 │ superblock │ 空闲空间管理 │ inode 区 │ 数据区 │ └────────┴────────┴────────┴───────────┴──────────────┴──────────┴──────────┘结合教材,各区域职责如下:
MBR(Master Boot Record,主引导记录):位于磁盘的第一个扇区(0 号扇区),包含引导代码和分区表。计算机开机后 BIOS 读取 MBR,执行其中的引导代码,定位到活动分区。
分区表:紧跟在 MBR 之后(或包含在 MBR 内),记录每个分区的起始位置和大小(C 盘从哪到哪、D 盘从哪到哪)。
引导块(Boot Block):每个分区的第一个扇区,包含该分区操作系统的引导程序。对于 Linux,引导块可能指向 GRUB 引导加载程序。
superblock(超级块):记录整个文件系统的元信息——文件系统类型(ext4 / NTFS / FAT32)、总块数、空闲块数、inode 总数、块大小等。这是文件系统挂载(mount)时第一个被读入内存的结构。
[经验] superblock 的损坏会导致整个文件系统无法挂载。因此 ext 系列文件系统会保存多个 superblock 镜像(备份)分散在不同块组中,以便主 superblock 损坏时从备份恢复。
空闲空间管理区:存放位示图(Bit Map)或成组链接法的空闲块组信息,记录哪些磁盘块是空闲的。每当 create 系统调用需要分配新块时,就是从这里查询的。
inode 区:存储所有文件的 inode(索引结点),每个文件一个 inode。inode 的数量在文件系统创建时就确定了——这意味着一个文件系统能存放的文件数量是有上限的。
数据区:实际存放文件内容的磁盘块,占据了磁盘的绝大部分空间。
文件系统在内存中的结构:两张表 + 一个缓存
[引用] 部分文件系统数据在系统运行期间会被加载到内存中,以提高访问速度:
系统打开文件表:整个系统只有一张,记录所有被打开文件的读写指针、打开模式(只读/只写)、inode 指针和打开计数。
进程打开文件表:每个进程各一张,每一项包含指向系统打开文件表条目的指针。fd(文件描述符)就是这张表的数组下标。
当进程调用 close(fd) 时:
- OS 清空进程打开文件表 fd 位置的条目
- 将对应系统打开文件表条目的打开计数 - 1
- 如果打开计数归零,释放系统打开文件表的条目,同时将 inode 中可能修改过的信息写回磁盘
- 如果该 inode 的引用计数也归零且没有硬链接指向它,释放 inode 和数据块
目录缓存(Directory Cache / dcache):为了提高路径解析速度,OS 会将近期访问过的目录 inode 缓存在内存中。这样下次访问同一目录时,不需要再从磁盘读取。
[共识] 这三种内存结构协同工作,构成了文件系统在内存中的"运行时状态"。理解它们之间的关系,就能理解整个文件操作的完整数据流。
从创建到关闭:一个文件的一生
让我们追踪一个文件从诞生到消亡的完整路径,把前面的知识点串起来:
- create(“D:/Demo/test.txt”)→ OS 在 D 盘分区的空闲空间管理区中找到空闲块 → 分配一个 inode → 在 D:/Demo 的目录文件中添加目录项 → 返回成功
- fd = open(“D:/Demo/test.txt”, O_RDWR)→ OS 解析路径 → 在 D:/Demo 目录中找到 test.txt 的目录项,获取 inode 编号 → 读取 inode → 检查权限 → 在进程打开文件表和系统打开文件表中创建条目 → 返回 fd = 3
- read(fd, buf, 1024)→ 通过 fd 找到进程打开文件表条目 → 指向系统打开文件表条目 → 通过 inode 的物理块指针读取磁盘数据到内核缓冲区 → 复制到用户 buf → 更新读写指针
- write(fd, buf, 1024)→ 流程同上但方向相反,并且可能需要分配新的磁盘块(如果写入超出当前文件大小)
- close(fd)→ 清理打开文件表条目 → 将可能的元数据修改写回磁盘 → 进程打开文件表中 fd=3 的条目清空
FAQ
open 返回的文件描述符(fd)为什么总是最小的可用整数?
因为进程打开文件表本质是一个数组,OS 从下标 0 开始扫描,返回第一个空闲位置的下标。进程启动时 0/1/2 已被标准输入/输出/错误占用,所以用户打开的第一个文件通常得到 fd=3。
为什么 read / write 之前必须 open?
因为 read / write 只知道 fd,不知道文件路径。open 的作用是把路径解析为 fd(建立了 fd → 进程打开文件表 → 系统打开文件表 → inode 的映射链)。没有 open,读写操作无法定位目标文件。
superblock 损坏了怎么办?
ext 系列文件系统在每个块组(Block Group)中都保存了 superblock 的备份。可以使用fsck工具从备份恢复。这也是为什么 ext4 比 FAT32 更健壮的原因之一。
为什么 inode 数量在文件系统创建时就确定了?
因为 inode 区是固定大小的(由 mkfs 时根据磁盘容量按比例分配)。如果 inode 用完了,即使数据区还有空闲块,也无法创建新文件。这就是所谓的"inode 耗尽"——常见于存储大量小文件的场景(如邮件服务器)。
进程退出时没有显式 close 文件会怎样?
OS 会自动关闭该进程打开的所有文件(释放进程打开文件表 → 递减系统打开文件表条目的打开计数)。这是 OS 的可靠性保证——用户进程崩溃不会导致系统资源泄漏。但这不应成为不写 close 的借口(依赖 OS 兜底是坏习惯)。
总结
六个基本操作中,open 是最复杂的——它建立了"进程 → 系统 → inode"的三级映射,是理解文件 I/O 的核心。文件描述符(fd)不过是一个整数,但这个整数背后串联起了打开文件表、inode、磁盘块等一整套数据结构。
文件系统在磁盘上的物理布局(MBR → 引导块 → superblock → 空闲管理 → inode 区 → 数据区)和在内存中的运行时结构(两张表 + 一个缓存),构成了理解"文件到底存在哪里、OS 如何找到它"的完整图景。