1. Linux 时间子系统
Linux时间系统是一个由多个不同组件构成的复杂系统,它主要包括硬件时钟、系统时钟、时间同步、时间处理等多个部分。以下是基于x86架构Linux时间系统主要的原理和工作流程:时间子系统的核心是Tick层,它的核心是基于硬件设备时钟事件触发的中断hrtimer interrupt。hrtimer会驱动上次的内核功能,比如更新jiffies;读取时钟源的数据并把数据同步到内核的timekeeper数据结构里;唤醒睡眠的进程等等。
1.1时钟源
硬件时钟源通常是一种低功率的晶体振器,通过振荡产生稳定的时钟信号,产生一个单调递增的计数,内核负责把这个计数转化为系统当前的时间点。通常这个计数的更新是由硬件完成的,比如目前主流64位服务器的首选时钟源为更新频率接近cpu的主频的tsc (时间戳计数器),tsc是一个高精度的时钟源,为固定的频率,可以使用rdtsc指令访问。可使用命令cat /sys/devices/system/clocksource/clocksource0/current_clocksource查询当前的时钟源。
除此之外系统还有一些non-stop的低精度时钟源比如硬件时钟寄存器rtc,rtc在下电的时候可以独立持续运行,一般用于启动时恢复初始时间。
1.2 时钟事件
时钟事件通常由底层硬件支持,通常是可编程的,负责在某个时间点触发中断,内核基于定时中断来做CPU调度,同时更新全局时间。通常时钟事件会引发Per-CPU level和全局level的tick更新。
1.3 Tick
Tick是一个逻辑概念,就是周期性的时钟中断,用来作为OS的心跳,它可以驱动scheduler运转,并且统计相关的运行信息。内核编译的时候会指定HZ这个参数,通常为1000,表示在系统中,每个Tick是1/HZ秒,这样把Tick和时间关联起来, Tick的数量由jiffies这个变量来表示。
1.4 Jiffies
Jiffies为Linux核心变量,它被用来纪录系统自开机以来,已经过多少的tick。每发生一次timer interrupt,Jiffies变数会被加1。
2. C库时间函数总结
用户态层面的用户使用时间主要基于c库函数,常用的获取与使用函数总结如下:
2.1 time()获取时间戳
函数定义如下:
time_t time(time_t *calptr);
time_t 是一个unsigned long类型,time函数用来获取日历时间的时间戳,该时间戳是从1970年1月1日0点(00:00:00 UTC, January 1, 1970)到现在经历的秒数。
calptr = NULL时,为得到当前日历时间(从1970-01-01 00:00:00到现在的秒数);
calptr = 时间数值时,用于设置日历时间,其返回值存储在timer中;
2.2 gettimeofday()和clock_gettime()高精度的时间
函数定义如下:
int gettimeofday(struct timeval *tp, void *tzp);。
int clock_gettime(clockid_t clock_id, strcut timespec *tsp);
time函数只能得到秒精度的时间,为了获得更高精度的时间戳,需要其他函数。gettimeofday函数可以获得微秒精度的时间戳,用结构体timeval来保存;clock_gettime函数可以获得纳秒精度的时间戳,用结构体timespec来保存。
两个函数使用的结构体定义如下:
struct timeval
{
long tv_sec; //秒
long tv_usec; //微秒
};
struct timespec
{
time_t tv_sec; //秒
long tv_nsec; //纳秒
};
2.3 gmtime()和localtime()时间戳-可视化时间转化
函数定义如下:
struct tm* gmtime(const time_t *timep);
struct tm* localtime(const time_t *timep);
gmtime将time_t格式的时间数值变换成格林威治标准时区时间,没有夏令时。
localtime将time_t格式的时间数值变换成本地时间,考虑到本地时区和夏令时标志;
得到的时间戳不能直观的展示现在的时间,为此需要使用tm结构体来表示成我们日常所见的时间,gmtime 和localtime可以将time_t类型的时间戳转为tm结构体。两者的区别是gmtime转换后的事件是对时间戳基于UTC进行转换;而localtime会基于当前系统的时区进行转换。
tm结构体定义如下:
struct tm
{
int tm_sec; /*秒,正常范围0-59, 但允许至61*/
int tm_min; /*分钟,0-59*/
int tm_hour; /*小时, 0-23*/
int tm_mday; /*日,即一个月中的第几天,1-31*/
int tm_mon; /*月, 从一月算起,0-11*/
int tm_year; /*年, 从1900至今已经多少年*/
int tm_wday; /*星期,一周中的第几天, 从星期日算起,0-6*/
int tm_yday; /*从今年1月1日到目前的天数,范围0-365*/
int tm_isdst;
};
2.4 mktime()可视化时间-时间戳转化
函数定义如下:
time_t mktime(struct tm *tm);
mktime将tm格式时间转换为自1970年1月1日以来持续时间的秒数
在业务程序中,经常需要对比时间的先后,如果用字符串格式时间进行对比,需要保证格式完全一致,而且转为字符串形式也比较麻烦,因此更多时候用时间戳来进行比较。有的时候就需要将字符串形式的时间或者struct tm表示的时间转化为time_t的时间戳,这个转化就可以通过mktime函数来实现。
2.5 strftime()时间转换函数
函数定义如下:
size_t strftime(char* buf, size_t maxsize, const char *format, const struct tm *tmptr);
我们可以根据format指向字符串中格式,将timeptr中存储的时间信息按照format指定的形式输出到buf中,最多向缓冲区buf中存放maxsize个字符。该函数返回向buf指向的字符串中放置的字符数。
参数解释:
buf:存储输出的时间
maxsize:缓存区的最大字节长度
format:指定输出时间的格式
tmptr:指向结构体tm指针
函数strftime()的操作有些类似于sprintf():识别以百分号(%)开始的格式命令集合,格式化输出结果放在一个字符串中。格式化命令说明串 strDest中各种日期和时间信息的确切表示方法。格式串中的其他字符原样放进串中。格式命令列可以参考linux man手册。