Linux fork() 后文件描述符继承机制总结

本文核心围绕 fork() 调用后,文件描述符、struct file、inode 三者的关联与变化规则,梳理核心机制、易错点和底层逻辑,形成完整、清晰的技术总结。

一、fork() 调用后,进程究竟继承了什么?

用户在父进程中执行如下系统调用:

cpp 复制代码
fork();

Linux 内核会创建一个全新的子进程,并深度复制父进程的运行环境,核心复制内容包含:

  • 全新的进程控制块(task_struct)

  • 全新的虚拟地址空间描述结构(mm_struct,基于写时拷贝 COW 机制实现,初始与父进程共享数据,修改时才独立复制)

  • 全新的文件描述符表(files_struct)

核心要点:fork() 会为子进程复制一份独立的文件描述符表,父子进程并非共享同一个文件描述符表。

这意味着父进程和子进程,各自拥有一套完全独立的 fd 表,初始状态下两张表格的索引映射关系完全一致,示例如下:

cpp 复制代码
父进程 fd table          子进程 fd table

0 -> stdin              0 -> stdin
1 -> stdout             1 -> stdout
2 -> stderr             2 -> stderr

虽然父子进程的 fd 表相互独立,但表格中存储的指针,初始会指向同一个内核文件资源结构体。

二、进程已打开的文件资源,fork() 后会如何变化?

这是该机制最容易混淆的核心知识点。首先明确层级关系:进程的文件描述符并非直接指向文件本身,而是遵循如下层级:

cpp 复制代码
文件描述符(fd)
 │
 ▼
struct file(文件打开实例)
 │
 ▼
inode(文件本体标识)

fork() 执行后,整体资源映射关系如下:

cpp 复制代码
父进程 fd3
   │
   ├──────────────┐
   ▼              ▼
          struct file(全局唯一)
               │
               ▼
             inode(文件本体)
   ▲              ▲
   │              │
子进程 fd3────────────┘

核心结论:fork() 后,父子进程会共享同一个 struct file 结构体。

基于该特性,父子进程会共享该文件的核心状态信息:

  • 文件读写偏移量(f_pos)

  • 文件打开标志(flags)

  • 绑定的 inode 节点

典型场景:若父进程从文件中读取 100 字节数据,文件偏移量会同步更新,子进程后续读取该文件时,会从第 101 字节的位置继续读取,而非从头读取。

三、一方关闭文件,会影响另一方吗?

不会互相影响

假设父进程执行关闭标准输入的操作:

cpp 复制代码
close(0);

该操作仅会清空当前进程自身 fd 表中 0 号文件描述符的映射关系:

cpp 复制代码
父进程 fd table 
0 (空,无映射) 
1 -> stdout 
2 -> stderr

而子进程的 fd 表完全不受影响,依旧保持原有映射:

cpp 复制代码
子进程 fd table 
0 -> stdin 
1 -> stdout 
2 -> stderr

底层原理:close() 系统调用仅执行两个操作:

  1. 删除当前进程文件描述符表中对应的索引项;

  2. 对目标 struct file 结构体的引用计数减 1。

由于子进程仍持有该 struct file 的引用,引用计数不会归零,内核不会释放该文件资源。

核心结论:close() 仅修改当前进程的文件描述符表,不会影响其他进程,仅单独减少共享 struct file 的引用计数。

四、父进程新建打开文件,子进程会受影响吗?

完全不会

若 fork() 之后,父进程执行 open 打开新文件:

cpp 复制代码
open("a.txt");

内核会执行两项操作:

  1. 创建一个全新的 struct file 结构体;

  2. 仅在父进程的 fd 表中新增文件描述符映射项。

子进程的文件描述符表是独立的内存结构,不会发生任何变化,无法感知父进程的新建文件操作。

五、子进程单独打开同一文件,会共享 struct file 吗?

不会共享 struct file,仅共享 inode

场景:父子进程分别执行 open 打开同一个物理文件 a.txt。

  1. 父进程先执行 open("a.txt"):内核创建 struct file A,绑定 a.txt 对应的 inode 节点。

  2. 子进程后续执行 open("a.txt"):内核会新建一个 struct file B,而非复用父进程的 struct file A,同时绑定同一个 a.txt 的 inode 节点。

cpp 复制代码
struct file A ≠ struct file B 
但二者指向同一个 inode(a.txt)

核心规则:每一次 open() 系统调用,都会生成一个全新的 struct file 结构体。

struct file 的本质是一次文件打开的实例(Open File Description),记录单次打开的状态。因此两次独立的 open 操作,会拥有相互独立的文件状态:

  • 独立的文件读写偏移量(offset)

  • 独立的文件打开标志(flags)

  • 独立的私有数据(private_data)

而 inode 代表文件本身,是物理文件的唯一标识,因此同一文件的多次打开操作,会复用同一个 inode。

六、哪些场景会让多个 fd 共享同一个 struct file?

仅有三类场景会出现多文件描述符共享同一个 struct file 的情况,其余场景均会新建 struct file:

1. fork() 进程创建

cpp 复制代码
父进程 fd3
     \
      \
   共享同一个 struct file
      /
     /
子进程 fd3

2. dup() 文件描述符复制

cpp 复制代码
dup(fd);
cpp 复制代码
原 fd3
  \
   \
  共享同一个 struct file
   /
  /
新 fd5

3. dup2() 文件描述符重定向

与 dup() 机制完全一致,仅多了指定目标 fd 的功能,依旧共享原有的 struct file 结构体。

除此以外,任何一次 open() 调用都会创建全新的 struct file,不会与已有实例共享。

七、完整层级关系全景图

cpp 复制代码
                 fork()

      父进程                     子进程
┌──────────────┐          ┌──────────────┐
│ files_struct │          │ files_struct │
│──────────────│          │──────────────│
│ fd0 ─────────┼────────┐ │ fd0 ─────────┼────────┐
│ fd1 ─────────┼──────┐ │ │ fd1 ─────────┼──────┐ │
│ fd2 ─────────┼───┐  │ │ │ fd2 ─────────┼───┐  │ │
│ fd3 ─────────┼─┐ │  │ │ │ fd3 ─────────┼─┐ │  │ │
└──────────────┘ │ │  │ │ └──────────────┘ │ │  │ │
                 ▼ ▼  ▼ ▼                  ▼ ▼  ▼ ▼
            ┌───────────────────────┐
            │     struct file       │
            │───────────────────────│
            │ f_pos(文件偏移)     │
            │ flags(打开标志)      │
            │ f_op(文件操作方法)  │
            │ inode *(指向文件节点)│
            └──────────┬────────────┘
                       │
                       ▼
                ┌──────────────┐
                │    inode     │
                │──────────────│
                │ 文件大小        │
                │ 访问权限        │
                │ inode 唯一编号  │
                │ 地址空间管理     │
                └──────┬────────┘
                       │
                       ▼
                  page cache(页缓存)
                       │
                       ▼
                    磁盘物理数据

八、核心机制一句话终极总结

  • fork() :复制一份独立的进程文件描述符表,父子进程共享原有 struct file 实例及文件状态

  • close():仅清空当前进程的 fd 表映射,仅递减 struct file 引用计数,不影响其他进程;

  • open():每次调用均新建独立的 struct file 实例,拥有独立文件状态,打开同一文件时,多个 struct file 会共享同一个 inode;

  • 三者本质区分:fd 是进程访问文件的索引、struct file 是文件的一次打开状态实例、inode 是磁盘文件的唯一本体标识,理清三者关系是掌握 Linux 文件进程机制的核心。