fd 只是数组下标:`ksys_write()` 怎么找到 `struct file`

fd 只是数组下标:ksys_write() 怎么找到 struct file

系统调用系列的上一篇已经把 write(fd, buf, count) 送到了 ksys_write()。到这里,内核拿到的仍然只是三个参数:

text 复制代码
fd      = 一个整数
buf     = 用户空间中的一段数据
count   = 想写多少字节

真正的问题是:整数 3 怎么知道自己代表普通文件、终端、管道还是 socket?

答案是:它不知道。

fd 不是文件,也不是 inode。它只是当前进程"打开文件表"中的一个编号。内核用这个编号找到一次打开实例,也就是 struct file,再由它决定这次写入应该交给谁。

text 复制代码
进程
  │
  ▼
打开文件表
  │ fd=3
  ▼
struct file(一次打开实例)
  │
  ├─ 当前偏移
  ├─ 打开方式
  └─ 操作入口
        │
        ▼
普通文件 / 终端 / 管道 / socket

理解这张图,就理解了 ksys_write() 查找目标的主线。

一、fd 是进程自己的编号

从用户视角看,每个进程都有自己的 fd 空间。表里的每一项都可以指向一个 struct file

text 复制代码
fd 0  ──> 标准输入
fd 1  ──> 标准输出
fd 2  ──> 标准错误
fd 3  ──> 某次打开得到的对象

这很像寄存柜:fd 是取件号,struct file 才是这个号码关联的那一格。

因此,fd 只有放在具体进程中才有意义:

text 复制代码
进程 A 的 fd=3 ──> 日志文件
进程 B 的 fd=3 ──> socket

两个进程都使用整数 3,不代表它们正在操作同一个对象。即使在同一进程中,关闭 fd 后再打开别的对象,编号 3 也可能被重新利用。

所以日志里只记录"fd=3"通常不够。要判断它当时指向什么,还需要知道进程身份和 fd 的生命周期。

二、三层核心结构:files_structfdtablestruct file

进程并不是把 fd 数组直接放在 task_struct 里。task_struct 是 Linux task 的核心描述符,它与线程、调度、内核栈、mm_structcurrent 的关系,已经在《线程和进程的完整内存布局:task、栈、页表和 VMA》中详细讲过。

本文只沿着其中的 files 字段继续向下:

系统调用运行时,current 表示当前 CPU 上正在执行的 task。查找关系从这里开始:

text 复制代码
current
  -> task_struct.files
  -> files_struct.fdt
  -> fdtable.fd[fd]
  -> struct file

2.1 files_struct:一组 task 使用的打开文件上下文

只保留与 fd 查找有关的字段,files_struct 可以读成:

c 复制代码
struct files_struct {
    atomic_t count;
    bool resize_in_progress;
    wait_queue_head_t resize_wait;

    struct fdtable __rcu *fdt;
    struct fdtable fdtab;

    spinlock_t file_lock;
    unsigned int next_fd;

    unsigned long close_on_exec_init[1];
    unsigned long open_fds_init[1];
    unsigned long full_fds_bits_init[1];
    struct file __rcu *fd_array[NR_OPEN_DEFAULT];
};

这些字段可以从下面几个角度理解:

字段 含义
count files_struct 自身的引用计数,不是"打开了多少文件"
fdt 指向当前生效的 fdtable,查 fd 时首先沿这个指针走
fdtabfd_array 和三个 *_init 位图 内嵌的小表和配套存储,fd 数量较少时避免额外分配
file_lockresize_in_progressresize_wait 协调安装、关闭和扩容等修改操作
next_fd 下次寻找空闲 fd 时的起始提示,减少从头扫描

fdtfdtab 很容易混淆:

text 复制代码
files_struct
├─ fdt ───────────────> 当前正在使用的 fdtable
├─ fdtab                内嵌的小型 fdtable
├─ fd_array[]           内嵌的小型 file 指针数组
├─ open_fds_init[]      内嵌的已占用位图
└─ close_on_exec_init[] 内嵌的 CLOEXEC 位图

刚开始打开的 fd 很少时,关系大致是:

text 复制代码
files->fdt       -> &files->fdtab
files->fdtab.fd  -> files->fd_array

当 fd 数量超过内嵌容量时,内核分配更大的 fdtable、file 指针数组和位图,再让 files->fdt 指向新表。调用者始终从 fdt 找"当前表",不应该假设当前使用的一定是内嵌的 fdtab

2.2 fdtable:真正完成下标查找的表

fdtable 本身并不保存文件内容。它保存容量、file 指针数组和几组与槽位对应的位图:

c 复制代码
struct fdtable {
    unsigned int max_fds;
    struct file __rcu **fd;
    unsigned long *close_on_exec;
    unsigned long *open_fds;
    unsigned long *full_fds_bits;
    struct rcu_head rcu;
};

假设要查 fd=3,这些字段的关系是:

text 复制代码
fdtable
├─ max_fds              先确认 3 没有越界
├─ fd[3] ─────────────> struct file *
├─ open_fds bit 3       这个槽位是否正在使用
└─ close_on_exec bit 3  exec 时是否自动关闭

full_fds_bits 用来加速寻找有空闲槽位的位图区间,主要服务于"分配一个新 fd";fd[] 才是从已有 fd 找到 struct file 的关键数组。

这里还有一个容易忽略的点:fd[]open_fdsclose_on_exec 是并行关系。FD_CLOEXEC 不在 struct file 中,而是属于 fdtable 的某一个槽位;同一个 struct file 可以被两个 fd 槽位指向,而两个槽位各自拥有不同的 FD_CLOEXEC

2.3 struct file:表项指向的一次打开实例

查表得到的不是 inode,而是一个 struct file。下面只保留本文关心的字段,用来说明语义,不代表完整结构或固定内存布局:

c 复制代码
struct file {
    atomic_long_t f_count;
    fmode_t f_mode;
    const struct file_operations *f_op;
    struct address_space *f_mapping;
    void *private_data;
    struct inode *f_inode;
    unsigned int f_flags;
    const struct cred *f_cred;
    struct path f_path;
    union {
        struct mutex f_pos_lock;
        u64 f_pipe;
    };
    loff_t f_pos;
};

这些字段分别回答:

字段 回答的问题
f_count 还有多少内核引用持有这次打开实例
f_mode 这次打开在内核侧允许读、写、seek,还是一个 stream
f_op readwritepollmmap 等操作交给谁
f_posf_pos_lock 当前读写位置是什么,共享时如何保护
f_flags 是否设置 O_APPENDO_NONBLOCK 等打开状态
f_pathf_inode 对普通文件,打开的是哪个路径和 inode
f_mapping 文件内容对应哪一个 address_space,也就是 page cache 的对象入口
private_data 具体文件系统、设备或伪文件挂在本次打开实例上的私有状态
f_cred 打开时保存的凭据

struct file 不是磁盘文件本身,而是"一次打开":

text 复制代码
open("a.txt") ──> struct file A ──┐
                                  ├─> 同一个 inode
open("a.txt") ──> struct file B ──┘

两次 open() 通常产生两个 struct file,所以各自拥有独立的 f_posf_flagsdup() 则只增加一个 fd 槽位:

text 复制代码
fd=3 ─┐
      ├─> 同一个 struct file
fd=7 ─┘

因此两个 fd 会共享 f_posf_flags,但仍各自拥有 fdtable 中的 close_on_exec 位。

2.4 四个容易混淆的名字

ksys_write() 的代码中还会出现 struct fd。它不是 fdtable,也不是一次打开实例,而是 fdget_pos() 返回的临时包装值:

名字 生命周期和作用
整数 fd 用户态传入的数组下标
fdtable->fd[] 长期存在于打开文件表中的指针数组
struct fd 一次内核查找期间使用的临时包装,携带 file 指针和退出时如何清理的信息
struct file 被表项指向的一次打开实例

2.5 从一个真实进程看三种变化

假设进程 P 已经打开 data.txt,得到 fd=3,这个槽位指向 struct file A。下面分别对它执行线程创建、fork() 和再次 open(),观察每一层怎样变化。

先看一张全景图。它表示下面这组操作完成后的状态:

  1. 父进程 P 打开 data.txt 得到 fd=3
  2. dup(3) 得到 fd=7,两个槽位共享 struct file A;随后只给 fd=7 设置 FD_CLOEXEC
  3. 再次 open("data.txt") 得到 fd=4,创建独立的 struct file B
  4. P 创建线程 T2,T1 和 T2 共享 files_struct P
  5. T1 调用 fork() 创建子进程 C;C 得到复制的 fdtable,但槽位仍指向 A、B。
  6. C 随后创建线程 T4,因此 T3 和 T4 共享 files_struct C
  7. 另一个独立进程 Q 的 fd=3 指向 socket,与 P、C 的整数 3 无关。

读这张图时,先看箭头是否汇合:

  • T1、T2 的箭头汇合到 files_struct P,表示两个线程共享整张表。
  • P 和 C 的 files_struct 没有汇合,表示 fork() 后父子各自修改自己的 fdtable。
  • P3P7C3C7 汇合到 struct file A,表示同一打开实例同时被多个槽位、多个进程持有。
  • struct file Astruct file B 只在 inode 处汇合,表示它们访问同一份文件内容,却不共享打开状态。
  • Q 也有 fd=3,但它沿自己的表找到 socket,说明 fd 数字不能跨进程比较对象身份。
情况一:线程共享 fdtable,一个线程 close() 会修改共同的槽位

进程 P 创建线程 T1 和 T2 后,每个线程都有自己的 task_struct,但普通 POSIX 线程通常共享同一个 files_struct。因此,进程中并不存在"T1 的 fd 3"和"T2 的 fd 3"两个槽位;这里只有共享表中的一个 fd[3]

close(3) 修改的是两条 task_struct->files 指针共同指向的那张表。因此,如果这个编号尚未被新的 open() 复用,T2 在关闭完成后再次使用 fd=3,就会看到空槽位并得到 EBADF;如果编号已经被复用,整数 3 甚至可能代表一个新对象。

这里说的是"之后发起的新操作"。如果 T2 在 close() 之前已经通过 fdget() 取得了 file 并开始 I/O,这次正在进行的操作可能依靠自己的临时引用继续完成。

情况二:fork() 复制表,但父子槽位仍指向同一个 file

如果最初只有父进程 P,fork() 会为子进程 C 复制一份 files_struct 和 fdtable。父子不再修改同一张表,但复制得到的 fd[3] 仍保存同一个 struct file A 指针:

这时子进程执行 close(3),只会把子 fdtable 的第 3 项清空,父进程的第 3 项仍然存在。但只要父子槽位都还指向 struct file A,它们就共享 f_posf_flags

text 复制代码
子进程 close(3) 后:

父 fdtable:fd[3] ──> struct file A
子 fdtable:fd[3] ──> NULL
情况三:再次 open() 复用 inode,不复用打开实例

如果进程 P 不执行 fork(),而是再次调用 open("data.txt"),内核会在同一张 fdtable 中分配一个新槽位,例如 fd=4,并创建新的 struct file B

在两次打开之间路径没有被替换的前提下,两个槽位最后关联到同一个 inode,所以读写的是同一份文件内容;但它们经过的是两个 struct file,因此文件位置和打开状态各自独立。关闭 fd=3 不会清除 fd=4,也不会直接销毁 struct file B

把三种情况与 dup() 放在一起,就能看到共享边界:

操作 files_struct / fdtable struct file
普通线程创建 共享同一张表 自然看到相同表项
fork() 子进程复制出一张新表 父子对应槽位指向同一个 file
dup() 在同一张表中新建槽位 新旧槽位指向同一个 file
再次 open() 在同一张表中新建槽位 创建另一个 file

因此,"是不是同一张 fd 表"和"是不是同一个打开实例"是两个不同的问题。

三、ksys_write() 做的是一次查表

忽略并发保护和引用计数后,查找过程可以抽象成:

text 复制代码
file = 当前进程.打开文件表[fd]

如果这一项不存在:
    返回"无效文件描述符"
否则:
    用 file 完成写入

对照实际代码,ksys_write() 的主体并不长:

c 复制代码
struct fd f = fdget_pos(fd);
ssize_t ret = -EBADF;

if (fd_file(f)) {
    loff_t pos, *ppos = file_ppos(fd_file(f));
    if (ppos) {
        pos = *ppos;
        ppos = &pos;
    }
    ret = vfs_write(fd_file(f), buf, count, ppos);
    if (ret >= 0 && ppos)
        fd_file(f)->f_pos = pos;
    fdput_pos(f);
}

这段代码实际完成五件事:

  1. fdget_pos(fd) 根据 fd 取得一个安全使用的 struct file
  2. 如果没有找到,ret 保持为 -EBADF
  3. file_ppos() 判断这个对象是否有可更新的文件位置。
  4. vfs_write() 完成通用检查,再调用对象自己的写操作。
  5. 写入成功后更新位置,最后用 fdput_pos() 结束这次临时使用。

真正发生"查数组"的位置可以继续压缩成:

c 复制代码
files = current->files;
fdt = files->fdt;
file = fdt->fd[fd];

这三行是便于理解的等价表达,不是忽略边界就能直接放进内核的实现。真实代码还要检查 fd 是否超出 max_fds,使用 RCU 或引用计数防止并发释放,并在必要时保护共享的 f_pos

因此,源码中的 fdget_pos() 可以理解为一个安全的"查表并暂时持有"操作。它对应的正是上一节图中的:

text 复制代码
current -> files_struct -> fdtable -> fd[fd] -> struct file

这些保护很重要,但不改变主线:

ksys_write() 用 fd 在当前进程的表中找到 struct file

四、为什么同一个 write() 能写向不同对象

找到 struct file 后,内核仍然不能假设目标是磁盘文件。它可能是:

fd 关联的对象 写入大致流向
普通文件 文件系统和缓存层
终端 TTY 子系统
管道 管道缓冲区
socket 网络协议栈

struct file 中保存着一组操作入口。可以把它看成这个对象提供给 VFS 的"操作说明书":有人负责读,有人负责写,有人负责定位。

因此,写入过程可以概括为:

text 复制代码
write(fd, data)
      │
      ▼
根据 fd 找到 struct file
      │
      ▼
VFS 做通用检查
      │
      ▼
调用这个对象自己的写操作

VFS 负责统一部分,例如确认对象允许写入、检查用户缓冲区和写入范围。通过检查后,它再根据 struct file 中的操作入口,把请求交给具体对象。

至于最后调用的函数叫什么、内部还会经过多少层,是各个对象的实现细节,不影响理解 fd 到 struct file 的关系。

"万物皆文件"也应该在这个意义上理解:

许多不同的内核对象,都可以通过 fd 接入一组相似的系统调用接口。

它不表示所有对象都是磁盘文件,也不表示所有写入都会进入文件系统。

五、不是所有对象都有"当前偏移"

普通文件通常有明确的位置:

text 复制代码
第一次写 2 字节:位置 0 -> 2
第二次写 2 字节:位置 2 -> 4

这个位置属于打开实例,也就是 struct file,而不是 fd 这个整数本身。因此,两个 fd 如果共享同一个打开实例,也会共享文件位置。

管道和 socket 则是数据流。对它们来说,"现在写到第几个字节"没有同样的含义,也不能像普通文件那样随意 lseek()

ksys_write() 在写入前会区分这两类对象:

  • 有文件位置的对象:把当前位置交给写入过程,成功后更新它。
  • 流式对象:不传递可修改的文件位置。

这也是为什么同一个 write() 接口既能服务普通文件,也能服务管道和 socket。

六、一个容易误判的实验

下面是验证程序的完整代码:

程序先用 open()dup() 构造两种不同的共享关系,再依次观察路径、文件偏移、两类 flags 和 unlink() 后的状态。代码看起来较长,主要是因为 write_all()read_exact()die() 完整处理了短读写、信号中断和错误退出。

c 复制代码
#define _GNU_SOURCE

#include <errno.h>
#include <fcntl.h>
#include <limits.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>

static void die(const char *what)
{
    perror(what);
    exit(EXIT_FAILURE);
}

static void write_all(int fd, const void *buffer, size_t length)
{
    const char *p = buffer;

    while (length > 0) {
        ssize_t n = write(fd, p, length);

        if (n < 0) {
            if (errno == EINTR)
                continue;
            die("write");
        }
        p += n;
        length -= (size_t)n;
    }
}

static void read_exact(int fd, char *buffer, size_t length)
{
    size_t done = 0;

    while (done < length) {
        ssize_t n = read(fd, buffer + done, length - done);

        if (n < 0) {
            if (errno == EINTR)
                continue;
            die("read");
        }
        if (n == 0) {
            fprintf(stderr, "unexpected EOF\n");
            exit(EXIT_FAILURE);
        }
        done += (size_t)n;
    }
    buffer[length] = '\0';
}

static void show_fd_target(const char *label, int fd)
{
    char proc_path[64];
    char target[PATH_MAX];
    ssize_t n;

    snprintf(proc_path, sizeof(proc_path), "/proc/self/fd/%d", fd);
    n = readlink(proc_path, target, sizeof(target) - 1);
    if (n < 0)
        die("readlink");
    target[n] = '\0';
    printf("%-12s fd=%d -> %s\n", label, fd, target);
}

static long fdinfo_position(int fd)
{
    char path[64];
    char line[256];
    FILE *fp;
    long position = -1;

    snprintf(path, sizeof(path), "/proc/self/fdinfo/%d", fd);
    fp = fopen(path, "r");
    if (fp == NULL)
        die("fopen fdinfo");

    while (fgets(line, sizeof(line), fp) != NULL) {
        if (sscanf(line, "pos:\t%ld", &position) == 1)
            break;
    }

    if (fclose(fp) != 0)
        die("fclose fdinfo");
    return position;
}

int main(void)
{
    char path[] = "/tmp/fd-lifecycle-XXXXXX";
    char a[3];
    char b[3];
    char c[3];
    char all[11];
    int original;
    int duplicate;
    int independent;
    int original_fd_flags;
    int duplicate_fd_flags;
    int status_flags;

    original = mkstemp(path);
    if (original < 0)
        die("mkstemp");

    write_all(original, "ABCDEFGHIJ", 10);
    if (fsync(original) < 0)
        die("fsync");
    if (lseek(original, 0, SEEK_SET) < 0)
        die("lseek original");

    duplicate = dup(original);
    if (duplicate < 0)
        die("dup");

    independent = open(path, O_RDWR);
    if (independent < 0)
        die("independent open");

    printf("== descriptor targets ==\n");
    show_fd_target("original", original);
    show_fd_target("duplicate", duplicate);
    show_fd_target("independent", independent);

    read_exact(original, a, 2);
    read_exact(duplicate, b, 2);
    read_exact(independent, c, 2);

    printf("\n== shared and independent offsets ==\n");
    printf("read(original)    = %s\n", a);
    printf("read(duplicate)   = %s\n", b);
    printf("read(independent) = %s\n", c);
    printf("fdinfo positions: original=%ld duplicate=%ld independent=%ld\n",
           fdinfo_position(original),
           fdinfo_position(duplicate),
           fdinfo_position(independent));

    duplicate_fd_flags = fcntl(duplicate, F_GETFD);
    if (duplicate_fd_flags < 0)
        die("F_GETFD duplicate");
    if (fcntl(duplicate, F_SETFD, duplicate_fd_flags | FD_CLOEXEC) < 0)
        die("F_SETFD duplicate");

    original_fd_flags = fcntl(original, F_GETFD);
    duplicate_fd_flags = fcntl(duplicate, F_GETFD);
    if (original_fd_flags < 0 || duplicate_fd_flags < 0)
        die("F_GETFD");

    status_flags = fcntl(duplicate, F_GETFL);
    if (status_flags < 0)
        die("F_GETFL");
    if (fcntl(duplicate, F_SETFL, status_flags | O_APPEND) < 0)
        die("F_SETFL");

    printf("\n== per-fd flags versus shared file status flags ==\n");
    printf("FD_CLOEXEC: original=%d duplicate=%d\n",
           !!(original_fd_flags & FD_CLOEXEC),
           !!(duplicate_fd_flags & FD_CLOEXEC));
    printf("O_APPEND:   original=%d duplicate=%d\n",
           !!(fcntl(original, F_GETFL) & O_APPEND),
           !!(fcntl(duplicate, F_GETFL) & O_APPEND));

    if (unlink(path) < 0)
        die("unlink");

    printf("\n== open file survives unlink ==\n");
    show_fd_target("after unlink", original);
    if (lseek(independent, 0, SEEK_SET) < 0)
        die("lseek independent");
    read_exact(independent, all, 10);
    printf("content through still-open fd = %s\n", all);

    if (close(independent) < 0)
        die("close independent");
    if (close(duplicate) < 0)
        die("close duplicate");
    if (close(original) < 0)
        die("close original");

    return EXIT_SUCCESS;
}

6.1 三个 fd 是怎么创建出来的

实验的第一阶段先准备十个字节:

c 复制代码
original = mkstemp(path);
write_all(original, "ABCDEFGHIJ", 10);
fsync(original);
lseek(original, 0, SEEK_SET);

mkstemp() 创建一个临时文件并返回第一个 fd,这里把它命名为 original。写入完成后,文件位置已经到达末尾;lseek() 再把它移回 0,为后面的读取实验准备相同起点。

接下来是整个实验最关键的两行:

c 复制代码
duplicate = dup(original);
independent = open(path, O_RDWR);

它们看起来都产生了一个新 fd,但内核对象关系不同:

  • dup(original) 只分配一个新 fd 槽位,让它指向 original 已经使用的那个 struct file
  • open(path, O_RDWR) 重新执行一次打开,创建另一个 struct file;它与前一个打开实例最终关联同一个 inode。
flowchart LR subgraph FDT[&#34;同一进程的 fdtable&#34;] O[&#34;fd[original]<br/>一个 fd 槽位&#34;] D[&#34;fd[duplicate]<br/>另一个 fd 槽位&#34;] I[&#34;fd[independent]<br/>第三个 fd 槽位&#34;] end O --> FILEA[&#34;struct file A<br/>共享的打开实例&#34;] D --> FILEA I --> FILEB[&#34;struct file B<br/>独立的打开实例&#34;] FILEA --> INODE[&#34;同一个 inode<br/>同一份文件内容&#34;] FILEB --> INODE

这张图表达了两个不同层次的"相同":

  • originalduplicate 指向同一个 struct file,所以共享打开状态。
  • independent 只是在更下层关联同一个 inode,它有自己的打开状态。

6.2 /proc/self/fd 只能看到目标名字

show_fd_target() 把 fd 拼成 /proc/self/fd/N,再用 readlink() 读取这个符号链接的目标。fd 的具体数值由进程当时的打开文件表决定,某次运行的结果是:

text 复制代码
== descriptor targets ==
original     fd=9  -> /tmp/fd-lifecycle-...
duplicate    fd=10 -> /tmp/fd-lifecycle-...
independent  fd=11 -> /tmp/fd-lifecycle-...

三行显示为同一路径,只能说明它们最终关联到同一个文件名字,不能证明它们指向同一个 struct file/proc/self/fd/N 展示的是 fd 当前可以访问的对象,不会直接打印打开实例的内核地址。

6.3 连续读取让共享偏移暴露出来

程序按顺序执行:

c 复制代码
read_exact(original, a, 2);
read_exact(duplicate, b, 2);
read_exact(independent, c, 2);

实际输出是:

text 复制代码
read(original)    = AB
read(duplicate)   = CD
read(independent) = AB
fdinfo positions: original=4 duplicate=4 independent=2

逐步看文件位置的变化:

text 复制代码
struct file A:original 从位置 0 读出 AB,位置变为 2
struct file A:duplicate 从共享的位置 2 读出 CD,位置变为 4
struct file B:independent 从自己的位置 0 读出 AB,位置变为 2

fdinfo_position() 读取 /proc/self/fdinfo/N 中的 pos 字段。结果中 originalduplicate 都显示 4,是因为两个槽位最终读取的是同一个 struct file->f_posindependent 对应另一个 struct file->f_pos,所以是 2

6.4 两类 flags 分别属于表项和打开实例

程序随后只对 duplicate 设置 FD_CLOEXEC

c 复制代码
fcntl(duplicate, F_SETFD, duplicate_fd_flags | FD_CLOEXEC);

输出是:

text 复制代码
FD_CLOEXEC: original=0 duplicate=1

FD_CLOEXEC 属于 fd 槽位,对应 fdtable->close_on_exec 位图中的一位。因此,修改 duplicate 的这一位不会改变 original 槽位。

然后,程序仍然通过 duplicate 设置 O_APPEND

c 复制代码
fcntl(duplicate, F_SETFL, status_flags | O_APPEND);

这次两个 fd 都能观察到变化:

text 复制代码
O_APPEND:   original=1 duplicate=1

O_APPEND 属于打开实例,保存在共享的 struct file->f_flags 中。虽然操作入口是 duplicate,真正被修改的却是 originalduplicate 共同指向的对象。

把偏移和 flags 放回前面的关系图中,就能看清状态到底存在哪里:

text 复制代码
fd[original]  ─┐
               ├─> struct file A
fd[duplicate] ─┘      ├─ f_pos = 4
  各自拥有             └─ f_flags 包含 O_APPEND
  FD_CLOEXEC 位

fd[independent] ──> struct file B
                         └─ f_pos = 2

6.5 unlink() 删除名字,不会立即销毁打开实例

最后,程序执行 unlink(path) 删除目录中的名字,再通过仍然打开的 fd 访问文件:

text 复制代码
after unlink fd=9 -> /tmp/fd-lifecycle-... (deleted)
content through still-open fd = ABCDEFGHIJ

(deleted) 表示路径名已经被删除;内容仍然可读,说明 fd 对应的 struct file 以及它引用的底层对象仍然存在。程序末尾依次 close() 三个 fd,清除表中的槽位并减少打开引用;最后一个相关引用消失后,内核才有条件释放对应的打开实例。

这个实验从用户空间验证了共享语义,但不能直接打印 struct file * 的内核地址。它的价值不是"看见指针",而是通过偏移和 flags 的变化,反推出这些状态分别属于 fd 槽位还是打开实例。

七、最后把概念串起来

当用户调用:

c 复制代码
write(3, "hello", 5);

内核完成的核心工作可以压缩成四步:

  1. 在当前进程的打开文件表中查找第 3 项。
  2. 取得这一项关联的 struct file
  3. 完成所有对象都需要的通用写入检查。
  4. 根据这个打开实例提供的操作入口,把数据交给具体对象。

所以:

  • fd 解决"在当前进程的表里选哪一项"。
  • struct file 解决"这次打开保存了什么状态"。
  • 对象的操作入口解决"这次写入最终交给谁"。

源码中的 fdget_pos()vfs_write() 等函数,是这套模型的具体实现,而不是理解它的前提。

下一篇:struct file 不是磁盘文件会把打开实例的共享规则继续展开,系统区分 dup()fork()、线程共享、独立 open()close() 分别改变了哪一层。

相关推荐
liang_jy13 小时前
文件管理(四)—— 文件存储空间管理
面试·操作系统
liang_jy14 小时前
文件管理(三)—— 文件目录
面试·操作系统
小宇子2B1 天前
shrinker:页 LRU 之外的 VFS(虚拟文件系统)缓存怎么回收
操作系统
liang_jy2 天前
文件管理(一)—— 初识文件管理
面试·操作系统
liang_jy2 天前
文件管理(二)—— 文件结构
面试·操作系统
liang_jy3 天前
内存管理(七)—— 内存映射
面试·操作系统
liang_jy3 天前
内存管理(六)—— 进程的内存映像
面试·操作系统
纵有疾風起3 天前
处理机调度算法深度对比:FCFS到多级反馈队列
操作系统·进程与线程·调度算法·408考研·处理机调度·fcfs·sjf
liang_jy3 天前
内存管理(四)—— 传统物理内存非连续分配管理方式
面试·操作系统