nginx 事件模块(4) - 事件处理

2018-07-27 16:04:32

我们先来看一下 worker 进程里有个无限循环的函数

for(;;){
    ngx_process_events_and_timers(cycle);
}

如果不考虑负载均衡,这个函数很简单,所以我们先略过负载均衡和延后处理队列部分。

void
ngx_process_events_and_timers(ngx_cycle_t *cycle)
{
    ngx_uint_t  flags;
    ngx_msec_t  timer, delta;

    // nginx 更新缓存时间的精度,如果设置了会定时发送sigalarm信号更新时间
    // ngx_timer_resolution = ccf->timer_resolution;默认值是0
    if (ngx_timer_resolution) {
        // 要求epoll无限等待事件的发生,直至被sigalarm信号中断
        timer = NGX_TIMER_INFINITE;
        flags = 0;

    } else {
        // 没有设置时间精度,默认设置
        // 在定时器红黑树里找到最小的时间,二叉树查找很快
        // timer >0 红黑树里即将超时的事件的时间
        // timer <0 表示红黑树为空,即无超时事件
        // timer==0意味着在红黑树里已经有事件超时了,必须立即处理
        // timer==0,epoll就不会等待,收集完事件立即返回
        timer = ngx_event_find_timer();

        // NGX_UPDATE_TIME要求epoll等待这个时间,然后主动更新时间
        flags = NGX_UPDATE_TIME;
    }

    // 1.11.3开始,默认不使用负载均衡锁,提高性能
    // 省去了锁操作和队列操作

    // 不管是否获得了负载均衡锁,都要处理事件和定时器
    // 如果获得了负载均衡锁,事件就会多出一个accept事件
    // 否则只有普通的读写事件和定时器事件

    // 获取当前的时间,毫秒数
    delta = ngx_current_msec;

    // #define ngx_process_events   ngx_event_actions.process_events
    // 实际上就是ngx_epoll_process_events
    //
    // epoll模块核心功能,调用epoll_wait处理发生的事件
    // 使用event_list和nevents获取内核返回的事件
    // timer是无事件发生时最多等待的时间,即超时时间
    // 如果ngx_event_find_timer返回timer==0,那么epoll不会等待,立即返回
    // 函数可以分为两部分,一是用epoll获得事件,二是处理事件,加入延后队列
    //
    // 如果不使用负载均衡(accept_mutex off)
    // 那么所有IO事件均在此函数里处理,即搜集事件并调用handler
    (void) ngx_process_events(cycle, timer, flags);

    // 在ngx_process_events里缓存的时间肯定已经更新
    // 计算得到epoll一次调用消耗的毫秒数
    delta = ngx_current_msec - delta;


    // 如果消耗了一点时间,那么看看是否定时器里有过期的
    if (delta) {
        // 遍历定时器红黑树,找出所有过期的事件,调用handler处理超时
        // 其中可能有的socket读写超时,那么就结束请求,断开连接
        ngx_event_expire_timers();
    }
}


下面就是 epoll 真正处理的函数,如果使用负载均衡且抢到了 accept 锁,那么flags里有 NGX_POST_EVENTS 标志,如果没有设置更新缓存时间的精度,那么 flags 里有 NGX_UPDATE_TIME。

Nginx先从data.ptr 里取出之前添加事件时存储的连接对象指针,因为最低位被用来存储失效标志位 instance,所以必须要用位操作把它们分离开。

事件只有读和写两种,但读事件里又有需要优先处理的 accept 事件,所以 Nginx 先检查读事件。如果事件的 instance 标志位与连接里取出的 instance 不一致,那么可能是在等待事件发生的过程中连接被关闭后被重用了,也就意味着这个事件已经没有意义,所以不需要处理,直接忽略。

如果读事件的标志位是 EPOLLIN 或 EPOLLHUP,说明连接上有数据可读,就可以置 ready 标志位,然后立即调用 rev-> handler 处理这个事件。同样的,如果写事件的标志位是 EPOLLOUT,说明连接可写,同样置 ready 标志位后立即调用 rev-> handler 处理就这样反复循环,直至数组 event_list 里的所有事件处理完毕,完成一次 epol_wait 事件处理,然后再由 worker进程的 for(;;) 通过 ngx_process_events_and_timers() 再次调用,不停地收集 epoll 事件通知并处理。

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;

    // 如果epoll有事件发生,那么等待时间timer无意义,epoll_wait立即返回
    // 如果ngx_event_find_timer返回timer==0,那么epoll不会等待,立即返回
    events = epoll_wait(ep, event_list, (int) nevents, timer);

    // 检查是否发生了错误
    // 如果调用epoll_wait获得了0个或多个事件,就没有错误
    err = (events == -1) ? ngx_errno : 0;

    // 如果要求更新时间,或者收到了更新时间的信号
    // 通常event模块调用时总会传递NGX_UPDATE_TIME,这时就会更新缓存的时间
    // sigalarm信号的处理函数设置ngx_event_timer_alarm变量
    if (flags & NGX_UPDATE_TIME || ngx_event_timer_alarm) {
        ngx_time_update();
    }

    // 错误处理
    if (err) {
        // 错误是由信号中断引起的
        if (err == NGX_EINTR) {

            // 如果是更新时间的信号,那么就不是错误
            if (ngx_event_timer_alarm) {
                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;
    }

    // 0个事件,说明nginx没有收到任何请求或者数据收发
    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;
    }

    // 调用epoll_wait获得了多个事件,存储在event_list里,共events个
    // 遍历event_list数组,逐个处理事件
    for (i = 0; i < events; i++) {

        // 从epoll结构体的union.ptr获得连接对象指针
        c = event_list[i].data.ptr;

        // 因为目前的32位/64位的计算机指针地址低位都是0(字节对齐)
        // 所以用最低位来存储instance标志,即一个bool值
        // 在真正取出连接对象时需要把低位的信息去掉
        instance = (uintptr_t) c & 1;

        // 此时才是真正的连接对象指针,把指针地址的低位重新置为0
        c = (ngx_connection_t *) ((uintptr_t) c & (uintptr_t) ~1);

        // 优先查看连接里的读事件
        rev = c->read;

        // fd == -1描述符无效
        // instance不对,连接失效,不做处理
        if (c->fd == -1 || rev->instance != instance) {
            continue;
        }

        // 获取epoll的事件标志
        revents = event_list[i].events;

        // EPOLLERR|EPOLLHUP是发生了错误,或者是客户端发生了断连
        //那么加上读写标志位方便后续处理
        if (revents & (EPOLLERR|EPOLLHUP)) {
            revents |= EPOLLIN|EPOLLOUT;
        }

        // 有读事件,且读事件是可用的
        if ((revents & EPOLLIN) && rev->active) {

            if (revents & EPOLLRDHUP) {
                rev->pending_eof = 1;
            }

            // nginx 1.11.x新增,用在ngx_recv时检查
            rev->available = 1;

            // 读事件可用
            rev->ready = 1;

            // 不延后,立即调用读事件的handler回调函数处理事件
            // 1.9.x reuseport直接处理,省去了入队列出队列的成本,更快
            rev->handler(rev);
        }

        // 读事件处理完后再查看连接里的写事件
        wev = c->write;

        // 有写事件,且写事件是可用的
        if ((revents & EPOLLOUT) && wev->active) {

            // fd == -1描述符无效
            // instance不对,连接有错误
            if (c->fd == -1 || wev->instance != instance) {
                continue;
            }

            // 写事件可用
            wev->ready = 1;

            // 不延后,立即调用写事件的handler回调函数处理事件
            // 1.9.x reuseport直接处理,省去了入队列出队列的成本,更快
            wev->handler(wev);
        }
    }       //for循环结束,处理完epoll_wait获得的内核事件

    return NGX_OK;
}

流程图大致如下


备注

1.测试环境centos7 64位,nginx版本为 1.14.0。
2..原文地址http://www.freecls.com/a/2712/de


©著作权归作者所有
收藏
推荐阅读
简介
天降大任于斯人也,必先苦其心志。