Nginx 源码阅读笔记8 epoll 模块

终于到事件模块了,只看了 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_MODevents会加上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 且errnoNGX_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不是又回去了吗
接下来是文件描述符或对端错误的情况,此时会设置EPOLLERREPOLLHUP

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);
    }
}

这里又判断了一次无效连接,理论上来说可以不用判断两次,因为revwev中的instance在同一时刻应该是相同的,我觉得可能是担心前面的读事件处理函数把连接关闭了

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值