当前位置: 代码迷 >> 综合 >> Linux时间子系统之时钟源层(Clock Source)
  详细解决方案

Linux时间子系统之时钟源层(Clock Source)

热度:58   发布时间:2023-12-17 01:18:29.0

所谓时钟源设备,Linux将其抽象为一个可以记录时间流逝的设备,其值随着时间的流逝递增。将前一次读取的值和当前的值做比较就知道过去了多长的时间。但是它不一定是记录当前具体时间的设备,它只记录过去了多少时间。

对于时间Linux还抽象了一个叫做定时事件设备(Clock Event Device),这两种设备的区别是,时钟源虽然也会按照一定周期递增,但其并不会触发中断,如果CPU不读,那时钟源设备就自己默默的在那里递增,不会打扰CPU。

时钟源设备在Linux内核中使用clocksource结构来表示(代码位于include/linux/clocksource.h中):

struct clocksource {u64 (*read)(struct clocksource *cs);u64 mask;u32 mult;u32 shift;u64 max_idle_ns;u32 maxadj;
#ifdef CONFIG_ARCH_CLOCKSOURCE_DATAstruct arch_clocksource_data archdata;
#endifu64 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);#ifdef CONFIG_CLOCKSOURCE_WATCHDOG......
#endifstruct module *owner;
};

这个结构体中的各个字段含义如下:

  • read:当要读取时钟源设备当前的周期数值时,会调用对应设备的该函数。

  • mask:代表了这个时钟源一共用了多少二进制位来计数。如果用了56位,那这个mask就是最低56位是1,最高8位是0。

  • mult和shift:用于将时钟周期数值转换成纳秒值。为什么要用这两个值进行转换,怎么转换,以及这两个值怎么算在后面会介绍(基本上和定时事件设备一样,只不过刚好反过来)。

  • max_idle_ns:表示该时钟源设备最多能记录多长跨度的时间,其值是根据max_cycles、mult和shift算出来的。

  • maxadj:对mult的最大调整值,其比率是固定的,是mult值的11%,也就是说如果需要对mult值进行调整的话,不能超过正负11%的范围。这个值在时间维持层(Time Keeping)会用到。

static u32 clocksource_max_adjustment(struct clocksource *cs)
{u64 ret;ret = (u64)cs->mult * 11;do_div(ret,100);return (u32)ret;
}
  • archdata:存放不同架构平台专用的一些数据。

  • max_cycles:表示该时钟源设备最多能记录多长周期数值。首先,这个值肯定要小于mask的值,硬件本身都记录不了,那肯定不行。其次,还要考虑mult和shift的值,因为在将周期数转换成纳秒数时是需要用这两个参数计算的,如果计算的过程中产生了溢出(64位)那也不行。

  • name:是给这个时钟源设备起的一个名字,一般比较直观,在/proc/timer_list中或者dmesg中都会出现。

  • list:系统中所有的时钟源设备实例都会保存在全局链表clocksource_list中,这个变量作为链表的元素(代码位于kernel/time/clocksource.c)。

static LIST_HEAD(clocksource_list);
  • rating:代表这个时钟源设备的精度值,其取值范围从1到499,数字越大代表设备的精度越高。当系统中同时有多个定时事件设备存在的时候,内核可以根据这个值选一个最佳的设备。

  • enable:当要打开时钟源设备时,会调用对应设备的该函数。

  • disable:当要关闭时钟源设备时,会调用对应设备的该函数。

  • flags:表示该时钟源设备的一些特征属性,一个时钟源可以同时包含多个属性。如果新注册的时钟源的的属性包含CLOCK_SOURCE_MUST_VERIFY,表示该时钟源需要经过看门狗(Watch Dog)的监测。如果误差过大,则会被标记为CLOCK_SOURCE_UNSTABLE。CLOCK_SOURCE_IS_CONTINUOUS表示该时钟源设备是连续的。CLOCK_SOURCE_VALID_FOR_HRES表明该设备是高分辨率的。

#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
  • suspend:当要暂停时钟源设备时,会调用对应设备的该函数。

  • resume:当要恢复时钟源设备时,会调用对应设备的该函数。

  • mark_unstable:如果Linux的系统看门狗(Watch Dog)发现这个时钟源不稳定,会调用对应设备的该函数。

  • tick_stable:如果Linux的系统看门狗(Watch Dog)发现这个时钟源比较稳定,会调用对应设备的该函数。

  • owner:拥有这个时钟源设备的模块。

1)mult、shift的计算和周期数到纳秒的转换

在时钟源层,从周期数转换成纳秒是在函数clocksource_cyc2ns里面完成的:

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

非常的简单,不过同样是从周期数转换成纳秒数,计算公式和在定时事件层是不一样的。这里是用(cycles * mult) >> shift公式计算出来的,而在定时事件层,是用(cycles << shift) / mult公式计算出来的,刚好相反。而这两种设备mult和shift的值又都是调用时钟源层的clocks_calc_mult_shift函数计算出来的:

void
clocks_calc_mult_shift(u32 *mult, u32 *shift, u32 from, u32 to, u32 maxsec)
{u64 tmp;u32 sft, sftacc= 32;/* 计算最大纳秒数前面有多少个0 */tmp = ((u64)maxsec * from) >> 32;while (tmp) {tmp >>=1;sftacc--;}/* 试探计算mult和shift的最大值 */for (sft = 32; sft > 0; sft--) {/* 左移sft位 */tmp = (u64) to << sft;/* 四舍五入 */tmp += from / 2;do_div(tmp, from);/* 判断是否会越界 */if ((tmp >> sftacc) == 0)break;}*mult = tmp;*shift = sft;
}
EXPORT_SYMBOL_GPL(clocks_calc_mult_shift);

细心观察可以发现,两个层调用clocks_calc_mult_shift函数传递的参数是不一样的。在时钟源层,from传递的是频率,to传递的是NSEC_PER_SEC;而在定时时间层,from传递的是NSEC_PER_SEC,to传递的是频率,刚好就是相反的。

2)时钟源设备的注册

时钟源设备的注册需要调用clocksource_register_hz或者clocksource_register_khz函数:

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,只是单位不同:

int __clocksource_register_scale(struct clocksource *cs, u32 scale, u32 freq)
{unsigned long flags;clocksource_arch_init(cs);/* 计算mult、shift、max_idle_ns和maxadj的值 */__clocksource_update_freq_scale(cs, scale, freq);mutex_lock(&clocksource_mutex);clocksource_watchdog_lock(&flags);/* 将时钟源设备插入clocksource_list全局列表中 */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;
}
EXPORT_SYMBOL_GPL(__clocksource_register_scale);

clocksource_arch_init是不同架构平台需要自己实现的初始化函数,至少对于Arm64来说,其定义为什么都不做。然后调用__clocksource_update_freq_scale函数更新本设备的mult、shift、max_idle_ns和maxadj的值。mult和shift的计算前面已经说过了,我们来看看max_idle_ns和maxadj的值是怎么计算的:

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);}/* 计算maxadj的值 */cs->maxadj = clocksource_max_adjustment(cs);while (freq && ((cs->mult + cs->maxadj < cs->mult)|| (cs->mult - cs->maxadj > cs->mult))) {cs->mult >>= 1;cs->shift--;cs->maxadj = clocksource_max_adjustment(cs);}/* 对于freq为0的情况,mult是自己定的,有可能会越界。 */WARN_ONCE(cs->mult + cs->maxadj < cs->mult,"timekeeping: Clocksource %s might overflow on 11%% adjustment\n",cs->name);/* 计算max_cycles和max_idle_ns的值 */clocksource_update_max_deferment(cs);/* 打印日志 */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);
}
EXPORT_SYMBOL_GPL(__clocksource_update_freq_scale);

这个初始化函数首先计算时钟源设备能记录的最大跨度是多少秒。mask表示时钟源能用多少二进制位计数,那么最大跨度当然就是从mask的值,然后用mask/freq就可以计算出最大跨度是多少秒。如果最大跨度大于10分钟,且mask的位数大于32位的话,为了保证精度,人为限定最大跨度为10分钟。

前面说了,maxadj的值其实就是mult * 11%,不过如果mult + maxadj的值越界的话,还需要再相应调小mult和shift的值,然后再计算,直到不越界为止。注意,它们都是32位的。

设置这些参数的前提条件是函数必须传入一个大于0的freq参数值。如果freq传入的是0,则mult和shift都由时钟源自己设置,maxadj也不会调整。那什么时钟源注册的时候会把freq设置成0呢?一般在系统初始化的时候,Linux内核会默认注册一个精度最低的缺省时钟源设备,叫做jiffies(代码位于kernel/time/jiffies.c中):

static struct clocksource clocksource_jiffies = {.name		= "jiffies",.rating		= 1,.read		= jiffies_read,.mask		= CLOCKSOURCE_MASK(32),.mult		= TICK_NSEC << JIFFIES_SHIFT,.shift		= JIFFIES_SHIFT,.max_cycles	= 10,
};

可以看到,该时钟源的mult和shift全都是自己事先定义好的,不需要再动态计算,所以注册的时候freq设置成0了。这个时钟源完全是根据系统jiffies来工作的,而系统jiffies又是由Tick设备来更新的,其更新频率是由编译选项决定的。如果编译选项选择了250Hz,那么其更新周期就是4毫秒。这个精度是非常差的,所以其精度值被设置成了1,除非实在没有可用的设备了,否则绝对不会使用。

初始化参数完成后,__clocksource_update_freq_scale函数会接着调用clocksource_enqueue函数,将本设备插入到clocksource_list全局链表中去:

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)break;entry = &tmp->list;}list_add(&cs->list, entry);
}

可以看出来,clocksource_list全局链表里面的所有时钟源设备是按照精度值由高到低排序的。

最后,__clocksource_update_freq_scale函数会调用clocksource_update_max_deferment函数更新max_cycles和max_idle_ns的值:

u64 clocks_calc_max_nsecs(u32 mult, u32 shift, u32 maxadj, u64 mask, u64 *max_cyc)
{u64 max_nsecs, max_cycles;/* 必须保证max_cycles * mult不能越界 */max_cycles = ULLONG_MAX;do_div(max_cycles, mult+maxadj);/* max_cycles不能大于mask的值 */max_cycles = min(max_cycles, mask);max_nsecs = clocksource_cyc2ns(max_cycles, mult - maxadj, shift);/* 设置max_cycles参数 */if (max_cyc)*max_cyc = max_cycles;/* 只返回实际max_nsecs的一半 */max_nsecs >>= 1;return max_nsecs;
}static inline void clocksource_update_max_deferment(struct clocksource *cs)
{cs->max_idle_ns = clocks_calc_max_nsecs(cs->mult, cs->shift,cs->maxadj, cs->mask,&cs->max_cycles);
}

clocksource_update_max_deferment函数直接调用了函数clocks_calc_max_nsecs,注意最后一个参数max_cyc是指针传递的。

max_cycles是系统能记录的最大时间跨度的周期数。它有两个限定条件,一个是必须小于mask的值,另外一个是max_cycles * mult的值不能64位溢出。由于在实际计算时,mult有可能要加上最大或减去最大maxadj调整值,所以max_cycles必须小于等于64位无符号数能便是的最大数值除以mult+maxadj。

max_cycles的值确定下来后,可以通过clocksource_cyc2ns函数,直接将对应的周期数值转换成纳秒值,计算公式就是(max_cycles * mult) >> shift。考虑到有可能会用maxadj调整的因素,这里取其可能的最小值,也就是计算公式是(max_cycles * (mult - maxadj)) >> shift。这样算出来后还不放心,又将其减了一半才返回。

2)时钟源设备的挑选

前面的代码中已经看到了,当注册新的时钟源设备到系统中时,内核会调用函数clocksource_select函数选择出一个最佳的时钟源:

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

其就是简单调用了__clocksource_select函数:

static void __clocksource_select(bool skipcur)
{bool oneshot = tick_oneshot_mode_active();struct clocksource *best, *cs;/* 找到最好的时钟源设备 */best = clocksource_find_best(oneshot, skipcur);if (!best)return;if (!strlen(override_name))goto found;/* 遍历所有当前注册的设备 */list_for_each_entry(cs, &clocksource_list, list) {/* 是否忽略当前正在使用的设备 */if (skipcur && cs == curr_clocksource)continue;/* 是否是指定的设备 */if (strcmp(cs->name, override_name) != 0)continue;/* 如果当前Tick设备处于单次触发模式,则时钟源设备必须支持高精度模式。*/if (!(cs->flags & CLOCK_SOURCE_VALID_FOR_HRES) && oneshot) {/* 如果时钟源设备不稳定,则不能使用,并给出提示。 */if (cs->flags & CLOCK_SOURCE_UNSTABLE) {pr_warn("Override clocksource %s is unstable and not HRT compatible - cannot switch while in HRT/NOHZ mode\n",cs->name);override_name[0] = 0;} else {pr_info("Override clocksource %s is not currently HRT compatible - deferring\n",cs->name);}} else/* 使用指定设备 */best = cs;break;}found:if (curr_clocksource != best && !timekeeping_notify(best)) {pr_info("Switched to clocksource %s\n", best->name);curr_clocksource = best;}
}

该函数的参数skipcur表示选择的时候忽略当前正在使用的时钟源设备,一定要选择一个新的设备。该函数调用clocksource_find_best函数找寻最佳设备,如果没有则直接返回。如果有的话,还有要判断是不是系统用户手动指定了一个时钟源设备(如果手动指定的话,会将override_name设置成指定的时钟源设备名称)。最后,如果现在正在使用的设备和挑选出来的设备不同,且时间维持层“同意”(调用timekeeping_notify函数)对当前时钟源设备的更改后,会正式将代表当前正在使用时钟源设备的全局变量curr_clocksource指向新注册的设备。

下面我们来看看clocksource_find_best函数是如何选择最佳设备的:

static struct clocksource *clocksource_find_best(bool oneshot, bool skipcur)
{struct clocksource *cs;if (!finished_booting || list_empty(&clocksource_list))return NULL;list_for_each_entry(cs, &clocksource_list, list) {/* 如果skipcur是True,则跳过当前设备。 */if (skipcur && cs == curr_clocksource)continue;/* 如果当前Tick设备处于单次触发模式,则时钟源设备必须支持高精度模式。 */if (oneshot && !(cs->flags & CLOCK_SOURCE_VALID_FOR_HRES))continue;return cs;}return NULL;
}

如果系统还没有完成启动过程,或者clocksource_list全局链表为空,则直接返回空指针,表示什么都没选中。所以,在系统正在启动的过程中是不会选择最佳设备的。

另外,如果当前的Tick设备已经切换到支持单次触发模式了,则当前高精度定时器已经切换到高精度模式了,所以这里时钟源也必须同步支持高精度模式。

在前面讲设备注册的时候提到过,在将时钟源设备插入clocksource_list全局链表的时候,已经根据精度值从高到低排序过了,所以这里找到第一个全部满足条件的设备一定是精度最高的。

前面提到了,系统正在启动的过程中是不会选择最佳设备的,那系统时钟源设备是在什么时机选择的呢?答案在函数clocksource_done_booting里面:

static int __init clocksource_done_booting(void)
{mutex_lock(&clocksource_mutex);/* 获得系统缺省时钟源设备 */curr_clocksource = clocksource_default_clock();finished_booting = 1;/* 启动看门狗线程去除一些不稳定的时钟源 */__clocksource_watchdog_kthread();/* 选择时钟源设备 */clocksource_select();mutex_unlock(&clocksource_mutex);return 0;
}
fs_initcall(clocksource_done_booting);

调用函数clocksource_default_clock获得系统缺省的时钟源设备,其定义如下(代码位于kernel/time/jiffies.c中):

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

所以其实那个精度最差的,以系统jiffies更新为基准的时钟源设备clocksource_jiffies是系统的缺省设备,如果实在没得选,那至少还有一个可用。

3)注册sysfs

时钟源设备会在sysfs中注册对应的文件,可以通过访问这些文件的内容知道当前系统中关于时钟源设备的基本信息。

注册sysfs是在init_clocksource_sysfs函数中完成的:

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函数会根据参数将一个子系统注册在/sys/devices/system/目录下。注册信息保存在clocksource_subsys静态全局变量中:

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

所以总线名字叫做“clocksource”,而设备名字也叫做“clocksource”。

接着,函数调用device_register函数,在这个子系统下注册不同的设备,传入的参数是指向device_clocksource全局结构体变量的指针:

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

设备号一定是0,所以对应的目录是/sys/devices/system/clocksource/clocksource0/。属性组被设置成了clocksource_groups,也是一个全局变量:

static struct attribute *clocksource_attrs[] = {&dev_attr_current_clocksource.attr,&dev_attr_unbind_clocksource.attr,&dev_attr_available_clocksource.attr,NULL
};
ATTRIBUTE_GROUPS(clocksource);

共有三个属性:

  1. dev_attr_current_clocksource:对应的文件名是current_clocksource,访问权限是644,对root用户可写,对所有用户可读。写入的内容是时钟源设备的名字,内核会查找并用其替换当前的时钟源。如果读的话,其内容是当前正在使用的时钟源设备名字。
  2. dev_attr_unbind_clocksource:对应的文件名是unbind_clocksource,访问权限是200,对root用户可写。写入的内容是时钟源设备的名字,内核会查找并解绑该设备,如果要解绑的时钟源设备是当前正在使用的,还会再选择一个替换的。
  3. dev_attr_available_clocksource:对应的文件名是available_clocksource,访问权限是444,对所有用户可读。该文件的内容是系统中所有注册的时钟源设备的名字。

接下来,我们接着看看dev_attr_current_clocksource的实现。其定义如下:

static ssize_t current_clocksource_show(struct device *dev,struct device_attribute *attr,char *buf)
{ssize_t count = 0;mutex_lock(&clocksource_mutex);count = snprintf(buf, PAGE_SIZE, "%s\n", curr_clocksource->name);mutex_unlock(&clocksource_mutex);return count;
}ssize_t sysfs_get_uname(const char *buf, char *dst, size_t cnt)
{size_t ret = cnt;if (!cnt || cnt >= CS_NAME_LEN)return -EINVAL;if (buf[cnt-1] == '\n')cnt--;if (cnt > 0)memcpy(dst, buf, cnt);dst[cnt] = 0;return ret;
}static ssize_t current_clocksource_store(struct device *dev,struct device_attribute *attr,const char *buf, size_t count)
{ssize_t ret;mutex_lock(&clocksource_mutex);/* 读取写入的名字到全局变量override_name中 */ret = sysfs_get_uname(buf, override_name, count);/* 如果内容不为空则重新选择时钟源设备 */if (ret >= 0)clocksource_select();mutex_unlock(&clocksource_mutex);return ret;
}
static DEVICE_ATTR_RW(current_clocksource);

经过DEVICE_ATTR_RW宏的定义,写如文件会调用current_clocksource_store函数,而读取文件会调用current_clocksource_show,后缀是固定的。读取非常简单,直接访问代表当前正在使用时钟源设备的全局变量curr_clocksource,读取其name变量就行了。而写入的话,会将输入的内容读入全局变量override_name中,然后调用clocksource_select函数再选一次。前面分析过了clocksource_select函数在选择的时候会查看override_name,尽量选择这个指定的设备。

最后,我们来看一个实际的例子,在64位树莓派4系统下,访问/sys/devices/system/clocksource/clocksource/current_device将会返回arch_sys_counter,表明其当前使用的是Arm通用计时器。

用dmsg访问内核日志,可看到下面和时钟源相关的日志:

[    0.000000] clocksource: arch_sys_counter: mask: 0xffffffffffffff max_cycles: 0x46d987e47, max_idle_ns: 440795202767 ns
[    0.191511] clocksource: jiffies: mask: 0xffffffff max_cycles: 0xffffffff, max_idle_ns: 7645041785100000 ns
[    0.293498] clocksource: Switched to clocksource arch_sys_counter

就像前面分析的那样,系统内注册了两个时钟源设备,一个是缺省的jiffies(32位),另一个是Arm通用计时器架构提供的时钟源arch_sys_counter(56位),系统最后毫无疑问的选择了arch_sys_counter。

  相关解决方案