Linux系统时间、时区、夏令时杂谈
本篇博文对Linux用户层的时间做一番深层次的探究,设计各个时间概念,获取方式,源码窥探。
一、示例
先从一个基本示例开始,源码如下:
1 static void dump_tm(const struct tm *t, const char *var) 2 { 3 d("dump %s --------\n", var); 4 d("%d-%d-%d ", t->tm_year, t->tm_mon, t->tm_mday); 5 d("%d:%d:%d\n", t->tm_hour, t->tm_min, t->tm_sec); 6 d("tm_wday: %d\n", t->tm_wday); 7 d("tm_yday: %d\n", t->tm_yday); 8 d("tm_isdst: %d\n\n", t->tm_isdst); 9 } 10 11 static void dump_tv(const struct timeval *tv, const char *var) 12 { 13 d("dump %s --------\n", var); 14 d("%ld(s) %ld(us)\n\n", tv->tv_sec, tv->tv_usec); 15 } 16 17 static void dump_ts(const struct timespec *ts, const char *var) 18 { 19 d("dump %s --------\n", var); 20 d("%ld(s) %ld(ns)\n\n", ts->tv_sec, ts->tv_nsec); 21 } 22 23 int main(int argc, char *argv[]) 24 { 25 system("date -R"); 26 d("\n\n"); 27 time_t time_now = time(NULL); 28 d("time_now: %ld\n", time_now); 29 30 // int gettimeofday(struct timeval *tv, struct timezone *tz); 31 struct timeval tv; 32 gettimeofday(&tv, NULL); 33 dump_tv(&tv, "tv"); 34 35 // struct tm *gmtime_r(const time_t *timep, struct tm *result); 36 struct tm tm_gmt; 37 gmtime_r(&time_now, &tm_gmt); 38 dump_tm(&tm_gmt, "tm_gmt"); 39 40 time_t time_mk_utc = mktime(&tm_gmt); 41 d("time_mk_utc: %ld\n\n", time_mk_utc); 42 43 // struct tm *localtime_r(const time_t *timep, struct tm *result); 44 struct tm tm_loc; 45 localtime_r(&time_now, &tm_loc); 46 dump_tm(&tm_loc, "tm_loc"); 47 48 time_t time_mk_loc = mktime(&tm_loc); 49 d("time_mk_loc: %ld\n\n", time_mk_loc); 50 51 // int clock_gettime(clockid_t clk_id, struct timespec *tp); 52 struct timespec tp; 53 if (clock_gettime(CLOCK_REALTIME, &tp) < 0) { 54 perror("CLOCK_REALTIME:"); 55 } else { 56 dump_ts(&tp, "CLOCK_REALTIME"); 57 } 58 59 if (clock_gettime(CLOCK_MONOTONIC, &tp) < 0) { 60 perror("CLOCK_MONOTONIC:"); 61 } else { 62 system("cat /proc/uptime"); 63 dump_ts(&tp, "CLOCK_MONOTONIC"); 64 } 65 66 return 0; 67 }
运行结果:
1 Fri, 14 Sep 2018 10:45:39 +0800 2 3 4 time_now: 1536893139 5 dump tv -------- 6 1536893139(s) 391362(us) 7 8 dump tm_gmt -------- 9 118-8-14 2:45:39 10 tm_wday: 5 11 tm_yday: 256 12 tm_isdst: 0 13 14 time_mk_utc: 1536864339 15 16 dump tm_loc -------- 17 118-8-14 10:45:39 18 tm_wday: 5 19 tm_yday: 256 20 tm_isdst: 0 21 22 time_mk_loc: 1536893139 23 24 dump CLOCK_REALTIME -------- 25 1536893139(s) 391606167(ns) 26 27 253437.13 1983789.53 28 dump CLOCK_MONOTONIC -------- 29 253437(s) 135226931(ns)
二、示例分析
首先说下”标准时间“和”本地时间“。
标准时间:现用的时间标准为通用协调时(UTC,Coordinated Universal Time),由世界上最精确的原子钟提供。
本地时间:UTC+时区就是对应的本地时间,也是我们日常使用的时间。
说到UTC就不得不提格林威治平均时(GMT,Greenwich Mean Time),这是一个本地时间,GMT=UTC+0,所以GMT和UTC时间值是相等的。
对于Unix/Linux等系统,还有一个词"Epoch",它指的是一个特定时间:1970-01-01 00:00:00 +0000 (UTC)。
1、"date -R":命令返回本地时间,-R选项附带时区信息+0800
2、time()返回从Epoch, 1970-01-01 00:00:00 +0000 (UTC)起到现在经过的秒数
3、gettimeofday()返回的时间秒值和time()是一样的,可以说time()是gettimeofday()低精度版本,因为time()的秒值就是通过gettimeofday()拿到的:
uClibc/libc/misc/time/time.c
time_t time(time_t *tloc) { struct timeval tv; struct timeval *p = &tv; gettimeofday(p, NULL); if (tloc) { *tloc = p->tv_sec; } return p->tv_sec; }
4、Linux中_r后缀表示该函数是相应函数的可重入版本。gmtime_r()是gmtime()的可重入(reentrant)版本,用于把time_t类型的秒值转换为struct tm类型
5、mktime()用于把struct tm类型转换为time_t类型的秒值,转换包含时区信息。由于系统设置为+8区,所以计算时会减去8个时区:
time_mk_utc = time_now - 22800(+8小时秒数) = 1536893139-22800=1536864339
6、localtime_r()用于把time_t类型的秒值转换为struct tm类型,转换包含时区信息。48行经过mktime()的-8,得到最初time()返回的秒值。
7、CLOCK_REALTIME获取的是墙上时间(Wall time),该时间由系统启动时从RTC读取,在系统运行期间由系统时钟维护并在合适的时刻和RTC芯片进行同步。
CLOCK_MONOTONIC取的是相对时间,该时间由系统通过jiffies值来计算,不受时钟源的影响。
8、/proc/uptime文件保存从系统启动到现在的时间(以秒为单位)。
此外,还有一种叫”calendar time“的称呼,其和墙上时间是等效的。
三、时区和夏令时
看下uClibc/libc/misc/time/time.c中mktime()的源码:
time_t mktime(struct tm *timeptr) { time_t t; tzset(); t = _time_mktime_tzi(timeptr, store_on_success, _time_tzinfo); return t; }
如前所说,在执行转换前进行时区设置,那么时区怎么来的:
void _time_tzset(int use_old_rules) { e = getenv(TZ); /* TZ env var always takes precedence. */ if (!e) e = read_TZ_file(buf); tzname[0] = _time_tzinfo[0].tzname; tzname[1] = _time_tzinfo[1].tzname; daylight = !!_time_tzinfo[1].tzname[0]; timezone = _time_tzinfo[0].gmt_offset; }
1,查阅TZ环境变量;
2、如果TZ环境变量不存在,从系统文件读取,嵌入式Linux中该文件为"/etc/TZ"
3、根据获取的值初始化变量,设置时区名称(tzname),设置夏令时(DST)标志(daylight),设置时区相对于UTC偏移值(单位秒)
在构建嵌入式Linux系统时候,TZ的值需要我们自己设置正确,那么看下TZ的格式:
注意:规定地理位置相对于GMT以西为+,以东为-,我们位于东8区,所以在设置TZ的时候是"-"号。
The value of TZ can be one of three formats. The first format is used when there is no daylight saving time in the local timezone:
std offset
The std string specifies the name of the timezone and must be three or more alphabetic characters(经测试在uClibc中,字符个数最小3个,最多6个). The offset string immediately follows std
and specifies the time value to be added to the local time to get Coordinated Universal Time (UTC). The offset is positive if the local timezone is west of the Prime Meridian and negative if it is east. The hour must be between 0 and 24, and the minutes and seconds 0 and 59.
# echo "UTC+08:00" > /etc/TZ # date -R Sun, 16 Sep 2018 04:53:52 -0800 # echo "UTC-08:00" > /etc/TZ # date -R Sun, 16 Sep 2018 20:50:29 +0800 # echo "UTC-0800" > /etc/TZ # date -R Sun, 16 Sep 2018 20:50:36 +0800 # echo "xxxx-0800" > /etc/TZ # date -R Sun, 16 Sep 2018 20:51:33 +0800 # echo "xxxxxx-0800" > /etc/TZ # date -R Sun, 16 Sep 2018 20:51:43 +0800 # echo "xxxxxxx-0800" > /etc/TZ # date -R Sun, 16 Sep 2018 12:51:47 +0000
The second format is used when there is daylight saving time:
std offset dst [offset],start[/time],end[/time]
There are no spaces in the specification. The initial std and offset specify the standard timezone, as described above. The dst string and offset specify the name and offset for the corresponding daylight saving timezone. If the offset is omitted, it default to one hour ahead of standard time(如果不指定夏令时的偏移时间,默认提前1小时,即3600s).
The start field specifies when daylight saving time goes into effect and the end field specifies when the change is made back to standard time. These fields may have the following formats:
Jn This specifies the Julian day with n between 1 and 365. February 29 is never counted even in leap years.
n This specifies the Julian day with n between 1 and 365. February 29 is counted in leap years.
Mm.w.d This specifies day d (0 <= d <= 6) of week w (1 <= w <= 5) of month m (1 <= m <= 12). Week 1 is the first week in which day d occurs and week 5 is the last week in which day d occurs. Day 0 is a Sunday.
The time fields specify when, in the local time currently in effect, the change to the other time occurs. If omitted, the default is 02:00:00.
Here is an example for New Zealand, where the standard time (NZST) is 12 hours ahead of UTC, and daylight saving time (NZDT), 13 hours ahead of UTC, runs from the first Sunday in October to the third Sunday in March, and the changeovers happen at the default time of 02:00:00:
TZ="NZST-12:00:00NZDT-13:00:00,M10.1.0,M3.3.0"
# echo NZST-12:00:00NZDT-13:00:00,M10.1.0,M3.3.0 > /etc/TZ # date -R Mon, 17 Sep 2018 01:01:56 +1200 # echo NZST-12:00:00NZDT-13:00:00,M9.1.0,M12.3.0 > /etc/TZ # date -R Mon, 17 Sep 2018 02:02:29 +1300
The third format specifies that the timezone information should be readfrom a file.
/usr/share/zoneinfo /etc/localtime # zdump /etc/localtime /etc/localtime Sun Sep 16 21:19:48 2018 CST
如果系统不是由自己构建的,夏令时的处理参见“说说unix下的夏令时问题”。
四、uClib中夏令时的处理
在进行时间转换时,如果仅仅设置struct tm结构体的tm_isdst成员是不起作用的,mktime()对夏令时标志的处理流程参见源码:
time_t _time_mktime_tzi(struct tm *timeptr, int store_on_success, rule_struct *tzi) { time_t t; struct tm x; /* 0:sec 1:min 2:hour 3:mday 4:mon 5:year 6:wday 7:yday 8:isdst */ int *p = (int *) &x; const unsigned char *s; int d, default_dst; memcpy(p, timeptr, sizeof(struct tm)); /* 1、如果没有设置夏令时所在的时区,tm_isdst成员被重置为0 */ if (!tzi[1].tzname[0]) { /* No dst in this timezone, */ p[8] = 0; /* so set tm_isdst to 0. */ } default_dst = 0; /* 2、如果tm_isdst成员非零,设置为1或-1 */ if (p[8]) { /* Either dst or unknown? */ default_dst = 1; /* Assume advancing (even if unknown). */ p[8] = ((p[8] > 0) ? 1 : -1); /* Normalize so abs() <= 1. */ } return t; }
五、总结
以上叙述了下Unix/Linux下应用编程设计到的时间概念及内部原理。实际编码过程中,如果遇到时间差异的问题,可以从API是否绑定时区、夏令时、系统时区配置等方面切入分析;考虑到各个发行版、库版本等的差异,必要时应该研读源码。