为什么Linux内核无法保证动态定时器的执行时间

每每谈到Linux的动态定时器,我们都要说,内核无法保证其执行时间,一般会延迟几百毫秒。几百毫秒这个一个数,绝对不是空穴来风,万物总归有其缘由。文章主要就和大家谈谈这么一个几百毫秒,究竟是从哪里冒出来的呢?

首先需要说明的是,这里并不打算详细讨论Linux动态定时器的实现,而仅仅讲述与延迟相关的部分实现。关于其实现,参考《深入理解Linux内核》第二版247页(英文也刚好是第247页)。此外,这篇博文也写的挺详细的。


下面简要说明一下其对应的数据结构。

1. Per-CPU data structure

struct tvec_t_base_s {
    spinlock_t lock;
    unsigned long timer_jiffies;
    struct timer_list *running_timer;
    tvec_root_t tv1;
    tvec_t tv2;
    tvec_t tv3;
    tvec_t tv4;
    tvec_t tv5;
} ____cacheline_aligned_in_smp;

typedef struct tvec_t_base_s tvec_base_t;

每个CPU都有这么一个tvec_base_ttimer_jiffies存放的是最早到期的dynamic timer所对应的 jiffies。


2. tv1 ~ tv5本质上就是几个数组

typedef struct tvec_s {
    struct list_head vec[256];
} tvec_t;

typedef struct tvec_root_s {
    struct list_head vec[64];
} tvec_root_t;

根据定时器到期时间与当前jiffies的差值(这里姑且称其为delta),将其放置于tv1 ~ tv5的某一个链表中。

tv1: 0delta<28
tv2: 28delta<2141
tv3: 214delta<2201

tv4, tv5类推。

这里,tv1一个链表对应一个时间,tv2一个对应 28 个,tv3对应 214 个……


3. 执行

在 Linux 2.6 中,动态定时器用softirq执行,对应的函数的run_timer_softirq(),核心代码由函数__run_timers()执行

/***
 * __run_timers - run all expired timers (if any) on this CPU.
 * @base: the timer vector to be processed.
 *
 * This function cascades all vectors and executes all expired timer
 * vectors.
 */
#define INDEX(N) (base->timer_jiffies >> (TVR_BITS + N * TVN_BITS)) & TVN_MASK

static inline void __run_timers(tvec_base_t *base)
{
    struct timer_list *timer;

    spin_lock_irq(&base->lock);
    while (time_after_eq(jiffies, base->timer_jiffies)) {
        struct list_head work_list = LIST_HEAD_INIT(work_list);
        struct list_head *head = &work_list;
        int index = base->timer_jiffies & TVR_MASK;

        /*
         * Cascade timers:
         */
        if (!index &&
            (!cascade(base, &base->tv2, INDEX(0))) &&
                (!cascade(base, &base->tv3, INDEX(1))) &&
                    !cascade(base, &base->tv4, INDEX(2)))
            cascade(base, &base->tv5, INDEX(3));
        ++base->timer_jiffies; 
        list_splice_init(base->tv1.vec + index, &work_list);
repeat:
        if (!list_empty(head)) {
            void (*fn)(unsigned long);
            unsigned long data;

            timer = list_entry(head->next,struct timer_list,entry);
            fn = timer->function;
            data = timer->data;

            list_del(&timer->entry);
            set_running_timer(base, timer);
            smp_wmb();
            timer->base = NULL;
            spin_unlock_irq(&base->lock);
            {
                u32 preempt_count = preempt_count();
                fn(data);
                if (preempt_count != preempt_count()) {
                    printk("huh, entered %p with %08x, exited with %08x?\n",
                           fn, preempt_count, preempt_count());
                    BUG();
                }
            }
            spin_lock_irq(&base->lock);
            goto repeat;
        }
    }
    set_running_timer(base, NULL);
    spin_unlock_irq(&base->lock);
}

其中,与我们这里所要讨论的问题最为密切的,其实是while循环中那句很不起眼的

// TVR_MASK == 0xff
int index = base->timer_jiffies & TVR_MASK;

《深入理解Linux内核》中对他的描述是“计算base->tv1中链表的索引,该索引保存这下一次将要处理的定时器”,上面所提到的博文所说到,和这句话出入不大。可恰恰是这么一个平平淡淡的语句,让我百思不得其解。

为什么timer_jiffies的低8位就是下一次要执行的定时器索引呢?不是说,按照到期时间与当前jiffies的差值,依次插入tv1 ~ tv5的某一个链表吗(察看internal_add_timer()的源码,确实是根据其差值,插入了对应的链表)?既然如此,那为什么不是从tv1.vec[0]开始执行,他们不是都到期了吗?

好吧,我也不知道他们怎么就能够那么理直气壮地说,“执行该index对应的链表”。不过我这里并不准备反驳他们,因为他们说的没错,确实是执行timer_jiffies低8位对应的链表!

既然他们的对的,下面,我们就按照他们的思路,用我们的大脑,来执行一个这个算法吧。


首先,假定timer_jiffies == 1(如果你记得上面的提出的问题,从0开始,应该会让你比较舒服。但这并不利于问题的描述,所以,还是忍一忍吧)。于是便有了index == 1,然后,我们开始执行tv1.vec[index]链表中的定时器。两百多个时钟中断过去后(其实也不一定会有两百多个,但应该差不多),timer_jiffies == 256(注意上面程序中的++base->timer_jiffies)。现在,屏住呼吸,内核程序员们又要开始玩杂耍了。

现在,当我们再次计算index时,index终于等于0了!明明index = 0的差值最小的,我们却要经历两百多个轮回,才得以再见到他。这么一个珍贵的重逢,也让cascade()函数有了执行的机会。就这里而言,他将tv2.vec[0]中的定时器放到了tv1对应的链表中(回想一下,上面我们提到,tv2一个链表对应256个jiffies)。

if (!index &&
        (!cascade(base, &base->tv2, INDEX(0))) &&
            (!cascade(base, &base->tv3, INDEX(1))) &&
                !cascade(base, &base->tv4, INDEX(2)))
        cascade(base, &base->tv5, INDEX(3));

随着时间流逝,下一次是tv2.vec[1], tv2.vec[2] ... tv2.vec[63], tv3.vec[1] ...

注意,这里并没有写错,确确实实是从1开始的。考虑tv2,
其范围为 28delta<2141 ,或者说,0x100 ~ 0x3ff,计算tv2下标时:int i = (expires >> 8) & 63(详见internal_add_timer()),这里,i并不会等于0。


这里,在256个循环中,有一个会从tv2中补充定时器至tv1 25626=214 次循环后,会从tv3补充……由此可见,cascade()执行的频率其实是相当低的,换言之,该算法执行相当高效。


这里也许应该就是所谓的杂技代码了吧,像是玩杂耍一般的精巧。



现在,能够回答我们最初的问题了。

假定某个时刻,index == 123,某个倒霉的家伙,插入一个timer的时候,偏偏他所计算出来的差值为122,然后他就被插到了tv1.vec[122]中去了,然后,他就只能苦苦等待下一个循环——等base->timer_jiffies & 255绕回来!也就是说,本来是差了122 ticks,现在,他要等255 ticks(当然,也有可能提前执行,至于例子,还是留给读者去思考吧)。再加上更高优先级的 HI_SOFTIRQ和同一个链表中前面的定时器所对应函数的执行时间(还有其他中断的执行),也就有了动态定时器那几百毫秒的精度了。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值