Search code examples
linuxtimerkernelreal-time

Linux kernel: Why add_timer() is modifying my "expires" value?


I am trying to setup a periodic timer triggering a function every seconds, but there is a small drift between each call. After some investigations, I found that this is the add_timer() call which adds an offset of 2 to the expires field (~2ms in my case).

Why is this drift added? Is there a clean way to prevent it? I am not trying to get an accurate millisecond precision, I have a vague understanding of the kernel real-time limitations, but at least to avoid this intentional delay at each call.

Here is the output from a test module. Each couple of numbers is the value of the expires field just before and after the call:

[100047.127123] Init timer 1000
[100048.127986] Expired timer 99790884 99790886
[100049.129578] Expired timer 99791886 99791888
[100050.131146] Expired timer 99792888 99792890
[100051.132728] Expired timer 99793890 99793892
[100052.134315] Expired timer 99794892 99794894
[100053.135882] Expired timer 99795894 99795896
[100054.137411] Expired timer 99796896 99796898
[...]
[100071.164276] Expired timer 99813930 99813932
[100071.529455] Exit timer

And here is the source:

#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/init.h>
#include <linux/jiffies.h>
#include <linux/time.h>

static struct timer_list t;

static void timer_func(unsigned long data)
{
    unsigned long pre, post;
    t.expires = jiffies + HZ;
    pre = t.expires;
    add_timer(&t);
    post = t.expires;
    printk("Expired timer %lu %lu\n", pre, post);
}

static int __init timer_init(void)
{
    init_timer(&t);
    t.function = timer_func;
    t.expires = jiffies + HZ;
    add_timer(&t);
    printk("Init timer %d\n", HZ);
    return 0;
}

static void __exit timer_exit(void)
{
    del_timer(&t);
    printk("Exit timer\n");
}

module_init(timer_init);
module_exit(timer_exit);

Solution

  • I found the cause. Let's trace the add_timer function:

    The add_timer function calls:

    mod_timer(timer, timer->expires);
    

    The mod_timer function calls:

    expires = apply_slack(timer, expires);
    

    and then goes on to actually modify the timer.

    The apply_slack function says:

    /*
     * Decide where to put the timer while taking the slack into account
     *
     * Algorithm:
     *   1) calculate the maximum (absolute) time
     *   2) calculate the highest bit where the expires and new max are different
     *   3) use this bit to make a mask
     *   4) use the bitmask to round down the maximum time, so that all last
     *      bits are zeros
     */
    

    Before continuing, let's see what is the timer's slack. The init_timer macro eventually calls do_init_timer which sets the slack by default to -1.

    With this knowledge, let's reduce apply_slack and see what remains of it:

    static inline
    unsigned long apply_slack(struct timer_list *timer, unsigned long expires)
    {
            unsigned long expires_limit, mask;
            int bit;
    
            if (timer->slack >= 0) {
                    expires_limit = expires + timer->slack;
            } else {
                    long delta = expires - jiffies;
    
                    if (delta < 256)
                            return expires;
    
                    expires_limit = expires + delta / 256;
            }
            mask = expires ^ expires_limit;
            if (mask == 0)
                    return expires;
    
            bit = find_last_bit(&mask, BITS_PER_LONG);
    
            mask = (1 << bit) - 1;
    
            expires_limit = expires_limit & ~(mask);
    
            return expires_limit;
    }
    

    The first if, checking for timer->slack >= 0 fails, so the else part is applied. In that part the difference between expires and jiffies is slightly less than HZ (you just did t.expires = jiffies + HZ. Therefore, the delta in the function (with your data) is most likely about 4 and delta / 4 is non zero.

    This in turn implies that mask (which is expires ^ expires_limit) is not zero. The rest really depends on the value of expires, but for sure, it gets changed.

    So there you have it, since slack is automatically set to -1, the apply_slack function is changing your expires time to align with, I guess, the timer ticks.

    If you don't want this slack, you can set t.slack = 0; when you are initializing the timer in timer_init.