恒美微站
首页
关于我们
建站服务
主题模板
案例展示
资讯中心
联系我们
linux系统编程(十):eventfd-timerfd-signalfd
首页
资讯中心
/
linux系统编程(十):eventfd-timerfd-signalfd
linux系统编程(十):eventfd-timerfd-signalfd
发布时间:2026/9/1 7:15:30
eventfd / timerfd / signalfd —— 一切皆文件的现代演绎Linux 把任意事件都做成 fd —— 计数器变 fdeventfd定时器变 fdtimerfd信号变 fdsignalfd。这些 fd 都能塞进 epoll跟普通 socket / pipe 一起处理。这就是现代事件循环libuv / libev / Boost.Asio的标准玩法。0. 引言epoll 时代的统一事件源老 Unix 时代每种事件用不同 API事件类型老的处理方式网络数据select等 socket fd定时器alarmSIGALRM信号signalhandler进程内通知管道 / 条件变量问题异步信号安全限制、handler 不能复杂、跟 epoll 不能融合。Linux 内核给出新答案把所有事件都做成 fd一个 epoll 全包。事件源老办法新办法进程内通知pipe占两个 fdeventfd一个 fd定时器alarm SIGALRMtimerfd信号signal handlersignalfd进程退出SIGCHLD waitpidpidfd5.3文件变化inotify_initinotify_init1下面三个最常用的 fd 化事件源。1. eventfd —— 进程内通知一下#includesys/eventfd.hinteventfd(unsignedintinitval,intflags);/* flags: EFD_NONBLOCK | EFD_CLOEXEC | EFD_SEMAPHORE */返回一个 fd内部维护一个 64 位无符号计数器。1.1 默认模式计数器累加intefdeventfd(0,EFD_NONBLOCK|EFD_CLOEXEC);/* 写累加到计数器 */uint64_tv1;write(efd,v,sizeofv);/* 计数 1 */write(efd,v,sizeofv);/* 计数 2 *//* 读把累计计数取走重置为 0 */uint64_tgot;read(efd,got,sizeofgot);/* got 2计数 0 */write 和 read必须传 8 字节。1.2 信号量模式EFD_SEMAPHOREintefdeventfd(0,EFD_NONBLOCK|EFD_CLOEXEC|EFD_SEMAPHORE);/* write 跟之前一样累加 */uint64_tv3;write(efd,v,sizeofv);/* 计数 3 *//* read 每次只减 1 */uint64_tgot;read(efd,got,sizeofgot);/* got 1计数 2 */read(efd,got,sizeofgot);/* got 1计数 1 */read(efd,got,sizeofgot);/* got 1计数 0 */read(efd,got,sizeofgot);/* 阻塞 / EAGAIN */像信号量。1.3 用途替代 self-pipe trick老技巧写 pipe 通知主线程退出循环/* 老 pipe 做法占两个 fd */intp[2];pipe(p);fcntl(p[0],F_SETFL,O_NONBLOCK);/* 信号 handler 里 */write(p[1],x,1);/* 主循环 */epoll_ctl(ep,EPOLL_CTL_ADD,p[0],ev);eventfd 一个 fd 搞定intefdeventfd(0,EFD_NONBLOCK|EFD_CLOEXEC);/* 通知 */uint64_tv1;write(efd,v,sizeofv);/* 主循环 */epoll_ctl(ep,EPOLL_CTL_ADD,efd,ev);跨线程通知、跨进程通知fork 后共享都常用。1.4 经典场景场景怎么用 eventfd线程池boss 线程把任务放队列后写 eventfdworker 主循环epoll_wait醒来处理优雅退出信号 handler 写 eventfd主循环 epoll_wait 醒来知道该停了跨进程通知fork 前创建 eventfd父子都能用2. timerfd —— 定时器变成 fd#includesys/timerfd.hinttimerfd_create(intclockid,intflags);/* clockid: CLOCK_REALTIME / CLOCK_MONOTONIC / CLOCK_BOOTTIME ... * flags: TFD_NONBLOCK | TFD_CLOEXEC */inttimerfd_settime(intfd,intflags,conststructitimerspec*new_value,structitimerspec*old_value);structitimerspec{structtimespecit_interval;/* 周期0 单次定时器 */structtimespecit_value;/* 第一次触发时刻 */};2.1 单次定时器3 秒后触发一次inttfdtimerfd_create(CLOCK_MONOTONIC,TFD_NONBLOCK|TFD_CLOEXEC);structitimerspecspec{.it_value{.tv_sec3,.tv_nsec0},.it_interval{0}/* 0 不重复 */};timerfd_settime(tfd,0,spec,NULL);/* 加进 epoll */structepoll_eventev{.eventsEPOLLIN,.data.fdtfd};epoll_ctl(ep,EPOLL_CTL_ADD,tfd,ev);/* 触发后 read 拿到 1 (过期次数) */uint64_texpirations;read(tfd,expirations,sizeofexpirations);2.2 周期定时器每秒触发structitimerspecspec{.it_value{.tv_sec1},/* 1 秒后第一次 */.it_interval{.tv_sec1}/* 之后每 1 秒一次 */};timerfd_settime(tfd,0,spec,NULL);2.3 取消定时器structitimerspecstop{0};/* 全 0 表示停止 */timerfd_settime(tfd,0,stop,NULL);2.4 跟 epoll 配合的经典场景while(1){intnepoll_wait(ep,evs,MAX,-1);for(inti0;in;i){if(evs[i].data.fdtfd){uint64_texp;read(tfd,exp,sizeofexp);handle_timer();/* 定时任务 */}else{/* 其他 fd */}}}跟 alarm SIGALRM 比timerfd 的优势维度timerfd 的改进信号路径不走信号async-signal-safe 限制无关多 timer 共存多个 timer 互不干扰alarm全进程共一个时钟选择可用CLOCK_MONOTONIC不受系统时间跳变影响跟 epoll 集成天然作为 fd跟其他 I/O 平等处理3. signalfd —— 信号变 fd#includesys/signalfd.hintsignalfd(intfd,constsigset_t*mask,intflags);/* flags: SFD_NONBLOCK | SFD_CLOEXEC */3.1 用法sigset_tmask;sigemptyset(mask);sigaddset(mask,SIGINT);sigaddset(mask,SIGTERM);/* 必须先屏蔽这些信号否则还会走默认/handler 路径 */pthread_sigmask(SIG_BLOCK,mask,NULL);intsfdsignalfd(-1,mask,SFD_NONBLOCK|SFD_CLOEXEC);/* 加进 epoll */structepoll_eventev{.eventsEPOLLIN,.data.fdsfd};epoll_ctl(ep,EPOLL_CTL_ADD,sfd,ev);/* 主循环 */while(1){epoll_wait(...);if(evs[i].data.fdsfd){structsignalfd_siginfosi;while(read(sfd,si,sizeofsi)sizeofsi){printf(signal %d arrived\n,si.ssi_signo);if(si.ssi_signoSIGTERM)gotoout;}}}out:3.2 signalfd 解决了 signal handler 的痛点痛点signalfd 的改进handler 受 async-signal-safe 限制read 之后走主循环能printf/malloc/ 调用任何函数多信号到达可能丢失内核排队read一次拿一个siginfo不丢跟 epoll 集成困难信号变 fd跟其他 fd 平等处理4. 三者对比特性eventfdtimerfdsignalfd触发方式显式 write时间到期信号到达read 拿到64 位计数器过期次数siginfo_t典型场景线程间唤醒定时任务优雅退出不用时怎么办self-pipealarm/setitimersignal handler5. 三个一起用完整事件循环骨架#includesys/epoll.h#includesys/eventfd.h#includesys/timerfd.h#includesys/signalfd.h#includesignal.h#includeunistd.h#includestdio.hintmain(void){/* 1. 屏蔽信号 → 走 signalfd */sigset_tmask;sigemptyset(mask);sigaddset(mask,SIGINT);sigaddset(mask,SIGTERM);pthread_sigmask(SIG_BLOCK,mask,NULL);intsfdsignalfd(-1,mask,SFD_NONBLOCK|SFD_CLOEXEC);/* 2. 1秒周期 timer */inttfdtimerfd_create(CLOCK_MONOTONIC,TFD_NONBLOCK|TFD_CLOEXEC);structitimerspecspec{{1,0},{1,0}};timerfd_settime(tfd,0,spec,NULL);/* 3. 任务通知 */intefdeventfd(0,EFD_NONBLOCK|EFD_CLOEXEC);/* 4. epoll 把它们都加进来 */intepepoll_create1(EPOLL_CLOEXEC);structepoll_eventev;ev.eventsEPOLLIN;ev.data.fdsfd;epoll_ctl(ep,EPOLL_CTL_ADD,sfd,ev);ev.data.fdtfd;epoll_ctl(ep,EPOLL_CTL_ADD,tfd,ev);ev.data.fdefd;epoll_ctl(ep,EPOLL_CTL_ADD,efd,ev);/* 5. 主循环 */structepoll_eventevs[16];intrunning1;while(running){intnepoll_wait(ep,evs,16,-1);for(inti0;in;i){intfdevs[i].data.fd;if(fdsfd){structsignalfd_siginfosi;read(sfd,si,sizeofsi);printf(signal %d → exit\n,si.ssi_signo);running0;}elseif(fdtfd){uint64_texp;read(tfd,exp,sizeofexp);printf(tick\n);}elseif(fdefd){uint64_tv;read(efd,v,sizeofv);printf(got notify x%lu\n,v);}}}close(sfd);close(tfd);close(efd);close(ep);return0;}50 行做出一个能周期任务 接信号优雅退出 接受外部通知的事件循环。现代框架libuv / libevent / Asio的内核就是这个套路。6. 一些细节坑6.1 eventfd 必须读写 8 字节charc1;write(efd,c,1);/* 错误必须 8 字节 */6.2 signalfd 之前必须屏蔽信号不屏蔽信号会走默认路径或注册的 handlersignalfd 拿不到。6.3 timerfd 的 CLOCK_REALTIME 会被时间跳变影响服务器调时间或 NTP 跳变CLOCK_REALTIME 的 timerfd 可能立刻过期或永远不到期。做超时控制用 CLOCK_MONOTONIC。6.4 epoll 拿到事件后必须 read否则 LT 模式下死循环read 出来才会清除就绪状态。否则 epoll_wait 一直返回这个 fd。6.5 read signalfd 拿不全多个挂起信号每次只拿sizeof(signalfd_siginfo)。要循环 read 直到 EAGAINwhile(read(sfd,si,sizeofsi)sizeofsi){handle(si);}7. 总结函数作用替代什么eventfd(0, flags)进程内/跨进程通知self-pipe tricktimerfd_create settime单次/周期定时器alarm / setitimersignalfd信号 fd 化signal handler三者放一起的精髓任意事件源都能变成 fd → 全部塞进 epoll → 单线程统一处理。这就是 Linux 现代事件循环的根基理解之后再看 libuv / libevent / Boost.Asio / nginx / Redis 的网络层会发现底层都是这套架构。