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_struct、fdtable 与 struct file
进程并不是把 fd 数组直接放在 task_struct 里。task_struct 是 Linux task 的核心描述符,它与线程、调度、内核栈、mm_struct 和 current 的关系,已经在《线程和进程的完整内存布局: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 时首先沿这个指针走 |
fdtab、fd_array 和三个 *_init 位图 |
内嵌的小表和配套存储,fd 数量较少时避免额外分配 |
file_lock、resize_in_progress、resize_wait |
协调安装、关闭和扩容等修改操作 |
next_fd |
下次寻找空闲 fd 时的起始提示,减少从头扫描 |
fdt 和 fdtab 很容易混淆:
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_fds 和 close_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 |
read、write、poll、mmap 等操作交给谁 |
f_pos、f_pos_lock |
当前读写位置是什么,共享时如何保护 |
f_flags |
是否设置 O_APPEND、O_NONBLOCK 等打开状态 |
f_path、f_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_pos 和 f_flags。dup() 则只增加一个 fd 槽位:
text
fd=3 ─┐
├─> 同一个 struct file
fd=7 ─┘
因此两个 fd 会共享 f_pos 和 f_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(),观察每一层怎样变化。
先看一张全景图。它表示下面这组操作完成后的状态:
- 父进程 P 打开
data.txt得到fd=3。 dup(3)得到fd=7,两个槽位共享struct file A;随后只给fd=7设置FD_CLOEXEC。- 再次
open("data.txt")得到fd=4,创建独立的struct file B。 - P 创建线程 T2,T1 和 T2 共享
files_struct P。 - T1 调用
fork()创建子进程 C;C 得到复制的 fdtable,但槽位仍指向 A、B。 - C 随后创建线程 T4,因此 T3 和 T4 共享
files_struct C。 - 另一个独立进程 Q 的
fd=3指向 socket,与 P、C 的整数3无关。

读这张图时,先看箭头是否汇合:
- T1、T2 的箭头汇合到
files_struct P,表示两个线程共享整张表。 - P 和 C 的
files_struct没有汇合,表示fork()后父子各自修改自己的 fdtable。 P3、P7、C3、C7汇合到struct file A,表示同一打开实例同时被多个槽位、多个进程持有。struct file A和struct 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_pos 和 f_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);
}
这段代码实际完成五件事:
fdget_pos(fd)根据 fd 取得一个安全使用的struct file。- 如果没有找到,
ret保持为-EBADF。 file_ppos()判断这个对象是否有可更新的文件位置。vfs_write()完成通用检查,再调用对象自己的写操作。- 写入成功后更新位置,最后用
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。
这张图表达了两个不同层次的"相同":
original和duplicate指向同一个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 字段。结果中 original 和 duplicate 都显示 4,是因为两个槽位最终读取的是同一个 struct file->f_pos;independent 对应另一个 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,真正被修改的却是 original 和 duplicate 共同指向的对象。
把偏移和 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);
内核完成的核心工作可以压缩成四步:
- 在当前进程的打开文件表中查找第
3项。 - 取得这一项关联的
struct file。 - 完成所有对象都需要的通用写入检查。
- 根据这个打开实例提供的操作入口,把数据交给具体对象。
所以:
- fd 解决"在当前进程的表里选哪一项"。
struct file解决"这次打开保存了什么状态"。- 对象的操作入口解决"这次写入最终交给谁"。
源码中的 fdget_pos()、vfs_write() 等函数,是这套模型的具体实现,而不是理解它的前提。
下一篇:struct file 不是磁盘文件会把打开实例的共享规则继续展开,系统区分 dup()、fork()、线程共享、独立 open() 和 close() 分别改变了哪一层。