Linux之时间子系统(四): tick 层模块(periodic 和dynamic )

一、时间子系统的软件架构

 二、tick 层模块的文件

tick-common.c
tick-oneshot.c
tick-sched.c

tick-broadcast.c
tick-broadcast-hrtimer.c

这三个文件属于tick device layer。
tick-common.c文件是periodic tick模块,用于管理周期性tick事件。
tick-oneshot.c文件是for高精度timer的,用于管理高精度tick时间。
tick-sched.c是用于dynamic tick的。

tick-broadcast.c 和 tick-broadcast-hrtimer.c  是broadcast tick模块。

如果说每个.c文件是一个模块的话,我们可以首先简单描述tick device layer的各个模块。tick-common.c描述了tick device的一些通用操作,此外,该文件还包括了周期性tick的代码。想要让系统工作在tickless mode(更准确应该是Dynamic tick模块,也就是说根据系统的当前运行状况,动态的启停周期性tick)需要两个模块的支持,分别是tick-oneshot.c和tick-sched.c。tick-oneshot.c主要是提供和tick device的one shot mode相关的操作接口函数。从字面上看,tick-sched.c是和tick的调度相关,所谓tick的调度包括两个方面,一方面是在系统正常运行过程中,如何产生周期性的tick event,另一方面是在系统没有任务执行,进入idle状态的时候,如何停止周期性的tick,以及恢复的时候如何更新系统状态(例如:jiffies等)。tick-broadcast.c和tick-broadcast-hrtimer.c是和tick broadcast相关,本文不会涉及这部分的内容,会有专门的文档描述它

1、什么是tick

想要理解什么是tick device,什么是tickless kernel,首先当然要理解什么是tick?要理解什么是tick,首先要理解OS kernel是如何运作的。系统中有很多日常性的事情需要处理,例如:

---更新系统时间

---处理低精度timer

---处理正在运行进程的时间片信息

系统在处理这些事情的时候使用了轮询的方式,也就是说按照固定的频率去做这些操作。这时候就需要HW的协助,一般而言,硬件会有HW timer(称之system timer)可以周期性的trigger interrupt,让系统去处理上述的日常性事务。每次timer中断到来的时候,内核的各个模块就知道,一个固定的时间片已经过去。对于日常生活,tick这个概念是和钟表关联的:钟表会发出周期性的滴答的声音,这个声音被称为tick。CPU和OS kernel扩展了这个概念:周期性产生的timer中断事件被称为tick,而能够产生tick的设备就称为tick device。

如何选择tick的周期是需要在power comsuption、时间精度以及系统响应时间上进行平衡。我们考虑系统中基于tick的低精度timer模块,选择较高的tick频率会提高时间精度,例如对于,10ms的tick周期意味着低精度timer的时间精度就是10ms,设定3ms的低精度timer没有任何意义。为了提高时间精度,我们可以提高tick的频率,例如可以提升到1ms的tick,但是,这时更多的CPU的时间被花费在timer的中断处理,实际上,当系统不繁忙的时候,并不是每一个tick都是那么有意义,实际上大部分的tick到来的时候,OS kernel往往只是空转,实际上并有什么事情做,这对系统的power consumption是有害的。对于嵌入式设备,周期性的tick对power consumption危害更大,因为对于嵌入式设备,待机时间是一个很重要的指标,而周期性tick则意味着系统不可能真正的进入idle状态,而是会周期性的被wakeup,这些动作会吃掉电池的电量。同理,对于调度器而言亦然。如果设定10ms的tick,分配每个进程的时间片精度只是10ms,调度器计算每个进程占用CPU的时间也只能是以10ms为单位。为了提高进程时间片精度,我们可以提高tick的频率,例如可以提升到1ms的tick,但是,这时更多的CPU的时间被花费在进程上下文的切换上,但是,对应的好处是系统的响应时间会更短。

三、periodic tick 模块

1、数据结构

在内核中,使用struct tick_device来抽象系统中的tick设备,如下:

struct tick_device {
    struct clock_event_device *evtdev;
    enum tick_device_mode mode;
};

从上面的定义就可以看出:所谓tick device其实就是工作在某种模式下的clock event设备。工作模式体现在tick device的mode成员,evtdev指向了和该tick device关联的clock event设备。

tick device的工作模式定义如下:

enum tick_device_mode {
    TICKDEV_MODE_PERIODIC,
    TICKDEV_MODE_ONESHOT,
};

tick device可以工作在两种模式下,一种是周期性tick模式,另外一种是one shot模式。one shot模式主要和tickless系统以及高精度timer有关。

2、tick device的分类以及和CPU的关系

(1) local tick device。在单核系统中,传统的unix都是在tick驱动下进行任务调度、低精度timer触发等,在多核架构下,系统为每一个cpu建立了一个tick device,如下:

DEFINE_PER_CPU(struct tick_device, tick_cpu_device);

local tick device的clock event device应该具备下面的特点:

(a)该clock event device对应的HW timer必须是和该CPU core是有关联的的(也就是说,该hw timer的中断是可以送达到该CPU core的)。struct clock_event_device 有一个cpumask成员,它可以指示该clock event device为哪一个或者哪几个CPU core工作。如果采用ARM generic timer的硬件,其HW timer总是为一个CPU core服务的,我们称之为per cpu timer。

(b)该clock event device支持one shot模式,并且精度最高(rating最大)

(2)global tick device。具体定义如下:

int tick_do_timer_cpu __read_mostly = TICK_DO_TIMER_BOOT;

有些任务不适合在local tick device中处理,例如更新jiffies,更新系统的wall time,更新系统的平均负载(不是单一CPU core的负载),这些都是系统级别的任务,只需要在local tick device中选择一个作为global tick device就OK了。tick_do_timer_cpu指明哪一个cpu上的local tick作为global tick。

(3)broadcast tick device,定义如下:

static struct tick_device tick_broadcast_device;

我们会单独一份文档描述它,这里就不再描述了。

四、初始化tick device

1、注册一个新的clock event device的时候,tick device layer要做什么?

在clock_event的文章中,我们知道:底层的timer硬件驱动在初始化的时候会注册clock event device,在注册过程中就会调用tick_check_new_device函数来看看是否需要进行tick device的初始化,如果已经已经初始化OK的tick device是否有更换更高精度clock event device的需求。代码如下:

void tick_check_new_device(struct clock_event_device *newdev)
{
    struct clock_event_device *curdev;
    struct tick_device *td;
    int cpu;

    cpu = smp_processor_id();-----------------(1)
 

    td = &per_cpu(tick_cpu_device, cpu);---获取当前cpu的tick device
    curdev = td->evtdev; ---目前tick device正在使用的clock event device

    if (!tick_check_percpu(curdev, newdev, cpu))---------(2)
        goto out_bc;

    if (!tick_check_preferred(curdev, newdev))-----------(3)
        goto out_bc;

    if (!try_module_get(newdev->owner)) -----增加新设备的reference count
        return;


    if (tick_is_broadcast_device(curdev)) { -------------(4)
        clockevents_shutdown(curdev);
        curdev = NULL;
    }
    clockevents_exchange_device(curdev, newdev); ---通知clockevent layer
    tick_setup_device(td, newdev, cpu, cpumask_of(cpu)); --------(5)
    if (newdev->features & CLOCK_EVT_FEAT_ONESHOT) 
        tick_oneshot_notify();
    return;

out_bc: 
    tick_install_broadcast_device(newdev); ----其他文档中描述
}

(1)是否是为本CPU服务的clock event device?如果不是,那么不需要考虑per cpu tick device的初始化或者更换该cpu tick device的clock event device。当然,这是还是可以考虑用在broadcast tick device的。

(2)第二个关卡是per cpu的检查。如果检查不通过,那么说明这个新注册的clock event device和该CPU不来电,不能用于该cpu的local tick。如果注册的hw timer都是cpu local的(仅仅属于一个cpu,这时候该clock event device的cpumask只有一个bit被set),那么事情会比较简单。然而,事情往往没有那么简单,一个hw timer可以服务多个cpu。我们这里说HW timer服务于某个cpu其实最重要的是irq是否可以分发到指定的cpu上。我们可以看看tick_check_percpu的实现:

static bool tick_check_percpu(struct clock_event_device *curdev,
                  struct clock_event_device *newdev, int cpu)
{
    if (!cpumask_test_cpu(cpu, newdev->cpumask))---------(a)
        return false;
    if (cpumask_equal(newdev->cpumask, cpumask_of(cpu)))--------(b)
        return true;
    if (newdev->irq >= 0 && !irq_can_set_affinity(newdev->irq))-------(c)
        return false;
    if (curdev && cpumask_equal(curdev->cpumask, cpumask_of(cpu)))---(d)
        return false;
    return true;
}

(a)判断这个新注册的clock event device是否可以服务该CPU,如果它根本不鸟这个cpu那么不用浪费时间了。

(b)判断这个新注册的clock event device是否只服务该CPU。如果这个clock event device就是服务该cpu的,那么别想三想四了,这个clock event device就是你这个CPU的人了。

(c)如果能走到这里,说明该clock event device可以服务多个CPU,指定的cpu(作为参数传递进来)只是其中之一而已,这时候,可以通过设定irq affinity将该clock event device的irq定向到该cpu。当前,前提是可以进行irq affinity的设定,这里就是进行这样的检查。

(d)走到这里,说明该新注册的clock event device是可以进行irq affinity设定的。我们可以通过修改irq affinity让该hw timer服务于这个指定的CPU。恩,听起来有些麻烦,的确如此,如果当前CPU的tick device正在使用的clock event device就是special for当前CPU的(根本不鸟其他CPU),有如此专情的clock event device,夫复何求,果断拒绝新注册的设备。

(3)程序来到这里,说明tick_check_percpu返回true,CPU和该clock event device之间的已经是眉目传情了,不过是否可以入主,就看该cpu的原配是否有足够强大的能力(精度和特性)。tick_check_preferred代码如下:

static bool tick_check_preferred(struct clock_event_device *curdev,
                 struct clock_event_device *newdev)
{
   if (!(newdev->features & CLOCK_EVT_FEAT_ONESHOT)) {-----(a)
        if (curdev && (curdev->features & CLOCK_EVT_FEAT_ONESHOT))
            return false;
        if (tick_oneshot_mode_active())
            return false;
    }

    return !curdev ||
        newdev->rating > curdev->rating ||
           !cpumask_equal(curdev->cpumask, newdev->cpumask);------(b)
}

(a)首先进行one shot能力比拼。如果新的clock event device没有one shot能力而原配有,新欢失败。如果都没有one shot的能力,那么要看看当前系统是否启用了高精度timer或者tickless。本质上,如果clock event device没有oneshot功能,那么高精度timer或者tickless都是处于委曲求全的状态,如果这样,还是维持原配的委曲求全的状态,新欢失败

(b)如果current是NULL的话,事情变得非常简单,当然是新来的这个clock event device胜出了(这时候,后面的比较都没有意义了)。如果原配存在的话,那么可以看rating,如果新来的精度高,那也选择新来的clock event device。是否精度低就一定不选新的呢?也不是,新设备还是有机会力挽狂澜的:如果新来的是local timer,而原配是非local timer,这时候,也可以考虑选择新的,毕竟新来的clock event device是local timer,精度低一些也没有关系。

当tick_check_percpu返回true的时候有两种情况:一种是不管current是什么状态,新设备是CPU的local timer(只为这个cpu服务,即cpumask 和当前cpu相等)。另外一种情况是新设备不是CPU的local timer,当然原配也没有那么专一。

我们先看看第一种情况:如果cpumask_equal返回true,那么说明原配也是local timer,那么没有办法了,谁的rating高就选谁。如果cpumask_equal返回false,那么说明原配不是local timer,那么即便新来的rating低一些也还是优先选择local timer。

我们再看看第二种情况:新clock_event_device 虽然是个海王,但是可以设置中断亲和性,即可以给cpu 发生中断,同时本地的clock_event_device 不存在 或者 本地的clock_event_device 也是个海王,就返回true,进行下一轮的判断;

(4)OK,经过复杂的检查,我们终于决定要用这个新注册的clock event device来替代current了(当然,也有可能current根本不存在)。在进行替换之前,我们还有检查一下current是否是broadcast tick device,如果是的话,还不能将其退回clockevents layer,仅仅是设定其状态为shutdown。curdev = NULL这一句很重要,在clockevents_exchange_device函数中,如果curdev == NULL的话,old device将不会从全局链表中摘下,挂入clockevents_released链表。

(5)setup tick device,参考下一节描述。

2、如何Setup 一个 tick device?

所谓setup一个tick device就是对tick device心仪的clock event设备进行设置,并将该tick device的evtdev指向新注册的这个clock event 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;

    if (!td->evtdev) {
        if (tick_do_timer_cpu == TICK_DO_TIMER_BOOT) {-------(1)
            ……
        }

        td->mode = TICKDEV_MODE_PERIODIC;----------(2)
    } else {
        handler = td->evtdev->event_handler;
        next_event = td->evtdev->next_event;
        td->evtdev->event_handler = clockevents_handle_noop; -------(3)
    }

    td->evtdev = newdev; -----终于修成正果了,呵呵

    if (!cpumask_equal(newdev->cpumask, cpumask)) ---------------(4)
        irq_set_affinity(newdev->irq, cpumask);

    if (tick_device_uses_broadcast(newdev, cpu)) -------留给broadcast tick文档吧
        return;

    if (td->mode == TICKDEV_MODE_PERIODIC)
        tick_setup_periodic(newdev, 0); ----------------------(5)
    else
        tick_setup_oneshot(newdev, handler, next_event); -----其他文档描述
}

(1)在multi core的环境下,每一个CPU core都自己的tick device(可以称之local tick device),这些tick device中有一个被选择做global tick device,负责维护整个系统的jiffies。如果该tick device的是第一次设定,并且目前系统中没有global tick设备,那么可以考虑选择该tick设备作为global设备,进行系统时间和jiffies的更新。更细节的内容请参考timekeeping文档。

(2)在最初设定tick device的时候,缺省被设定为周期性的tick。当然,这仅仅是初始设定,实际上在满足一定的条件下,在适当的时间,tick device是可以切换到其他模式的,下面会具体描述。

(3)旧的clockevent设备就要退居二线了,将其handler修改为clockevents_handle_noop。

(4)如果不是local timer,那么还需要调用irq_set_affinity函数,将该clockevent的中断,定向到本CPU。

(5)tick_setup_periodic的代码如下(注:下面的代码分析中暂不考虑broadcast tick的情况):

void tick_setup_periodic(struct clock_event_device *dev, int broadcast)
{
    tick_set_periodic_handler(dev, broadcast); ----设定event handler为tick_handle_periodic

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

        do {
            seq = read_seqbegin(&jiffies_lock);
            next = tick_next_period; -----获取下一个周期性tick触发的时间
        } while (read_seqretry(&jiffies_lock, seq));

        clockevents_set_mode(dev, CLOCK_EVT_MODE_ONESHOT); ---模式设定

        for (;;) {
            if (!clockevents_program_event(dev, next, false)) ----program next clock event
                return;
            next = ktime_add(next, tick_period); ------计算下一个周期性tick触发的时间
        }
    }
}

(a)如果底层的clock event device支持periodic模式,那么直接调用clockevents_set_mode设定模式就OK了

(b)如果底层的clock event device不支持periodic模式,而tick device目前是周期性tick mode,那么要稍微复杂一些,需要用clock event device的one shot模式来实现周期性tick。

五、周期性tick的运作

1、从中断到clock event handler

一般而言,底层的clock event chip driver会注册中断,我们用ARM generic timer驱动为例,注册的代码如下:

err = request_percpu_irq(ppi, arch_timer_handler_phys, "arch_timer", arch_timer_evt);

……

具体的timer的中断handler如下:

static irqreturn_t arch_timer_handler_phys_mem(int irq, void *dev_id)
{

      ……
        evt->event_handler(evt);
   ……


}

也就是说,在timer interrupt handler中会调用clock event device的event handler,而在周期性tick的场景下,这个event handler被设定为tick_handle_periodic。

2、周期性tick的clock event handler的执行分析

由于每个cpu都有自己的tick device,因此,在每个cpu上,每个tick到了的时候,都会调用tick_handle_periodic函数进行周期性tick中要处理的task,具体如下:

/*
 * Event handler for periodic ticks
 */
void tick_handle_periodic(struct clock_event_device *dev)
{
        int cpu = smp_processor_id();
        ktime_t next = dev->next_event;

        tick_periodic(cpu); //周期性tick中要处理的内容

#if defined(CONFIG_HIGH_RES_TIMERS) || defined(CONFIG_NO_HZ_COMMON)
        /*
         * The cpu might have transitioned to HIGHRES or NOHZ mode via
         * update_process_times() -> run_local_timers() ->
         * hrtimer_run_queues().
         */
        if (dev->event_handler != tick_handle_periodic)
                return;
#endif

        if (!clockevent_state_oneshot(dev))
                return;
        for (;;) {
                /*
                 * Setup the next period for devices, which do not have
                 * periodic mode:
                 */
                next = ktime_add(next, tick_period); //计算下一个周期性tick触发的时间

                if (!clockevents_program_event(dev, next, false))//设定下一个clock event触发的时间
                        return;
                /*
                 * Have to be careful here. If we're in oneshot mode,
                 * before we call tick_periodic() in a loop, we need
                 * to be sure we're using a real hardware clocksource.
                 * Otherwise we could get trapped in an infinite
                 * loop, as the tick_periodic() increments jiffies,
                 * which then will increment time, possibly causing
                 * the loop to trigger again and again.
                 */
                if (timekeeping_valid_for_hres())
                        tick_periodic(cpu);
        }
}

如果该tick device所属的clock event device工作在one shot mode,那么还需要为产生周期性tick而进行一些额外处理。

2、周期性tick中要处理的内容

代码如下:

/*
 * Periodic tick
 */
static void tick_periodic(int cpu)
{
        if (tick_do_timer_cpu == cpu) { //global tick需要进行一些额外处理
                raw_spin_lock(&jiffies_lock);
                write_seqcount_begin(&jiffies_seq);

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

                do_timer(1); //更新jiffies,计算平均负载,更新墙上时间
                write_seqcount_end(&jiffies_seq);
                raw_spin_unlock(&jiffies_lock);
                update_wall_time();   //更新wall time
        }

        update_process_times(user_mode(get_irq_regs())); //更新和当前进程相关的内容
        profile_tick(CPU_PROFILING);// 和性能剖析相关
}

 update_process_times()的主要工作如下:
1、update_process_times()首先会判断是否已经切到了高精度模式,如果是直接执行步骤3操作;
2、内核启动前期,必然没有切到高精度模式。这个时候每个时钟tick便会检查是否可以切换,如果可以,则使用hrtimer_switch_to_hres()中尝试切换到高精度模式;否则在__hrtimer_run_queues中执行对应的定时器超时处理。
3、执行scheduler_tick()。

kernel/time/timer.c
    update_process_times()
        --> run_local_timers()
                /*
                * 判断是否切换到了高精度模式,如果切换到了高精度模式,不做任何处理
                * 如果没有切到高精度模式,判断是否具备切换到高精度模式条件
                * 如果具备条件,执行hrtimer_switch_to_hres()中尝试切换到高精度模式
                * 否则执行__hrtimer_run_queues()进行定时器超时查询处理
                */
            --> hrtimer_run_queues();
        --> scheduler_tick();    /* 检查是否需要进行任务切换 */

4切换到高精度模式:调度时钟(sched_timer)初始化及启动
        切换到高精度模式主要完成以下工作:
将clock event设备的event_handler改为hrtimer_interrupt()。高精度模式下,时钟到期后发生arch_timer中断,执行hrtimer_interrupt()完成到时时钟的处理,即执行超时处理函数。
将tick device的mode和clock eventdevice的state设置为oneshot。
初始化并启动高精度定时器sched_timer,周期为tick_period,超时处理函数为tick_sched_timer
linux内核切换到高精度模式后,tick通过高精度定时器sched_timer实现。

kernel/time/hrtimer.c
    hrtimer_run_queues(void)
            /* 如果已经是高精度模式,直接return */
        --> if (__hrtimer_hres_active(cpu_base))    return;
                /*
                 * 这里其实有个判断,如果当前满足条件,
                 * 执行hrtimer_switch_to_hres()尝试切换到高精度模式
                 */
            --> hrtimer_switch_to_hres()
                    /* 切换到高精度模式 */
                --> tick_init_highres()
                        /*
                         * 将clock event设备的event_handler改为hrtimer_interrupt;
                         * 将tick device的mode和clock eventdevice的state设置为oneshot
                         */
                    --> tick_switch_to_oneshot(hrtimer_interrupt);
                            /* 设置tick device mode为oneshot */
                        --> td->mode = TICKDEV_MODE_ONESHOT;
                            /*
                             * 设置高精度模式下clock event设备的event_handler
                             * 从之前的tick_handle_periodic切换为hrtimer_interrupt
                             */
                        --> dev->event_handler =hrtimer_interrupt;
                            /* 设置clock event设备的state为oneshot */
                        --> clockevents_switch_state(dev, CLOCK_EVT_STATE_ONESHOT);
                    /* 初始化并启动sched_timer */
                --> tick_setup_sched_timer()
                    --> hrtimer_init(&ts->sched_timer, CLOCK_MONOTONIC, HRTIMER_MODE_ABS_HARD);
                        /* sched_timer超时处理函数为tick_sched_timer */
                    --> ts->sched_timer.function = tick_sched_timer; 
                    --> hrtimer_set_expires(&ts->sched_timer, tick_init_jiffy_update());
                    --> hrtimer_forward(&ts->sched_timer, now, tick_period);
                    --> hrtimer_start_expires(&ts->sched_timer, HRTIMER_MODE_ABS_PINNED_HARD);
                    --> tick_nohz_activate(ts, NOHZ_MODE_HIGHRES);
                /*
                 * 当前不满足切换高精度模式条件,
                 * 执行__hrtimer_run_queues()进行定时器超时查询处理
                 */
            --> __hrtimer_run_queues()

 高精度模式下,tick处理:sched_timer超时处理​​​​​​​

当linux内核切换到高精度模式后,时钟节拍tick通过sched_timer高精度定时器实现,当定时器sched_timer超时后触发arch_timer中断,arch_timer中断处理中执行clock event设备的event_handler,此时为hrtimer_interrupt(),hrtimer_interrupt()完成到时定时器的处理,即执行超时处理函数tick_sched_timer()。

kernel/time/tick-sched.c
    tick_sched_timer()
        /* 增加jiffies计数、更新墙上时间 */
        --> tick_sched_do_timer(ts, now); 
        --> tick_sched_handle(ts, regs);
            /*
             * 此方法上面已经介绍过了,不同的是:这边检查到已经切换到高精度模式后,
             * 不在进行其他操作,直接执行scheduler_tick(),后面有简单逻辑介绍。
             */
            --> update_process_times(user_mode(regs));
        /* 重新推迟定时器,时间为tick_period,即下一个tick */
        --> hrtimer_forward(timer, now, tick_period);
        --> return HRTIMER_RESTART;
kernel/time/time.c
    update_process_times()
        --> run_local_timers();
            /* 因为已经切换到高精度模式,所以这里不做任何操作,直接return了 */
            --> hrtimer_run_queues() 
                { 
                    if (__hrtimer_hres_active(cpu_base))  return;
                } 
        /* 检查是否需要进行任务切换 */
        --> scheduler_tick();

 scheduler_tick

        linux内核在每个tick进行任务切换检查,检查标志TIF_NEED_RESCHED是否被设置,如被设置,则进行任务切换,详细参考linux任务调度。

kernel/sched/core.c
    scheduler_tick() { curr->sched_class->task_tick(rq, curr, 0); }
        /* 根据当前任务所属调度类执行对应task_tick方法 */
        --> task_tick_fair() / task_tick_rt() / task_tick_dl()

六、dynamic tick 模块

1、什么是tickless?

tickless本质上上是去掉那个烦恼的滴答声音。对于OS kernel而言,tickless也就是意味着没有那个固定周期的timer interrupt事件,可是,没有那个固定的tick,OS kernel如何运转呢?

①首先看看如何处理timer。各种驱动和内核模块(例如网络子系统的TCP模块)都有timer的需求,因此,时间子系统需要管理所有注册到系统的timer。对于有tick的系统,在每个tick中scan所有的timer是一个顺理成章的想法,如果检查到timer超期(或者即将超期)系统会调用该timer的callback函数。当然,由于要在每个tick到来的时候检查timer,因此效率非常重要,内核有一些有意思的设计,有兴趣的读者可以看看低精度timer的的scan过程。

没有tick怎么办?这时候需要找到所有timer中最近要超期的timer,将其时间值设定到实际的HW timer中就OK了,当然,这时候需要底层的HW timer支持one shot,也就是说,该timer的中断就来一次,在该timer的的中断处理中除了处理超期函数之外,还需要scan所有timer,找到最近要超期的timer,将其时间值设定到实际的HW timer中就OK了,然后不断的重复上面的过程就OK了。假设系统中注册了1200ns, 1870ns, 2980ns, 4500ns, 5000ns和6250ns的timer,在一个HZ=1000的系统上,timer的超期都是在规则的tick时间点上,对于tickless的系统,timer的中断不是均匀的,具体如下图所示:

timerline

②我们再来看看更新系统时间。对于有tick的系统,非常简单,在每个tick到来的时候调用update_wall_time来更新系统时间,当然,由于是周期性tick,这时候每次都是累加相同的时间。对于tickless的系统,我们可以选择在每个timer超期的中断中调用update_wall_time来更新系统时间,不过,这种设计不好,一方面,如果系统中的timer数目太多,那么update_wall_time调用太频繁,而实际上是否系统需要这么高精度的时间值呢?更重要的是:timer中断到来是不确定的,和系统中的timer设定相关,有的时间段timer中断比较频繁,获取的系统时间精度比较高,有的时间段,timer中断比较稀疏,那么获取的系统时间精度比较低。

有一个问题:目前最新内核中,对于tickless 时,是如何更新wall_time ?

③看调度器怎么适应tickless。我们知道,除非你是一个完全基于优先级的调度器,否则系统都会给进程分配一个时间片(time slice),当占用CPU的时间片配额使用完了,该进程会挂入队列,等待调度器分配下一个时间片,并调度运行。有tick当然比较简单,在该tick的timer中断中减去当前进程的时间片。没有tick总是比较麻烦,我能想到的方法是:假设我们给进程分配40ms的时间片,那么在调度该进程的时候需要设定一个40ms的timer,timer到期后,调度器选择另外一个进程,然后再次设定timer。当然,如果没有进程优先级的概念(或者说优先级仅仅体现在分配的时间片比较多的情况下),并且系统中处于runnable状态的进程较少,整体的运作还是OK的。如果有优先级概念怎么办?如果进程执行过程中被中断打断,切换到另外的进程怎么办?如果系统内的进程数目很多如何保证调度器的性能?算了,太复杂了,还是有tick比较好,因此实际中,linux kernel在有任务执行的时候还是会启动周期性的tick。当然,世界上没有绝对正确的设计,任何优雅的设计都是适用于一定的应用场景的。其实自然界的规律不也是这样吗?牛顿的定律也不是绝对的正确,仅仅适用于低速的场景,当物体运动的速度接近光速的时候,牛顿的经典力学定律都失效了。

2、内核中的tickless

本节我们主要来看看内核中的tickless的情况。传统的unix和旧的linux(2000年初之前的)都是有tick的(对于新的内核,配置CONFIG_HZ_PERIODIC的情况下也是有tick的),新的linux kernel中增加了tickless的选项:

---CONFIG_NO_HZ_IDLE

---CONFIG_NO_HZ_FULL

CONFIG_NO_HZ_IDLE是说在系统dile的时候是没有tick的,当然,在系统运行的时候还是有tick的,因此,我们也称之dynamic tick或者NO HZ mode。3.10版本之后,引入一个full tickless mode,听起来好象任何情况下都是没有tick的,不过实际上也没有那么强,除了CPU处于idle的时候可以停下tick,当CPU上有且只有一个进程运行的时候,也可以停下周期性tick,其他的情况下,例如有多个进程等待调度执行,都还是有tick的。这个配置实际上只是对High-performance computing (HPC)有意义的,因此不是本文的重点。

3、tick device概述

Tick device是能够提供连续的tick event的设备。目前linux kernel中有periodic tick和one-shot tick两种tick device。periodic tick可以按照固定的时间间隔产生tick event。one-shot tick则是设定后只能产生一次tick event,如果要连续产生tick event,那么需要每次都进行设定。

每一个cpu都有属于自己的tick device。定义为tick_cpu_device。每一个tick device都有自己的类型(periodic或者one-shot),每一个tick device其实就是一个clock event device(增加了表示mode的member),不同类型的tick device有不同的event handler。对于periodic类型的tick设备,其clock event device的event handler是tick_handle_periodic(没有配置高精度timer)或者hrtimer_interrupt(配置了高精度timer)。对于one-shot类型的tick设备,其clock event device的event handler是hrtimer_interrupt(配置了高精度timer)或者tick_nohz_handler(没有配置高精度timer)。

Tick Device模块负责管理系统中的所有的tick设备,在SMP环境下,每一个CPU都自己的tick device,这些tick device中有一个被选择做global tick device,该device负责维护整个系统的jiffies以及更新哪些基于jiffies进行的全系统统计信息。

4、kernel如何初始化tick device layer以及周期性tick的运作?

如果把tick device的逻辑当作一个故事,那么故事的开始来自clockevent device layer。每当底层有新的clockevent device加入到系统中的时候,会调用clockevents_register_device或者clockevents_config_and_register向通用clockevent layer注册一个新的clockevent设备,这时候,会调用tick_check_new_device通知tick device layer有新货到来。如果tick device和clockevent device你情我愿,那么就会调用tick_setup_device函数setup这个tick device了。一般而言,刚系统初始化的时候,所有cpu的tick device都没有匹配clock event device,因此,该cpu的local tick device也就是global tick device了。而且,如果tick device是新婚(匹配之前,tick device的clock event device等于NULL),那么tick device的模式将被设定为TICKDEV_MODE_PERIODIC,即便clock event有one shot能力,即便系统配置了NO HZ。好吧,反正无论如何都需要从周期性tick开始,那么看看如何进行周期性tick的初始化的。

tick_setup_periodic函数用来设定一个periodic tick device。当然,最重要的设定event handler,对于周期性tick device,其clock event device的handler被设定为tick_handle_periodic。光有handler也不行,还得kick off底层的硬件,让其周期性的产生clock event,这样才能推动系统的运作(这是通过调用clockevent device layer的接口函数完成的)。

最后,我们思考一个问题:系统启动过程中,什么时候开始有tick?多核系统,BSP首先启动,在其初始化过程中会调用time_init,这里会启动clocksource的初始化过程。这时候,周期性的tick就会开始了。在某个阶段,其他的processor会启动,然后会注册其自己的local timer,这样,各个cpu上的tick就都启动了

七、设置了高精度timer的情况下,dynamic tick如何运作?

1、软件层次

下面的这幅图是以tick device为核心,描述了该模块和其他时间子系统模块的交互过程(配置高精度timer和dynamic tick的情况):

dtick

上图中,红色边框的模块是per cpu的模块,所谓per cpu就是说每个cpu都会维护属于一个自己的对象。例如,对于tick device,每个CPU都会维护自己的tick device,不过,为了不让图片变得太大,上图只画了一个CPU的情况,其他CPU的动作是类似。为何clock event没有被涂上红色的边框呢?实际上clock event device并不是per cpu的,有些per cpu的local timer,也有global timer,如果硬件设计人员愿意的话,一个CPU可以有多个local timer,系统中所有的timer硬件被抽象成一个个的clock event device进行系统级别的管理,每个CPU并不会特别维护一个属于自己的clock event device。弱水三千,只取一瓢。每个CPU只会在众多clock event device中选取那个最适合自己的clock event device构建CPU local tick device。

tick device系统的驱动力来自中断子系统,当HW timer(tick device使用的那个)超期会触发中断,因此会调用hrtimer_interrupt来驱动高精度timer的运转(执行超期timer的call back函数)。而在hrtimer_interrupt中会扫描保存高精度timer的红黑树,找到下一个超期需要设定的时间,调用tick_program_event来设定下一次的超期事件,你知道的,这是我们的tick device工作在one shot mode,需要不断的set next expire time,才能驱动整个系统才会不断的向前。

传统的低精度timer是周期性tick驱动的,但是,目前tick 处于one shot mode,怎么办?只能是模拟了,Tick device layer需要设定一个周期性触发的高精度timer,在这个timer的超期函数中(tick_sched_timer)执行进行原来周期性tick的工作,例如触发TIMER_SOFTIRQ以便推动系统低精度timer的运作,更新timekeeping模块中的real clock。

2、如何切换到tickless

我们知道,开始tick device总是工作在周期性tick的mode,一切就像过去一样,无论何时,系统总是有那个周期性的tick到来。这个周期性的tick是由于硬件timer的中断推动,该HW Timer的中断会注册soft irq,因此,HW timer总会周期性的触发soft irq的执行,也就是run_timer_softirq函数。在该函数中会根据情况将hrtimer切换到高精度模式(hrtimer也有两种mode,一种高精度mode,一种是低精度mode,系统总是从低精度mode开始)。在系统切换到高精度timer mode的时候(hrtimer_switch_to_hres),由于高精度timer必须需要底层的tick device运行在one shot mode,因此,这时会调用tick_switch_to_oneshot函数将该CPU上的tick device的mode切换置one shot(Note:这时候event handler设定为hrtimer_interrupt)。同样的,底层的clock event device也会被设定为one shot mode。一旦进入one shot mode,那个周期性到来的timer中断就会消失了,从此系统只会根据系统中的hrtimer的设定情况来一次性的设定底层HW timer的触发。

3、如何产生周期性tick

虽然tick device以及底层的HW timer都工作在one shot mode,看起来系统的HW timer中断都是按需产生,多么美妙。但是,由于各种原因(此处省略3000字),在系统运行过程中,那个周期性的tick还需要保持,因此,在切换到one shot mode的同时,也会调用tick_setup_sched_timer函数创建一个sched timer(一个普通的hrtimer而已),该timer的特点就是每次超期后还会调用hrtimer_forward,不断的将自己挂回hrtimer的红黑树,于是乎,tick_sched_do_timer接口按照tick的周期不断的被调用,从而模拟了周期性的tick。

4、在idle的时候如何停掉tick

我们知道,各个cpu上的swapper进程(0号进程,又叫idle进程)最后都是会执行cpu_idle_loop函数,该函数在真正执行cpu idle指令之前会调用tick_nohz_idle_enter,在该函数中,sched timer会被停掉,因此,周期性的HW timer不会再来,这时候将cpu从idle中唤醒的只能是和实际上系统中的hrtimer中的那个最近的超期时间有关。

5、如何恢复tick

概念同上,当从idle中醒来,tick_nohz_idle_exit函数被调用,重建sched timer,一切恢复了原状。

6、没有设置高精度timer的情况下,dynamic tick如何运作?

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值