# linux-sides-Timers and time management in the Linux kernel. Part 3.
Date: 2019-09-13


这篇文章 [Timers and time management in the Linux kernel. Part 3.](https://0xax.gitbooks.io/linux-insides/Timers/linux-timers-3.html) 是出自 [linux-insides](https://0xax.gitbooks.io/linux-insides/content/)一书中 [Timers and time management](https://github.com/0xAX/linux-insides/tree/master/Timers) 章节 [The tick broadcast framework and dyntick](https://github.com/0xAX/linux-insides/blob/master/Timers/linux-timers-3.md)

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

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

## tick broadcast框架和dyntick

这是[本章](https://0xax.gitbooks.io/linux-insides/content/Timers/index.html) 的第三部分，他描述了定时器和时间管理的相关内容，[前一部分](https://0xax.gitbooks.io/linux-insides/content/Timers/linux-timers-2.html)我们讲到了 `clocksource`框架. 我们已经开始考虑这个框架，因为它与Linux内核提供的特殊计数器密切相关。在本章的[第一部分](https://0xax.gitbooks.io/linux-insides/content/Timers/linux-timers-1.html) 已经看到了其中的一个 - `jiffies`. 正如我在本章的第一部分已经写过的那样，我们将在Linux内核初始化期间逐步考虑与时间管理相关的内容。之前章节调用:

```C
register_refined_jiffies(CLOCK_TICK_RATE);
```

这个函数定义在 [kernel/time/jiffies.c](https://github.com/torvalds/linux/blob/master/kernel/time/jiffies.c) 文件中, 并初始化变量 `refined_jiffies` 时钟源. 被`setup_arch` 调用， `setup_arch`定义在[arch/x86/kernel/setup.c](https://github.com/torvalds/linux/blob/master/arch/x86/kernel/setup.c)中,并执行特定于体系结构的(以[x86\_64](https://en.wikipedia.org/wiki/X86-64) 为例)初始化。 查看`setup_arch`的实现，您将注意到`register_refined_jiffies`的调用是`setup_arch`函数完成其工作之前的最后一步。

在`setup_arch`执行结束后，已经配置了许多不同的`x86_64`特定的东西。 例如，一些早期[interrupt](https://en.wikipedia.org/wiki/Interrupt)处理程序已经能够处理中断, 为[initrd](https://en.wikipedia.org/wiki/Initrd)保留的内存空间，[DMI](https://en.wikipedia.org/wiki/Desktop_Management_Interface)扫描，Linux内核日志缓冲区已经设置，这意味着[printk](https://en.wikipedia.org/wiki/Printk)函数能够工作，[e820](https://en.wikipedia.org/wiki/E820)的解析，Linux内核已经知道可用内存和许多其他架构特定的东西(如果你感兴趣，您可以在本书的第二章[章节](https://0xax.gitbooks.io/linux-insides/content/Initialization/index.html)中阅读有关`setup_arch`函数和Linux内核初始化过程的更多信息。

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

```C
tick_init();
```

在`start_kernel`中。 `tick_init`函数在[kernel/time/tick-common.c](https://github.com/torvalds/linux/blob/master/kernel/time/tick-common.c)源代码文件中定义，做两件事：

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

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

## The idle process

首先看下`tick_init`函数的实现. 正如之前说得这个函数定义在 [kernel/time/tick-common.c](https://github.com/torvalds/linux/blob/master/kernel/time/tick-common.c) 源码文件中，由以下两个函数组成:

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

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

CPU的要点是执行程序。但有时处理器可能在没有被任何程序使用时处于特殊状态。这种特殊状态称为 - [idle](https://en.wikipedia.org/wiki/Idle_%28CPU%29)。当处理器无法执行任何操作时，Linux内核将启动`idle`任务。我们已经在[Linux内核初始化过程](https://0xax.gitbooks.io/linux-insides/content/Initialization/linux-initialization-10.html)的最后部分看到了一些这方面的内容。当Linux内核将从[init/main.c](https://github.com/torvalds/linux/blob/master/init/main.c)源代码文件中完成`start_kernel`函数中的所有初始化过程，它将从相同的源代码文件中调用`rest_init`函数。这个函数的要点是启动内核`init`线程和`kthreadd`线程，调用`schedule_preempt_disabled`, 调用`schedule`函数启动任务调度，并通过调用`cpu_startup_entry`函数进入睡眠状态。`cpu_startup_entry`函数定义在[kernel/sched/idle.c](https://github.com/torvalds/linux/blob/master/kernel/sched/idle.c) 源码文件中。

> _rest\_init代码有序有所调整,函数结构有调整_

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

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

```

```C
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任务。 但是系统定时器中断的处理对于[电源管理](https://en.wikipedia.org/wiki/Power_management)无效，因为如果处理器处于`idle`状态，那么发送它系统定时中断就没什么意义了。

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

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

每当选择空闲任务运行时，通过调用[kernel/time/tick-sched.c](https://github.com/torvalds/linux/blob/master/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`函数只调用以下两个函数：

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

让我们分析下第一个函数 `tick_broadcast_init` 定义在[kernel/time/tick-broadcast.c](https://github.com/torvalds/linux/blob/master/kernel/time/tick-broadcast.c) 源码文件中，初始化`tick broadcast` 框架相关数据结构的初始化,看下函数`tick_broadcast_init` 的实现:

```C
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](https://0xax.gitbooks.io/linux-insides/content/Concepts/linux-cpu-2.html)。 功能 `zalloc_cpumask_var`函数在[lib/cpumask.c](https://github.com/torvalds/linux/blob/master/lib/cpumask.c)源代码文件中定义，并扩展为以下函数的调用：

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

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

```C
*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](https://github.com/torvalds/linux/blob/master/include/linux/clockchips.h)头文件中的这些时钟事件设备定义了两个掩码：

```C
#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](https://github.com/torvalds/linux/blob/master/kernel/time/tick-sched.h)头文件中定义并显示：

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

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

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

```C
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](https://github.com/torvalds/linux/blob/master/kernel/time/tick-common.c)源代码文件中定义的`tick_check_new_device`函数,并检查给定的`clock events`设备应该由Linux内核使用。 在所有检查之后，`tick_check_new_device`函数执行以下调用：

```C
tick_install_broadcast_device(newdev);
```

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

```C
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](https://github.com/torvalds/linux/blob/master/kernel/time/tick-common.c)文件中:

```C
static struct tick_device tick_broadcast_device;
```

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

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

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

```C
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` 设备

```C
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` 请参看[第四部分](https://0xax.gitbooks.io/linux-insides/content/Initialization/linux-initialization-4.html) 内核初始化进程章节.

`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](https://github.com/torvalds/linux/blob/master/kernel/time/tick-common.c) 源码文件中并且通过调用以下函数为给定的`clock event`设备设置广播处理程序:

```C
tick_set_periodic_handler(dev, broadcast);
```

函数检查表示广播状态（`on`或`off`）的第二个参数，并设置广播处理程序取决于其值：

```C
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](https://en.wikipedia.org/wiki/High_Precision_Event_Timer)的中断处理程序，它位于[arch/x86/kernel/hpet.c](https://github.com/torvalds/linux/blob/master/arch/x86/kernel/hpet.c)源代码文件：

```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](https://en.wikipedia.org/wiki/Interrupt_request_%28PC_architecture%29)特定数据并检查`clock event`设备的事件处理程序。 回想一下，我们只是在`tick_set_periodic_handler`函数中设置。 因此`tick_handler_periodic_broadcast`函数最终将在高精度事件计时器中断处理程序中被调用。

`tick_handler_periodic_broadcast` 函数调用下面函数:

```C
bc_local = tick_do_periodic_broadcast();
```

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

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

`tick_do_broadcast` 调用广播时钟事件的函数`broadcast`,该函数发送终端 [IPI](https://en.wikipedia.org/wiki/Inter-processor_interrupt) 到处理器集合.最终调用时间处理函`tick_device`:

```C
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](https://github.com/torvalds/linux/blob/master/kernel/time/tick-sched.c)源代码文件中定义，并从检查`tick_nohz_full_running`变量的值，该变量表示的无滴答模式的`idle`状态，以及在处理器只有一个可运行任务期间禁用系统定时器中断的状态：

```C
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")](https://github.com/torvalds/linux/commit/a7c8655b073d89303911c89d0fd9fc4be7631fbe#diff-bb6ea9a0051c11f3e2275a763755d9b0) 原因是不能正常调用新的housekeeping_

`housekeeping_mask`通过`housekeeping_init`申请一块内存,函数定义在[/sched/isolation.h](https://github.com/torvalds/linux/blob/master/include/linux/sched/isolation.h)文件中:

> _[sched/isolation: Move housekeeping related code to its own file](https://github.com/torvalds/linux/commit/7863406143d8bbbbda07a61285c5f4c217908dfd#diff-bb6ea9a0051c11f3e2275a763755d9b0) housekeeping再后续版本中被独立出来，start\_kernel中调用housekeeping\_init 所以这些都放到启动处处理了_

```C
    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](https://en.wikipedia.org/wiki/X86-64)此函数在[arch/x86/include/asm/irq\_work.h](https://github.com/torvalds/linux/blob/master/arch/x86/include/asm/irq_work.h)头文件中定义,并从[CPUID](https://en.wikipedia.org/wiki/CPUID)检查处理器是否有[APIC](https://en.wikipedia.org/wiki/Advanced_Programmable_Interrupt_Controller)

```C
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`变量的值：

```C
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`中的相应位：

```C
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`中：

```C
cpumask_andnot(housekeeping_mask,
           cpu_possible_mask, tick_nohz_full_mask);
```

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

```C
for_each_cpu(cpu, tick_nohz_full_mask)
    context_tracking_cpu_set(cpu);
```

在[kernel/context\_tracking.c](https://github.com/torvalds/linux/blob/master/kernel/context_tracking.c)源代码文件中定义的`context_tracking_cpu_set`函数和该函数的要点是设置 `context_tracking.active` [percpu](https://0xax.gitbooks.io/linux-insides/content/Concepts/linux-cpu-1.html)变量为`true`。 当某个处理器的`active`字段被设置为'true\`时，Linux内核上下文跟踪子系统将忽略所有[上下文切换](https://en.wikipedia.org/wiki/Context_switch) 处理器。

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

## 结论

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

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

如果您有任何问题或建议，请随时在twitter [0xAX](https://twitter.com/0xAX)上ping我，给我发电子邮件[email](anotherworldofworld@gmail.com)或者只创建[问题](https://github.com/0xAX/linux-insides/issues/new)。

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

## Links

- [x86\_64](https://en.wikipedia.org/wiki/X86-64)
- [initrd](https://en.wikipedia.org/wiki/Initrd)
- [interrupt](https://en.wikipedia.org/wiki/Interrupt)
- [DMI](https://en.wikipedia.org/wiki/Desktop_Management_Interface)
- [printk](https://en.wikipedia.org/wiki/Printk)
- [CPU idle](https://en.wikipedia.org/wiki/Idle_%28CPU%29)
- [power management](https://en.wikipedia.org/wiki/Power_management)
- [NO\_HZ documentation](https://github.com/torvalds/linux/blob/16f73eb02d7e1765ccab3d2018e0bd98eb93d973/Documentation/timers/NO_HZ.txt)
- [cpumasks](https://0xax.gitbooks.io/linux-insides/content/Concepts/linux-cpu-2.html)
- [high precision event timer](https://en.wikipedia.org/wiki/High_Precision_Event_Timer)
- [irq](https://en.wikipedia.org/wiki/Interrupt_request_%28PC_architecture%29)
- [IPI](https://en.wikipedia.org/wiki/Inter-processor_interrupt)
- [CPUID](https://en.wikipedia.org/wiki/CPUID)
- [APIC](https://en.wikipedia.org/wiki/Advanced_Programmable_Interrupt_Controller)
- [percpu](https://0xax.gitbooks.io/linux-insides/content/Concepts/linux-cpu-1.html)
- [context switches](https://en.wikipedia.org/wiki/Context_switch)
- [Previous part](https://0xax.gitbooks.io/linux-insides/content/Timers/linux-timers-2.html)

