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是否綁定時區、夏令時、系統時區配置等方面切入分析;考慮到各個發行版、庫版本等的差異,必要時應該研讀源碼。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM