用户态只看到 int fd;内核用 files_struct →
fdtable → struct 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;
};- fork 不带 CLONE_FILES → 复制一份 files_struct(子进程有独立 fd 表)
- clone 带 CLONE_FILES → 共享同一个 files_struct(线程组内共享 fd 表)
三、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:
pipe2(fds, O_CLOEXEC)accept4(sockfd, &addr, &addrlen, SOCK_CLOEXEC)epoll_create1(EPOLL_CLOEXEC)signalfd4,eventfd2,timerfd_create均支持
五、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;
};- open → f_count = 1
- dup / fork → f_count++
- close → f_count– → =0 时
fput()真正释放
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十一、小结
- fd 是 fdtable 数组的下标 → struct file → dentry → inode
- fork 复制 fd 表但共享 struct file(共享 offset)
- O_CLOEXEC 是安全默认
- close_range 批量关 fd
- pidfd_getfd 跨进程传 fd
参考文献
include/linux/fdtable.h、fs/file.cDocumentation/filesystems/files.rst- Christian Brauner, “close_range().” LWN.net 2020
man 2 openat2、man 2 pidfd_getfd
工具
/proc/$$/fd/、/proc/$$/fdinfo/lsof -p $$ls -la /proc/$$/fdstrace -e close,openat,dup2
同主题继续阅读
把当前热点继续串成多页阅读,而不是停在单篇消费。
【操作系统百科】fd 化抽象
Linux 把信号、事件、定时器、进程都变成 fd——signalfd/eventfd/timerfd/pidfd。本文讲每种 fd 的用途、与 epoll 组合、KVM 的 eventfd、systemd 的 pidfd、以及 fd 化哲学。
【操作系统百科】进程生命周期:clone → exec → exit → reap
一个进程从诞生到尸体被回收,在内核里走过六个阶段:clone → run → exec(可选)→ exit → zombie → release。本文按阶段讲 do_fork、bprm_execve、do_exit、release_task,以及 waitpid/pidfd/subreaper 的收尸规则、孤儿与僵尸的语义、systemd PID 1 的特殊性。
【操作系统百科】信号:Unix 最拧巴的抽象
signal 在 Unix 里几乎等同「异步打断」,但它的 API 踩满雷:不可重入的 handler、ASS-safe 函数清单、SIGCHLD 丢失、多线程语义、SIG_DFL 的历史包袱。本文讲 kill/tgkill/rt_sigaction、signalfd、pidfd_send_signal、async-signal-safe 的真实边界,以及为什么新代码应该尽量把 signal 转成 fd。
【操作系统百科】内存回收
Linux 内存回收是 VM 最复杂的子系统之一。本文讲 active/inactive LRU、kswapd 与 direct reclaim、watermark 三线、swappiness 的真实含义、MGLRU 改造、memcg 回收与 PSI。