前面已经分析了nginx的事件处理机制的构建阶段,也分析过nginx的进程模型,利用上一篇文章中总结的并发模型设计的知识,先总结介绍一下nginx的并发模型,然后剖析nginx事件处理机制运行阶段的处理过程。
nginx 的进程模型采用的是prefork方式,预先分配的worker子进程数量由配置文件指定,默认为1。master主进程创建监听套接口并监听客户连接, 每个worker子进程独自accept已连接套接口,accept是否上锁可以配置,默认会上锁,如果操作系统支持原子整型,才会使用共享内存实现原子 上锁,否则使用文件上锁。
nginx的连接处理机制在不同的操作系统采用不同的IO模型,在linux使用epoll的IO多路复用模 型,在freebsd使用kqueue的IO多路复用模型,在solaris使用/dev/poll方式的IO多路复用模型,在windows使用的是 icop等等。
nginx把不同操作系统的IO模型抽象封装成ngx_event_module_t.actions中指定的钩子:
typedef struct {
ngx_int_t (*add)(ngx_event_t *ev, ngx_int_t event, ngx_uint_t flags);
ngx_int_t (*del)(ngx_event_t *ev, ngx_int_t event, ngx_uint_t flags);
ngx_int_t (*enable)(ngx_event_t *ev, ngx_int_t event, ngx_uint_t flags);
ngx_int_t (*disable)(ngx_event_t *ev, ngx_int_t event, ngx_uint_t flags);
ngx_int_t (*add_conn)(ngx_connection_t *c);
ngx_int_t (*del_conn)(ngx_connection_t *c, ngx_uint_t flags);
ngx_int_t (*process_changes)(ngx_cycle_t *cycle, ngx_uint_t nowait);
ngx_int_t (*process_events)(ngx_cycle_t *cycle, ngx_msec_t timer,
ngx_uint_t flags);
ngx_int_t (*init)(ngx_cycle_t *cycle, ngx_msec_t timer);
void (*done)(ngx_cycle_t *cycle);
} ngx_event_actions_t;
worker 初始化的时候会调用actions.init钩子初始化IO模型,比如在epoll的init钩子会创建epoll句柄和事件队列;之后worker就可 以在监听套接口上添加对连接建立状态的监听事件,调用accept获取已连接套接口,在已连接套接口上添加对读写操作的监听事件,nginx会调用 actions.process_events钩子轮询和处理事件,轮询有超时设定。nginx对时间的管理相当讲究,这会在后面详细分析。
nginx的每个worker子进程可以处理的最大连接数可由配置文件指定,默认为512,每个worker可以与内核间传递的事件数量也可由配置文件指定,对于epoll驱动的模型,默认为512。
nginx把监听、连接和事件抽象为三个结构体:ngx_listening_t、ngx_connection_t和ngx_event_t。监听也是一种连接,只是其比较特殊才定义一个独立的抽象。
nginx为监听结构预分配了一个数组cycle->listening,并且为每个ngx_listening_t分配一个ngx_connection_t连接结构用于设置读写事件,nginx的监听套接口是非阻塞的(非使用异步IO模型的情况)。
nginx 根据每个worker的最大连接数cycle->connecion_n预先分配了cycle->connecion_n个连接结构变量并构 成一个单向链表cycle->connections(cycle->free_connections指向其中空闲的结构),每个连接结构 分配一个读事件结构和一个写事件结构用于事件驱动。
预分配了cycle->connecion_n个读事件结构变量和 cycle->connecion_n个写事件结构变量并构成了两个双向链表cycle->read_events和 cycle->write_events,聪明如你,读写事件的数量和连接是一样的,所以就是一个连接对应一个读事件和一个写事 件:cycle->connections[i].read=&cycle->read_events[i],cycle-> connections[i].write=&cycle->write_events[i]。
这些数组、单向链表和双向链表方便集中访问和集中管理资源。
---------------------------------------------------
首先看一下ngx_listening_t监听结构,这个结构体包含了监听套接口的属性信息、配置参数和一些系统内部标识套接口状态的状态位等成员:
struct ngx_listening_s {
ngx_socket_t fd; // 监听套接口的套接字描述符
struct sockaddr *sockaddr; // 监听套接口地址结构
socklen_t socklen; /* size of sockaddr */
size_t addr_text_max_len;
ngx_str_t addr_text;
int type; // SOCK_STREAM
int backlog; // listen backlog
int rcvbuf; // 监听套接口的接收缓冲区长度
int sndbuf; // 监听套接口的发送缓冲区长度
/* handler of accepted connection */
ngx_connection_handler_pt handler;
void *servers; /* array of ngx_http_in_addr_t, for example */
ngx_log_t log;
ngx_log_t *logp;
size_t pool_size;
/* should be here because of the AcceptEx() preread */
size_t post_accept_buffer_size;
/* should be here because of the deferred accept */
ngx_msec_t post_accept_timeout;
ngx_listening_t *previous;
ngx_connection_t *connection; // 监听也是一个连接,要分配给监听一个连接资源
unsigned open:1;
unsigned remain:1;
unsigned ignore:1;
unsigned bound:1; /* already bound */
unsigned inherited:1; /* inherited from previous process */
unsigned nonblocking_accept:1;
unsigned listen:1;
unsigned nonblocking:1;
unsigned shared:1; /* shared between threads or processes */
unsigned addr_ntop:1;
...
}
ngx_listening_t有几个相关的操作接口:
1、ngx_int_t ngx_set_inherited_sockets(ngx_cycle_t *cycle);
nginx 启动的时候会尝试从环境变量中读取前次执行时候的监听套接口的id,并会创建对应数量的ngx_listening_t结构变量(存于 cycle->listening数组中),然后调用这个接口通过getsockname,getsockopt等系统调用把原来套接口的属性信息 和设置参数读取出来去设置那些新创建的ngx_listening_t结构变量,这样就继承了前次执行时候的监听套接口了,这个接口是在 ngx_init_cycle之前调用的;
2、ngx_listening_t *ngx_create_listening(ngx_conf_t *cf, void *sockaddr,
socklen_t socklen);
创 建一个ngx_listening_t结构,这个函数在ngx_init_cycle解析http的server配置项的时候会调用,它创建一个 ngx_listening_t结构变量(存于cycle->listening数组中),并设置其地址和一些基本的信息,比如backlog等;
3、ngx_int_t ngx_open_listening_sockets(ngx_cycle_t *cycle);
ngx_init_cycle 在解析完配置文件之后,会调用这个接口打开cycle->listening数组中的所有监听套接口,其实就是顺序调用socket、 setsockopt、bind、listen几个系统调用,如果事件驱动不是利用异步IO模型,还会把这些监听套接口设置为非阻塞方式;
4、void ngx_configure_listening_sockets(ngx_cycle_t *cycle);
ngx_init_cycle中紧接着ngx_open_listening_sockets接口之后调用这个接口利用setsockopt系统调用配置这些监听套接口;
5、void ngx_close_listening_sockets(ngx_cycle_t *cycle);
上面的几个接口都是在master进程调用的,而worker进程会继承master中设置好的这些监听套接口,当worker进程退出的时候,会调用这个接口关闭监听套接口,关闭之前会删除与其关联的连接上的读事件并释放这个连接资源。
对于那些支持deffered accept的操作系统,nginx会设置这个参数来增强功能,设置了这个参数,在 accept的时候,只有当实际收到了数据,才唤醒在accept等待的进程,可以减少一些无聊的上下文切换,如下:
val = 5;
setsockopt(socket_fd, SOL_TCP, TCP_DEFER_ACCEPT, &val, sizeof(val));
kernel 在 val 秒之内还没有收到数据,不会继续唤醒进程,而是直接丢弃连接,如果connect之后立刻收到数据,kernel才创建数据套接口并唤醒在accept上等待的进程。
---------------------------------------------------
nginx并发模型的核心是连接,我们接下来分析ngx_connection_t结构及其操作过程。
struct ngx_connection_s {
void *data; // next connection | ...
ngx_event_t *read; // 读事件
ngx_event_t *write; // 写事件
ngx_socket_t fd; // 连接套接口的套接口描述字
ngx_recv_pt recv;
ngx_send_pt send;
ngx_recv_chain_pt recv_chain;
ngx_send_chain_pt send_chain;
ngx_listening_t *listening; // 该连接对应的监听
off_t sent;
ngx_log_t *log;
ngx_pool_t *pool;
struct sockaddr *sockaddr;
socklen_t socklen;
ngx_str_t addr_text;
#if (NGX_SSL)
ngx_ssl_connection_t *ssl;
#endif
struct sockaddr *local_sockaddr;
socklen_t local_socklen;
ngx_buf_t *buffer;
ngx_atomic_uint_t number;
ngx_uint_t requests;
unsigned buffered:8;
unsigned log_error:3; /* ngx_connection_log_error_e */
unsigned single_connection:1;
unsigned unexpected_eof:1;
unsigned timedout:1;
unsigned error:1;
unsigned destroyed:1;
unsigned idle:1;
unsigned close:1;
unsigned sendfile:1;
unsigned sndlowat:1;
unsigned tcp_nodelay:2; /* ngx_connection_tcp_nodelay_e */
unsigned tcp_nopush:2; /* ngx_connection_tcp_nopush_e */
...
}
ngx_connection_t有几个相关的操作接口:
1、ngx_connection_t *ngx_get_connection(ngx_socket_t s, ngx_log_t *log);
从cycle->connections链表中摘取一个空闲的连接结构变量并修改空闲链,然后设置连接资源的读写事件的初始状态信息;
2、void ngx_close_connection(ngx_connection_t *c);
关闭连接,首先清理连接上的读写事件,这可能会删除定时器、删除等待事件,然后释放连接资源,并关闭连接对应的套接口;
3、void ngx_free_connection(ngx_connection_t *c);
释放连接资源,把连接资源放回空闲连接链表中。
---------------------------------------------------
另一个关键的资源是事件,事件结构体包含了对应的连接资源、事件处理函数和若干事件状态标志位等成员:
struct ngx_event_s {
void *data; // ngx_conncection_t | ...
unsigned write:1;
unsigned accept:1;
/* used to detect the stale events in kqueue, rtsig, and epoll */
unsigned instance:1;
/*
* the event was passed or would be passed to a kernel;
* in aio mode - operation was posted.
*/
unsigned active:1;
unsigned disabled:1;
/* the ready event; in aio mode 0 means that no operation can be posted */
unsigned ready:1;
unsigned oneshot:1;
/* aio operation is complete */
unsigned complete:1;
unsigned eof:1;
unsigned error:1;
unsigned timedout:1;
unsigned timer_set:1;
unsigned delayed:1;
unsigned read_discarded:1;
unsigned unexpected_eof:1;
unsigned deferred_accept:1;
/* the pending eof reported by kqueue or in aio chain operation */
unsigned pending_eof:1;
...
#if (NGX_HAVE_KQUEUE) || (NGX_HAVE_IOCP)
int available;
#else
unsigned available:1;
#endif
ngx_event_handler_pt handler;
...
ngx_uint_t index;
ngx_log_t *log;
ngx_rbtree_node_t timer;
unsigned closed:1;
/* to test on worker exit */
unsigned channel:1;
unsigned resolver:1;
...
/* the links of the posted queue */
ngx_event_t *next;
ngx_event_t **prev;
...
}
关于事件的几个操作接口就是前面反复提到过的10个actions钩子,这些钩子封装了一套事件驱动的接口,不同的IO模型不同的实现方式,我们以epoll为例分析这几个钩子:
1、static ngx_int_t ngx_epoll_init(ngx_cycle_t *cycle, ngx_msec_t timer);
init钩子,创建一个epoll句柄ep,指定在其上的监控套接字数量为每个worker最大连接数的一半,并分配足够的epoll_event结构数组event_list用于与内核间传递事件,数量nevents可以配置文件指定,默认为512;
2、static void ngx_epoll_done(ngx_cycle_t *cycle);
done钩子,释放epoll句柄资源和event_list;
3、static ngx_int_t ngx_epoll_add_event(ngx_event_t *ev, ngx_int_t event,
ngx_uint_t flags);
add 和enable钩子,调用epoll_ctl(ep, EPOLL_CTL_MOD/EPOLL_CTL_ADD, c->fd, &ee)添加对连接套接口的读事件或者写事件监控,并且设置事件的状态位:ev->action = 1,当添加读事件的时候会判断写事件是否已经设置(wev->action=1?),从而决定op是否使用EPOLL_CTL_MOD,添加写事件 也要判断读事件的状态;
4、static ngx_int_t ngx_epoll_del_event(ngx_event_t *ev, ngx_int_t event,
ngx_uint_t flags);
del 和disable钩子,调用epoll_ctl(ep, EPOLL_CTL_MOD/EPOLL_CTL_DEL, c->fd, &ee)删除对连接套接口的读事件或者写事件的监控,并且设置事件的状态位:ev->action = 0,当删除读事件的时候会判断写事件是否已经设置(wev->action=1?),从而决定op是否使用EPOLL_CTL_MOD,删除写事件 也要判断读事件的状态;
5、static ngx_int_t ngx_epoll_add_connection(ngx_connection_t *c);
add_conn钩子,调用epoll_ctl(ep, EPOLL_CTL_ADD, c->fd, &ee)同时添加对一条连接的读写事件的监控,并设置其读写事件的状态active = 1;
6、static ngx_int_t ngx_epoll_del_connection(ngx_connection_t *c,
ngx_uint_t flags);
del_conn钩子,调用epoll_ctl(ep, EPOLL_CTL_DEL, c->fd, &ee)同时删除对一条连接的读写事件的监控,并设置其读写事件的状态active = 0;
7、static ngx_int_t ngx_epoll_process_events(ngx_cycle_t *cycle, ngx_msec_t timer,
ngx_uint_t flags);
process_events 钩子,调用events = epoll_wait(ep, event_list, (int) nevents, timer)轮询事件,超时值为timer,事件存放于event_list数组中,最大数量为nevents。对于发生的读事件,若flags中置位了 NGX_POST_EVENTS,再根据被监控的套接口是监听套接口还是数据套接口决定暂时投递到事件队列 ngx_posted_accept_events还是ngx_posted_events等待进程处理,否则就直接调用事件处理函数 rev->handler处理事件;对于发生的写事件,若flags置位了NGX_POST_EVENTS,投递到事件队列 ngx_posted_events等待处理,否则就直接调用事件处理函数wev->handler处理事件。
---------------------------------------------------
nginx 在启动worker进程的过程中,在预分配了cycle->connections,cycle->read_events和 cycle->write_events等重要的资源之后,就会为从master继承来的cycle->listening数组中的每个监听 套接口分配一个连接资源和一对读写事件资源,并设置读事件的事件处理函数为ngx_event_accept,之后就循环调用 ngx_process_events_and_timers(cycle)等待事件和处理事件,并且会更改计时,nginx事件处理过程中的计时也是一 个关键的部分。
我们先分析一下监听套接口的事件处理函数ngx_event_accept。当监听套接口上发生读事件的时候,就会调用 ngx_event_accept处理这个事件,这个函数调用accept获取已连接的数据套接口,为获得的套接口分配连接资源和读写事件资源,为每个新 创建的连接资源创建一个内存池用于之后的数据处理,接下来初始化连接资源和读写事件资源的状态,最后调用add_conn钩子添加对读写事件的监控。
接着看一下ngx_process_events_and_timers函数,先简单的分析一下其处理事件的基本流程,另外关于时间的部分需要建立在理解nginx的计时优化机制的基础上,这个部分专门用一篇文章分析。
void
ngx_process_events_and_timers(ngx_cycle_t *cycle)
{
ngx_uint_t flags;
ngx_msec_t timer, delta;
// 如果配置文件中设置了时间精度
if (ngx_timer_resolution) {
timer = NGX_TIMER_INFINITE;
flags = 0;
} else {
timer = ngx_event_find_timer();
flags = NGX_UPDATE_TIME;
...
}
// 若accept使用上锁
if (ngx_use_accept_mutex) {
/*
ngx_accept_disabled是在监听套接口的事件处理函数ngx_event_accept中设置的
ngx_accept_disabled = ngx_cycle->connection_n / 8
- ngx_cycle->free_connection_n;
ngx_accept_disabled > 0表示当前worker进程已经分配了超过7/8的资源了
*/
if (ngx_accept_disabled > 0) {
ngx_accept_disabled--;
} else {
// 加锁
if (ngx_trylock_accept_mutex(cycle) == NGX_ERROR) {
return;
}
// 持有锁,设置标志位:把获取到的事件放入延迟处理队列
if (ngx_accept_mutex_held) {
flags |= NGX_POST_EVENTS;
} else {
if (timer == NGX_TIMER_INFINITE
|| timer > ngx_accept_mutex_delay)
{
timer = ngx_accept_mutex_delay;
}
}
}
}
delta = ngx_current_msec;
// 调用process_events钩子轮训事件,有些事件即时调用事件处理函数处理,有些事件放入延迟队列等待后面处理
(void) ngx_process_events(cycle, timer, flags);
delta = ngx_current_msec - delta;
ngx_log_debug1(NGX_LOG_DEBUG_EVENT, cycle->log, 0,
"timer delta: %M", delta);
// 有需要延迟处理的监听套接口事件
if (ngx_posted_accept_events) {
// 处理
ngx_event_process_posted(cycle, &ngx_posted_accept_events);
}
// 释放锁
if (ngx_accept_mutex_held) {
ngx_shmtx_unlock(&ngx_accept_mutex);
}
if (delta) {
ngx_event_expire_timers();
}
ngx_log_debug1(NGX_LOG_DEBUG_EVENT, cycle->log, 0,
"posted events %p", ngx_posted_events);
// 有需要延迟处理的数据套接口事件
if (ngx_posted_events) {
// 处理
if (ngx_threaded) {
ngx_wakeup_worker_thread(cycle);
} else {
ngx_event_process_posted(cycle, &ngx_posted_events);
}
}
}