几乎所有的计算机系统中都会存在一个所谓的定时设备,经过设置后,在某个固定的时间或某个相对的时间间隔后,达到触发条件,发送中断给处理器。
系统中的每一种实际的定时事件设备都由一个叫做clock_event_device的结构体变量表示(代码位于include/linux/clockchips.h):
struct clock_event_device {
void (*event_handler)(struct clock_event_device *);
int (*set_next_event)(unsigned long evt, struct clock_event_device *);
int (*set_next_ktime)(ktime_t expires, struct clock_event_device *);
ktime_t next_event;
u64 max_delta_ns;
u64 min_delta_ns;
u32 mult;
u32 shift;
enum clock_event_state state_use_accessors;
unsigned int features;
unsigned long retries;
int (*set_state_periodic)(struct clock_event_device *);
int (*set_state_oneshot)(struct clock_event_device *);
int (*set_state_oneshot_stopped)(struct clock_event_device *);
int (*set_state_shutdown)(struct clock_event_device *);
int (*tick_resume)(struct clock_event_device *);
void (*broadcast)(const struct cpumask *mask);
void (*suspend)(struct clock_event_device *);
void (*resume)(struct clock_event_device *);
unsigned long min_delta_ticks;
unsigned long max_delta_ticks;
const char *name;
int rating;
int irq;
int bound_on;
const struct cpumask *cpumask;
struct list_head list;
struct module *owner;
} ____cacheline_aligned;
这个结构体是“____cacheline_aligned”的,表明其是缓存行对齐的,频繁访问的话可以加快速度。
熟悉面向对象编程的一定觉得对这个结构体的定义非常熟悉,除了变量外还定义了一堆函数指针,并且每个函数的第一个参数是一个指向自己的指针。所以,每一个所谓的定时事件设备都是一个对象实例。
下面来说结构体中这些字段的具体意思:
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,
};
# define CLOCK_EVT_FEAT_PERIODIC 0x000001
# define CLOCK_EVT_FEAT_ONESHOT 0x000002
# define CLOCK_EVT_FEAT_KTIME 0x000004
# define CLOCK_EVT_FEAT_C3STOP 0x000008
# define CLOCK_EVT_FEAT_DUMMY 0x000010
# define CLOCK_EVT_FEAT_DYNIRQ 0x000020
# define CLOCK_EVT_FEAT_PERCPU 0x000040
# define CLOCK_EVT_FEAT_HRTIMER 0x000080
static LIST_HEAD(clockevent_devices);
有了前面的知识准备了之后,下面我们分几个方面来解释一下时钟事件(Clock Events)层的工作过程。
mult和shift值的计算主要是在函数clocks_calc_mult_shift中,和时钟源(Clock Source)中对应的值计算方式是一样的(代码位于kernel/time/clocksource.c中):
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);
对于定时时间设备来说,其主要需要计算时钟周期数到纳秒数的转换,所以虽然和时钟源计算mult和shift调用的函数是一样的,但参数并不同。这里,from设置成了NSEC_PER_SEC(1000000000L),即每秒多少纳秒数,而to设置成了时钟源的频率,maxsec表示最大能转换的秒数。转换后要满足等式:
也就是时钟源周期数除以to(频率)要等于纳秒数除以from(1000000000L)。而时钟源周期数用shift和mult转换成纳秒数的公式基本为:
两个公式结合一下就可以得到:
从公式中可以看出来,当然mult和shift越大越好,计算的精度损失越小。但是,整数运算位数是有限制的,对于64位系统来说只有64位的长度。所以,shift和mult就不能太大,否者计算的过程中就可能越界。这时候,maxsec就有用处了,它用来限制最大能转换的秒数,那么maxsec * from(NSEC_PER_SEC)就表示能转换的最大纳秒数,而通过前面的公式变换得到:
那么mult的位数一定要比这个最大数前面0的位数要多,否则就会越界。有点拗口,举个例子,假如最大表示的纳秒数有40位,那么如果mult超过24位的话,那以上等式两边的数值就会超过64位,也就意味着转换最大时钟源周期数到最大纳秒数时,肯定会越界。
do_div是一个宏定义(代码位于include/asm-generic/div64.h):
# define do_div(n,base) ({ \
uint32_t __base = (base); \
uint32_t __rem; \
__rem = ((uint64_t)(n)) % __base; \
(n) = ((uint64_t)(n)) / __base; \
__rem; \
})
可以看出其主要的功能是将第一个参数整数除第二个参数后再赋值给第一个参数。
将周期数转换成纳秒数主要在函数cev_delta2ns函数中实现(代码位于kernel/time/clockevents.c):
static u64 cev_delta2ns(unsigned long latch, struct clock_event_device *evt,
bool ismax)
{
u64 clc = (u64) latch << evt->shift;
u64 rnd;
if (WARN_ON(!evt->mult))
evt->mult = 1;
rnd = (u64) evt->mult - 1;
/* 判断是否越界,如果越界则将clc设置成最大值。 */
if ((clc >> evt->shift) != (u64)latch)
clc = ~0ULL;
if ((~0ULL - clc > rnd) &&
(!ismax || evt->mult <= (1ULL << evt->shift)))
clc += rnd;
do_div(clc, evt->mult);
return clc > 1000 ? clc : 1000;
}
latch表示经过的周期数,ismax表示latch传入的周期数是不是能表示的最大的那个值。可以看出来,基本是按照上面的计算公式转换的,中间加了一些越界检查。do_dive(clc, evt->mult)实际等价于clc = clc / evt->mult。最后,如果如果除出来的数小于等于1000的话,也就是等于1000纳秒或1毫秒,可以认为是噪声,强制返回1000。
当有新的定时事件设备加入内核后,有可能会切换当前tick设备使用的定时事件设备,这是在函数clockevents_exchange_device中实现的:
void clockevents_exchange_device(struct clock_event_device *old,
struct clock_event_device *new)
{
if (old) {
module_put(old->owner);
/* 将被替换的老设备设置到CLOCK_EVT_STATE_DETACHED状态 */
clockevents_switch_state(old, CLOCK_EVT_STATE_DETACHED);
/* 将被替换的老设备从clockevent_devices全局链表中删除 */
list_del(&old->list);
/* 将被替换的老设备加入到clockevents_released全局链表中 */
list_add(&old->list, &clockevents_released);
}
if (new) {
/* 替换的新设备必须处于CLOCK_EVT_STATE_DETACHED状态 */
BUG_ON(!clockevent_state_detached(new));
/* 将替换的新设备关闭 */
clockevents_shutdown(new);
}
}
值得注意的是,这个函数是在本地中断关闭并且获得自旋锁的情况下调用的。功能其实很简单,主要就是把被替换的老设备从原有的clockevent_devices全局链表中删除,并加入clockevents_released全局链表中,于此同时,把新替换的设备加入clockevent_devices全局链表中,当然还要更新设备的状态。新加入的设备的初始状态必须是CLOCK_EVT_STATE_DETACHED。
如果驱动程序发现了系统中的一个新的定时事件设备,它将会构造一个clock_event_device结构体数据,相应的填写好结构体内的各个字段,然后向时间子系统注册。注册的函数是clockevents_config_and_register或clockevents_register_device。
CLOCK_EVT_STATE_DETACHEDclockevents_config_and_register根据参数,对clock_event_device进行设置后,还是直接调用clockevents_register_device函数(代码位于kernel/time/clockevents.c):
void clockevents_config_and_register(struct clock_event_device *dev,
u32 freq, unsigned long min_delta,
unsigned long max_delta)
{
dev->min_delta_ticks = min_delta;
dev->max_delta_ticks = max_delta;
clockevents_config(dev, freq);
clockevents_register_device(dev);
}
函数clockevents_config主要用来设置对应的mult和shift的值:
static void clockevents_config(struct clock_event_device *dev, u32 freq)
{
u64 sec;
/* 如果不是单触发的定时时间设备则直接返回 */
if (!(dev->features & CLOCK_EVT_FEAT_ONESHOT))
return;
/* 根据max_delta_ticks计算定时事件设备支持的最大秒数 */
sec = dev->max_delta_ticks;
do_div(sec, freq);
if (!sec)
sec = 1;
else if (sec > 600 && dev->max_delta_ticks > UINT_MAX)
sec = 600;
/* 根据频率和最大秒数计算并更新mult和shift的值 */
clockevents_calc_mult_shift(dev, freq, sec);
/* 根据min_delta_ticks计算min_delta_ns */
dev->min_delta_ns = cev_delta2ns(dev->min_delta_ticks, dev, false);
/* 根据max_delta_ticks计算max_delta_ns */
dev->max_delta_ns = cev_delta2ns(dev->max_delta_ticks, dev, true);
}
在调用了clockevents_config后就马上调用clockevents_register_device了:
void clockevents_register_device(struct clock_event_device *dev)
{
unsigned long flags;
/* 将待注册设备的状态设置成CLOCK_EVT_STATE_DETACHED */
clockevent_set_state(dev, CLOCK_EVT_STATE_DETACHED);
/* 检查并修正该设备的cpumask */
if (!dev->cpumask) {
WARN_ON(num_possible_cpus() > 1);
dev->cpumask = cpumask_of(smp_processor_id());
}
if (dev->cpumask == cpu_all_mask) {
WARN(1, "%s cpumask == cpu_all_mask, using cpu_possible_mask instead\n",
dev->name);
dev->cpumask = cpu_possible_mask;
}
/* 持有自旋锁并关本地中断 */
raw_spin_lock_irqsave(&clockevents_lock, flags);
/* 将本定时事件设备加入全局链表 */
list_add(&dev->list, &clockevent_devices);
/* 检查该定时事件设备是否可以替换原设备成为新的tick设备 */
tick_check_new_device(dev);
clockevents_notify_released();
/* 释放自旋锁并打开本地中断 */
raw_spin_unlock_irqrestore(&clockevents_lock, flags);
}
EXPORT_SYMBOL_GPL(clockevents_register_device);
函数会对传入设备的cpumask变量进行修正。如果cpumask没有设置,这会将其设置成当前正在运行程序的这个CPU,即将这个设备占为己有了,并且系统中不止一个CPU的话还会报警告。
tick_check_new_device是一个tick设备层提供的函数,如果有新的定时事件设备加入内核,则可以将新加的这个设备和原有的设备进行比较,看哪个更适合作为tick设备层的驱动设备。如果新设备更时候的话,tick设备层会调用前面分析的clockevents_exchange_device函数。接着,在从tick设备层返回后,会调用clockevents_notify_released函数:
static void clockevents_notify_released(void)
{
struct clock_event_device *dev;
/* 循环遍历全局变量clockevents_released中的所有链表元素 */
while (!list_empty(&clockevents_released)) {
dev = list_entry(clockevents_released.next,
struct clock_event_device, list);
/* 将该设备从clockevents_released链表中删除 */
list_del(&dev->list);
/* 将该设备重新加入clockevent_devices全局链表中 */
list_add(&dev->list, &clockevent_devices);
/* 检查该设备是否可以替换当前的设备成为tick设备 */
tick_check_new_device(dev);
}
}
这个函数会遍历前一步添加到clockevents_released全局链表中的所有设备(在注册的过程中实际只会添加一个,也就是被替换的设备),将其从clockevents_released中删除并重新添加回clockevent_devices全局链表中,再检查一下这个设备是否是更好的tick设备(在这个场景中,被替换的设备肯定不如替换的新设备,所以其实这个调用应该不起作用)。
当定时事件设备的状态有变化时,比如频率变动了,或者当定时到期且,需要设置下一次定时事件的时候,都有可能会对定时事件设备重新进行编程。如果频率变化了,那同样的纳秒数转换成的周期数就肯定会改变,当然需要重新计算编程。而定时事件到期后,且定时事件设备是单触发模式的,如果不对其再编程,那这个设备将不会再产生任何定时中断。
对定时事件设备重编程是在函数clockevents_program_event中完成的:
int clockevents_program_event(struct clock_event_device *dev, ktime_t expires,
bool force)
{
unsigned long long clc;
int64_t delta;
int rc;
if (WARN_ON_ONCE(expires < 0))
return -ETIME;
/* 储存下一次定时到期时间 */
dev->next_event = expires;
/* 先关闭该设备 */
if (clockevent_state_shutdown(dev))
return 0;
/* 定时事件设备必须处于CLOCK_EVT_STATE_ONESHOT状态 */
WARN_ONCE(!clockevent_state_oneshot(dev), "Current state: %d\n",
clockevent_get_state(dev));
/* 如果这个设备要用绝对到期时间设置 */
if (dev->features & CLOCK_EVT_FEAT_KTIME)
return dev->set_next_ktime(expires, dev);
/* 计算到期时间和当前时间之间差多少纳秒 */
delta = ktime_to_ns(ktime_sub(expires, ktime_get()));
/* 如果当前时间已经超过到期时间了 */
if (delta <= 0)
return force ? clockevents_program_min_delta(dev) : -ETIME;
/* 设置的时间间隔必须大于min_delta_ns且小于max_delta_ns */
delta = min(delta, (int64_t) dev->max_delta_ns);
delta = max(delta, (int64_t) dev->min_delta_ns);
/* 将纳秒值转成时钟源周期数 */
clc = ((unsigned long long) delta * dev->mult) >> dev->shift;
rc = dev->set_next_event((unsigned long) clc, dev);
return (rc && force) ? clockevents_program_min_delta(dev) : rc;
}
这个函数有三个参数,dev表示要重新编程的定时事件设备;expires表示要设定的下一次到期时间,以ktime表示;force表示如果这个定时事件设置出了问题,是不是需要尝试用最小的时间间隔设定该设备。可以看到,如果当前时间已经超过了要设定的到期时间,或者在调用set_next_event出错时,且force是真的情况下,还会尝试调用clockevents_program_min_delta设置一个最小的到期事件,否则直接返回错误。
static int clockevents_program_min_delta(struct clock_event_device *dev)
{
unsigned long long clc;
int64_t delta = 0;
int i;
/* 共尝试10次 */
for (i = 0; i < 10; i++) {
/* 每次加上定时事件设备允许的最小事件间隔 */
delta += dev->min_delta_ns;
dev->next_event = ktime_add_ns(ktime_get(), delta);
if (clockevent_state_shutdown(dev))
return 0;
dev->retries++;
/* 将纳秒数转换为时钟周期数 */
clc = ((unsigned long long) delta * dev->mult) >> dev->shift;
if (dev->set_next_event((unsigned long) clc, dev) == 0)
return 0;
}
return -ETIME;
}
这个函数非常简单,共尝试10次设置下一次到期事件,每次将间隔递增min_delta_ns,直到成功为止。如果10次都不成功,则返回错误码退出。设备结构体中的retries变量在这里记录尝试了多少次。
定时事件设备会在sysfs中注册对应的文件,可以通过访问这些文件的内容知道当前系统中关于定时事件设备的基本信息。
注册sysfs是在clockevents_init_sysfs函数中完成的:
static int __init clockevents_init_sysfs(void)
{
/* 注册子系统 */
int err = subsys_system_register(&clockevents_subsys, NULL);
if (!err)
err = tick_init_sysfs();
return err;
}
device_initcall(clockevents_init_sysfs);
subsys_system_register函数会根据参数将一个子系统注册在/sys/devices/system/目录下。
注册信息保存在clockevents_subsys静态全局变量中:
static struct bus_type clockevents_subsys = {
.name = "clockevents",
.dev_name = "clockevent",
};
所以总线名字叫做“clockevents”,而设备名字叫做“clockevent”。
注册完子系统后,如果没问题,会接着调用tick_init_sysfs函数:
static int __init tick_init_sysfs(void)
{
int cpu;
/* 遍历系统中的每个CPU */
for_each_possible_cpu(cpu) {
/* 读取每CPU变量tick_percpu_dev */
struct device *dev = &per_cpu(tick_percpu_dev, cpu);
int err;
/* 填写要注册的设备信息 */
dev->id = cpu;
dev->bus = &clockevents_subsys;
/* 注册设备 */
err = device_register(dev);
if (!err)
/* 在设备目录下创建current_device文件 */
err = device_create_file(dev, &dev_attr_current_device);
if (!err)
/* 在设备目录下创建unbind_device文件 */
err = device_create_file(dev, &dev_attr_unbind_device);
if (err)
return err;
}
return tick_broadcast_init_sysfs();
}
经过这些函数的注册后,将会在/sys/devices/system/clockevents/目录下创建多个目录,系统中有几个CPU(包含超线程)就会创建几个目录,例如笔者的笔记本是4核8线程的,就会创建clockevent0到clockevent7,共8个目录。每个目录下会创建两个文件,分别是current_device和unbind_device。以current_device为例,其文件属性定义为:
static ssize_t sysfs_show_current_tick_dev(struct device *dev,
struct device_attribute *attr,
char *buf)
{
struct tick_device *td;
ssize_t count = 0;
/* 获得自旋锁并关闭本地中断 */
raw_spin_lock_irq(&clockevents_lock);
/* 获得当前tick设备所使用的定时事件设备 */
td = tick_get_tick_dev(dev);
if (td && td->evtdev)
/* 输出定时事件设备的名字 */
count = snprintf(buf, PAGE_SIZE, "%s\n", td->evtdev->name);
/* 释放自旋锁并打开本地中断 */
raw_spin_unlock_irq(&clockevents_lock);
return count;
}
/* 申明了dev_attr_current_device全局变量 */
static DEVICE_ATTR(current_device, 0444, sysfs_show_current_tick_dev, NULL);
所以,访问了对应目录下的current_device文件,其内容将是对应CPU所使用的定时事件设备的名字。
例如,在64位树莓派4系统下,访问/sys/devices/system/clockevents/clockevent3/current_device将会返回arch_sys_timer,表明其当前使用的是Arm通用计时器。