linux-sides-Timers and time management-Introduction to the clocksource framework

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

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

clocksource框架简介

上一节Part 1Timer的第一部分,描述了Linux内核中与计时器和时间管理相关的东西。我们在前一部分中了解了两个概念:

  • jiffies
  • clocksource

第一个全局变量定义在include/linux/jiffies.h头文件中,代表在每个定时器中断期间增加的计数器。因此,如果我们可以访问这个全局变量并且我们知道定时器中断率,我们可以将jiffies转换为人类时间单位。我们已经知道在Linux内核中由编译时常量(称为“HZ”)表示的定时器中断速率。HZ的值等于CONFIG_HZ内核配置选项的值,如果我们通过文件arch/x86/configs/x86_64_defconfig查找到内核相关配置:

CONFIG_HZ_1000 = Y

设置内核配置选项后. x86_64 体系架构下 CONFIG_HZ值默认是 1000 , 如果我们将jiffies除以HZ:

jiffies / HZ

我们将获得自Linux内核开始工作之后经过的秒数,换句话说,我们将获得系统uptime. 由于HZ代表一秒钟内的定时器中断量,我们可以在未来的某个时间设置一个值。例如:

/* one minute from now */
unsigned long later = jiffies + 60*HZ;

/* five minutes from now */
unsigned long later = jiffies + 5*60*HZ;

这是Linux内核中非常常见的做法。例如,如果您将查看 arch/x86/kernel/smpboot.c
源代码文件,你会发现do_boot_cpu函数。此功能启动除引导处理器之外的所有处理器。您可以找到等待十秒钟的应用程序处理器响应的代码段:

if (!boot_error) {
    timeout = jiffies + 10*HZ;
    while (time_before(jiffies, timeout)) {
        ...
        ...
        ...
        schedule();
    }
    ...
    ...
    ...
}

备注
原书udelay(100)后修改为schedule()
详见x86/smpboot: Remove udelay(100) when polling cpu_initialized_map


我们在这里将jiffies + 10 * HZ值赋给timeout变量。这意味着超时十秒。在此之后,我们进入一个循环,我们使用time_before宏来比较当前的jiffies值和判断是否的超时。

或者例如,如果我们查看sound/isa/sscape.c源代码文件,它代表Ensoniq Soundscape Elite声卡驱动,我们将看到obp_startup_ack函数,它等待给定的超时,以便On-Board Processor返回其启动确认序列:

static int obp_startup_ack(struct soundscape *s, unsigned timeout)
{
    unsigned long end_time = jiffies + msecs_to_jiffies(timeout);

    do {
        ...
        ...
        ...
        x = host_read_unsafe(s->io_base);
        ...
        ...
        ...
        if (x == 0xfe || x == 0xff)
            return 1;
        msleep(10);
    } while (time_before(jiffies, end_time));

    return 0;
}

正如您所看到的,jiffies变量在Linux内核代码中广泛引用, 正如我已经写过的那样,我们在前一部分中遇到了另一个与时间管理相关的新概念 - clocksource。我们只看到了这个概念的简短描述和用于时钟源注册的API。让我们仔细看看这一部分.

clocksource简介

clocksource概念表示Linux内核中时钟源管理的通用API。为什么我们需要一个单独的框架呢?让我们回到开头。time概念是Linux内核和其他操作系统内核的基本概念。计时是使用这个概念的必要条件之一。例如,Linux内核必须知道并更新自系统启动以来经过的时间,它必须确定当前进程已为每个处理器运行多长时间等等。Linux内核可以获取有关时间的信息吗?

  • 首先是实时时钟或RTC 由非易失性设备代表。您可以在Linux内核文件夹drivers/rtc中找到一组与架构无关的实时时钟驱动程序目录。除此之外,每个架构都可以为依赖于架构的实时时钟提供驱动程序,例如, x86架构下的, CMOS/RTC-arch/x86/kernel/rtc.c
  • 第二个是系统计时器 - 以定期速率激活 interrupts。例如,对于 IBM PC兼容机的 - programmable interval timer.

我们已经知道,为了计时目的,我们可以在Linux内核中使用jiffiesjiffies可以被认为是只读全局变量,它以HZ频率更新。我们知道HZ是一个编译时内核参数,其合理范围是从1001000Hz。因此,保证有一个时间测量接口,分辨率为1 - 10毫秒。除了标准的jiffies之外,我们在前一部分中看到了refined_jiffies时钟源,它基于i8253/i8254 programmable interval timer 频率大约1193182赫兹。因此,我们可以使用refined_jiffies获得关于1微秒分辨率的信息。这时, nanoseconds是给定时钟源的时间值单位的最佳选择。

时间间隔测量是否可以做到的更精确取决于硬件。我们对x86依赖的定时器硬件知之甚少。但每个架构都提供自己的定时器硬件。早期,每个架构都有自己的实现。该问题的解决方案是通过公共代码框架中的抽象层和相关API,管理各种时钟源并独立于定时器中断。这个通用的代码框架变成了clocksource框架。

通用时间和时钟源管理框架将大量计时代码移动到独立于体系结构的框架代码部分,依赖于体系结构的部分简化为定义和管理低级硬件的时钟源。使用不同的硬件测量不同架构的时间间隔需要大量资源,而且非常复杂。每个时钟相关服务的实现与单个硬件设备密切相关,它导致不同体系结构的类似实现。

在此框架内,每个时钟源都需要将时间表示维持为单调递增值。正如我们在Linux内核代码中看到的那样,纳秒是当时时钟源的时间值单位的最佳选择。时钟源框架的要点之一是允许用户在配置系统以及选择,访问和缩放不同时钟源时,在从一系列可用时钟硬件设备中选择时钟源。

clocksource结构

clocksource框架的基本定义是在 include/linux/clocksource.h头文件的clocksource结构体中. 我们已经看到了前面part 1 中的clocksource结构提供的一些字段. 让我们看看这个结构的完整定义,并尝试描述它的所有字段:

struct clocksource {
    u64 (*read)(struct clocksource *cs);
    u64 mask;
    u32 mult;
    u32 shift;
    u64 max_idle_ns;
    u32 maxadj;
#ifdef CONFIG_ARCH_CLOCKSOURCE_DATA
    struct arch_clocksource_data archdata;
#endif
    u64 max_cycles;
    const char *name;
    struct list_head list;
    int rating;
    int (*enable)(struct clocksource *cs);
    void (*disable)(struct clocksource *cs);
    unsigned long flags;
    void (*suspend)(struct clocksource *cs);
    void (*resume)(struct clocksource *cs);
+   void (*mark_unstable)(struct clocksource *cs); 
+   void (*tick_stable)(struct clocksource *cs); 

    /* private: */
#ifdef CONFIG_CLOCKSOURCE_WATCHDOG
    /* Watchdog related data, used by the framework */
    struct list_head wd_list;
    u64 cs_last;
    u64 wd_last;
#endif
    struct module *owner;
};

我们已经看到了前一部分中clocksource结构的第一个字段 - 它是指向read函数的指针,它返回由clocksource框架选择的最佳计数器。例如,我们使用jiffies_read函数来读取jiffies值:

static struct clocksource clocksource_jiffies = {
    ...
    .read       = jiffies_read,
    ...
}

jiffies_read 返回:

static cycle_t jiffies_read(struct clocksource *cs)
{
    return (cycle_t) jiffies;
}

使用read_tsc函数读取jiffies:

static struct clocksource clocksource_tsc = {
    ...
    .read               = read_tsc,
    ...
};

对于 time stamp counter 请参考.

下一个字段是mask,它允许确保来自非64位计数器的计数器值之间的减法不需要处理特殊的溢出逻辑。

mask字段之后,我们可以看到两个字段:multshift。这些是基于数学函数的字段,它们提供转换特定于每个时钟源的时间值的能力。换句话说,这两个字段帮助我们将计数器的抽象机器时间单位转换为纳秒。

在这两个字段之后,我们可以看到64max_idle_ns字段表示clocksource允许的最大空闲时间(以纳秒为单位)。我们需要在此字段中为Linux内核启用CONFIG_NO_HZ内核配置选项。此内核配置选项使Linux内核能够在没有常规计时器tick的情况下运行(我们将在其他部分中看到对此内容的完整说明)。dynamic tick允许内核睡眠时间长于单个tick的问题,而且睡眠时间可以是无限的。max_idle_ns字段表示此睡眠限制。

max_idle_ns之后的下一个字段是maxadj字段,它是mult的最大调整值。我们将周期转换为纳秒的主要公式:

((u64) cycles * mult) >> shift;

不是100%准确。相反,数字尽可能接近纳秒,maxadj有助于纠正这一点,并使得clocksource API避免调整后可能溢出的mult值。

下一个字段是max_cycles,正如我们从名称中可以理解的那样,该字段表示潜在溢出之前的最大循环值。

接下来的六个字段是函数的指针:

  • enable - 启用clocksource的可选功能;
  • disable - 禁用clocksource的可选功能;
  • suspend - 暂停clockource的功能;
  • resume - resumeource的恢复功能;
  • mark_unstable - 通过watchdog标志clocksource不稳定功能;
  • tick_stable - 通过watchdog设置稳定的同步点;

最后一个字段是owner表示对作为clocksource所有者的内核模块 module的引用

我们刚刚浏览了clocksource结构的所有标准字段。但你可以注意到我们错过了clocksource结构的一些领域。我们可以将所有错过的字段划分为两种类型:

  • 第一种类型, 已经为我们所知。例如,它们是name字段,表示clocksource的名称,rating字段有助于Linux内核选择最佳时钟源等。
  • 第二种类型,依赖于不同Linux内核配置选项的字段。 让我们来看看这些字段。

第一个字段是archdata。该字段具有arch_clocksource_data类型,取决于CONFIG_ARCH_CLOCKSOURCE_DATA内核配置选项。此字段仅适用于x86IA64体系结构。同样,正如我们从字段名称中可以理解的那样,它代表了时钟源的体系结构特定数据。例如,它代表vDSO时钟模式:

struct arch_clocksource_data {
    int vclock_mode;
};

对于x86架构。vDSO时钟模式可以是以下之一:

#define VCLOCK_NONE 0
#define VCLOCK_TSC  1
#define VCLOCK_HPET 2
#define VCLOCK_PVCLOCK 3
#define VCLOCK_MAX 3

最后三个字段是wd_listcs_lastwd_last取决于CONFIG_CLOCKSOURCE_WATCHDOG内核配置选项。 首先让我们试着了解它是什么watchdog。简而言之,watchdog是一个计时器,用于检测计算机故障并从中恢复。所有这三个字段都包含clocksource框架使用的watchdog相关数据。只有在arch/x86/KConfig内核配置文件包含CONFIG_CLOCKSOURCE_WATCHDOG内核配置选项,才可以看到。 那么,为什么在在x86x86_64中需要 watchdog? 您可能已经知道所有x86处理器都有特殊的64位寄存器 - time stamp counter. 该寄存器包含重启后的 cycles.有时需要针对另一个时钟源验证时间戳计数器。我们不会在这部分中看到watchdog计时器的初始化,在此之前我们必须了解更多关于计时器的信息。

描述特殊属性的标志flag

#define CLOCK_SOURCE_IS_CONTINUOUS     0x01
#define CLOCK_SOURCE_MUST_VERIFY       0x02
#define CLOCK_SOURCE_WATCHDOG          0x10
#define CLOCK_SOURCE_VALID_FOR_HRES        0x20
#define CLOCK_SOURCE_UNSTABLE          0x40
#define CLOCK_SOURCE_SUSPEND_NONSTOP       0x80
#define CLOCK_SOURCE_RESELECT          0x100

就这样。从这一刻起,我们就知道了clocksource结构的所有领域。这些知识将帮助我们学习clocksource框架的内部。

新的时钟源注册

我们前面从 part 1只看到了 clocksource 框架的一个函数 -clocksource_register. 函数定义在头文件 include/linux/clocksource.h我们能够从函数名看出,他的主要功能是注册新的clocksource, 如果我们查看 __clocksource_register 的实现, 将会发现他只是调用了 clocksource_register_scale 并返回:

static inline int __clocksource_register(struct clocksource *cs)
{
    return __clocksource_register_scale(cs, 1, 0);
}

在我们看到__clocksource_register_scale函数的实现之前,我们可以看到clocksource为新的时钟源注册提供了额外的API:

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

static inline int clocksource_register_khz(struct clocksource *cs, u32 khz)
{
        return __clocksource_register_scale(cs, 1000, khz);
}

所有这些功能都是一样的。它们返回clocksource_register_scale函数的值,但具有不同的参数集。clocksource_register_scale函数在kernel/time/clocksource.c源代码文件中定义。为了理解这些函数之间的区别,让我们看一下clocksource_register_khz函数的参数。我们可以看到,这个函数有三个参数:

  • cs - 要安装的clocksource;
  • scale - 时钟源的比例因子。换句话说,如果我们将在频率上乘以该参数的值,我们将得到一个时钟源的hz;
  • freq - 时钟源频率除以比例。

下面看下 __clocksource_register_scale 函数的实现:

int __clocksource_register_scale(struct clocksource *cs, u32 scale, u32 freq)
{
    unsigned long flags;

+   clocksource_arch_init(cs);

    __clocksource_update_freq_scale(cs, scale, freq);

    mutex_lock(&clocksource_mutex);

    clocksource_watchdog_lock(&flags);
    clocksource_enqueue(cs);
    clocksource_enqueue_watchdog(cs);
    clocksource_watchdog_unlock(&flags);

+   clocksource_select();
+   clocksource_select_watchdog(false);
+   __clocksource_suspend_select(cs);
+   mutex_unlock(&clocksource_mutex);
    return 0;
}

+:后续版本增加部分



clocksource_arch_init(cs) 涉及archDaTa内容的完整性检查

void clocksource_arch_init(struct clocksource *cs)
{
    if (cs->archdata.vclock_mode == VCLOCK_NONE)
        return;

    if (cs->archdata.vclock_mode > VCLOCK_MAX) {
        pr_warn("clocksource %s registered with invalid vclock_mode %d. Disabling vclock.\n",
            cs->name, cs->archdata.vclock_mode);
        cs->archdata.vclock_mode = VCLOCK_NONE;
    }

    if (cs->mask != CLOCKSOURCE_MASK(64)) {
        pr_warn("clocksource %s registered with invalid mask %016llx. Disabling vclock.\n",
            cs->name, cs->mask);
        cs->archdata.vclock_mode = VCLOCK_NONE;
    }
}

后续版本增加函数


首先,我们可以看到clocksource_register_scale函数从调用同一源代码文件中定义的clocksource_update_freq_scale函数开始,并以新频率更新指定的时钟源。

我们来看看这个函数的实现。在第一步中,我们需要检查给定频率,如果它没有作为zero传递,我们需要计算给定时钟源的multshift参数。为什么我们需要检查frequency的值 ? 实际上它可以为0。如果你仔细查看__clocksource_register函数的实现,你可能已经注意到我们将frequency传递为0。我们只对一些具有自定义multshift参数的时钟源进行此操作。查看上一节 part 1 我们看到了jiffiesmultshift的计算。__clocksource_update_freq_scale函数将为我们的其他时钟源提供更新时钟频率此操作。

所以在__clocksource_update_freq_scale函数的开头我们检查frequency参数的值,如果不是零,我们需要为给定的时钟源计算multshift。让我们看看multshift计算:

void __clocksource_update_freq_scale(struct clocksource *cs, u32 scale, u32 freq)
{
        u64 sec;

        if (freq) {
             sec = cs->mask;
             do_div(sec, freq);
             do_div(sec, scale);

             if (!sec)
                   sec = 1;
             else if (sec > 600 && cs->mask > UINT_MAX)
                   sec = 600;

             clocks_calc_mult_shift(&cs->mult, &cs->shift, freq,
                                    NSEC_PER_SEC / scale, sec * scale);
       }
        ...
        ...
        ...
}

在这里,我们可以看到在时钟源计数器溢出之前我们可以运行的最大秒数的计算。首先,我们使用时钟源掩码mask的值填充sec变量。请记住,时钟源的掩码表示对给定时钟源有效的最大位数。在此之后,我们可以看到两个分工操作。

  • 首先,我们将sec变量除以时钟源频率,然后除以比例因子。freq参数显示我们将在一秒钟内发生多少个定时器中断。因此,我们将mask值除以表示计时器频率的计数器的最大数量(例如jiffy),并获得特定时钟源的最大秒数。
  • 其次,第二次除法操作将给出我们特定时钟源的最大秒数, 取决于其比例因子,其可以是1Hz1kHz(10^3Hz)。

在我们获得最大秒数后,我们检查此值并将其设置为1600取决于下一步的结果。这些值是clockource的最大休眠时间(以秒为单位)。在下一步中,我们可以看到clocks_calc_mult_shift的调用。该函数的要点是计算给定时钟源的multshift值。在__clocksource_update_freq_scale函数的最后,我们检查刚刚计算出的给定时钟源的mult值在调整后不会导致溢出,更新给定时钟源的最大空闲时间max_idle_nsmax_cycles,并将结果打印到内核缓冲区:

pr_info("%s: mask: 0x%llx max_cycles: 0x%llx, max_idle_ns: %lld ns\n",
    cs->name, cs->mask, cs->max_cycles, cs->max_idle_ns);

看下 dmesg 输出:

$ dmesg | grep "clocksource:"
[    0.000000] clocksource: refined-jiffies: mask: 0xffffffff max_cycles: 0xffffffff, max_idle_ns: 1910969940391419 ns
[    0.000000] clocksource: hpet: mask: 0xffffffff max_cycles: 0xffffffff, max_idle_ns: 133484882848 ns
[    0.094084] clocksource: jiffies: mask: 0xffffffff max_cycles: 0xffffffff, max_idle_ns: 1911260446275000 ns
[    0.205302] clocksource: acpi_pm: mask: 0xffffff max_cycles: 0xffffff, max_idle_ns: 2085701024 ns
[    1.452979] clocksource: tsc: mask: 0xffffffffffffffff max_cycles: 0x7350b459580, max_idle_ns: 881591204237 ns

执行完 __clocksource_update_freq_scale , 将返回函数__clocksource_register_scale 他将注册新的clock source, 我们可以看到下面几个函数:

    mutex_lock(&clocksource_mutex);
    clocksource_watchdog_lock(&flags);
    clocksource_enqueue(cs);
    clocksource_enqueue_watchdog(cs);
    clocksource_watchdog_unlock(&flags);

+   clocksource_select();
+   clocksource_select_watchdog(false);
+   __clocksource_suspend_select(cs);
+   mutex_unlock(&clocksource_mutex);

请注意,在调用clocksource_enqueue函数前, 锁定 clocksource_mutex mutex. clocksource_mutex互斥体主要是保护变量curr_clocksource , 该变量代表当前选择的 clocksource和已注册的clocksources 列表clocksource_list, 现在,让我们来看看三个功能:

  • clocksource_enqueue 函数和其他两个函数在同一源代码file. 我们通过所有已经注册的clocksources或遍历clocksource_list的所有元素,并试图找到给定clocksource的最佳位置
static void clocksource_enqueue(struct clocksource *cs)
{
    struct list_head *entry = &clocksource_list;
    struct clocksource *tmp;

    list_for_each_entry(tmp, &clocksource_list, list)
        if (tmp->rating >= cs->rating)
            entry = &tmp->list;
    list_add(&cs->list, entry);
}

最后我们将新的clocksource 放入 clocksource_list.

  • clocksource_enqueue_watchdog 与之前的函数几乎完全相同,但是它将新的时钟源插入到wd_list中, 取决于struct clocksourceflags并启动新的watchdog计时器。正如我已经写过的那样,我们不会在这部分中考虑watchdog相关的东西,但会在接下来的部分进行。

  • clocksource_select 我们可以从函数的名称中理解这个函数的要点 - 从注册的时钟源中选择最好的clocksource:

static void clocksource_select(void)
{
    return __clocksource_select(false);
}

注意__clocksource_select函数接受一个参数(在我们的例子中是false)。这个bool参数显示了如何遍历clocksource_list。在我们的例子中,我们传递false,这意味着我们将遍历clocksource_list的所有条目。我们已经知道,在调用clocksource_enqueue函数之后,具有最佳评级的clocksource将是clocksource_list中的第一个,所以我们可以很容易地从这个列表中获取它。在我们找到具有最佳评级的时钟源后,我们切换到它:

if (curr_clocksource != best && !timekeeping_notify(best)) {
    pr_info("Switched to clocksource %s\n", best->name);
    curr_clocksource = best;
}

可以从dmesg输出结果中看到:

$ dmesg | grep Switched
[    0.199688] clocksource: Switched to clocksource hpet
[    2.452966] clocksource: Switched to clocksource tsc

请注意,我们可以在dmesg输出中看到两个时钟源(在我们的例子中是hpettsc)。是的,实际上特定硬件上可能有许多不同的时钟源。因此,Linux内核知道所有已注册的时钟源,并在每次注册新时钟源后切换到具有更好评级的时钟源。


  • __clocksource_suspend_select设置挂起时钟源来源
static void __clocksource_suspend_select(struct clocksource *cs)
{
    if (!(cs->flags & CLOCK_SOURCE_SUSPEND_NONSTOP))
        return;
    if (cs->suspend || cs->resume) {
        pr_warn("Nonstop clocksource %s should not supply suspend/resume interfaces\n",
            cs->name);
    }

    if (!suspend_clocksource || cs->rating > suspend_clocksource->rating)
        suspend_clocksource = cs;
}

后续版本增加


如果我们将查看kernel/time/clocksource.c源代码文件的底部,我们将看到它有sysfs接口。主要初始化发生在init_clocksource_sysfs函数中,该函数将在设备initcalls期间调用。让我们看一下init_clocksource_sysfs函数的实现:

static struct bus_type clocksource_subsys = {
    .name = "clocksource",
    .dev_name = "clocksource",
};

static int __init init_clocksource_sysfs(void)
{
    int error = subsys_system_register(&clocksource_subsys, NULL);

    if (!error)
        error = device_register(&device_clocksource);

    return error;
}

device_initcall(init_clocksource_sysfs);

这部分与原书版本略有不同

首先,我们可以看到它通过调用subsys_system_register函数来注册clocksource子系统。换句话说,在调用此函数后,我们将有以下目录:

$ pwd
/sys/devices/system/clocksource

在这一步之后,我们可以看到device_clocksource设备的注册,它由以下结构表示:

static struct device device_clocksource = {
    .id = 0,
    .bus    = &clocksource_subsys,
    .groups = clocksource_groups,
};

并创建三个文件:

  • dev_attr_current_clocksource;
  • dev_attr_unbind_clocksource;
  • dev_attr_available_clocksource.

这些文件将提供有关系统中当前时钟源信息, 系统中可用的时钟源以及允许取消绑定的时钟源接口

init_clocksource_sysfs 执行后, 我们将在一下内容中找到可用的时钟源的一些信息:

$ cat /sys/devices/system/clocksource/clocksource0/available_clocksource 
tsc hpet acpi_pm 

有关系统中当前时钟源的信息

$ cat /sys/devices/system/clocksource/clocksource0/current_clocksource 
tsc

在上一节中, 我们看到了注册jiffies时钟源的API, 但是没有深入了解clocksource框架,这一节中我们完成相关工作并且看到了注册新时钟源的实现和选择系统最佳评级的时钟源. 当然这并不是clocksource所有的API ,还有一些额外的函数,比如,clocksource_unregister 用于从 clocksource_list中删除给定的时钟源头等等. 但是我并不会在这部分描述他, 他们现在不是很重要, 如果你感兴趣可以查看kernel/time/clocksource.c.

就这样。

结论

这是第二部分的结尾,本节描述了Linux内核中定时器和定时器管理机制的内容。在前一个章节中,我们
了解到以下两个概念:jiffies and clocksource. 这一部分我们看到了一些jiffies用法的例子,并且了解了更多 clocksource的实现细节

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

请注意,英语不是我的第一语言,我很抱歉给您带来不便。如果您发现任何错误,请将PR发送至linux-insides

Links

Be First to Comment

发表评论

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