菜鸟学习Nginx之定时器

今天来分析一下Nginx定时器实现原理。

一、Nginx定时器

在网络通信中,客户端给服务端发送一个请求,如果服务端一直没有响应,那么客户端是不能一直傻傻的等待的,一般情况下,客户端需要设置一个超时定时器,当定时器超时后客户端需要进行后续处理。

Nginx定期器常用在upstream、子请求中,这些在分析http模块时已经介绍。

二、定时器实现

2.1 定时器

Nginx中定时器事件是一种事件,因此Nginx也将其统一到ngx_event_t中。该结构在《XX》介绍过,这里只把相关结构体成员拿出来备注说明一下:

typedef struct ngx_event_s           ngx_event_t;
struct ngx_event_s {
    ...
    unsigned timedout : 1; /* 1表示已经超时 */
    unsigned timer_set : 1;/* 1表示当前事件为定时器事件 */
    ngx_rbtree_node_t timer; /* 红黑树节点 定时器事件存储结构*/
    ...
}

Nginx中定时器存储结构采用的是红黑树,全局变量为ngx_event_timer_rbtree。对于红黑树原理以及具体源码不想介绍,网上介绍红黑树的文章很多,而且对于红黑树这中变态级数据结构,如果一周不看就会忘的很彻底。大家只要会用就行了。

2.2 源码介绍

Nginx定时器相关文件是ngx_event_timer.c,ngx_event_timer.h文件,具体内容不并很多。

/*
 * the event timer rbtree may contain the duplicate keys, however,
 * it should not be a problem, because we use the rbtree to find
 * a minimum timer value only
 * 初始化红黑树
 */
ngx_int_t
ngx_event_timer_init(ngx_log_t *log)
{
    ngx_rbtree_init(&ngx_event_timer_rbtree, &ngx_event_timer_sentinel,
                    ngx_rbtree_insert_timer_value);

    return NGX_OK;
}
/**
 * 查找时间最小的那个树节点 用于epoll等待时间
 * @return 当返回-1 表示永久阻塞
 */
ngx_msec_t
ngx_event_find_timer(void)
{
    ngx_msec_int_t      timer;//实际int值
    ngx_rbtree_node_t  *node, *root, *sentinel;

    if (ngx_event_timer_rbtree.root == &ngx_event_timer_sentinel) {//表示没有定时任务
        return NGX_TIMER_INFINITE;//返回永久不超时
    }
    /* 找到最近,要超时的事件 并且返回它所包含的时间 */
    root = ngx_event_timer_rbtree.root;
    sentinel = ngx_event_timer_rbtree.sentinel;

    node = ngx_rbtree_min(root, sentinel);

    timer = (ngx_msec_int_t) (node->key - ngx_current_msec);

    return (ngx_msec_t) (timer > 0 ? timer : 0);
}
/**
 * 循环遍历 注册的超时事件
 * 如果有超时事件 则执行回调函数
 */
void
ngx_event_expire_timers(void)
{
    ngx_event_t        *ev;
    ngx_rbtree_node_t  *node, *root, *sentinel;

    sentinel = ngx_event_timer_rbtree.sentinel;

    for ( ;; ) {
        root = ngx_event_timer_rbtree.root;

        if (root == sentinel) {
            return;
        }

        node = ngx_rbtree_min(root, sentinel);

        /* node->key > ngx_current_msec */

        if ((ngx_msec_int_t) (node->key - ngx_current_msec) > 0) {
            return;
        }
        /* 获取event事件 */
        ev = (ngx_event_t *) ((char *) node - offsetof(ngx_event_t, timer));

        ngx_log_debug2(NGX_LOG_DEBUG_EVENT, ev->log, 0,
                       "event timer del: %d: %M",
                       ngx_event_ident(ev->data), ev->timer.key);

        ngx_rbtree_delete(&ngx_event_timer_rbtree, &ev->timer); /* 从树中摘除树节点 */

#if (NGX_DEBUG)
        ev->timer.left = NULL;
        ev->timer.right = NULL;
        ev->timer.parent = NULL;
#endif

        ev->timer_set = 0;

        ev->timedout = 1;

        ev->handler(ev);//执行超时处理事件 指定回调函数
    }
}

 此函数用于定时器超时后,执行回调函数。执行ev->handler指向回调函数,由应用层软件实现。 

/**
 * 添加定时器任务
 * @param ev 事件
 * @param timer 等待时间 int值
 */
static ngx_inline void
ngx_event_add_timer(ngx_event_t *ev, ngx_msec_t timer)
{
    ngx_msec_t      key;
    ngx_msec_int_t  diff;

    key = ngx_current_msec + timer;

    if (ev->timer_set) {//表示在二叉树已经注册了超时事件

        /*
         * Use a previous timer value if difference between it and a new
         * value is less than NGX_TIMER_LAZY_DELAY milliseconds: this allows
         * to minimize the rbtree operations for fast connections.
         * 当新事件与旧事件,时间差值小于300ms则使用旧事件 不在创建新事件
         */

        diff = (ngx_msec_int_t) (key - ev->timer.key);

        if (ngx_abs(diff) < NGX_TIMER_LAZY_DELAY) {//沿用旧事件,不在创建新节点
            ngx_log_debug3(NGX_LOG_DEBUG_EVENT, ev->log, 0,
                           "event timer: %d, old: %M, new: %M",
                            ngx_event_ident(ev->data), ev->timer.key, key);
            return;
        }

        ngx_del_timer(ev);
    }

    ev->timer.key = key;

    ngx_log_debug3(NGX_LOG_DEBUG_EVENT, ev->log, 0,
                   "event timer add: %d: %M:%M",
                    ngx_event_ident(ev->data), timer, ev->timer.key);

    ngx_rbtree_insert(&ngx_event_timer_rbtree, &ev->timer);//注册到红黑树中 实际为插入到二叉树中

    ev->timer_set = 1;//标记当前事件为定时器事件
}
/**
 * 删除定时器事件
 * @param 待删除事件
 */
static ngx_inline void
ngx_event_del_timer(ngx_event_t *ev)
{
    ngx_log_debug2(NGX_LOG_DEBUG_EVENT, ev->log, 0,
                   "event timer del: %d: %M",
                    ngx_event_ident(ev->data), ev->timer.key);

    ngx_rbtree_delete(&ngx_event_timer_rbtree, &ev->timer);//从红黑树中删除

#if (NGX_DEBUG)
    ev->timer.left = NULL;
    ev->timer.right = NULL;
    ev->timer.parent = NULL;
#endif

    ev->timer_set = 0;
}

 上面介绍添加与删除定时器,主要是往红黑树中插入节点、删除节点。逻辑并不是很复杂。

三、使用场景

为了提升Nginx性能,尽量少调用系统调用gettimeofday(),Nginx实现了时间缓存功能。同时为了提升时间精度,Nginx也支持在nginx.conf中指定时间精度timer_resolution。

在分析事件模型时,就已经介绍过时间这部分处理。今天在仔细深入探讨一下这部分源码

3.1、定时器

在Nginx启动完成后,worker进程会进入无限循环中ngx_worker_process_cycle,然后Nginx会在ngx_process_events_and_timers阻塞/等待超时,如下:

/**
 * 此函数是处理事件的入口函数,所有业务流程起始函数
 * 《深入理解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)
    {//用户指定时间精度
        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
    }

    ...

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

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

    ...

    if (delta)
    {//时间差 表示时间超时,需要处理超时事件
        ngx_event_expire_timers();
    }

    ngx_event_process_posted(cycle, &ngx_posted_events);
}

说明:

1)当ngx_timer_resolution非0时,表示nginx.conf配置文件中设置时间精度,定时器超时严格按照该时间设定。否则在红黑树中查找最近要超时节点(红黑树节点key值为时间),换句话说就是按照时间最小值设定超时时间。

2)ngx_process_events(cycle, timer, flags);该方法会调用epoll_wait,epoll_wait最后一个参数表示等待时间,如果为-1则表示永久不超时。如下:参数timer则是ngx_process_events_and_timers函数中变量timer

static ngx_int_t
ngx_epoll_process_events(ngx_cycle_t *cycle, ngx_msec_t timer, ngx_uint_t flags)
{
   ...

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

Nginx通过epoll_wait,巧妙的实现定时器任务。网上也有settimer、select等方式,整体来说是大同小异的。在进入事件循环之前,记录时间点保存在delta中,当事件循环退出之后进行时间差比较,当delta大于0表示遍历红黑树节点,查找已经超时的节点,调用回调函数。

3.2、时间精度

上一节中不知道大家有没有注意到,当ngx_timer_resolution不为0时表示采用时间精度,那么Nginx会将timer设置为-1,如下:

    if (ngx_timer_resolution)
    {//用户指定时间精度,超时事件由SIGALRM触发
        timer = NGX_TIMER_INFINITE;// -1 
        flags = 0;
    }

 如果timer是-1,那么在epoll_wait岂不会永远不能结束了?实则不然。我们来看一下,ngx_timer_resolution相关代码,代码在ngx_event_module_init中,如下:

    /**
     * nginx.conf开启毫秒定时器精度
     */
    if (ngx_timer_resolution && !(ngx_event_flags & NGX_USE_TIMER_EVENT))
    {
        struct sigaction sa;
        struct itimerval itv;
        /* 注册信号 */
        ngx_memzero(&sa, sizeof(struct sigaction));
        sa.sa_handler = ngx_timer_signal_handler;
        sigemptyset(&sa.sa_mask);

        if (sigaction(SIGALRM, &sa, NULL) == -1)
        {
            ngx_log_error(NGX_LOG_ALERT, cycle->log, ngx_errno,
                          "sigaction(SIGALRM) failed");
            return NGX_ERROR;
        }

        itv.it_interval.tv_sec = ngx_timer_resolution / 1000;
        itv.it_interval.tv_usec = (ngx_timer_resolution % 1000) * 1000;
        itv.it_value.tv_sec = ngx_timer_resolution / 1000;
        itv.it_value.tv_usec = (ngx_timer_resolution % 1000) * 1000;
        //启动定时器 当超时后会产生SIGALRM信号
        if (setitimer(ITIMER_REAL, &itv, NULL) == -1)
        {
            ngx_log_error(NGX_LOG_ALERT, cycle->log, ngx_errno,
                          "setitimer() failed");
        }
    }

通过上面代码可知,当开启时间精度,Nginx会向操作系统注册SIGALRM信号, 当时间流失ngx_timer_resolution毫秒后,操作系统就会产生SIGALRM信号中断,然后进程转而调用中断处理函数,当中断处理函数结束后epoll_wait就会返回,返回值为-1且errno为EINTR。以上内容需要了解,这样整个流程就串起来了。

四、总结

通常定时器实现主要需要两点:

1)注册定时器,可以使用系统提供setitimer,select,epoll或者像Nginx一样实现在自己的定时器管理模块。

2)定时器回调函数,这个也是必须的。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值