Linux高性能编程:timerfd+epoll实现单线程海量定时器与I/O事件管理
2026/8/3 18:47:16 网站建设 项目流程

1. 项目概述:为什么timerfd+epoll是Linux高性能编程的基石

在Linux服务器开发领域,性能瓶颈往往不是CPU的计算能力,而是I/O的等待。当你的服务需要同时处理成千上万个网络连接,并且每个连接上都有定时任务(比如心跳检测、超时控制、定时推送)时,传统的select/poll或者多线程+sleep方案会迅速将系统拖垮。这时,timerfdepoll的组合,就从一个“可选项”变成了“必选项”。这个组合的核心思想,是将“时间”也抽象成一种“可读的事件”,纳入到统一的事件驱动模型中,从而实现用单线程(或少量线程)高效管理海量定时器与I/O事件。

我经历过不少项目,早期用setitimer加信号处理,代码复杂且信号异步处理容易踩坑;后来用多线程配合条件变量和sleep,上下文切换开销巨大,定时精度也成问题。直到深入使用timerfd+epoll,才真正体会到什么叫“优雅”和“高效”。它不仅仅是两个API的简单调用,更代表了一种事件驱动架构的设计哲学。本文将彻底拆解这对组合,从原理到最硬核的实战代码,让你不仅会用,更能理解其背后的设计精妙与工程实践中的每一个细节。

2. 核心原理深度拆解:从文件描述符到统一事件源

2.1 epoll的本质:高效的事件通知机制

在深入timerfd之前,必须先把epoll吃透。很多开发者知道epollselect好,但知其然不知其所以然。epoll的高效,核心在于其“准备就绪列表”模型。

想象一下,select/poll就像一个尽职但方法笨拙的保安。每次有业主(文件描述符)可能有快递(事件)到来时,保安都需要拿着整个小区(所有监听的fd集合)的名单,从头到尾挨家挨户敲门问:“有你的快递吗?”(内核需要遍历所有fd)。小区户数(并发连接)一多,保安就跑断了腿,大部分时间都在做无用功。

epoll则是一个聪明的物业管理系统。它做了两件事:

  1. epoll_create:建立一个物业中心(epoll实例)。
  2. epoll_ctl:业主(fd)在入住时,就到物业中心登记:“我有快递来,请帮我留意一下,到了直接通知我”(注册感兴趣的事件,如EPOLLIN)。
  3. epoll_wait:快递员(内核)把快递送到小区的集中收发室。物业中心不用再挨家问,只需要去收发室查看一下“当前有哪些快递已经到了”(就绪事件列表),然后精准通知对应的业主来取。

这个“收发室”就是内核维护的一个就绪列表(ready list)。当某个fd的事件就绪(如socket可读、timerfd超时),内核会将其放入这个列表。epoll_wait只是从这个列表中取出已经就绪的项,其时间复杂度是O(1)或O(就绪fd数量),与监听的fd总数无关。这就是epoll能轻松应对C10K甚至C100K问题的根本原因。

注意epoll有两种触发模式:水平触发(LT)和边沿触发(ET)。LT模式下,只要fd处于就绪状态(比如缓冲区有数据),每次epoll_wait都会报告该事件;ET模式下,仅在fd状态发生变化时(比如从无数据变为有数据)报告一次。对于timerfd,我们通常使用LT模式,因为每次超时都是一个“就绪”状态,我们需要读取它来清除这个状态。

2.2 timerfd的魔法:将时间变成文件

传统的定时器,无论是alarm信号还是setitimer,都是通过中断和信号来通知应用程序。信号处理函数(signal handler)执行上下文不确定,且不能安全地调用很多标准库函数(如printf,malloc),编程复杂,容易引入竞态条件。

timerfd的创造性地解决了这个问题。它的核心思想是**“一切都是文件”**。timerfd_create系统调用会创建一个专门用于定时的“文件描述符”。你可以通过timerfd_settime为这个“文件”设置闹钟(初始超时时间和间隔)。当时钟走到设定的时间点时,这个“文件”就会变得“可读”(read操作不会阻塞)。

关键在于,这个“可读”的事件,可以被epoll监听!这样一来,定时事件就和网络I/O事件(socket可读/可写)完全平等了。你的主事件循环只需要调用一个epoll_wait,就可以同时等待网络数据到来和定时器超时。当epoll_wait返回时,如果是因为timerfd超时而返回,那么该timerfd对应的fd就会在就绪事件列表中,你只需像读取socket数据一样去read这个timerfd,读取的内容是一个uint64_t的整数,表示自上次读取后超时发生的次数。这个设计将异步的定时事件同步化到了事件循环中,极大地简化了编程模型。

一个生活化的类比:假设你是一个咖啡店老板,既要等外卖平台的订单(网络I/O),又要盯着烤箱里的面包(定时任务)。传统信号方式就像烤箱定时器响了会大声“哔哔”叫(发送信号),可能会吓到顾客,而且你必须立刻放下手中的活去处理面包,无法与处理订单协调。而timerfd+epoll的方式,是把烤箱也接入了你的订单管理系统(epoll)。当面包烤好时,系统里会 quietly 地多出一条“烤箱任务”待办项。你只需要定期查看这个统一的待办列表(epoll_wait),然后按顺序处理里面的“外卖订单”和“取出面包”任务即可,一切井然有序。

2.3 二者结合的优势:单线程事件驱动架构

timerfd注册到epoll中,就实现了统一事件源。所有需要异步处理的事情——网络报文、用户输入、定时任务——都变成了epoll_wait返回的一个个事件。主程序结构可以变得异常清晰和强大:

int epoll_fd = epoll_create1(0); // 将监听socket、连接socket、timerfd等都加入epoll_fd的监听 while (!quit) { int n = epoll_wait(epoll_fd, events, MAX_EVENTS, -1); // 阻塞等待任何事件发生 for (int i = 0; i < n; i++) { if (events[i].data.fd == timer_fd) { // 处理定时事件:读取timerfd,执行回调函数 handle_timer_event(); } else if (events[i].data.fd == listen_fd) { // 处理新连接 handle_new_connection(); } else { // 处理已连接socket上的数据 handle_client_data(events[i].data.fd); } } }

这种架构避免了多线程的锁竞争和上下文切换开销,也避免了信号处理的复杂性和不确定性,是构建高性能、高并发网络服务(如游戏服务器、实时通信系统、金融交易系统)的经典模式。

3. 硬核实战:从零构建一个高精度定时器服务

理解了原理,我们动手实现一个实用的高精度定时器服务。这个服务将允许我们添加、删除单次或循环定时任务,所有任务在一个epoll循环中得到调度。

3.1 基础框架搭建:创建timerfd并加入epoll

首先,我们创建timerfd。这里的关键是时钟源的选择。CLOCK_MONOTONIC是一个从系统启动开始单调递增的时钟,不受系统时间被用户修改或NTP调整的影响,是定时器的首选。

#include <sys/timerfd.h> #include <sys/epoll.h> #include <unistd.h> #include <stdint.h> #include <stdio.h> #include <string.h> #include <errno.h> int create_and_start_timerfd(int initial_sec, int interval_sec) { // 1. 创建timerfd,使用单调时钟 int timer_fd = timerfd_create(CLOCK_MONOTONIC, TFD_NONBLOCK); if (timer_fd == -1) { perror("timerfd_create failed"); return -1; } // 2. 设置定时器参数 struct itimerspec new_value; memset(&new_value, 0, sizeof(new_value)); // 首次超时时间 new_value.it_value.tv_sec = initial_sec; new_value.it_value.tv_nsec = 0; // 间隔时间(0表示单次定时器) new_value.it_interval.tv_sec = interval_sec; new_value.it_interval.tv_nsec = 0; // 3. 启动定时器 // TFD_TIMER_ABSTIME标志表示设置的是绝对时间,这里我们使用相对时间,所以传0 if (timerfd_settime(timer_fd, 0, &new_value, NULL) == -1) { perror("timerfd_settime failed"); close(timer_fd); return -1; } printf("Timerfd created: fd=%d, initial=%ds, interval=%ds\n", timer_fd, initial_sec, interval_sec); return timer_fd; }

接下来,将这个timerfd添加到epoll实例中:

int add_fd_to_epoll(int epoll_fd, int fd, uint32_t events) { struct epoll_event ev; ev.events = events; ev.data.fd = fd; // 这里简单用fd作为标识,实际项目可能用更复杂的结构体 if (epoll_ctl(epoll_fd, EPOLL_CTL_ADD, fd, &ev) == -1) { perror("epoll_ctl: add fd failed"); return -1; } return 0; } // 在主函数中 int main() { int epoll_fd = epoll_create1(0); int timer_fd = create_and_start_timerfd(1, 1); // 1秒后启动,每秒触发一次 if (add_fd_to_epoll(epoll_fd, timer_fd, EPOLLIN | EPOLLET) < 0) { // 错误处理 } // ... 事件循环 }

实操心得:在将timerfd加入epoll时,我强烈建议使用**边沿触发(EPOLLET)**模式,尽管前面提到LT模式更常用。原因在于,如果使用LT模式,且定时器触发后你没有及时readepoll_wait会一直返回,导致事件循环空转,CPU飙升。而ET模式只在定时器状态变化(从未超时到超时)时通知一次,迫使你必须read干净,行为更可控。但使用ET模式就必须保证一次read调用读取完全部数据(即读到EAGAIN错误),否则会丢失事件。

3.2 设计一个可管理的定时器任务队列

单一的定时器不够用,我们需要一个能管理多个定时任务的结构。一个常见的做法是使用时间轮最小堆。这里为了清晰,我们实现一个基于timerfd的简化版多定时器,思路是:维护一个按超时时间排序的任务列表,只用一个timerfd,其超时时间总是设置为最近一个将要到期任务的时间。

#include <stdbool.h> typedef void (*timer_callback)(void* arg); struct timer_task { int id; // 任务ID long long expire_at_ms; // 绝对到期时间(毫秒,基于CLOCK_MONOTONIC) long long interval_ms; // 间隔时间,0表示单次 timer_callback cb; // 到期回调函数 void* arg; // 回调参数 struct timer_task* next; }; struct timer_manager { int timer_fd; struct timer_task* task_list; // 按到期时间排序的单链表 long long (*get_current_ms)(); // 获取当前时间的函数指针 };

添加任务逻辑

  1. 计算新任务的绝对到期时间:current_ms + delay_ms
  2. 将任务按到期时间插入到有序链表中。
  3. 如果新任务被插到了链表头部(即它是最快到期的一个),则需要更新timerfd的超时时间,使其在新的最早到期时刻触发。

事件循环处理逻辑

  1. epoll_waittimerfd就绪而返回。
  2. read(timer_fd, &expired_count, sizeof(uint64_t)),获取超时次数。
  3. 获取当前时间current_ms
  4. 遍历任务链表,执行所有expire_at_ms <= current_ms的任务。
    • 如果是循环任务,重新计算下一次到期时间并重新插入链表。
    • 如果是单次任务,执行后释放。
  5. 重新设置timerfd的超时时间为链表头部任务的时间(如果链表不为空)。

这个设计避免了为每个定时任务都创建一个timerfd(系统资源有限),而是用一个timerfd驱动了整个定时器队列,非常高效。

3.3 完整事件循环与错误处理

一个健壮的事件循环必须处理各种边界情况和错误。

#define MAX_EVENTS 64 #define BUFFER_SIZE 1024 void event_loop(int epoll_fd, int timer_fd, struct timer_manager* tm) { struct epoll_event events[MAX_EVENTS]; char buffer[BUFFER_SIZE]; while (true) { // 阻塞等待,-1表示无限等待 int nfds = epoll_wait(epoll_fd, events, MAX_EVENTS, -1); if (nfds == -1) { // 被信号中断,是正常情况,继续循环 if (errno == EINTR) { continue; } perror("epoll_wait fatal error"); break; } for (int i = 0; i < nfds; ++i) { if (events[i].data.fd == timer_fd) { // 定时器事件 uint64_t expirations; ssize_t s = read(timer_fd, &expirations, sizeof(expirations)); if (s != sizeof(expirations)) { // 读取错误,可能是ET模式下未读尽,或者fd已关闭 // 处理错误,可能需要重新设置定时器或退出 fprintf(stderr, "Read timerfd error: %s\n", strerror(errno)); } else { // 成功读取,处理定时任务 process_expired_tasks(tm); } } else if (events[i].events & EPOLLIN) { // 网络socket可读事件 int client_fd = events[i].data.fd; ssize_t count = read(client_fd, buffer, BUFFER_SIZE - 1); if (count > 0) { buffer[count] = '\0'; // 处理业务逻辑... } else if (count == 0) { // 对端关闭连接 printf("Client closed connection.\n"); epoll_ctl(epoll_fd, EPOLL_CTL_DEL, client_fd, NULL); close(client_fd); } else { // read错误 if (errno != EAGAIN && errno != EWOULDBLOCK) { perror("read error"); epoll_ctl(epoll_fd, EPOLL_CTL_DEL, client_fd, NULL); close(client_fd); } } } else if (events[i].events & (EPOLLERR | EPOLLHUP)) { // 错误或挂起事件,关闭对应的fd fprintf(stderr, "Epoll error/hup on fd %d\n", events[i].data.fd); epoll_ctl(epoll_fd, EPOLL_CTL_DEL, events[i].data.fd, NULL); close(events[i].data.fd); } } } }

注意事项epoll_wait的返回值nfds为0是可能的,但前提是设置了超时时间且超时前没有任何事件发生。我们这里设置超时为-1(无限等待),所以nfds为0的情况不会出现。但在有超时设置的事件循环中,nfds为0意味着一次“空返回”,这通常可以用来处理一些周期性的后台任务(如日志刷新),而不需要单独的定时器。

4. 高级应用与性能调优实战

4.1 实现一个带时间轮的多定时器服务

前面的链表方案在任务数量多时,插入和查找效率是O(n)。对于需要管理数万甚至更多定时器的场景(如游戏服务器中每个玩家都有多个技能冷却定时器),我们需要更高效的数据结构。时间轮是网络编程中管理大量定时器的经典数据结构。

简单时间轮就像一个时钟表盘,分为多个槽(slot),每个槽对应一个时间间隔。一个指针按固定频率(比如每100毫秒)跳动一格。每个槽挂载一个链表,链表中的任务将在指针指向该槽时到期。添加一个delay时间后的任务,只需计算它应该落在哪个槽(current_index + delay / tick) % N,然后插入该槽的链表即可。到期检查的复杂度是O(1)。

结合timerfd,我们可以将timerfd的间隔设置为时间轮的tick间隔(如100ms)。每次timerfd触发,我们就移动时间轮指针,处理当前槽中的所有任务。对于循环任务,重新计算其下一次的槽位并插入。

#define TIME_WHEEL_SIZE 512 // 时间轮大小 #define TICK_INTERVAL_MS 100 // 每100ms一跳 struct time_wheel_slot { struct timer_task* head; struct timer_task* tail; }; struct time_wheel { struct time_wheel_slot slots[TIME_WHEEL_SIZE]; int current_index; pthread_mutex_t lock; // 如果多线程添加任务需要加锁 }; // timerfd触发后的处理函数 void on_timer_tick(struct time_wheel* tw) { pthread_mutex_lock(&tw->lock); struct time_wheel_slot* slot = &tw->slots[tw->current_index]; // 执行当前槽中所有任务 struct timer_task* task = slot->head; while (task) { struct timer_task* next = task->next; task->cb(task->arg); if (task->interval_ms > 0) { // 循环任务,重新计算并插入未来槽位 int ticks = task->interval_ms / TICK_INTERVAL_MS; int target_index = (tw->current_index + ticks) % TIME_WHEEL_SIZE; // 将task插入到slots[target_index]链表中... } else { // 单次任务,释放内存 free(task); } task = next; } // 清空当前槽 slot->head = slot->tail = NULL; // 移动指针 tw->current_index = (tw->current_index + 1) % TIME_WHEEL_SIZE; pthread_mutex_unlock(&tw->lock); }

这种方案非常适合对精度要求不是极端高(百毫秒级),但定时器数量巨大的场景。

4.2 与多线程/线程池的协作模式

虽然epoll主循环是单线程的,但耗时的任务不应该阻塞它。常见的模式是:

  1. 主线程(I/O线程):只负责epoll_wait、接收新连接、读取/写入数据(非阻塞I/O)。当收到一个完整的业务请求包时,将其封装成一个任务对象。
  2. 任务队列:一个线程安全的队列,用于存放待处理的任务。
  3. 工作线程池:一组预先创建好的线程,不断从任务队列中取出任务并执行。执行完成后,如果需要回包,则将回包任务再投递回给主线程(通常通过管道或eventfd通知)。

那么定时器事件如何与线程池协作?当timerfd触发,执行到期的定时任务时,如果这个任务本身是计算密集型的,也应该将其投递到线程池中,而不是在主线程直接执行回调函数。这保证了事件循环的响应速度。

// 假设有一个全局的线程池任务队列 thread_pool_queue void timer_callback_that_submits_to_threadpool(void* arg) { struct heavy_task* task = (struct heavy_task*)arg; // 将繁重任务提交到线程池,而不是自己执行 if (thread_pool_submit(thread_pool_queue, &task->work) != 0) { // 提交失败处理 free(task); } // 注意:task的内存在线程池工作函数中释放 }

4.3 精度、性能与资源权衡

精度问题timerfd的精度取决于内核的hrtimer(高精度定时器)。理论上可以达到纳秒级,但实际精度受系统负载、内核配置影响。对于大多数网络应用,毫秒级精度完全足够。如果你需要微秒级精度的周期性任务(如高频交易),可能需要结合clock_nanosleep和忙等待,但这超出了epoll模型的范畴。

性能调优

  1. epoll_wait的超时时间:在纯网络服务器中,通常设为-1(无限等待)。但如果你的系统也有需要周期性执行的低优先级任务(比如统计信息输出),可以设置一个较小的超时(如100ms),这样即使没有I/O事件,epoll_wait也会定期返回,给你执行这些后台任务的机会。
  2. 文件描述符上限epoll本身对并发连接数没有硬限制,但系统对单个进程可打开的文件描述符数有限制。使用ulimit -n查看和修改。对于要支持万级连接的服务,务必在启动前调高这个限制(如ulimit -n 100000或在代码中用setrlimit设置)。
  3. 内存与CPU:使用ET模式并配合非阻塞I/O,可以避免不必要的系统调用和上下文切换。确保你的read/write循环在遇到EAGAIN时及时退出。

资源管理

  • timerfd泄漏:和所有fd一样,用完必须close。在事件循环中,如果某个timerfd对应的任务管理器被销毁,记得先从epoll中注销(EPOLL_CTL_DEL)再关闭fd。
  • 惊群问题:虽然timerfd本身不涉及多进程惊群,但如果你用epoll管理监听socket,并在多进程模型中,子进程继承了epoll_fd,那么新连接到来时,所有子进程的epoll_wait都可能被唤醒,但只有一个能accept成功,造成资源浪费。解决方法是使用EPOLLEXCLUSIVE标志(Linux 4.5+)或让每个进程创建自己的epoll实例并添加监听socket。

5. 生产环境常见问题与深度排查

5.1 timerfd不触发或触发异常

这是最常见的问题,可能的原因和排查步骤:

  1. 没有读取timerfd:这是ET模式下最容易犯的错。timerfd超时后,内核会将其标记为可读。如果你使用ET模式且没有read,或者read没有读完整(即没有读到返回EAGAIN),那么下一次epoll_wait将不会再次通知你,导致定时器“停止”。务必在每次触发后,循环读取直到errnoEAGAIN

    uint64_t exp; ssize_t s; while ((s = read(timer_fd, &exp, sizeof(exp))) > 0) { total_expirations += exp; } if (s == -1 && errno != EAGAIN) { // 真正的错误 }
  2. timerfd_settime参数错误:检查itimerspec结构体是否已正确初始化。it_value为0表示解除定时器。确保new_value参数不为NULL,且时间单位正确(秒和纳秒)。

  3. epoll监听事件错误:确保在epoll_ctl添加timer_fd时,事件类型包含了EPOLLIN

  4. 多个进程/线程共用一个timerfd:如果一个进程read了超时次数,另一个进程就读取不到了。确保定时器的管理在单一上下文中。

5.2 epoll_wait返回但timerfd未就绪

有时epoll_wait返回的事件数量nfds大于0,但遍历事件数组时发现timer_fd对应的事件并没有被设置。这通常是因为:

  • 你修改了events数组或epoll_event结构体,但在epoll_wait调用后,内核会覆盖这个数组的内容。确保传入的events数组是有效的,并且在调用后立即处理。
  • 极少数情况下,可能是内核bug或内存越界导致的数据损坏。可以用strace跟踪系统调用,看epoll_wait返回的具体事件是什么。

5.3 定时精度漂移与累积误差

即使你设置了1秒的间隔,timerfd的实际触发间隔也可能不是精确的1秒,尤其是在系统负载高时。timerfd的触发是“下一次超时”基于“上一次设定的时间”,而不是基于“上一次实际触发的时间”。这意味着如果某次处理超时事件延迟了,不会影响下一次的预定时间,从而避免了误差累积。这是timerfd的一个优点。

但是,如果你的定时任务处理函数本身执行时间很长,超过了定时间隔,那么任务就会堆积。对于循环任务,你需要决定是“固定延迟”(每次任务结束后再计算下一次)还是“固定速率”(严格按计划时间执行)。timerfd提供的是“固定速率”的基础。要实现“固定延迟”,需要在任务回调中,根据任务执行结束的时间,重新计算并设置timerfd的下一次超时。

5.4 在多线程环境中安全使用

如果定时器管理器(添加/删除任务)和事件循环(处理超时)在不同的线程,就需要加锁保护共享数据(如任务链表或时间轮)。

  • 粗粒度锁:在操作整个定时器管理器(如add_timer,cancel_timer,process_expired_tasks)时加一把大锁。简单,但可能影响性能。
  • 细粒度锁:例如在时间轮中,每个槽一把锁。添加任务时锁住目标槽,处理到期任务时锁住当前槽。更复杂,但并发度高。

一个更优雅的、避免在事件循环中加锁的模式是使用无锁队列。工作线程将添加或删除定时器的请求放入一个无锁队列,事件循环在每次epoll_wait返回后,先批量处理这个队列中的请求,更新其内部的定时器数据结构,然后再处理到期任务。这样,事件循环线程是唯一修改定时器数据结构的线程,自然避免了竞态条件。

5.5 系统负载与定时器风暴

当有大量定时器在同一时刻到期(例如,午夜所有定时任务同时触发),会导致事件循环瞬间要处理海量回调,造成请求堆积、延迟飙升,甚至服务假死。这就是“定时器风暴”。

应对策略

  1. 错峰:在添加定时器时,加入一个随机的小偏移量。例如,一个每天执行的任务,不要都设置在00:00:00,可以设置在00:00:00到00:05:00之间的随机时间。
  2. 分层处理:将定时器分为不同的优先级队列。高优先级的立即执行,低优先级的放入一个后台队列,由单独的线程慢慢处理。
  3. 限流:在事件循环中,每次timerfd触发后,限制处理到期任务的最大数量。比如一次最多处理100个,剩下的留到下一个循环迭代。这能保证事件循环不会被长时间阻塞,依然能响应网络I/O。

6. 从理论到实践:一个简易HTTP服务器的心跳检测实现

让我们用一个具体的例子来串联所有知识:为一个简易的HTTP服务器实现心跳检测,10秒内未收到客户端数据则断开连接。

步骤拆解

  1. 数据结构扩展:在每个客户端连接的结构体中,加入一个字段记录其对应的定时器ID。

    struct client_conn { int fd; int timer_id; // 关联的心跳超时定时器ID // ... 其他数据,如读缓冲区、请求状态等 };
  2. 连接建立:当accept一个新连接时,为其创建一个10秒后触发的单次定时器。定时器回调函数是close_conn

    void on_new_connection(int listen_fd, int epoll_fd, struct timer_manager* tm) { int conn_fd = accept(listen_fd, NULL, NULL); set_nonblocking(conn_fd); // 设置为非阻塞 struct client_conn* conn = create_client_conn(conn_fd); // 添加一个10秒后超时的定时器,回调函数传入conn作为参数 conn->timer_id = add_timer(tm, 10000, 0, close_conn, conn); // 将conn_fd添加到epoll,监听读事件 add_fd_to_epoll(epoll_fd, conn_fd, EPOLLIN | EPOLLET); // 将conn结构体指针通过epoll_event的data.ptr传递,方便后续使用 }
  3. 收到数据:当从conn_fd读到数据时,表示客户端活跃。我们需要刷新这个连接的心跳定时器。

    void on_client_data(struct client_conn* conn, struct timer_manager* tm) { // 1. 处理HTTP请求... // 2. 刷新心跳定时器:先删除旧的,再添加新的 delete_timer(tm, conn->timer_id); conn->timer_id = add_timer(tm, 10000, 0, close_conn, conn); }
  4. 定时器到期:如果10秒内未收到数据,定时器回调close_conn被调用。

    void close_conn(void* arg) { struct client_conn* conn = (struct client_conn*)arg; printf("Connection fd=%d timeout, closing.\n", conn->fd); epoll_ctl(epoll_fd, EPOLL_CTL_DEL, conn->fd, NULL); close(conn->fd); free(conn); }
  5. 连接关闭:当连接正常关闭(收到FIN包)或出错时,除了关闭socket,还必须删除其对应的定时器,防止回调函数访问已释放的内存。

    void cleanup_connection(struct client_conn* conn, struct timer_manager* tm) { delete_timer(tm, conn->timer_id); // 关键! epoll_ctl(epoll_fd, EPOLL_CTL_DEL, conn->fd, NULL); close(conn->fd); free(conn); }

这个例子清晰地展示了如何将网络事件(数据到达)与定时事件(超时)通过epoll统一管理,以及如何维护网络连接与定时器之间的生命周期关联,这是构建稳定网络服务的核心模式之一。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询