ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

【Socket 进阶之路】第 8 章 epoll 高并发 IO 多路复用完整实战|LT 水平触发 ET 边缘触发、Reactor 单线程模型

【Socket 进阶之路】第 8 章 epoll 高并发 IO 多路复用完整实战|LT 水平触发  ET 边缘触发、Reactor 单线程模型 点赞 收藏 关注Socket 工业级网络编程全套实战持续更新✨ 本专栏全程手写源码、无阉割、纯工程0. 前言上一章我们学习了 select、poll二者核心短板是无论有多少连接每次事件就绪后程序必须遍历全部 fd 数组查找就绪描述符时间复杂度 O (n)。当并发连接达到数千上万时CPU 开销会急剧上升不再适合高并发网关、物联网接入服务器。为了解决这个问题Linux 在 2.6 内核引入了epoll它是 Linux 专属的高性能 IO 多路复用组件也是 Nginx、Redis、MQTT Broker 底层网络核心。epoll 采用内核就绪回调机制只会返回已经发生事件的 fd复杂度近似 O (1)支持海量长连接是工业高并发服务首选方案。本章重点内容epoll 三大核心 APIepoll_create /epoll_ctl/epoll_waitepoll 两种触发模式LT 水平触发、ET 边缘触发原理与差异epoll 完整 TCP 多并发服务端LT 标准实现适合入门量产ET 边缘触发改造、一次性读完缓冲区、EAGAIN 处理规范单线程 Reactor 基础模型介绍epoll 工程坑点、性能调优、与 select/poll 选型对比重要提醒epoll不具备跨平台能力Windows 无原生 epoll跨平台项目优先使用 pollLinux 高并发场景再选用 epoll。1. epoll 三大核心 API 详解1.1 epoll_create创建 epoll 实例返回 epoll 文件描述符#include sys/epoll.h int epoll_create(int size);size早期内核用于提示监听 fd 预估数量新版本内核已废弃传入大于 0 任意正数即可返回值成功返回 epoll_fd失败返回 - 1使用完毕必须 close (epoll_fd) 释放内核资源。新版本推荐使用epoll_create1(0)更加规范推荐工程使用。1.2 epoll_ctl向 epoll 内核实例注册、修改、删除 fd 监听事件核心控制接口int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);参数说明epfdepoll_create 返回的 epoll 句柄op操作类型三种宏EPOLL_CTL_ADD新增 fd 到 epoll 监听EPOLL_CTL_MOD修改已注册 fd 的监听事件EPOLL_CTL_DEL从 epoll 中删除 fd不再监听fd待操作的文件描述符event结构体设置监听事件附带私有数据struct epoll_event { uint32_t events; // 监听事件 EPOLLIN / EPOLLOUT / EPOLLET epoll_data_t data; }; typedef union epoll_data { void *ptr; int fd; uint32_t u32; uint64_t u64; } epoll_data_t;常用 events 宏EPOLLINfd 可读新连接、收到数据、连接关闭EPOLLOUTfd 可写内核发送缓冲区空闲可以发送数据EPOLLET开启边缘触发 ET默认不加该标记为水平触发 LTEPOLLERRfd 错误内核自动监听无需手动注册EPOLLHUP对端挂断连接内核自动监听1.3 epoll_wait阻塞等待事件就绪内核返回就绪的 fd 事件数组int epoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout);参数说明epfdepoll 句柄events输出数组内核填充就绪的 fd 和事件maxevents本次最多接收多少个就绪事件数组长度timeout超时时间 ms-1 永久阻塞0 非阻塞轮询 返回值0就绪事件个数0超时0调用出错2. LT 水平触发 VS ET 边缘触发重中之重2.1 LT 水平触发默认模式推荐新手 / 量产通用触发规则只要缓冲区还有未读完数据每次 epoll_wait 都会持续通知 EPOLLIN 可读事件。 特点编码简单容错高即使本次没有读完缓冲区剩余半包下一次 epoll_wait 依旧再次通知风险如果单次不处理、不读取数据会持续唤醒程序造成 CPU 空转适用绝大多数物联网网关、中小型并发服务稳定性优先项目首选。2.2 ET 边缘触发EPOLLET高性能模式触发规则只有状态发生变化的一瞬间通知一次举例客户端发送数据内核缓冲区从空→有数据仅触发一次 EPOLLIN 如果程序本次没有把缓冲区全部读完剩余数据不会再次触发事件除非新数据再次写入缓冲区。 特点唤醒次数更少高并发场景性能更好编码严格必须循环 read 直到返回 EAGAIN一次性清空缓冲区风险漏读半包会永久丢失事件连接卡死调试难度高ET 模式强制要求 fd 设置为非阻塞阻塞 read 会直接卡死线程。✅ 选型建议业务优先稳定用 LT追求极致高吞吐、开发人员功底深厚再使用 ET。3. 实战epoll LT 水平触发 TCP 回声服务端量产标准版业务流程创建 listen_fd设置端口复用、非阻塞注册到 epoll监听 EPOLLINepoll_wait 阻塞等待就绪事件listen_fd 就绪 → accept 获取新 connfd设置非阻塞注册进 epoll普通 connfd EPOLLIN 就绪 → read 读取数据送入环形缓冲区解包回声回复read 返回 0 / EPOLLHUP → close 关闭 fdepoll_ctl 删除监听异常错误释放资源防止 fd 泄漏。epoll_lt_server.c#include stdio.h #include stdlib.h #include string.h #include unistd.h #include sys/socket.h #include netinet/in.h #include arpa/inet.h #include fcntl.h #include errno.h #include sys/epoll.h #define LISTEN_PORT 8888 #define BUF_MAX 256 #define EPOLL_MAX_EVENTS 128 int SetSocketNonBlock(int fd) { int flags fcntl(fd, F_GETFL, 0); if (flags 0) return -1; return fcntl(fd, F_SETFL, flags | O_NONBLOCK); } // 将fd添加进epoll监听 int EpollAddFd(int epfd, int fd, uint32_t events) { struct epoll_event ev; ev.data.fd fd; ev.events events; return epoll_ctl(epfd, EPOLL_CTL_ADD, fd, ev); } // 修改已注册fd监听事件 int EpollModFd(int epfd, int fd, uint32_t events) { struct epoll_event ev; ev.data.fd fd; ev.events events; return epoll_ctl(epfd, EPOLL_CTL_MOD, fd, ev); } // 删除fd停止监听 int EpollDelFd(int epfd, int fd) { return epoll_ctl(epfd, EPOLL_CTL_DEL, fd, NULL); } int main(void) { int listen_fd socket(AF_INET, SOCK_STREAM, 0); if (listen_fd 0) { perror(socket create fail); exit(EXIT_FAILURE); } int opt 1; setsockopt(listen_fd, SOL_SOCKET, SO_REUSEADDR, opt, sizeof(opt)); struct sockaddr_in serv_addr; memset(serv_addr, 0, sizeof(serv_addr)); serv_addr.sin_family AF_INET; serv_addr.sin_port htons(LISTEN_PORT); serv_addr.sin_addr.s_addr htonl(INADDR_ANY); if (bind(listen_fd, (struct sockaddr *)serv_addr, sizeof(serv_addr)) 0) { perror(bind fail); close(listen_fd); exit(EXIT_FAILURE); } if (listen(listen_fd, 5) 0) { perror(listen fail); close(listen_fd); exit(EXIT_FAILURE); } SetSocketNonBlock(listen_fd); // 创建epoll实例 int epfd epoll_create1(0); if (epfd 0) { perror(epoll_create1 fail); close(listen_fd); exit(EXIT_FAILURE); } // 注册监听fdLT模式监听可读 if (EpollAddFd(epfd, listen_fd, EPOLLIN) 0) { perror(epoll add listen fd fail); close(epfd); close(listen_fd); exit(EXIT_FAILURE); } printf(epoll LT tcp server running on port %d\n, LISTEN_PORT); struct epoll_event events[EPOLL_MAX_EVENTS]; uint8_t recv_buf[BUF_MAX]; while (1) { // 阻塞等待就绪事件超时1000ms int nready epoll_wait(epfd, events, EPOLL_MAX_EVENTS, 1000); if (nready 0) { perror(epoll_wait error); continue; } else if (nready 0) { continue; } // 遍历所有就绪事件 for (int i 0; i nready; i) { int fd events[i].data.fd; uint32_t revents events[i].events; // 监听fd就绪新客户端连接 if (fd listen_fd) { struct sockaddr_in cli_addr; socklen_t cli_len sizeof(cli_addr); int connfd accept(listen_fd, (struct sockaddr *)cli_addr, cli_len); if (connfd 0) { perror(accept fail); continue; } SetSocketNonBlock(connfd); // 新连接注册进epoll LT可读 if (EpollAddFd(epfd, connfd, EPOLLIN) 0) { perror(epoll add connfd fail); close(connfd); continue; } printf(new client: %s:%d fd%d\n, inet_ntoa(cli_addr.sin_addr), ntohs(cli_addr.sin_port), connfd); } else { // 普通客户端fd事件 if (revents (EPOLLIN | EPOLLHUP | EPOLLERR)) { int rlen read(fd, recv_buf, BUF_MAX - 1); if (rlen 0) { printf(fd:%d recv data:%s\n, fd, recv_buf); // 回声回复 write(fd, recv_buf, rlen); memset(recv_buf, 0, BUF_MAX); } else if (rlen 0) { // 对端正常关闭 printf(client fd:%d disconnect\n, fd); EpollDelFd(epfd, fd); close(fd); } else { if (errno ! EAGAIN errno ! EWOULDBLOCK) { perror(read error); EpollDelFd(epfd, fd); close(fd); } } } } } } close(epfd); close(listen_fd); return 0; }编译运行gcc epoll_lt_server.c -o epoll_lt_server ./epoll_lt_server4. ET 边缘触发改造要点必看规范如果开启 EPOLLET 边缘触发代码必须修改两点所有 fd 强制设置非阻塞已经在 Demo 实现收到 EPOLLIN 事件后循环调用 read直到返回 -1 errno EAGAIN清空内核缓冲区全部数据 示例 ET 读取代码片段if (revents EPOLLIN) { while(1) { int rlen read(fd, recv_buf, BUF_MAX); if(rlen 0) { // 处理数据 } else if(rlen 0) { // 连接关闭 break; } else { if(errno EAGAIN || errno EWOULDBLOCK) { // 缓冲区读完退出循环等待下一次边缘事件 break; } else { // 真实读写错误关闭连接 } } } }ET 常见致命 bug只 read 一次缓冲区残留半包不再触发事件连接卡死无响应。5. Reactor 单线程基础模型介绍上面完整 epoll Demo就是最基础的单线程 Reactor模型主线程epoll_wait 负责事件分发Reactor 反应堆收到就绪事件后同步执行读写、协议解析业务处理 优点无多线程锁竞争代码简单没有线程上下文切换开销 局限如果业务存在耗时操作数据库查询、文件读写会阻塞整个主线程所有连接停滞。扩展方案多 Reactor主 Reactor 负责监听 accept子 Reactor 负责连接读写Nginx 架构Reactor 工作线程池IO 事件主线处理耗时业务投递线程池异步执行6. epoll 工程高频踩坑汇总epoll_ctl 删除 fd 后务必调用 close (fd)否则产生文件描述符泄漏长期运行 fd 耗尽ET 边缘触发必须非阻塞 循环读写到 EAGAIN严禁阻塞 readepoll_wait 返回的 events 数组是内核临时拷贝不要长期保存内部指针EPOLLHUP、EPOLLERR 会自动触发不需要手动注册监听事件高并发服务调整系统参数ulimit -n增大进程最大 fdsysctl 调整 epoll 内核参数LT 模式下如果收到可读事件一直不 read会持续唤醒 epoll_waitCPU 占用 100%多线程操作同一个 epfd 需要加互斥锁epoll 本身不是线程安全发送大数据时EPOLLOUT 可写事件管理缓冲区满后删除 EPOLLOUT缓冲区空闲再重新注册避免持续可写唤醒。7. select /poll/epoll 横向性能对比特性selectpollepoll并发上限默认 1024fd受进程 fd 限制受进程 fd 限制支持十万级长连接就绪查找O (n) 全量遍历O (n) 全量遍历O (1)仅返回就绪 fd触发模式LTLTLT / ET 双模式跨平台全平台支持Linux 为主Linux 专属适用场景几十几百连接小型程序中小型网关、跨平台项目Linux 高并发接入服务8. 本章小结本章完整讲解 epoll 三大 API区分 LT 水平触发、ET 边缘触发底层原理实现可直接编译的 epoll LT 标准 TCP 服务端介绍基础单线程 Reactor 网络模型梳理工程常见 bug。 核心要点epoll_create1 创建实例epoll_ctl 管理 fd 注册删除epoll_wait 阻塞等待就绪事件LT 容错好适合量产通用ET 唤醒更少但编码规范严格必须循环读写清空缓冲区epoll 相比 select/poll海量长连接下性能优势巨大是 Linux 高性能网络基石基础单 Reactor 模型适合纯 IO 业务耗时任务需要搭配线程池异步解耦。遗留思考 网络服务中经常遇到大量短连接频繁创建销毁、半关闭连接、心跳断开检测、内存池管理等量产稳定性问题同时如何实现可移植的通用网络框架 下一章Linux 网络服务量产稳定性优化心跳保活、TIME_WAIT、SO_LINGER、内存池、断线重连、完整异常防护框架。
返回列表