linux kernel 时钟系统的前世今生

趁工作不忙想把最近工作中研究到的kernel的时钟系统 软中断 定时器 tasklet 工作队列实现机制总结下,首先说明,这些原理实现对编写driver不会有多大帮助,但是明白理解这些kernel机制的实现原理,对于我们从系统角度去思考解决问题,会有很大帮助。上篇博文《一个奇葩bug的解决》就印证了这一点,链接如下:http://blog.csdn.net/skyflying2012/article/details/44623515。

那么为什么要把这些内容放在一起总结,因为他们之间是相关联的,kernel的时钟系统和软中断结合实现了定时器,tasklet 工作队列的实现也都是基于软中断的。

内核版本:3.4.55 arm平台

首先把kernel的时钟系统依照我的理解总结下。

时钟系统中最重要的结构体变量:clockevent clocksource xtime,结构体内容不贴了。

clockevent为kernel提供了时钟中断的一些处理函数,特别是对于tickless系统,提供了设置下一次时钟中断时间点的接口set_next_event和timer模式设置接口set_mode。

clocksource则是kernel真正的计数者 时钟源,常规的时钟中断中的计数以及提高精度的补充计数都来自于clocksource。

clocksource成员rating代表了时钟精度,参考值如下:

1--99: 不适合于用作实际的时钟源,只用于启动过程或用于测试;

100--199:基本可用,可用作真实的时钟源,但不推荐;

200--299:精度较好,可用作真实的时钟源;

300--399:很好,精确的时钟源;

400--499:理想的时钟源,如有可能就必须选择它作为时钟源;

xtime是kernel的墙上时间,记录从1970-1-1至今的时间差,不管是用户空间还是内核空间要获取的系统时间都需要去读取xtime。

接下来跟随代码,我们来看下kernel时钟系统的前世今生吧。

一 时钟系统的初始化

start_kernel中与时钟系统相关的函数,按照执行先后有:tick_init  timekeeping_init  time_init。

 

1 tick_init

tick_init注册了与clockevent相关的通知处理函数tick_notify:

static int tick_notify(struct notifier_block *nb, unsigned long reason,
                   void *dev)
{
    switch (reason) {

    case CLOCK_EVT_NOTIFY_ADD:
        return tick_check_new_device(dev);

    case CLOCK_EVT_NOTIFY_BROADCAST_ON:
    case CLOCK_EVT_NOTIFY_BROADCAST_OFF:
    case CLOCK_EVT_NOTIFY_BROADCAST_FORCE:
        tick_broadcast_on_off(reason, dev);
        break;

    case CLOCK_EVT_NOTIFY_BROADCAST_ENTER:
    case CLOCK_EVT_NOTIFY_BROADCAST_EXIT:
        tick_broadcast_oneshot_control(reason);
        break;

    case CLOCK_EVT_NOTIFY_CPU_DYING:
        tick_handover_do_timer(dev);
        break;
<pre name="code" class="plain">    case CLOCK_EVT_NOTIFY_CPU_DEAD:
        tick_shutdown_broadcast_oneshot(dev);
        tick_shutdown_broadcast(dev);
        tick_shutdown(dev);
        break;

    case CLOCK_EVT_NOTIFY_SUSPEND:
        tick_suspend();
        tick_suspend_broadcast();
        break;

    case CLOCK_EVT_NOTIFY_RESUME:
        tick_resume();
        break;

    default:
        break;
    }

    return NOTIFY_OK;
}

在后面初始化中我们会用到CLOCK_EVT_NOTIFY_ADD,来添加我们平台相关的clockevent。

2 timekeeping_init

void __init timekeeping_init(void)
{
    struct clocksource *clock;
    unsigned long flags;
    struct timespec now, boot;

    read_persistent_clock(&now);
    if (!timespec_valid_strict(&now)) {
        pr_warn("WARNING: Persistent clock returned invalid value!\n"
            "         Check your CMOS/BIOS settings.\n");
        now.tv_sec = 0;
        now.tv_nsec = 0;
    }

    read_boot_clock(&boot);
    if (!timespec_valid_strict(&boot)) {
        pr_warn("WARNING: Boot clock returned invalid value!\n"
            "         Check your CMOS/BIOS settings.\n");
        boot.tv_sec = 0;
        boot.tv_nsec = 0;
    }

首先获取外部RTC时间和系统启动时间。这里需要说明下:

kernel运行时系统时间频繁使用,为了提高效率,kernel不会每次获取系统时间时去读外部RTC,访问效率太低,而是在内存中使用xtime来维护系统时间。

RTC单独电池供电不掉电,系统启动中会根据RTC时间同步系统时钟,之后系统时钟根据时钟中断来独立更新运行。

这里read_persistent_clock和read_boot_clock2个平台级函数提供接口来获取外部RTC时间和系统启动时间。

/**
 * read_persistent_clock -  Return time from the persistent clock.
 *
 * Weak dummy function for arches that do not yet support it.
 * Reads the time from the battery backed persistent clock.
 * Returns a timespec with tv_sec=0 and tv_nsec=0 if unsupported.
 *
 *  XXX - Do be sure to remove it once all arches implement it.
 */
void __attribute__((weak)) read_persistent_clock(struct timespec *ts)
{
    ts->tv_sec = 0;
    ts->tv_nsec = 0;
}

/**
 * read_boot_clock -  Return time of the system start.
 *
 * Weak dummy function for arches that do not yet support it.
 * Function to read the exact time the system has been started.
 * Returns a timespec with tv_sec=0 and tv_nsec=0 if unsupported.
 *
 *  XXX - Do be sure to remove it once all arches implement it.
 */
void __attribute__((weak)) read_boot_clock(struct timespec *ts)
{
    ts->tv_sec = 0;
    ts->tv_nsec = 0;
}

这2个函数都是weak类型的,我们可以在平台支持代码中去实现这2个函数来获取RTC时间。但是大部分平台没有实现这2个函数。(可能考虑到外部RTC芯片需在加载driver后初始化才能使用)

而是在加载RTC driver后根据CONFIG_RTC_HCTOSYS内核选项决定是否同步时间。相关代码在driver/rtc/hctsys.c中,这里就不贴了。

接着看timkeeping_init代码。

   seqlock_init(&timekeeper.lock);

    ntp_init();

    write_seqlock_irqsave(&timekeeper.lock, flags);
    clock = clocksource_default_clock();
    if (clock->enable)
        clock->enable(clock);
    timekeeper_setup_internals(clock);

    timekeeper.xtime.tv_sec = now.tv_sec;
    timekeeper.xtime.tv_nsec = now.tv_nsec;
    timekeeper.raw_time.tv_sec = 0;
    timekeeper.raw_time.tv_nsec = 0;
    if (boot.tv_sec == 0 && boot.tv_nsec == 0) {
        boot.tv_sec = timekeeper.xtime.tv_sec;
        boot.tv_nsec = timekeeper.xtime.tv_nsec;
    }
    set_normalized_timespec(&timekeeper.wall_to_monotonic,
                -boot.tv_sec, -boot.tv_nsec);
    update_rt_offset();
    timekeeper.total_sleep_time.tv_sec = 0;
    timekeeper.total_sleep_time.tv_nsec = 0;
    write_sequnlock_irqrestore(&timekeeper.lock, flags);
}

初始化锁和ntp(时间校正),获取kernel下默认的clocksource,clocksource_default_clock如下:

struct clocksource clocksource_jiffies = {
    .name       = "jiffies",
    .rating     = 1, /* lowest valid rating*/
    .read       = jiffies_read,
    .mask       = 0xffffffff, /*32bits*/
    .mult       = NSEC_PER_JIFFY << JIFFIES_SHIFT, /* details above */
    .shift      = JIFFIES_SHIFT,
};
......
struct clocksource * __init __weak clocksource_default_clock(void)
{
    return &clocksource_jiffies;
}

这里也有weak属性,因此如果平台没有实现clocksource_default_clock,则使用该处定义。默认clocksource为jiffies。rating精度为1,是最低精度,随时可以被其他clocksource替换。

接下来timekeeper_setup_internals根据default clock初始化timekeeper相关成员变量。

timekeeping_init剩余代码也是将timekeeper相关变量进行初始化,我们最关心的xtime成员则使用获取RTC时间的now初始化。

timekeeping_init根据RTC时间和default clock来初始化全局时间结构体timekeeper,特别注意其成员变量xtime,其使用获取的RTC时间来初始化。

3 time_init

void __init time_init(void)
{
    system_timer = machine_desc->timer;
    system_timer->init();
    sched_clock_postinit();
}

time_init开始根据平台设备描述符来初始化timer,以我的平台timer代码为例,如下:

static void __init
timer_init( void )
{
    //关闭timer,清中断
    timer_stop_all();
    timer_clr_all_pnd();

    //注册clockevent和timer irq,使能中断
    timer_clockevent_init();
    timer_init_irq();

    //注册clocksource
    timer_clocksource_init();
}

struct sys_timer vtimer =
{
    .init    = timer_init,

#if defined( CONFIG_PM ) && ( !CONFIG_GENERIC_CLOCKEVENTS )
    .suspend = timer_suspend,
    .resume  = timer_resume,
#endif
};

来看timer_clockevent_init和timer_clocksource_init,如下

static struct clock_event_device timer_clockevent =
{
    .name           = MTAG_TIMER,
    .features       = CLOCK_EVT_FEAT_PERIODIC | CLOCK_EVT_FEAT_ONESHOT,
    .rating         = 200,
    .set_mode       = timer_set_mode,
    .set_next_event = timer_set_next_event,
};

static void __init
timer_clockevent_init( void )
{
    clockevents_calc_mult_shift( &timer_clockevent, CLOCK_TICK_RATE, 4 );

    timer_clockevent.max_delta_ns = clockevent_delta2ns( 0xffffffff, &timer_clockevent );
    timer_clockevent.min_delta_ns = clockevent_delta2ns( CLOCKEVENT_MIN_DELTA, &timer_clockevent );
    timer_clockevent.cpumask      = cpumask_of( 0 );
    clockevents_register_device( &timer_clockevent );
}
......
static struct clocksource timer_clocksource =
{
    .name   = MTAG_TIMER,
    .rating = 300,
    .read   = timer_get_cycles,
    .mask   = CLOCKSOURCE_MASK( 32 ),
    .flags  = CLOCK_SOURCE_IS_CONTINUOUS,
};

static u32 notrace
update_sched_clock( void )
{
    return __raw_readl(IO_ADDRESS( REG_TIMER_TMR2DL ));
}

static int __init
timer_clocksource_init( void )
{
    u32 val = 0, mode = 0;

    timer_stop( 2 );
    __raw_writel( TIMER2_TARGET, IO_ADDRESS( REG_TIMER_TMR2TGT ));
    val = __raw_readl( IO_ADDRESS( REG_TIMER_TMRMODE ));
    mode = ( val & ~( 0x0f << TIMER2_MODE_OFFSET )) | TIMER2_CONTINUOUS_MODE;
    __raw_writel( mode, IO_ADDRESS( REG_TIMER_TMRMODE ));
    timer_start( 2 );
    setup_sched_clock( update_sched_clock, 32, CLOCK_TICK_RATE );
    if(clocksource_register_hz( &timer_clocksource, CLOCK_TICK_RATE ))
    {
        panic("%s: can't register clocksource\n", timer_clocksource.name);
    }

    return 0;
}

先来看timer_clockevent_init,clockevents_calc_mult_shift计算针对该timer工作频率CLOCK_TICK_RATE,clockevent将ns时间转换为计数cycles所需的mult和shift,

该mult shift用于在设置下次timer intr时间点时,将ns时间转换为计数cycles,从而调用set_next_event写入timer的计数寄存器中。

timer_clockevent_init的主体函数是clockevents_register_device,

void clockevents_register_device(struct clock_event_device *dev)
{
    unsigned long flags;

    BUG_ON(dev->mode != CLOCK_EVT_MODE_UNUSED);
    if (!dev->cpumask) {
        WARN_ON(num_possible_cpus() > 1);
        dev->cpumask = cpumask_of(smp_processor_id());
    }

    raw_spin_lock_irqsave(&clockevents_lock, flags);

    list_add(&dev->list, &clockevent_devices);
    clockevents_do_notify(CLOCK_EVT_NOTIFY_ADD, dev);
    clockevents_notify_released();

    raw_spin_unlock_irqrestore(&clockevents_lock, flags);
}

调用clockevents_do_notify发通知,根据之前tick_init,CLOCK_EVT_NOTIFY_ADD通知类型最终会调用tick_check_new_device:

/*
 * Check, if the new registered device should be used.
 */
static int tick_check_new_device(struct clock_event_device *newdev)
{
    struct clock_event_device *curdev;
    struct tick_device *td;
    int cpu, ret = NOTIFY_OK;
    unsigned long flags;

    raw_spin_lock_irqsave(&tick_device_lock, flags);

    cpu = smp_processor_id();
    if (!cpumask_test_cpu(cpu, newdev->cpumask))
        goto out_bc;

    //不同的CPU可以使用不同的clock event device,每一个CPU都有一个tick_device的结构来表示当前CPU使用的clock event device对象。
    //tick_check_new_device的作用是通知当前CPU现在有个新的clock event device对象可以用了
    //单核处理器这里tick_cpu_device还是空的,curdev是NULL。
    td = &per_cpu(tick_cpu_device, cpu);
    curdev = td->evtdev;

    /* cpu local device ? */
    if (!cpumask_equal(newdev->cpumask, cpumask_of(cpu))) {
        /*
         * If the cpu affinity of the device interrupt can not
         * be set, ignore it.
         */
        if (!irq_can_set_affinity(newdev->irq))
            goto out_bc;
        /*
         * If we have a cpu local device already, do not replace it
         * by a non cpu local device
         */
        if (curdev && cpumask_equal(curdev->cpumask, cpumask_of(cpu)))
            goto out_bc;
    }

    /*
     * 如果有一个clockevent,则检查其oneshot属性以及rating是否高于新clockevent
     * 是的话就不使用新clockevent
     */
    if (curdev) {
        if ((curdev->features & CLOCK_EVT_FEAT_ONESHOT) &&
            !(newdev->features & CLOCK_EVT_FEAT_ONESHOT))
            goto out_bc;

        if (curdev->rating >= newdev->rating)
            goto out_bc;
    }

    if (tick_is_broadcast_device(curdev)) {
        clockevents_shutdown(curdev);
        curdev = NULL;
    }
    //newdev满足要求且比curdev精度高,换掉curdev!
    //clockevent_exchange_device中设置curdev的mode为CLOCK_EVT_MODE_UNUSED
    //设置newdev mode为CLOCK_EVT_MODE_SHUTDOWN
    clockevents_exchange_device(curdev, newdev);
    tick_setup_device(td, newdev, cpu, cpumask_of(cpu));
    if (newdev->features & CLOCK_EVT_FEAT_ONESHOT)
        tick_oneshot_notify();

    raw_spin_unlock_irqrestore(&tick_device_lock, flags);
    return NOTIFY_STOP;

out_bc:
    /*
     * Can the new device be used as a broadcast device ?
     */
    if (tick_check_broadcast_device(newdev))
        ret = NOTIFY_STOP;

    raw_spin_unlock_irqrestore(&tick_device_lock, flags);

    return ret;
}

首先来看下tick_setup_device,如下:

/*
 * Setup the tick device
 */
static void tick_setup_device(struct tick_device *td,
                  struct clock_event_device *newdev, int cpu,
                  const struct cpumask *cpumask)
{
    ktime_t next_event;
    void (*handler)(struct clock_event_device *) = NULL;

    /*
     * td->evtdev是NULL
     */
    if (!td->evtdev) {
        /*
         * If no cpu took the do_timer update, assign it to
         * this cpu:
         */
        if (tick_do_timer_cpu == TICK_DO_TIMER_BOOT) {
            tick_do_timer_cpu = cpu;
            tick_next_period = ktime_get();
            tick_period = ktime_set(0, NSEC_PER_SEC / HZ);
        }

        /*
         * 第一次初始化tick_device,mode为periodic。
         */
        td->mode = TICKDEV_MODE_PERIODIC;
    } else {
        handler = td->evtdev->event_handler;
        next_event = td->evtdev->next_event;
        td->evtdev->event_handler = clockevents_handle_noop;
    }
    //替换为新clockevent
    td->evtdev = newdev;

    /*
     * When the device is not per cpu, pin the interrupt to the
     * current cpu:
     */
    if (!cpumask_equal(newdev->cpumask, cpumask))
        irq_set_affinity(newdev->irq, cpumask);

    if (tick_device_uses_broadcast(newdev, cpu))
        return;

    if (td->mode == TICKDEV_MODE_PERIODIC)
	//第一次初始化tick_device,mode为periodic。
        tick_setup_periodic(newdev, 0);
    else
        tick_setup_oneshot(newdev, handler, next_event);
}

这里需要说明下,

kernel下timer的中断模式支持2种:周期性和一次性,也就是periodic和oneshot。

对于固定tick(1/HZ)系统的timer使用periodic。但是对于tickless系统,则必须要使用oneshot 模式了,因为每次timer中断间隔不一样长。

这里我们的timer2种模式都支持(clockevent的属性.features = CLOCK_EVT_FEAT_PERIODIC | CLOCK_EVT_FEAT_ONESHOT)。但是tick_device第一次初始化默认mode为periodic,所以最终会调用tick_setup_periodic,如下:

void tick_setup_periodic(struct clock_event_device *dev, int broadcast)
{
    tick_set_periodic_handler(dev, broadcast);

    /* Broadcast setup ? */
    if (!tick_device_is_functional(dev))
        return;

    if ((dev->features & CLOCK_EVT_FEAT_PERIODIC) &&
        !tick_broadcast_oneshot_active()) {
        clockevents_set_mode(dev, CLOCK_EVT_MODE_PERIODIC);
    } else {
        unsigned long seq;
        ktime_t next;

        do {
            seq = read_seqbegin(&xtime_lock);
            next = tick_next_period;
        } while (read_seqretry(&xtime_lock, seq));

        clockevents_set_mode(dev, CLOCK_EVT_MODE_ONESHOT);

        for (;;) {
            if (!clockevents_program_event(dev, next, false))
                return;
            next = ktime_add(next, tick_period);
        }
    }
}

首先设置clockevnt->evt_handler。evt_handler成员是平台实现的timer中断处理函数中必须调用的处理函数,evt_handler会更新系统时间并设置下次timer中断时间。

tick_set_periodic_handler中设置为tick_handle_periodic。

之后tick_setup_periodic中设置timer的mode为CLOCK_EVT_MODE_PERIODIC。

来看这时的中断处理函数evt_handler,如下:

static void tick_periodic(int cpu)
{
    if (tick_do_timer_cpu == cpu) {
        write_seqlock(&xtime_lock);

        /* Keep track of the next tick event */
        tick_next_period = ktime_add(tick_next_period, tick_period);

        do_timer(1);
        write_sequnlock(&xtime_lock);
    }

    update_process_times(user_mode(get_irq_regs()));
    profile_tick(CPU_PROFILING);
}

void tick_handle_periodic(struct clock_event_device *dev)
{
    int cpu = smp_processor_id();
    ktime_t next;

    tick_periodic(cpu);

    if (dev->mode != CLOCK_EVT_MODE_ONESHOT)
        return;
    /*
     * Setup the next period for devices, which do not have
     * periodic mode:
     */
    next = ktime_add(dev->next_event, tick_period);
    for (;;) {
        if (!clockevents_program_event(dev, next, false))
            return;
        if (timekeeping_valid_for_hres())
            tick_periodic(cpu);
        next = ktime_add(next, tick_period);
    }
}

tick_handle_periodic主要调用do_timer来更新系统时间(后面还会讲时间如何更新),由于其是周期性的timer中断,所以只能用于固定tick系统,do_timer更新的是1/HZ秒。

到这里tick_setup_device结束,但是发现一个问题,

我们的系统是选的NOHZ,也就是tickless系统,不应该使用periodic mode的timer,而应该是oneshot mode的timer。

那么kernel在哪里进行mode的变换呢?

接着tick_check_new_device看,如果newdev支持CLOCK_EVT_FEAT_ONESHOT,调用tick_oneshot_notify,

void tick_oneshot_notify(void)
{
    struct tick_sched *ts = &__get_cpu_var(tick_cpu_sched);

    set_bit(0, &ts->check_clocks);
}

int tick_check_oneshot_change(int allow_nohz)
{
    struct tick_sched *ts = &__get_cpu_var(tick_cpu_sched);

    if (!test_and_clear_bit(0, &ts->check_clocks))
        return 0;

    if (ts->nohz_mode != NOHZ_MODE_INACTIVE)
        return 0;

    if (!timekeeping_valid_for_hres() || !tick_is_oneshot_available())
        return 0;

    if (!allow_nohz)
        return 1;

    tick_nohz_switch_to_nohz();
    return 0;
}

tick_check_oneshot_change是在hrtimer softirq中调用(讲软中断时会讲到),这里tick_oneshot_notify设置check_clocks的bit0。

等到clockevents_register_device结束后,我们会注册使能timer的中断,在timer中断处理函数evt_handler中会触发hrtimer softirq。

而在中断退出中会调用softirq处理函数,从而调用tick_check_oneshot_change !

tick_check_oneshot_change会检查check_clocks的bit0,如果置位,则会调用tick_nohz_switch_to_nohz,该函数完成timer mode到oneshot的切换以及中断处理函数evt_handler的切换。

关键代码是tick_switch_to_oneshot(tick_nohz_handler),

函数实现如下:

int tick_switch_to_oneshot(void (*handler)(struct clock_event_device *))
{
    struct tick_device *td = &__get_cpu_var(tick_cpu_device);
    struct clock_event_device *dev = td->evtdev;

    if (!dev || !(dev->features & CLOCK_EVT_FEAT_ONESHOT) ||
            !tick_device_is_functional(dev)) {

        printk(KERN_INFO "Clockevents: "
               "could not switch to one-shot mode:");
        if (!dev) {
            printk(" no tick device\n");
        } else {
            if (!tick_device_is_functional(dev))
                printk(" %s is not functional.\n", dev->name);
            else
                printk(" %s does not support one-shot mode.\n",
                       dev->name);
        }
        return -EINVAL;
    }

    td->mode = TICKDEV_MODE_ONESHOT;
    dev->event_handler = handler;
    clockevents_set_mode(dev, CLOCK_EVT_MODE_ONESHOT);
    tick_broadcast_switch_to_oneshot();
    return 0;
}

很清晰,修改tick-device的mode,修改当前clockevent的evt_handler为tick_nohz_handler,设置timer的mode为oneshot。

之后timer就采用oneshot模式了!

到这里timer_clockevent_init就分析完了。根据平台代码timer_init,之后timer_init_irq使能timer中断。

根据上面分析,在平台clockevent注册时,第一次初始化tick_device,我们的timer默认使用periodic mode,evt_handler是tick_handle_periodic,系统还是固定tick的(1/HZ秒)

但是在timer中断使能,第一次中断处理完成后timer就切换成了oneshot mode,evt_handler替换为tick_nohz_handler,kernel正式开始了tickless !

再来简单分析下timer_clocksource_init,关键函数是clocksource_register_hz,定义如下:

static inline int clocksource_register_hz(struct clocksource *cs, u32 hz)
{
    return __clocksource_register_scale(cs, 1, hz);
}

int __clocksource_register_scale(struct clocksource *cs, u32 scale, u32 freq)
{

    /* Initialize mult/shift and max_idle_ns */
    __clocksource_updatefreq_scale(cs, scale, freq);

    /* Add clocksource to the clcoksource list */
    mutex_lock(&clocksource_mutex);
    clocksource_enqueue(cs);
    clocksource_enqueue_watchdog(cs);
    clocksource_select();
    mutex_unlock(&clocksource_mutex);
    return 0;
}

首先根据timer的工作频率计算计数cycles到ns时间的转换关系:mult和shift。

需要注意的是:

clockevent主要应用来设置下次timer中断时间,需要根据kernel计算的时间,换算为timer的计数cycles来配置计数寄存器。所以clockevent注册时mult shift代表的是ns到cycles的转换关系。

而clocksource应用在系统时间更新时根据timer的计数cycles换算为ns时间,所以clocksource注册时计算的mult shift代表cycles到ns的转换关系。

最后调用clocksource_select,在clocksource_list上跟之前注册的clocksource对比(我们这里只有default clocksource)选择精度更高的clocksource为curr_clocksource。

到这里平台代码timer_init结束,time_init结束,kernel时钟系统的初始化就结束了。

针对kernel时钟系统初始化我有3个地方的思考:

(1)根据上面分析,clocksource在kernel下默认是有default,也就是jiffies,从default_clocksource获取的cycle就是jiffies值,精度只有1。而clockevent没有default,所以在编写timer driver时clockevent实现是必须的,我感觉clocksource倒是可选的,如果没有定义,使用default clocksource,jiffies在timer中断中更新,倒是也可以用。

(2)为什么要使用外部timer提供的clocksource,是为了提高精度,default clocksource精度只有1/100 s(HZ在kernel下默认配置为100,固定tick系统timer中断间隔就是1/HZ,在timer中断中对jiffies +1),而使用外部timer,以我们的timer为例,工作频率为24MHZ,则1 cycle = 41.6 ns,就可以完全达到纳秒级的计时。

(3)上面分析是针对arm处理器,想起之前调试的mips以及ppc处理器,却没有注意到timer相关平台代码。翻看之前的代码发现,mips ppc处理器是将timer集成在了核内(mips将timer放在了cp0协处理器上),并实现专门指令来操作timer。这样timer代码就不是设备平台相关,而是直接写在了处理器相关代码中,我们需要提供的只是timer的工作频率(集成核内,与核频率相关)即可。如果timer工作频率提供不准,可以预见系统时间就会走的慢了或者快了。

mips ppc处理器这样做硬件上的好处是timer访问更快了,arm是外挂在APB上,效率肯定更低一点。

二 系统时钟的更新

系统时间更新是在timer中断中进行,平台代码中注册timer中断处理函数,该函数会调用clockevent->evt_handler。

根据初始化的分析,evt_handler在tickless系统下最终为tick_nohz_handler,该函数主要完成2个任务:

(1)do_timer更新系统时间

(2)计算下次中断时间,换算为cycle,调用clockevent->set_next_event设置计数寄存器

void do_timer(unsigned long ticks)
{
    jiffies_64 += ticks;
    update_wall_time();
    calc_global_load(ticks);
}
更新jiffies,调用update_wall_time更新时间,如下:
static void update_wall_time(void)
{
    struct clocksource *clock;
    cycle_t offset;
    int shift = 0, maxshift;
    unsigned long flags;

    write_seqlock_irqsave(&timekeeper.lock, flags);

    /* Make sure we're fully resumed: */
    if (unlikely(timekeeping_suspended))
        goto out;

    clock = timekeeper.clock;

#ifdef CONFIG_ARCH_USES_GETTIMEOFFSET
    offset = timekeeper.cycle_interval;
#else
    //获取当前cycle,减去最近更新时的cycle,offset就是距上次中断的时间间隔
    offset = (clock->read(clock) - clock->cycle_last) & clock->mask;
#endif
    /* Check if there's really nothing to do */
    if (offset < timekeeper.cycle_interval)
        goto out;

    timekeeper.xtime_nsec = (s64)timekeeper.xtime.tv_nsec <<
                        timekeeper.shift;

    shift = ilog2(offset) - ilog2(timekeeper.cycle_interval);
    shift = max(0, shift);
    /* Bound shift to one less than what overflows tick_length */
    maxshift = (64 - (ilog2(ntp_tick_length())+1)) - 1;
    shift = min(shift, maxshift);
    while (offset >= timekeeper.cycle_interval) {
        offset = logarithmic_accumulation(offset, shift);
        if(offset < timekeeper.cycle_interval<<shift)
            shift--;
    }

    /* 使用NTP校准offset*/
    timekeeping_adjust(offset);

    if (unlikely((s64)timekeeper.xtime_nsec < 0)) {
        s64 neg = -(s64)timekeeper.xtime_nsec;
        timekeeper.xtime_nsec = 0;
        timekeeper.ntp_error += neg << timekeeper.ntp_error_shift;
    }

    //将offset换算为ns时间
    timekeeper.xtime.tv_nsec = ((s64)timekeeper.xtime_nsec >>
                        timekeeper.shift) + 1;
    timekeeper.xtime_nsec -= (s64)timekeeper.xtime.tv_nsec <<
                        timekeeper.shift;
    timekeeper.ntp_error += timekeeper.xtime_nsec <<
                timekeeper.ntp_error_shift;

    if (unlikely(timekeeper.xtime.tv_nsec >= NSEC_PER_SEC)) {
        int leap;
        timekeeper.xtime.tv_nsec -= NSEC_PER_SEC;
        timekeeper.xtime.tv_sec++;
        leap = second_overflow(timekeeper.xtime.tv_sec);
        timekeeper.xtime.tv_sec += leap;
        timekeeper.wall_to_monotonic.tv_sec -= leap;
        if (leap)
            clock_was_set_delayed();
    }

    timekeeping_update(false);

out:
    write_sequnlock_irqrestore(&timekeeper.lock, flags);

}

很清楚,update_wall_time中使用timekeeper.clock->read(平台注册的clocksource)获取计数值,与上次计数值相减,换算为ns时间更新xtime。

xtime中存储的时间是纳秒级的!

三 系统时间的获取

用户空间获取系统时间的命令最常用的是date,看date实现源码(下个glibc库就可以看到)可以知道,最终是调用的gettimeofday,这是一个系统调用,软中断陷入内核后代码如下:

SYSCALL_DEFINE2(gettimeofday, struct timeval __user *, tv,
        struct timezone __user *, tz)
{
    if (likely(tv != NULL)) {
        struct timeval ktv;
        do_gettimeofday(&ktv);
        if (copy_to_user(tv, &ktv, sizeof(ktv)))
            return -EFAULT;
    }
    if (unlikely(tz != NULL)) {
        if (copy_to_user(tz, &sys_tz, sizeof(sys_tz)))
            return -EFAULT;
    }
    return 0;
}

调用do_gettimeofday,最终是调用getnstimeofday,如下:

void getnstimeofday(struct timespec *ts)
{
    unsigned long seq;
    s64 nsecs;

    WARN_ON(timekeeping_suspended);

    do {
        seq = read_seqbegin(&timekeeper.lock);

        *ts = timekeeper.xtime;
        nsecs = timekeeping_get_ns();

        /* If arch requires, add in gettimeoffset() */
        nsecs += arch_gettimeoffset();

    } while (read_seqretry(&timekeeper.lock, seq));

    timespec_add_ns(ts, nsecs);
}

首先直接获取xtime,之后调用timekeeping_get_ns,如下:

static inline s64 timekeeping_get_ns(void)
{
    cycle_t cycle_now, cycle_delta;
    struct clocksource *clock;

    /* read clocksource: */
    clock = timekeeper.clock;
    cycle_now = clock->read(clock);

    /* calculate the delta since the last update_wall_time: */
    cycle_delta = (cycle_now - clock->cycle_last) & clock->mask;

    /* return delta convert to nanoseconds using ntp adjusted mult. */
    return clocksource_cyc2ns(cycle_delta, timekeeper.mult,
                  timekeeper.shift);
}

获取当前timer的cycles,计算距最后更新时间的cycles,换算为ns。

最后调用timspec_add_ns,将获取的补充时间nsecs添加到xtime中,返回给用户空间,从而获取更加精确的纳秒级时间。

这样kernel时钟系统的前世今生算是小结完成,从初始化,到更新,到获取,有些地方写的有纰漏,作为初学者,还希望跟大家一起学习。

接下来有时间总结下kernel下软中断的实现,时钟和软中断如何结合创造出定时器机制,以及tasklet 工作队列的实现!

时间: 2024-11-14 13:03:42

linux kernel 时钟系统的前世今生的相关文章

linux 时钟源初步分析linux kernel 时钟框架详细介绍

初步概念: 看datasheet的关于时钟与定时器的部分, FCLK供给cpu, HCLK供给AHB总线设备(存储器控制器,中断控制器.LCD控制器.DMA.USB主机控制器等), PCLK供给APB总线上的设备(watchdog.IIS.i2c. pwm.定时器.ADC.uart.gpio.rtc.spi) 上电时 fclk的时钟等于外部时钟fin, 然后等待LOCKTIME后, 依照MPLLCON寄存器的设置,倍频到高频. UPLLCON专用于USB同于MPLLCON. 关于分频: CLKD

linux kernel软中断及其衍生品-定时器 tasklet的实现

软中断概念在嵌入式开发可以有两个不同的解释: 其一,软中断在处理器设计中是处理器异常之一,程序软件使用指定指令(如arm的SWI指令)引发该异常从而陷入内核态执行,最典型的软件应用就是系统调用. 其二,在kernel代码中实现了一套软中断机制,区别于硬件中断的硬件触发软件处理,而是软件触发软件处理. 今天来学习的是kernel下的软中断机制, 学习最常用的硬件中断,我们最关心的是中断触发(硬件)-中断分发-中断处理这个流程如何完成,对于软中断我们也需要搞明白这几点. 首先来看下kernel中跟软

Linux硬件时钟和系统时钟设置

Linux时钟分为系统时钟(System Clock)和硬件时钟(Real Time Clock,简称RTC).系统时钟是指当前Linux Kernel中的时钟:而硬件时钟则是主板上由电池供电的时钟,硬件时钟可以在BIOS中进行设置.当Linux启动时,系统时钟会去读取硬件时钟的设置,然后系统时钟就会独立于硬件时钟运作. Linux关于时间的设置的shell命令有date和hwclock两种: 嵌入式s3c6410 ARM开发板中Linux时间设置: 1. date  -- 用来读取或设置系统时

Linux Kernel 2.6.28 以上有BUG,系统运行第208.5天down机

简介: 业务服务器有一台服务器出现意外down机,服务器ping 不通.无法登陆,本想通过公司KVM系统登陆系统重启解决,登陆KVM后发现系统屏幕打印大量的内核错误,KVM无法使用.无法发送重启服务器.果断联系 机房人员帮助手工重启,手动重启服务器后服务器运行状态回归正常. 在服务器重启前通过KVM管理系统能看到系统屏幕上打印的内核错误,此时做了一个页面截屏,后根据屏幕打印的报错关键字,进行查找追踪发现原来我们的linux服务器内核bug问题导致. 此内核BUG问题会直接导致服务器down机,查

嵌入式Linux裸机开发(六)——S5PV210时钟系统

嵌入式Linux裸机开发(六)--S5PV210时钟系统 一.时钟系统简介 外设工作需要一定频率的时钟,这些时钟都由系统时钟提供.系统时钟一般由外部低频24MHZ晶体振荡器通过锁相环电路PLL倍频产生.通过外部的低频晶体振荡器产生系统时钟不仅可以减少干扰还可以降低成本.外设的工作频率越高,功耗越高,越不稳定.通过关闭外设的时钟可以关闭外设. 二.时钟域 S5PV210 中包含 3 大类时钟 domain, 分别是主系统时钟 domain (简称 MSYS).显示相关的时钟 domain (DSY

使用 linux kernel +busybox 定制linux系统

目的: 了解linux的启动过程 主要内容: 1.grub 是启动程序的bootloader 2.linux-kernel 是linux的开源内核 3.busybox 是linux的工具集合 启动顺序: grub-> bzimage > initrd > init > chroot sbin/init (从内存镜像转换成rootfs)>/etc/inittab > fstab>etc/init.d/rcS 实验环境: 操作系统(编译使用): CentOS 7.4K

Linux Kernel - Debug Guide (Linux内核调试指南 )

http://blog.csdn.net/blizmax6/article/details/6747601 linux内核调试指南 一些前言 作者前言 知识从哪里来 为什么撰写本文档 为什么需要汇编级调试 ***第一部分:基础知识*** 总纲:内核世界的陷阱 源码阅读的陷阱 代码调试的陷阱 原理理解的陷阱 建立调试环境 发行版的选择和安装 安装交叉编译工具 bin工具集的使用 qemu的使用 initrd.img的原理与制作 x86虚拟调试环境的建立 arm虚拟调试环境的建立 arm开发板调试环

CentOS启动流程、Grub legacy配置、linux kernel模块管理、伪文件系统介绍

写在前面: 博客书写牢记5W1H法则:What,Why,When,Where,Who,How. 本篇主要内容: ● 启动相关基础概念汇总 ● 启动流程 ● init程序类型     /etc/rc.d/rc     chkconfig     /etc/rc.d/rc.sysinit ● GRUB legacy     命令行接口     配置文件 ● Linux Kernel     内核模块查看与管理         lsmod         modinfo         modprob

linux 学习笔记-系统的初始化和服务-详细版

我们运行程序只需要点击应用程序的图标就可以了,但在这之前,我们必须启动我们的系统.在一切之前,我们必须有某些程序去引导我们系统的内核,这些程序就是内核引导程序了,例如LILO.GRUB.U-Boot.RedBoot.而这些引导程序同样需要被其他程序加载和运行,这样说下去,何处才是尽头啊?想必大家可以想到的----硬件!这么长的过程复杂.崎岖!正所谓万事开头难,但不怕,我们来一起走过去吧! CPU自身初始化-->POST加电自检-->BIOS(Boot Sequence)-->加载对应引导