几乎所有的计算机系统中都会存在一个所谓的定时设备,经过设置后,在某个固定的时间或某个相对的时间间隔后,达到触发条件,发送中断给处理器。
系统中的每一种实际的定时事件设备都由一个叫做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”的,表明其是缓存行对齐的,频繁访问的话可以加快速度。
熟悉面向对象编程的一定觉得对这个结构体的定义非常熟悉,除了变量外还定义了一堆函数指针,并且每个函数的第一个参数是一个指向自己的指针。所以,每一个所谓的定时事件设备都是一个对象实例。
下面来说结构体中这些字段的具体意思:
- event_handler:这是一个回调函数,当定时器触发条件满足后,会发送中断给处理器,对应的中断处理程序在执行的时候会调用这个函数。
- set_next_event:设置下一次触发的时间,用经过了多少个时钟源的周期数作为参数。
- set_next_ktime:同样是设置下一次触发的时间,直接使用ktime时间作为参数。
- next_event:该定时事件设备的下一次到期绝对时间,用ktime表示。
- max_delta_ns:表示当前定时事件设备能分辨的最大定时时间间隔,以纳秒数表示。系统中的时钟源计数器一般都有一个最大计数值,超过这个值后就会回滚了,这也就是单次定时能设定的最大时间间隔。假如系统时钟源计数超过10分钟就会越界回滚,如果定时在10分钟内,那没关系,即使会越界系统回滚后也可以正确定时。而如果定时超过10分钟,那系统就无法区分到底是越界之前的值是对了还是越界之后的值是对的。所以,超过这个定时间隔系统一定会出错。这个值可以和max_delta_ticks通过mult和shift互相转换。
- min_delta_ns:表示当前定时事件设备能分辨的最小定时时间间隔,以纳秒数表示。系统中的时钟源一般都有一个最小分辨率,如果时钟源以10MHz运行,那么其最小的定时时间间隔肯定要大于100纳秒,小于这个定时间隔在这个系统上是无法实现的。这个值可以和min_delta_ticks通过mult和shift互相转换。
- mult和shift:系统中都会有一个时钟源(Clock Source),有的系统会将其称作计数器,它会按照一个固定的频率周期工作,不停的累加。注意区分时钟源和本文说的定时器,时钟源只是自顾自的累加,频率很高,让系统“感知”时间的流逝,它不会触发中断,计数器的值是CPU自己主动读取的;而定时器是会触发中断的,而且其定时间隔肯定比时钟源的周期间隔要大。内核可以通过不同渠道知道时钟源的频率(Frequency),也可以通过比较现在的时钟计数器数值和上一次时钟计数器数值获得已经过去了多少个周期(Cycle),有了这两个参数就可以知道过去了多少秒了(Cycle / Frequency)。但是,内核是没有浮点运算单元的,因此,只能通过整数运算进行模拟。mult表示乘数,shift表示位移多少位。这样,拿到了计数器的值后,先用shift左移位,然后再整数除以mult之后,就可以算出过了多少纳秒((Cycle << shift) / mult)。这两个值是需要精心计算了,如果太大了会造成溢出,如果太小了,会造成精度不够。关于如何计算这两个值,以及如何将周期数转换成纳秒数在后面章节中有介绍。
- state_use_accessors:表示当前定时事件设备所处的状态,是一个枚举变量,一共有五种。CLOCK_EVT_STATE_DETACHED,表示这个设备目前没有被内核事件子系统使用,也是设备的初始状态;CLOCK_EVT_STATE_SHUTDOWN,表示该设备已经被关闭了;CLOCK_EVT_STATE_PERIODIC,表示这个设备一旦设置完成后就可以产生周期性事件,一般都是低精度的设备;CLOCK_EVT_STATE_ONESHOT,表示该设备只能产生单次触发的时钟事件,一般都是高精度设备;CLOCK_EVT_STATE_ONESHOT_STOPPED,表示该设备是单次触发设备,但是已经被停止了(代码位于include/linux/clockchips.h)。
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,
};
- features:表示这个定时事件设备支持的功能特性。例如,如果是周期设备那就要包含CLOCK_EVT_FEAT_PERIODIC。不像前面说的clock_event_state是排它的,这个字段是按位与的,可以包含多个。但有些也是互斥的,比如CLOCK_EVT_FEAT_PERIODIC(表示该设备可以支持周期触发)和CLOCK_EVT_FEAT_ONESHOT(表示该设备只支持单次触发)一般不会在一起出现。CLOCK_EVT_FEAT_KTIME表示该设备只支持以ktime绝对时间定时,只能调用set_next_ktime函数。CLOCK_EVT_FEAT_C3STOP表示该定时事件设备支持C3_STOP工作模式,在对应的CPU进入空闲状态后,有可能会被关闭。CLOCK_EVT_FEAT_PERCPU表示该定时事件设备是某个CPU私有的。CLOCK_EVT_FEAT_DYNIRQ表示该定时事件设备可以设定CPU亲缘性,也就是可以指定到期后触发某个特定CPU的中断。CLOCK_EVT_FEAT_DUMMY表示这个定时事件设备是一个“假”的占位设备。CLOCK_EVT_FEAT_HRTIMER表示该定时事件设备实际上是有高分辨率定时器模拟出来的。
# 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
- retries:重试次数(在clockevents_program_min_delta函数中使用)。
- set_state_periodic:当定时事件设备将要被切换到周期触发状态(也就是CLOCK_EVT_STATE_PERIODIC)时,时间子系统会调用这个函数。
- set_state_oneshot:当定时事件设备将要被切换到单次触发状态(也就是CLOCK_EVT_STATE_ONESHOT)时,时间子系统会调用这个函数。
- set_state_oneshot_stopped:当定时事件设备将要被切换到单次触发停止状态(也就是CLOCK_EVT_STATE_ONESHOT_STOPPED)时,时间子系统会调用这个函数。
- set_state_shutdown:当定时事件设备将要被切换到关闭状态(也就是CLOCK_EVT_STATE_SHUTDOWN)时,时间子系统会调用这个函数。
- tick_resume:当一个tick设备恢复的时候,会调用对应的定时事件设备的该函数。
- broadcast:发送广播事件的函数。
- suspend:当要暂停定时事件设备时,会调用对应设备的该函数。
- resume:当要恢复定时事件设备时,会调用对应设备的该函数。
- min_delta_ticks:表示当前定时事件设备能分辨的最小定时时间间隔,以时钟源设备的周期数表示,肯定是一个大于1的值。
- max_delta_ticks:表示当前定时事件设备能分辨的最大定时时间间隔,以时钟源设备的周期数表示,肯定不能大于时钟源设备的最大计数器值。
- name:是给这个定时事件设备起的一个名字,一般比较直观,在/proc/timer_list中或者dmesg中都会出现。
- rating:代表这个定时事件设备的精度值,其取值范围从1到499,数字越大代表设备的精度越高。当系统中同时有多个定时事件设备存在的时候,内核可以根据这个值选一个最佳的设备。
- irq:指定了该定时事件设备使用的中断号。
- bound_on:绑定的CPU,主要在Tick广播层使用。
- cpumask:指定了这个定时事件设备所服务的CPU号,系统中高精度定时事件设备一般都是每个CPU核私有的。
- list:系统中所有的定时事件设备实例都会保存在全局链表clockevent_devices中,这个变量作为链表的元素(代码位于kernel/time/clockevents.c)。
static LIST_HEAD(clockevent_devices);
- owner:拥有这个定时事件设备的模块。
有了前面的知识准备了之后,下面我们分几个方面来解释一下时钟事件(Clock Events)层的工作过程。
1)mult、shift的计算和周期数到纳秒的转换
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。
2)更换当前定时事件设备
当有新的定时事件设备加入内核后,有可能会切换当前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。
3)定时事件设备的注册
如果驱动程序发现了系统中的一个新的定时事件设备,它将会构造一个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设备(在这个场景中,被替换的设备肯定不如替换的新设备,所以其实这个调用应该不起作用)。
4)对定时事件设备重编程
当定时事件设备的状态有变化时,比如频率变动了,或者当定时到期且,需要设置下一次定时事件的时候,都有可能会对定时事件设备重新进行编程。如果频率变化了,那同样的纳秒数转换成的周期数就肯定会改变,当然需要重新计算编程。而定时事件到期后,且定时事件设备是单触发模式的,如果不对其再编程,那这个设备将不会再产生任何定时中断。
对定时事件设备重编程是在函数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变量在这里记录尝试了多少次。
5)注册sysfs
定时事件设备会在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通用计时器。