土法炼钢兴趣小组的算法知识备份

【操作系统百科】fd 表与 struct file

文章导航

分类入口
os
标签入口
#fd#files-struct#cloexec#pidfd#close-range

目录

用户态只看到 int fd;内核用 files_structfdtablestruct file 三层把 fd 映射到打开文件。fork、exec、dup、close 的语义都在这三层上演。

一、先看图

flowchart TD
    PROC[进程 task_struct] --> FS[files_struct]
    FS --> FDT[fdtable<br/>fd → file* 数组]
    FDT -->|fd=0| F0[struct file: stdin]
    FDT -->|fd=3| F3[struct file: socket]
    FDT -->|fd=4| F4[struct file: /tmp/data]
    F0 --> DENTRY0[dentry → inode]
    F3 --> SOCK[socket]
    F4 --> DENTRY4[dentry → inode]
    classDef proc fill:#388bfd22,stroke:#388bfd,color:#adbac7;
    classDef file fill:#3fb95022,stroke:#3fb950,color:#adbac7;
    class PROC,FS,FDT proc
    class F0,F3,F4 file

二、files_struct

struct files_struct {
    atomic_t count;             // 引用计数(clone CLONE_FILES 共享)
    struct fdtable __rcu *fdt;
    struct fdtable fdtab;       // 内联小表(初始 64 个 fd)
    spinlock_t file_lock;
    unsigned int next_fd;       // 下一个分配的 fd 号
    struct embedded_fd_set close_on_exec_init;
    struct embedded_fd_set open_fds_init;
};

三、fdtable

struct fdtable {
    unsigned int max_fds;       // 当前容量
    struct file __rcu **fd;     // fd → struct file 指针数组
    unsigned long *close_on_exec; // bitmap
    unsigned long *open_fds;      // bitmap
};

fd 号就是数组下标。分配新 fd 时找 open_fds 中最小的空位。

扩容:初始 64 → 256 → 1024 → … RCU 安全替换。

四、fork/exec 的 fd 语义

4.1 fork

子进程获得 fd 表的副本——fd 号相同、指向同一个 struct file(引用计数+1)。

→ 父子共享 file offset(同一 f_pos)→ 交替 write 会交错。

4.2 exec

exec 关闭所有 close_on_exec 位为 1 的 fd。

int fd = open("/tmp/data", O_RDWR | O_CLOEXEC);  // exec 时自动关

4.3 O_CLOEXEC 默认化趋势

不设 O_CLOEXEC → exec 后 fd 泄漏到子进程 → 安全隐患。

现代 API 默认 CLOEXEC:

五、dup / dup2 / dup3

int new_fd = dup(old_fd);           // 最小空闲 fd
int new_fd = dup2(old_fd, target);  // 强制复制到 target
int new_fd = dup3(old_fd, target, O_CLOEXEC);

dup 后两个 fd 指向同一个 struct file → 共享 offset、flags。

六、close_range(5.9+)

close_range(3, ~0U, CLOSE_RANGE_CLOEXEC);
// 把 fd 3 到最大全部标记 CLOEXEC

close_range(3, ~0U, 0);
// 直接关掉 fd 3 到最大

一次批量操作——比循环 close 快几个数量级。systemd、Go runtime 已采用。

七、pidfd_getfd(5.6+)

int pidfd = pidfd_open(target_pid, 0);
int stolen_fd = pidfd_getfd(pidfd, target_fd, 0);

跨进程”偷” fd——等价于 SCM_RIGHTS 但更安全(不需要 socket)。

调试/监控进程的 fd 不再需要 /proc/pid/fd/N + open。

八、file 引用计数

struct file {
    atomic_long_t f_count;
};

fput 实际在 task_work 里延迟执行——确保 RCU 宽限期。

九、RLIMIT_NOFILE

ulimit -n         # 当前进程最大 fd 数
cat /proc/sys/fs/nr_open   # 系统硬上限(默认 1048576)
cat /proc/sys/fs/file-max  # 系统级 struct file 上限
cat /proc/sys/fs/file-nr   # 已分配 / 空闲 / 上限

常见生产问题:Too many open files → 调 ulimit 或 systemd LimitNOFILE。

十、观察

ls -la /proc/$$/fd/         # 看进程打开的 fd
cat /proc/$$/fdinfo/3       # fd 3 的详细信息(pos、flags、mnt_id)

# 系统全局
cat /proc/sys/fs/file-nr

# 每进程
ls /proc/$$/fd | wc -l

十一、小结


参考文献

工具


上一篇路径名解析 下一篇文件锁

同主题继续阅读

把当前热点继续串成多页阅读,而不是停在单篇消费。

2026-05-21 · os

【操作系统百科】fd 化抽象

Linux 把信号、事件、定时器、进程都变成 fd——signalfd/eventfd/timerfd/pidfd。本文讲每种 fd 的用途、与 epoll 组合、KVM 的 eventfd、systemd 的 pidfd、以及 fd 化哲学。

2026-04-17 · os

【操作系统百科】进程生命周期:clone → exec → exit → reap

一个进程从诞生到尸体被回收,在内核里走过六个阶段:clone → run → exec(可选)→ exit → zombie → release。本文按阶段讲 do_fork、bprm_execve、do_exit、release_task,以及 waitpid/pidfd/subreaper 的收尸规则、孤儿与僵尸的语义、systemd PID 1 的特殊性。

2026-04-17 · os

【操作系统百科】信号:Unix 最拧巴的抽象

signal 在 Unix 里几乎等同「异步打断」,但它的 API 踩满雷:不可重入的 handler、ASS-safe 函数清单、SIGCHLD 丢失、多线程语义、SIG_DFL 的历史包袱。本文讲 kill/tgkill/rt_sigaction、signalfd、pidfd_send_signal、async-signal-safe 的真实边界,以及为什么新代码应该尽量把 signal 转成 fd。

2026-04-27 · os

【操作系统百科】内存回收

Linux 内存回收是 VM 最复杂的子系统之一。本文讲 active/inactive LRU、kswapd 与 direct reclaim、watermark 三线、swappiness 的真实含义、MGLRU 改造、memcg 回收与 PSI。


By .