本文介绍与linux全局时间变量和C库gettimeofday和settimeofday的底层操作。
软件平台:linux 4.x.x
1.基础原理
1.1 时钟源
晶振
(或其它时钟源)是嵌入式系统的“心跳
”。其输出可视为一个个脉冲,每个脉冲视为一个节拍(tick)
,每两个tick间的时间几乎是一定的,其频率称为节拍率(tick rate)
。
补充:
实际上,晶振起到类似滤波器的效果,仅允许特定频率的信号通过,该频率为晶振的谐振频率。而且,晶振电路输出的信号,还需经过分频或倍频处理,才最终作为CPU工作的心跳。
1.2 时间
所谓时间
,便是对该tick的统计结果。犹如沙漏计时一般,只是此刻的时间用电信号的tick
衡量。那么,如何获取时间呢?
根据晶振的物理特性,我们可以推断出晶振的tick rate
,根据倍频或分频电路后,得到实际输入系统的频率,此时称为系统频率,称为HZ
。
我们设计一个计数器(很常见的一类电子器件),每检测到一个脉冲,计数值+1。将该计数器与晶振电路相接,便可以实现实时统计脉冲。当计数器时数为jiffies
时,此时对应的时间为jiffies/HZ
。
由此可以看出,时间和计数器密不可分,有时候,这两者会混为一谈。(毕竟,它两取值之间成线性关系<因为HZ为常数>)
补充:
当前,时间计算不会那么简单,还有单位换算、计数器溢出、初始相位偏移、频率偏移等问题需要考虑。
2. 内核时间变量
2.1 linux HZ和jiffies
linux系统中,通过全局HZ
和jiffies
,表示节拍率和自系统启动以来的节拍总数。两者定义在:
asm/param.h //不绝对啊,有些通过CONFIG_HZ配置,或者其他文件中定义
linux/jiffies.h //应该没跑了,就是这个文件
extern unsigned long volatile __cacheline_aligned_in_smp __jiffy_arch_data jiffies;
想知道当前系统的两个参数取值如何,在命令行查看:
cat /proc/cpuinfo | grep z
如果已知源码,可以在配置文件下查看是否CONFIG_HZ定义:
CONFIG_HZ_250=y
# CONFIG_HZ_300 is not set
# CONFIG_HZ_1000 is not set
CONFIG_HZ=250
此处设置的系统频率为250Hz,(换算成时间为4ms)。
2.2 linux内核时间
在多核CPU及多进程并发和并行的情况下,我们需要关注一下两种时间:
(1)真实时间:UTC,也称为Calendar time
(2)进程时间:进程占用CPU的时间
本文讲述的时真实时间。
2.1.1 时间全局变量
(1)时间存储在哪里?
//kernel/time/timekeeping.c
static struct {
seqcount_t seq;
struct timekeeper timekeeper;
} tk_core ____cacheline_aligned;
tk_core
就是保存内核时间信息的变量。____cacheline_aligned宏定义指示编译器对应于L1缓存行开头的地址处实例化一个结构体或变量(请参考其他文献,本文不深入讨论)。
(2)seqcount 和timekeeper 成员
该变量的两个成员函数类型如下:
//include/linux/seqlock.h
typedef struct seqcount {
unsigned sequence;
#ifdef CONFIG_DEBUG_LOCK_ALLOC
struct lockdep_map dep_map;
#endif
} seqcount_t;
//include/linux/timekeeper_internal.h
struct timekeeper {
struct tk_read_base tkr_mono;
struct tk_read_base tkr_raw;
u64 xtime_sec;
unsigned long ktime_sec;
struct timespec64 wall_to_monotonic;
ktime_t offs_real;
ktime_t offs_boot;
ktime_t offs_tai;
s32 tai_offset;
unsigned int clock_was_set_seq;
u8 cs_was_changed_seq;
ktime_t next_leap_ktime;
u64 raw_sec;
u64 cycle_interval;
u64 xtime_interval;
s64 xtime_remainder;
u64 raw_interval;
u64 ntp_tick;
s64 ntp_error;
u32 ntp_error_shift;
u32 ntp_err_mult;
#ifdef CONFIG_DEBUG_TIMEKEEPING
long last_warning;
int underflow_seen;
int overflow_seen;
#endif
};
3. C库函数
3.1 gettimeofday函数如何获取到时间?
gettimeofday底层调用__getnstimeofday64函数,调用流程如下:
gettimeofday底层就是读取tk_core
全局变量。
注意:以下函数原型有适当省略。
(1)gettimeofday->do_gettimeofday
void do_gettimeofday(struct timeval *tv)
{
struct timespec64 now;
getnstimeofday64(&now);//读取时间
tv->tv_sec = now.tv_sec;
tv->tv_usec = now.tv_nsec/1000;//完成时间格式转换
}
EXPORT_SYMBOL(do_gettimeofday);
(2)do_gettimeofday->getnstimeofday64
void getnstimeofday64(struct timespec64 *ts)
{
WARN_ON(__getnstimeofday64(ts));//一个接口
}
EXPORT_SYMBOL(getnstimeofday64);
(3)__getnstimeofday64(核心)
int __getnstimeofday64(struct timespec64 *ts)
{
struct timekeeper *tk = &tk_core.timekeeper;//全局变量tk_core
unsigned long seq;
u64 nsecs;
do {
seq = read_seqcount_begin(&tk_core.seq);
ts->tv_sec = tk->xtime_sec;//读取全局变量tk_core的xtime_sec变量
nsecs = timekeeping_get_ns(&tk->tkr_mono);//获取ns值
} while (read_seqcount_retry(&tk_core.seq, seq));
ts->tv_nsec = 0;
timespec64_add_ns(ts, nsecs);//将nsec添加入结构体ts中。完成的是ts->tv_nsec=nsecs;
if (unlikely(timekeeping_suspended))
return -EAGAIN;
return 0;
}
EXPORT_SYMBOL(__getnstimeofday64);
3.2 settimeofday如何设置时间?
settimeofday底层调用do_settimeofday64函数,调用流程如下:
(1)settimeofday->do_settimeofday
static inline int do_settimeofday(const struct timespec *ts)
{
struct timespec64 ts64;
ts64 = timespec_to_timespec64(*ts);//时间格式转换
return do_settimeofday64(&ts64);
}
(2)do_settimeofday->do_settimeofday64
int do_settimeofday64(const struct timespec64 *ts)
{
struct timekeeper *tk = &tk_core.timekeeper;//指向时钟全局变量tk_core
struct timespec64 ts_delta, xt;
unsigned long flags;
int ret = 0;
if (!timespec64_valid_strict(ts))
return -EINVAL;
raw_spin_lock_irqsave(&timekeeper_lock, flags);
write_seqcount_begin(&tk_core.seq);
timekeeping_forward_now(tk);
xt = tk_xtime(tk);//xt.tv_sec = tk->xtime_sec; xt.tv_nsec = (long)(tk->tkr_mono.xtime_nsec >> tk->tkr_mono.shift);
ts_delta.tv_sec = ts->tv_sec - xt.tv_sec;//获取要设置的时间ts与当前系统时间tx之间的时间差
ts_delta.tv_nsec = ts->tv_nsec - xt.tv_nsec;
if (timespec64_compare(&tk->wall_to_monotonic, &ts_delta) > 0) {
ret = -EINVAL;
goto out;
}
tk_set_wall_to_mono(tk, timespec64_sub(tk->wall_to_monotonic, ts_delta));
tk_set_xtime(tk, ts);// tk->xtime_sec = ts->tv_sec; tk->tkr_mono.xtime_nsec = (u64)ts->tv_nsec << tk->tkr_mono.shift;
out:
timekeeping_update(tk, TK_CLEAR_NTP | TK_MIRROR | TK_CLOCK_WAS_SET);
write_seqcount_end(&tk_core.seq);
raw_spin_unlock_irqrestore(&timekeeper_lock, flags);
clock_was_set();
return ret;
}
EXPORT_SYMBOL(do_settimeofday64);
settimeofday就是设置全局变量tk_core
。
参考资料:
[1] 《linux内核设计与实现》
[2] 《linux/Uinx系统编程》
写在最后的话:
作者水平有限,如有错误,欢迎大家指出。谢谢~