1 定义
ngx_close_listening_sockets 函数 定义在 ./nginx-1.24.0/src/core/ngx_connection.c
voidngx_close_listening_sockets(ngx_cycle_t*cycle){ngx_uint_ti;ngx_listening_t*ls;ngx_connection_t*c;if(ngx_event_flags&NGX_USE_IOCP_EVENT){return;}ngx_accept_mutex_held=0;ngx_use_accept_mutex=0;ls=cycle->listening.elts;for(i=0;i<cycle->listening.nelts;i++){c=ls[i].connection;if(c){if(c->read->active){if(ngx_event_flags&NGX_USE_EPOLL_EVENT){/* * it seems that Linux-2.6.x OpenVZ sends events * for closed shared listening sockets unless * the events was explicitly deleted */ngx_del_event(c->read,NGX_READ_EVENT,0);}else{ngx_del_event(c->read,NGX_READ_EVENT,NGX_CLOSE_EVENT);}}ngx_free_connection(c);c->fd=(ngx_socket_t)-1;}ngx_log_debug2(NGX_LOG_DEBUG_CORE,cycle->log,0,"close listening %V #%d ",&ls[i].addr_text,ls[i].fd);if(ngx_close_socket(ls[i].fd)==-1){ngx_log_error(NGX_LOG_EMERG,cycle->log,ngx_socket_errno,ngx_close_socket_n" %V failed",&ls[i].addr_text);}#if(NGX_HAVE_UNIX_DOMAIN)if(ls[i].sockaddr->sa_family==AF_UNIX&&ngx_process<=NGX_PROCESS_MASTER&&ngx_new_binary==0&&(!ls[i].inherited||ngx_getppid()!=ngx_parent)){u_char*name=ls[i].addr_text.data+sizeof("unix:")-1;if(ngx_delete_file(name)==NGX_FILE_ERROR){ngx_log_error(NGX_LOG_EMERG,cycle->log,ngx_socket_errno,ngx_delete_file_n" %s failed",name);}}#endifls[i].fd=(ngx_socket_t)-1;}cycle->listening.nelts=0;}
`ngx_close_listening_sockets` 函数用于 在进程退出、重启或配置 reload 时,安全关闭 `cycle` 中所有处于监听状态的套接字。 它遍历每个监听项,将其读事件从事件驱动模块中移除、 回收关联的连接结构、关闭底层套接字并妥善清理 Unix 域套接字文件, 最后将监听数量清零,完成监听资源的彻底释放。
2 详解
1 函数签名
voidngx_close_listening_sockets(ngx_cycle_t*cycle)
返回类型:void 函数不需要向调用者返回任何结果。 关闭监听套接字是一个资源清理过程,无论成功或失败, 函数都会尽量完成所有清理操作(仅通过日志记录错误), 调用者无需根据返回值采取不同行为。
参数 ngx_cycle_t *cycle 指向当前运行周期上下文
2 逻辑流程
1 局部变量 2 IOCP 事件模型检查 3 清理 accept 互斥锁状态 4 遍历监听套接字 5 重置监听总数
1 局部变量
{ngx_uint_ti;ngx_listening_t*ls;ngx_connection_t*c;
2 IOCP 事件模型检查
if(ngx_event_flags&NGX_USE_IOCP_EVENT){return;}
检查全局事件标志 ngx_event_flags 是否包含 NGX_USE_IOCP_EVENT(Windows IOCP 事件模型)。 如果当前使用 IOCP,则直接返回,不执行后续关闭逻辑。 原因在于 IOCP 对监听套接字的管理机制与其它事件模型(select/poll/epoll/kqueue)差异很大, 关闭操作需要通过 IOCP 专用函数处理,这里的通用路径不适用,强行走下去可能导致未定义行为或资源泄漏。 提前返回保证了代码的健壮性和平台兼容性。
3 清理 accept 互斥锁状态
ngx_accept_mutex_held=0;ngx_use_accept_mutex=0;
#1 将全局变量 ngx_accept_mutex_held 置 0。 该变量指示当前 worker 进程是否持有 accept 互斥锁。 既然即将关闭所有监听套接字,进程不再有能力 accept 新连接, 自然需要放弃锁状态,避免其它逻辑误以为仍持有锁。 #2 将全局变量 ngx_use_accept_mutex 置 0。 该变量表示是否启用 accept 互斥机制。 关闭监听时直接禁用互斥锁,可以防止在清理期间其它代码路径尝试获取或释放锁,确保状态一致。
4 遍历监听套接字
ls=cycle->listening.elts;for(i=0;i<cycle->listening.nelts;i++){c=ls[i].connection;if(c){if(c->read->active){if(ngx_event_flags&NGX_USE_EPOLL_EVENT){/* * it seems that Linux-2.6.x OpenVZ sends events * for closed shared listening sockets unless * the events was explicitly deleted */ngx_del_event(c->read,NGX_READ_EVENT,0);}else{ngx_del_event(c->read,NGX_READ_EVENT,NGX_CLOSE_EVENT);}}ngx_free_connection(c);c->fd=(ngx_socket_t)-1;}ngx_log_debug2(NGX_LOG_DEBUG_CORE,cycle->log,0,"close listening %V #%d ",&ls[i].addr_text,ls[i].fd);if(ngx_close_socket(ls[i].fd)==-1){ngx_log_error(NGX_LOG_EMERG,cycle->log,ngx_socket_errno,ngx_close_socket_n" %V failed",&ls[i].addr_text);}#if(NGX_HAVE_UNIX_DOMAIN)if(ls[i].sockaddr->sa_family==AF_UNIX&&ngx_process<=NGX_PROCESS_MASTER&&ngx_new_binary==0&&(!ls[i].inherited||ngx_getppid()!=ngx_parent)){u_char*name=ls[i].addr_text.data+sizeof("unix:")-1;if(ngx_delete_file(name)==NGX_FILE_ERROR){ngx_log_error(NGX_LOG_EMERG,cycle->log,ngx_socket_errno,ngx_delete_file_n" %s failed",name);}}#endifls[i].fd=(ngx_socket_t)-1;}
#1 获取 cycle 中监听数组的起始地址。 cycle->listening 是一个 ngx_array_t 类型的动态数组, 其 elts 成员指向实际数据存储区域的首地址,元素类型为 ngx_listening_t。 赋值给 ls,后续可通过 ls[i] 访问每个监听项。
#2 开始 for 循环,遍历所有监听项。 cycle->listening.nelts 是数组中有效元素的个数。 循环将对每个监听套接字依次执行清理操作。
#3 将当前监听项 ls[i] 的 connection 指针取出,赋给局部变量 c。 每个监听套接字都会分配一个 ngx_connection_t 对象,用于挂载读事件、记录套接字描述符等信息。 通过该连接可以访问事件状态,以便后续从事件驱动模块中删除
#4 检查连接对象 c 是否为非空。正常情况下一定存在,但这里是一种防御性编程,增加安全性。 若确实为 NULL,则跳过事件删除和连接释放步骤,直接关闭套接字。
#5 检查监听连接上的读事件是否处于活跃(active)状态。 active 标志表示该事件当前已被注册到事件驱动模块中。 如果事件已不活跃,则无需再从事件模块删除,可避免无效操作。
#6 判断当前事件模型是否为 epoll(Linux 特有)。 如果采用 epoll,需要特殊处理。 注释说明特殊处理的理由:在某些虚拟化环境(如 Linux 2.6.x 下的 OpenVZ)中, 即使监听套接字已经关闭,如果之前注册的 epoll 事件没有被显式删除(EPOLL_CTL_DEL), 内核仍可能继续发送事件,导致错误触发回调。 为了彻底避免这种问题,必须显式删除事件注册。 调用 ngx_del_event(c->read, NGX_READ_EVENT, 0) 删除 epoll 实例中的读事件。 若非 epoll 事件模型(如 select、poll、kqueue、/dev/poll 等), 则调用 ngx_del_event(c->read, NGX_READ_EVENT, NGX_CLOSE_EVENT)。
#7 释放连接对象 c。 调用 ngx_free_connection 将该连接结构归还给 Nginx 的连接池(cycle->free_connections), 使其可以被后续新连接复用。这样避免了内存泄漏,同时维持了连接池的高效利用。 显式将连接结构中的文件描述符字段 fd 设置为 -1(ngx_socket_t 类型的无效值)。 因为连接对象即将回池,如果不清除其旧描述符,将来被分配用于新连接且未立即设置有效 fd 时, 可能会意外引用一个已关闭的文件描述符,导致错误的读写或事件操作。 这是防御性初始化,确保状态干净。
#8 输出调试日志
#9 调用 ngx_close_socket(ls[i].fd) 真正关闭底层套接字描述符。 检查返回值是否为 -1,表示关闭失败。 如果关闭失败,记录一条 NGX_LOG_EMERG(紧急)级别的错误日志。
#9 检查当前监听项的地址族是否为 AF_UNIX(Unix 域套接字)。 只有 Unix 域套接字才会在文件系统中创建文件,TCP/UDP 不需要文件清理。 ngx_process <= NGX_PROCESS_MASTER 判断当前进程角色。 ngx_process 可以是 NGX_PROCESS_SINGLE(单进程模式)或 NGX_PROCESS_MASTER(多进程的 master 进程)。 只有这些管理型进程才有权限删除 Unix socket 文件; worker 进程不应删除,否则会影响 master 或其他 worker 再次使用。 ngx_new_binary == 0 判断当前是否不是“平滑升级”过程中新启动的二进制(新 master)。 平滑升级时,旧 master 传递监听套接字给新 master,新 master 启动后 ngx_new_binary 被设置为 1。 此时新 master 不应该删除 Unix socket 文件,因为旧 master 可能还在运行并使用它。 只有非升级场景才可安全删除。 !ls[i].inherited || ngx_getppid() != ngx_parent 是一个复杂条件。 ls[i].inherited 表示该监听套接字是否从父进程继承而来(通过环境变量传递)。 如果是继承的,且当前进程的父进程 ID (ngx_getppid()) 与存储的父进程 ID (ngx_parent) 不同, 说明原父进程已退出(如平滑升级时旧 master 终止),此时文件已无人使用,可以安全删除。 如果是非继承的套接字,即本地新创建的,则任何时候都可以删除。 所有条件都满足时,进入清理文件的代码块。
#10 计算 Unix socket 文件路径。 ls[i].addr_text 是类似 "unix:/path/to/socket" 的字符串, 通过跳过 "unix:" 前缀(长度为 sizeof("unix:") - 1,即 5), name 指针直接指向文件路径部分(/path/to/socket)。 这样就可以传递给文件删除函数。 调用 ngx_delete_file(name) 删除该文件。 若返回 NGX_FILE_ERROR(表示删除失败),则进入错误处理。 删除失败时输出紧急级别错误日志,包含错误原因和文件名。 同样,即使删除失败也不会中断清理流程,但需要通过日志告知管理员可能存在残留文件, 防止下次启动因文件已存在而绑定失败
#11 将当前监听项 ls[i] 的 fd 字段置为 -1,标记该监听项不再代表有效套接字。 即使数组元素仍存在(数组大小不变),无效的 fd 可防止其它代码意外操作已关闭的套接字。
5 重置监听总数
cycle->listening.nelts=0;}
将监听数组的有效元素个数清零。 nelts = 0 向 cycle 的其他使用者表明当前已没有任何监听套接字, 后续如果调用相关函数会看到数量为 0,从而避免数组越界访问或重复关闭。 这是最终的状态重置。