ngx_epoll_init

张开发
2026/4/21 1:42:37 15 分钟阅读

分享文章

ngx_epoll_init
1 定义ngx_epoll_init 函数 定义在 ./nginx-1.24.0/src/event/modules/ngx_epoll_module.cstaticngx_int_tngx_epoll_init(ngx_cycle_t*cycle,ngx_msec_ttimer){ngx_epoll_conf_t*epcf;epcfngx_event_get_conf(cycle-conf_ctx,ngx_epoll_module);if(ep-1){epepoll_create(cycle-connection_n/2);if(ep-1){ngx_log_error(NGX_LOG_EMERG,cycle-log,ngx_errno,epoll_create() failed);returnNGX_ERROR;}#if(NGX_HAVE_EVENTFD)if(ngx_epoll_notify_init(cycle-log)!NGX_OK){ngx_epoll_module_ctx.actions.notifyNULL;}#endif#if(NGX_HAVE_FILE_AIO)ngx_epoll_aio_init(cycle,epcf);#endif#if(NGX_HAVE_EPOLLRDHUP)ngx_epoll_test_rdhup(cycle);#endif}if(neventsepcf-events){if(event_list){ngx_free(event_list);}event_listngx_alloc(sizeof(structepoll_event)*epcf-events,cycle-log);if(event_listNULL){returnNGX_ERROR;}}neventsepcf-events;ngx_iongx_os_io;ngx_event_actionsngx_epoll_module_ctx.actions;#if(NGX_HAVE_CLEAR_EVENT)ngx_event_flagsNGX_USE_CLEAR_EVENT#elsengx_event_flagsNGX_USE_LEVEL_EVENT#endif|NGX_USE_GREEDY_EVENT|NGX_USE_EPOLL_EVENT;returnNGX_OK;}ngx_epoll_init 函数是 Nginx epoll 事件模块的初始化函数2详解1 函数签名staticngx_int_tngx_epoll_init(ngx_cycle_t*cycle,ngx_msec_ttimer)返回值 NGX_OK初始化成功。 NGX_ERROR初始化失败参数1 ngx_cycle_t *cycle 指向当前运行周期上下文 参数2 ngx_msec_t timer 本次初始化传入的定时器值毫秒 在本函数体内并未使用。 保留该参数是为了与其它事件模块的初始化接口保持一致。2 逻辑流程1 获取配置 2 首次创建 epoll 实例懒初始化 3 可选高级特性初始化 4 事件数组动态扩容 5 更新全局状态变量 6 挂载 epoll 操作函数表 7 设置事件机制特性标志位 8 返回成功1 获取配置{ngx_epoll_conf_t*epcf;epcfngx_event_get_conf(cycle-conf_ctx,ngx_epoll_module);2 首次创建 epoll 实例懒初始化if(ep-1){epepoll_create(cycle-connection_n/2);if(ep-1){ngx_log_error(NGX_LOG_EMERG,cycle-log,ngx_errno,epoll_create() failed);returnNGX_ERROR;}惰性初始化检查。 ep 是模块静态变量初始为 -1,表示 epoll 文件描述符 若 ep 仍为 -1说明这是第一次调用 ngx_epoll_init或之前未成功创建 需要创建新的 epoll 实例。调用 Linux 系统调用 epoll_create 创建一个 epoll 实例 参数 size 在 Linux 2.6.8 之后已被忽略但需大于 0。 Nginx 使用 cycle-connection_n / 2 作为初始提示大小 该值为每个 worker 最大连接数的一半一个相对合理的估算值。3 可选高级特性初始化#if(NGX_HAVE_EVENTFD)if(ngx_epoll_notify_init(cycle-log)!NGX_OK){ngx_epoll_module_ctx.actions.notifyNULL;}#endif#if(NGX_HAVE_FILE_AIO)ngx_epoll_aio_init(cycle,epcf);#endif#if(NGX_HAVE_EPOLLRDHUP)ngx_epoll_test_rdhup(cycle);#endif}#1 NGX_HAVE_EVENTFD Nginx 在 configure 阶段检测系统是否支持 eventfd 系统调用Linux 2.6.22 及以上版本后定义的宏。 支持时值为 1否则未定义。 eventfd 是 Linux 特有的机制 如果系统支持 eventfdLinux 2.6.22 则初始化用于 post 事件通知 的机制。 ngx_epoll_notify_init Nginx 内部函数 用于创建并初始化一个 eventfd 对象 并将其注册到 epoll 实例中。 若初始化失败 则禁用通知功能将函数表里的 notify 指针置空 不会影响核心事件循环。 eventfd 的用途 在 Nginx 的事件循环中 使用 eventfd 与 不使用 eventfd 的核心区别在于 如何主动唤醒阻塞在 epoll_wait 上的主线程。 对比 #1 使用 eventfd 唤醒介质 一个专用的、由内核维护的 eventfd 文件描述符。 无专用唤醒介质。 触发方式 其他线程/信号处理函数调用 write() 向 eventfd 写入数据触发 EPOLLIN 事件 epoll_wait 返回条件 1 监听的 Socket 有 I/O 活动。 2 eventfd 变得可读。 3 设定的超时时间到达。 #2 不使用 eventfd 唤醒介质 无专用唤醒介质。 触发方式 依赖 超时返回 或 其他 I/O 事件 顺带唤醒。 epoll_wait 返回条件 1 监听的 Socket 有 I/O 活动。 2 设定的超时时间到达。 有 eventfd 时epoll 是“呼之即来”的即时响应系统 没有 eventfd 时epoll 退化为“定时查看”的轮询等待系统。 eventfd 不改变 epoll 监听网络 I/O 的能力 而是为事件循环提供了一条更安全、更快、更省资源的“自我打断”通道。 在现代 Linux 异步服务器架构中epoll eventfd 已成为事实标准 而不使用 eventfd 的方案通常仅作为兼容性兜底或历史遗留实现。 eventfd 是给 epoll 一个额外的描述符 没有它 epoll 只能在定时器超时和监听的事件发生时才被唤醒 (严格来说epoll_wait 还可被 信号(signal) 中断返回) 而 eventfd 是专用于主动唤醒 epoll 的在没有超时也没有事件发生时 通过 写入eventfd (写入的数据Value不仅是唤醒信号还是“任务计数”。 如果写 1epoll 醒来read 消费掉 1代表有 1 个 待处理通知。 如果连续写 3 次 1epoll 只被唤醒一次可读状态已触发但 read 会读出 3。 这种设计使得 即使唤醒速度慢于通知产生速度也不会丢失通知次数) 让 eventfd 可读可以主动的唤醒 epoll 加强了对 epoll 的控制管理#2 如果系统支持文件异步 I/ONGX_HAVE_FILE_AIO 则调用该函数初始化与 epoll 的集成。 该函数会为 AIO 上下文设置一个专用的 epoll 文件描述符或管道 以便异步 I/O 完成时能触发 epoll 事件。 epoll 与 AIO 的本质区别 epoll 是“事件通知”机制网络I/O epoll 本质上是 I/O 多路复用 (I/O Multiplexing)。 它监控多个文件描述符一旦某个就绪如网络数据到达便通知程序去处理。 但关键点在于这个“处理”步骤即 read()/write() 调用本身是同步的会在该阶段阻塞 直到数据从内核空间拷贝到用户空间完成。 AIO 是“全程异步”机制磁盘I/O AIO (Asynchronous I/O) 则更进一步。 程序发起 I/O 请求后直接返回后续的数据传输和准备完全由内核完成。 当一切就绪程序通过回调或信号得到最终通知全程非阻塞。 不使用 AIO (默认路径) Nginx 默认的磁盘 I/O 是同步阻塞的 read()/write()。 worker 进程会在读取磁盘文件时被短暂阻塞直到数据被读入内存。 在高并发下这会引发“惊群效应”多个 worker 因磁盘操作阻塞最终导致 CPU 空转、并发能力下降。 使用 AIO (加速路径) Nginx 配置 aio on 后通过 io_submit/io_getevents 发起直接 I/O (Direct I/O) 内核 DMA 直接将数据从磁盘读入内存完成后通过 eventfd 通知 Nginx。 worker 进程在等待磁盘 I/O 的间隙可以处理其他网络事件。 总结内核层面的同步与异步 不使用 AIO epoll_wait 返回后仍需主动、同步地调用 read()/write() 来搬移数据数据从内核拷贝到用户态时会阻塞。 使用 AIO 通过 io_submit 将 I/O 请求直接提交给内核处理待 I/O 完成通知才处理数据完全非阻塞。 盲目启用 aio on 并非总是最佳实践。 对于普通静态 Web 服务器sendfile on; 通常是更稳定高效的选择。 也就说 有了 AIO 后 对于读写操作只要发送命令给内核 内核便会自行让文件系统去处理这时 epoll 可以同时去做其他事情 epoll 如何知道内核干完活了 epoll 并不是通过“轮询”去问内核“你好了没” 而是通过 eventfd 让内核“反通知” epoll。 发送命令时 ngx_file_aio_read 会设置一个特殊标志 IOCB_FLAG_RESFD 并把 ngx_eventfd 的文件描述符塞给内核。 epoll 去做别的事主循环愉快地跑 epoll_wait处理网络 I/O。 内核干完活磁盘数据准备好内核直接向那个 ngx_eventfd 写入一个 8 字节数据。 epoll 被唤醒 因为 ngx_eventfd 在 epoll 监听列表中epoll_wait 立刻返回主 线程一看是 eventfd 有动静就知道“磁盘数据好了”。 AIO 让 Worker 线程“发完命令就撤” epoll 负责“盯完成通知” 线程利用等待期并行处理其他网络请求 从而实现单 Worker 高并发下的磁盘 I/O 零阻塞。#3 NGX_HAVE_EPOLLRDHUP Nginx 在 configure 阶段通过检测系统头文件sys/epoll.h是否定义了 EPOLLRDHUP 宏而设置。 若支持该宏被定义为 1否则整个代码块被跳过。 EPOLLRDHUP 是 Linux 2.6.17 内核引入的事件标志。 它允许 epoll 精确地检测到对端半关闭连接即对端调用 shutdown(SHUT_WR) 或仅关闭写端。 老旧内核可能虽定义了该宏但实际存在 Bug 或不支持因此需要进行运行时测试。 ngx_epoll_test_rdhup 函数通过创建一个真实的 socketpair 并模拟半关闭行为来确认内核的行为是否正确 若测试成功设置全局标志 ngx_epoll_flags | NGX_USE_EPOLLRDHUP#1 不使用 EPOLLRDHUP 当客户端发送 FIN半关闭或异常断开时 Linux 内核不会主动通知 epoll。 该 Socket 仅变为“可读”状态触发 EPOLLIN。应用必须调用 read() 若返回 0正常数据 若返回 0对端已关闭写端连接终止 若返回 -1 且 errnoEAGAIN无数据连接仍存活 缺陷无法通过 epoll 直接区分“新数据到达”和“连接已关闭”必须依赖 read() 结果反推。 #2 使用 EPOLLRDHUP 内核在检测到对端发送 FIN 或连接异常断开时 直接在 epoll_event.events 中设置 EPOLLRDHUP 位。 应用无需 read() 试探即可明确知道“对端已挂起写端”。 也就是说 使用 EPOLLRDHUP 后可以在对面 终止连接不再写数据时 立刻获知这一状态变化 若不使用 EPOLLRDHUP 则只能等到做读取数据操作 但读不到任何有效数据时才获知这一状态变化4 事件数组动态扩容if(neventsepcf-events){if(event_list){ngx_free(event_list);}event_listngx_alloc(sizeof(structepoll_event)*epcf-events,cycle-log);if(event_listNULL){returnNGX_ERROR;}}#1 nevents 记录当前已分配的 event_list 数组能容纳的最大事件数量 epcf-events 从 epoll 模块配置中读取的 events 字段值对应 epoll_events 指令。 它表示期望的数组容量即 epoll_wait 一次最多返回的事件数。 比较当前已分配的缓冲区容量 nevents 与配置要求容量 epcf-events。 仅当新配置要求更大时才进入分配逻辑。#2 判断全局指针 event_list 是否已指向有效内存。 若 event_list 不为 NULL说明此前已分配过内存 需要在分配新内存前先释放旧内存 首次运行时 event_list 为 NULL此条件不成立直接跳过释放步骤#3 分配新缓冲区 根据配置的 epcf-events 值 分配一块能够容纳该数量的 epoll_event 结构体的连续内存。5 更新全局状态变量neventsepcf-events;ngx_iongx_os_io;#1 同步缓冲区容量状态 若前一步因扩容重新分配了 event_list 数组 或因配置重载导致容量发生变化此处将静态变量 nevents 更新为最新值。 这使得后续调用 epoll_wait 时能正确传递数组大小参数#2 绑定 OS 专属 I/O 操作集 ngx_io Nginx 全局 I/O 操作函数表指针类型为 ngx_io_t 用于执行底层的 read、write、sendfile 等系统调用。 ngx_os_io Nginx 在操作系统抽象层定义的一组默认 I/O 函数实现 如 ngx_unix_recv、ngx_unix_send 等。 poll 模块本身不改变文件读写行为因此将 ngx_io 指向系统默认的 I/O 函数集。 这确保在 epoll 接管事件驱动后连接上的数据收发仍然使用标准的系统调用。6 挂载 epoll 操作函数表ngx_event_actionsngx_epoll_module_ctx.actions;注册 epoll 事件操作接口 ngx_event_actions Nginx 事件核心模块的全局事件操作表指针类型为 ngx_event_actions_t。 它定义了事件驱动机制的接口add_event、del_event、process_events 等。 ngx_epoll_module_ctx.actions epoll 模块上下文结构体中预定义的一组操作函数集合 包含 ngx_epoll_add_event、ngx_epoll_del_event、ngx_epoll_process_events 等具体实现。 逻辑与意义 注册 epoll 为实现引擎 通过这行赋值Nginx 的事件框架就知道后续所有对事件的添加、删除、等待操作都应当调用 epoll 模块提供的函数。这是实现多事件机制可插拔的关键步骤。 控制权转移 一旦赋值完成Nginx 的事件循环将完全由 epoll 驱动直到进程结束或配置重载切换到其他模块。7 设置事件机制特性标志位#if(NGX_HAVE_CLEAR_EVENT)ngx_event_flagsNGX_USE_CLEAR_EVENT#elsengx_event_flagsNGX_USE_LEVEL_EVENT#endif|NGX_USE_GREEDY_EVENT|NGX_USE_EPOLL_EVENT;#4 NGX_HAVE_CLEAR_EVENT 编译期宏表示该平台是否支持边缘触发模式。 在 Linux 系统上该宏通常被定义为 1因为 epoll 支持 EPOLLET。 ngx_event_flags Nginx 全局事件特性标志位ngx_uint_t 类型 用于向上层事件处理逻辑告知当前事件模块的能力边界。 NGX_USE_CLEAR_EVENT 标志位宏表示使用边缘触发模式事件就绪仅通知一次。 NGX_USE_LEVEL_EVENT 标志位宏表示使用水平触发模式事件未处理完会持续通知。 逻辑与意义 触发模式声明Nginx 在 epoll 环境中优先使用边缘触发EPOLLET 因为它能减少事件通知次数提高效率。 如果系统不支持边缘触发则回退到水平触发模式。 此处的条件编译确保生成的二进制代码与平台能力匹配。 影响范围 上层事件处理函数如 ngx_handle_read_event会根据此标志调整行为 例如在边缘触发模式下必须循环读取数据直到返回 EAGAIN否则可能丢失事件。#5 NGX_USE_GREEDY_EVENT 标志位宏表示该事件模块采用贪婪处理策略。 即当 process_events 被调用时会尽可能多地处理就绪事件而不是处理一个就返回。 这有助于减少上下文切换提升吞吐量。 NGX_USE_EPOLL_EVENT 标志位宏明确标识当前正在使用 epoll 机制。 逻辑与意义 补充特性声明 通过按位或|将这些预定义的标志位组合到 ngx_event_flags 中 向事件框架完整描述了 epoll 模块的全部能力。 最终效果 假设在 Linux 平台ngx_event_flags 最终的值将同时包含 NGX_USE_CLEAR_EVENT边缘触发 NGX_USE_GREEDY_EVENT贪婪处理 NGX_USE_EPOLL_EVENTepoll 特定8 返回成功returnNGX_OK;}

更多文章