ARM64 芯片的 Jiffies 更新流程
最近在调试 arm64 机器时遇到了一个比较蛋疼的时钟问题,这个时钟问题会导致在部分机器类型上导致无法启动,为了深入了解并解决掉这个问题,特定决定研究一下整个 jiffies 的更新逻辑过程,本篇文章写于 site 上传之前的半年前,所以可能存在某些纰漏,检查也不够细致但是希望能够为后来者多少提供 jiffies 的一个基本的逻辑架构描述,让大家少走弯路,针对于别的平台,jiffies 的更新也相差无几,可以举一反三。
Jiffies 的更新
内核的 tick 相当于人的心脏,他随着 cpu 的运转会不停的增加,然后触发各类事件,jiffies 的更新也是基于内核的 tick 子系统。文件kernel/time/timekeeping.c
中定义了 jiffies_641 的增加过程。
void do_timer(unsigned long ticks)
{
jiffies_64 += ticks;
calc_global_load(ticks);
}
从函数可以很清晰的知道,这个 jiffies_64 的值与 ticks 息息相关,所以,我们需要了解 ticks 是否正确增加了。首先,需要确认在何处调用了这个函数,文件kernel/time/tick-common.c
就给了答案:
static void tick_periodic(int cpu)
{
if (tick_do_timer_cpu == cpu) {
write_seqlock(&jiffies_lock);
/* Keep track of the next tick event */
tick_next_period = ktime_add(tick_next_period, tick_period);
do_timer(1);
write_sequnlock(&jiffies_lock);
update_wall_time();
}
update_process_times(user_mode(get_irq_regs()));
profile_tick(CPU_PROFILING);
}
函数定义告诉我们,每一次调用tick_periodic
,且处理定时器的 cpu 就是当前 cpu 的话,那么定时器就自增一次。划重点,真的只增加 1。既然知道是在这个位置开始增加 jiffies 值,那么谁又在调用这个函数呢?其实就在当前文件的函数:
void tick_handle_periodic(struct clock_event_device *dev)
{
int cpu = smp_processor_id();
ktime_t next = dev->next_event;
tick_periodic(cpu);
#if defined(CONFIG_HIGH_RES_TIMERS) || defined(CONFIG_NO_HZ_COMMON)
if (dev->event_handler != tick_handle_periodic)
return;
#endif
if (!clockevent_state_oneshot(dev))
return;
for (;;) {
next = ktime_add(next, tick_period);
if (!clockevents_program_event(dev, next, false))
return;
if (timekeeping_valid_for_hres())
tick_periodic(cpu);
}
}
从函数的名称可以看出来,这肯定是一个回调函数,而且是一个 clock event 的回调函数,找到文件kernel/time/tick-broadcast.c
,里面定义了回调函数所设置的位置。
void tick_set_periodic_handler(struct clock_event_device *dev, int broadcast)
{
if (!broadcast)
dev->event_handler = tick_handle_periodic;
else
dev->event_handler = tick_handle_periodic_broadcast;
}
可以很清楚的看出来,对时钟事件有两种处理方式,一种是广播模式2,一种是一般模式,也就是我们最开始追踪到的 jiffies 会增加的那条线路。文件kernel/time/tick-common.c
有对上面函数的调用关系。
void tick_setup_periodic(struct clock_event_device *dev, int broadcast)
{
tick_set_periodic_handler(dev, broadcast);
...
}
static void tick_setup_device(struct tick_device *td,
struct clock_event_device *newdev, int cpu,
const struct cpumask *cpumask)
{
...
if (td->mode == TICKDEV_MODE_PERIODIC)
tick_setup_periodic(newdev, 0);
else
tick_setup_oneshot(newdev, handler, next_event);
else
tick_setup_oneshot(newdev, handler, next_event);
}
void tick_check_new_device(struct clock_event_device *newdev)
{
...
clockevents_exchange_device(curdev, newdev);
tick_setup_device(td, newdev, cpu, cpumask_of(cpu));
if (newdev->features & CLOCK_EVT_FEAT_ONESHOT)
tick_oneshot_notify();
return;
}
上面的三个函数给出了最顶层的 tick 注册时,设置的一些调用关系,当 tick_device 的模式为 TICKDEV_MODE_PERIODIC
时,会继续往下深入直到设置 jiffies 等等其他的数值。沿着调用信息继续往上层找,在kernel/time/clockevents.c
中:
void clockevents_register_device(struct clock_event_device *dev)
{
unsigned long flags;
/* Initialize state to DETACHED */
clockevent_set_state(dev, CLOCK_EVT_STATE_DETACHED);
if (!dev->cpumask) {
WARN_ON(num_possible_cpus() > 1);
dev->cpumask = cpumask_of(smp_processor_id());
}
raw_spin_lock_irqsave(&clockevents_lock, flags);
list_add(&dev->list, &clockevent_devices);
tick_check_new_device(dev);
clockevents_notify_released();
raw_spin_unlock_irqrestore(&clockevents_lock, flags);
}
EXPORT_SYMBOL_GPL(clockevents_register_device);
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);
}
EXPORT_SYMBOL_GPL(clockevents_config_and_register);
上面的两个函数就是对 clockevent 事件的注册以及配置,其实注册流程所有的平台都是一致的,重要的是配置不一样,譬如 arm64 平台的配置信息就在drivers/clocksource/arm_arch_timer.c
中,这里充斥了大量的对时钟的设置,所有的配置信息都是从 dtb 中读取得到的,我们一一来进行剖析。clockevents_config_and_register
函数的堆栈信息很容易得到:
static void __arch_timer_setup(unsigned type,
struct clock_event_device *clk)
{
...
clk->set_state_shutdown(clk);
clockevents_config_and_register(clk, arch_timer_rate, 0xf, 0x7fffffff);
}
static int arch_timer_setup(struct clock_event_device *clk)
{
__arch_timer_setup(ARCH_CP15_TIMER, clk);
...
return 0;
}
static int __init arch_timer_register(void)
{
...
/* Immediately configure the timer on the boot CPU */
arch_timer_setup(this_cpu_ptr(arch_timer_evt));
return 0;
}
static void __init arch_timer_init(void)
{
...
arch_timer_register();
arch_timer_common_init();
}
static void __init arch_timer_of_init(struct device_node *np)
{
...
arch_timer_init();
}
到此,整个定时器堆栈已经展示完毕,由 arch_timer_of_init
开始,逐级的对整个定时器机制进行初始化以及设置。那么在什么位置又开始执行这个arch_timer_of_init
函数呢?很简单,在同一个文件中的 arch_timer_of_init 后面就有定义:
CLOCKSOURCE_OF_DECLARE(armv7_arch_timer, "arm,armv7-timer", arch_timer_of_init);
CLOCKSOURCE_OF_DECLARE(armv8_arch_timer, "arm,armv8-timer", arch_timer_of_init);
这个CLOCKSOURCE_OF_DECLARE
宏定义代表定义一个表项,意思很明显,定义一个这样的表项内核就会根据这样的表项注册一个内核定时器。注册的入口就是 arch_timer_of_init,通过接口一层层的设置回调函数与接口,逐级的初始化以及配置定时器。这个表项具体是如何被调用和关联起来的呢?其实用到了一点点的技巧,这就是宏CLOCKSOURCE_OF_DECLARE
的展开,你可以看到有个__clksrc_of_table
字段,这个很关键。
#define CLOCKSOURCE_OF_DECLARE(name, compat, fn) \
static const struct of_device_id __clksrc_of_table_##name \
__used __section(__clksrc_of_table) \
= { .compatible = compat, \
.data = (fn == (clocksource_of_init_fn)NULL) ? fn : fn }
可以看到,字段__clksrc_of_table
是保存在一个 section 中的,是一个独立的段,这利用到了ELF
文件格式的一些特点,让这个字段能够保存在二进制文件格式的特殊段落中,被永久保存起来,这个是在编译结束之后就确定了的,就像存在那里的一段可执行代码指令。有了表项的定义,那么自然需要有地方可以解析这个表项,很显然,文件drivers/clocksource/clksrc-probe.c
的函数clocksource_probe
专门来做这个事情。
void __init clocksource_probe(void)
{
struct device_node *np;
const struct of_device_id *match;
of_init_fn_1 init_func;
unsigned clocksources = 0;
for_each_matching_node_and_match(np, __clksrc_of_table, &match) {
if (!of_device_is_available(np))
continue;
init_func = match->data;
init_func(np); /* <==== 看这里 */
clocksources++;
}
clocksources += acpi_probe_device_table(clksrc);
if (!clocksources)
pr_crit("%s: no matching clocksources found\n", __func__);
}
通过函数init_func(np)
就这么调用过去了,然后就是到了表项,然后就是arch_timer_of_init
,最后就是一系列的初始化等等。好吧,还有最后一个问题,谁调用的clocksource_probe
,在 start_kernel 里面不会专门调用这个函数,那么又是谁呢?继续跟踪。好吧,就在这里arch/arm64/kernel/time.c
void __init time_init(void)
{
u32 arch_timer_rate;
of_clk_init(NULL);
clocksource_probe();
tick_setup_hrtimer_broadcast();
arch_timer_rate = arch_timer_get_rate();
if (!arch_timer_rate)
panic("Unable to initialise architected timer.\n");
/* Calibrate the delay loop directly */
lpj_fine = arch_timer_rate / HZ;
}
到这里一切自不必再说,start_kernel 的时候会主动调用 timer_init 的函数进行个架构的定时器初始化,然后自文档逆序结构的函数一步步初始化到最后的 jiffies 更新,整个调用堆栈即是如此。
结束
至于为什么开机过程中无法获取到真正的 jiffies 值,其实从堆栈信息可以的出来,很早之前,内核的定时器各部分已经都准备好了,但是真正执行定时器的就是在中断上报时才会触发定时器的回调函数,那个时候才会正式的更新 jiffies,所以一旦中断无法正常的捅上去,那么自然定时器就无法工作3。
-
为什么是 jiffies_64 呢?因为在 arm64 平台直接将 jiffies 等于了 jiffies_64,详情可见
arch/arm64/kernel/vmlinux.lds.S
↩︎ - 时钟事件的广播模式暂时不深入理解,因为与当前追踪的 jiffies 暂时没有关系 ↩︎
- 当前调试的机器,定时器中断的确无法往上传递,导致 jiffies 无法正确更新,所以前期的 xor 测试程序无法通过;至于为什么系统起来之后又可以正确的 jiffies 增加呢?那是因为硬件上将其中的一个串口的中断转为了定时器中断,让他去调用定时器的中断处理函数,最后的结果就是,当前的定时器中断频率为 100HZ,而且前期是无法正确执行 jiffies 的 ↩︎