Linux内核之时间系统

1、Linux时间系统

Linux系统中有两个时钟源,一个叫做RTC,另一个叫做系统时钟。时钟运作机制如下图:
在这里插入图片描述

(1)CMOS时钟

RTC(Real Time Clock,实时时钟)也叫做CMOS时钟,它独立于操作系统,由PC主板上的一块纽扣电池供电,当操作系统关机的时候,用它来记录时间,当系统启动时,内核通过读取RTC来初始化墙上时间,它为计算机提供一个计时标准,是最原始最底层的时钟数据。(后面介绍墙上时间)

(2)系统时钟

系统时钟是由操作系统控制PC主板上的定时/计数芯片来工作的,它依赖CMOS时钟而启动,初始化后由操作系统完全管理,操作系统通过OS时钟提供给应用程序所有和时间有关的服务。系统时钟产生于PC主板上的定时/计数芯片,常见的有8253/8254可编程定时/计数芯片,其工作原理是由晶振、电容等组成的振荡电路,产生脉冲(高低电平),这些脉冲输入到中断控制器上,产生中断信号,触发时钟中断,由时钟中断服务程序维持OS时钟正常工作。
系统时钟记录的时间也就是我们常见的系统时间,它是以“时钟节拍”为单位的,时钟中断的频率(节拍率)决定了一个时钟节拍的长短。节拍率是通过静态预处理定义的,也就是Hz(赫兹),Linux内核版本4.19中是这样定义的:

#ifndef __ASM_GENERIC_PARAM_H
#define __ASM_GENERIC_PARAM_H

#include <uapi/asm-generic/param.h>

# undef HZ
# define HZ		CONFIG_HZ	/* Internal kernel timer frequency */
# define USER_HZ	100		/* some user interfaces are */
# define CLOCKS_PER_SEC	(USER_HZ)       /* in "ticks" like times() */
#endif /* __ASM_GENERIC_PARAM_H */

一个tick代表多长时间,在内核的CONFIG_HZ中定义。比如CONFIG_HZ=200,则一个jiffies对应5ms时间,所以内核基于jiffies的定时器精度也是5ms。(下面介绍jiffies)

(3)节拍数(jiffies)

jiffies是Linux内核中的一个全局变量,用来记录自系统启动以来产生的节拍总数。启动时内核将该变量初始化为0,此后每次时钟中断jiffies的值+1,每一秒钟中断次数HZ,jiffies一秒内增加HZ。

  • 将秒转换为jiffies可采用公式:seconds*HZ
  • jiffies转换为秒可采用公式:jiffies/HZ
  • 系统运行时间(s) = jiffies/HZ

jiffies用途:计算流逝时间和时间管理。
jiffies 变量总是无符号长整数(Unsigned Long)。因此,在32位体系结构上是32位,在64位体系结构是64位,当 jiffies 的值超过它的最大存放范围后就会发生溢出,它的值会回绕到0。内核提供了这样的宏来帮助解决由于jiffies溢出而造成程序逻辑出错的情况:

#define time_after(a,b)		\
	(typecheck(unsigned long, a) && \
	 typecheck(unsigned long, b) && \
	 ((long)((b) - (a)) < 0))
#define time_before(a,b)	time_after(b,a)

#define time_after_eq(a,b)	\
	(typecheck(unsigned long, a) && \
	 typecheck(unsigned long, b) && \
	 ((long)((a) - (b)) >= 0))
#define time_before_eq(a,b)	time_after_eq(b,a)

在宏time_after中,首先确保两个输入参数a和b的数据类型为unsigned long,然后才执行实际的比较。

(4)墙上时间(xtime)

墙上时间就是实际时间,实际时间的获取是在开机后,内核初始化时从RTC读取的。内核读取这个时间后就将其放入内核中的 xtime 变量中,并且在系统的运行中不断更新这个值。内核中并不常用墙上时间,主要是方便用户空间的程序获取当前时间。它的精度可以达到纳秒级别,因为xtime实际上是一个内存中的变量,它的访问速度非常快,内核大部分时间都是使用xtime来获得当前时间信息。Linux中的xtime记录的是自1970年1月1日0时到当前时刻所经历的纳秒数。

2、重要数据结构

Linux内核提供各种time line,real time clock,monotonic clock、monotonic raw clock等,timekeeping模块就是负责跟踪、维护这些timeline的,并且向其他模块(timer相关模块、用户空间的时间服务等)提供服务,而timekeeping模块维护timeline的基础是基于clocksource模块和tick模块。通过tick模块的tick事件,可以周期性的更新time line,通过clocksource模块、可以获取tick之间更精准的时间信息。
Linux内核版本4.19中在linux-4.19\include\linux\timekeeper_internal.h下有这样两个数据结构:

(1)struct tk_read_base

读取时间的基本结构tk_read_base,内核版本4.19源码如下:

struct tk_read_base {
	struct clocksource	*clock;
	u64			mask;
	u64			cycle_last;
	u32			mult;
	u32			shift;
	u64			xtime_nsec;
	ktime_t			base;
	u64			base_real;
};

其中:

  • clock:timekeeping当前使用的时钟源

这个clock应该是系统中最优的那个,如果有好过当前clocksource注册入系统,那么clocksource模块会通知timekeeping模块来切换clocksource。

  • mask:非64位时钟的二进制补码减法的位掩码
  • cycle_last:最后一次更新的时钟周期值
  • mult :(经过NTP调整)数学换算的乘数
  • shift:数学换算的移位值
  • xtime_nsec:用于读出纳秒的偏移量
  • base:用于读取ktime_t(纳秒)的基本时间
  • base_real:用于读出实际时间的基本纳秒值
  • base_real:用于快速NMI安全访问器,以允许读取时钟

此结构在64位上的大小为56字节,连同一个seqcount占用64字节缓存。
这个结构体与timekeeper结构是分开的,因为它也被使用用于快速NMI安全访问器。
base_real用于快速NMI安全访问器以允许从任何上下文读取实际时间。

(2)struct timekeeper

timekeeper结构用于保存计时值,内核版本4.19源码如下:

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;

	/* The following members are for timekeeping internal use */
	u64			cycle_interval;
	u64			xtime_interval;
	s64			xtime_remainder;
	u64			raw_interval;
	/* The ntp_tick_length() value currently being used.
	 * This cached copy ensures we consistently apply the tick
	 * length for an entire tick, as ntp_tick_length may change
	 * mid-tick, and we don't want to apply that new value to
	 * the tick in progress.
	 */
	u64			ntp_tick;
	/* Difference between accumulated time and NTP time in ntp
	 * shifted nano seconds. */
	s64			ntp_error;
	u32			ntp_error_shift;
	u32			ntp_err_mult;
	/* Flag used to avoid updating NTP twice with same second */
	u32			skip_second_overflow;
#ifdef CONFIG_DEBUG_TIMEKEEPING
	long			last_warning;
	/*
	 * These simple flag variables are managed
	 * without locks, which is racy, but they are
	 * ok since we don't really care about being
	 * super precise about how many events were
	 * seen, just that a problem was observed.
	 */
	int			underflow_seen;
	int			overflow_seen;
#endif
};

其中:

  • tkr_mono:CLOCK_MONOTONIC的读出基本结构
  • tkr_raw:CLOCK_MONOTONIC_RAW的读出基本结构
  • xtime_sec:当前的CLOCK_REALTIME时间,以秒为单位

Linux中CLOCK_REALTIME time,直接使用秒以及纳秒在当前秒内的偏移来表示。这里xtime_sec用秒这个的刻度单位来度量CLOCK_REALTIME time line上,时间原点到当前点的距离值。当然xtime_sec是一个对current time point的取整值,为了更好的精度,还需要一个纳秒表示的offset,也就是在刚才那个数据结构tk_read_base结构中的xtime_nsec。不过为了内核内部计算精度(内核对时间的计算是基于cycle的),并不是保存了时间的纳秒偏移值,而是保存了一个shift之后的值,因此,用户看来,当前时间点的值应该是距离时间原点xtime_sec + (xtime_nsec << shift)距离的那个时间点值。

  • ktime_sec:当前的CLOCK_MONOTONIC时间,以秒为单位
  • wall_to_monotonic:从CLOCK_REALTIME到CLOCK_MONOTONIC偏移

CLOCK_MONOTONIC类型的系统时钟。这种系统时钟并没有像墙上时钟一样定义一个相对于linux epoch的值,这个成员定义了monotonic clock到real time clock的偏移,也就是说,这里的wall_to_monotonic和offs_real需要加上real time clock的时间值才能得到monotonic clock的时间值。wall_to_monotonic和offs_real的意思是一样的,不过时间的格式不一样,用在不同的场合,以便获取性能的提升。

  • offs_real:偏移时钟单调->时钟实时
  • offs_boot:偏移时钟单调->时钟启动时间
  • offs_tai:偏移时钟单调->时钟tai
  • tai_offset:当前UTC到TAI的偏移量,以秒为单位

CLOCK_TAI类型的系统时钟。TAI(international atomic time)是原子钟,在时间的基本概念文档中,UTC就是base TAI的,也就是说用铯133的振荡频率来定义秒的那个时钟,CLOCK_TAI类型的系统时钟就是完完全全使用铯133的振荡频率来定义秒的那个时钟。

  • clock_was_set_seq:时钟被设置事件的序号
  • cs_was_changed_seq:时钟源更改事件的序列号
  • next_leap_ktime:待处理的leap秒的CLOCK_MONOTONIC时间值
  • raw_sec:CLOCK_MONOTONIC_RAW时间以秒为单位
  • cycle_interval:一个NTP间隔中的时钟周期数
  • xtime_interval:一个NTP中的时钟移位纳秒数间隔
  • xtime_remainder:四舍五入时还剩纳秒
  • cycle_interval:一个NTP间隔中的时钟周期数
  • raw_interval:每个NTP间隔累积的偏移原始毫微秒
  • ntp_error:ntp中的累积时间和NTP时间之间的差偏移了纳秒
  • ntp_error_shift:时钟移位纳秒和ntp移位纳秒之间的转换
  • last_warning:警告速率限制器(DEBUG_TIMEKEEPING)
  • underflow_seen:下溢警告标志(DEBUG_TIMEKEEPING)
  • overflow_seen:溢出警告标志(DEBUG_TIMEKEEPING)

(3)内核中表示时间的数据结构

在内核版本4.19linux-4.19\include\uapi\linux\time.h中有这样几个表示时间的数据结构,源码如下:

#ifndef _STRUCT_TIMESPEC
#define _STRUCT_TIMESPEC
struct timespec {
	__kernel_time_t	tv_sec;			/* seconds */
	long		tv_nsec;		/* nanoseconds */
};
#endif

该数据结构来自于POSIX.1b规范,用于在用户态和内核态之间传递时间信息。POSIX还定义了许多API供用户态调用,例如clock_gettime(),clock_settime()等,其中的表示时间的结构都是timespec。

struct timeval {
	__kernel_time_t		tv_sec;		/* seconds */
	__kernel_suseconds_t	tv_usec;	/* microseconds */
};

该结构也用于用户态和内核态间的时间信息传递。不同于timespec的是它的两个成员分别是秒和微妙,精度要比timespec差。另外一个区别是相关API也不同,timeval相关的API是gettimeofday()等。

3、Linux常用的关于时间的命令

命令如下,结果如图:
(1)查看时间和日期

date

在这里插入图片描述
(2)设置时间和日期
将日期设置为2019年11月18日

date -s 11/18/2019

将系统时间设定成下午6点16分26秒

date -s 6:16:26

在这里插入图片描述
(3)同步网络时间

ntpdate -u 0.asia.pool.ntp.org

在这里插入图片描述
(4)将当前时间和日期写入BIOS,避免重启后失效

hwclock -w

(5)查看月历

call

在这里插入图片描述
更多关于时间的操作,可以使用命令man date来查看。。。

猜你喜欢

转载自blog.csdn.net/qq_34258344/article/details/102998727