ngx_event_accept 1 定义ngx_event_accept 函数 定义在 ./nginx-1.24.0/src/event/ngx_event_accept.c2 作用ngx_event_accept 是 Nginx 中处理新连接到达的核心函数。 它从监听套接字上 accept 新的客户端连接 为其创建并初始化 ngx_connection_t 结构体 然后调用监听端口配置的 handler 将连接交给上层协议如 HTTP处理。3 详解1 函数签名voidngx_event_accept(ngx_event_t*ev)1. 返回值类型 void 含义 函数不返回任何值调用者无法获得操作成功或失败的直接反馈。 ngx_event_accept 是一个 事件处理器event handler 由事件框架在监听套接字上有新连接到达时回调。 它的职责是 “尽可能地接受并初始化新连接” 然后将连接交给上层协议如 HTTP处理而不需要向事件分发器汇报结果。 函数内部遇到的所有错误 都会被当场处理 记录日志、暂时禁用监听事件、释放资源等。 上层事件循环无需知道每次 accept 的细节只需继续等待下一次事件。2. 函数名 ngx_event_accept 命名空间前缀 ngx_Nginx 函数的标准前缀。 event_accept 准确描述了函数的用途 —— 处理监听套接字上的 “可接受新连接” 事件。 event表明这是一个事件处理函数。 accept直接对应 TCP 的 accept 系统调用表示接收新连接。 整体语义 该函数是 Nginx 中所有监听端口上新连接到达的统一入口。 无论是 HTTP、HTTPS、Stream 还是 Mail 当监听 socket 上有新连接时 最终都会调用此函数来执行 accept 并初始化连接。3. 参数 ngx_event_t *ev 类型 指向 ngx_event_t 结构体的指针 这是 Nginx 事件系统的核心数据结构代表一个 注册在事件驱动机制中的事件。总结 ngx_event_accept 的函数签名是 Nginx 事件处理器接口的经典范例 - void 返回值 体现了回调函数的 “执行并遗忘” 特性错误处理内聚在函数内部。 - 函数名直接揭示了它的业务含义处理 TCP 连接的 accept 事件。 - 唯一的 ev 参数 作为事件对象的抽象承载了触发上下文的所有信息 使得框架能够以极简的接口驱动复杂的网络 I/O 处理。2 逻辑流程1 文件描述符耗尽时的延迟重试 2 设置一次性接受连接的数量 3 接受新连接 3-1 提取监听连接与监听配置重置 ready 标志 3-2 循环持续接受新连接 3-2-1 调用 accept 获取新套接字 3-2-2 accept 失败 3-2-3 accept 成功{socklen_tsocklen;ngx_err_terr;ngx_log_t*log;ngx_uint_tlevel;ngx_socket_ts;ngx_event_t*rev,*wev;ngx_sockaddr_tsa;ngx_listening_t*ls;ngx_connection_t*c,*lc;ngx_event_conf_t*ecf;#if(NGX_HAVE_ACCEPT4)staticngx_uint_tuse_accept41;#endif局部变量1 文件描述符耗尽时的延迟重试if(ev-timedout){if(ngx_enable_accept_events((ngx_cycle_t*)ngx_cycle)!NGX_OK){return;}ev-timedout0;}当服务器因为文件描述符耗尽而暂时无法接受新连接时 通过定时器延迟重试并在重试时恢复监听。if (ev-timedout) { timedout 是一个标志位 当一个事件被触发不是因为它所等待的 I/O 操作就绪 而是因为一个关联的 定时器超时 了Nginx 就会将该事件的 timedout 设置为 1。 什么时候会发生这种情况 当 worker 进程的可用连接数文件描述符达到了上限worker_connections 它就无法再调用 accept 接受新连接。 此时Nginx 会把这个监听事件从 epoll 等事件驱动模块中暂时移除disable 避免 epoll 持续报告可读事件同时设置一个定时器。 当定时器到期后事件模块会再次触发这个监听事件并标记 ev-timedout 1 于是 ngx_event_accept 被调用 进入这个条件 调用 ngx_enable_accept_events 重新尝试恢复监听事件以便继续接受新连接if (ngx_enable_accept_events((ngx_cycle_t *) ngx_cycle) ! NGX_OK) { 它的作用是 重新将之前被移除的监听事件注册到事件驱动模块中。 返回值检查 该函数返回 NGX_OK 表示重新启用监听事件成功否则表示失败。 如果失败意味着无法恢复监听那么继续执行 accept 也没有意义 所以直接 return退出当前事件处理。ev-timedout 0; 成功重新启用监听事件后将 ev-timedout 标志 清零 这样下次该事件被触发时 timedout 已经恢复为初始状态0 避免错误地再次进入这个超时处理分支。总结 这段代码实现了 Nginx 在 文件描述符耗尽 时的一种 延迟重试 机制 1. 当连接数满 worker 进程无法 accept它不会忙等而是主动移除了监听事件并设置定时器。 2. 超时后重试 定时器到期后事件回调再次进入 ngx_event_accept 但此时 timedout1表明这不是真正的新连接到达而是一次 “请重新尝试监听” 的通知。 3. 恢复监听 代码检查到超时标志就调用 ngx_enable_accept_events 将监听事件重新加回 epoll。 如果恢复成功则清除标志如果恢复失败则直接返回等待后续可能的再次尝试。2 设置一次性接受连接的数量ecfngx_event_get_conf(ngx_cycle-conf_ctx,ngx_event_core_module);if(!(ngx_event_flagsNGX_USE_KQUEUE_EVENT)){ev-availableecf-multi_accept;}获取事件核心模块的配置判断当前使用的事件模型是否为 Kqueue 在 Kqueue 模型中监听事件返回时 内核会直接告诉应用程序当前有多少个新连接已经就绪 这个数值会被填充到 ev-available 中。 因此 Kqueue 下 ev-available 的值是由内核提供的准确数量不应该被配置覆盖。 所以代码跳过了对它的赋值直接保留内核给出的值。设置 ev-available 的值 ecf-multi_accept 是 multi_accept 指令的值。 这是一个配置指令可以设置为 on 或 off。 当 multi_accept off 时该值为 0。 当 multi_accept on 时该值为 1 ev-available 是 ngx_event_t 结构体中的一个整型字段。 在 accept 过程中它作为一个循环控制变量使用 函数末尾是一个 do ... while (ev-available) 循环 只要 ev-available 的值不为 0循环就会继续尝试 accept 更多的连接。3 接受新连接lcev-data;lslc-listening;ev-ready0;提取监听连接与监听配置重置 ready 标志ngx_log_debug2(NGX_LOG_DEBUG_EVENT,ev-log,0,accept on %V, ready: %d,ls-addr_text,ev-available);do{socklensizeof(ngx_sockaddr_t);#if(NGX_HAVE_ACCEPT4)if(use_accept4){saccept4(lc-fd,sa.sockaddr,socklen,SOCK_NONBLOCK);}else{saccept(lc-fd,sa.sockaddr,socklen);}#elsesaccept(lc-fd,sa.sockaddr,socklen);#endif调用 accept 获取新套接字处理 accept 失败if(s(ngx_socket_t)-1){errngx_socket_errno;if(errNGX_EAGAIN){ngx_log_debug0(NGX_LOG_DEBUG_EVENT,ev-log,err,accept() not ready);return;}levelNGX_LOG_ALERT;if(errNGX_ECONNABORTED){levelNGX_LOG_ERR;}elseif(errNGX_EMFILE||errNGX_ENFILE){levelNGX_LOG_CRIT;}#if(NGX_HAVE_ACCEPT4)ngx_log_error(level,ev-log,err,use_accept4?accept4() failed:accept() failed);if(use_accept4errNGX_ENOSYS){use_accept40;ngx_inherited_nonblocking0;continue;}#elsengx_log_error(level,ev-log,err,accept() failed);#endifif(errNGX_ECONNABORTED){if(ngx_event_flagsNGX_USE_KQUEUE_EVENT){ev-available--;}if(ev-available){continue;}}if(errNGX_EMFILE||errNGX_ENFILE){if(ngx_disable_accept_events((ngx_cycle_t*)ngx_cycle,1)!NGX_OK){return;}if(ngx_use_accept_mutex){if(ngx_accept_mutex_held){ngx_shmtx_unlock(ngx_accept_mutex);ngx_accept_mutex_held0;}ngx_accept_disabled1;}else{ngx_add_timer(ev,ecf-accept_mutex_delay);}}return;}accept 成功#if(NGX_STAT_STUB)(void)ngx_atomic_fetch_add(ngx_stat_accepted,1);#endifngx_accept_disabledngx_cycle-connection_n/8-ngx_cycle-free_connection_n;计算 ngx_accept_disabled 若空闲连接池剩余数量低于总容量的 1/8计算结果为正数后续将触发接收节流机制。cngx_get_connection(s,ev-log);if(cNULL){if(ngx_close_socket(s)-1){ngx_log_error(NGX_LOG_ALERT,ev-log,ngx_socket_errno,ngx_close_socket_n failed);}return;}为新连接分配连接对象c-typeSOCK_STREAM;设置连接类型 将连接的类型设置为 SOCK_STREAM表示这是一个 TCP 流式连接。#if(NGX_STAT_STUB)(void)ngx_atomic_fetch_add(ngx_stat_active,1);#endifc-poolngx_create_pool(ls-pool_size,ev-log);if(c-poolNULL){ngx_close_accepted_connection(c);return;}创建连接专属内存池if(socklen(socklen_t)sizeof(ngx_sockaddr_t)){socklensizeof(ngx_sockaddr_t);}c-sockaddrngx_palloc(c-pool,socklen);if(c-sockaddrNULL){ngx_close_accepted_connection(c);return;}ngx_memcpy(c-sockaddr,sa,socklen);保存客户端地址logngx_palloc(c-pool,sizeof(ngx_log_t));if(logNULL){ngx_close_accepted_connection(c);return;}分配日志对象设置套接字的阻塞或非阻塞模式/* set a blocking mode for iocp and non-blocking mode for others */if(ngx_inherited_nonblocking){if(ngx_event_flagsNGX_USE_IOCP_EVENT){if(ngx_blocking(s)-1){ngx_log_error(NGX_LOG_ALERT,ev-log,ngx_socket_errno,ngx_blocking_n failed);ngx_close_accepted_connection(c);return;}}}else{if(!(ngx_event_flagsNGX_USE_IOCP_EVENT)){if(ngx_nonblocking(s)-1){ngx_log_error(NGX_LOG_ALERT,ev-log,ngx_socket_errno,ngx_nonblocking_n failed);ngx_close_accepted_connection(c);return;}}}ngx_inherited_nonblocking 一个全局标志指示新接受的套接字是否已经自动设置为非阻塞。 在 Linux 上如果使用了 accept4 并成功这个标志为 1如果没有则需要手动设置。 IOCP 特殊处理IOCPWindows 下的完成端口需要套接字为阻塞模式 所以如果当前事件模型是 IOCP则调用 ngx_blocking 将套接字设为阻塞。 其他事件模型 默认epoll, kqueue 等都要求非阻塞模式 因此如果继承的非阻塞标志为 0即需要手动设置 且不是 IOCP 模型则调用 ngx_nonblocking 设置 O_NONBLOCK 标志。 任何设置失败都会关闭连接并返回确保后续不会在错误的阻塞模式下运行。*logls-log;初始化日志c-recvngx_recv;c-sendngx_send;c-recv_chainngx_recv_chain;c-send_chainngx_send_chain;初始化 基础接收/发送函数 recv、send基本的接收和发送函数对应系统调用 read/write 的封装。 recv_chain、send_chain用于处理 ngx_chain_t 链表形式的批量收发c-loglog;c-pool-loglog;将连接对象和连接内存池的日志指针都指向新创建的日志对象。 这样通过连接或池进行任何内存分配或日志输出都会使用同一个日志上下文c-socklensocklen;c-listeningls;c-local_sockaddrls-sockaddr;c-local_socklenls-socklen;保存监听配置和地址信息 将客户端地址长度、监听配置结构体指针、 本地监听地址及长度保存到连接对象中。 这些信息后续可能用于日志、匹配虚拟主机、连接管理等功能。Unix 域套接字的特殊处理#if(NGX_HAVE_UNIX_DOMAIN)if(c-sockaddr-sa_familyAF_UNIX){c-tcp_nopushNGX_TCP_NOPUSH_DISABLED;c-tcp_nodelayNGX_TCP_NODELAY_DISABLED;#if(NGX_SOLARIS)/* Solariss sendfilev() supports AF_NCA, AF_INET, and AF_INET6 */c-sendfile0;#endif}#endif获取读写事件对象并设置初始状态revc-read;wevc-write;wev-ready1;写事件初始设为就绪ready 1 表示连接一建立就可以发送数据if(ngx_event_flagsNGX_USE_IOCP_EVENT){rev-ready1;}对于 IOCP 模型读事件也初始设为就绪因为 IOCP 的接受方式不同。if(ev-deferred_accept){rev-ready1;#if(NGX_HAVE_KQUEUE||NGX_HAVE_EPOLLRDHUP)rev-available1;#endif}果监听套接字上设置了 deferred_accept 选项TCP_DEFER_ACCEPT 则 accept 返回的连接上已经有数据到达客户端的第一个数据包已经在内核缓冲区中 因此读事件可以立即认为是就绪的从而直接开始读取请求减少一次事件循环。 rev-available 1 表示有数据可读。rev-loglog;wev-loglog;将读写事件的日志对象也指向新连接的日志对象以便事件处理时输出日志/* * TODO: MT: - ngx_atomic_fetch_add() * or protection by critical section or light mutex * * TODO: MP: - allocated in a shared memory * - ngx_atomic_fetch_add() * or protection by critical section or light mutex */c-numberngx_atomic_fetch_add(ngx_connection_counter,1);c-start_timengx_current_msec;分配连接编号和记录开始时间#if(NGX_STAT_STUB)(void)ngx_atomic_fetch_add(ngx_stat_handled,1);#endif统计已处理的连接数生成客户端地址的可读文本if(ls-addr_ntop){c-addr_text.datangx_pnalloc(c-pool,ls-addr_text_max_len);if(c-addr_text.dataNULL){ngx_close_accepted_connection(c);return;}c-addr_text.lenngx_sock_ntop(c-sockaddr,c-socklen,c-addr_text.data,ls-addr_text_max_len,0);if(c-addr_text.len0){ngx_close_accepted_connection(c);return;}}#if(NGX_DEBUG){ngx_str_taddr;u_char text[NGX_SOCKADDR_STRLEN];ngx_debug_accepted_connection(ecf,c);if(log-log_levelNGX_LOG_DEBUG_EVENT){addr.datatext;addr.lenngx_sock_ntop(c-sockaddr,c-socklen,text,NGX_SOCKADDR_STRLEN,1);ngx_log_debug3(NGX_LOG_DEBUG_EVENT,log,0,*%uA accept: %V fd:%d,c-number,addr,s);}}#endif调试日志输出非 epoll 事件模型下注册连接事件if(ngx_add_conn(ngx_event_flagsNGX_USE_EPOLL_EVENT)0){if(ngx_add_conn(c)NGX_ERROR){ngx_close_accepted_connection(c);return;}}对于 epollNginx 采用更高效的边沿触发模式并延迟添加事件 直到确实需要等待某个事件时才调用 ngx_add_event 因此这里对 epoll 模型跳过直接添加。对于其他模型则立即注册。log-dataNULL;log-handlerNULL;ls-handler(c);调用上层协议处理函数 ls-handler 是在配置监听端口时设置的回调函数 这个函数将接管连接开始读取请求、解析协议。从此该连接的生命周期就由上层协议模块管理。if(ngx_event_flagsNGX_USE_KQUEUE_EVENT){ev-available--;}Kqueue 模型下的 available 递减 在 Kqueue 模型中ev-available 初始由内核设置表示就绪的连接数。 每成功接受一个连接递减该计数器直到 0 时循环结束。}while(ev-available);循环条件 检查 ev-available 是否为非 0。 在非 Kqueue 模型中如果 multi_accept on ev-available 在函数开始时被设置为 1 并且循环内未被修改除非遇到 ECONNABORTED 等特定错误 因此会一直循环直到 accept 返回错误并触发 return从而跳出整个函数。 如果 multi_accept offev-available 为 0循环只执行一次。 这种设计让 multi_accept on 时 可以在一次事件处理中连续接受多个新连接 减少了事件循环的开销。#if(NGX_HAVE_EPOLLEXCLUSIVE)ngx_reorder_accept_events(ls);#endif}EPOLLEXCLUSIVE 重排接受事件 如果系统支持 EPOLLEXCLUSIVELinux 4.5并且监听套接字设置了该选项 则多个 worker 可能会同时监听同一个套接字 但在任一时刻只有一个 worker 被唤醒。 ngx_reorder_accept_events 用于在每次处理完一批新连接后 调整监听事件在 epoll 中的位置或优先级 以确保新连接能在不同的 worker 之间更公平地分布避免单个 worker 垄断。