菜鸟学习nginx之事件模块epoll(1)

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/xxb249/article/details/84974890

上一篇介绍核心事件模块,本篇介绍事件模块ngx_epoll_module。Nginx在linux环境下采用epoll网络模型,对于epoll网络型不了解的可自行百度查询,本篇不在阐述。

一、问题

本篇要澄清以下几个问题:

1、当客户端发起TCP连接后,事件模块是如何管理新连接?

2、Nginx是如何接收到客户端请求(只是TCP层请求非HTTP请求)?

3、Nginx是如何发送响应给客户端(只是TCP层响应)?

4、超时事件管理方式

二、事件模块ngx_epoll_module

2.0、接口定义

typedef struct
{
    ngx_int_t (*add)(ngx_event_t *ev, ngx_int_t event, ngx_uint_t flags);
    ngx_int_t (*del)(ngx_event_t *ev, ngx_int_t event, ngx_uint_t flags);

    ngx_int_t (*enable)(ngx_event_t *ev, ngx_int_t event, ngx_uint_t flags);
    ngx_int_t (*disable)(ngx_event_t *ev, ngx_int_t event, ngx_uint_t flags);

    ngx_int_t (*add_conn)(ngx_connection_t *c);
    ngx_int_t (*del_conn)(ngx_connection_t *c, ngx_uint_t flags);

    ngx_int_t (*notify)(ngx_event_handler_pt handler);

    ngx_int_t (*process_events)(ngx_cycle_t *cycle, ngx_msec_t timer,
                                ngx_uint_t flags);

    ngx_int_t (*init)(ngx_cycle_t *cycle, ngx_msec_t timer);
    void (*done)(ngx_cycle_t *cycle);
} ngx_event_actions_t;

typedef struct
{
    ngx_str_t *name;

    void *(*create_conf)(ngx_cycle_t *cycle);
    char *(*init_conf)(ngx_cycle_t *cycle, void *conf);

    ngx_event_actions_t actions;
} ngx_event_module_t;

 通过ngx_event_module_t定义一个新的事件模块。事件模块主要是用接收、发送报文,通常情况下我们不需要自己创建一个新的事件模块。接口ngx_event_actions_t中add、del是一对操作,主要用于向epoll注册事件、删除事件。init用于初始化事件模块,对于epoll来说init回调函数是ngx_epoll_init。

2.1、模块定义

static ngx_str_t epoll_name = ngx_string("epoll");

static ngx_command_t ngx_epoll_commands[] = {

    {ngx_string("epoll_events"),
     NGX_EVENT_CONF | NGX_CONF_TAKE1,
     ngx_conf_set_num_slot,
     0,
     offsetof(ngx_epoll_conf_t, events),
     NULL},

    {ngx_string("worker_aio_requests"),
     NGX_EVENT_CONF | NGX_CONF_TAKE1,
     ngx_conf_set_num_slot,
     0,
     offsetof(ngx_epoll_conf_t, aio_requests),
     NULL},

    ngx_null_command};

static ngx_event_module_t ngx_epoll_module_ctx = {
    &epoll_name,
    ngx_epoll_create_conf, /* create configuration */
    ngx_epoll_init_conf,   /* init configuration */

    {
        ngx_epoll_add_event,      /* add an event */
        ngx_epoll_del_event,      /* delete an event */
        ngx_epoll_add_event,      /* enable an event */
        ngx_epoll_del_event,      /* disable an event */
        ngx_epoll_add_connection, /* add an connection */
        ngx_epoll_del_connection, /* delete an connection */
#if (NGX_HAVE_EVENTFD)
        ngx_epoll_notify, /* trigger a notify */
#else
        NULL, /* trigger a notify */
#endif
        ngx_epoll_process_events, /* process the events */
        ngx_epoll_init,           /* init the events */
        ngx_epoll_done,           /* done the events */
    }};

ngx_module_t ngx_epoll_module = {
    NGX_MODULE_V1,
    &ngx_epoll_module_ctx, /* module context */
    ngx_epoll_commands,    /* module directives */
    NGX_EVENT_MODULE,      /* module type */
    NULL,                  /* init master */
    NULL,                  /* init module */
    NULL,                  /* init process */
    NULL,                  /* init thread */
    NULL,                  /* exit thread */
    NULL,                  /* exit process */
    NULL,                  /* exit master */
    NGX_MODULE_V1_PADDING};

2.2、事件模块初始化

在上一篇有介绍到,在初始化ngx_event_core_module时会调用具体事件模块的init回调函数即ngx_epoll_init,如果对于epoll模型比较了解,看起来应该很容易,具体代码如下:

/**
 * epoll模型初始化
 * @param cycle 核心结构体
 * @param timer 定时器超时时间
 */
static ngx_int_t
ngx_epoll_init(ngx_cycle_t *cycle, ngx_msec_t timer)
{
    ngx_epoll_conf_t *epcf;
    /* 获取epoll模型下配置结构 */
    epcf = ngx_event_get_conf(cycle->conf_ctx, ngx_epoll_module);

    if (ep == -1)
    {   
        /**
         * 创建epoll对象 其中epoll_create参数没有意义
         */
        ep = epoll_create(cycle->connection_n / 2);

        if (ep == -1)
        {
            ngx_log_error(NGX_LOG_EMERG, cycle->log, ngx_errno,
                          "epoll_create() failed");
            return NGX_ERROR;
        }

#if (NGX_HAVE_EVENTFD)
        /**
         * 使用eventfd实现 事件通知功能 主要用于多线程模式下  目前可忽略
         */
        if (ngx_epoll_notify_init(cycle->log) != NGX_OK)
        {
            ngx_epoll_module_ctx.actions.notify = NULL;
        }
#endif

#if (NGX_HAVE_FILE_AIO)
        ngx_epoll_aio_init(cycle, epcf);
#endif

#if (NGX_HAVE_EPOLLRDHUP)
        ngx_epoll_test_rdhup(cycle);
#endif
    }
    /**
     * 创建epoll_event数组 用于存储epoll返回事件
     * 数组大小为512
     */
    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;//一次性 最大可保存 epoll事件数

    ngx_io = ngx_os_io;

    ngx_event_actions = ngx_epoll_module_ctx.actions;

#if (NGX_HAVE_CLEAR_EVENT)
    ngx_event_flags = NGX_USE_CLEAR_EVENT  /* ET模式 */
#else
    ngx_event_flags = NGX_USE_LEVEL_EVENT
#endif
                      | NGX_USE_GREEDY_EVENT | NGX_USE_EPOLL_EVENT;

    return NGX_OK;
}

2.3、事件模块关闭

事件模块关闭比较简单,直接调用close方法,将对应的对象关闭即可

static void
ngx_epoll_done(ngx_cycle_t *cycle)
{
    /* 关闭epoll对象 */
    if (close(ep) == -1)
    {
        ngx_log_error(NGX_LOG_ALERT, cycle->log, ngx_errno,
                      "epoll close() failed");
    }

    ep = -1;

#if (NGX_HAVE_EVENTFD)
    /* 关闭eventfd对象 */
    if (close(notify_fd) == -1)
    {
        ngx_log_error(NGX_LOG_ALERT, cycle->log, ngx_errno,
                      "eventfd close() failed");
    }

    notify_fd = -1;

#endif

#if (NGX_HAVE_FILE_AIO)

    if (ngx_eventfd != -1)
    {

        if (io_destroy(ngx_aio_ctx) == -1)
        {
            ngx_log_error(NGX_LOG_ALERT, cycle->log, ngx_errno,
                          "io_destroy() failed");
        }

        if (close(ngx_eventfd) == -1)
        {
            ngx_log_error(NGX_LOG_ALERT, cycle->log, ngx_errno,
                          "eventfd close() failed");
        }

        ngx_eventfd = -1;
    }

    ngx_aio_ctx = 0;

#endif

    ngx_free(event_list); //释放事件数组

    event_list = NULL;
    nevents = 0;
}

三、事件循环

在上一小节介绍事件模块初始化和关闭流程,那最主要的事件循环是在哪里实现的呢?其实在模块定义的时候就已经声明了ngx_epoll_process_events,该函数在什么地方调用呢?在介绍《菜鸟学习Nginx之启动流程(3)》中有提到,如下所示:

static void
ngx_worker_process_cycle(ngx_cycle_t *cycle, void *data)
{
    ...
    for (;;)
    {
        ...
        /* 阻塞 等待事件或者定时器超时事件 */
        ngx_process_events_and_timers(cycle);
    }
}

3.1、事件循环

事件循环具体流程图如下:

从流程图中可得,主要有三部分内容需要处理:

1)获取定时器超时事件,因为epoll_wait需要指定返回时间。超时时间可能为-1(表示永远不超时), 也可能是大于0

2)是否开启进程间互斥锁,当有多个worker进程时会开启。当开启互斥锁时就需要处理"惊群"问题。

3)处理事件

具体代码如下:

/**
 * 此函数是处理事件的入口函数,所有业务流程起始函数
 * 《深入理解Nginx模块开发与架构解析》 P331 
 */
void ngx_process_events_and_timers(ngx_cycle_t *cycle)
{
    ngx_uint_t flags;
    ngx_msec_t timer, delta;

    if (ngx_timer_resolution)
    {//用户指定时间精度,超时事件由SIGALARM触发
        timer = NGX_TIMER_INFINITE;
        flags = 0;
    }
    else
    {
        //获取下一个超时时间 如果二叉树中没有超时事件则返回-1 代表永久不超时
        timer = ngx_event_find_timer();
        flags = NGX_UPDATE_TIME; //表示需要更新时间缓存

#if (NGX_WIN32)

        /* handle signals from master in case of network inactivity */

        if (timer == NGX_TIMER_INFINITE || timer > 500)
        {
            timer = 500;
        }

#endif
    }
    
    /* 解决惊群 */
    if (ngx_use_accept_mutex)
    {
        if (ngx_accept_disabled > 0)
        {//实现worker进程间负载均衡
            ngx_accept_disabled--;
        }
        else
        {//解决惊群,通过进程间同步锁
            if (ngx_trylock_accept_mutex(cycle) == NGX_ERROR)
            {
                return;
            }

            if (ngx_accept_mutex_held)
            {
                flags |= NGX_POST_EVENTS;
            }
            else
            {
                if (timer == NGX_TIMER_INFINITE || timer > ngx_accept_mutex_delay)
                {
                    timer = ngx_accept_mutex_delay;
                }
            }
        }
    }

    /* 记录时间差 */
    delta = ngx_current_msec;
    
    /**
     * 如果是epoll模型 此处实际调用函数是ngx_epoll_process_events
     * 阻塞在epoll_wait
     */    
    (void)ngx_process_events(cycle, timer, flags);

    delta = ngx_current_msec - delta; /* 记录时间差 */

    ngx_log_debug1(NGX_LOG_DEBUG_EVENT, cycle->log, 0,
                   "timer delta: %M", delta);
    /**
     * 如果是epoll模型 在ngx_epoll_process_events函数中可能会对事件进行划分
     * 划分到不同队列中,其中accept队列要优先处理
     */    
    ngx_event_process_posted(cycle, &ngx_posted_accept_events);
    
    if (ngx_accept_mutex_held)
    {//释放进程间锁
        ngx_shmtx_unlock(&ngx_accept_mutex);
    }

    if (delta)
    {//时间差 表示时间超时,需要处理超时事件
        ngx_event_expire_timers();
    }
    /* 处理其他事件 */
    ngx_event_process_posted(cycle, &ngx_posted_events);
}

对于惊群处理,有单独文章介绍,本篇不深入剖析。 

3.2、事件驱动 

接下来看一下事件驱动是如何处理的。上一小节中ngx_process_events函数实际调用的是ngx_epoll_process_events函数。该函数逻辑相对复杂一些,采用分片展示代码逻辑:

/**
 * 事件驱动
 * @param cycle 核心结构体
 * @param timer 等待时间
 * @param flags
 *        取值: NGX_POST_EVENTS、NGX_UPDATE_TIME 
 */
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);

   /**
     * timer不是固定不变的,如果没有任何事件发生(空闲期),
     * timer可能是NGX_TIMER_INFINITE 即表示永久阻塞
     */
    events = epoll_wait(ep, event_list, (int)nevents, timer);

说明:

epoll_wait返回有三种场景:

1、超时时间到期,即timer > 0的场景,当超时时间到了仍然没有事件发生(读写事件)

2、超时时间为timer=-1(永远不超时),但是发生了SIGARLM信号,当信号处理函数结束后,epoll_wait返回,errno为NGX_EINTR

3、超时间未到期(timer=-1或者timer>0),发生了读写事件(一般正常情况)

所以下面的代码会针对这三种场景进行处理:

    /**
     * Nginx两种时间策略: 
     *   1、如果nginx.conf文件中定义时间精度timer_resolution,则表示nginx的时间
     *      缓存精确到ngx_timer_resolution毫秒
     *   2、如果没有定义时间精度 则严格按照系统时间
     * ----------------------------------------------------------------
     * 条件说明:
     *     flags & NGX_UPDATE_TIME  -- 表示强制更新系统时间
     *     ngx_event_timer_alarm  当采用时间精度时,nginx会启动一个定时器,每次
     *                            超时,都会产生SIGALRM信号。具体参考ngx_event_process_init
     */
    if (flags & NGX_UPDATE_TIME || ngx_event_timer_alarm)
    {//只要是时间相关的事件 就立即更新时间缓存
        ngx_time_update(); //更新时间缓存
    }

    if (err)
    {
        if (err == NGX_EINTR)
        {

            if (ngx_event_timer_alarm)
            {//表示发生了SIGALRM信号中断 认为是正常场景
                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;
    }

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

上面这段代码主要是针对场景1,场景2的处理 ,相对简单。接下来看一下重头戏,对于正常读写事件的处理流程。

for (i = 0; i < events; i++)
    {
        c = event_list[i].data.ptr;
        /* 指针变量 最后一位始终为0  节省内存空间 */
        instance = (uintptr_t)c & 1;
        c = (ngx_connection_t *)((uintptr_t)c & (uintptr_t)~1);

        rev = c->read;

        if (c->fd == -1 || rev->instance != instance)
        {/**
          * 当fd=-1 或者instance不一致表示 当前事件是过期事件不需要处理
          * 《深入理解Nginx模块开发与架构解析》一书:318页 
          */
            /*
             * 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;

        ngx_log_debug3(NGX_LOG_DEBUG_EVENT, cycle->log, 0,
                       "epoll: fd:%d ev:%04XD d:%p",
                       c->fd, revents, event_list[i].data.ptr);

        if (revents & (EPOLLERR | EPOLLHUP))
        {
            ngx_log_debug2(NGX_LOG_DEBUG_EVENT, cycle->log, 0,
                           "epoll_wait() error on fd:%d ev:%04XD",
                           c->fd, revents);

            /*
             * if the error events were returned, add EPOLLIN and EPOLLOUT
             * to handle the events at least in one active handler
             */

            revents |= EPOLLIN | EPOLLOUT;
        }

循环遍历事件队列events_list中事件,从私有数据字段中取出instance和conection对象。

说明:

1、instance值,利用指针最后一位始终为0的特性保存。为什么指针最后一位始终为0呢?操作系统在分配内存,为了提升访问速度,经常是4字节/8字节对其,这样分配出来的地址最后一位始终为0。

2、为什么需要判断rev->instance 和 instance是否相等?下面这段话摘自《深入理解Nginx模块开发与架构解析》一书:318页:

那么,过期事件又是怎么回事呢?举个例子,假设epoll_wait一次返回3个事件,在第1个事件的处理过程中,由于业务的需要,所以关闭了一个连接,而这个连接恰好对应第3个事件。这样的话,在处理到第3个事件时,这个事件就已经是过期事件了,一旦处理必然出错。既然如此,把关闭的这个连接的fd套接字置为–1能解决问题吗?答案是不能处理所有情况。

下面先来看看这种貌似不可能发生的场景到底是怎么发生的:假设第3个事件对应的ngx_connection_t连接中的fd套接字原先是50,处理第1个事件时把这个连接的套接字关闭了,同时置为–1,并且调用ngx_free_connection将该连接归还给连接池。在
ngx_epoll_process_events方法的循环中开始处理第2个事件,恰好第2个事件是建立新连接事件,调用ngx_get_connection从连接池中取出的连接非常可能就是刚刚释放的第3个事件对应的连接。由于套接字50刚刚被释放,Linux内核非常有可能把刚刚释放的套接字50又分配给新建立的连接。因此,在循环中处理第3个事件时,这个事件就是过期的了!它对应的事件是关闭的连接,而不是新建立的连接。

下面是读事件处理,若是NGX_POST_EVENTS事件则延迟处理并且按照优先级进行区分,Accept事件优先级高于其他的读写事件,所以下面在处理读事件进行区分处理,如下所示:

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

 写事件处理与读事件处理大同小异,如下:

        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 (NGX_THREADS)
            wev->complete = 1;
#endif

            if (flags & NGX_POST_EVENTS)
            {//延迟处理该事件
                ngx_post_event(wev, &ngx_posted_events);
            }
            else
            {//立即处理该事件
                wev->handler(wev);
            }
        }

四、总结

到这里把epoll事件驱动处理流程介绍完毕。但是事件是如何注册到epoll中呢又如何删除呢?在下一篇介绍。

猜你喜欢

转载自blog.csdn.net/xxb249/article/details/84974890