1、Linux时钟框架
[^DroidPhoneo]
上图是linux时钟框架一个经典的描述。本质上linux各种时钟架构和服务是基于硬件提供的两种timer而构建的。
1、定时Timer
- 这类timer每个cpu都有一个独立的,称为local timer。这类timer的中断一般都是PPI(Private Peripheral Interrupt)类型,即每个cpu都有独立一份中断。 与PPI对应的是SPI(Shared Peripheral Interrupt,即多个cpu共享同一个中断。
- 这类timer一般是32bit宽度count,最重要的它会频繁的溢出并产生timer到期中断。
- 这类timer服务于tick timer(低精度)或者hrtimer(高精度)。
- 低精度模式,local timer工作在PERIODIC模式。即timer以tick时间(1/HZ)周期性的产生中断。在tick timer中处理任务调度tick、低精度timer、其他时间更新和统计profile。在这种模式下,所有利用时间的进行的运算,精度都是以tick(1/HZ)为单位的,精度较低。比如HZ=1000,那么tick=1ms。
- 高精度模式,local timer工作在ONESHOT模式。即系统可以支持hrtimer(high resolution)高精度timer,精度为local timer的计数clk达到ns级别。这种情况下把tick timer也转换成一种hrtimer。
2、时间戳Timer
- 这类timer一个系统多个cpu共享一个,称为global timer。
- 这类timer一般是32bit/64bit宽度count,一般不会溢出产生中断,系统实时的去读取count的值来计算当前的时间戳。
- 这类timer服务于clocksource/timekeeper。
本文的代码分析基于linux kernel 4.4.22,最好的学习方法还是”RTFSC”
1.1、Exynos MCT(Multi-Core Timer)
我们以samsung exynos架构为例来说明linux对timer的使用。
从上图可以看到,exynos有1个64bit global timer用来做时间戳timer,有8个31bit localtimer用来做定时timer,每个cpu拥有一个localtimer。
上图是exynos driver的初始化流程,mct_init_dt()中包含了主要的初始化流程:
1 | static void __init mct_init_dt(struct device_node *np, unsigned int int_type) |
后面结合clocksource和clockevent的子系统的解析,再来详细描述exynos系统的具体实现。
2、clocksource & timekeeper
上图描述的是clocksource和timekeeper的关系:
- 一个global timer对应注册一个clocksource。
- 一个系统中可以有多个clocksource,timekeeper选择精度最高的那个来使用。
- 用户使用timekeeper提供的接口来获取系统的时间戳。
- 为了避免无人主动获取时间clocksource定时器的溢出,timekeeper需要定期的去获取clocksource的值来更新系统时间,一般是在tick处理中更新。
2.1、clocksource
下面来看一看clocksource的定义:
1 | static struct clocksource mct_frc = { |
看一下clocksource的注册过程:
1 | static void __init exynos4_clocksource_init(void) |
2.1.1、exynos4_clocksource_init()
exynos将global timer注册成clocksource,虽然global timer拥有64bit的位宽,但是注册的时候把其当成32bit的clocksource注册。
1 | static u32 notrace exynos4_read_count_32(void) |
2.2、timekeeper
timerkeeper提供了几种时间:xtime、monotonic time、raw monotonic time、boot time。
- xtime 即是wall time,和RTC时间一样可以表示当前的时刻,它的起始时间是公元0世纪0秒,精度大于RTC时间;
- monotonic time 从系统开机后到现在的累计时间,不过不计算系统休眠的时间;
- raw monotonic time 和monotonic time含义一样,不过更纯粹,不会受到NTP时间调整的影响;
- boot time 在monotonic time的基础上加上了系统休眠的时间,它代表着系统上电后的总时间。
时间种类 | 精度(统计单位) | 访问速度 | 累计休眠时间 | 受NTP调整的影响 | 获取函数 |
---|---|---|---|---|---|
RTC | 低 | 慢 | Yes | Yes | |
xtime | 高 | 快 | Yes | Yes | do_gettimeofday()、ktime_get_real_ts()、ktime_get_real() |
monotonic | 高 | 快 | No | Yes | ktime_get()、ktime_get_ts64() |
raw monotonic | 高 | 快 | No | No | ktime_get_raw()、getrawmonotonic64() |
boot time | 高 | 快 | Yes | Yes | ktime_get_boottime() |
2.2.1、timekeeper的定义
虽然clocksource定时器只有一个,但是timekeeper提供了xtime、monotonic time、raw time、boot time等几种时间,所以timekeeper结构体中定义了多个变量来记住这些差值。
1 | /** |
2.2.2、timekeeper的初始化
timekeeper在初始化的过程中,读取当前的RTC值和clocksource的值,来初始化xtime、monotonic time、raw time、boot time,以及各种offset。
1 | void __init timekeeping_init(void) |
timekeeper原理上的初始化是在timekeeping_init()函数中完成的,但是read_persistent_clock64()、read_boot_clock64()都是空函数,所以实际上的初始化是另外的路径:rtc_hctosys() -> do_settimeofday64(),rtc初始化的时候重新配置timekeeper。
1 | static int __init rtc_hctosys(void) |
2.2.3、timekeeper的update
clocksource定时器的值要定时的读出来,并且把增量加到timekeeper中,不然clocksource定时器会溢出。这个定时更新的时间一般是1 tick,调用的函数是update_wall_time():
1 | void update_wall_time(void) |
2.2.4、timekeeper的获取
- xtime/wall time 的获取:
do_gettimeofday()、ktime_get_real_ts()最后调用的getnstimeofday64() -> __getnstimeofday64()获取到xtime:
1 | int __getnstimeofday64(struct timespec64 *ts) |
ktime_get_real()使用monotonic time再加上差值timekeeper.offs_real的方法来获取xtime:
1 | static inline ktime_t ktime_get_real(void) |
- monotonic time 的获取;
ktime_get()直接获取monotonic time:
1 | ktime_t ktime_get(void) |
ktime_get_ts64()通过xtime加上差值tk->wall_to_monotonic的方法来获取monotonic time:
1 | void ktime_get_ts64(struct timespec64 *ts) |
- raw monotonic time 的获取;
ktime_get_raw()通过tk->tkr_raw.base获取raw monotonic time:
1 | ktime_t ktime_get_raw(void) |
getrawmonotonic64()通过tk->raw_time获取raw monotonic time:
1 | void getrawmonotonic64(struct timespec64 *ts) |
- boot time 的获取;
ktime_get_boottime()使用monotonic time再加上差值timekeeper.offs_boot的方法来获取boot time:
1 | static inline ktime_t ktime_get_boottime(void) |
2.2.5、timekeeper suspend
系统在进入suspend以后,clocksource不会再工作,这部分时间会计入xtime和boot time,但是不会计入monotonic time。
1 | void timekeeping_resume(void) |
和初始化一样的原因,理论上timekeeper的操作在timekeeping_resume()、timekeeping_suspend(),但是实际上在rtc的操作中执行rtc_suspend()、rtc_resume()。
1 | static int rtc_suspend(struct device *dev) |
3、clock_event
clock_event其实就是对local timer的使用,每个cpu对应一个本地local timer。global timer启动后不需要主动做任何事情,只需要等待timekepper的读取就可以了。而local timer需要触发中断,它的主要价值就体现在定时中断处理了,中断的时间可以是固定的(period mode)也或者是不固定的(oneshot mode)。
3.1、clock_event的注册
3.1.1、exynos clock_event的注册
exynos clock_event的注册分为两部分:
- 第一部分:localtimer中断的注册:
1 | static void __init exynos4_timer_resources(struct device_node *np, void __iomem *base) |
- 第二部分:clock_event_device注册:
1 | static int exynos4_local_timer_setup(struct mct_clock_event_device *mevt) |
3.1.2、clock_event_device的注册
我们来分析一下clock_event_device的注册过程。
1 | void clockevents_config_and_register(struct clock_event_device *dev, |
3.2、tick_device的period mode
接上节,在cpu第一次注册clock_event_deviced的时候,td->mode默认被设置成period模式。event_handler会被初始化成tick_handle_periodic:
1 | void tick_setup_periodic(struct clock_event_device *dev, int broadcast) |
仔细分析一下tick_handle_periodic:
1 | void tick_handle_periodic(struct clock_event_device *dev) |
3.3、运行Mode
关于mode,有几个结构涉及到:tick_device、clock_event_device、tick_sched、hrtimer_cpu_base、。组合起来有以下几种情况:
tick_device | clock_event_device | tick_sched | hrtimer_cpu_base | 模式说明 | 切换路径 | handler处理路径 | ||
---|---|---|---|---|---|---|---|---|
成员 | ->mode | ->state_use_accessors | ->nohz_mode | ->hres_active | — | — | — | |
情况1 | TICKDEV_MODE_PERIODIC | CLOCK_EVT_STATE_PERIODIC | NOHZ_MODE_INACTIVE | 0 | td=period模式, dev=period模式, hrtimer=low res, noHz=dis | 初始状态 | tick_handle_periodic() -> tick_periodic() -> update_process_times() -> run_local_timers()、scheduler_tick() | |
情况2 | TICKDEV_MODE_PERIODIC | CLOCK_EVT_STATE_ONESHOT | NOHZ_MODE_INACTIVE | 0 | td=period模式, dev=oneshot模式, hrtimer=low res, noHz=dis | 初始状态 | tick_handle_periodic() -> tick_periodic() -> update_process_times() -> run_local_timers()、scheduler_tick() | |
情况3 | TICKDEV_MODE_ONESHOT | CLOCK_EVT_STATE_ONESHOT | NOHZ_MODE_LOWRES | 0 | td=oneshot模式, dev=oneshot模式, hrtimer=low res, noHz=en | tick_handle_periodic() -> tick_periodic() -> update_process_times() -> run_local_timers() -> hrtimer_run_queues() -> tick_check_oneshot_change() -> tick_nohz_switch_to_nohz() | tick_nohz_handler() -> tick_sched_handle() -> update_process_times() | |
情况4 | TICKDEV_MODE_ONESHOT | CLOCK_EVT_STATE_ONESHOT | NOHZ_MODE_HIGHRES | 1 | td=oneshot模式, dev=oneshot模式, hrtimer=high res, noHz=en | update_process_times() -> run_local_timers() -> hrtimer_run_queues() -> hrtimer_switch_to_hres() | hrtimer_interrupt() -> __hrtimer_run_queues() -> ts->sched_timer() -> tick_sched_timer() -> tick_sched_handle() -> update_process_times() |
tick_device | clock_event_device | tick_sched | hrtimer_cpu_base | 模式说明 | 切换路径 | handler处理路径 | |
成员 | ->mode | ->state_use_accessors | ->nohz_mode | ->hres_active | |||
情况1 | TICKDEV_MODE_PERIODIC | CLOCK_EVT_STATE_PERIODIC | NOHZ_MODE_INACTIVE | 0 | td=period模式, dev=period模式, hrtimer=low res, noHz=dis | 初始状态 | tick_handle_periodic() -> tick_periodic() -> update_process_times() -> run_local_timers()、scheduler_tick() |
情况2 | TICKDEV_MODE_PERIODIC | CLOCK_EVT_STATE_ONESHOT | NOHZ_MODE_INACTIVE | 0 | td=period模式, dev=oneshot模式, hrtimer=low res, noHz=dis | 初始状态 | tick_handle_periodic() -> tick_periodic() -> update_process_times() -> run_local_timers()、scheduler_tick() |
情况3 | TICKDEV_MODE_ONESHOT | CLOCK_EVT_STATE_ONESHOT | NOHZ_MODE_LOWRES | 0 | td=period模式, dev=oneshot模式, hrtimer=low res, noHz=en | tick_handle_periodic() -> tick_periodic() -> update_process_times() -> run_local_timers() -> hrtimer_run_queues() -> tick_check_oneshot_change() -> tick_nohz_switch_to_nohz() | tick_nohz_handler() -> tick_sched_handle() -> update_process_times() |
情况4 | TICKDEV_MODE_ONESHOT | CLOCK_EVT_STATE_ONESHOT | NOHZ_MODE_HIGHRES | 1 | td=period模式, dev=oneshot模式, hrtimer=high res, noHz=en | update_process_times() -> run_local_timers() -> hrtimer_run_queues() -> hrtimer_switch_to_hres() | hrtimer_interrupt() -> __hrtimer_run_queues() -> ts->sched_timer() -> tick_sched_timer() -> tick_sched_handle() -> update_process_times() |
其实归结起来就3种mode:NOHZ_MODE_INACTIVE、NOHZ_MODE_LOWRES、NOHZ_MODE_HIGHRES。下面来逐个解析一下。
3.3.1、NOHZ_MODE_INACTIVE
NOHZ_MODE_INACTIVE就是系统初始化时的状态:“td=period模式, dev=period/oneshot模式, hrtimer=low res, noHz=dis”。
NOHZ_MODE_INACTIVE模式:
- tick_device工作在period模式,HW local timer工作在period/oneshot模式;
- noHZ没有使能,进入idle会被tick timer中断打断;
- hrtimer工作在低精度模式,和低精度定时器(SW local timer)的精度一样,都是基于tick的;
3.3.2、NOHZ_MODE_LOWRES
在系统的运行过程中系统尝试进入精度更高的模式,如果noHZ可以使能,但是hrtimer高精度不能使能,即进入NOHZ_MODE_LOWRES模式:“td=period模式, dev=oneshot模式, hrtimer=low res, noHz=en”。
NOHZ_MODE_LOWRES模式:
- tick_device工作在oneshot模式,HW local timer工作在oneshot模式;
- noHZ使能,进入idle不会被tick timer中断打断;
- hrtimer工作在低精度模式,和低精度定时器(SW local timer)的精度一样,都是基于tick的;
为了支持noHZ,tick_device必须切换成oneshot模式,在进入idle时停掉tick timer(tick_nohz_idle_enter() -> __tick_nohz_idle_enter() -> tick_nohz_stop_sched_tick()),在离开idle时恢复tick timer(tick_nohz_idle_exit() -> tick_nohz_restart_sched_tick()),这样idle过程就不会被tick中断。就实现了noHZ模式(tickless)。
NOHZ_MODE_LOWRES模式下,没有进入idle时tick_device还是以固定周期工作的:
1 | static void tick_nohz_handler(struct clock_event_device *dev) |
3.3.3、NOHZ_MODE_HIGHRES
在系统的运行过程中系统尝试进入精度更高的模式,如果noHZ可以使能,hrtimer高精度可以使能,即进入NOHZ_MODE_HIGHRES模式:“td=period模式, dev=oneshot模式, hrtimer=high res, noHz=en”。
NOHZ_MODE_HIGHRES:
- tick_device工作在oneshot模式,HW local timer工作在oneshot模式;
- noHZ使能,进入idle不会被tick timer中断打断;
- hrtimer工作在高精度模式,和硬件定时器(HWlocal timer)的精度一样,远大于低精度定时器tick精度;
为了支持hrtimer的高精度模式,hrtimer必须直接使用tick_device的oneshot模式,而常规的tick timer转换成hrtimer的一个子timer。
上图是NOHZ_MODE_HIGHRES模式下,用ftrace抓取HW timer硬件中断和tick任务的执行情况:
- tick任务是以固定周期4ms固定执行的;
- 遇到tick任务超过4ms的间隔,这时就是进入了idle状态,且发生了noHZ(tickless);
- 硬件timer中断的发生周期是不固定的,是和hrtimer绑定的;
- 发生tick的时候肯定发生了timer硬中断,因为tick是其中一个hrtimer;
3.3.4、Mode切换
系统初始状态工作在NOHZ_MODE_INACTIVE模式时,会动态检测是否可以进入更高级别的模式NOHZ_MODE_LOWRES、NOHZ_MODE_HIGHRES。这个检测工作是在这个路径中做的:tick_device工作在period模式:tick_handle_periodic() -> tick_periodic() -> update_process_times() -> run_local_timers() -> hrtimer_run_queues()
1 | void hrtimer_run_queues(void) |
4、noHZ
系统在NOHZ_MODE_LOWRES、NOHZ_MODE_HIGHRES两种模式下支持noHZ。noHZ是一个功耗优化的feature,在系统负载比较轻的时候没有任务需要调度cpu会进入idle状态,但是系统的tick任务(update_process_times())默认会以固定周期执行,这种固定周期会打断idle状态让系统恢复成正常耗电状态。
tick任务这种不管有没有任务都是固定周期运行的特性是需要改进的,noHZ就是为了解决这一问题而产生的:如果在idle状态的过程中tick任务没有到期需要处理的低精度timer和高精度timer,tick任务可以继续保持睡眠,直到真正有timer到期。
idle进程的主要执行序列如下:
1 | static void cpu_idle_loop(void) |
可以看到,其中的关键在tick_nohz_idle_enter()/tick_nohz_idle_exit()函数。
4.1、tick_nohz_idle_enter/exit()
tick_nohz_idle_enter()的解析:
1 | void tick_nohz_idle_enter(void) |
tick_nohz_idle_exit()的解析:
1 | void tick_nohz_idle_exit(void) |
4.2 tick_nohz_irq_enter/exit()
因为在idle退出执行完本tick需要处理的timer后又需要重新关闭tick,系统设计了tick_nohz_irq_enter()/tick_nohz_irq_exit()来处理这种操作。在本次中断处理完timer后,在tick_nohz_irq_exit()中判断是否重新关闭tick任务。
1 | static void cpu_idle_loop(void) |
tick_nohz_irq_enter()/tick_nohz_irq_exit()的代码解析:
1 | static inline void tick_nohz_irq_enter(void) |
4.3、local timer时钟被关闭时的处理
还有一种情况需要考虑,在系统进入深层次的idle状态时,local timer本身的时钟可能会被关闭。比如MTK平台进入soidle状态时,local timer本身会被停止,这时会用一个GPT timer来替代local timer继续工作。
核心函数是timer_setting_before_wfi()/timer_setting_after_wfi():
- timer_setting_before_wfi()在进入idle前被调用,读出local timer的剩余值并配置到GPT timer中;
- timer_setting_after_wfi()在退出idle后被调用,读出GPT timer的值来重新恢复local timer;
1 | static void timer_setting_before_wfi(bool f26m_off) |
需要特别说明的是,这种GPT timer全局只有一个,进入soidle的状态时cpu也只有一个在线,所以能正常的工作。
5、hrtimer
5.1、hrtimer的组织
hrtimer的组织相对来说还是比较简单的,每个cpu对应一个hrtimer_cpu_base,每个hrtimer_cpu_base中有4类clock_base代表4种时间类型(HRTIMER_BASE_REALTIME、HRTIMER_BASE_MONOTONIC、HRTIMER_BASE_BOOTTIME、HRTIMER_BASE_TAI)的hrtimer,每个clock_base是以红黑树来组织同一类型的hrtimer的:
1 | DEFINE_PER_CPU(struct hrtimer_cpu_base, hrtimer_bases) = |
5.2、低精度模式(NOHZ_MODE_INACTIVE/NOHZ_MODE_LOWRES)
前面几章已经详细描述了执行路径,在低精度模式下hrtimer的实际精度和低精度定时器是一样的,都是基于tick精度的。他的执行路径如下。
NOHZ_MODE_INACTIVE模式:
1 | tick_handle_periodic() |
NOHZ_MODE_LOWRES模式:
1 | tick_nohz_handler() |
5.3、高精度模式(NOHZ_MODE_HIGHRES)
在高精度模式下hrtimer才能发挥出真正的精度,他的可以精确定时到小于一个tick,精度依赖于硬件local timer。
NOHZ_MODE_LOWRES模式:
1 | hrtimer_interrupt() |
6、低精度timer(lowres timer)
低精度timer在系统中的应用范围更广,若非特别声明是hrtimer其他都是使用低精度timer,类如schedule_timeout()、msleep()。他有以下特点:
- 精度低,以tick为单位计时;
- 执行上下文,低精度timer执行时是在softirq中,而hrtimer的实际执行是在中断当中。所以低精度的执行精度更小于hrtimer;
- 对系统的实时影响小,softirq比irq对系统的实时性影响更小;
6.1、低精度timer的组织
低精度timer的组织形式和hrtimer类似,只是timer的链接不是采用红黑树,而是采用tv1 - tv5等一系列的链表。
tv1 - tv5中保留着一系列槽位,每个槽位代表一个超时时间,把相同超时时间的低精度timer链接到同一槽位当中。
6.2、低精度timer的执行路径
低精度timer的实际执行时在softirq中执行的,在中断中的动作只是简单触发softirq。
中断中:
1 | tick_handle_periodic()/tick_nohz_handler()/hrtimer_interrupt() |
软中断中:
1 | run_timer_softirq() |
参考资料
[^DroidPhoneo]: Linux 时间子系统
[^wowo]: wowotech time subsystem