相关参考
内核时间
在玩嵌入式得时候,我们都知道,外接晶振,内接CPU的时钟树,产生了各种时钟:AXI时钟,APB时钟,CPU时钟。
那么内核时间是怎么获取?
内核时钟框架
在 Linux Time 中详细的描述了Linux内核时钟框架
可以看到:linux上 各种时钟架构和服务是基于硬件提供的两种timer而构建的。
- 基于硬件定时器 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 Time 中 列举了 Exynos4412的时钟框架
可以看到, 基于PLL出来的TCLK,出来一个64位的时间戳timer;
基于PLL出来的TCLK,引出了8个32位的定时器timer
LDD中关于时间的接口
HZ和tick
定时器中断由系统定时硬件以规律地间隔产生。每一次中断称为一个tick嘀嗒值,HZ代表频率,即1s产生多少次tick值
这两个值依赖具体硬件,不应该直接使用。
jiffies 计数器
jiffies计数器代表自系统启动后,到现在产生了多少tick值(中断计数)。
<linux/jiffies.h>
// 获取jiffies值
u64 a = get_jiffies_64()
u64 b = get_jiffies_64()
// 毕竟时间先后
time_after(a,b); // returns true if the time a is after time b
time_before(a,b); // returns true if the time a is before time a
// jiffies和msec转换
jiffies_to_msecs();
jiffies_to_usecs();
jiffies_to_nsecs();
// jiffies和时间转换
struct timespec64 {
time64_t tv_sec; /* seconds */
long tv_nsec; /* nanoseconds */
};
unsigned long timespec64_to_jiffies(const struct timespec64 *value);
void jiffies_to_timespec64(const unsigned long jiffies,struct timespec64 *value);
timers-howto.rst 中描述了 udelay,mdelay,ndelay; 以及 msleep等
Linux下表示时间的接口
基于硬件定时器的时间操作接口
在内核中常见定时器操作,比如 定时任务,meleep,schedule_timeout等都使用timer进行定时。
timer
低分辨率定时器的原理和实现
利用定时器,我们可以设定在未来的某一时刻,触发一个特定的事件。所谓低分辨率定时器,是指这种定时器的计时单位基于jiffies值的计数,也就是说,它的精度只有1/HZ,假如你的内核配置的HZ是1000,那意味着系统中的低分辨率定时器的精度就是1ms。
回顾下Linux内核定时器
#include <linux/timer.h>
init_timer(struct timer_list *timer);
struct timer_list {
unsigned long expires;
void (*function)(unsigned long);
unsigned long data;
};
add_timer(struct timer_list * timer);
del_timer(struct timer_list * timer);
mod_timer(struct timer_list *timer, unsigned long expires);
很简单的一个借口,可惜Linux内核不用了,这里上新版本使用方法
#include <linux/timer.h>
struct timer_list {
unsigned long expires;
void (*function)(struct timer_list *);
u32 flags
};
struct TEST_INFO{
.....
struct timer_list timer;
};
timer_setup(struct timer_list * timer, callback, flags); // 初始化timer,
// 回调函数接口
static void ad7879_timer(struct timer_list *t)
{
struct TEST_INFO *ts = from_timer(ts, t, timer);
ad7879_ts_event_release(ts);
}
htimer
基于时钟源的时间操作接口
clocksource 时钟源
参考: Linux时间子系统之一:clock source(时钟源)
clock source 为linux内核提供一个时间基线(也是个计数器),如果你用linux的date命令获取当前时间,内核会读取当前的clock source,转换并返回合适的时间单位给用户空间。
在硬件层,它通常实现为一个由固定时钟频率驱动的计数器,计数器只能单调地增加,直到溢出为止。
时钟源是内核计时的基础,系统启动时,内核通过硬件RTC获得当前时间,在这以后,在大多数情况下,内核通过选定的时钟源更新实时时间信息(墙上时间),而不再读取RTC的时间
内核注册时钟源
#include <linux/clocksource.h>
int clocksource_register_hz(struct clocksource *cs, u32 hz); // API
参考:drivers/clocksource/ingenic-timer.c 的ingenic_tcu_clocksource_init 接口
struct clk *cs_clk; // 从设备树获取时钟源
clk_prepare_enable(tcu->cs_clk);
unsigned long rate = clk_get_rate(tcu->cs_clk); // 这就就是时钟频率
struct clocksource * cs;
cs->name = "ingenic-timer";
cs->rating = 200;
cs->flags = CLOCK_SOURCE_IS_CONTINUOUS;
cs->mask = CLOCKSOURCE_MASK(16);
cs->read = ingenic_tcu_timer_cs_read;
clocksource_register_hz(cs, rate);
这里有几个参数比较有意思:
- 利用mult和shift推算频率
- 每次clocksource注册,都会触发 clocksource_select 来使用rating 最好的值 作为系统时钟。并通知timekeeping。
- 全局变量 clocksource_list 为系统所有的clocksource,curr_clocksource 为__clocksource_select 后选择当前时钟 作为系统时钟使用。
# linux-git/kernel/time/jiffies.c 注册了 精度只有1/HZ秒,rating值为1的时钟源
static struct clocksource clocksource_jiffies = {
.name = "jiffies",
.rating = 1, /* lowest valid rating*/
.read = jiffies_read,
.mask = CLOCKSOURCE_MASK(32),
.mult = TICK_NSEC << JIFFIES_SHIFT, /* details above */
.shift = JIFFIES_SHIFT,
.max_cycles = 10,
};
static int __init init_jiffies_clocksource(void)
{
return __clocksource_register(&clocksource_jiffies);
}
timekeeper 墙上时间(重点)
clocksource 给我们提供的其实就是个计数器。
但我们真是使用时间基本上都基于 墙上时间(真实时间) 来计算,那么clocksource怎么转换为墙上时间?
Linux时间子系统之三:时间的维护者:timekeeper
Linux内核文档-ktime accessors 和 timekeeping.rst
注:
gettimeofday和do_gettimeofday 使用墙上时间,可内核在5.0不支持do_gettimeofday了, 所以这里主要考虑ktime
以及 current_kernel_time(void); 等 都不支持了,所以不再研究。
推荐使用:
参考:timekeeping.rst
#include <linux/time.h>
#include <linux/timekeeping.h>
void ktime_get_real_ts64(struct timespec64 *ts);
ktime_t ktime_get_real( void )
注:coarse 相关API是精度不准确的时间,在精度要求不高的情况可以使用
时间分类
内核管理着多种时间,它们分别是:
- RTC时间
- wall time:墙上时间 xtime(关键点)
- monotonic time
- raw monotonic time
- boot time:总启动时间
RTC时间 在PC中,RTC时间又叫CMOS时间,它通常由一个专门的计时硬件来实现,软件可以读取该硬件来获得年月日、时分秒等时间信息,而在嵌入式系统中,有使用专门的RTC芯片,也有直接把RTC集成到Soc芯片中,读取Soc中的某个寄存器即可获取当前时间信息。一般来说,RTC是一种可持续计时的,也就是说,不管系统是否上电,RTC中的时间信息都不会丢失,计时会一直持续进行,硬件上通常使用一个后备电池对RTC硬件进行单独的供电。因为RTC硬件的多样性,开发者需要为每种RTC时钟硬件提供相应的驱动程序,内核和用户空间通过驱动程序访问RTC硬件来获取或设置时间信息。
xtime xtime和RTC时间一样,都是人们日常所使用的墙上时间,只是RTC时间的精度通常比较低,大多数情况下只能达到毫秒级别的精度,如果是使用外部的RTC芯片,访问速度也比较慢,为此,内核维护了另外一个wall time时间:xtime,取决于用于对xtime计时的clocksource,它的精度甚至可以达到纳秒级别,因为xtime实际上是一个内存中的变量,它的访问速度非常快,内核大部分时间都是使用xtime来获得当前时间信息。xtime记录的是自1970年1月1日24时到当前时刻所经历的纳秒数。
monotonic time 该时间自系统开机后就一直单调地增加,它不像xtime可以因用户的调整时间而产生跳变,不过该时间不计算系统休眠的时间,也就是说,系统休眠时,monotoic时间不会递增。
raw monotonic time 该时间与monotonic时间类似,也是单调递增的时间,唯一的不同是:raw monotonic time“更纯净”,他不会受到NTP时间调整的影响,它代表着系统独立时钟硬件对时间的统计。
boot time 与monotonic时间相同,不过会累加上系统休眠的时间,它代表着系统上电后的总时间。
我们这里只关心xtime
timekeeper的初始化
start_kernel
->timekeeping_init();
->获取RTC时间-参考http://kernel.meizu.com/linux-time.html 中描述了,在RTC初始化后会更新timekeeping的
->clock = clocksource_default_clock(); // 获取默认时钟源
->tk_set_xtime(tk, &wall_time); // 设置默认xtime
# 时间的更新
/boot/config-5.4.0-58-generic
CONFIG_NO_HZ=y # 可能会在好几个tick后,do_timer才会被调用一次
do_timer 函数在几个tick后,调用clocksource来获取计数器值,来更新墙上时间
常用API接口
## 内核态墙上时间获取
#include <linux/timekeeping.h>
void getboottime64(struct timespec64 *ts) 获取系统启动时刻的实时时间
void get_monotonic_boottime(struct timespec *ts); 获取系统启动以来所经过的时间,包含休眠时间
ktime_t ktime_get_boottime(void); 获取系统启动以来所经过的c时间,包含休眠时间,返回ktime类型
ktime_t ktime_get(void); 获取系统启动以来所经过的c时间,不包含休眠时间,返回ktime类型
void ktime_get_ts(struct timespec *ts) ; 获取系统启动以来所经过的c时间,不包含休眠时间,返回timespec结构
unsigned long get_seconds(void); 返回xtime中的秒计数值
struct timespec current_kernel_time(void); 返回内核最后一次更新的xtime时间,不累计最后一次更新至今clocksource的计数值
// void getnstimeofday(struct timespec *ts); 获取当前时间,返回timespec结构 已删除
// void do_gettimeofday(struct timeval *tv); 获取当前时间,返回timeval结构 已删除
## 用户态墙上时间获取
#include <time.h>
clock_gettime(clockid_t clk_id, struct timespec *tp);
对应ID配置
xtime CLOCK_REALTIME
monotonic-time CLOCK_MONOTONIC
raw_time CLOCK_MONOTONIC_RAW
注:在Linux内核删除 do_gettimeofday的今天,我们使用什么来计算时间?(在: 33e26418193f58d1895f2f968e1953b1caf8deb7 版本删除,参考 timekeeping.rst)
在 Linux内核中的时间函数 描述了部分替代接口
struct timespec64 {
time64_t tv_sec; /* seconds */
long tv_nsec; /* nanoseconds */
};
其中,Ktime和其他结构体间转换可参考:Linux时间子系统之二:表示时间的单位和结构
# ktime和timespec64的转换
#define ktime_to_timespec64(kt) ns_to_timespec64((kt))
ktime_t timespec64_to_ktime(struct timespec64 ts);
ktime_t ktime_set(const s64 secs, const unsigned long nsecs); 可以将时间转为ktime_t的数值
s64 ktime_to_ns(const ktime_t kt);
int ktime_equal(const ktime_t cmp1, const ktime_t cmp2);
s64 ktime_to_us(const ktime_t kt);
s64 ktime_to_ms(const ktime_t kt);
ktime_t ns_to_ktime(u64 ns)
struct ktime得使用
相关API
#include <linux/ktime.h>
typedef signed long long s64;
typedef s64 ktime_t;
ktime_t ktime_set(const long secs, const unsigned long nsecs);
ktime_t ktime_sub(const ktime_t lhs, const ktime_t rhs);
ktime_t ktime_add(const ktime_t add1, const ktime_t add2);
ktime_t ktime_add_ns(const ktime_t kt, u64 nsec);
ktime_t ktime_sub_ns(const ktime_t kt, u64 nsec);
ktime_t timespec_to_ktime(const struct timespec ts);
ktime_t timeval_to_ktime(const struct timeval tv);
struct timespec ktime_to_timespec(const ktime_t kt);
struct timeval ktime_to_timeval(const ktime_t kt);
s64 ktime_to_ns(const ktime_t kt);
int ktime_equal(const ktime_t cmp1, const ktime_t cmp2);
s64 ktime_to_us(const ktime_t kt);
s64 ktime_to_ms(const ktime_t kt);
demo
# 内核态时间
// dmatest.c 中dmatest_func
#include <linux/ktime.h>
ktime_t ktime, start, diff;
ktime_t filltime = 0;
ktime_t comparetime = 0;
s64 runtime = 0;
ktime = ktime_get(); // 获取当前时间
start = ktime_get(); // 获取每次任务开始时间
..... // 任务处理
diff = ktime_sub(ktime_get(), start); // 获取任务处理所用得时间
filltime = ktime_add(filltime, diff); // 总时间
runtime = ktime_to_us(ktime); // 时间转为us数