Redis源码探究系列—epoll、kqueue 在 Redis 中的封装实现(上) 欢迎各位同学关注我哦~在这个 AI 喧嚣的时代不忘初心戒骄戒躁认真沉淀在之前的文章中我们分析过Redis事件循环框架了解过aeEventLoop的整体设计其中apidata字段用于存储底层多路复用机制的私有数据。Redis通过统一的抽象层屏蔽了不同操作系统I/O多路复用接口的差异机制操作系统性能epollLinux高kqueueBSD/macOS高evportSolaris高select跨平台低兜底方案本文将深入剖析Redis对epoll的封装实现下一篇文章将分析kqueue的封装。一、多路复用机制的选择策略1.1 编译时选择Redis在编译时根据操作系统自动选择最优的多路复用机制// src/ae.c:49-61#ifdefHAVE_EVPORT#includeae_evport.c#else#ifdefHAVE_EPOLL#includeae_epoll.c#else#ifdefHAVE_KQUEUE#includeae_kqueue.c#else#includeae_select.c#endif#endif#endif编译时按性能从高到低的顺序检测系统支持的多路复用机制选择第一个可用的首先是Solaris的evport其次是Linux的epoll然后是BSD/macOS的kqueue最后是作为兜底方案的select。1.2 统一的API抽象Redis定义了一套统一的多路复用API每种实现都遵循相同的接口规范// 以下是需要实现的统一接口内部使用staticintaeApiCreate(aeEventLoop*eventLoop);// 创建多路复用实例staticintaeApiResize(aeEventLoop*eventLoop,intsetsize);// 调整容量staticvoidaeApiFree(aeEventLoop*eventLoop);// 释放资源staticintaeApiAddEvent(aeEventLoop*eventLoop,intfd,intmask);// 注册事件staticvoidaeApiDelEvent(aeEventLoop*eventLoop,intfd,intmask);// 删除事件staticintaeApiPoll(aeEventLoop*eventLoop,structtimeval*tvp);// 等待事件staticchar*aeApiName(void);// 返回机制名称二、epoll基础知识epoll是Linux 2.6内核引入的I/O多路复用机制相比传统的select/poll它在处理大量并发连接时有显著的性能优势。2.1 epoll的核心优势相比传统的select/pollepoll具有以下优势特性select/pollepollfd数量限制有限制FD_SETSIZE默认1024几乎无限制效率O(n)遍历所有fdO(1)只返回就绪的fd内存拷贝每次调用需要拷贝共享内存无需拷贝触发模式仅水平触发支持边缘触发ET2.2 epoll的三个核心系统调用// 创建epoll实例返回epoll文件描述符intepoll_create(intsize);// 管理epoll实例中的文件描述符添加、修改、删除intepoll_ctl(intepfd,intop,intfd,structepoll_event*event);// 等待事件发生intepoll_wait(intepfd,structepoll_event*events,intmaxevents,inttimeout);2.3 epoll_event结构体epoll_event是epoll机制中用于描述事件的核心结构体structepoll_event{uint32_tevents;// 事件类型EPOLLIN、EPOLLOUT 等epoll_data_tdata;// 用户数据可存储fd或指针};typedefunionepoll_data{void*ptr;intfd;uint32_tu32;uint64_tu64;}epoll_data_t;events是一个位掩码字段用于表示关注的事件类型。最常用的是EPOLLIN可读和EPOLLOUT可写此外还有EPOLLERR和EPOLLHUP等事件。通过按位或操作可以同时监听多种事件。还有一个特殊的EPOLLET标志用于启用边缘触发模式不过Redis并没有使用这个特性它采用的是默认的水平触发模式。data用于存储用户自定义数据。最常见的用法是data.fd存储文件描述符或者data.ptr存储一个指针指向更复杂的结构体。Redis选择了前者因为它的aeEventLoop中已经维护了一个以fd为索引的events数组只需要从epoll拿到触发事件的fd就能快速定位到对应的事件处理信息。三、Redis中epoll的封装实现3.1 数据结构定义Redis使用aeApiState结构体封装epoll相关状态// src/ae_epoll.c:34-37typedefstructaeApiState{intepfd;// epoll实例的文件描述符structepoll_event*events;// 用于接收epoll_wait返回的事件数组}aeApiState;结构体很简单只有两个字段epfd保存epoll_create返回的epoll实例文件描述符events则是预分配的事件数组用于接收epoll_wait返回的就绪事件。预先分配好这个数组可以避免每次调用epoll_wait时临时分配内存。3.2 创建epoll实例aeApiCreate// src/ae_epoll.c:39-56staticintaeApiCreate(aeEventLoop*eventLoop){aeApiState*statezmalloc(sizeof(aeApiState));// 分配状态结构体if(!state)return-1;// 预分配事件数组大小为setsize// setsize决定了Redis能处理的最大fd数量state-eventszmalloc(sizeof(structepoll_event)*eventLoop-setsize);if(!state-events){zfree(state);return-1;}// 创建epoll实例// 参数size在Linux 2.6.8后已被忽略但必须0state-epfdepoll_create(1024);/* 1024 is just a hint for the kernel */if(state-epfd-1){// 创建失败释放已分配的资源zfree(state-events);zfree(state);return-1;}// 将epoll状态保存到事件循环的apidata字段eventLoop-apidatastate;return0;}整个函数的逻辑比较清晰先分配aeApiState结构体然后预分配epoll_event数组数组大小由eventLoop-setsize决定这个值对应Redis能处理的最大文件描述符数量。接着调用epoll_create创建epoll实例参数1024在Linux 2.6.8以后已经被忽略所以只需要传一个大于0的值即可。最后将状态结构体保存到eventLoop-apidata完成初始化。3.3 调整容量aeApiResize// src/ae_epoll.c:58-63staticintaeApiResize(aeEventLoop*eventLoop,intsetsize){aeApiState*stateeventLoop-apidata;// 重新分配事件数组的大小// 当Redis需要动态调整最大连接数时调用state-eventszrealloc(state-events,sizeof(structepoll_event)*setsize);return0;}这个函数用于动态调整事件数组的大小。当通过CONFIG SET maxclients修改最大客户端连接数时会触发容量调整对应的setsize也会变化此时需要重新分配events数组以适应新的容量。3.4 释放资源aeApiFree// src/ae_epoll.c:65-71staticvoidaeApiFree(aeEventLoop*eventLoop){aeApiState*stateeventLoop-apidata;close(state-epfd);// 关闭epoll实例文件描述符zfree(state-events);// 释放事件数组zfree(state);// 释放状态结构体}3.5 注册事件aeApiAddEvent// src/ae_epoll.c:73-88staticintaeApiAddEvent(aeEventLoop*eventLoop,intfd,intmask){aeApiState*stateeventLoop-apidata;structepoll_eventee{0};// 判断操作类型如果该fd已有事件注册则使用MOD否则使用ADDintopeventLoop-events[fd].maskAE_NONE?EPOLL_CTL_ADD:EPOLL_CTL_MOD;ee.events0;// 合并已有的事件类型MOD操作时保留之前注册的事件mask|eventLoop-events[fd].mask;/* Merge old events */// 将 Redis 的事件类型转换为epoll的事件类型if(maskAE_READABLE)ee.events|EPOLLIN;// 可读事件if(maskAE_WRITABLE)ee.events|EPOLLOUT;// 可写事件ee.data.fdfd;// 将fd存储在epoll_event.data中// 调用epoll_ctl注册或修改事件监听if(epoll_ctl(state-epfd,op,fd,ee)-1)return-1;return0;}这个函数的关键在于判断该使用EPOLL_CTL_ADD还是EPOLL_CTL_MOD。如果这个fd之前没有任何事件注册mask AE_NONE就使用ADD操作如果已经有事件在监听就使用MOD操作来修改。MOD操作时要注意合并已有的事件比如一个fd已经注册了可读事件现在又要注册可写事件就需要把两种事件合并后一起设置否则会覆盖掉之前的监听。事件类型转换也很直观Redis的AE_READABLE对应 epoll 的EPOLLINAE_WRITABLE对应EPOLLOUT。事件注册示例假设fd5当前无任何事件注册 1. 调用aeApiAddEvent(eventLoop, 5, AE_READABLE) 2. op EPOLL_CTL_ADD因为mask AE_NONE 3. ee.events EPOLLIN 4. 调用epoll_ctl(epfd, EPOLL_CTL_ADD, 5, ee) 假设fd5已注册AE_READABLE现在要添加AE_WRITABLE 1. 调用aeApiAddEvent(eventLoop, 5, AE_WRITABLE) 2. op EPOLL_CTL_MOD因为mask ! AE_NONE 3. ee.events EPOLLIN | EPOLLOUT合并已有事件 4. 调用 epoll_ctl(epfd, EPOLL_CTL_MOD, 5, ee)3.6 删除事件aeApiDelEvent// src/ae_epoll.c:90-106staticvoidaeApiDelEvent(aeEventLoop*eventLoop,intfd,intdelmask){aeApiState*stateeventLoop-apidata;structepoll_eventee{0};/* avoid valgrind warning */// 计算删除后剩余的事件类型intmaskeventLoop-events[fd].mask(~delmask);ee.events0;// 重新构建 epoll 事件掩码if(maskAE_READABLE)ee.events|EPOLLIN;if(maskAE_WRITABLE)ee.events|EPOLLOUT;ee.data.fdfd;if(mask!AE_NONE){// 还有其他事件使用MOD操作更新epoll_ctl(state-epfd,EPOLL_CTL_MOD,fd,ee);}else{// 所有事件都已删除从epoll中移除该fd/* Note, Kernel 2.6.9 requires a non null event pointer even for * EPOLL_CTL_DEL. */epoll_ctl(state-epfd,EPOLL_CTL_DEL,fd,ee);}}删除事件时需要考虑一个问题这个fd可能同时注册了可读和可写事件我们只想删除其中一个。所以函数首先计算删除后剩余的事件类型用位运算mask (~delmask)来实现。如果删除后还有剩余事件就用MOD操作更新如果全部删完了就用DEL操作把这个fd从epoll中移除。注意内核2.6.9之前的版本要求DEL 操作时event参数不能为NULL所以这里即使删除也传了一个有效的ee指针。事件删除示例假设 fd5已注册AE_READABLE | AE_WRITABLE 场景1删除AE_WRITABLE 1. mask (AE_READABLE | AE_WRITABLE) (~AE_WRITABLE) AE_READABLE 2. ee.events EPOLLIN 3. 调用epoll_ctl(epfd, EPOLL_CTL_MOD, 5, ee) 场景2删除AE_READABLE | AE_WRITABLE全部删除 1. mask (AE_READABLE | AE_WRITABLE) (~(AE_READABLE | AE_WRITABLE)) AE_NONE 2. 调用epoll_ctl(epfd, EPOLL_CTL_DEL, 5, ee)3.7 等待事件aeApiPoll// src/ae_epoll.c:108-131staticintaeApiPoll(aeEventLoop*eventLoop,structtimeval*tvp){aeApiState*stateeventLoop-apidata;intretval,numevents0;// 调用epoll_wait等待事件发生// 参数epoll实例fd、事件数组、数组大小、超时时间毫秒retvalepoll_wait(state-epfd,state-events,eventLoop-setsize,tvp?(tvp-tv_sec*1000tvp-tv_usec/1000):-1);if(retval0){intj;numeventsretval;// 遍历所有触发的事件for(j0;jnumevents;j){intmask0;structepoll_event*estate-eventsj;// 将 epoll 事件类型转换回Redis事件类型if(e-eventsEPOLLIN)mask|AE_READABLE;if(e-eventsEPOLLOUT)mask|AE_WRITABLE;// 错误和挂断事件也标记为可写便于上层处理if(e-eventsEPOLLERR)mask|AE_WRITABLE;if(e-eventsEPOLLHUP)mask|AE_WRITABLE;// 将触发的事件信息填充到fired数组eventLoop-fired[j].fde-data.fd;eventLoop-fired[j].maskmask;}}// 返回触发的事件数量returnnumevents;}这是事件循环的核心函数。它调用epoll_wait阻塞等待事件发生超时参数tvp为NULL时表示无限等待否则需要把秒和微秒转换成毫秒传给epoll。当有事件返回时遍历所有就绪的事件把epoll的事件类型转换回Redis内部的类型然后填充到eventLoop-fired数组中供上层使用。值得注意的是对EPOLLERR和EPOLLHUP的处理。这两个异常事件被标记为可写事件这样上层的写回调函数就能被触发去处理异常情况。这是一种巧妙的设计避免了单独处理异常事件的复杂性。3.8 获取机制名称aeApiName// src/ae_epoll.c:133-135staticchar*aeApiName(void){returnepoll;}用于日志输出和调试信息。四、epoll封装的完整调用链五、Redis对epoll的优化点5.1 预分配事件数组state-eventszmalloc(sizeof(structepoll_event)*eventLoop-setsize);在创建epoll实例时就预先分配好事件数组避免每次调用epoll_wait时临时分配内存。对于Redis这种高频调用I/O多路复用的服务来说减少内存分配次数对性能有实际意义。5.2 事件合并策略mask|eventLoop-events[fd].mask;/* Merge old events */添加新事件时先查询该fd已注册的事件然后合并后再设置。这样即使一个fd需要分别注册可读和可写事件也不需要调用两次epoll_ctl减少系统调用次数。5.3 使用水平触发LTRedis使用epoll的默认触发模式——水平触发Level Triggered而非边缘触发Edge Triggered。水平触发的特点是只要fd处于可读或可写状态epoll_wait就会持续通知。相比之下边缘触发只在状态变化时通知一次必须一次性把数据读完或写完否则可能遗漏事件。Redis选择水平触发的原因很简单编程模型更直观可以灵活控制每次读写的数据量配合非阻塞I/O使用也不会有阻塞风险。边缘触发虽然理论上性能更高但编程复杂度也随之增加对于Redis这种单线程事件驱动的架构来说水平触发已经足够高效。5.4 动态容量调整staticintaeApiResize(aeEventLoop*eventLoop,intsetsize){aeApiState*stateeventLoop-apidata;state-eventszrealloc(state-events,sizeof(structepoll_event)*setsize);return0;}Redis支持运行时通过CONFIG SET maxclients调整最大连接数事件数组的大小也需要相应调整。这个函数使得Redis无需重启就能根据负载情况动态调整容量。六、总结本文详细分析了Redis对epoll的封装实现核心要点如下APIepoll系统调用功能aeApiCreateepoll_create创建epoll实例aeApiAddEventepoll_ctl(ADD/MOD)注册/修改事件aeApiDelEventepoll_ctl(MOD/DEL)删除事件aeApiPollepoll_wait等待事件触发aeApiFreeclose(epfd)释放资源下一篇文章将分析Redis对kqueue的封装实现展示Redis是如何在BSD/macOS系统上实现高性能I/O多路复用。欢迎各位同学关注我哦~在这个 AI 喧嚣的时代不忘初心戒骄戒躁认真沉淀