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

这篇文章 Timers and time management in the Linux kernel. Part 5. 是出自 linux-insides一书中 Timers and time management 章节 Introduction to timers

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

clockevents框架简介

这是本章节的第五部分,它描述了Linux内核中与计时器和时间管理相关的内容。正如您可能从本部分的标题中注意到的那样,将讨论clockevents框架。我们已经在本章的第二部分中看到了一个框架。这是clocksource框架。这两个框架都是Linux内核中的计时抽象。

首先,让我们回忆一下clocksource框架以及它的用途。 clocksource框架的主要目标是提供timeline。如documentation中所述:

例如,在Linux终端上输入命令date将最终读取时钟源以确定确切的时间。

Linux内核支持许多不同的时钟源。 您可以在drivers/clocksource中找到其中的一些。 例如,老一些的比如Intel 8253-可编程间隔计时器,频率为1193182, 还有一个-ACPI PM计时器,频率为3579545Hz。 除了drivers/clocksource目录之外,每种体系结构都可以提供自己的体系结构专用时钟源。 例如x86架构提供了高精度事件计时器,或者例如是powerpc通过timebase寄存器提供对处理器计时器的访问。

每个时钟源都提供单调原子计数器。 Linux内核支持大量不同的时钟源,每个时钟源都有自己的参数,例如frequencyclocksource框架的主要目标是提供API,以选择系统中最佳的可用时钟源,即频率最高的时钟源。 clocksource框架的另一个目标是由时钟源提供的以人类时间单位的表示的原子计数器。 在这个时候,纳秒是Linux内核中给定时钟源的时间值单位的首选。

clocksource框架被定义在include/linux/clocksource.h头文件中的clocksource结构中,其中包含时钟源的name,系统中某些时钟源的额定值(频率较高的时钟源在系统中具有最大的额定值),系统中所有已注册时钟源的listenabledisable字段来启用和禁用时钟源,指向read函数(必须返回时钟源的原子计数器)的指针等等。

另外,clocksource结构提供了两个字段:multshift,这是由某个时钟源提供给人类计时单位的原子计数器转换所必需的,即nanoseconds.通过以下公式进行转换:

ns ~= (clocksource * mult) >> shift

我们已经知道,除了clocksource结构之外,clocksource框架还提供了一个API,用于注册具有不同频率比例因子的时钟源:

static inline int clocksource_register_hz(struct clocksource *cs, u32 hz)
static inline int clocksource_register_khz(struct clocksource *cs, u32 khz)

时钟源注销:

int clocksource_unregister(struct clocksource *cs)

...

除了clocksource框架之外,Linux内核还提供了clockevents框架。 如文档中所述:

时钟事件与时钟源在概念上相反

主要目标是管理时钟事件设备,或者换句话说,即管理允许注册事件的设备,换句话说,就是要进行以下操作的设备:中断将在将在将来的指定时间发生。

现在,我们对Linux内核中的clockevents框架有了一些了解,现在是时候来看一下API.

clockevents框架相关API

描述时钟事件设备的主要结构是clock_event_device结构。 此结构在include/linux/clockchips.h头文件中定义,并且包含大量字段。 除了clocksource结构之外,它还具有name字段,其中包含时钟事件设备的可读名称,例如local APIC计时器:

static struct clock_event_device lapic_clockevent = {
    .name                   = "lapic",
    ...
    ...
    ...
}

某个时钟事件设备的event_handlerset_next_eventnext_event函数的地址是中断处理程序,下一个事件的设置程序和本地分别存储下一个事件。 clock_event_device结构的另一个字段是-features字段。 它的价值可能来自以下通用功能:

#define CLOCK_EVT_FEAT_PERIODIC 0x000001
#define CLOCK_EVT_FEAT_ONESHOT  0x000002

其中CLOCK_EVT_FEAT_PERIODIC表示可以被编程为定期生成事件的设备。 CLOCK_EVT_FEAT_ONESHOT代表只能产生一次事件的设备。 除了这两个功能,还有特定于体系结构的功能。 例如,x86_64支持两个附加功能:

#define CLOCK_EVT_FEAT_C3STOP 0x000008

The first CLOCK_EVT_FEAT_C3STOP means that a clock event device will be stopped in the C3 state. Additionally the clock_event_device structure has mult and shift fields as well as clocksource structure. The clocksource structure also contains other fields, but we will consider it later.

第一个CLOCK_EVT_FEAT_C3STOP表示时钟事件设备将在C3状态下停止。 另外,clock_event_device结构具有multshift字段以及clocksource结构。 clocksource结构还包含其他字段,但我们稍后会考虑。

在考虑了clock_event_device结构的一部分之后,我们来看看clockevents框架的API。 要使用时钟事件设备,首先我们需要初始化clock_event_device结构并注册一个时钟事件设备。 clockevents框架提供以下API来注册时钟事件设备

void clockevents_register_device(struct clock_event_device *dev)
{
   ...
   ...
   ...
}

kernel/time/clockevents.c源代码文件中定义的函数,正如我们所看到的, clockevents_register_device函数仅采用一个参数:

*代表时钟事件设备的clock_event_device结构的地址。

因此,要注册一个时钟事件设备,首先我们需要使用某个时钟事件设备的参数来初始化clock_event_device结构。 让我们看一下Linux内核源代码中的一个随机时钟事件设备。 我们可以在drivers/clocksource目录中找到一个,或尝试查看特定于体系结构的时钟事件设备。 让我们举个例子-at91sam926x 的定期间隔计时器(PIT)

原书给的链接已失效,这里修改链接,文档中19章节有关于PIT描述 P155

您可以在drivers/clocksource中找到其实现。

首先,让我们看一下clock_event_device结构的初始化。 这发生在at91sam926x_pit_common_init函数中:

struct pit_data {
    ...
    ...
    struct clock_event_device       clkevt;
    ...
    ...
};

static int __init at91sam926x_pit_dt_init(struct device_node *node)
{
    ...
    ...
    ...
    struct pit_data *data;
    data->clksrc.mask = CLOCKSOURCE_MASK(bits);
    data->clksrc.name = "pit";
    data->clksrc.rating = 175;
    data->clksrc.read = read_pit_clk;
    data->clksrc.flags = CLOCK_SOURCE_IS_CONTINUOUS;

    data->clkevt.name = "pit";
    data->clkevt.features = CLOCK_EVT_FEAT_PERIODIC;
    data->clkevt.shift = 32;
    data->clkevt.mult = div_sc(pit_rate, NSEC_PER_SEC, data->clkevt.shift);
    data->clkevt.rating = 100;
    data->clkevt.cpumask = cpumask_of(0);

    data->clkevt.set_state_shutdown = pit_clkevt_shutdown;
    data->clkevt.set_state_periodic = pit_clkevt_set_periodic;
    data->clkevt.resume = at91sam926x_pit_resume;
    data->clkevt.suspend = at91sam926x_pit_suspend;

    ...
}

下面根据当前版本(v5.5-rc6)进行调整

在这里我们可以看到at91sam926x_pit_dt_init本地变量pit_data结构的指针,该结构包含clock_event_device结构,该结构将包含at91sam926x的时钟事件相关信息定期间隔计时器。首先,我们填写计时器设备的namefeatures。在我们的情况下,我们处理周期性的计时器,众所周知,可以将其编程为定期生成事件。

接下来的两个字段shiftmult是我们熟悉的。它们将用于将计时器的计数器转换为纳秒。此后,我们将计时器的rating设置为100。这意味着如果系统中不存在具有更高额定值的计时器,则该计时器将用于计时。下一个字段cpumask指示设备将在系统中的哪些处理器上运行。在我们的情况下,该设备将适用于第一个处理器。在include/linux/cpumask.h头文件中定义的cpumask_of宏,并扩展为调用的:

#define cpumask_of(cpu) (get_cpu_mask(cpu))

其中get_cpu_mask返回仅包含给定cpu编号的cpumask。有关cpumasks概念的更多信息,您可以在Linux内核中的CPU掩码部分中阅读。 在代码的最后四行中,我们为时钟事件设备挂起/恢复,设备关闭和时钟事件设备状态的更新设置了回调。

完成对at91sam926x定期计时器的初始化之后,我们可以通过调用以下函数进行注册:

clockevents_register_device(&data->clkevt);

现在我们可以考虑实现clockevents_register_device函数。 正如我上面已经写过的,此函数在kernel/time/clockevents.c源代码文件中定义,并且 从初始事件设备状态的初始化开始:

clockevent_set_state(dev, CLOCK_EVT_STATE_DETACHED);

实际上,事件设备可能处于以下状态之一:

enum clock_event_state {
    CLOCK_EVT_STATE_DETACHED,
    CLOCK_EVT_STATE_SHUTDOWN,
    CLOCK_EVT_STATE_PERIODIC,
    CLOCK_EVT_STATE_ONESHOT,
    CLOCK_EVT_STATE_ONESHOT_STOPPED,
};

其中:

  • CLOCK_EVT_STATE_DETACHED-clockevents框架不使用时钟事件设备。 实际上,它是所有时钟事件设备的初始状态。
  • CLOCK_EVT_STATE_SHUTDOWN- 时钟事件设备已关闭电源;
  • CLOCK_EVT_STATE_PERIODIC- 时钟事件设备可以被编程为周期性地产生事件;
  • CLOCK_EVT_STATE_ONESHOT- 时钟事件设备可以编程为仅生成一次事件;
  • CLOCK_EVT_STATE_ONESHOT_STOPPED - 时钟事件设备被编程为仅生成一次事件,现在暂时停止。

函数 clock_event_set_state 实现十分简单:

static inline void clockevent_set_state(struct clock_event_device *dev, 
enum clock_event_state state)
{
    dev->state_use_accessors = state;
}

如我们所见,它只是使用给定值(在我们的情况下为CLOCK_EVT_STATE_DETACHED)填充给定clock_event_device结构的state_use_accessors字段。实际上,所有时钟事件设备在注册期间都具有此初始状态。 clock_event_device结构的state_use_accessors字段提供时钟事件设备的当前状态。

在设置了给定的clock_event_device结构的初始状态之后,我们检查给定的时钟事件设备的cpumask不为零:

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

在注册函数中 clockevents_register_device

记住,我们已经将at91sam926x定期定时器的cpumask设置为第一个处理器。 如果cpumask字段为0,则我们检查系统中可能的处理器数量,并打印警告消息(如果少于)。 另外,我们将给定时钟事件设备的cpumask设置为当前处理器。 如果您对smp_processor_id宏的实现方式感兴趣,则可以在第四部分

进行此检查后,我们通过调用以下宏来锁定时钟事件设备注册的实际代码:

raw_spin_lock_irqsave(&clockevents_lock, flags);
...
...
...
raw_spin_unlock_irqrestore(&clockevents_lock, flags);

在注册函数中 clockevents_register_device

另外,raw_spin_lock_irqsaveraw_spin_unlock_irqrestore宏禁用本地中断,但是其他处理器上的中断仍然可能发生。 如果我们将新的时钟事件设备添加到时钟事件设备列表中并且其他时钟事件设备发生中断,则需要这样做以防止潜在的死锁

我们可以在raw_spin_lock_irqsaveraw_spin_unlock_irqrestore宏之间看到以下时钟事件设备注册代码:

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

首先,我们将给定的时钟事件设备添加到由clockevent_devices表示的时钟事件设备列表中:

static LIST_HEAD(clockevent_devices);

在下一步,我们调用在kernel/time/tick-common.c源代码文件,并检查是否应使用新的已注册时钟事件设备。tick_check_new_device函数检查给定的clock_event_device获取由tick_device结构表示的当前注册的滴答设备,并比较它们的等级和功能。 实际上,首选CLOCK_EVT_STATE_ONESHOT:

static bool tick_check_preferred(struct clock_event_device *curdev,
         struct clock_event_device *newdev)
{
    if (!(newdev->features & CLOCK_EVT_FEAT_ONESHOT)) {
        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);
}

/kernel/time/tick-common.c
clockevents_register_device
->tick_check_new_device

如果新注册的时钟事件设备比旧的tick设备更受青睐,我们将交换旧的和新的注册设备并安装新设备:

clockevents_exchange_device(curdev, newdev);
tick_setup_device(td, newdev, cpu, cpumask_of(cpu));

/kernel/time/tick-common.c
clockevents_register_device
->tick_check_new_device

释放clockevents_exchange_device函数,或者从clockevent_devices列表中删除旧的时钟事件设备。 下一个功能-tick_setup_device,我们可能会从其名称中了解到,它会设置新的tick设备。 这个函数检查新注册的时钟事件设备的模式,并调用tick_setup_periodic函数或tick_setup_oneshot取决于时钟设备模式:

if (td->mode == TICKDEV_MODE_PERIODIC)
    tick_setup_periodic(newdev, 0);
else
    tick_setup_oneshot(newdev, handler, next_event);

/kernel/time/tick-common.c
clockevents_register_device
->tick_check_new_device
->tick_setup_device

这两个函数都调用clockevents_switch_state以更改时钟事件设备的状态,并调用clockevents_program_event函数根据最大和最小当前时间差与下一个事件的时间之间的差值来设置时钟事件设备的下一个事件。 tick_setup_periodic

clockevents_switch_state(dev, CLOCK_EVT_STATE_PERIODIC);
clockevents_program_event(dev, next, false))

/kernel/time/tick-common.c
clockevents_register_device
->tick_check_new_device
->tick_setup_device
->tick_setup_periodic

tick_setup_oneshot_periodic:

clockevents_switch_state(newdev, CLOCK_EVT_STATE_ONESHOT);
clockevents_program_event(newdev, next_event, true);

函数调用链同上

clockevents_switch_state函数检查时钟事件设备是否未处于给定状态,并从同一源代码文件调用__clockevents_switch_state函数:

if (clockevent_get_state(dev) != state) {
    if (__clockevents_switch_state(dev, state))
        return;

/kernel/time/clockevents.c
clockevents_register_device
->tick_check_new_device
->tick_setup_device
->tick_setup_periodic
->clockevents_switch_state

__clockevents_switch_state函数仅根据给定的状态来调用特定的回调:

static int __clockevents_switch_state(struct clock_event_device *dev,
                                   enum clock_event_state state)
{
    if (dev->features & CLOCK_EVT_FEAT_DUMMY)
        return 0;

    switch (state) {
    case CLOCK_EVT_STATE_DETACHED:
    case CLOCK_EVT_STATE_SHUTDOWN:
        if (dev->set_state_shutdown)
            return dev->set_state_shutdown(dev);
        return 0;

    case CLOCK_EVT_STATE_PERIODIC:
        if (!(dev->features & CLOCK_EVT_FEAT_PERIODIC))
            return -ENOSYS;
        if (dev->set_state_periodic)
            return dev->set_state_periodic(dev);
        return 0;
    ...
    ...
    ...

/kernel/time/clockevents.c
clockevents_register_device
->tick_check_new_device
->tick_setup_device
->tick_setup_periodic
->clockevents_switch_state
->__clockevents_switch_state

在我们的at91sam926x定期计时器的情况下,状态为CLOCK_EVT_FEAT_PERIODIC

data->clkevt.features = CLOCK_EVT_FEAT_PERIODIC;
data->clkevt.set_state_periodic = pit_clkevt_set_periodic;

因此,对于pit_clkevt_set_periodic回调将被调用。 如果我们阅读 at91sam926x 的定期间隔计时器(PIT)
我们将看到存在定期间隔计时器模式寄存器(Periodic Interval Timer Mode Register), 使我们能够控制定期间隔计时器。

看起来想这样 :

31                                                   25        24
+---------------------------------------------------------------+
|                                          |  PITIEN  |  PITEN  |
+---------------------------------------------------------------+
23                            19                               16
+---------------------------------------------------------------+
|                             |               PIV               |
+---------------------------------------------------------------+
15                                                              8
+---------------------------------------------------------------+
|                            PIV                                |
+---------------------------------------------------------------+
7                                                               0
+---------------------------------------------------------------+
|                            PIV                                |
+---------------------------------------------------------------+

其中PIV定期间隔值-定义了与定期间隔计时器的主要20位计数器相比的值。 如果该位为1,则启用PITEN周期间隔定时器; 如果该位为1,则启用PITIEN周期间隔定时器中断。 因此,要设置周期模式,我们需要在周期间隔定时器模式寄存器中设置2425位。 我们在pit_clkevt_set_periodic函数中执行此操作:

static int pit_clkevt_set_periodic(struct clock_event_device *dev)
{
        struct pit_data *data = clkevt_to_pit_data(dev);
        ...
        ...
        ...
        pit_write(data->base, AT91_PIT_MR,
                  (data->cycle - 1) | AT91_PIT_PITEN | AT91_PIT_PITIEN);

        return 0;
}

AT91_PT_MR, AT91_PT_PITEN AT91_PIT_PITIEN 定义如下:

#define AT91_PIT_MR             0x00
#define AT91_PIT_PITIEN       BIT(25)
#define AT91_PIT_PITEN        BIT(24)

新时钟事件设备的设置完成后,我们可以返回clockevents_register_device函数。 clockevents_register_device函数中的最后一个函数是:

clockevents_notify_released();

该函数检查包含释放的时钟事件设备的clockevents_released列表(请记住,它们可能在调用clockevents_exchange_device函数之后发生)。 如果此列表不为空,则从clock_events_released列表中浏览时钟事件设备,然后从
clockevent_devices列表中将其删除:

static void clockevents_notify_released(void)
{
    struct clock_event_device *dev;

    while (!list_empty(&clockevents_released)) {
        dev = list_entry(clockevents_released.next,
            struct clock_event_device, list);
        list_del(&dev->list);
        list_add(&dev->list, &clockevent_devices);
        tick_check_new_device(dev);
    }
}

就这样。从这一刻起,我们已经注册了新的时钟事件设备。因此,clockevents框架的用法很简单明了。架构在时钟事件核心中注册了他们的时钟事件设备。 clockevents核心的用户可以获取时钟事件设备供其使用。 clockevents框架为各种与时钟相关的管理事件提供了通知机制,例如已注册或未注册的时钟事件设备,支持CPU hotplug的系统中的处理器脱机等。

我们仅看到clockevents_register_device函数的实现。但通常,时钟事件层API很小。除了用于时钟事件设备注册的API外,clockevents框架还提供了安排下一个事件中断,时钟事件设备通知服务的功能,并支持时钟事件设备的挂起和恢复。

If you want to know more about clockevents API you can start to research following source code and header files: kernel/time/tick-common.c, kernel/time/clockevents.c and include/linux/clockchips.h.

如果您想进一步了解clockevents API,可以开始研究以下源代码和头文件:kernel/time/tick-common.ckernel/time/clockevents.cinclude/linux/clockchips.h

就这样。

小结

这是本章chapter第五部分的结尾,该部分描述了Linux内核中与计时器和计时器管理相关的内容。 在上一部分中,我们熟悉了计时器的概念。 在这一部分中,我们继续学习Linux内核中与时间管理相关的内容,并了解了另一种框架-clockevents

如果您有任何疑问或建议,请随时在Twitter 0xAX上ping我,给我发电子邮件(anotherworldofworld@gmail.com)或直接创建issue

请注意,英语不是我的母语,对于给您带来的不便,我深表歉意。 如果发现任何错误,请将PR发送给linux-insides

相关链接

Be First to Comment

发表评论

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