nginx 事件模块(3) - 添加删除事件

2018-07-27 14:48:35

nginx 定义了一些宏来屏蔽不同系统事件的差异,我们来看下epoll

// 读事件,即可读或者有accept连接
// EPOLLRDHUP表示客户端关闭连接(断连),也当做读事件处理
// 这时recv返回0
#define NGX_READ_EVENT     (EPOLLIN|EPOLLRDHUP)

// 写事件,可写
#define NGX_WRITE_EVENT    EPOLLOUT

// 水平触发,仅用于accept接受连接
#define NGX_LEVEL_EVENT    0

// 边缘触发,高速模式
#define NGX_CLEAR_EVENT    EPOLLET


添加单个事件

// 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;

    // 获取事件关联的连接对象
    c = ev->data;

    // 计算epoll的标志位
    events = (uint32_t) event;

    // prev是对应事件的标志
    // 添加读事件则查看写事件
    if (event == NGX_READ_EVENT) {
        e = c->write;
        prev = EPOLLOUT;

        // 读事件的epoll标志
        events = EPOLLIN|EPOLLRDHUP;

    // 添加写事件则查看读事件
    } else {
        e = c->read;
        prev = EPOLLIN|EPOLLRDHUP;

        // 写事件的epoll标志
        events = EPOLLOUT;
    }

    // 如果另外的读写事件是活跃的那么就意味着已经加过了
    // active的设置就在下面的代码里
    // epoll的操作就是修改EPOLL_CTL_MOD
    // 需要加上对应事件的读写标志,即prev
    if (e->active) {
        op = EPOLL_CTL_MOD;
        events |= prev;

    } else {
        op = EPOLL_CTL_ADD;
    }

    // 加上flags标志,里面有ET
    ee.events = events | (uint32_t) flags;

    // union的指针成员,关联到连接对象
    // 因为目前的32位/64位的计算机指针地址低位都是0(字节对齐)
    // 所以用最低位来存储instance标志,即一个bool值
    // 在真正取出连接对象时需要把低位的信息去掉
    ee.data.ptr = (void *) ((uintptr_t) c | ev->instance);

    // 到这里,已经确定了是新添加还是修改epoll事件
    // 执行系统调用,添加epoll关注事件
    if (epoll_ctl(ep, op, c->fd, &ee) == -1) {
        ngx_log_error(NGX_LOG_ALERT, ev->log, ngx_errno,
                      "epoll_ctl(%d, %d) failed", op, c->fd);
        return NGX_ERROR;
    }

    // 添加事件成功,此事件就是活跃的,即已经使用
    ev->active = 1;

    return NGX_OK;
}

虽然一次操作只修改一个事件,但事件对应的 socket 上是关联了两个事件的(读和写), 不能因为这次修改而删除了另一个事件的关联,所以必须先保存之前的标志位,并检查事件是否已经添加到 epoll 了,如果已经添加就必须补上保存的标志。

使用例子可以更好地理解 Nginx 的这种处理方式。假设在一个连接上己经添加了读事件 EPOLLIN,事件的状态是 active,现在又要添加写事件,如果直接使用 EPOLLOUT 那么连接上的 EPOLLIN 就会丢失,不能收到读事件的通知,所以必须使用 EPOLLIN | EPOLLOUT。

结构体 epoll_event 里的 data.ptr 保存了连接对象的指针,只有这样当事件发生时 Nginx 才能知道事件对应的是哪个连接,而由于连接对象里又关联了读写事件和监听端口, 所以就可以得到连接的所有关联数据。

这里 Nginx还使用了一个特别的技巧,利用目前32位/64位的计算机指针地址低位都是0的特性(字节对齐),用 data.ptr 的最低位来存储失效标志 e->instance,事件发生时比较判断是否有变化,在真正取出指针时需要把低位的信息去掉。

添加读写事件

很多时候连接上的读写事件都是我们关心的,这时就没那么复杂,我们直接添加。

static ngx_int_t
ngx_epoll_add_connection(ngx_connection_t *c)
{
    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) {
        ngx_log_error(NGX_LOG_ALERT, c->log, ngx_errno,
                      "epoll_ctl(EPOLL_CTL_ADD, %d) failed", c->fd);
        return NGX_ERROR;
    }

    // 添加事件成功,读写事件都是活跃的,即已经使用
    c->read->active = 1;
    c->write->active = 1;

    return NGX_OK;
}

上面两个函数比较底层,在我们自己代码中最好使用 nginx 帮我们封装的函数。

// 添加读事件的便捷接口,适合epoll/kqueue/select等各种事件模型
// 内部还是调用ngx_add_event
ngx_int_t
ngx_handle_read_event(ngx_event_t *rev, ngx_uint_t flags)
{
    // 使用et模式,epoll/kqueue
    if (ngx_event_flags & NGX_USE_CLEAR_EVENT) {

        /* kqueue, epoll */

        if (!rev->active && !rev->ready) {
            if (ngx_add_event(rev, NGX_READ_EVENT, NGX_CLEAR_EVENT)
                == NGX_ERROR)
            {
                return NGX_ERROR;
            }
        }

        return NGX_OK;

    }

    return NGX_OK;
}
// 添加写事件的便捷接口,适合epoll/kqueue/select等各种事件模型
// 内部还是调用ngx_add_event,多了个send_lowat操作
// linux不支持send_lowat指令,send_lowat总是0
ngx_int_t
ngx_handle_write_event(ngx_event_t *wev, size_t lowat){
    ngx_connection_t  *c;

    if (ngx_event_flags & NGX_USE_CLEAR_EVENT) {

        /* kqueue, epoll */

        if (!wev->active && !wev->ready) {
            if (ngx_add_event(wev, NGX_WRITE_EVENT,
                              NGX_CLEAR_EVENT | (lowat ? NGX_LOWAT_EVENT : 0))
                == NGX_ERROR)
            {
                return NGX_ERROR;
            }
        }

        return NGX_OK;

    }

    return NGX_OK;
}


删除单个事件

// epoll删除事件
// 检查事件关联的连接对象,决定是完全删除还是修改
// 避免误删除了读写事件的关注
static ngx_int_t
ngx_epoll_del_event(ngx_event_t *ev, ngx_int_t event, ngx_uint_t flags)
{
    int                  op;
    uint32_t             prev;
    ngx_event_t         *e;
    ngx_connection_t    *c;
    struct epoll_event   ee;

    // 如果文件描述符关闭了,epoll会自动从队列中删除
    // 是否是要求事件关闭
    if (flags & NGX_CLOSE_EVENT) {
        // 设置active成员
        ev->active = 0;
        return NGX_OK;
    }

    // 获取事件关联的连接对象
    c = ev->data;

    // prev是对应事件的标志
    // 添加读事件则查看写事件
    if (event == NGX_READ_EVENT) {
        e = c->write;
        prev = EPOLLOUT;

    // 添加写事件则查看读事件
    } else {
        e = c->read;
        prev = EPOLLIN|EPOLLRDHUP;
    }


    // 如果另外的读写事件是活跃的那么就意味着已经加过了
    // active的设置就在下面的代码里
    // epoll的操作就是修改EPOLL_CTL_MOD
    // 需要加上对应事件的读写标志,即prev
    if (e->active) {
        op = EPOLL_CTL_MOD;
        ee.events = prev | (uint32_t) flags;

        // union的指针成员,关联到连接对象
        // 因为目前的32位/64位的计算机指针地址低位都是0(字节对齐)
        // 所以用最低位来存储instance标志,即一个bool值
        // 在真正取出连接对象时需要把低位的信息去掉
        ee.data.ptr = (void *) ((uintptr_t) c | ev->instance);

    } else {
        // 对应的读写事件没有,那么就可以删除整个事件
        op = EPOLL_CTL_DEL;
        ee.events = 0;
        ee.data.ptr = NULL;
    }

    // 到这里,已经确定了是删除还是修改epoll事件
    // 执行系统调用,删除epoll关注事件
    if (epoll_ctl(ep, op, c->fd, &ee) == -1) {
        ngx_log_error(NGX_LOG_ALERT, ev->log, ngx_errno,
                      "epoll_ctl(%d, %d) failed", op, c->fd);
        return NGX_ERROR;
    }

    // 删除事件成功,此事件不活跃,即已停止关注
    ev->active = 0;

    return NGX_OK;
}

删除读写事件

// epoll删除连接的读写事件
// 删除事件成功,读写事件都不活跃
static ngx_int_t
ngx_epoll_del_connection(ngx_connection_t *c, ngx_uint_t flags)
{
    int                 op;
    struct epoll_event  ee;

    if (flags & NGX_CLOSE_EVENT) {
        // 删除事件成功,读写事件都不活跃
        c->read->active = 0;
        c->write->active = 0;
        return NGX_OK;
    }

    // 直接删除所有的事件
    op = EPOLL_CTL_DEL;
    ee.events = 0;
    ee.data.ptr = NULL;

    // 执行系统调用,直接删除epoll关注事件
    if (epoll_ctl(ep, op, c->fd, &ee) == -1) {
        ngx_log_error(NGX_LOG_ALERT, c->log, ngx_errno,
                      "epoll_ctl(%d, %d) failed", op, c->fd);
        return NGX_ERROR;
    }

    // 删除事件成功,读写事件都不活跃
    c->read->active = 0;
    c->write->active = 0;

    return NGX_OK;
}


备注

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


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