Elastic UI Framework头像和徽章组件:打造专业用户界面的终极指南
2026/6/17 7:07:17
ngx_event_accept 函数 定义在 ./nginx-1.24.0/src/event/ngx_event_accept.cngx_event_accept 是 Nginx 中处理新连接到达的核心函数。 它从监听套接字上 accept 新的客户端连接, 为其创建并初始化 `ngx_connection_t` 结构体, 然后调用监听端口配置的 handler 将连接交给上层协议(如 HTTP)处理。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 处理。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_accept4=1;#endif局部变量if(ev->timedout){if(ngx_enable_accept_events((ngx_cycle_t*)ngx_cycle)!=NGX_OK){return;}ev->timedout=0;}当服务器因为文件描述符耗尽而暂时无法接受新连接时, 通过定时器延迟重试,并在重试时恢复监听。`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`, 但此时 `timedout=1`,表明这不是真正的新连接到达,而是一次 “请重新尝试监听” 的通知。 3. 恢复监听: 代码检查到超时标志,就调用 `ngx_enable_accept_events` 将监听事件重新加回 epoll。 如果恢复成功,则清除标志;如果恢复失败,则直接返回,等待后续可能的再次尝试。ecf=ngx_event_get_conf(ngx_cycle->conf_ctx,ngx_event_core_module);if(!(ngx_event_flags&NGX_USE_KQUEUE_EVENT)){ev->available=ecf->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 更多的连接。lc=ev->data;ls=lc->listening;ev->ready=0;提取监听连接与监听配置,重置 ready 标志ngx_log_debug2(NGX_LOG_DEBUG_EVENT,ev->log,0,"accept on %V, ready: %d",&ls->addr_text,ev->available);do{socklen=sizeof(ngx_sockaddr_t);#if(NGX_HAVE_ACCEPT4)if(use_accept4){s=accept4(lc->fd,&sa.sockaddr,&socklen,SOCK_NONBLOCK);}else{s=accept(lc->fd,&sa.sockaddr,&socklen);}#elses=accept(lc->fd,&sa.sockaddr,&socklen);#endif调用 accept 获取新套接字处理 accept 失败if(s==(ngx_socket_t)-1){err=ngx_socket_errno;if(err==NGX_EAGAIN){ngx_log_debug0(NGX_LOG_DEBUG_EVENT,ev->log,err,"accept() not ready");return;}level=NGX_LOG_ALERT;if(err==NGX_ECONNABORTED){level=NGX_LOG_ERR;}elseif(err==NGX_EMFILE||err==NGX_ENFILE){level=NGX_LOG_CRIT;}#if(NGX_HAVE_ACCEPT4)ngx_log_error(level,ev->log,err,use_accept4?"accept4() failed":"accept() failed");if(use_accept4&&err==NGX_ENOSYS){use_accept4=0;ngx_inherited_nonblocking=0;continue;}#elsengx_log_error(level,ev->log,err,"accept() failed");#endifif(err==NGX_ECONNABORTED){if(ngx_event_flags&NGX_USE_KQUEUE_EVENT){ev->available--;}if(ev->available){continue;}}if(err==NGX_EMFILE||err==NGX_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_held=0;}ngx_accept_disabled=1;}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_disabled=ngx_cycle->connection_n/8-ngx_cycle->free_connection_n;计算 ngx_accept_disabled: 若空闲连接池剩余数量低于总容量的 1/8,计算结果为正数,后续将触发接收节流机制。c=ngx_get_connection(s,ev->log);if(c==NULL){if(ngx_close_socket(s)==-1){ngx_log_error(NGX_LOG_ALERT,ev->log,ngx_socket_errno,ngx_close_socket_n" failed");}return;}为新连接分配连接对象c->type=SOCK_STREAM;设置连接类型 将连接的类型设置为 SOCK_STREAM,表示这是一个 TCP 流式连接。#if(NGX_STAT_STUB)(void)ngx_atomic_fetch_add(ngx_stat_active,1);#endifc->pool=ngx_create_pool(ls->pool_size,ev->log);if(c->pool==NULL){ngx_close_accepted_connection(c);return;}创建连接专属内存池if(socklen>(socklen_t)sizeof(ngx_sockaddr_t)){socklen=sizeof(ngx_sockaddr_t);}c->sockaddr=ngx_palloc(c->pool,socklen);if(c->sockaddr==NULL){ngx_close_accepted_connection(c);return;}ngx_memcpy(c->sockaddr,&sa,socklen);保存客户端地址log=ngx_palloc(c->pool,sizeof(ngx_log_t));if(log==NULL){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_flags&NGX_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_flags&NGX_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 特殊处理:IOCP(Windows 下的完成端口)需要套接字为阻塞模式, 所以如果当前事件模型是 IOCP,则调用 ngx_blocking 将套接字设为阻塞。 其他事件模型: 默认(epoll, kqueue 等)都要求非阻塞模式, 因此如果继承的非阻塞标志为 0(即需要手动设置), 且不是 IOCP 模型,则调用 ngx_nonblocking 设置 O_NONBLOCK 标志。 任何设置失败都会关闭连接并返回,确保后续不会在错误的阻塞模式下运行。*log=ls->log;初始化日志c->recv=ngx_recv;c->send=ngx_send;c->recv_chain=ngx_recv_chain;c->send_chain=ngx_send_chain;初始化 基础接收/发送函数 recv、send:基本的接收和发送函数,对应系统调用 read/write 的封装。 recv_chain、send_chain:用于处理 ngx_chain_t 链表形式的批量收发c->log=log;c->pool->log=log;将连接对象和连接内存池的日志指针都指向新创建的日志对象。 这样通过连接或池进行任何内存分配或日志输出都会使用同一个日志上下文c->socklen=socklen;c->listening=ls;c->local_sockaddr=ls->sockaddr;c->local_socklen=ls->socklen;保存监听配置和地址信息 将客户端地址长度、监听配置结构体指针、 本地监听地址及长度保存到连接对象中。 这些信息后续可能用于日志、匹配虚拟主机、连接管理等功能。Unix 域套接字的特殊处理#if(NGX_HAVE_UNIX_DOMAIN)if(c->sockaddr->sa_family==AF_UNIX){c->tcp_nopush=NGX_TCP_NOPUSH_DISABLED;c->tcp_nodelay=NGX_TCP_NODELAY_DISABLED;#if(NGX_SOLARIS)/* Solaris's sendfilev() supports AF_NCA, AF_INET, and AF_INET6 */c->sendfile=0;#endif}#endif获取读写事件对象并设置初始状态rev=c->read;wev=c->write;wev->ready=1;写事件初始设为就绪(ready = 1), 表示连接一建立,就可以发送数据if(ngx_event_flags&NGX_USE_IOCP_EVENT){rev->ready=1;}对于 IOCP 模型,读事件也初始设为就绪,因为 IOCP 的接受方式不同。if(ev->deferred_accept){rev->ready=1;#if(NGX_HAVE_KQUEUE||NGX_HAVE_EPOLLRDHUP)rev->available=1;#endif}果监听套接字上设置了 deferred_accept 选项(TCP_DEFER_ACCEPT), 则 accept 返回的连接上已经有数据到达(客户端的第一个数据包已经在内核缓冲区中), 因此读事件可以立即认为是就绪的,从而直接开始读取请求,减少一次事件循环。 rev->available = 1 表示有数据可读。rev->log=log;wev->log=log;将读写事件的日志对象也指向新连接的日志对象,以便事件处理时输出日志/* * 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->number=ngx_atomic_fetch_add(ngx_connection_counter,1);c->start_time=ngx_current_msec;分配连接编号和记录开始时间#if(NGX_STAT_STUB)(void)ngx_atomic_fetch_add(ngx_stat_handled,1);#endif统计已处理的连接数生成客户端地址的可读文本if(ls->addr_ntop){c->addr_text.data=ngx_pnalloc(c->pool,ls->addr_text_max_len);if(c->addr_text.data==NULL){ngx_close_accepted_connection(c);return;}c->addr_text.len=ngx_sock_ntop(c->sockaddr,c->socklen,c->addr_text.data,ls->addr_text_max_len,0);if(c->addr_text.len==0){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_level&NGX_LOG_DEBUG_EVENT){addr.data=text;addr.len=ngx_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_flags&NGX_USE_EPOLL_EVENT)==0){if(ngx_add_conn(c)==NGX_ERROR){ngx_close_accepted_connection(c);return;}}对于 epoll,Nginx 采用更高效的边沿触发模式,并延迟添加事件, 直到确实需要等待某个事件时才调用 ngx_add_event, 因此这里对 epoll 模型跳过直接添加。对于其他模型,则立即注册。log->data=NULL;log->handler=NULL;ls->handler(c);调用上层协议处理函数 ls->handler 是在配置监听端口时设置的回调函数 这个函数将接管连接,开始读取请求、解析协议。从此该连接的生命周期就由上层协议模块管理。if(ngx_event_flags&NGX_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 off,ev->available 为 0,循环只执行一次。 这种设计让 multi_accept on 时, 可以在一次事件处理中连续接受多个新连接, 减少了事件循环的开销。#if(NGX_HAVE_EPOLLEXCLUSIVE)ngx_reorder_accept_events(ls);#endif}EPOLLEXCLUSIVE 重排接受事件 如果系统支持 EPOLLEXCLUSIVE(Linux 4.5+),并且监听套接字设置了该选项, 则多个 worker 可能会同时监听同一个套接字, 但在任一时刻只有一个 worker 被唤醒。 ngx_reorder_accept_events 用于在每次处理完一批新连接后, 调整监听事件在 epoll 中的位置或优先级, 以确保新连接能在不同的 worker 之间更公平地分布,避免单个 worker 垄断。