linux 时间子系统
这篇草稿有了 4 年了吧,已经不是拖延症的问题了…
- 篇幅还不少,丢掉可惜,稍微整理后发.
- 因为当时动笔时属于应付 KPI,东拼西凑,无法保证内容准确性.
- 时间实在太长了,资料来源有很多缺失了,版权问题请联系我.
资料来源:
http://www.wowotech.net/timer_subsystem/time_concept.html
更新
1
2022.03.27 初始
导语
曾经以为对 linux 能刨根问底,于是轻率的进入了 linux 驱动/应用开发,最后却身心俱疲.个中原因,哎😔.
- 当时个人的基础并不能支撑想探寻的疑问.
- 接手的工作环境..那些遗留代码..已经不回想了..
- 兴趣被工作挟裹后,对当时的自己实在是个非常大的打击.
这篇草稿有了 3 年了吧,如今没有那个时间/精力重写,只能稍微整理后发出.
- 因为当时动笔时属于应付 KPI,东拼西凑,无法保证内容准确性.
- 因时间实在太久了,很多文献的出处也记不清楚了,因此有很多部分无法标注来源..
概述
背景
时间在 linux 系统的概念 or 内容
时间标准
一般接触到的时间基准有两个 UTC GMT
- UTC(Coordinated Universal Time),又称世界标准时间。是以原子时秒长为基础,在时刻上尽量接近于世界时的一种时间计量系统。这套时间系统被应用于许多互联网和万维网的标准中,linux系统中联网状态下同步时间即同步UTC时间。
- GMT (Greenwich Mean Time,GMT)格林尼治标准时间(Greenwich Mean Time,GMT)是指位于伦敦郊区的皇家格林尼治天文台的标准时间,因为本初子午线被定义在通过那里的经线。地球的椭圆轨道里的运动速度不均匀,这个时刻可能和实际的太阳时相差16分钟。格林尼治时间已经不再被作为标准时间使用。现在的标准时间——世界标准时间(UTC)——由原子钟提供,UTC是基于标准的GMT提供的准确时间。
北京时间= UTC + 8 = GMT + 8 ;
linux 时间精度
Linux系统中传统的时间精度单位为秒,但已远远满足不了需求,进而拓展到了微秒、纳秒级别。Linux系统中精度有4种表示,内核提供了不同类型的转换的接口。
__kernel_time_t
: 精度为1s,如使用被定义为32位的time_t的变量,存在2038年问题1
2typedef __kernel_long_t __kernel_time_t;
typedef long __kernel_long_t;Timeval(include/uapi/linux/time.h) 精度为1us
1
2
3
4struct timeval {
__kernel_time_t tv_sec; /* seconds */
__kernel_suseconds_t tv_usec; /* microseconds */
};timespec(include/uapi/linux/time.h)精度1ns
1
2
3
4struct timespec {
__kernel_time_t tv_sec; /* seconds _/
long tv_nsec; /_ nanoseconds _/
};64位拓展 timespec64,定义在time64.h文件中。高精度计时器一般采用纳秒级别的计量。
1
2
3
4struct timespec64 {
time64_t tv_sec; /_ seconds _/
long tv_nsec; /_ nanoseconds */
};ktime(include/linux/ktime.h)精度1ns,高精度计时器中常用,只能在内核中使用。
1
2
3union ktime {
s64 tv64;
};
对应人类习惯年月日时分秒的表示方法,linux内核提供了struct tm结构体。同时内核提供了各个时间类型的转换接口。
1 | struct tm { |
POSIX标准
POSIX(Portable Operating System Interface),是IEEE为要在各种UNIX操作系统上运行软件,而定义API的一系列互相关联的标准的总称,其正式称呼为IEEE Std 1003,而国际标准名称为ISO/IEC 9945。
Linux基本上逐步实现了POSIX兼容,但并没有参加正式的POSIX认证。时间子系统接口部分符合POSIX标准。
Linux系统时钟
这里指的是clock(时钟)可以类比为手腕上的表,作为linux内核的计时工具存在。Linux内核内存在多种clock,各种类型的timer(计时器)都是基于某个特定的系统时钟运行,特殊的计时器(如定时开关机所使用的定时器)需要基于特殊的系统时钟,保证关机时系统时钟仍然在运行。
时间是一个没有首尾的长线,任何有实际数值的时间都包含一个参考点。Linux中时间参考点称为linux Epoch,对应1970年1月1日0点0分0秒(UTC)时间点。Linux系统内并非所有时间都是以linux Epoch为参考点。
RTC时间 通常由一个专门的计时硬件来实现,使用专门的RTC芯片。不管系统是否上电,RTC中的时间信息都不会丢失,硬件上通常使用一个后备电池对RTC硬件进行单独的供电。内核和用户空间通过驱动程序访问RTC硬件来获取或设置时间信息。
realtime 和RTC一样,都是人们日常所使用的墙上时间,只是RTC时钟的精度通常比较低,只能达到毫秒级别的精度,外部的RTC芯片,访问速度也比较慢,为此,内核维护了另外一个wall time时间:realtime,取决于用于对计时的clocksource,它的精度可以达到纳秒级别, realtime存在于内存中,它的访问速度很快。realtime记录的是自1970年1月1日0点0时到当前时刻所经历的纳秒数。(linux4.x以前内核对应为全局变量xtime,4.x以后不再有全局xtime定义)
monotonic time 该时钟自系统开机后就一直单调地增加,不因用户的调整时间而产生跳变,该时间不计算系统休眠的时间。
raw monotonic time 与monotonic类似,属于单调递增的时钟,raw monotonic time不会受到NTP调整,它代表着系统独立时钟硬件对时间的统计。
boot time 与monotonic相同,系统休眠时同样增加,它代表着系统上电后的总时间。
早期Linux系统中是通过全局变量形式更改对应时间,而随着Linux内核迭代以上提及的时间类型被定义在timekeeper结构体中统一管理。
对应Linux系统内存在的系统时钟ID
1 | /* |
CLOCK_PROCESS_CPUTIME_ID 和 CLOCK_THREAD_CPUTIME_ID 这两个clock是专门用来计算进程或者线程的执行时间的(用于性能剖析),一旦进程(线程)被切换出去,那么该进程(线程)的clock就会停下来。因此,这两种的clock都是per-process或者per-thread的,而其他的clock都是系统级别的。
Linux系统时间有关变量
Linux系统与时间有关的变量或结构。
HZ:linux核心每隔固定周期会发出timer interrupt (IRQ 0)
- HZ是用来定义每一秒有几次timer interrupts。Linux内核2.6及以后版本可以在内核编译时指定HZ的值。HZ取值一般为100、250、300或1000。对于linux而言HZ一般为固定值。
- 高HZ对系统而言是更快的响应速度、更精准的进程调度、更准确的计时精度。随之带来的是系统处理timer interrupt中断开销的上升。需要根据实际需求合理选择HZ大小。
Tick:翻译为“节拍”,数值上为HZ的倒数
- 对应1次timer interrupt的间隔长短,定期的tick事件或者用于全局的时间管理(jiffies和时间的更新),或者用于本地cpu的进程统计、时间轮定时器框架
- 大部分情况下LInux系统都会尽量维持周期性tick。但Linux2.6内核以后,引入了动态时钟,在系统处于idle模式时,可以关闭周期性tick以节省电量。这一特性需要配置内核选项CONFIG_NO_HZ来激活,这一特性也被叫做tickless。
jiffies:记录系统启动以来的节拍总数,作为内核内计算时间间隔的一个很重要变量。
1
2extern u64 __jiffy_data jiffies_64;
extern unsigned long volatile __jiffy_data jiffies;- 定义上看jiffies为32位变量,对应100HZ的系统溢出间隔50天左右,内核定义了专门的宏处理比较时间长短时jiffies溢出的问题。同时还存在jiffies_64变量,jiffies_64的低32位对应jiffies,溢出时间间隔忽略不计。一般应用内jiffies已足够。
时间子系统概述
硬件计时器只是一个按照固定间隔单调递增的计时器,时间本身是一个没有首尾的直线,任何时间的度量都有一个参考点,对于linux系统而言,时间是自linux exposed以来的具体的数值。通过时间子系统linux将时间、计时事件与硬件计时器联系起来。
实现一个定时(timer),简单来说需要一个用来计时的腕表(clock)、表示时间的度量、在这个度量下需要计时的数值。在linux中表示时间的度量在4.1节提到了有3种,对应s、us、ns。数值一般都会明确给出。
最大的问题是需要一个计时的腕表,linux系统内与其功能相似的是硬件计时器,使用硬件计时器实现腕表的功能,linux最开始确实是这样做的。但需要计时的地方越来越多,硬件计时器数量不够,之后出现了软件时钟的概念。
硬件计时器不再被那一个timer使用,取而代之的是内核维护一个软件时钟,随硬件计时器更新。系统其他的timer都是使用这个软件时钟。软件时钟的好处是时钟数量不再限制,系统可以维持多个全局的软件时钟(多个system clock),相当于系统手里边又平白无故的多了很多可以用的腕表。
随着系统更进一步发展,将一个软件计时器作为了其他软件计时器的基准,其他的软件计时器都基于这个全局的软件计时器(jiffies),jiffies 时钟产生周期性中断,作为linux系统的时间参考,早期的时间子系统就是如此工作。
随着需要的需要的精度越来越高,这个全局的软件时钟(jiffies)越来越不能满足需求,系统时钟(system clock)开始直接基于硬件高精度计时器工作,不再依赖jiffies的周期计时。这部分的系统时钟(system clock)直接基于硬件计时器,使用这些系统时钟的timer可以摆脱jiffies 时钟的精度限制。但系统内需要jiffies时钟的地方太多,没法移除这部分功能,于是选中了一个timer作为sched timer模拟早期硬件计时器产生周期中断,供jiffies时钟使用。这里对应的是新的时间子系统。
时间子系统文件系统
时间子系统文件源码在 kernel\time
文件夹下。具体整理如下:
time.c :向用户空间提供时间接口。包括:time, stime, gettimeofday, settimeofday,adjtime。还有一些在其他内核模块使用时间格式转换的接口函数如jiffes和微秒之转换等。
timeconv.c:从calendar time转换broken-down time转换接口。
timer.c:低精度timer模块
time_list.c 与timer_status.c:用户空间提供的调试接口。
hrtimer.c:高精度timer模块。
itimer.c:interval timer模块。
posix-timers.c 、posix-cpu-timers.c 、posix-clock.c:OSIX timer模块和POSIX clock模块。
alarmtimer.c:alarmtimer模块
clocksource.c:clocksource.c是通用clocksource driver。
jiffies.c:全局system tick对应的clocksource。
clockevent.c:clockevent模块。
timekeeping.c、timekeeping_debug.c:timekeeping模块。
ntp.c:ntp模块。
tick-common.c、tick-oneshot.c、tick-sched.c:属于tick device layer,tick-common.c是periodic tick,管理周期性tick事件。tick-oneshot.c管理高精度tick时间。 tick-sched.c用于dynamic tick。
tick-broadcast.c、tick-broadcast-hrtimer.c:用于广播模式。
sched_clock.c:通用sched clock模块。这个模块主要是提供一个
sched_clock
的接口函数,调用该函数可以获取当前时间点到系统启动之间的纳秒值。 需要配置CONFIG_GENERIC_SCHED_CLOCK
。该模块扩展了64-bit的counter
,即使底层的 HW counter 比特数目。
时间子系统框架
Linux内核中现有两种定时器:低分辨率(又称经典)定时器和2.6内核以后引入的高精度定时器。二者在使用上有一定差异。在linux发展早期低分辨率定时器毫秒级别的分辨率可以很好满足需要,但随着linux系统在多媒体等需要高精度定时的设备的应用,需要对linux内核时间子系统改进以满足需要,但低分辨率的时间系统已经很完善,引入高分辨率计时需要保证向前兼容,低分率时间系统涉及时间片轮等内容,试图整合高分辨率的尝试失败,考虑内核的健壮,内核针对高精度时钟实现了一个新的时间子系统。
高分/低分在使用上有差异。低精度的分辨率在毫秒级别,高精度计时的分辨率在纳秒级别。Linux支持多核cpu后,新的时间子系统也做了相应处理。
整个时间子系统框图如图5-1所示:
由上到下分为三层:用户层、核心层、硬件相关层
硬件相关层:
- 单核cpu时的HW timer在新的时间子系统功能上划分为了两部分,一部分是free running的system counter,全局,不属于任何一个CPU,与时间子系统框图相关的是HW block,各个cpu的cpu core中都有对应的硬件timer,称之为CPU local Timer,这些CPU local Timer 都是基于Global counter运行。为运行这些硬件提供驱动的就是Clock Source driver。系统存在多个HW timer和counter clock时,对应多个Clock Source driver。
核心层:与硬件无关,处理时间子系统核心功能。
clock event和clock source:
- clock event和clock source位于核心层底层,是内核抽象出来与硬件无关的模块clock source对应硬件设备的system free running counter提供一个基础的timeline,64位的计时器,在ns级别的溢出间隔对于应用而言,完全可以满足.
- Clock event对应实在timeline上的特定点尝试clock even。Clock Source drive通过clock source和clock event的向下提供的接口注册clock sorece和clock event设备。Clock event事件的回掉是Clock Source drive申请中断调用clock event模块的callback函数实现的异步通知。
tick_device tick_device: 基于clock event,但两者并非一一对应。
- 硬件中存在几个计时器系统就会注册几个clck event device。单个cpu拥有单独的tick_device用于进程统计、调度等,系统内有几个cpu就存在几个tick_device。各个cpu的tick_device会选择合适的clck event device。(这种情况下tick_device有时又称为local tick_device)。
- 面向整个系统的需求,在所有的local tick_device中会选定一个作为全局的tick_device使用,称为global tick device,负责维护系统jiffies、更新wall clock、计算系统负荷等任务。
- tick device支持periodic mode(周期模式)和one shot mode(单触发模式)。两者的区别是periodic mode下只需要配置一次定时器,之后等事情就会周期性产生中断,是内核早期基于的模式。one shot mode下定时器每产生一次中断,系统需要再次对定时器进行配置,才可以触发下一次定时器中断。periodic mode定时精度较低在毫秒级别,one shot mode定时精度可以达到纳秒级别。
低精度timer和高精度timer
- Timer是基于tick device实现的,上文提到tick device支持periodic mode和one shot mode两种模式,低精度timer对应periodic mode,高精度timer对应one shot mode模式。tick device系统只能工作在一个模式下,对linux系统有4种定时器和tick device模式组合,4种组合详情见下一节。
- 高精度timer+one shot mode组合时,用户层依然可以使用低精度timer对应的API,究其原因是低精度timer在内核中提供了大量重要功能,系统会特别设置一个处于one shot mode的tick device,周期性的触发模拟传统的periodic tick,这个tick device被称为sched timer。
- Linux内核编译时即使没有选择高精度计时器,与高精度计时器有关的一部分代码依然会被编译入内核,保证即使没有设置one shot mode,应用层依然可以正常调用高精度timer的接口,不过此时高精度timer的精度与低精度timer相同,都为毫秒级别。
- 低精度时钟调度基于时间轮,高精度时钟调度基于红黑树,实现细节详情见各自章节。
Timekeeper: timekeeping模块提供时间服务的基础模块。
- Linux内核提供各种time line,real time clock,monotonic clock、monotonic raw clock等,timekeeping模块就是负责跟踪、维护这些timeline的,并且向其他模块(timer相关模块、用户空间的时间服务等)提供服务,而timekeeping模块维护timeline的基础是基于clocksource模块和tick模块。通过tick模块的tick事件,可以周期性的更新time line,通过clocksource模块、可以获取tick之间更精准的时间信息。开机后由RTC系统读取RTC时间,更新到系统时间。
用户层
- 代码上可以划归成两方面:与time有关的接口和与timer有关的接口。功能上区分包括:系统时间相关(例:记录当前时间等)、进程休眠(典型数sleep等)、定时器有关(alert进程等)。
时间子系统系统配置
系统编译时需要选择 CONFIG_GENERIC_CLOCKEVENTS 启用新的时间子系统(一般在arch中)
本文档默认均为普通的dynamic tick系统(tickless idle system)。
系统配置
tick device配置: 在使用新的时间子系统的前提下,内核会提供Timers subsystem的配置选项,有3种选项。这3个选项,只能使能1项。
- CONFIG_HZ_PERIODIC: 始终启用周期性tick,即使系统处于idle时。
- CONFIG_NO_HZ_IDLE: 对应Idle dynticks system模式,系统处于idle时,自动停止周期性tick。启用改选项使,系统自动使能CONFIG_NO_HZ_COMMON。
- CONFIG_NO_HZ_FULL: 对应Full dynticks system模式,即便在非idle的状态下,也可能会停掉周期性tick。启用改选项使,系统自动使能CONFIG_NO_HZ_COMMON。
- 除此之外还有一个用来配置tick模式的选项: CONFIG_TICK_ONESHOT 表示系统内所有的tick设备都是oneshot mode。
timer配置
- 高精度timer只有一个CONFIG_HIGH_RES_TIMERS的配置项。如果配置了高精度timer,或者配置了NO_HZ_COMMON的选项,那么一定需要配置CONFIG_TICK_ONESHOT,表示系统支持支持one-shot类型的tick device。
4种组合方式
periodic tick+低精度timer: 最为传统的组合方式,向前兼容性能最好。
- 配置
- tick device:CONFIG_HZ_PERIODIC,不配置CONFIG_TICK_ONESHOT
- Timer 不配置CONFIG_HIGH_RES_TIMERS。
- 注意:即使配置了CONFIG_NO_HZ和CONFIG_TICK_ONESHOT,系统硬件未提供支持one shot的clock event device,系统依然运行在周期性tick模式下。
- 配置
periodic tick+高精度timer: 一般不会选择这种组合,多半用于系统硬件无法支持one shot的clock event device,系统依然运行在周期性tick下。
- 配置
- tick device:CONFIG_HZ_PERIODIC
- timer:CONFIG_HIGH_RES_TIMERS
- 配置
dynamic tick+低精度timer
- 系统开始时并不是直接进入dynamic tick mode,系统开始会运行在周期tick模式下,各个cpu对应的tick device的event handler为tick_handle_periodic。在timer的软中断上下文中,系统调用tick_check_oneshot_change检查支持one shot的clock event device发生tick mode的切换。tick device切换到one shot模式,event handler设置为tick_nohz_handler。
- 系统正常运行时,event handler每次都要重新对clock event设置,以此产生周期性tick。系统处于idle时,clock event device的event handler不在对clock event进行设置,周期性tick停滞。
- 配置
- tick device:CONFIG_NO_HZ_IDLE + CONFIG_TICK_ONESHOT
- timer:CONFIG_HIGH_RES_TIMERS
dynamic tick +高精度timer: 除非为了绝对保证向前兼容,一般推荐使用dynamic tick +高精度timer组合。
- 与dynamic tick+低精度timer时相同,系统启动后会向处于周期tick模式下。
- 进入tick软中断后进行如下切换:
- hrtimer_switch_to_hres将timer切换至高精度模式。
- Tick device的clock event设备切换到oneshot mode
- Tick device的clock event设备的event handler会更新为hrtimer_interrupt
- 设定sched timer模拟周期性tick。sched timer会在系统进入idle时候停止,降低功耗。
时间子系统详细分析
在新的时间子系统中Linux系统将硬件计时器功能上抽象为了两个实体:clock source(时钟源) 与 clock_event(时钟事件)。
clock source(时钟源): 顾名思义,是系统时钟的源头。clock source与硬件关联,硬件计时器单调计时递增(当然有溢出问题,当64位计时器溢出时间相当长,可以忽略这种情况),在clock source的反应是提供了一条基础的timeline。clock source本身没有产生任何事件/中断的能力。clock source在系统内主要功能是供timekeep使用,维持多个系统时钟。
clock_event(时钟事件): 是在timeline上特定时间点产生事件。tick device正是基于clock_event工作的。
tick device 是基于clock_event设置定时时间,处理周期事件。
Timekeep 维护各种系统time line,real time clock,monotonic clock、monotonic raw clock。维护timeline的基础是基于clocksource模块和tick模块。通过tick模块的tick事件,可以周期性的更新time line,通过clocksource模块可以获取tick之间更精准的时间信息。系统的time line在高精度模式下,都是基于与硬件关联clock source提供的time line。
clock source
clocksource是对真实的时钟源进行软件抽象,留有注册/卸载接口,供驱动调用。
源码
1 | struct clocksource { |
我们只关注几个重要字段:
- rating,代表了时钟源的精度范围,与每个时钟源晶振的频率有关,当有更好的时钟源注册时,timekeep会主动切换到精度更好的时钟源。
- 1–99: 不适合于用作实际的时钟源,只用于启动过程或用于测试;
- 100–199:基本可用,可用作真实的时钟源,但不推荐;
- 200–299:精度较好,可用作真实的时钟源;
- 300–399:很好,精确的时钟源;
- 400–499:理想的时钟源,如有可能就必须选择它作为时钟
cycle_t (*read)(struct clocksource *cs);
获得时钟源的当前计数,只能通过调用read回调函数来获得当前的计数值,注意这里只能获得计数值(cycle)要获得相应的时间,必须要借助clocksource的mult和shift字段进行转换计算。u32 mult;u32 shift;
使用公式进行cycle和t的转换:
$t = (cycle * mult) >> shift;$
clocksource的注册和初始化
clocksource 要在初始化阶段通过 clocksource_register_hz 函数通知内核它的工作时钟的频率
大部分工作在clocksource_register_scale完成,该函数首先完成对mult和shift值的计算,然后根据mult和shift值,clocksource_enqueue函数负责按clocksource的rating的大小,把该clocksource挂载到全局链表clocksource_list上,rating值越大,在链表上的位置越靠前。
每次新的clocksource注册进来,都会触发clocksource_select函数被调用,它按照rating值选择最好的clocksource,并记录在全局变量curr_clocksource中,然后通过timekeeping_notify函数通知timekeeping
clocksource watchdog
clocksource不止一个,为了筛选clocksource。内核启用了一个周期为0.5秒的定时器。
clocksource被注册时,除clocksource_list外,还会同时挂载到watchdog_list链表。定时器每0.5秒检查watchdog_list上的clocksource,WATCHDOG_THRESHOLD的值定义为0.0625秒,如果在0.5秒内,clocksource的偏差大于这个值就表示这个clocksource是不稳定的,定时器的回调函数通过clocksource_watchdog_kthread线程标记该clocksource,并把它的rate修改为0,表示精度极差。
系统启动时clocksource变化
系统的启动时,内核会注册了一个基于jiffies的clocksource(kernel/time/jiffies.c),它精度只有1/HZ秒,rating值为1。
如果平台的代码没有提供定制的clocksource_default_clock函数,系统将返回这个基于jiffies的clocksource。启动的后半段,clocksource的代码会把全局变量curr_clocksource设置为clocksource_default_clock返回的clocksource。
当然即使平台的代码没有提供clocksource_default_clock函数,在平台的硬件计时器注册时,经过clocksource_select()函数,系统还是会切换到精度更好的硬件计时器上。
clock_event
clocksource不能被编程,clock_event则是可编程的,它可以工作在周期触发或单次触发模式,系统通过clock_event确定下一次事件触发的时间,clock_event主要用于实现普通定时器和高精度定时器,同时也用于产生tick事件,供给进程调度子系统使用。
多核系统内,每个CPU形成自己的一个小系统,有自己的调度、有自己的进程统计等,拥有自己的tick设备,而且是唯一的。clock event有多少硬件timer注册多少clock event device,各个cpu的tick device会选择自己适合的那个clock event设备,这个设备称为clock_event_device。
源码
1 | struct clock_event_device { |
clock_event_device是clock_event的核心数据结构,这里只注释较重要部分。
- event_handler 一个回调函数指针,通常由通用框架层设置,在时间中断到来时,硬件的中断服务程序会调用该回调,实现对时钟事件的处理。
- set_next_event 设置下一次时间触发的时间,使用离现在的cycle差值作为参数。
- set_next_ktime 设置下一次时间触发的时间,直接使用ktime时间作为参数。
- max_delta_ns 可设置的最大时间差,单位是纳秒。
- min_delta_ns 可设置的最小时间差,单位是纳秒。
- mult shift 与clocksource中的类似,只不过是用于把纳秒转换为cycle。
- mode 该时钟事件设备的工作模式,两种主要的工作模式分别是:
- CLOCK_EVT_MODE_PERIODIC 周期触发,设置后按给定的周期不停地触发事件;
- CLOCK_EVT_MODE_ONESHOT 单次触发,只在设置好的触发时刻触发一次;
- set_mode 函数指针,用于设置时钟事件设备的工作模式。
- rating 表示该设备的精度等级。
- list 系统中注册的时钟事件设备用该字段挂在全局链表变量clockevent_devices上。
全局变量
除核心的结构体外,clock_event同时还有两个相关的全局变量。
- clockevent_devices: 定义在在kernel/time/clockevents.c,系统内所有注册的clock_event_device都会挂载到该链表
- clockevents_chain: 系统中的clock_event设备的状态发生变化时,利用该通知链通知系统的其它模块。
clock event注册
clock event 留有注册函数 clockevents_register_notifier
由start_kernel开始,调用tick_init,调起clockevents_register_notifier,同时把类型为notifier_block的tick_notifier作为参数传入。clockevents_register_notifier注册了一个通知链,当系统中的clock_event_device状态发生变化时(新增,删除,挂起,唤醒等等),tick_notifier中的notifier_call字段中设定的回调函数tick_notify就会被调用。
接下来start_kernel调用了time_init函数,该函数通常定义在体系相关的代码中,它主要完成机器对时钟系统的初始化工作,最终通过clockevents_register_device注册系统中的时钟事件设备,把每个时钟时间设备挂在clockevent_device全局链表上,最后通过clockevent_do_notify触发框架层事先注册好的通知链clockevents_chain上。
tick_device
tick_device本身是对clock event的进一步封装
1 | struct tick_device { |
tick device其实是工作在某种模式下的clock event设备。工作模式体现在tick device的mode成员,evtdev指向了和该tick device关联的clock event设备.
1 | enum tick_device_mode { |
tick device可以有两种模式,一种是周期性tick模式,另外一种是one shot模式
分类及cpu的关系
Tick device有3种分类。
- local tick device
DEFINE_PER_CPU(struct tick_device, tick_cpu_device);
- 在多核架构下,系统会为每一个cpu建立了一个tick device。每一个cpu就像是一个小系统一样运行在各自的tick上,实现任务调度等工作。
- global tick device
int tick_do_timer_cpu __read_mostly = TICK_DO_TIMER_BOOT;
- 有些全局的系统任务,不适合使用local tick device,如更新jiffies、更新wall time等。这时系统会选择一个local tick device,在tick_do_timer_cpu中指明,作为global tick device负责这些全局任务。
- broadcast tick device
static struct tick_device tick_broadcast_device;
- 涉及cpu广播模式,当选用的global tick device因cpu休眠等原因而停止运行时,broadcast tick device就会接入global tick device的tick处理程序,代替global tick device产生定期中断。但在系统看来global tick device依旧在运作。如图5-3所示。
timekeeper
timekeeping模块提供时间服务的基础模块。Linux内核提供各种time line,real time clock,monotonic clock、monotonic raw clock等,timekeeping模块就是负责跟踪、维护这些timeline的,并且向其他模块(timer相关模块、用户空间的时间服务等)提供服务,而timekeeping模块维护timeline的基础是基于clocksource模块和tick模块。通过tick模块的tick事件,可以周期性的更新time line,通过clocksource模块、可以获取tick之间更精准的时间信息。
源码
1 | struct timekeeper { |
前面提及Linux系统内时钟,目前均为timekeep维护。
时间种类 | 精度(统计单位) | 访问速度 | 累计休眠时间 | 受NTP调整的影响 |
---|---|---|---|---|
xtime | 高 | 快 | Yes | Yes |
monotonic | 高 | 快 | No | Yes |
raw monotonic | 高 | 快 | No | No |
boot time | 高 | 快 | Yes | Yes |
timekeep初始化
timekeeper的初始化由timekeeping_init完成,该函数在start_kernel的初始化序列中被调用,timekeeping_init首先从RTC中获取当前时间。
从persistent clock获取当前的时间值
系统启动后,会从persistent clock中中取出当前时间值(例如RTC),根据情况初始化各种system clock。具体代码如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16read_persistent_clock64(&now);
if (!timespec64_valid_strict(&now)) {
pr_warn("WARNING: Persistent clock returned invalid value!\n"
" Check your CMOS/BIOS settings.\n");
now.tv_sec = 0;
now.tv_nsec = 0;
} else if (now.tv_sec || now.tv_nsec)
persistent_clock_exists = true;
read_boot_clock64(&boot);
if (!timespec64_valid_strict(&boot)) {
pr_warn("WARNING: Boot clock returned invalid value!\n"
" Check your CMOS/BIOS settings.\n");
boot.tv_sec = 0;
boot.tv_nsec = 0;
}read_persistent_clock在linux/arch/arm/kernel/time.c文件中。主要功能就是从系统中的HW clock(例如RTC)中获取时间信息。
!timespec64_valid_strict用来校验一个timespec是否是有效。需要满足timespec中的秒数值要大于等于0,小于KTIME_SEC_MAX,纳秒值要小于NSEC_PER_SEC(10^9)。KTIME_SEC_MAX这个宏定义了ktime_t这种类型的数据可以表示的最大的秒数值。
设定persistent_clock_exist flag,说明系统中存在RTC的硬件模块,timekeeping模块会和RTC模块进行交互。例如:在suspend的时候,如果该flag是true的话,RTC driver不能sleep,因为timekeeping模块还需要在resume的时候通过RTC的值恢复其时间值呢。
为timekeeping模块设置default的clock source
1
2
3
4clock = clocksource_default_clock();
if (clock->enable)
clock->enable(clock);
tk_setup_internals(tk, clock);- 在timekeeping初始化的时候,很难选择一个最好的clock source。在平台没有定义clocksource_default_clock的情况下,默认就是采用一个基于jiffies 的clocksource。
- 建立default clocksource和timekeeping伙伴关系。
初始化real time clock、monotonic clock和monotonic raw clock
1
2
3
4
5
6
7
8
9
10
11
12
13tk_set_xtime(tk, &now);
tk->raw_time.tv_sec = 0;
tk->raw_time.tv_nsec = 0;
if (boot.tv_sec == 0 && boot.tv_nsec == 0)
boot = tk_xtime(tk);
set_normalized_timespec64(&tmp, -boot.tv_sec, -boot.tv_nsec);
tk_set_wall_to_mono(tk, tmp);
timekeeping_update(tk, TK_MIRROR);
write_seqcount_end(&tk_core.seq);
raw_spin_unlock_irqrestore(&timekeeper_lock, flags);- 根据从RTC中获取的时间值来初始化timekeeping中的real time clock,如果没有获取到正确的RTC时间值,那么缺省时间为linux epoch。
- monotonic raw clock被设定为从0开始。
- 启动时将monotonic clock设定为负的real time clock。
低精度timer
低分辨率定时器准确来说指的是使用jiffies值计数的计时器,精度最高只有1/HZ.
创建定时器
讨论定时器的实现原理前,我们先看看如何使用定时器。要在内核编程中使定时器,首先我们要定义一个time_list结构,该结构在include/Linux/timer.h。
1 | struct timer_list { |
- entry 用于把一组定时器组成一个链表,低精度timer调度使用时间片轮。
- expires 该定时器的到期时刻的jiffies计数值。
- base 指向该定时器所属的cpu所对应tvec_base结构。
- function 定时器到期时,调用该回调函数,用于响应该定时器的到期事件。
- data 回调函数的参数。
- slack 对到期时间精度不太敏感的定时器,到期时刻允许适当地延迟一小段时间,该字段用于计算每次延迟的HZ数。
定义一个timer_list,可以使用静态和动态两种办法。
- 静态方法使用DEFINE_TIMER宏:
#define DEFINE_TIMER(_name, _function, _expires, _data)
该宏将得到一个名字为_name,并分别用_function,_expires,_data参数填充timer_list的相关字段。
使用动态的方法,需要自行声明一个timer_list结构,然后手动初始化它的各个字段。
1
2
3
4
5
6struct timer_list timer;
......
init_timer(&timer);
timer.function = _function;
timer.expires = _expires;
timer.data = _data;
定时器其他操作
- 要激活一个定时器,调用add_timer
add_timer(&timer);
- 修改定时器的到期时间,调用mod_timer即可
mod_timer(&timer, jiffies+50);
- 移除一个定时器,调用del_timer
del_timer(&timer);
定时器系统还提供了以下这些API供我们使用:
- void add_timer_on(struct timer_list *timer, int cpu); // 在指定的cpu上添加定时器
- int mod_timer_pending(struct timer_list *timer, unsigned long expires); 只有当timer已经处在激活状态时,才修改timer的到期时刻
- void set_timer_slack(struct timer_list *time, int slack_hz); 设定timer允许的到期时刻的最大延迟,用于对精度不敏感的定时器
- int del_timer_sync(struct timer_list *timer); 如果该timer正在被处理中,则等待timer处理完成才移除该timer
低精度timer的软件架构
系统中有可能有成百上千个定时器,对于这些定时器的处理,早期时间子系统采用了时间轮进行统一管理即按照定时器到期的时间按照时间轮的方式排列,统一处理。
上图的轮上由8个bucket,可以将每一个bucket代表一秒,那么 bucket [1] 代表的时间点就是“1 秒钟以后”,bucket [8] 代表的时间点为“8 秒之后”。Bucket 存放着一个 timer 链表,链表中的所有 Timer 将在该 bucket 所代表的时间点触发。
每次时钟中断产生时,时间轮增加一格,然后中断处理代码检查bucket,假如该 bucket 非空,则触发该 bucket 指向的 Timer 链表中的所有 Timer。
按照类似的做法,内核将时间轮上单一的 bucket 数组分成了几个不同的数组,每个数组表示不同的时间精度。如下图5-5 所示,时间轮有三级,分别表示小时,分钟和秒。在 Hour 数组中,每个 bucket 代表一个小时,根据其定时器到期值,Timer 被放到不同的 bucket 数组中管理。
最终形成如下图所示的时间轮调度。
添加Timer:
- 根据其到期值,Timer 被放到不同的 bucket 数组中,这个比较简单。
删除Timer:
- Timer 本身有指向 bucket 的指针,因只需要从该 Timer 的 bucket 指针读取到 指向该bucket的指针,然后从该 List 中删除自己即可。
定时器处理:
- 每个时钟中断产生时(假设时钟间隔为 1 秒),将 SECOND ARRAY 的 cursor 加一,假如 SECOND ARRAY 当前 cursor 指向的 bucket 非空,则触发其中的所有 Timer。
优点:
- 确实基于时间轮的定时器调度,使得定时器处理相当高效,节省了系统开支。满足了低精度下定时器处理的要求。
缺点:
- 系统计时的最高精度被限制为周期中断的精度,当然则与定时器本身也有关系。
- 在时间轮走完一圈后,时间轮需要由上一级重填充,这个过程中无法相应中断处理,切换过程的系统开销较大。这在需要高精度实时响应的场合是不可接受的。
高精度timer
高精度timer克服了低精度timer的限制,实现了真正的高精度计时。
1 | struct hrtimer { |
内核使用了一个hrtimer结构来表示一个高精度定时器。
_softexpires
ktime_t精度的时间,用来记录到期时间,到期后调用function指定的回调函数(*function)(struct hrtimer *)
回调函数定时器到期时调用。函数的返回值为一个枚举值,它决定了该hrtimer是否需要被重新激活。State
用于表示hrtimer当前的状态,有几种组合:#define HRTIMER_STATE_INACTIVE 0x00
定时器未激活#define HRTIMER_STATE_ENQUEUED 0x01
定时器已经被排入红黑树中#define HRTIMER_STATE_CALLBACK 0x02
定时器的回调函数正在被调用#define HRTIMER_STATE_MIGRATE 0x04
定时器正在CPU之间做迁移
hrtimer的到期时间可以基于以下几种时间基准系统:
- HRTIMER_BASE_MONOTONIC, 单调递增的monotonic时间,不包含休眠时间
- HRTIMER_BASE_REALTIME, 平常使用的墙上真实时间
- HRTIMER_BASE_BOOTTIME, 单调递增的boottime,包含休眠时间
- HRTIMER_MAX_CLOCK_BASES, 用于后续数组的定义
Hrtimer其他操作:
- hrtimer_init() 初始化一个 Timer 对象,
- hrtimer_start() 设定到期时间和到期操作,并添加启动该 Timer。
- remove_hrtimer() 删除一个 Timer。
高精度timer的软件架构
与低精度timer使用时间轮作为调度不同,高精度timer使用红黑树作为调度手段,在高精度硬件计时器的基础上实现了ns级别的定时。
所有的 hrtimer 实例都被保存在红黑树中,添加 Timer 就是在红黑树中添加新的节点;删除 Timer 就是删除树节点。红黑树的键值为到期时间。
Timer 的触发和设置与定期的 tick 中断无关。当前 Timer 触发后,在中断处理的时候,将高精度时钟硬件的下次中断触发时间设置为红黑树中最早到期的 Timer 的时间。时钟到期后从红黑树中得到下一个 Timer 的到期时间,并设置硬件,如此循环反复。
高精度的硬件计时器与每个cpu紧密相关,其相关数据结构如下图所示
在多处理器系统中,每个 CPU 都保存和维护自己的高精度定时器,在每个 CPU 上,hrtimer 还分为两大类:
- Monotonic:与系统时间无关,不可以被人修改。
- Real time:实时时间即系统时间,可以被人修改。
每个 CPU 都需要两个 clock_base 数据结构:一个指向所有 monotonic hrtimer;另一个指向所有的 realtime hrtimer。
clock_base 数据结构中,active 指向一个红黑树,每个 hrtimer 都是该红黑树的一个节点,用到期时间作为 key。这样所有的定时器便按照到期时间的先后被顺序加入这棵平衡树。first 指向最近到期的 hrtimer, 即红黑树最左边的叶子节点。
添加 Timer,即在相应的 clock_base 指向的红黑树中增加一个新的节点,红黑树的 key 由 hrtimer 的到期时间表示,因此越早到期的 hrtimer 在树上越靠左。
删除 Timer,即从红黑树上删除该 hrtimer。
高精度时钟模式下,定时器直接由高精度定时器硬件产生的中断触发。以一个实例分析:
- 假如3 个 hrtimer,其到期时间分别为 10ns、100ns 和 1000ns。添加第一个 hrtimer 时,系统通过对应 clock_event_device 操作硬件将其下一次中断触发时间设置为 10ns。
- 10ns 到期,中断产生,最终会调用到 hrtimer_interrrupt() 函数,该函数从红黑树中得到所有到期的 Timer,并负责调用 hrtimer 数据结构中维护的用户处理函数。
- hrtimer_interrupt 读取下一个到期的 hrtimer,并且通过 clock_event_device 操作时钟硬件将下一次中断到期时间设置为 90ns ,如此反复操作。
系统依然创建一个模拟 tick 时钟的特殊 hrtimer,并且该时钟按照 tick 的间隔时间(比如 10ms)定期启动自己,从而模拟出 tick 时钟,不过在 tickless 情况下,会跳过一些 tick。
接口
除非特别情况下都应该将系统配置未dynamic tick +高精度timer模式。具体配置如下:
- 编译时选择CONFIG_GENERIC_CLOCKEVENTS启用新的时间子系统。
- tick device配置为CONFIG_NO_HZ_IDLE和CONFIG_TICK_ONESHOT
- timer配置CONFIG_HIGH_RES_TIMERS,启用高精度计时器
时间子系统用户层接口
时间子系统面向用户层的接口主要有3种:系统时间相关、进程统计相关、timer相关。以下一一说明。
系统时间相关
与系统时间相关的接口主要是获取/设定不同精度的当前系统时间、获取系统定时精度等。设定时间的进程必须拥有CAP_SYS_TIME权限。
time_t time(time_t *t);int stime(time_t *t);
- 定义在time.h,精度s。返回/设定自linux epoch以来的秒数。编译内核时,需要启用__ARCH_WANT_SYS_TIME,一般需要兼容旧版本应用时选用。系统内存在将time_t类型转换为其他时间表示的接口。
int gettimeofday(struct timeval *tv, struct timezone *tz);
int settimeofday(const struct timeval *tv, const struct timezone *tz);
- 定义在<sys/time.h>,精度us。
- gettimeofday将目前的时间用tv 结构体返回,当地时区的信息则放到tz所指的结构中。Settimeofday将对应时区和时间写入系统。
int clock_getres(clockid_t clk_id, struct timespec *res);
int clock_gettime(clockid_t clk_id, struct timespec *tp);
int clock_settime(clockid_t clk_id, const struct timespec *tp);
- 定义在<time.h>,精度可以达到ns级别,但实际clock_gettime返回的时间值的粒度要比ns大。
- clock ID是指system clock(系统时钟)ID,具见4.1节。
获取clock ID有两种方式,在进程中使用定义在<time.h>的int clock_getcpuclockid(pid_t pid, clockid_t *clock_id)函数,在线程中则需要包含 <pthread.h> 调用int pthread_getcpuclockid(pthread_t thread, clockid_t *clock_id)函数。
虽然clock_xx的精度可以达到ns级别但实际精度与系统所使用的计时器有关。clock_getres()函数是用来获取系统时钟精度。
进程统计相关
unsigned int sleep(unsigned int seconds);
- 定义在<unistd.h>,延时精度s,基于CLOCK_REALTIME时钟。返回值为与设定相比进程未休眠时间。
int usleep(useconds_t usec);
- 定义在<unistd.h>,延时精度us,基于CLOCK_REALTIME时钟。返回值为0执行成功,返回值为-1执行失败。
int nanosleep(const struct timespec *req, struct timespec *rem);
- 定义在<time.h>,延时精度ns,基于CLOCK_REALTIME时钟。返回值为0执行成功,返回值-1执行失败,错误码定义在errno中。传入参数为延时的秒数和纳秒数。建议取代sleep和usleep。
int clock_nanosleep(clockid_t clock_id, int flags, const struct timespec *request, struct timespec *remain);
- 定义在<time.h>,延时精度ns。
- 传入参数:clock_id为依赖的系统时钟id,不仅仅是CLOCK_REALTIME时钟。Flags取值0/1代表相对时间/绝对时间。request和remain代表延时时间。
timer相关
unsigned int alarm(unsigned int seconds);
- 定义在<unistd.h>,精度s。指定时间过后,向进程发送SIGALRM信号。基于CLOCK_REALTIME。
int getitimer(int which, struct itimerval *curr_value)
int setitimer(int which, const struct itimerval *new_value, struct itimerval *old_value);
定义在<sys/time.h>中。效果与alarm()函数类似,延迟精度us。可以指定多长时间后执行任务,还可以设定间隔时间执行。已有更新的函数取代.
传入参数:
- Which,指明使用的timer,有3中取值。
- ITIMER_REAL。基于CLOCK_REALTIME计时,超时后发送SIGALRM信号
- ITIMER_VIRTUAL。当该进程的用户空间代码执行的时候计时,超时后发送SIGVTALRM信号。
- ITIMER_PROF。该进程执行的时候计时,不论是执行用户空间代码还是进入内核执行(例如系统调用),超时后发送SIGPROF信号。
1
2
3
4struct itimerval {
struct timeval it_interval; /* next value */
struct timeval it_value; /* current value */
};- 指定本次和下次超期后设定的时间值,可以工作在one shot类型的timer上。it_value的值会在到期后充新加载为it_interval。old_value装载上次setitimer的设定值。
- Which,指明使用的timer,有3中取值。
getitimer函数获取当前的Interval timer的状态,其中的it_value成员可以得到当前时刻到下一次触发点的时间信息
POSIX timer接口函数
- 基础有3个函数,用于创建/设定/删除timer。延时精度ns级别,是目前最复杂的定时器函数。
- 创建timer
int timer_create(clockid_t clockid, struct sigevent *sevp, timer_t *timerid);
- clockid代表此timer使用的系统时钟
- Timerid为timer ID的句柄
- struct sigevent代表通知进程的方式
- SIGEV_NONE。程序自己调用timer_gettime来轮询timer的当前状态
- SIGEV_SIGNAL。使用sinal这样的异步通知方式。发送的信号由sigev_signo定义。如果发送的是realtime signal,该信号的附加数据由sigev_value定义。
- SIGEV_THREAD。创建一个线程执行timer超期callback函数,_attribute定义了该线程的属性
- SIGEV_THREAD_ID。行为和SIGEV_SIGNAL类似,不过发送的信号被送达进程内的一个指定的thread,这个thread由_tid标识
- 设定timer
int timer_settime(timer_t timerid, int flags, const struct itimerspec *new_value, struct itimerspec * old_value);
- timerid为创建的timer。
- flag等于0或者1,代表new_value参数设定的时间值是相对时间还是绝对时间。
- new_value.it_value是一个非0值,那么调用timer_settime可以启动该timer。如果new_value.it_value是一个0值,那么调用timer_settime可以stop该timer。
- int timer_gettime(timer_t timerid, struct itimerspec *curr_value);获取timer的剩余时间。
- 删除timer
- timer_delete用来删除指定的timer,释放资源
具体使用
获取系统时间
使用time() 函数,系统提供了常见时间格式之间的转换
使用gettimeofday() 以及 clock_gettime()获取时间,暂时没有直接转换的库函数,一般的做法是将时间精度的个位提取,转换到tm格式,再进行输出。
进程休眠
建议使用nanosleep()函数,nanosleep()函数的精度可以达到ns级别,在使用高精度时钟的系统上nanosleep()的精度可以得到充分利用。
如要求非常稳定的计时,换用clock_nanosleep()函数,指定比nanosleep()默认使用的CLOCK_REALTIME更稳定的系统时钟。
精度要求不高可以使用sleep( int )函数实现简单的秒级别的暂停。Linux系统内是调用nanosleep()实现的sleep()函数。
高精度计时器
需要定时器的场合下,上文提及的setitimer()函数可以满足大部分要求,但需要us甚至ns的定时场合下,其不再适用。这里需要使用符合POSIX标准的POSIX Timer。
POSIX Timer 是针对有实时要求的应用所设计的,接口支持 ns 级别的时钟精度。
- 创建一个 Timer。指定该 Timer 的一些特性
int timer_create(clockid_t clockid, struct sigevent *sevp, timer_t *timerid);
- clock ID,即timer依赖的系统时钟,在Posix Timer中有4中取值
- CLOCK_REALTIME 系统墙上时间,受修改系统时间影响
- CLOCK_MONOTONIC 自开机后开始计数,调整时间不影响计数
- CLOCK_PROCESS_CPUTIME_ID 只记录当前进程所实际花费的时间
- CLOCK_THREAD_CPUTIME_ID 只记录当前线程所实际花费的时间
- struct sigevent,即到期后,通知到达方式。
- SIGEV_NONE 到期时不通知,应用timer_gettime查询处理
- SIGEV_SIGNAL 到期时将给进程投递信号,可以用来指定信号
- SIGEV_THREAD 到期时将启动新的线程进行处理
- SIGEV_THREAD_ID 到期时将向指定线程发送信号
- clock ID,即timer依赖的系统时钟,在Posix Timer中有4中取值
- 启动定时器
int timer_settime(timer_t timerid, int flags, const struct itimerspec *new_value,struct itimerspec * old_value);
andint timer_gettime(timer_t timerid, struct itimerspec *curr_value);
- 调用 timer_settime() 函数指定定时器的时间间隔,启动该定时器。启动和停止.
1
2
3
4
5struct itimerspec
{
struct timespec it_interval; //定时器周期值
struct timespec it_value; //定时器到期值
};- new_value->it_interval 为定时器的周期值,比如 1 秒,表示定时器每隔 1 秒到期;
- new_value->it_value 如果大于 0,表示启动定时器,Timer 将在 it_value 这么长的时间过去后到期,此后每隔 it_interval 便到期一次。如果 it_value 为 0,表示停止该 Timer。
- 应用程序会先启动用一个时间间隔启动定时器,随后又修改该定时器的时间间隔,这都可以通过修改 new_value 来实现;假如应用程序在修改了时间间隔之后希望了解之前的时间间隔设置,则传入一个非 NULL 的 old_value 指针,这样在 timer_settime() 调用返回时,old_value 就保存了上一次 Timer 的时间间隔设置。
- 调用 timer_settime() 函数指定定时器的时间间隔,启动该定时器。启动和停止.
直接使用hrtimer
用户态使用定时器因为优先级等问题,多少都会收到进程调度影响,如在用户层POSIX Timer依旧无法满足要求,则需要自行实现一个调用hrtimer的设备,在内核中直接调用hrtime,到期后通过接口通知应用层。
实现一个挂载至platform总线的设备。
调用hrtimer的简单实现:
- 需要定义一个hrtimer结构的实例
- 用hrtimer_init函数对它进行初始化
void hrtimer_init(struct hrtimer *timer, clockid_t which_clock,enum hrtimer_mode mode);
- which_clock 可以是CLOCK_REALTIME、CLOCK_MONOTONIC、CLOCK_BOOTTIME
- mode则可以是相对时间HRTIMER_MODE_REL,也可以是绝对时间HRTIMER_MODE_ABS
- 设定回调函数:timer.function = hr_callback;
- 如果定时器无需指定一个到期范围,可以在设定回调函数后直接使用hrtimer_start激活该定时器;如果需要指定到期范围,则可以使用hrtimer_start_range_ns激活定时器。函数原型:
int hrtimer_start(struct hrtimer *timer, ktime_t tim, const enum hrtimer_mode mode);
hrtimer_start_range_ns(struct hrtimer *timer, ktime_t tim, unsigned long range_ns, const enum hrtimer_mode mode);
- 要取消一个hrtimer,使用hrtimer_cancel。
尾声
有错误请留意,尽力修补.