上一篇介绍核心事件模块,本篇介绍事件模块ngx_epoll_module。Nginx在linux环境下采用epoll网络模型,对于epoll网络型不了解的可自行百度查询,本篇不在阐述。
一、问题
本篇要澄清以下几个问题:
1、当客户端发起TCP连接后,事件模块是如何管理新连接?
2、Nginx是如何接收到客户端请求(只是TCP层请求非HTTP请求)?
3、Nginx是如何发送响应给客户端(只是TCP层响应)?
4、超时事件管理方式
二、事件模块ngx_epoll_module
2.0、接口定义
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 (*notify)(ngx_event_handler_pt handler);
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;
typedef struct
{
ngx_str_t *name;
void *(*create_conf)(ngx_cycle_t *cycle);
char *(*init_conf)(ngx_cycle_t *cycle, void *conf);
ngx_event_actions_t actions;
} ngx_event_module_t;
通过ngx_event_module_t定义一个新的事件模块。事件模块主要是用接收、发送报文,通常情况下我们不需要自己创建一个新的事件模块。接口ngx_event_actions_t中add、del是一对操作,主要用于向epoll注册事件、删除事件。init用于初始化事件模块,对于epoll来说init回调函数是ngx_epoll_init。
2.1、模块定义
static ngx_str_t epoll_name = ngx_string("epoll");
static ngx_command_t ngx_epoll_commands[] = {
{ngx_string("epoll_events"),
NGX_EVENT_CONF | NGX_CONF_TAKE1,
ngx_conf_set_num_slot,
0,
offsetof(ngx_epoll_conf_t, events),
NULL},
{ngx_string("worker_aio_requests"),
NGX_EVENT_CONF | NGX_CONF_TAKE1,
ngx_conf_set_num_slot,
0,
offsetof(ngx_epoll_conf_t, aio_requests),
NULL},
ngx_null_command};
static ngx_event_module_t ngx_epoll_module_ctx = {
&epoll_name,
ngx_epoll_create_conf, /* create configuration */
ngx_epoll_init_conf, /* init configuration */
{
ngx_epoll_add_event, /* add an event */
ngx_epoll_del_event, /* delete an event */
ngx_epoll_add_event, /* enable an event */
ngx_epoll_del_event, /* disable an event */
ngx_epoll_add_connection, /* add an connection */
ngx_epoll_del_connection, /* delete an connection */
#if (NGX_HAVE_EVENTFD)
ngx_epoll_notify, /* trigger a notify */
#else
NULL, /* trigger a notify */
#endif
ngx_epoll_process_events, /* process the events */
ngx_epoll_init, /* init the events */
ngx_epoll_done, /* done the events */
}};
ngx_module_t ngx_epoll_module = {
NGX_MODULE_V1,
&ngx_epoll_module_ctx, /* module context */
ngx_epoll_commands, /* module directives */
NGX_EVENT_MODULE, /* module type */
NULL, /* init master */
NULL, /* init module */
NULL, /* init process */
NULL, /* init thread */
NULL, /* exit thread */
NULL, /* exit process */
NULL, /* exit master */
NGX_MODULE_V1_PADDING};
2.2、事件模块初始化
在上一篇有介绍到,在初始化ngx_event_core_module时会调用具体事件模块的init回调函数即ngx_epoll_init,如果对于epoll模型比较了解,看起来应该很容易,具体代码如下:
/**
* epoll模型初始化
* @param cycle 核心结构体
* @param timer 定时器超时时间
*/
static ngx_int_t
ngx_epoll_init(ngx_cycle_t *cycle, ngx_msec_t timer)
{
ngx_epoll_conf_t *epcf;
/* 获取epoll模型下配置结构 */
epcf = ngx_event_get_conf(cycle->conf_ctx, ngx_epoll_module);
if (ep == -1)
{
/**
* 创建epoll对象 其中epoll_create参数没有意义
*/
ep = epoll_create(cycle->connection_n / 2);
if (ep == -1)
{
ngx_log_error(NGX_LOG_EMERG, cycle->log, ngx_errno,
"epoll_create() failed");
return NGX_ERROR;
}
#if (NGX_HAVE_EVENTFD)
/**
* 使用eventfd实现 事件通知功能 主要用于多线程模式下 目前可忽略
*/
if (ngx_epoll_notify_init(cycle->log) != NGX_OK)
{
ngx_epoll_module_ctx.actions.notify = NULL;
}
#endif
#if (NGX_HAVE_FILE_AIO)
ngx_epoll_aio_init(cycle, epcf);
#endif
#if (NGX_HAVE_EPOLLRDHUP)
ngx_epoll_test_rdhup(cycle);
#endif
}
/**
* 创建epoll_event数组 用于存储epoll返回事件
* 数组大小为512
*/
if (nevents < epcf->events)
{
if (event_list)
{
ngx_free(event_list);
}
event_list = ngx_alloc(sizeof(struct epoll_event) * epcf->events,
cycle->log);
if (event_list == NULL)
{
return NGX_ERROR;
}
}
nevents = epcf->events;//一次性 最大可保存 epoll事件数
ngx_io = ngx_os_io;
ngx_event_actions = ngx_epoll_module_ctx.actions;
#if (NGX_HAVE_CLEAR_EVENT)
ngx_event_flags = NGX_USE_CLEAR_EVENT /* ET模式 */
#else
ngx_event_flags = NGX_USE_LEVEL_EVENT
#endif
| NGX_USE_GREEDY_EVENT | NGX_USE_EPOLL_EVENT;
return NGX_OK;
}
2.3、事件模块关闭
事件模块关闭比较简单,直接调用close方法,将对应的对象关闭即可
static void
ngx_epoll_done(ngx_cycle_t *cycle)
{
/* 关闭epoll对象 */
if (close(ep) == -1)
{
ngx_log_error(NGX_LOG_ALERT, cycle->log, ngx_errno,
"epoll close() failed");
}
ep = -1;
#if (NGX_HAVE_EVENTFD)
/* 关闭eventfd对象 */
if (close(notify_fd) == -1)
{
ngx_log_error(NGX_LOG_ALERT, cycle->log, ngx_errno,
"eventfd close() failed");
}
notify_fd = -1;
#endif
#if (NGX_HAVE_FILE_AIO)
if (ngx_eventfd != -1)
{
if (io_destroy(ngx_aio_ctx) == -1)
{
ngx_log_error(NGX_LOG_ALERT, cycle->log, ngx_errno,
"io_destroy() failed");
}
if (close(ngx_eventfd) == -1)
{
ngx_log_error(NGX_LOG_ALERT, cycle->log, ngx_errno,
"eventfd close() failed");
}
ngx_eventfd = -1;
}
ngx_aio_ctx = 0;
#endif
ngx_free(event_list); //释放事件数组
event_list = NULL;
nevents = 0;
}
三、事件循环
在上一小节介绍事件模块初始化和关闭流程,那最主要的事件循环是在哪里实现的呢?其实在模块定义的时候就已经声明了ngx_epoll_process_events,该函数在什么地方调用呢?在介绍《菜鸟学习Nginx之启动流程(3)》中有提到,如下所示:
static void
ngx_worker_process_cycle(ngx_cycle_t *cycle, void *data)
{
...
for (;;)
{
...
/* 阻塞 等待事件或者定时器超时事件 */
ngx_process_events_and_timers(cycle);
}
}
3.1、事件循环
事件循环具体流程图如下:
从流程图中可得,主要有三部分内容需要处理:
1)获取定时器超时事件,因为epoll_wait需要指定返回时间。超时时间可能为-1(表示永远不超时), 也可能是大于0
2)是否开启进程间互斥锁,当有多个worker进程时会开启。当开启互斥锁时就需要处理"惊群"问题。
3)处理事件
具体代码如下:
/**
* 此函数是处理事件的入口函数,所有业务流程起始函数
* 《深入理解Nginx模块开发与架构解析》 P331
*/
void ngx_process_events_and_timers(ngx_cycle_t *cycle)
{
ngx_uint_t flags;
ngx_msec_t timer, delta;
if (ngx_timer_resolution)
{//用户指定时间精度,超时事件由SIGALARM触发
timer = NGX_TIMER_INFINITE;
flags = 0;
}
else
{
//获取下一个超时时间 如果二叉树中没有超时事件则返回-1 代表永久不超时
timer = ngx_event_find_timer();
flags = NGX_UPDATE_TIME; //表示需要更新时间缓存
#if (NGX_WIN32)
/* handle signals from master in case of network inactivity */
if (timer == NGX_TIMER_INFINITE || timer > 500)
{
timer = 500;
}
#endif
}
/* 解决惊群 */
if (ngx_use_accept_mutex)
{
if (ngx_accept_disabled > 0)
{//实现worker进程间负载均衡
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;
/**
* 如果是epoll模型 此处实际调用函数是ngx_epoll_process_events
* 阻塞在epoll_wait
*/
(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);
/**
* 如果是epoll模型 在ngx_epoll_process_events函数中可能会对事件进行划分
* 划分到不同队列中,其中accept队列要优先处理
*/
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_event_process_posted(cycle, &ngx_posted_events);
}
对于惊群处理,有单独文章介绍,本篇不深入剖析。
3.2、事件驱动
接下来看一下事件驱动是如何处理的。上一小节中ngx_process_events函数实际调用的是ngx_epoll_process_events函数。该函数逻辑相对复杂一些,采用分片展示代码逻辑:
/**
* 事件驱动
* @param cycle 核心结构体
* @param timer 等待时间
* @param flags
* 取值: NGX_POST_EVENTS、NGX_UPDATE_TIME
*/
static ngx_int_t
ngx_epoll_process_events(ngx_cycle_t *cycle, ngx_msec_t timer, ngx_uint_t flags)
{
int events;
uint32_t revents;
ngx_int_t instance, i;
ngx_uint_t level;
ngx_err_t err;
ngx_event_t *rev, *wev;
ngx_queue_t *queue;
ngx_connection_t *c;
/* NGX_TIMER_INFINITE == INFTIM */
ngx_log_debug1(NGX_LOG_DEBUG_EVENT, cycle->log, 0,
"epoll timer: %M", timer);
/**
* timer不是固定不变的,如果没有任何事件发生(空闲期),
* timer可能是NGX_TIMER_INFINITE 即表示永久阻塞
*/
events = epoll_wait(ep, event_list, (int)nevents, timer);
说明:
epoll_wait返回有三种场景:
1、超时时间到期,即timer > 0的场景,当超时时间到了仍然没有事件发生(读写事件)
2、超时时间为timer=-1(永远不超时),但是发生了SIGARLM信号,当信号处理函数结束后,epoll_wait返回,errno为NGX_EINTR
3、超时间未到期(timer=-1或者timer>0),发生了读写事件(一般正常情况)
所以下面的代码会针对这三种场景进行处理:
/**
* Nginx两种时间策略:
* 1、如果nginx.conf文件中定义时间精度timer_resolution,则表示nginx的时间
* 缓存精确到ngx_timer_resolution毫秒
* 2、如果没有定义时间精度 则严格按照系统时间
* ----------------------------------------------------------------
* 条件说明:
* flags & NGX_UPDATE_TIME -- 表示强制更新系统时间
* ngx_event_timer_alarm 当采用时间精度时,nginx会启动一个定时器,每次
* 超时,都会产生SIGALRM信号。具体参考ngx_event_process_init
*/
if (flags & NGX_UPDATE_TIME || ngx_event_timer_alarm)
{//只要是时间相关的事件 就立即更新时间缓存
ngx_time_update(); //更新时间缓存
}
if (err)
{
if (err == NGX_EINTR)
{
if (ngx_event_timer_alarm)
{//表示发生了SIGALRM信号中断 认为是正常场景
ngx_event_timer_alarm = 0;
return NGX_OK;
}
level = NGX_LOG_INFO;
}
else
{//异常场景
level = NGX_LOG_ALERT;
}
ngx_log_error(level, cycle->log, err, "epoll_wait() failed");
return NGX_ERROR;
}
if (events == 0)
{
if (timer != NGX_TIMER_INFINITE)
{//表示时间超时
return NGX_OK;
}
ngx_log_error(NGX_LOG_ALERT, cycle->log, 0,
"epoll_wait() returned no events without timeout");
return NGX_ERROR;
}
上面这段代码主要是针对场景1,场景2的处理 ,相对简单。接下来看一下重头戏,对于正常读写事件的处理流程。
for (i = 0; i < events; i++)
{
c = event_list[i].data.ptr;
/* 指针变量 最后一位始终为0 节省内存空间 */
instance = (uintptr_t)c & 1;
c = (ngx_connection_t *)((uintptr_t)c & (uintptr_t)~1);
rev = c->read;
/**
* 两种场景:
* 1) fd = -1,意味着在之前的事件处理时,把当前请求关闭了,即close fd
* 并且当前事件对应的连接已被还回连接池,此时该次事件就不应该处理了,忽略掉。
* 2) 如果fd > 0就一定是合法的吗?不一定。请看下面详细分析。
if (c->fd == -1 || rev->instance != instance)
{/**
* 当fd=-1 或者instance不一致表示 当前事件是过期事件不需要处理
* 《深入理解Nginx模块开发与架构解析》一书:318页
*/
/*
* the stale event from a file descriptor
* that was just closed in this iteration
*/
ngx_log_debug1(NGX_LOG_DEBUG_EVENT, cycle->log, 0,
"epoll: stale event %p", c);
continue;
}
revents = event_list[i].events;
ngx_log_debug3(NGX_LOG_DEBUG_EVENT, cycle->log, 0,
"epoll: fd:%d ev:%04XD d:%p",
c->fd, revents, event_list[i].data.ptr);
if (revents & (EPOLLERR | EPOLLHUP))
{
ngx_log_debug2(NGX_LOG_DEBUG_EVENT, cycle->log, 0,
"epoll_wait() error on fd:%d ev:%04XD",
c->fd, revents);
/*
* if the error events were returned, add EPOLLIN and EPOLLOUT
* to handle the events at least in one active handler
*/
revents |= EPOLLIN | EPOLLOUT;
}
循环遍历事件队列events_list中事件,从私有数据字段中取出instance和conection对象。
说明:
1、instance值,利用指针最后一位始终为0的特性保存。为什么指针最后一位始终为0呢?操作系统在分配内存,为了提升访问速度,经常是4字节/8字节对其,这样分配出来的地址最后一位始终为0。
2、为什么需要判断rev->instance 和 instance是否相等?下面这段话摘自《深入理解Nginx模块开发与架构解析》一书:318页:
【那么,过期事件又是怎么回事呢?举个例子,假设epoll_wait一次返回3个事件,在第1个事件的处理过程中,由于业务的需要,所以关闭了一个连接,而这个连接恰好对应第3个事件。这样的话,在处理到第3个事件时,这个事件就已经是过期事件了,一旦处理必然出错。既然如此,把关闭的这个连接的fd套接字置为–1能解决问题吗?答案是不能处理所有情况。
下面先来看看这种貌似不可能发生的场景到底是怎么发生的:假设第3个事件对应的ngx_connection_t连接中的fd套接字原先是50,处理第1个事件时把这个连接的套接字(fd=50)关闭了,同时置为–1,并且调用ngx_free_connection将该连接归还给连接池。在
ngx_epoll_process_events方法的循环中开始处理第2个事件,恰好第2个事件是建立新连接事件,调用ngx_get_connection从连接池中取出的连接非常可能就是刚刚释放的第3个事件对应的连接。由于套接字50刚刚被释放,Linux内核非常有可能把刚刚释放的套接字50又分配给新建立的连接。因此,在循环中处理第3个事件时,这个事件就是过期的了!它对应的事件是关闭的连接,而不是新建立的连接。】
下面是读事件处理,若是NGX_POST_EVENTS事件则延迟处理并且按照优先级进行区分,Accept事件优先级高于其他的读写事件,所以下面在处理读事件进行区分处理,如下所示:
if ((revents & EPOLLIN) && rev->active)
{
#if (NGX_HAVE_EPOLLRDHUP)
if (revents & EPOLLRDHUP)
{
rev->pending_eof = 1;
}
rev->available = 1;
#endif
rev->ready = 1;
if (flags & NGX_POST_EVENTS)
{ //需要延迟处理该事件
queue = rev->accept ? &ngx_posted_accept_events
: &ngx_posted_events;
ngx_post_event(rev, queue); //将事件加入到事件队列中
}
else
{//立即处理该事件
rev->handler(rev); //ngx_http_keepalive_handler
}
}
写事件处理与读事件处理大同小异,如下:
wev = c->write;
if ((revents & EPOLLOUT) && wev->active)
{
if (c->fd == -1 || wev->instance != instance)
{
/*
* the stale event from a file descriptor
* that was just closed in this iteration
*/
ngx_log_debug1(NGX_LOG_DEBUG_EVENT, cycle->log, 0,
"epoll: stale event %p", c);
continue;
}
wev->ready = 1;
#if (NGX_THREADS)
wev->complete = 1;
#endif
if (flags & NGX_POST_EVENTS)
{//延迟处理该事件
ngx_post_event(wev, &ngx_posted_events);
}
else
{//立即处理该事件
wev->handler(wev);
}
}
四、总结
到这里把epoll事件驱动处理流程介绍完毕。但是事件是如何注册到epoll中呢又如何删除呢?在下一篇介绍。