关于epoll事件驱动模块, 这里不做过多分析. 主要着眼于事件添加和事件处理上.
添加事件
static ngx_int_t
ngx_epoll_add_event(ngx_event_t *ev, ngx_int_t event, ngx_uint_t flags)
{
int op;
uint32_t events, prev;
ngx_event_t *e;
ngx_connection_t *c;
struct epoll_event ee;
//之前有过了解, 每个事件的data可能存储的是连接结构体
//每个连接都是一个"事件", 且每个连接对应于读写池的两个事件
c = ev->data;
//这里的event用于确定当前事件是读事件还是写事件. 即将来调用epoll_ctl是使用EPOLLIN还是EPOLLOUT
events = (uint32_t) event;
//如果当前事件是以读事件插入, 那么事件对象e就会记录下此连接事件的写事件(当然, 写事件可能并不存在, 但即使写
//事件不存在, 它的事件对象早已被创建好, 其中的active标志就反映了该事件是否被定义).
//与此同时, 记住此时prev值是EPOLLOUT, events则为读标记
if (event == NGX_READ_EVENT) {
e = c->write;
prev = EPOLLOUT;
#if (NGX_READ_EVENT != EPOLLIN|EPOLLRDHUP)
events = EPOLLIN|EPOLLRDHUP;
#endif
} else {
e = c->read;
prev = EPOLLIN|EPOLLRDHUP;
#if (NGX_WRITE_EVENT != EPOLLOUT)
events = EPOLLOUT;
#endif
}
//上面, 如果某事件作为读事件插入, 那么e记录的是该连接写事件.
//也说道, active记录了该事件是否激活(被定义).
//如果我们在插入读事件的时候, 发现该连接的写事件曾经已经被插入到epoll中了, 也就是说该连接的描述符已经被插入了
//那么, 现在我们可能就会重复插入. 为了避免这样的问题, 就有了下面的判断
//如果该描述符已经被注册过了, 那么不是添加描述符而是修改描述符
//且事件类型修改为读写
if (e->active) {
op = EPOLL_CTL_MOD;
events |= prev;
} else {
//否则就是添加描述符
op = EPOLL_CTL_ADD;
}
ee.events = events | (uint32_t) flags;
//插入到epoll中的描述符对应的存储信息的结构体的data.ptr用于存储连接结构体
//这里可能是个令人疑惑的地方, 但也是特别需要在意的地方.
//这个instance标志是判断一个事件是否过期的标志. 下面会涉及到.
ee.data.ptr = (void *) ((uintptr_t) c | ev->instance);
ngx_log_debug3(NGX_LOG_DEBUG_EVENT, ev->log, 0,
"epoll add event: fd:%d op:%d ev:%08XD",
c->fd, op, ee.events);
if (epoll_ctl(ep, op, c->fd, &ee) == -1) {
...
return NGX_OK;
}
收集分发事件
接下来就是分析实现了 收集分发事件的process_event方法.每个worker子进程主要就是调用了这里的方法进行事件的收集分发
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);
events = epoll_wait(ep, event_list, (int) nevents, timer);
err = (events == -1) ? ngx_errno : 0;
//还记得这里的ngx_event_timer_alarm吗, 就是利用setitimer开启的间歇定时器
if (flags & NGX_UPDATE_TIME || ngx_event_timer_alarm) {
ngx_time_update();
}
//如果发生了错误
if (err) {
//被信号打断. 有可能是被定时器信号打断的
if (err == NGX_EINTR) {
//每次定时器信号发生后的信号处理函数都会将这个变量置1.
if (ngx_event_timer_alarm) {
//表明的确是被定时器打断的
//上面已经更新过时间了, 所以重置为0.
ngx_event_timer_alarm = 0;
return NGX_OK;
}
. . .
}
//因为epoll返回了0, 要么是因为epoll_wait的时间参数超时了, 要么就是出错了
if (events == 0) {
if (timer != NGX_TIMER_INFINITE) {
return NGX_OK;
}
. . .
}
//处理每一个发生的事件
for (i = 0; i < events; i++) {
//得到该事件的连接(从连接中我们可以得到其对应的读写事件(如果存在的话))
c = event_list[i].data.ptr;
//取出存在地址最后一位的instance变量
instance = (uintptr_t) c & 1;
//还原连接结构体的原本地址
c = (ngx_connection_t *) ((uintptr_t) c & (uintptr_t) ~1);
//得到连接对应的读写事件的instance成员的值
rev = c->read;
//如果发现fd变成-1那么说明这个连接已经过期了
//如果发现此连接的fd并没有变成-1, 这说明此事件对应的连接可能已经过期了, 但此连接使用的结构体虽然被free但又被其他新连接使用了
//这时候需要看instance是否发生变化. 如果不再与原来相同, 那么表明确实是被其他新连接复用了; 相同则表明没有过期
//过期的事件就不再需要处理
if (c->fd == -1 || rev->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;
}
//得到此事件的事件类型
revents = event_list[i].events;
...
if ((revents & EPOLLIN) && rev->active) {
...
//表示需要延后处理
if (flags & NGX_POST_EVENTS) {
//对于EPOLLIN事件, 其有可能是普通的读事件, 也有可能是有新连接到来
//之所以要将他们区别对待, 之后会讲到
queue = rev->accept ? &ngx_posted_accept_events
: &ngx_posted_events;
ngx_post_event(rev, queue);
} else {
//否则直接调用回调函数处理
rev->handler(rev);
}
}
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 (flags & NGX_POST_EVENTS) {
ngx_post_event(wev, &ngx_posted_events);
} else {
wev->handler(wev);
}
}
}
return NGX_OK;
}
ngx_event_process_events方法会收集当前触发的所有事件. 对于不需要加入到post队列延后处理的事件, 该方法会立即执行他们的回调方法. 这其实是在做分发事件的工作, 只是它会在自己的进程中调用这些方法而已, 因此每个回调方法都不能导致进程休眠或者消耗太多时间, 以免epoll不能及时处理其他事件.
关于ngx_event_t中的instance标志位
它为什么能判断事件是否过期呢?
1.instance放在哪里 ?它利用了指针最后一位一定是0的特性. 既然最后一位始终是0, 那么就可以用来包含一些信息.
这样, 在使用ngx_epoll_add_event方法向epoll中添加事件时, 就把epoll_event中data成员的ptr指针指向连接结构体的同时,把最后一位置为这个事件的instance标志.
在ngx_event_process_events方法中取出指向连接结构体的指针时, 先把instance取出, 再把最后一位还原成原本连接结构体的地址.
2. 为什么一定要放在这一位?
因为ptr指针已经指向了连接结构体, 然而连接结构体中并没有这个成员变量. 但这个变量又是必需的, 所以得找个地方存它
3. 什么是过期事件?
举个例子, 假设epoll_wait一次返回了3个事件. 在第一个事件处理的过程中, 由于业务的需要, 所以关闭了一个连接. 而这个连接恰好是第三个事件的连接, 这样一来, 在处理到第三个事件的时候, 这个事件已经是过期事件了, 一旦处理就肯定会出错了. 既然如此, 解决的方案可能并不需要instance, 如果把该连接的fd设置为-1是否就能解决这个问题呢?
假设第三个事件对应的连接结构体中的fd套接字原先是50, 处理第一个事件时把这个连接套接字关闭了, 并将fd设置为-1, 并调用ngx_free_connection将该连接归还给连接池. 当我们处理第二个连接的时候, 恰好第二个事件是建立新的连接(属于监听套接字的事件), 调用ngx_get_connection从连接池取出的连接结构体恰好就是刚刚归还给连接池的第三个事件的连接. 又由于套接字50刚刚被释放了, linux内核很可能把这个刚释放的套接字50又分配给新建立的连接. 因此, 在处理到第三个事件的时候, 这个连接应该是过期的而不能被处理!
如何解决这个问题就是依靠instance了.
当调用ngx_get_connection从连接池获取一个新连接时, instance位就会被置反:
ngx_connection_t *
ngx_get_connection(ngx_socket_t s, ngx_log_t *log)
{
...
c = ngx_cycle->free_connections;
...
ngx_cycle->free_connections = c->data;
ngx_cycle->free_connection_n--;
...
//可以看到, 新获得的连接却依然持有以前的连接对应的读写事件
rev = c->read;
wev = c->write;
ngx_memzero(c, sizeof(ngx_connection_t));
//并在初始化连接结构体后依然把原本的事件放回去.
c->read = rev;
c->write = wev;
c->fd = s;
c->log = log;
instance = rev->instance;
//初始化读写事件
ngx_memzero(rev, sizeof(ngx_event_t));
ngx_memzero(wev, sizeof(ngx_event_t));
//将读写事件的instance置反
rev->instance = !instance;
wev->instance = !instance;
...
return c;
}
这样处理后, 当某个连接结构体被重复使用后, 它的instance位一定是不同的. 因此, 在ngx_epoll_process_events方法中一旦判断instance发生了变化, 就认为这是过期事件而不处理.