linux-sides-Timers and time management in the Linux kernel. Part 3.

这篇文章 Timers and time management in the Linux kernel. Part 3. 是出自 linux-insides一书中 Timers and time management 章节 The tick broadcast framework and dyntick

内核版本比对5.3-rc8 进行了相关调整, 增加相关备注

Linux内核中的定时器和时间管理.Part 3.

tick broadcast框架和dyntick

这是本章 的第三部分,他描述了定时器和时间管理的相关内容,前一部分我们讲到了 clocksource框架. 我们已经开始考虑这个框架,因为它与Linux内核提供的特殊计数器密切相关。在本章的第一部分 已经看到了其中的一个 - jiffies. 正如我在本章的第一部分已经写过的那样,我们将在Linux内核初始化期间逐步考虑与时间管理相关的内容。之前章节调用:

register_refined_jiffies(CLOCK_TICK_RATE);

这个函数定义在 kernel/time/jiffies.c 文件中, 并初始化变量 refined_jiffies 时钟源. 被setup_arch 调用, setup_arch定义在arch/x86/kernel/setup.c中,并执行特定于体系结构的(以x86_64 为例)初始化。 查看setup_arch的实现,您将注意到register_refined_jiffies的调用是setup_arch函数完成其工作之前的最后一步。

setup_arch执行结束后,已经配置了许多不同的x86_64特定的东西。 例如,一些早期interrupt处理程序已经能够处理中断, 为initrd保留的内存空间,DMI扫描,Linux内核日志缓冲区已经设置,这意味着printk函数能够工作,e820的解析,Linux内核已经知道可用内存和许多其他架构特定的东西(如果你感兴趣,您可以在本书的第二章章节中阅读有关setup_arch函数和Linux内核初始化过程的更多信息。

现在,setup_arch完成了它的工作,我们可以回到通用Linux内核代码。回想一下setup_arch函数是从start_kernel函数调用的,该函数在init/ main.c源代码文件中定义。所以,我们将回到这个功能。您可以看到在start_kernel函数内部的setup_arch函数之后有很多不同的函数被调用,但由于我们的章节专门讨论定时器和时间管理相关的部分,我们将跳过所有与此无关的代码话题。与Linux内核中的时间管理相关的第一个函数是:

tick_init();

start_kernel中。 tick_init函数在kernel/time/tick-common.c源代码文件中定义,做两件事:

  • 初始化tick broadcast框架相关的数据结构;
  • 初始化full tickless模式相关的数据结构。

我们在本书中没有看到与tick broadcast框架相关的任何内容,并且对Linux内核中的tickless模式一无所知。因此,这一部分的要点是研究这些概念并了解它们是什么。

The idle process

首先看下tick_init函数的实现. 正如之前说得这个函数定义在 kernel/time/tick-common.c 源码文件中,由以下两个函数组成:

void __init tick_init(void)
{
    tick_broadcast_init();
    tick_nohz_init();
}

正如您从段落标题中可以理解的那样,我们现在只对tick_broadcast_init函数感兴趣。 此函数在kernel/time/tick-broadcast.c源代码文件中定义并执行初始化 tick broadcast框架相关的数据结构。 在我们查看tick_broadcast_init函数的实现之前,我们将尝试了解这个函数的作用,我们需要了解tick broadcast框架。

CPU的要点是执行程序。但有时处理器可能在没有被任何程序使用时处于特殊状态。这种特殊状态称为 - idle。当处理器无法执行任何操作时,Linux内核将启动idle任务。我们已经在Linux内核初始化过程的最后部分看到了一些这方面的内容。当Linux内核将从init/main.c源代码文件中完成start_kernel函数中的所有初始化过程,它将从相同的源代码文件中调用rest_init函数。这个函数的要点是启动内核init线程和kthreadd线程,调用schedule_preempt_disabled, 调用schedule函数启动任务调度,并通过调用cpu_startup_entry函数进入睡眠状态。cpu_startup_entry函数定义在kernel/sched/idle.c 源码文件中。

rest_init代码有序有所调整,函数结构有调整

cpu_startup_entry函数表示无限循环,它检查每次迭代重新调度的需要。 在调度程序找到要执行的内容之后,idle进程将完成其工作,并且控制将通过调用schedule_preempt_disabled函数移动到新的可运行任务:

void cpu_startup_entry(enum cpuhp_state state)
{
    arch_cpu_idle_prepare();
    cpuhp_online_idle(state);
    while (1)
        do_idle();
}

static void do_idle(void)
{
    int cpu = smp_processor_id();

    while (!need_resched()) {
        ...
        ...
        if (cpu_idle_force_poll ||\
         tick_check_broadcast_expired()) {
            tick_nohz_idle_restart_tick();
            cpu_idle_poll();
        } else {
            cpuidle_idle_call();
        }
    }
    ...
    ...
    schedule_idle();
}

schedule_idle()类似于schedule_preempt_disable(),它不会启用抢占,因为它不会调用sched_submit_work

当然,我们不会考虑在这部分中完全实现do_idle函数和idle状态的细节,因为它与我们的主题无关。 但对我们来说有一个关心的时刻。 我们知道处理器一次只能执行一个任务。 如果处理器在cpu_startup_entry中执行无限循环,Linux内核如何决定重新调度并停止idle进程? 答案是系统计时器中断。 当发生中断时,处理器停止idle线程并将控制转移到中断处理程序。 处理完系统定时器中断处理程序后,need_resched将返回true,Linux内核将停止idle进程,并将控制转移到当前的runnable任务。 但是系统定时器中断的处理对于电源管理无效,因为如果处理器处于idle状态,那么发送它系统定时中断就没什么意义了。

默认情况下,有一个CONFIG_HZ_PERIODIC内核配置选项在Linux内核中启用,并告诉处理系统定时器的每个中断。 为了解决这个问题,Linux内核提供了另外两种管理调度时钟中断的方法:

第一种是省略空闲处理器上的调度时钟滴答。要在Linux内核中启用此行为,我们需要启用CONFIG_NO_HZ_IDLE内核配置选项。此选项允许Linux内核避免向空闲处理器发送定时器中断。在这种情况下,周期性定时器中断将被替换为按需中断。这种模式称为 - dyntick-idle模式。但是如果内核不处理系统定时器的中断,内核如何判断系统是否无事可做?

每当选择空闲任务运行时,通过调用kernel/time/tick-sched.c中定义的tick_nohz_idle_enter函数来禁用周期性滴答源代码文件,并通过调用tick_nohz_idle_exit函数启用。 Linux内核中有一个特殊的概念,称为clock event devices,用于安排下一个中断。这个概念为设备提供API,这些设备可以在将来的特定时间提供中断,并由Linux内核中的clock_event_device结构表示。我们现在不会深入研究clock_event_device结构的实现。我们将在本章的下一部分中看到它,这里仅是一个开胃菜。

第二种方法是省略处于idle状态或只有一个可运行任务的处理器上的调度时钟节拍,或者说繁忙的处理器。 我们可以使用CONFIG_NO_HZ_FULL内核配置选项启用此功能,它可以显着减少定时器中断的数量。

除了do_idle之外,空闲处理器可以处于休眠状态。 Linux内核提供了特殊的cpuidle框架。 该框架的要点是将空闲处理器置于休眠状态。 这些状态集的名称是 - C状态。 但是如果本地定时器被禁用,处理器将如何被唤醒? linux内核为此提供了tick broadcast框架。 该框架的要点是分配一个不受C状态影响的计时器。 此计时器将唤醒睡眠处理器。

现在,经过一些理论我们可以回到我们的函数的实现。 让我们回想一下tick_init函数只调用以下两个函数:

void __init tick_init(void)
{
    tick_broadcast_init();
    tick_nohz_init();
}

让我们分析下第一个函数 tick_broadcast_init 定义在kernel/time/tick-broadcast.c 源码文件中,初始化tick broadcast 框架相关数据结构的初始化,看下函数tick_broadcast_init 的实现:

void __init tick_broadcast_init(void)
{
        zalloc_cpumask_var(&tick_broadcast_mask, GFP_NOWAIT);
        zalloc_cpumask_var(&tick_broadcast_on, GFP_NOWAIT);
        zalloc_cpumask_var(&tmpmask, GFP_NOWAIT);
#ifdef CONFIG_TICK_ONESHOT
         zalloc_cpumask_var(&tick_broadcast_oneshot_mask, GFP_NOWAIT);
         zalloc_cpumask_var(&tick_broadcast_pending_mask, GFP_NOWAIT);
         zalloc_cpumask_var(&tick_broadcast_force_mask, GFP_NOWAIT);
#endif
}

我们可以看到,tick_broadcast_init函数在zalloc_cpumask_var的帮助下分配不同的cpumasks。 功能 zalloc_cpumask_var函数在lib/cpumask.c源代码文件中定义,并扩展为以下函数的调用:

bool zalloc_cpumask_var(cpumask_var_t *mask, gfp_t flags)
{
        return alloc_cpumask_var(mask, flags | __GFP_ZERO);
}

最终,在kmalloc_node函数的帮助下,将使用特定标志为给定的cpumask分配内存空间:

*mask = kmalloc_node(cpumask_size(), flags, node);

现在让我们看看将在tick_broadcast_init函数中初始化的cpumasks。我们可以看到,tick_broadcast_init函数将初始化六个cpumasks,而且,最后三个cpumasks的初始化将依赖于CONFIG_TICK_ONESHOT内核配置选项。

前三个cpumasks是:

  • tick_broadcast_mask - 表示处于休眠模式的处理器列表的位图;
  • tick_broadcast_on - 存储处于周期性广播状态的处理器数量的位图;
  • tmpmask - 这个用于临时使用的位图。

我们已经知道,接下来的三个cpumasks取决于CONFIG_TICK_ONESHOT内核配置选项。实际上每个时钟事件设备可以采用以下两种模式之一:

  • periodic - 支持周期性事件的时钟事件设备;
  • oneshot - 能够发出仅发生一次的事件的时钟事件设备。

linux内核为include/linux/clockchips.h头文件中的这些时钟事件设备定义了两个掩码:

#define CLOCK_EVT_FEAT_PERIODIC 0x000001
#define CLOCK_EVT_FEAT_ONESHOT 0x000002

所以,最后三个cpumasks是:

  • tick_broadcast_oneshot_mask - 存储必须通知的处理器数量;
  • tick_broadcast_pending_mask - 存储待处理广播的处理器数量;
  • tick_broadcast_force_mask - 存储强制广播的处理器数量。

我们在tick broadcast框架中初始化了六个cpumasks,现在我们可以继续实现这个框架了。

The tick broadcast framework

硬件可以提供一些时钟源设备。 当处理器休眠并且其本地定时器停止时,必须有额外的时钟源设备来处理唤醒处理器。 Linux内核使用这些特殊时钟源设备,可以在指定时间产生中断。 我们已经知道这些定时器在Linux内核中称为clock events设备。 除了clock events设备之外,系统中的每个处理器都有自己的本地定时器,该定时器被编程为在下一个延期任务时发出中断。 这些定时器也可以编程来执行定期工作,比如更新jiffies等。这些定时器由Linux内核中的tick_device结构表示。 这个结构在kernel/time/tick-sched.h头文件中定义并显示:

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

注意,tick_device结构包含两个字段。 第一个字段-evtdev表示指向include/linux/clockchips.h头文件中定义的clock_event_device结构的指针, 表示时钟事件设备的描述符。 clock event设备允许注册将来会发生的事件。 正如我已经写过的那样,我们不会在这部分中考虑clock_event_device结构和相关API,但会在下一部分中看到它。

tick_device结构的第二个字段代表tick_device的模式。 我们已经知道,模式可以是以下之一:

enum tick_device_mode {
        TICKDEV_MODE_PERIODIC,
        TICKDEV_MODE_ONESHOT,
};

系统中的每个clock events设备通过在Linux内核的初始化过程中调用clockevents_register_device函数或clockevents_config_and_register函数来注册自身。 在注册新的clock events设备期间,Linux内核调用kernel/time/tick-common.c源代码文件中定义的tick_check_new_device函数,并检查给定的clock events设备应该由Linux内核使用。 在所有检查之后,tick_check_new_device函数执行以下调用:

tick_install_broadcast_device(newdev);

如果给定的设备可以是广播设备,则检查给定的clock event设备是否可以广播设备并安装它的功能。 让我们看一下tick_install_broadcast_device函数的实现:

void tick_install_broadcast_device(struct clock_event_device *dev)
{
    struct clock_event_device *cur = tick_broadcast_device.evtdev;

    if (!tick_check_broadcast_device(cur, dev))
        return;

    if (!try_module_get(dev->owner))
        return;

    clockevents_exchange_device(cur, dev);

    if (cur)
        cur->event_handler = clockevents_handle_noop;

    tick_broadcast_device.evtdev = dev;

    if (!cpumask_empty(tick_broadcast_mask))
        tick_broadcast_start_periodic(dev);

    if (dev->features & CLOCK_EVT_FEAT_ONESHOT)
        tick_clock_notify();
}

首先从tick_broadcast_device得到了当前 clock event, tick_broadcast_device 定义在 kernel/time/tick-common.c文件中:

static struct tick_device tick_broadcast_device;

并表示跟踪处理器事件的外部时钟设备。获得当前时钟设备后的第一步是调用tick_check_broadcast_device函数,该函数检查给定的时钟事件设备是否可以用作广播设备。 tick_check_broadcast_device函数的要点是检查给定clock events设备的features字段的值。正如我们从该字段的名称中可以理解的那样,features字段包含时钟事件设备功能。 include/linux/clockchips.h头文件中定义的可用值,可以是CLOCK_EVT_FEAT_PERIODIC之一 - 表示支持周期性事件等的时钟事件设备。
因此,tick_check_broadcast_device函数检查具有CLOCK_EVT_FEAT_ONESHOTCLOCK_EVT_FEAT_DUMMY和其他标志的标志,如果给定的时钟事件设备具有这些功能之一,则返回false
换句话说,tick_check_broadcast_device函数比较给定的时钟事件设备和当前时钟事件设备的rating并返回最佳值。

tick_check_broadcast_device函数之后,我们可以看到try_module_get函数的调用,该函数检查时钟事件的模块所有者。我们需要这样做以确保给定的clock events设备被正确初始化。下一步是调用kernel/time/clockevents.c源码文件中定义的clockevents_exchange_device函数并将释放旧时钟事件设备并用虚拟处理程序替换以前的功能处理程序。

tick_install_broadcast_device函数的最后一步中,我们检查tick_broadcast_mask是否为空,并通过调用tick_broadcast_start_periodic函数以周期模式启动给定的clock events设备:

if (!cpumask_empty(tick_broadcast_mask))
    tick_broadcast_start_periodic(dev);

if (dev->features & CLOCK_EVT_FEAT_ONESHOT)
    tick_clock_notify();

tick_broadcast_mask传递给tick_device_uses_broadcast函数使用,该函数在注册clock events设备期间检查clock events 设备

int cpu = smp_processor_id();

int tick_device_uses_broadcast(struct clock_event_device *dev, int cpu)
{
    ...
    ...
    ...
    if (!tick_device_is_functional(dev)) {
        ...
        cpumask_set_cpu(cpu, tick_broadcast_mask);
        ...
    }
    ...
    ...
    ...
}

更多的关于 smp_processor_id 请参看第四部分 内核初始化进程章节.

tick_broadcast_start_periodic 函数检查 clock event 设备并且调用 tick_setup_periodic函数:

static void tick_broadcast_start_periodic(struct clock_event_device *bc)
{
    if (bc)
        tick_setup_periodic(bc, 1);
}

函数定义在 kernel/time/tick-common.c 源码文件中并且通过调用以下函数为给定的clock event设备设置广播处理程序:

tick_set_periodic_handler(dev, broadcast);

函数检查表示广播状态(onoff)的第二个参数,并设置广播处理程序取决于其值:

void tick_set_periodic_handler(struct clock_event_device *dev, int broadcast)
{
    if (!broadcast)
        dev->event_handler = tick_handle_periodic;
    else
        dev->event_handler = tick_handle_periodic_broadcast;
}

clock event设备发出中断时,将调用dev->event_handler。 例如,让我们看看high precision event timer的中断处理程序,它位于arch/x86/kernel/hpet.c源代码文件:

static irqreturn_t hpet_interrupt_handler(int irq, void *data)
{
    struct hpet_dev *dev = (struct hpet_dev *)data;
    struct clock_event_device *hevt = &dev->evt;

    if (!hevt->event_handler) {
        printk(KERN_INFO "Spurious HPET timer interrupt on HPET timer %d\n",
                dev->num);
        return IRQ_HANDLED;
    }

    hevt->event_handler(hevt);
    return IRQ_HANDLED;
}

hpet_interrupt_handler获取irq特定数据并检查clock event设备的事件处理程序。 回想一下,我们只是在tick_set_periodic_handler函数中设置。 因此tick_handler_periodic_broadcast函数最终将在高精度事件计时器中断处理程序中被调用。

tick_handler_periodic_broadcast 函数调用下面函数:

bc_local = tick_do_periodic_broadcast();

被唤醒的处理器数量被存储在临时的cpumask类型变量tmpmask中,传递给tick_do_broadcast函数:

cpumask_and(tmpmask, cpu_online_mask, tick_broadcast_mask);
return tick_do_broadcast(tmpmask);

tick_do_broadcast 调用广播时钟事件的函数broadcast,该函数发送终端 IPI 到处理器集合.最终调用时间处理函tick_device:

if (bc_local)
    td->evtdev->event_handler(td->evtdev);

它实际上代表处理器本地定时器的中断处理程序。 在此之后, 一个处理器将被唤醒。 这就是Linux内核中的tick broadcast框架。 我们已经忽略了这个框架的某些方面,例如重新编程clock event设备并使用oneshot定时器等进行广播。但Linux内核非常大,覆盖它的所有方面并不现实的。 感兴趣的话可以深入研究.

如果你还记得,我们已经通过调用tick_init函数启动了这一部分。 我们只考虑tick_broadcast_init函数和相关理论,但tick_init函数包含另一个函数调用,这个函数是 - tick_nohz_init。 我们来看看这个函数的实现。

dyntick相关数据结构的初始化

我们已经在这部分中看到了关于dyntick概念的一些信息, 我们知道这个概念允许内核在idle状态下禁用系统定时器中断。 tick_nohz_init函数初始化与此概念相关的不同数据结构。 此函数在kernel/time/tick-sched.c源代码文件中定义,并从检查tick_nohz_full_running变量的值,该变量表示的无滴答模式的idle状态,以及在处理器只有一个可运行任务期间禁用系统定时器中断的状态:

if (!tick_nohz_full_running) {
    return;
}

后续版本未调用tick_nohz_init_all
sched/isolation: Eliminate NO_HZ_FULL_ALL Commit 6f1982f ("sched/isolation: Handle the nohz_full= parameter") 原因是不能正常调用新的housekeeping

housekeeping_mask通过housekeeping_init申请一块内存,函数定义在/sched/isolation.h文件中:

sched/isolation: Move housekeeping related code to its own file housekeeping再后续版本中被独立出来,start_kernel中调用housekeeping_init 所以这些都放到启动处处理了

    if (!alloc_cpumask_var(&housekeeping_mask, GFP_KERNEL)) {
        WARN(1, "NO_HZ: Can't allocate not-full dynticks cpumask\n");
        cpumask_clear(tick_nohz_full_mask);
        tick_nohz_full_running = false;
        return;
    }

这个housekeeping_mask将存储housekeeping的处理器数量,换句话说我们至少需要一个处理器不会处于NO_HZ模式,因为它会进行计时等等。

之后我们检查结果特定于体系结构的arch_irq_work_has_interrupt函数。此函数检查为特定体系结构发送处理器间中断的能力。我们需要检查一下,因为在NO_HZ模式期间处理器的系统定时器将被禁用,因此必须至少有一个在线处理器可以发送处理器间中断以唤醒离线处理器。对于x86_64此函数在arch/x86/include/asm/irq_work.h头文件中定义,并从CPUID检查处理器是否有APIC


static inline bool arch_irq_work_has_interrupt(void) { return boot_cpu_has(X86_FEATURE_APIC); }

如果处理器没有APIC,则Linux内核打印警告消息,清除tick_nohz_full_mask cpumask,将系统中所有可能处理器的数量复制到housekeeping_mask并重置tick_nohz_full_running变量的值:

if (!arch_irq_work_has_interrupt()) {
    pr_warning("NO_HZ: Can't run full dynticks because arch doesn't "
           "support irq work self-IPIs\n");
    cpumask_clear(tick_nohz_full_mask);
    cpumask_copy(housekeeping_mask, cpu_possible_mask);
    tick_nohz_full_running = false;
    return;
}

在此步骤之后,我们通过调用smp_processor_id来获取当前处理器的编号,并在tick_nohz_full_mask中检查该处理器。 如果tick_nohz_full_mask包含给定的处理器,我们清除tick_nohz_full_mask中的相应位:

cpu = smp_processor_id();

if (cpumask_test_cpu(cpu, tick_nohz_full_mask)) {
    pr_warning("NO_HZ: Clearing %d from nohz_full range for timekeeping\n", cpu);
    cpumask_clear_cpu(cpu, tick_nohz_full_mask);
}

因为这个处理器将用于计时。 在这一步之后,我们将所有处理器放在cpu_possible_mask中而不是tick_nohz_full_mask中:

cpumask_andnot(housekeeping_mask,
           cpu_possible_mask, tick_nohz_full_mask);

在此操作之后,housekeeping_mask将包含系统的所有处理器,除了用于计时的处理器。 在tick_nohz_init_all函数的最后一步,我们将浏览tick_nohz_full_mask中定义的所有处理器,并为每个处理器调用以下函数:

for_each_cpu(cpu, tick_nohz_full_mask)
    context_tracking_cpu_set(cpu);

kernel/context_tracking.c源代码文件中定义的context_tracking_cpu_set函数和该函数的要点是设置 context_tracking.active percpu变量为true。 当某个处理器的active字段被设置为'true`时,Linux内核上下文跟踪子系统将忽略所有上下文切换 处理器。

就这样。 这是tick_nohz_init函数的结束。 在此之后,将初始化与NO_HZ相关的数据结构。 我们没有看到NO_HZ模式的API,但很快就会看到它。

结论

这是本章第三部分的结尾,描述了Linux内核中与计时器和计时器管理相关的内容。在前一部分中,我们熟悉Linux内核中的clocksource概念,它代表了在中断和独立于硬件特性的方式管理不同时钟源的框架。我们继续在本部分的时间管理上下文中查看Linux内核初始化过程,并熟悉了我们的两个新概念:tick broadcast框架和tick-less模式。第一个概念帮助Linux内核处理处于深度睡眠状态的处理器,第二个概念代表内核可以用来改善idle处理器的电源管理的模式。

在下一部分中,我们将继续深入研究Linux内核中与计时器管理相关的事情,并将为我们看到新概念 - timers

如果您有任何问题或建议,请随时在twitter 0xAX上ping我,给我发电子邮件email或者只创建问题

请注意,英语不是我的第一语言,我很抱歉给您带来不便。如果您发现任何错误,请将PR发送至[linux-insides](https://github.com/0xAX/linux-insides)。

Links

Be First to Comment

发表评论

电子邮件地址不会被公开。 必填项已用*标注