终于到事件模块了,只看了 epoll 模块,所以写的东西都是基于 epoll 的情况,其中忽略了异步 IO 和 eventfd 的部分
ngx_epoll_init
首先是 epoll 的初始化部分,一开始创建了 epoll 实例
epcf = ngx_event_get_conf(cycle->conf_ctx, ngx_epoll_module);
if (ep == -1) {
ep = epoll_create(cycle->connection_n / 2);
if (ep == -1) {
return NGX_ERROR;
}
#if (NGX_HAVE_EPOLLRDHUP)
ngx_epoll_test_rdhup(cycle);
#endif
}
这里的ep
是一个全局变量,保存着指向 epoll 实例的文件描述符,由epoll_create
返回,其中的参数根据 man 中的描述,若 linux 版本大于2.6.8
,则该参数没有意义,只要大于 0 即可
后面的ngx_epoll_test_rdhup
函数不太理解,里面创建了一对 socket,关闭一端然后看另一端是否被触发,可是events
里设置了EPOLLIN
关闭对端不是一定会触发吗
接着为event_list
分配空间,如果之前的空间小于配置文件中设置的大小(可能是单进程模式 reload 时),则重新分配空间,否则直接使用之前的即可
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;
接下来设置了当前系统环境下使用的 io 函数
ngx_io = ngx_os_io;
在 linux 下定义如下
static ngx_os_io_t ngx_linux_io = {
ngx_unix_recv,
ngx_readv_chain,
ngx_udp_unix_recv,
ngx_unix_send,
ngx_udp_unix_send,
ngx_udp_unix_sendmsg_chain,
ngx_linux_sendfile_chain,
NGX_IO_SENDFILE
};
然后是设置事件模块的相关函数和事件模块的标志位
ngx_event_actions = ngx_epoll_module_ctx.actions;
#if (NGX_HAVE_CLEAR_EVENT)
ngx_event_flags = NGX_USE_CLEAR_EVENT
#else
ngx_event_flags = NGX_USE_LEVEL_EVENT
#endif
|NGX_USE_GREEDY_EVENT
|NGX_USE_EPOLL_EVENT;
从auto/os/linux
文件中看,好像有 epoll 就会定义NGX_HAVE_CLEAR_EVENT
,代表使用边缘触发,这种情况下,实际上 accept 事件用的是水平触发,可能是为了防止一个进程过多地创建连接,但是如果配置了multi_accept
,则会尝试多次 accept,创建完连接后用的就是边缘触发
NGX_USE_GREEDY_EVENT
表示尽量多读,体现在ngx_unix_recv
中,如果设置了这个位,在读取数据后即使是部分读,也不会取消事件的ready
位,所以下次还是会调用 recv 函数,此时可能返回 -1,也可能有新数据到达,其实如果使用了EPOLLRDHUP
,则NGX_USE_GREEDY_EVENT
相当于没用,因为 recv 函数中控制逻辑不会到达那里
NGX_USE_EPOLL_EVENT
顾名思义,表示使用 epoll 模块
ngx_epoll_add_event
接下来是添加事件的函数,先看下函数原型
ngx_int_t ngx_epoll_add_event(ngx_event_t *ev, ngx_int_t event, ngx_uint_t flags);
ev
参数表示需要添加的事件,event
参数用于指示读或写事件,而flags
就是额外的参数,例如用于表示边缘触发的NGX_CLEAR_EVENT
,用于防止惊群现象的NGX_EXCLUSIVE_EVENT
等
uint32_t events, prev;
ngx_event_t *e;
ngx_connection_t *c;
c = ev->data;
events = (uint32_t) event;
if (event == NGX_READ_EVENT) {
e = c->write;
prev = EPOLLOUT;
} else {
e = c->read;
prev = EPOLLIN|EPOLLRDHUP;
}
首先是从事件的data
域中取出连接,接下来判断需要添加的事件类型,如果是读事件则取出连接的写事件赋予e
,然后设置prev
为写事件的标志,如果是写事件则反之,这里的逻辑可能有点不太直观,需要看后面才能看懂,那先来看下后面吧
if (e->active) {
op = EPOLL_CTL_MOD;
events |= prev;
} else {
op = EPOLL_CTL_ADD;
}
事件的active
域代表是否添加到了 epoll 中,如果添加了则使用EPOLL_CTL_MOD
修改,看到这里应该就可以理解之前的那段逻辑了,举例来说,如果一开始只有写事件在 epoll 中,现在需要添加读事件,则会使用EPOLL_CTL_MOD
且events
会加上prev
中的写事件标志,这样一来修改后读写事件就都在 epoll 中了
接下来又是一段我不太懂的代码
if (flags & NGX_EXCLUSIVE_EVENT) {
events &= ~EPOLLRDHUP;
}
个人理解是,只有监听描述符会设置NGX_EXCLUSIVE_EVENT
,用于防止惊群现象,而监听描述符可以不需要EPOLLRDHUP
,因为没有对端关闭的问题,但是这里设置了好像也没什么影响
函数最后将事件加入 epoll 实例
ee.events = events | (uint32_t) flags;
ee.data.ptr = (void *) ((uintptr_t) c | ev->instance);
if (epoll_ctl(ep, op, c->fd, &ee) == -1) {
return NGX_ERROR;
}
ev->active = 1;
这里连接的地址和事件的instance
域做与运算的结果,被保存在data
域中,从内存池的代码中可以看到,分配的内存一般是按字对齐的,也就是二进制形式的最后一位为 0,这里利用最后一位来保存instance
,用于之后辨别无效连接,具体如何实现等等就会看到
ngx_epoll_del_event
删除事件的代码和添加事件的代码类似,除了开头的一段
if (flags & NGX_CLOSE_EVENT) {
ev->active = 0;
return NGX_OK;
}
根据man epoll
中的描述,如果文件描述符被删除后,则事件会自动从 epoll 中删除,所以可以不用再次调用epoll_ctl
,只需要设置一下active
位即可
ngx_epoll_add_connection
添加连接就是将连接的读写事件一起加入到 epoll 中,代码比较好理解
struct epoll_event ee;
ee.events = EPOLLIN|EPOLLOUT|EPOLLET|EPOLLRDHUP;
ee.data.ptr = (void *) ((uintptr_t) c | c->read->instance);
if (epoll_ctl(ep, EPOLL_CTL_ADD, c->fd, &ee) == -1) {
return NGX_ERROR;
}
c->read->active = 1;
c->write->active = 1;
而删除连接与这个类似,就不贴代码了
ngx_epoll_process_events
到了最重要的部分,也就是处理事件,一开始是之前说过的那段代码
events = epoll_wait(ep, event_list, (int) nevents, timer);
err = (events == -1) ? ngx_errno : 0;
if (flags & NGX_UPDATE_TIME || ngx_event_timer_alarm) {
ngx_time_update();
}
接下来处理返回值为 -1 和 0 的两种特殊情况
if (err) {
if (err == NGX_EINTR) {
if (ngx_event_timer_alarm) {
ngx_event_timer_alarm = 0;
return NGX_OK;
}
}
return NGX_ERROR;
}
if (events == 0) {
if (timer != NGX_TIMER_INFINITE) {
return NGX_OK;
}
return NGX_ERROR;
}
返回值为 -1 且errno
为NGX_EINTR
则表示被信号中断,如果是被SIGALRM
中断,则信号处理函数会设置ngx_event_timer_alarm
位,此时直接返回成功即可,返回后会处理过期的定时器事件,其余情况都返回错误
返回值为 0 表示等待超时,如果此时timer
不是 -1 则返回成功,否则返回错误
接下来遍历event_list
中已触发的事件,首先取出保存在data
中的值
for (i = 0; i < events; i++) {
c = event_list[i].data.ptr;
instance = (uintptr_t) c & 1;
c = (ngx_connection_t *) ((uintptr_t) c & (uintptr_t) ~1);
// ...
}
这里将data
的最后一位取出并置 0,得到真正的连接地址,然后是先处理读事件
rev = c->read;
if (c->fd == -1 || rev->instance != instance) {
/*
* the stale event from a file descriptor
* that was just closed in this iteration
*/
continue;
}
首先是判断无效连接,如果本次循环需要使用的连接被前几次循环的事件处理函数关闭了,则fd
为 -1,表示此次循环处理的是无效连接,然而还有一种情况,就是这个连接被关闭后被再次使用了,这时不能简单地根据fd
来判断无效连接,所以需要使用到instance
域,先看一下从连接池获取连接的一段代码
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));
rev->instance = !instance;
wev->instance = !instance;
可以看到,这里取出了原来事件中的instance
域,并将其取反设置到新连接读写事件的instance
域,这样就保证了两次连接中的instance
互不相同,以此来判断是否表示同一连接
话是这么说,但是我觉得好像还是有点奇怪,比如ngx_event_accept
中调用ngx_get_connection
获取到连接后,由于函数中某个错误需要调用ngx_close_accepted_connection
关闭连接,之后又被另一个监听描述符获取到了同一个连接,那么instance
不是又回去了吗
接下来是文件描述符或对端错误的情况,此时会设置EPOLLERR
或EPOLLHUP
revents = event_list[i].events;
if (revents & (EPOLLERR|EPOLLHUP)) {
/*
* if the error events were returned, add EPOLLIN and EPOLLOUT
* to handle the events at least in one active handler
*/
revents |= EPOLLIN|EPOLLOUT;
}
根据注释中的说法,即使文件描述符或对端错误也要调用事件处理函数,应该是事件处理函数中会考虑到这种情况
最后是真正的处理读事件
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);
}
}
如果含有EPOLLRDHUP
位,则表示对端关闭,需要设置pending_eof
位,按字面意思来看是即将收到 EOF,如果设置了这个位则和NGX_USE_GREEDY_EVENT
的情况类似,在读取数据后即使是部分读,也不会取消事件的ready
位,而available
这个位应该有两个作用,在这里应该是表示是否有数据可读,其实我觉得这部分逻辑和读取数据那部分连起来的话好乱,自己不能完全弄懂
然后是如果使用 accept 锁,则会设置NGX_POST_EVENTS
位,此时需要按之前说过的,将事件加入延迟处理队列
最后处理写事件,整体逻辑和处理写事件是差不多
wev = c->write;
if ((revents & EPOLLOUT) && wev->active) {
if (c->fd == -1 || wev->instance != instance) {
continue;
}
wev->ready = 1;
if (flags & NGX_POST_EVENTS) {
ngx_post_event(wev, &ngx_posted_events);
} else {
wev->handler(wev);
}
}
这里又判断了一次无效连接,理论上来说可以不用判断两次,因为rev
和wev
中的instance
在同一时刻应该是相同的,我觉得可能是担心前面的读事件处理函数把连接关闭了