本地资料文件
SCHEDULE LOCAL高优先级17 个小节覆盖真题 20092025
关联考点文件物理结构10inode7进程文件管理3文件概念2文件分配表1做相关真题 · 22 道 →
做相关真题 · 22 道选中文字可高亮或加下划线
选中文字高亮 · 下划线

文件

真题练习

这一节是操作系统文件管理中最重要的一节,每个知识点的知识都要深入掌握,尤其是文件的物理结构,在大题中也经常考察。

文件元信息

UNIX 和类 UNIX 操作系统中,inode(索引节点)用于存储 文件的元数据。它包含文件的大部分元数据和数据块地址,但不包括 文件名;文件实际内容存放在数据块中。

inode 保存文件类型、权限、所有者、大小、时间戳和数据块指针

inode 中包含如下信息:

  • 文件类型:文件是普通文件、目录还是链接文件等。
  • 权限:文件的访问控制信息,如用户、组和其他用户的读、写、执行权限。
  • 所有者:文件的所有者和组 ID。
  • 大小:文件的大小(字节数)。
  • 时间戳:典型 UNIX inode 至少记录访问时间、内容修改时间和 inode 状态改变时间;是否记录创建时间取决于具体文件系统。
  • 链接计数:指向该 inode 的硬链接数量。计数降为 0 且没有任何进程继续打开该文件时,数据块和 inode 才能被回收。
  • 数据块指针:文件内容所在的数据块(block)的位置信息。这包括直接指针、间接指针、二级间接指针和三级间接指针,用于指向存储文件内容的磁盘块。

进程文件管理

在进程中可能会打开若干文件,操作系统需要记录“进程使用的描述符、一次打开操作的状态、文件对象元数据”这三个层次。内存中的典型结构是 文件描述符表系统打开文件表活动 inode 表(或 vnode/inode 缓存);文件系统还会在外存中持久保存 inode 记录。内存表与磁盘 inode 区不能混为同一层。

  • 文件描述符表(File Descriptor Table):每个进程都有自己的 文件描述符表,这个表对应于该进程打开的文件描述符。文件描述符是进程范围内的一个小的非负整数。
  • 文件打开表(Open File Table):操作系统维护一个 全局的 文件打开表,该表记录了所有打开文件的状态信息。每次 open 调用成功时,都会创建一个新的 文件打开表条目
  • 活动 inode 表/缓存(Active Inode Table/Cache):内核把当前正在使用的文件元数据装入内存对象。多个打开文件表项可以引用同一个活动 inode(或等价 vnode)对象;该对象再对应文件系统中持久化的 inode 记录与数据块。

inode 表

“inode 表”在不同上下文中可能指两个层次,解题时要根据题目区分:

  • 磁盘 inode 表/区域:在采用经典 inode 组织的文件系统中,外存上划出一个或多个区域持久保存 inode 记录。每个 inode 占用一个表项,inode 编号(inode number)在该文件系统内标识对应文件对象。具体区域是否固定、如何分组属于文件系统实现细节。
  • 内存活动 inode 表/缓存:只保存当前已装入并正在引用的 inode 对象,用于加速访问和维护引用状态;它不是保存所有 inode 的“唯一容器”。

对于经典 UNIX 类 inode 文件系统:

  • 每个文件对象在所属文件系统内有一个唯一的 inode 编号;多个硬链接目录项可以共享该编号。
  • 文件系统通过 inode 编号定位磁盘 inode 记录,并在需要时把它装入内存活动 inode 表/缓存。

下表给出了一个简化的 磁盘 inode 记录表示例:

inode 编号 文件类型 权限 所有者 文件大小/字节 数据块指针(简略) 状态时间 修改时间
1 目录 drwxr-xr-x root 4096 [100, 101, 102] 2024-01-01 10:00 2024-01-02 10:00
2 文件 -rw-r--r-- user 1024 [200, 201] 2024-01-01 11:00 2024-01-01 12:00
3 符号链接 lrwxrwxrwx user 14 [路径字符串: "/etc/abc"] 2024-01-01 13:00 2024-01-01 13:00
4 文件 -rwxr-xr-x user 8192 [300, 301, 302, 303] 2024-01-02 08:00 2024-01-02 08:00

表中的每一行表示一个持久化的 inode 记录;文件被打开后,相关信息还会出现在内存活动 inode 对象中。

系统打开文件表

系统打开文件表(System-wide Open File Table)是整个操作系统内核维护的一个全局结构,它记录了所有进程当前打开的文件的状态信息。

每当一个进程调用 open() 打开一个文件时,系统会:

  1. 为这次独立的 open() 创建一项“系统打开文件表”记录;同一文件被再次 open() 通常得到新的打开文件对象和独立偏移量。
  2. 创建/更新该进程的“文件描述符表”项,让它指向这条系统表项

举个例子:

int fd = open("file.txt", O_RDWR);  // 进程 A 打开
write(fd, "Hello", 5);              // offset 从 0 到 5

这次 open 会创建一个 系统文件表 项:

  • 偏移量 0 → 写入 5 字节后变成 5
  • 文件模式:O_RDWR
  • inode 指针:指向 file.txtinode

📊 下表给出了一个 系统文件打开表 实例:

表项编号 inode 编号 打开模式 当前偏移量 状态标志 引用计数 示例路径
0 1024 读写(rw) 120 2 /home/user/a.txt
1 1050 只读(r) 0 非阻塞 1 /etc/hosts
2 1024 读写(rw) 0 1 /home/user/a.txt(另一次独立 open,偏移独立)
3 2001 只写(w) 45 O_APPEND 1 /var/log/sys.log

表项 0 的引用计数为 2,可表示两个文件描述符通过 dup()fork() 共享同一个打开文件对象,因此共享当前偏移量;表项 2 虽指向同一 inode,却是独立 open() 产生的对象,偏移量互不影响。

文件描述符表

文件描述符表(File Descriptor Table)是进程级别的表,它将整数类型的“文件描述符”(如 0, 1, 2, 3, …)映射到 系统打开文件表 的条目上。

每个进程在内核中有自己的 文件描述符表。当你调用 open() 打开一个文件时,操作系统:

  • 系统打开文件表 中新建一项(记录 inode 指针、偏移和状态标志);dup()/fork() 才会让多个描述符共享既有表项
  • 在该进程的 文件描述符表 中添加一个条目(索引号)

📊 下表给出了一个进程的 文件描述符表 实例:

文件描述符(fd) 指向系统打开文件表项编号
0 5
1 6
2 7
3 9
4 10

总体视角

在进程 1 中执行 fdA1 = open("fileA.txt", O_RDONLY)fdAdup = dup(fdA1) 系统调用,在进程 2 中执行 fdA2 = open("fileA.txt", O_RDONLY),系统中的三种用于进程文件管理的内存表如下图所示:

两个进程的文件描述符表经打开文件表共同或独立指向 inode

三张内存表的逻辑关系可以理解为:进程描述符表(进程级) → 系统文件打开表(系统级) → 活动 inode 表/缓存(系统级);活动 inode 对象再对应外存中的 inode 记录与文件数据块。

每个进程有自己的 文件描述符表,记录 文件描述符(如 3、4 等)指向 系统打开文件表 中的某一项;系统打开文件表 是全局共享结构,记录本次打开的读写偏移量、打开模式,并指向对应的 活动 inode 对象;活动 inode 对象缓存文件元数据和数据块映射,并与外存中的持久 inode 记录对应。通过这些层次,系统实现了多个进程共享文件对象、按打开操作管理偏移量并统一访问底层数据。

系统调用

Unix 操作系统中,“一切皆文件”是一项核心设计哲学。无论是普通文件、设备文件,还是网络连接,操作系统都通过统一的机制进行管理——这套机制的核心,就是一组用于文件管理的 系统调用

其中,openclosereadwritelseek 是最基本、最常用的五个调用,很多考试题目都会以间接形式考查它们的工作原理与使用方式。

打开和关闭

所谓“打开一个文件”,并不仅仅是打开字面上的内容,而是操作系统在后台完成了两个关键步骤:

  1. 检查文件是否存在、权限是否允许访问
  2. 在内核中创建一个 文件描述符,用于追踪该文件的使用状态

open 成功后,内核创建打开文件对象,并在进程的 文件描述符表 中选择一个空闲项指向它,返回该项索引作为文件描述符。若 inode 尚未在内存缓存中,内核还需从外存读取其元数据。

相对应的,close 从进程文件描述符表中删除该项,并使所指打开文件对象的引用计数减 1。计数变为 0 时可释放打开文件对象;inode 是否立刻从内存移除取决于缓存策略。若程序持续遗漏 close,可能耗尽进程可用的文件描述符。

读写

文件一旦成功打开,程序即可通过 readwrite 系统调用与其进行交互:

  • read(fd, buf, count):将 文件内容从内核空间读取到用户缓冲区
  • write(fd, buf, count):将用户缓冲区的内容写入内核缓存区

每次读写操作结束后,系统会自动更新当前的读写偏移量(文件偏移指针),下一次操作会从上次结束的地方继续。

为提高读写效率,现代操作系统在 readwrite 调用背后引入了多种 缓存与优化策略,常见包括:

1. 延迟写

Unix 类系统中,普通 write 通常不会立即把数据持久化到存储介质,而是先更新内核的 页缓存(page cache)并把相应页标记为脏。内核可在缓存压力增大、达到写回阈值或周期性写回时把脏页送往设备(思路与 cache 中的 回写法 类似,需要记录脏状态)。close() 的职责是释放文件描述符及其引用,不保证脏数据已经持久化到磁盘

这种设计 显著减少了磁盘 I/O 操作,提高了写入效率,尤其是在对同一数据区域频繁写入的场景下,还能合并多次写操作,从而进一步降低成本。

然而,这种 “延迟写” 机制也带来了风险:如果在数据尚未刷盘前系统发生异常(如断电或崩溃),这些暂存在内存中的数据可能会丢失。为此,如果应用场景对数据持久性要求较高,应使用 fsync(fd) 等持久化接口,并按文件系统语义处理相关目录项;不能把 close() 当成持久化屏障。

  1. 预读取

与写入类似,read 操作在内核层面也进行了性能优化。当系统识别到程序正在顺序读取文件时,会自动启用“预读取”机制,提前将后续的多个页加载到 页缓存 中,即使用户当前并未发出读取请求。

这样一来,后续的读取请求很可能直接命中缓存,从而避免了等待磁盘 I/O 的开销。这一机制充分利用了磁盘的顺序读取特性,在处理大文件或进行流式读取时,能够显著提升整体读性能

定位

在某些情况下,程序需要跳过部分内容、从文件中任意位置读取或写入数据,这就涉及到了 lseek 系统调用。

lseek 允许显式地移动文件的读写位置,从而实现“随机访问”。常见用途包括:

  • 跳过文件前面的若干字节
  • 返回文件开头重新读取
  • 移动到文件末尾以进行追加写入

lseek 为文件操作提供了更高的灵活性,使得程序不仅能顺序处理数据,也能高效地实现定位和修改。

文件的逻辑结构

文件的 逻辑结构 指的是文件内部数据的组织方式,是用户和程序员所看到和使用的数据排列形式。根据记录的排列和访问方式,逻辑结构通常分为以下两种:

顺序文件 是将记录按一定顺序依次存储的一种 文件逻辑结构,每条记录紧跟在前一条之后。在访问时,通常采用顺序读取的方式,即从文件的开头开始,依次读取每条记录,直到找到目标记录或读完整个文件。

随机文件(也称为直接文件)则不要求记录按固定顺序排列,记录可以以任意顺序存放在文件中。其显著特点是支持 随机访问,程序可以依据记录号、关键字等定位信息直接访问某条记录,而无需逐条读取。

文件的物理结构

文件的 物理结构(或称 存储结构)是指文件在物理存储介质(如磁盘、SSD)上的实际存放方式。它关注的是操作系统如何管理文件的块(block)或簇(cluster)等单位,并将逻辑上的文件映射到磁盘上的物理地址空间。

与逻辑结构面向用户和程序不同,物理结构更多反映了操作系统和文件系统层面的实现细节,决定了文件的实际读写效率、空间利用率以及文件访问策略的复杂性。

文件的物理结构和逻辑结果的 对应关系 如下图所示:

文件逻辑结构经连续、链接或索引方式映射到磁盘块

连续分配

连续分配 方案中,每个文件占用磁盘上一个 连续的块集。若文件需要 nn 个块,起始块号为 bb,则占用块号为 b,b+1,,b+n1b,b+1,\ldots,b+n-1。给定起始块号和块数即可定位任意逻辑块。

例如,文件从第 19 块开始并占用 6 块时,其物理块号就是 19~24。

连续分配方案对应的目录包含如下信息:

  • 文件的起始地址
  • 分配块数量

下图中的文件 “file3” 从区块 19 开始,长度为 6 个区块。因此,它占用 19、20、21、22、23、24 个块。

文件 file3 从第 19 块开始连续占用 6 个磁盘块
  • 优点:因为文件块的连续分配,查找的次数很少,访问速度非常快
  • 缺点:会产生外部碎片,文件最后一块还可能有内部碎片;文件增长困难,因为紧邻的后续磁盘块可能已经被占用。

链式分配

链式分配 方案中,每个文件都是一个 不需要连续的磁盘块链表。磁盘块可以分散在磁盘上的任何地方。目录项包含指向起始和结束文件块的指针。每个块包含一个指针,指向文件所占用的下一个块。

目录项包含指向起始和结束文件块的指针。每个磁盘块的最后一部分字节用来存储下一个块的块号(相当于链接),前面的部分用来存储文件数据,如下图所示:

链式分配中每个磁盘块末尾保存下一块编号

下图中的文件 “file1” 显示了块是如何随机分布的。最后一个块 (25) 包含 -1,表示空指针,不指向任何其他块。

文件 file1 的磁盘块分散并由指针串成链
  • 优点:没有外部碎片,文件可按需增长,空闲磁盘块都可被利用。
  • 缺点:随机访问必须沿链查找,指针占用块内空间且指针损坏会使后续数据丢失;最后一块仍可能有内部碎片。

文件分配表

基于链表的链接为 隐式链接,每个磁盘块的末尾包含文件的下一个盘块号。

文件分配表 FAT 也是基于链接的方式,不过文件的 链接方式是存储在一个表格当中,表格包含两列,一列是 盘块号,另一列是在该文件盘块之后的 下一个盘块号,这种方式也叫做 显式链接

FAT 把各盘块的后继编号集中保存在文件分配表

索引分配

索引分配 中,一个文件所占用的所有盘块号被存储在另一个盘块中,这个盘块叫做 索引块(Index Block)。

假设文件大小为 4 MiB,盘块大小为 4 KiB,盘块号占 4 B,则

数据块数=4MiB4KiB=1024,每个索引块可存盘块号数=4KiB4B=1024.\text{数据块数}=\frac{4\,\mathrm{MiB}}{4\,\mathrm{KiB}}=1024, \qquad \text{每个索引块可存盘块号数}=\frac{4\,\mathrm{KiB}}{4\,\mathrm{B}}=1024.

因此一个一级索引块恰好能记录该文件的全部 1024 个数据块号:

4 KiB 索引块保存 1024 个 4 B 盘块号

4 KiB 的盘块刚好可以存储 1 Ki 个盘块号作为索引。顺序读取整个文件时,可以按索引项依次取得各数据块号;随机访问某个逻辑块时,只需读取包含该块号的索引块(多级索引则沿相应索引路径查找),再读取目标数据块,不必先找出或读入文件的全部数据块。如下图所示:

一级索引块把文件逻辑块号映射到离散数据块

上图给出的索引是 一级索引,索引块的层级只有一层,索引块中直接存储数据块的盘块号。

在实践中还有 多级索引,其思路与 多级页表 一致。一级索引块中存储的是二级索引块的盘块号,最后一级索引块中才存储有数据块的盘块号。

一级索引块经二级索引块定位数据块的多级索引

混合索引

N 级索引 的一个显著缺点是:即使是只有两三个盘块的极小文件,也必须为其分配一个完整的 索引块 来保存这些数据块的盘块号。由于索引块的容量远大于实际需要,导致大量空间闲置,磁盘块的利用率极低。

为了解决这一问题,可以采用 混合索引(combined allocation) 的方式。该方式根据文件大小采用不同的存储策略:

  • 小文件(仅占少数盘块)直接使用 直接块(direct blocks),把数据块的盘块号写入 inode 中的几个固定条目;
  • 大文件 则借助 单级双级三级 索引块进行间接寻址,以支持更大的文件规模。

UNIX 系统正是采用了这种混合索引结构。其 inode(如下图)由以下几部分组成:

UNIX inode 中直接块与一到三级间接索引的混合结构
  • 直接块(Direct blocks):若文件只有几块数据,则直接在这些条目中存放相应的盘块号,无需额外的索引块。
  • 一级间接索引块(Single indirect):指向一个仅存放数据块号的块数组;
  • 二级间接索引块(Double indirect):先指向若干一级索引块,再由这些一级索引块指向实际的数据块号;
  • 三级间接索引块(Triple indirect):层层递进,最终定位到数据块号。

通过这种 直接块 + 多级间接块 的混合方案,系统能够在支持大文件的同时,避免小文件仅为保存少量块号就占用完整索引块,从而减少索引开销并提高磁盘块利用率。它并不天然改变数据块是否连续,因此不能据此断言会减少文件数据的外部碎片。