文件

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

文件元信息

在 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.txt 的 inode

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

表项编号(索引)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 等)指向系统打开文件表中的某一项;系统打开文件表是全局共享的,记录当前文件的读写偏移量、打开模式,并指向对应的 inode;inode 表保存了所有文件的元数据和数据块地址,是文件的最终物理信息所在。通过这三层结构,系统实现了多个进程共享文件、独立管理偏移量和统一访问底层文件的能力。

系统调用

在 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

索引分配

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

假设一个文件的大小是 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 级索引有一个显著的缺点,就是对于非常小的文件,比如大小只有两三个盘块的文件,仍然需要使用一个完整的索引块来存储这些数据块的盘块号,这样索引块中的很多空间都没有使用,这导致盘块利用率非常的低。

还有一种混合索引的方式,可以解决上述提到的缺点,这种方式针对小文件或者大文件进行不同的处理。如果文件比较小的话,使用直接块即可,如果文件比较大的话,可以使用单级或多级索引。

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 中存储的就是直接块的盘块号,如果文件的大小只有几个盘块,直接将数据块的盘块号存储在 direct blocks 中即可。

此外,混合索引中还包含不同层次的索引块。 single indirect 是一级索引,double indirect 是二级索引,triple indirect 是三级索引。 通过多级索引,可以尽量避免文件碎片的存在,增大磁盘块的利用率。