文件

重点内容,需熟练掌握文件的物理结构,也需了解文件的元信息和逻辑结构,常在选择题中考察,也会在大题中考察。

文件元信息

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

inode 中包含如下信息:

  • 文件类型:文件是普通文件、目录还是链接文件等。
  • 权限:文件的访问控制信息,如用户、组和其他用户的读、写、执行权限。
  • 所有者:文件的所有者和组 ID。
  • 大小:文件的大小(字节数)。
  • 时间戳:文件的创建时间、最后修改时间和最后访问时间。
  • 链接计数:指向该文件的硬链接数量。当计数为 0 时,文件会被删除。
  • 数据块指针:文件内容所在的数据块(block)的位置信息。这包括直接指针、间接指针、二级间接指针和三级间接指针,用于指向存储文件内容的磁盘块。

进程文件管理

在进程中可能会打开若干文件,操作系统需要以某种方式对进程打开的文件信息进行记录。这就需要用到三个数据结构:文件描述符表文件表inode 表

  • 文件描述符表(File Descriptor Table):每个进程都有自己的 文件描述符表,这个表对应于该进程打开的文件描述符。文件描述符是进程范围内的一个小的非负整数。
  • 文件打开表(Open File Table):操作系统维护一个 全局的 文件打开表,该表记录了所有打开文件的状态信息。每次 open 调用成功时,都会创建一个新的 文件打开表条目
  • INode 表(INode Table):系统还维护了一个 inode 表,该表包含了文件的元数据和指向文件实际数据的指针。如果多个进程打开同一个文件,它们的 文件打开表条目 将指向 inode 表 中的同一 inode

inode 表

inode 表(inode table)是一个固定的、专门的区域,用来存储文件系统中所有文件和目录的 inode 结构。每个 inode 占用一个表项,表中的每个 inode 编号(inode number)对应一个唯一的文件或目录。

注意 inode 表是存储所有 inode 的唯一容器

  • 每个文件/目录都有一个唯一的 inode 编号
  • 文件系统通过该编号在 inode 表 中找到对应的 inode

📊 下表给出了一个简化的 inode 表示例:

inode 编号文件类型权限所有者文件大小 (字节)数据块指针(简略)创建时间修改时间
1目录drwxr-xr-xroot4096[100, 101, 102]2024-01-01 10:002024-01-02 10:00
2文件-rw-r–r–user1024[200, 201]2024-01-01 11:002024-01-01 12:00
3符号链接lrwxrwxrwxuser14[路径字符串: “/etc/abc”]2024-01-01 13:002024-01-01 13:00
4文件-rwxr-xr-xuser8192[300, 301, 302, 303]2024-01-02 08:002024-01-02 08:00

inode 表中的每一行就是一个 inode

系统打开文件表

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

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

  1. 创建一项“系统打开文件表”记录(如果该文件尚未打开,或者是独立打开的)
  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 编号打开模式当前偏移量状态标志引用计数指向的文件路径
01024读写 (rw)1202/home/user/a.txt
11050只读 (r)0非阻塞1/etc/hosts
21024读写 (rw)120共享同表项 0/home/user/a.txt
32001只写 (w)45O_APPEND1/var/log/sys.log

文件描述符表

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

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

  • 系统打开文件表 中新建或复用一项(记录 inode、偏移等)
  • 在该进程的 文件描述符表 中添加一个条目(索引号)

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

文件描述符(fd)指向系统文件表项编号
05
16
27
39
410

总体视角

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

file descriptor
file pointer
fdA1
1
file descriptor
file pointer
fdA2
3
fdAdup
1
file offset
access mode
reference count
inode pointer
0
O_RDONLY
2
4
1
O_RDONLY
1
4
0
1
2
3
4
5
file type
file locks
file properties
Regular
...
...
0
1
2
3
4
5
6
Process A File Descriptor Table
Open File Table
Process B File Descriptor Table
INode Table

三张表的逻辑关系可以理解为:进程描述符表(进程级) → 系统文件打开表(系统级) → inode 表(系统级)

每个进程有自己的 文件描述符表,记录 文件描述符(如 3、4 等)指向 系统打开文件表 中的某一项;系统打开文件表 是全局共享的,记录当前文件的读写偏移量、打开模式,并指向对应的 inodeinode 表 保存了所有文件的 元数据 和数据块地址,是文件的最终物理信息所在。通过这三层结构,系统实现了多个进程共享文件、独立管理偏移量和统一访问底层文件的能力。

系统调用

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

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

打开和关闭

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

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

因此,open 实际上是一次向内核“申请访问权限”的操作,它返回 文件描述符(file descriptor),文件描述符会被存储到进程的 文件描述符表 中,并将 系统文件打开表 中的引用计数 +1。此外,如果文件是第一次被打开时,该文件的 inode 也会从外存中被加载到内存中。

相对应的,close 的作用是通知内核:该文件不再使用,可以释放相关资源。若程序未正确调用 close文件描述符将无法释放,可能导致资源泄露,最终耗尽进程可用的 文件句柄close 可以理解为 open 的反向操作:从 文件描述符表 中删除 文件描述符;将 系统文件打开表 中的引用计数 -1;如果引用计数为 0 时,内存中的 inode 会被释放。

读写

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

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

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

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

1. 延迟写

Unix 系统中,write 系统调用并不会立即将数据写入磁盘,而是将其暂存在内核的 页缓存(page cache)中。只有在特定时机——如缓存空间不足、文件被关闭,或系统周期性同步——才会将缓存中的数据统一刷新到磁盘(思路与 cache 中的 回写法 一致,需要使用到脏位)。

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

然而,这种 “延迟写” 机制也带来了风险:如果在数据尚未刷盘前系统发生异常(如断电或崩溃),这些暂存在内存中的数据可能会丢失。为此,如果应用场景对数据持久性要求较高,可以使用 fsync(fd) 显式触发缓存刷盘,确保数据安全地写入磁盘。

  1. 预读取

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

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

定位

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

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

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

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

文件的逻辑结构

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

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

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

文件的物理结构

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

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

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

连续分配

连续分配 方案中,每个文件占用磁盘上一个 连续的块集。例如,如果一个文件需要 n 个块,并且给定一个块 b 作为起始位置,那么分配给该文件的块将是 b, b+1, b+2,……b+n-1。这意味着给定起始块地址和文件的长度,我们可以确定文件占用的块。

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

  • 文件的其实地址
  • 分配块数量

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

0
1
2
3
4
5
6
7
8
9
10
3
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
file
start
length
file1
0
2
file2
14
3
file3
19
6
file4
28
4
file5
6
2
Directory
  • 优点:因为文件块的连续分配,查找的次数很少,访问速度非常快
  • 缺点:存在内部和外部 碎片化 的问题,使得内存利用效率低下。增加文件大小困难,因为它取决于特定实例中连续内存的可用性。

链式分配

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

目录项包含指向起始和结束文件块的指针。每个块包含一个指针,指向文件所占用的下一个块。

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

0
1:10
2
3
4
5
6
7
8
9:16
10:25
3
12
13
14
15
16:1
17
18
19
20
21
22
23
24
25:-1
26
27
28
29
30
31
file
start
end
file1
9
25
Directory
  • 优点:可以 充分利用磁盘空间,不会遭遇到碎片的问题。
  • 缺点:因为文件是在磁盘上随机分配的,所以访问一个文件的过程需要经历多次磁盘的检索,这使得文件读取的 速度变慢

文件分配表

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

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

FILENAME
· · · · · ·
START
file1
· · · · · ·
2
file2
· · · · · ·
7
BLOCK NUM
NEXT
0
-2
1
-1
2
8
3
-2
4
-2
5
-1
6
-2
7
1
8
5
9
-2
· · · ·
· · · · 
98
-2
99
-2

索引分配

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

假设一个文件的大小是 4MB,一个 盘块的大小 为 4KB,盘块编号的大小 为 4B(32 位)。 在这种情况下,文件需要用 1K 个盘块存储,索引块 的内容如下图所示:

block 0
numer
block 1
numer
block 2
numer
block 3
numer
block 0
numer
block 1
numer
block 2
numer
block 3
numer
block 1020
numer
block 1021
numer
block 1022
numer
block 1023
numer
Index Block = 4KB
block num = 4B

4KB 的盘块刚好可以存储下 1K 个盘块号作为索引,在读取文件时,操作系统先读取 索引块,确定存储文件的所有盘块号,再去读取所有存储有文件数据的盘块,如下图所示:

0
1
2
3
4
5
6
7
8
9
10
3
12
13
14
15
16
17
18
20
21
22
23
24
25
26
27
28
29
30
31
file
start
end
file1
9
25
Directory
19
Index Block
Data Block

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

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

一级索引块
二级索引块
三级索引块
数据块

混合索引

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

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

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

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

mode
owners(2)
timestamps(3)
size block count
direct blocks
single indirect
double indirect
triple indirect
data
data
data
data
data
data
data
data
  • 直接块(Direct blocks):若文件只有几块数据,则直接在这些条目中存放相应的盘块号,无需额外的索引块。
  • 一级间接索引块(Single indirect):,指向一个仅存放数据块号的块数组;
  • 二级间接索引块(Double indirect):,先指向若干一级索引块,再由这些一级索引块指向实际的数据块号;
  • 三级间接索引块(Triple indirect):,层层递进,最终定位到数据块号。

通过这种 直接块 + 多级间接块 的混合方案,系统能够在保证大文件可用的同时,避免小文件因占用完整索引块而造成的磁盘空间浪费,从而显著提升磁盘块的利用率并减少文件碎片的产生。