我们来分析一下 nginx 如何调用 accept 函数。
跳出 ngx_init_cycle 函数,继续阅读 nginx main函数的主流程,我们发现直到 main 函数调用 ngx_master_process_cycle 函数生成 worker 进程,worker 进程进入主循环都没有发现调用 accept 函数的踪迹。worker 进程的主循环一直在处理各种信号、读写以及定时器事件。那么 accept 到底在哪里被调用的呢?
先说结论,accept 函数是当nginx 发现监听套接字有可读事件发生,即有客户端向 nginx 发起连接时才会被调用。那么这个调用的位置具体在代码中的什么地方呢?
上篇文章中说到,worker 进程是单线程结构,它的并发是通过非阻塞套接字加IO多路复用的方案实现的。要问 nginx 如何调用 Linux 的IO 多路复用接口,那就必须说一说 nginx 的事件模块。
nginx的worker进程在进入主循环前会先调用每个模块的进程初始化接口,这段代码位于 ngx_worker_process_cycle 函数调用的 ngx_worker_process_init 函数中。
nginx 的事件处理机制便从 nginx 的事件核心模块—— ngx_event_core_module 模块,的 init_process 接口(ngx_event_process_init函数)开始。
ngx_event_process_init 函数做了许多重要的事情,首先它通过用户对nginx的配置来确定是否使用 accept 锁(accept 锁用来解决 nginx 的惊群问题,我们随后会介绍 accept 锁解决惊群问题的原理)。
然后初始化了 nginx 的延迟处理事件队列,包括 accept 事件队列和普通事件队列,并初始化了定时器。
看到这可以说明一个问题,nginx的定时器机制只能在 worker 进程中使用,master 进程不能使用定时器(但是 master 进程可以通过 sigalarm 信号来执行定时任务)。
接下来开始对 IO 多路复用模块进行初始化。
我们知道,在Linux系统中,功能最成熟的IO多路复用接口非 epoll 接口莫属。但是上面图片代码中的逻辑是通过 ngx_event_core_module 模块的配置结构中的 use 成员来决定到底使用哪一个IO多路复用的模块的。也就是说只有在 nginx 配置文件的 event 配置块中配置 use epoll/poll/select; 才能明确指定当前使用哪一个 IO 多路复用模块。然而我们一般配置 nginx 的时候并不会明确标明使用哪种模块,它却自己知道使用 epoll 做为IO多路复用接口(在Linux系统中),这是怎么做到的呢?其实很简单,既然使用哪个 IO 多路复用模块的决定权在 ngx_event_core_module 模块的配置里,那我们看看 ngx_event_core_module 模块的配置初始化代码就可以了。也就是 ngx_event_core_init_conf 函数。
ngx_event_core_init_conf 函数通过一些宏定义来判断当前环境支持哪些IO多路复用接口,
对Linux系统来说,如果宏定义显示支持 epoll,nginx 会先尝试调用 epoll_create 接口,看是否会出现接口不存在的情况,由此判断当前系统是否支持 epoll。当确定支持 epoll 后,module 指针会指向 ngx_epoll_module 模块的地址,然后通过 module 指针对 ngx_event_core_module 模块的配置参数进行初始化。
回头再看 ngx_event_process_init 函数的流程,我们就知道这个 module 调用的到底是哪一个IO多路复用模块了。
没错这个module 指针指向的正是 ngx_epoll_module 模块的上下文结构:
那我们就知道这个 module->actions.init(cycle, ngx_timer_resolution) 调用的函数正是 ngx_epoll_module 模块定义的 ngx_epoll_init 函数。这个函数做的最主要的事情就是创建 epfd 和创建用于存放 epoll_wait 返回事件列表的事件结构数组 event_list。
接下来 ngx_event_process_init 函数就开始创建 nginx 的连接池和读写事件池。连接池中的数据结构为 ngx_connection_t 结构,具体分配数量为用户在配置文件中配置的数目。并且连接池中连接结构的数量和读写事件池中读写事件的数目相同,即一个连接对应一个读事件一个写事件,ngx_connection_t 结构中的 read 和 write数据成员正是用来指定本连接结构的读写事件的。
既然创建好了读写事件池和连接池,ngx_event_process_init 函数下一步就开始为每一个监听套接字分配连接数据结构,并指定读事件处理函数。
到此我们终于看到了 nginx 调用 accept 函数的曙光,正是监听套接字的读事件处理函数 ngx_event_accept 。为监听套接字指定了读事件处理函数之后,nginx就需要把监听套接字的读事件添加到 epoll 中去。nginx 在此调用的是一个名为 ngx_add_event 的宏定义。
ngx_add_event 宏定义如下:
这个 ngx_event_actions 全局变量的赋值位置正是在我们前面提到的 ngx_epoll_init 函数中。
所以 ngx_add_event 其实就是 ngx_epoll_add_event 函数。此函数主要通过调用 epoll_ctl 对epoll 对象增加我们要关注的事件。
添加关注完毕后,当监听套接字有可读事件发生时,nginx 就会自动调用该事件对应的处理函数,从而进行 accept 的调用。
我们在上篇文章的最后提出过一个问题,如果每个 worker进程都对监听套接字的读事件添加关注,那么当一个客户端连接nginx监听的某个端口时,这些worker进程会有怎样的反应呢?没错,所有worker的 epoll_wait 都会被唤醒,但是只有一个worker进程可以进行相关事件处理函数的调用,其他worker则继续进入等待态。这一所有worker进程被唤醒的现象就叫做 nginx 的惊群现象。
我们前面说到,nginx 的accept 锁可以杜绝nginx的惊群现象,这是怎么做得到的呢?原理其实很简单,如果用户开启了accept锁的使用,那么 ngx_event_process_init 函数就不会立即把监听套接字的读事件添加关注。
那么如果开启了 accept 锁, worker进程又该如何对监听套接字的读事件添加关注呢?这就得看 worker 进程的事件处理代码了,即 ngx_process_events_and_timers 函数。
可以看到,如果用户使用了 accept锁,nginx 会调用 ngx_trylock_accept_mutex 函数去 try lock 这个锁。如果 ngx_trylock_accept_mutex 函数上锁成功,本 worker进程就会使能 accept事件。
这样一来,如果用户配置使用了 accept锁,那么每次就只有竞争得到锁的worker进程才能对监听套接字的读事件做出响应,从而杜绝了惊群现象的发生。但是这样做也会出现一个新问题,就是各个 worker 进程的工作效率会出现较大差异,会出现部分进程繁忙部分进程经常闲置的状态。
最后我们来分析 ngx_event_accept 函数都干了些什么。
ngx_event_accept 函数调用了 accept ,并为 accept 函数返回的读写套接字分配了连接结构,为该连接结构分配内存池并保存客户端地址。
接下来开始对连接结构的各种参数进行设置
这段设置代码从头看到尾,我们发现似乎缺了一个至关重要的东西,就是这些代码似乎没有对这个读写套接字连接的读事件事件进行设置。其实nginx在这段代码里做了这方面的设置的,就是一个很不起眼的函数调用。
这个 ls->handler 成员是在哪里被赋的值呢?它是在每一个应用层协议配置框架指令的解析函数中被赋予的。也就是 “http”,“mail”,“stream”等配置指令的处理函数,以“http”指令对应的处理函数 ngx_http_block 为例,它最后调用了 ngx_http_optimize_servers 函数对监听套接字结构做了处理,也就是上篇文章介绍到的 ngx_http_init_listening 函数。该函数调用 ngx_http_add_listening 分配 ngx_listening_t 类型的结构体时,ngx_http_add_listening 为每一个 ngx_listening_t 结构对象的 handler 成员进行了赋值。
这个 handler成员的主要工作就是对当前读写套接字的读写事件处理函数进行设置,由于客户端和服务端的通信都是通过不同的应用层协议进行的,所以不同应用层配置框架设置的 handler 成员函数也不一样,读写套接字连接被赋予的读写事件处理函数也就不一样。从这里就相当于开始了nginx的对http协议的处理流程。需要重点说明的是,nginx作为一个 TCP 服务器并不会主动调用某个读写事件处理函数,它是将这些函数同通信套接字绑定,当这些套接字发生可读可写事件时才会调用这些处理函。nginx对协议的处理流程通常也以这样的方式进行,例如 ngx_http_init_connection 函数将通信套接字的读事件处理函数设置为了 ngx_http_wait_request_handler ,即等待http请求函数;
接着又在 ngx_http_wait_request_handler 函数中将通信套接字的读事件处理函数设置成了 ngx_http_process_request_line函数,即处理请求行函数。
由此类推,一个阶段完成后相应的事件处理函数会被替换成下一个流程的处理函数,而这些函数通常都是有对应事件触发时才会被执行,这样的编程模式就被称为反应式编程的模式。
以上就是本人在学习nginx源码过程中的一点总结,各位同学如果感兴趣,建议对照nginx源码阅读本文,欢迎大家点评指正。
nginx如何调用 socket() bind() listen() accept()等网络编程接口(二)
最新推荐文章于 2022-06-06 07:35:48 发布