[toc]

fs/file_table.c 文件表管理(File Table Management) VFS中“打开文件”对象的分配与管理核心

历史与背景

这项技术是为了解决什么特定问题而诞生的?

fs/file_table.c 及其管理的核心数据结构 struct file 是整个Linux/Unix虚拟文件系统(VFS)的基石。它的诞生是为了解决一个操作系统设计中的根本性问题:如何清晰、高效地管理和区分“文件本身”与“对文件的打开实例”

具体来说,它解决了以下几个核心问题:

  1. 分离“打开”的状态:一个磁盘上的文件(例如 /home/user/data.txt)是静态的。但当一个进程打开它时,就需要一个地方来存储与这次“打开”相关的动态状态,最关键的就是当前读写位置(file position/offset)struct file 对象就是为此而生。
  2. 支持多个独立的打开实例:同一个进程或不同进程可以多次打开同一个文件。每一次open()调用都应该获得一个独立的“会话”,拥有自己独立的读写位置。这意味着需要为每次open()创建一个新的struct file对象。
  3. 支持共享的打开实例:在某些情况下(如fork()dup()之后),又需要让多个文件描述符指向同一个“打开会话”,共享同一个读写位置。
  4. 提供统一的资源管理struct file对象作为“打开文件”的内核表示,是管理所有相关资源(如锁、内存映射等)的中心锚点。通过对其进行引用计数,内核可以精确地知道何时可以安全地释放这些资源。

因此,file_table.c不是为了管理文件本身,而是为了管理代表“打开的文件描述”(Open File Description)的struct file对象池。

它的发展经历了哪些重要的里程碑或版本迭代?

struct file 和文件表的概念直接继承自经典的Unix设计,是VFS的核心,因此其基本思想非常稳定。其发展主要体现在性能和可伸缩性的优化上。

  • 从静态到动态:早期的Unix系统可能使用静态大小的文件表。而Linux从一开始就使用动态分配的struct file对象,能够支持系统中有大量文件被打开。
  • 性能优化:随着服务器需要处理数十万甚至上百万的并发连接(每个连接都对应至少一个文件描述符),file_table.c中的分配(get_empty_filp)和释放逻辑经历了持续的性能优化,例如使用专门的Slab缓存来加速struct file对象的分配和回收。
  • O_CLOEXEC的引入:这是一个重要的安全和健壮性里程碑。为了从根本上解决“文件描述符泄漏”(FD Leak)问题(即子进程意外继承了父进程不应继承的文件描述符),open()系统调用增加了O_CLOEXEC标志。这使得内核可以在execve()执行新程序时自动关闭这些被标记的文件描述符。这个逻辑的实现与files_struct的管理密切相关。

目前该技术的社区活跃度和主流应用情况如何?

fs/file_table.c是Linux内核VFS层最核心、最稳定的部分之一。它不是一个经常添加新功能的领域,但其性能和正确性对整个系统的影响是全局性的,因此受到社区的持续关注和严格维护。

  • 主流应用系统上运行的每一个进程,只要它打开了任何文件(包括设备、套接字、管道),就必然会使用到file_table.c所管理的机制。它是所有I/O操作的基础。

核心原理与设计

它的核心工作原理是什么?

fs/file_table.c的工作原理必须放在VFS的三级结构中来理解,这三级结构清晰地划分了用户空间句柄、打开文件实例和磁盘文件的关系。

  1. 第一级:每进程文件描述符表 (Per-Process File Descriptor Table)

    • 内核为每个进程维护一个struct files_struct。这个结构中最重要的成员是一个指向struct file *的指针数组,通常称为fd_array
    • 用户空间程序使用的文件描述符(File Descriptor, FD),即那个小整数,就是这个数组的索引
    • 所以,fd_table[fd]就得到了一个指向第二级结构的指针。
  2. 第二级:系统级打开文件表 (System-wide Open File Table)

    • 这就是由fs/file_table.c管理的核心。它维护着系统中所有活跃的struct file对象。
    • struct file代表一个打开的文件描述。它包含了这次打开操作的动态信息:
      • f_pos: 当前的读写位置(偏移量)。这是struct file最重要的状态
      • f_flags: 打开文件时指定的标志(O_RDONLY, O_APPEND等)。
      • f_op: 一个指向file_operations结构体的指针,由具体的文件系统或设备驱动提供。当用户对FD调用read(), write()时,VFS最终会通过这个指针调用到真正的驱动代码。
      • f_count: 引用计数。记录了有多少个FD指向这个struct file对象。
      • 一个指向第三级结构(inode)的指针。
  3. 第三级:系统级i-node表 (System-wide i-node Table)

    • struct inode代表一个物理文件本身。它包含了文件的静态元数据,如权限、大小、所有者、时间戳以及指向数据块的指针。
    • 一个inode可以被多个struct file对象引用(对应同一个文件被多次独立打开)。

操作流程示例:

  • fd = open("a.txt", O_RDONLY): 内核创建一个新的struct file对象(f_pos=0),找到a.txt对应的inode并让struct file指向它,然后在当前进程的FD表中找一个空位,将FD号返回给用户,并将fd_table[fd]指向这个新的struct file
  • fork(): 子进程会获得一个父进程files_struct副本。这个副本中的指针和父进程指向完全相同struct file对象。因此,fork之后,父子进程共享文件偏移量。
  • dup(fd): 内核在当前进程的FD表中找到一个新的空闲位置new_fd,然后让fd_table[new_fd]指向与fd_table[fd]相同struct file对象,并增加该struct file的引用计数。
  • close(fd): 内核将fd_table[fd]设为NULL,并递减其指向的struct filef_count。当f_count减为0时,这个struct file对象才会被真正销毁。

它的主要优势体现在哪些方面?

  • 清晰的抽象层次:完美地区分了进程句柄、打开实例和物理文件。
  • 灵活的共享机制:通过指针的复制和共享,优雅地实现了fork()dup()等核心Unix语义。
  • 健壮的资源管理:基于引用计数的生命周期管理,确保了资源的安全释放。

它存在哪些已知的劣势、局限性或在特定场景下的不适用性?

  • 资源限制:虽然是动态的,但系统能打开的文件总数(/proc/sys/fs/file-max)和每个进程能打开的文件数(ulimit -n)都是有限的。
  • 文件描述符泄漏(FD Leak):这是应用程序的常见bug,而非框架本身的缺陷。如果程序不断地打开文件而忘记关闭,就会耗尽可用的FD,最终导致无法打开新文件。

使用场景

在哪些具体的业务或技术场景下,它是首选解决方案?

这不是一个可选的解决方案,而是Linux/Unix进行文件I/O的唯一方式。所有涉及文件描述符的操作都构建在其之上。

  • 标准文件I/Ocat file.txtcat进程打开file.txt,获得一个FD,然后循环read()这个FD。
  • 管道(Pipes)ls | grep foopipe()系统调用会创建两个struct file对象,它们指向同一个内存管道的inodels进程持有写端的FD,grep进程持有读端的FD。
  • Shell I/O重定向echo "hello" > output.txt。Shell会先open("output.txt", ...)得到一个FD(例如3),然后使用dup2(3, 1),使得代表标准输出的FD 1也指向output.txtstruct file对象。之后echo程序通过标准输出的所有内容就都写入了文件。2>&1也是同理,让FD 2指向与FD 1相同的struct file对象。

是否有不推荐使用该技术的场景?为什么?

这个机制本身是普适的。问题不在于是否使用它,而在于是否应该使用“文件”这个抽象。对于某些不需要文件语义的场景,有更合适的IPC(进程间通信)机制。但有趣的是,即使是Pipes和Sockets,在Linux中也通过这个文件描述符和struct file框架来实现,只是它们的file_operations指向了不同的内核子系统。

对比分析

请将其 与 其他相似技术 进行详细对比。

在VFS中,最好的对比分析就是详细地区分这三个核心结构。

特性 文件描述符 (FD) 打开文件对象 (struct file) i-node对象 (struct inode)
核心功能 用户空间的句柄 (Handle)。一个整数,用于标识一个打开的文件。 打开文件实例的描述。代表一次open操作,维护其动态状态。 物理文件的元数据。代表一个磁盘上或内存中的文件实体。
作用域 每进程 (Per-Process) 系统全局 (System-wide) 系统全局 (System-wide)
生命周期 open/dup开始,到close或进程退出结束。 从第一次open开始,到最后一个指向它的FD被close结束。 从第一次被访问(加载到内存)开始,到没有被任何东西引用并被从缓存中回收结束。
主要包含信息 自身就是信息(一个整数)。 当前文件位置 (f_pos),打开标志 (f_flags),文件操作集 (f_op)。 文件元数据(权限、大小、所有者、时间戳),指向数据块的指针。
共享方式 进程退出时自动销毁。 通过fork()dup(),可以让多个FD指向同一个struct file实例。 同一个物理文件可以被多次独立open,对应多个struct file实例指向同一个inode

files_init 于设置与文件管理相关的缓存和计数器

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
void __init files_init(void)
{
struct kmem_cache_args args = {
.use_freeptr_offset = true,
.freeptr_offset = offsetof(struct file, f_freeptr),
};
/* SLAB_HWCACHE_ALIGN 确保缓存对齐以优化硬件性能。
SLAB_PANIC 确保在分配失败时触发内核错误,避免系统进入不稳定状态。
SLAB_ACCOUNT 用于内存使用的统计和控制。
SLAB_TYPESAFE_BY_RCU 确保缓存支持 RCU(Read-Copy-Update)机制,允许在 RCU 读取期间安全地释放对象。 */
filp_cachep = kmem_cache_create("filp", sizeof(struct file), &args,
SLAB_HWCACHE_ALIGN | SLAB_PANIC |
SLAB_ACCOUNT | SLAB_TYPESAFE_BY_RCU);

args.freeptr_offset = offsetof(struct backing_file, bf_freeptr);
/* 创建后备文件缓存 */
bfilp_cachep = kmem_cache_create("bfilp", sizeof(struct backing_file),
&args, SLAB_HWCACHE_ALIGN | SLAB_PANIC |
SLAB_ACCOUNT | SLAB_TYPESAFE_BY_RCU);
/* 初始化 nr_files 计数器,用于跟踪系统中打开的文件数量 */
percpu_counter_init(&nr_files, 0, GFP_KERNEL);
}

files_maxfiles_init 设置系统允许打开的最大文件数(max_files)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
/*
* 一个带有关联 inode 和 dcache 的文件大约为 1K。
* 默认情况下,不要使用超过 10% 的文件内存。
*/
void __init files_maxfiles_init(void)
{
unsigned long n;
/* 获取系统的总内存页数
每页内存的大小由 PAGE_SIZE 定义,通常为 4KB*/
unsigned long nr_pages = totalram_pages();
/* nr_free_pages 返回当前系统的空闲内存页数
估算需要保留的内存量*/
unsigned long memreserve = (nr_pages - nr_free_pages()) * 3/2;

memreserve = min(memreserve, nr_pages - 1);
/* 根据剩余内存(nr_pages - memreserve)计算最大文件数
假设每个文件(包括关联的 inode 和 dcache)大约占用 1KB内存,
函数限制文件管理占用的内存不超过总内存的 10% */
n = ((nr_pages - memreserve) * (PAGE_SIZE / 1024)) / 10;

files_stat.max_files = max_t(unsigned long, n, NR_FILE);
}

Sysctl接口创建:通过/proc/sys/fs导出文件系统统计信息

本代码片段的功能是利用Linux的sysctl机制,在/proc/sys/fs/目录下创建一系列文件,用于向用户空间展示和配置内核中与文件系统相关的核心统计数据和限制。具体来说,它创建了file-nr(显示当前打开的文件句柄数)、file-max(显示和设置系统级最大文件句柄数)和nr_open(显示和设置单进程最大文件描述符数)这三个接口。

实现原理分析

该功能的实现完全建立在内核的sysctl和procfs框架之上。Sysctl是一种允许管理员在系统运行时读取和修改内核参数的机制,而procfs是这种机制在用户空间的主要表现形式。

  1. Sysctl表定义 (ctl_table):

    • 代码的核心是fs_stat_sysctls数组,它是一个ctl_table结构体的集合。每一个ctl_table实例定义了一个sysctl条目(即/proc/sys/fs/下的一个文件)。
    • 关键字段包括:
      • .procname: 在procfs中显示的文件名。
      • .data: 指向该sysctl条目关联的内核变量的指针。
      • .maxlen: data指向的变量的大小。
      • .mode: 文件的访问权限(如0444为只读,0644为可读写)。
      • .proc_handler: 处理该文件读写操作的函数。可以是内核提供的标准处理函数(如proc_doulongvec_minmax用于处理带范围检查的整数),也可以是自定义的特殊处理函数(如proc_nr_files)。
  2. 自定义处理函数 (proc_nr_files):

    • 对于file-nr,它的值不是一个简单的静态变量,而是需要实时计算的。内核使用了一个percpu_counter(每CPU计数器)来高效、无锁地统计全局打开的文件句柄数。
    • 因此,file-nr需要一个自定义的.proc_handler——proc_nr_files。当用户读取/proc/sys/fs/file-nr时,此函数被调用。它首先调用percpu_counter_sum_positive来汇总所有CPU上的计数值,得到一个全局的总和,并将这个总和存入files_stat.nr_files。然后,它再调用标准的proc_doulongvec_minmax函数来将这个刚刚计算出的值格式化并返回给用户。
  3. 注册与初始化 (init_fs_stat_sysctls):

    • register_sysctl_init("fs", fs_stat_sysctls)是注册的核心。它告诉sysctl框架,将fs_stat_sysctls表中定义的所有条目注册到 “fs” 命名空间下,从而创建出/proc/sys/fs/目录及其中的文件。
    • fs_initcall确保此函数在内核启动过程中,在文件系统相关的子系统初始化之后被调用。
    • 代码还包含了对CONFIG_BINFMT_MISC的条件编译,如果该配置开启,还会为杂项二进制格式注册一个sysctl挂载点。

代码分析

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
// 仅在内核配置了SYSCTL和PROC_FS时,以下代码才会被编译。
/*
* 处理 nr_files sysctl
*/
// proc_nr_files: /proc/sys/fs/file-nr 文件的专用处理函数。
static int proc_nr_files(const struct ctl_table *table, int write, void *buffer,
size_t *lenp, loff_t *ppos)
{
// 实时计算当前打开的文件句柄总数。
// nr_files 是一个 per-cpu 计数器,这里将其在所有CPU上的值加总。
files_stat.nr_files = percpu_counter_sum_positive(&nr_files);
// 调用标准的处理函数,将计算出的值格式化并返回给用户空间。
return proc_doulongvec_minmax(table, write, buffer, lenp, ppos);
}

// fs_stat_sysctls: 定义在 /proc/sys/fs/ 目录下的sysctl条目表。
static const struct ctl_table fs_stat_sysctls[] = {
{
.procname = "file-nr", // 文件名: file-nr
.data = &files_stat, // 关联的数据结构
.maxlen = sizeof(files_stat),
.mode = 0444, // 权限: 只读
.proc_handler = proc_nr_files, // 使用自定义的处理函数
},
{
.procname = "file-max", // 文件名: file-max
.data = &files_stat.max_files, // 直接关联到最大文件数的变量
.maxlen = sizeof(files_stat.max_files),
.mode = 0644, // 权限: 可读写
.proc_handler = proc_doulongvec_minmax, // 使用标准的整数处理函数
.extra1 = SYSCTL_LONG_ZERO, // 附加参数: 允许设置的最小值为0
.extra2 = SYSCTL_LONG_MAX, // 附加参数: 允许设置的最大值为LONG_MAX
},
{
.procname = "nr_open", // 文件名: nr_open
.data = &sysctl_nr_open, // 关联到单进程最大文件描述符数的变量
.maxlen = sizeof(unsigned int),
.mode = 0644, // 权限: 可读写
.proc_handler = proc_douintvec_minmax, // 使用标准的无符号整数处理函数
.extra1 = &sysctl_nr_open_min, // 附加参数: 最小值的地址
.extra2 = &sysctl_nr_open_max, // 附加参数: 最大值的地址
},
};

// init_fs_stat_sysctls: 初始化函数,用于注册上述的sysctl表。
static int __init init_fs_stat_sysctls(void)
{
// 将 fs_stat_sysctls 表注册到 "fs" 目录下。
register_sysctl_init("fs", fs_stat_sysctls);
// 如果内核配置了 BINFMT_MISC 支持。
if (IS_ENABLED(CONFIG_BINFMT_MISC)) {
struct ctl_table_header *hdr;

// 为 binfmt_misc 注册一个sysctl挂载点,即创建 /proc/sys/fs/binfmt_misc 目录。
hdr = register_sysctl_mount_point("fs/binfmt_misc");
// 告知内存泄漏检测器,这里返回的hdr不是内存泄漏。
kmemleak_not_leak(hdr);
}
return 0;
}
// 将初始化函数注册为 fs_initcall,确保在文件系统初始化后执行。
fs_initcall(init_fs_stat_sysctls);

VFS文件对象分配:创建内核中打开文件的实例

本代码片段展示了Linux内核中一个至关重要的底层操作:分配并初始化一个struct file对象。这个结构体,在内核源码中常被称为filp(file pointer),是VFS层对一个“打开的文件”的抽象表示。它并非文件本身,而是进程与文件系统中的一个具体文件(由inode表示)进行交互的会话。每当用户空间调用open()socket()等系统调用时,内核都会执行类似本代码的逻辑,创建一个struct file实例来代表这个打开的文件句柄。

实现原理分析

此功能的实现围绕着资源管理、内存分配和对象初始化这三个核心环节,并体现了Linux内核设计中的性能与安全考量。

  1. 资源限制检查 (alloc_empty_file):

    • 在分配任何新资源之前,内核首先检查系统当前打开的文件总数(get_nr_files())是否已达到系统上限(files_stat.max_files)。
    • 特权豁免capable(CAP_SYS_ADMIN)检查允许拥有特定权限的特权进程(通常是root)超出这个硬性限制,这为系统关键任务提供了保障。
    • 性能优化nr_files是一个percpu_counter(每CPU计数器),它允许在不使用昂贵原子操作或锁的情况下进行快速、无竞争的计数更新。这会导致其总和(get_nr_files())可能略有延迟或不精确。因此,代码采用两阶段检查:首先用快速但不精确的get_nr_files(),如果它超限,再调用percpu_counter_sum_positive()进行一次更精确但开销更大的求和,确认是否真的达到了上限,避免了错误的拒绝。
  2. 内存分配 (alloc_empty_file):

    • struct file对象是通过kmem_cache_alloc(filp_cachep, ...)从一个专门的slab缓存(filp_cachep)中分配的。对于这种频繁创建和销毁的同尺寸对象,使用slab分配器远比通用的kmalloc高效,因为它能减少内存碎片并复用对象,提升性能。
  3. 对象初始化 (init_file):

    • 这是一个“构造函数”,负责将一块原始内存初始化为一个有效的、空的struct file对象。
    • 安全上下文:首先,它通过get_cred(cred)将打开文件操作的进程凭证(cred)与文件对象关联起来,并调用security_file_alloc为LSM(Linux安全模块,如SELinux)提供挂钩,这是安全策略实施的基础。
    • 状态初始化:初始化对象内的锁(f_lock, f_pos_lock),并将关键指针(如f_op, f_inode)设为NULL,标志、模式等字段根据传入的flags设置。此时的对象是“空的”,因为它尚未与任何具体的文件系统或inode关联。
    • 引用计数file_ref_init(&f->f_ref, 1)将对象的引用计数初始化为1。这是对象生命周期管理的基石。注释中特别提到,此操作被放在最后是为了SLAB_TYPESAFE_BY_RCU。这是一种高级的RCU(读-复制-更新)安全模式,要求引用计数在对象所有其他成员都初始化完毕后才能被设置,以防止RCU读端在对象尚未完全初始化时就访问到它。

代码分析

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
// init_file: 初始化一个新分配的file结构体。
static int init_file(struct file *f, int flags, const struct cred *cred)
{
int error;

// 获取并关联打开文件进程的凭证(credentials),增加凭证的引用计数。
f->f_cred = get_cred(cred);
// 调用Linux安全模块(LSM)的钩子函数,为文件对象分配安全结构。
error = security_file_alloc(f);
if (unlikely(error)) {
// 如果安全分配失败,则递减凭证的引用计数并返回错误。
put_cred(f->f_cred);
return error;
}

// 初始化用于保护file结构体内部字段的自旋锁。
spin_lock_init(&f->f_lock);
/*
* f_pos_lock 互斥锁仅用于需要原子化文件位置更新的文件和目录。
* 其他类型的文件(如管道)不需要它,并且由于它位于一个联合(union)中,
* 这块内存可能被复用于其他目的。
*/
mutex_init(&f->f_pos_lock);
// 将路径和预读(read-ahead)相关的结构体清零。
memset(&f->f_path, 0, sizeof(f->f_path));
memset(&f->f_ra, 0, sizeof(f->f_ra));

// 根据传入的打开标志,设置文件的内部标志和访问模式。
f->f_flags = flags;
f->f_mode = OPEN_FMODE(flags);

// 将核心指针初始化为NULL,表示这是一个“空”文件对象,尚未关联到具体的文件系统。
f->f_op = NULL; // 文件操作函数表
f->f_mapping = NULL; // 地址空间映射
f->private_data = NULL; // 文件系统私有数据
f->f_inode = NULL; // 关联的inode
f->f_owner = NULL; // fasync/F_SETOWN的所有者
#ifdef CONFIG_EPOLL
f->f_ep = NULL; // epoll相关数据
#endif

f->f_iocb_flags = 0; // 异步I/O控制块标志
f->f_pos = 0; // 文件偏移量
f->f_wb_err = 0; // 回写错误码
f->f_sb_err = 0; // 超级块错误码

/*
* slab缓存使用了SLAB_TYPESAFE_BY_RCU标志,因此最后才初始化引用计数。
* 这是为了确保在使用RCU模式查找并获取文件对象时,
* 不会在对象未完全初始化时就意外地增加其引用计数。
*/
file_ref_init(&f->f_ref, 1);
/*
* 默认禁用所有文件的权限和内容变更前事件通知。
* fsnotify_open_perm_and_set_mode() 稍后可能会启用它们。
*/
file_set_fsnotify_mode(f, FMODE_NONOTIFY_PERM);
return 0;
}

// alloc_empty_file: 查找一个未使用的文件结构体并返回其指针。
struct file *alloc_empty_file(int flags, const struct cred *cred)
{
static long old_max;
struct file *f;
int error;

/*
* 特权用户可以超出 max_files 的限制。
*/
if (unlikely(get_nr_files() >= files_stat.max_files) &&
!capable(CAP_SYS_ADMIN)) {
/*
* 每CPU计数器(percpu_counters)是不精确的。在失败返回前,
* 先进行一次开销较大的精确检查。
*/
if (percpu_counter_sum_positive(&nr_files) >= files_stat.max_files)
goto over;
}

// 从名为'filp_cachep'的slab缓存中分配一个file对象。
f = kmem_cache_alloc(filp_cachep, GFP_KERNEL);
if (unlikely(!f))
return ERR_PTR(-ENOMEM); // 内存不足。

// 调用初始化函数来设置新分配的对象。
error = init_file(f, flags, cred);
if (unlikely(error)) {
// 如果初始化失败,则将对象释放回slab缓存。
kmem_cache_free(filp_cachep, f);
return ERR_PTR(error);
}

// 成功分配并初始化后,增加全局文件计数。
percpu_counter_inc(&nr_files);

return f;

over:
/* 文件结构体耗尽 - 报告该情况 */
// 为了避免日志泛滥,只有在当前文件数超过上次报告的峰值时才打印信息。
if (get_nr_files() > old_max) {
pr_info("VFS: file-max limit %lu reached\n", get_max_files());
old_max = get_nr_files();
}
return ERR_PTR(-ENFILE); // 返回“文件表溢出”错误。
}