Linux时间子系统之:时钟源

Linux内核那些事

共 6244字,需浏览 13分钟

 ·

2022-12-30 17:53

clock source 用于为 Linux 内核提供一个时间基线,如果你用 Linux 的 date 命令获取当前时间,内核会读取当前的 clock source,转换并返回合适的时间单位给用户空间。

在硬件层,它通常实现为一个由固定时钟频率驱动的计数器,计数器只能单调地增加,直到溢出为止。时钟源是内核计时的基础,系统启动时,内核通过硬件RTC获得当前时间,在这以后,在大多数情况下,内核通过选定的时钟源更新实时时间信息(墙上时间),而不再读取RTC的时间。

本节的内核代码树基于V3.4.10。

struct clocksource结构

内核用一个clocksource结构对真实的时钟源进行软件抽象,现在我们从clock source的数据结构开始,它的定义如下:

struct clocksource {
 /*
  * Hotpath data, fits in a single cache line when the
  * clocksource itself is cacheline aligned.
  */

 cycle_t (*read)(struct clocksource *cs);
 cycle_t cycle_last;
 cycle_t mask;
 u32 mult;
 u32 shift;
 u64 max_idle_ns;
 u32 maxadj;
#ifdef CONFIG_ARCH_CLOCKSOURCE_DATA
 struct arch_clocksource_data archdata;
#endif
 
 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);
 
 /* private: */
#ifdef CONFIG_CLOCKSOURCE_WATCHDOG
 /* Watchdog related data, used by the framework */
 struct list_head wd_list;
 cycle_t cs_last;
 cycle_t wd_last;
#endif
} ____cacheline_aligned;

我们只关注clocksource中的几个重要的字段。

1 rating:时钟源的精度

同一个设备下,可以有多个时钟源,每个时钟源的精度由驱动它的时钟频率决定,比如一个由10MHz时钟驱动的时钟源,他的精度就是100nS。clocksource结构中有一个rating字段,代表着该时钟源的精度范围,它的取值范围如下:

  • 1--99:不适合于用作实际的时钟源,只用于启动过程或用于测试;
  • 100--199:基本可用,可用作真实的时钟源,但不推荐;
  • 200--299:精度较好,可用作真实的时钟源;
  • 300--399:很好,精确的时钟源;
  • 400--499:理想的时钟源,如有可能就必须选择它作为时钟源;

1.2 read回调函数

时钟源本身不会产生中断,要获得时钟源的当前计数,只能通过主动调用它的read回调函数来获得当前的计数值,注意这里只能获得计数值,也就是所谓的cycle,要获得相应的时间,必须要借助clocksource的mult和shift字段进行转换计算。

1.3 mult和shift字段

因为从clocksource中读到的值是一个cycle计数值,要转换为时间,我们必须要知道驱动clocksource的时钟频率F,一个简单的计算就可以完成:

t = cycle / F;

可是clocksource并没有保存时钟的频率F,因为使用上面的公式进行计算,需要使用浮点运算,这在内核中是不允许的。

因此,内核使用了另外一个变通的办法,根据时钟的频率和期望的精度,事先计算出两个辅助常数mult和shift,然后使用以下公式进行cycle和t的转换:

t = (cycle * mult) >> shift;

只要我们保证:

F = (1 << shift) / mult;

内核内部使用64位进行该转换计算:

static inline s64 
clocksource_cyc2ns(cycle_t cycles, u32 mult, u32 shift)
{
    return ((u64) cycles * mult) >> shift;
}

从转换精度考虑,mult 的值是越大越好,但是为了计算过程不发生溢出,mult 的值又不能取得过大。为此内核假设 cycle 计数值被转换后的最大时间值:10分钟(600秒),主要的考虑是 CPU 进入 IDLE 状态后,时间信息不会被更新,只要在10分钟内退出IDLE,clocksource 的 cycle 计数值就可以被正确地转换为相应的时间,然后系统的时间信息可以被正确地更新。

当然最后的结果不一定是10分钟,它由 clocksource_max_deferment 进行计算,并保存 max_idle_ns 字段中,tickless 的代码要考虑这个值,以防止在 NO_HZ 配置环境下,系统保持 IDLE 状态的时间过长。在这样,由 10 分钟这个假设的时间值,我们可以推算出合适的 mult 和 shift 值。

2. clocksource的注册和初始化

通常,clocksource要在初始化阶段通过 clocksource_register_hz 函数通知内核它的工作时钟的频率,调用的过程如下:

由上图可见,最终大部分工作会转由 __clocksource_register_scale 完成,该函数首先完成对 mult 和 shift 值的计算,然后根据 mult 和 shift 值,最终通过 clocksource_max_deferment 获得该 clocksource 可接受的最大 IDLE 时间,并记录在 clocksource 的 max_idle_ns 字段中。clocksource_enqueue 函数负责按 clocksource 的 rating 的大小,把该 clocksource 按顺序挂在全局链表 clocksource_list 上,rating 值越大,在链表上的位置越靠前。

每次新的 clocksource 注册进来,都会触发 clocksource_select 函数被调用,它按照 rating 值选择最好的 clocksource,并记录在全局变量 curr_clocksource 中,然后通过 timekeeping_notify 函数通知 timekeeping,当前clocksource 已经变更,关于 timekeeping,我将会在后续的博文中阐述。

3. clocksource watchdog

系统中可能同时会注册对个 clocksource,各个 clocksource 的精度和稳定性各不相同,为了筛选这些注册的 clocksource,内核启用了一个定时器用于监控这些 clocksource 的性能,定时器的周期设为0.5秒:

#define WATCHDOG_INTERVAL (HZ >> 1)
#define WATCHDOG_THRESHOLD (NSEC_PER_SEC >> 4)

当有新的 clocksource 被注册时,除了会挂在全局链表 clocksource_list 外,还会同时挂在一个 watchdog 链表上:watchdog_list

定时器周期性地(0.5秒)检查 watchdog_list 上的 clocksource,WATCHDOG_THRESHOLD 的值定义为0.0625秒。如果在0.5秒内,clocksource 的偏差大于这个值就表示这个 clocksource 是不稳定的,定时器的回调函数通过 clocksource_watchdog_kthread 线程标记该 clocksource,并把它的 rate 修改为0,表示精度极差。

4. 建立clocksource的简要过程

在系统的启动阶段,内核注册了一个基于 jiffies 的 clocksource,代码位于 kernel/time/jiffies.c:

struct clocksource clocksource_jiffies = {
 .name  = "jiffies",
 .rating  = 1/* lowest valid rating*/
 .read  = jiffies_read,
 .mask  = 0xffffffff/*32bits*/
 .mult  = NSEC_PER_JIFFY << JIFFIES_SHIFT, /* details above */
 .shift  = JIFFIES_SHIFT,
};
......
 
static int __init init_jiffies_clocksource(void)
{
 return clocksource_register(&clocksource_jiffies);
}
 
core_initcall(init_jiffies_clocksource);

它的精度只有 1/HZ 秒,rating 值为1,如果平台的代码没有提供定制的 clocksource_default_clock 函数,它将返回该 clocksource:

struct clocksource * __init __weak clocksource_default_clock(void)
{
 return &clocksource_jiffies;
}

然后,在初始化的后段,clocksource 的代码会把全局变量 curr_clocksource 设置为上述的 clocksource:

static int __init clocksource_done_booting(void)
{
        ......
 curr_clocksource = clocksource_default_clock();
        ......
 finished_booting = 1;
        ......
 clocksource_select();
        ......
 return 0;
}
fs_initcall(clocksource_done_booting);

当然,如果平台级的代码在初始化时也会注册真正的硬件 clocksource,所以经过 clocksource_select 函数后,curr_clocksource 将会被设为最合适的 clocksource。如果 clocksource_select 函数认为需要切换更好的时钟源,它会通过 timekeeping_notify 通知 timekeeping 系统,使用新的 clocksource 进行时间计数和更新操作。

原文地址:https://blog.csdn.net/DroidPhone/article/details/7975694

浏览 67
点赞
评论
收藏
分享

手机扫一扫分享

分享
举报
评论
图片
表情
推荐
点赞
评论
收藏
分享

手机扫一扫分享

分享
举报