Unix 哲学”一切皆文件”——Linux 更进一步:把信号、事件、定时器、进程都变成 fd,统一用 epoll/io_uring 等待。
一、先看图
flowchart TD
EPOLL[epoll / io_uring]
EPOLL --- SFD[signalfd<br/>信号 → fd]
EPOLL --- EFD[eventfd<br/>计数器 → fd]
EPOLL --- TFD[timerfd<br/>定时器 → fd]
EPOLL --- PFD[pidfd<br/>进程 → fd]
EPOLL --- SOCK[socket fd]
EPOLL --- FILE[文件 fd]
classDef center fill:#388bfd22,stroke:#388bfd,color:#adbac7;
classDef leaf fill:#3fb95022,stroke:#3fb950,color:#adbac7;
class EPOLL center
class SFD,EFD,TFD,PFD,SOCK,FILE leaf
二、signalfd
2.1 问题
信号打断 epoll_wait → 处理复杂、不安全。
2.2 方案
sigset_t mask;
sigemptyset(&mask);
sigaddset(&mask, SIGINT);
sigaddset(&mask, SIGTERM);
sigprocmask(SIG_BLOCK, &mask, NULL); // 阻塞传统信号
int sfd = signalfd(-1, &mask, SFD_NONBLOCK | SFD_CLOEXEC);
// 加入 epoll → read(sfd) 读取 struct signalfd_siginfo信号变成可 read 的事件 → 和 socket 统一处理。
2.3 内核实现
signalfd_poll() 检查 pending signal → 加入
epoll 就绪。
三、eventfd
3.1 用途
进程/线程间的简单通知——内核维护一个 64 位计数器。
int efd = eventfd(0, EFD_NONBLOCK | EFD_CLOEXEC);
// 通知方:
uint64_t val = 1;
write(efd, &val, sizeof(val)); // 计数器 += val
// 等待方:
uint64_t val;
read(efd, &val, sizeof(val)); // 读出计数器并清零3.2 EFD_SEMAPHORE
int efd = eventfd(0, EFD_SEMAPHORE);
// read 每次返回 1,计数器减 13.3 KVM 的 eventfd
KVM 用 eventfd 连接 guest 中断和 host 事件:
KVM_IRQFD:guest 中断 → eventfd → host epollKVM_IOEVENTFD:guest MMIO 写 → eventfd → host 处理
四、timerfd
4.1 问题
setitimer / timer_create
用信号通知 → 不安全、不可组合。
4.2 方案
int tfd = timerfd_create(CLOCK_MONOTONIC, TFD_NONBLOCK | TFD_CLOEXEC);
struct itimerspec its = {
.it_value = { .tv_sec = 1 },
.it_interval = { .tv_sec = 1 },
};
timerfd_settime(tfd, 0, &its, NULL);
// 加入 epoll → read(tfd) 返回超时次数4.3 精度
底层用 hrtimer → 纳秒精度(取决于硬件)。
五、pidfd(5.3+)
5.1 问题
waitpid(pid) 有 PID 重用竞争——进程退出后 PID
可能被新进程占用。
5.2 方案
int pidfd = pidfd_open(child_pid, 0);
// 加入 epoll → 子进程退出时就绪
// 或者
pidfd_send_signal(pidfd, SIGTERM, NULL, 0); // 不怕 PID 重用5.3 systemd 使用
systemd 254+ 用 pidfd 管理服务进程生命周期。
六、userfaultfd
int uffd = userfaultfd(O_NONBLOCK | O_CLOEXEC);
// 注册地址范围 → page fault 时通知到 uffd → 用户态处理用于:
- 实时迁移(QEMU postcopy)
- 用户态内存管理
七、fd 化哲学
7.1 为什么把一切变成 fd
- 统一等待:一个 epoll 等待所有类型的事件
- 生命周期管理:close() 自动清理
- 权限传递:SCM_RIGHTS 通过 socket 传 fd
- 可 fork 继承:子进程继承 fd
7.2 代价
- 每个 fd 消耗内核资源(struct file + 私有数据)
- fd 数量有限(
RLIMIT_NOFILE) - 不是所有抽象都适合 fd(如 futex)
八、统一事件循环模式
int epfd = epoll_create1(EPOLL_CLOEXEC);
epoll_ctl(epfd, EPOLL_CTL_ADD, listen_fd, &ev_listen);
epoll_ctl(epfd, EPOLL_CTL_ADD, signalfd, &ev_signal);
epoll_ctl(epfd, EPOLL_CTL_ADD, timerfd, &ev_timer);
epoll_ctl(epfd, EPOLL_CTL_ADD, eventfd, &ev_notify);
while (1) {
int n = epoll_wait(epfd, events, MAX, -1);
for (int i = 0; i < n; i++) {
dispatch(events[i]);
}
}网络连接、信号、定时、自定义事件 → 一个循环处理全部。
九、观察
ls -la /proc/<pid>/fd # 查看 fd 类型
cat /proc/<pid>/fdinfo/<fd> # fd 详情
lsof -p <pid> # anon_inode:[eventfd] 等
# 统计系统 fd 使用
cat /proc/sys/fs/file-nr # 已分配 / 未用 / 上限十、小结
- signalfd:信号变 fd → 可 epoll
- eventfd:轻量计数器 → 线程/KVM 通知
- timerfd:定时器变 fd → 替代 setitimer
- pidfd:进程变 fd → 无 PID 重用风险
- fd 化让所有事件统一在一个事件循环处理
参考文献
man 2 signalfd、man 2 eventfd、man 2 timerfd_create、man 2 pidfd_openfs/signalfd.c、fs/eventfd.c、fs/timerfd.c- Michael Kerrisk, “The Linux Programming Interface.” ch22, ch63
工具
lsofstrace -e signalfd4,eventfd2,timerfd_create,pidfd_open/proc/<pid>/fdinfo/
上一篇:select/poll 下一篇:splice/tee/vmsplice
同主题继续阅读
把当前热点继续串成多页阅读,而不是停在单篇消费。
【操作系统百科】信号:Unix 最拧巴的抽象
signal 在 Unix 里几乎等同「异步打断」,但它的 API 踩满雷:不可重入的 handler、ASS-safe 函数清单、SIGCHLD 丢失、多线程语义、SIG_DFL 的历史包袱。本文讲 kill/tgkill/rt_sigaction、signalfd、pidfd_send_signal、async-signal-safe 的真实边界,以及为什么新代码应该尽量把 signal 转成 fd。
【操作系统百科】fd 表与 struct file
fd 是用户态访问文件的句柄——但 fd 的共享语义在 fork/exec/dup/CLOEXEC 下极其微妙。本文讲 files_struct、fdtable、close_range、pidfd_getfd、cloexec 默认化趋势。
【操作系统百科】进程生命周期:clone → exec → exit → reap
一个进程从诞生到尸体被回收,在内核里走过六个阶段:clone → run → exec(可选)→ exit → zombie → release。本文按阶段讲 do_fork、bprm_execve、do_exit、release_task,以及 waitpid/pidfd/subreaper 的收尸规则、孤儿与僵尸的语义、systemd PID 1 的特殊性。
【操作系统百科】内存回收
Linux 内存回收是 VM 最复杂的子系统之一。本文讲 active/inactive LRU、kswapd 与 direct reclaim、watermark 三线、swappiness 的真实含义、MGLRU 改造、memcg 回收与 PSI。