開門見山先上圖
界定一些術語,方便後面說明:
unix 通過介面 time 將 Epoch 作為整數返回,自然的包含了日期和時間兩部分:
time_t time(time_t *tloc);
其中 time_t 在 64 位系統上是 8 位元組整數 (long long):
sizeof (time_t) = 8
在 32 位系統上可能是 4 位元組整數,沒有試。
time 例程的 tloc 引數如果不為空,則時間值也存放在由 tloc 指向的單元內。
如果想獲取更精準的時間,需要藉助另外的介面:
int gettimeofday(struct timeval *tv, struct timezone *tz);
時間通過引數 tv 返回:
struct timeval {
time_t tv_sec; /* seconds */
suseconds_t tv_usec; /* microseconds */
};
除了代表 UTC 的 tv_sec 外還有代表微秒的 tv_usec,注意如果只需要精確到毫秒,需要將這個值除以 1000。
在 64 位 CentOS 上它是 8 位元組整數:
sizeof (suseconds_t) = 8, sizeof (struct timeval) = 16
不過不是所有 64 位系統這個欄位都是 long long,在 64 位 Darwin 上它是 4 位元組整數:
sizeof (suseconds_t) = 4, sizeof (struct timeval) = 16
但最終 timeval 結構體的長度還是 16,可能是記憶體對齊的緣故。
tz 引數用來指定時區資訊:
struct timezone {
int tz_minuteswest; /* minutes west of Greenwich */
int tz_dsttime; /* type of DST correction */
};
因為一些原因,tz 在 SUS 標準中唯一合法值是 NULL,某些平臺支援使用 tz 說明時區,但完全沒有可移植性,例如在 Linux 上,建議這個引數設定為 NULL:
The use of the timezone structure is obsolete; the tz argument should normally be specified as NULL. (See NOTES below.)
不為 NULL 也不會報錯,但是不會修改指定引數的內容。Darwin 支援這個引數,下面是它的日常返回:
minuteswest = -480, dsttime = 0
具體可參考時區和夏時制一節。
time_t 型別利於介面返回,但可讀性比較差,需要將它轉換為人能理解的日期和時間。
struct tm {
int tm_sec; /* seconds */
int tm_min; /* minutes */
int tm_hour; /* hours */
int tm_mday; /* day of the month */
int tm_mon; /* month */
int tm_year; /* year */
int tm_wday; /* day of the week */
int tm_yday; /* day in the year */
int tm_isdst; /* daylight saving time */
};
這就是 struct tm,除了年月日時分秒,還有兩個欄位 wday / yday 用於方便的展示當前周/年中的天數,另外 isdst 標識了是否為夏時制 (參考夏時制一節)。
int tm_sec; /* seconds (0 - 60) */
int tm_min; /* minutes (0 - 59) */
int tm_hour; /* hours (0 - 23) */
int tm_mday; /* day of month (1 - 31) */
int tm_mon; /* month of year (0 - 11) */
int tm_year; /* year - 1900 */
int tm_wday; /* day of week (Sunday = 0) */
int tm_yday; /* day of year (0 - 365) */
int tm_isdst; /* is summer time in effect? */
char *tm_zone; /* abbreviation of timezone name */
long tm_gmtoff; /* offset from UTC in seconds */
上面給出了各個欄位的取值範圍,有幾個點值得注意:
如果直接用這個結構體顯示給使用者,經常會看到以下校正程式碼:
printf ("%04d/%02d/%02d %02d:%02d:%02d",
tm.tm_year + 1900, tm.tm_mon + 1, tm.tm_mday,
tm.tm_hour, tm.tm_min, tm.tm_sec);
對 yday 的處理類似 mon。
再複習一下開始的關係圖:
將 time_t 轉換為 struct tm 的是 localtime 和 gmtime,反過來是 mktime:
struct tm *gmtime(const time_t *timep);
struct tm *localtime(const time_t *timep);
time_t mktime(struct tm *tm);
localtime 和 gmttime 的區別是,前者將 Epoch 轉換為本地時間 (受時區、夏時制影響)、後者將 Epoch 轉換為 UTC (不受時區、夏時制影響)。
mktime 只接受本地時間作為引數、將其轉換為 Epoch,注意沒有 mkgmtime 這類東東。
mktime 並不使用 tm 引數的所有欄位,例如 wday 和 yday 就會被忽略,isdst 引數將按如下取值進行解釋:
mktime 還會自動規範化 (normalize) 各個欄位,例如 70 秒會被更新為 1 分 10 秒。除此之外,還有以下欄位會被更新:
極端的情況下,struct tm 中的每個欄位都可能被修改,這也是引數 tm 沒有加 const 修飾的原因。
利用 mktime 的 normalize 特性,很容易就可以求出 "N 年/月/日/時/分/秒" 前/後的時間,像下面這段程式碼:
#include "../apue.h"
#include <sys/time.h>
#include <time.h>
void print_tm (struct tm* t)
{
printf ("%04d-%02d-%02d %02d:%02d:%02d (week day %d) (year day %d) (daylight saving time %d)\n",
t->tm_year + 1900,
t->tm_mon + 1,
t->tm_mday,
t->tm_hour,
t->tm_min,
t->tm_sec,
t->tm_wday == 0 ? 7 : t->tm_wday,
t->tm_yday + 1,
t->tm_isdst);
}
int main (int argc, char *argv[])
{
if (argc < 2)
{
printf ("Usage: %s [+/-] [N[Y/M/D/H/m/S/w/y]]\n", argv[0]);
return 0;
}
int ret = 0;
time_t now = time (NULL);
printf ("sizeof (time_t) = %d, now = %ld\n", sizeof(time_t), now);
struct tm *tm_now = localtime (&now);
print_tm (tm_now);
int shift = 0;
char *endptr = 0;
shift = strtol (argv[1], &endptr, 10);
switch (*endptr)
{
case 'Y':
tm_now->tm_year += shift;
break;
case 'M':
tm_now->tm_mon += shift;
break;
case 'D':
case 'd':
tm_now->tm_mday += shift;
break;
case 'H':
case 'h':
tm_now->tm_hour += shift;
break;
case 'm':
tm_now->tm_min += shift;
break;
case 's':
case 'S':
tm_now->tm_sec += shift;
break;
/*
* tm_wday & tm_yday is ignored normally,
* here just do a test !!
*/
case 'w':
case 'W':
tm_now->tm_wday += shift;
break;
case 'y':
tm_now->tm_yday += shift;
break;
default:
printf ("unkonwn postfix %c", *endptr);
break;
}
print_tm (tm_now);
time_t tick = mktime (tm_now);
printf ("tick = %ld\n", tick);
print_tm (tm_now);
return 0;
}
執行時隨意指定:
> ./timeshift +70s
sizeof (time_t) = 8, now = 1678544442
2023-03-11 22:20:42 (week day 6) (year day 70) (daylight saving time 0)
2023-03-11 22:20:112 (week day 6) (year day 70) (daylight saving time 0)
tick = 1678544512
2023-03-11 22:21:52 (week day 6) (year day 70) (daylight saving time 0)
觀察到增加 sec 欄位 70 秒後達到 112 秒,經過 mktime 規範化後變為 52 秒並向上進位 1 分鐘。
這個 demo 還可以用來驗證設定 wday 或 yday 沒有效果,例如:
> ./timeshift +100y
sizeof (time_t) = 8, now = 1678544584
2023-03-11 22:23:04 (week day 6) (year day 70) (daylight saving time 0)
2023-03-11 22:23:04 (week day 6) (year day 170) (daylight saving time 0)
tick = 1678544584
2023-03-11 22:23:04 (week day 6) (year day 70) (daylight saving time 0)
直接被忽略了,yday 根據其它欄位推導,恢復了 70 的初始值。
同時也可以驗證 mday = 0 時其實是指上個月最後一天,例如:
> ./timeshift -11d
sizeof (time_t) = 8, now = 1678544711
2023-03-11 22:25:11 (week day 6) (year day 70) (daylight saving time 0)
2023-03-00 22:25:11 (week day 6) (year day 70) (daylight saving time 0)
tick = 1677594311
2023-02-28 22:25:11 (week day 2) (year day 59) (daylight saving time 0)
觀察到 2023-03-00 其實是 2023-02-28。
為了減少學習曲線,一些相對零碎的概念將在遇到的時候再行說明,閏秒就屬於這種情況。在解釋閏秒之前,先介紹兩個新的術語:
在確定 TAI 起點之後,由於地球自轉速度有變慢的趨勢 (非常小),UT 與 TAI 之間的時差便逐年積累。為彌補這一差距,便採用跳秒 (閏秒) 的方法使 TAI 與 UT 的時刻相接近,其差不超過 1 秒,這樣既保持時間尺度的均勻性,又能近似地反映地球自轉的變化。一般會在每年的 6 月 30 日、12 月 31 日的最後一秒進行調整。
現在回過頭來看 UTC 的定義——UTC 時間是經過平均太陽時、地軸運動修正後的新時標以及以秒為單位的國際原子時所綜合精算而成——是不是加深了印象?可以認為 UTC 是參考 TAI 增加閏秒的 UT。
較早的 SUS 標準允許雙閏秒,tm_sec 的取值範圍是 [0-61],UTC 的正式定義不允許雙閏秒,所以後來的 tm_sec 的的範圍被定義為 [0-60]。
不過 gmtime / localtime / mktime 都不處理閏秒,以最近的閏秒為例,2016/12/31 23:59:60,通過 linux date 命令來驗證:
> date -d "2016/12/31 23:59:59" "+%s"
1483199999
> date -d @1483200000
Sun Jan 1 00:00:00 CST 2017
先反解 2016/12/31 23:59:59 的 Epoch,將其加一秒後再通過 date 展示為直觀的時間,發現並沒有展示為 23:59:60,而是直接進入 2017 年。
難道是範例中的這個閏秒太"新"了?找個老一點的閏秒試試:
> date -d "1995/12/31 23:59:59" "+%s"
820425599
> date -d @820425600
Mon Jan 1 00:00:00 CST 1996
1995 年這個同樣不行。使用 mktime 傳遞 struct tm 的方式也試了,效果一樣。
特別是直接反解閏秒時,date 直接報錯:
> date -d "2016/12/31 23:59:60"
date: invalid date ‘2016/12/31 23:59:60’
上面特別強調使用 linux date,因為 mac date 有另外一套語法:
> date -j -f "%Y/%m/%d %H:%M:%S" "2016/12/31 23:59:59" "+%s"
1483199999
> date -r 1483200000 "+%Y/%m/%d %H:%M:%S"
2017/01/01 00:00:00
這一點需要注意。
來看一下閏秒的分佈:
可見是完全沒有規律的,甚至沒辦法提前把幾年之後的閏秒寫到系統庫裡,要讓庫可以長久的使用,只有不去管它。
想象一下 gmtime 是如何根據 Epoch 計算時間的:
壓根不可能處理閏秒這種複雜的東東,反過來看,這個介面叫 gmtime 而不是 utctime 也是有道理的。
總結一下:基於 POSIX 標準的系統不處理閏秒,不過這並不影響它的精度,因為絕大多數時間來講,GMT 時間和 UTC 給使用者展示的字串是一致的,畢竟 GTC 多出來的閏秒被安插在了 59:59:60 這種時間位置,對後面的時間沒有影響。唯一的區別在於,GTC 時間的 time_t 表示會比 GMT 多那麼幾十秒,除非要精確計算大跨度時間範圍內的絕對時間差,才需要用到閏秒。
從格林威治本初子午線起,經度每向東或者向西間隔 15°,就劃分一個時區,在這個區域內,大家使用同樣的標準時間。
但實際上,為了照顧到行政上的方便,常將一個國家或一個省份劃在一起。所以時區並不嚴格按南北直線來劃分,而是按自然條件來劃分。
全球共分為24個標準時區,相鄰時區的時間相差一個小時。全部的時區定義:Time Zone Abbreviations – Worldwide List
中國位於東八區 (UTC+8),沒有像美國那樣劃分多個時區,中國一整個都在一個時區:CST。
不過由於幅員遼闊,新疆烏魯木齊實際位於東六區,早上 9 點才相當於北京早上 7 點,因此如果觀察一個國內伺服器早高峰,會發現新疆是最後上線的。
時區是導致同一個系統 localtime 和 gmtime 返回值有差異的主要原因。回顧一下開始的關係圖:
紅色表示介面會受時區影響,以 localtime 為例,man 中是這樣解釋它如何獲取當前時區設定的:
優先檢查 TZ 環境變數,如果存在,不論是否有效,都不再檢查系統設定。
void tzset (void);
extern char *tzname[2];
extern long timezone;
extern int daylight;
tzset 介面用於將 TZ 中的資訊解析到全域性變數 tzname / timezone / daylight 欄位,紅色介面通過呼叫它來設定正確的時區、夏時制等資訊,用於後期時間轉換。
下面的程式片段演示了各個呼叫對 tzset 的呼叫情況:
#include "../apue.h"
#include <sys/time.h>
#include <time.h>
struct timezone
{
int tz_minuteswest; /* of Greenwich */
int tz_dsttime; /* type of dst correction to apply */
};
void print_tm (struct tm* t)
{
printf ("%04d-%02d-%02d %02d:%02d:%02d (week day %d) (year day %d) (daylight saving time %d)\n",
t->tm_year + 1900,
t->tm_mon + 1,
t->tm_mday,
t->tm_hour,
t->tm_min,
t->tm_sec,
t->tm_wday == 0 ? 7 : t->tm_wday,
t->tm_yday + 1,
t->tm_isdst);
}
void print_tz ()
{
printf ("tzname[0] = %s, tzname[1] = %s, timezone = %d, daylight = %d\n", tzname[0], tzname[1], timezone, daylight);
}
int main (int argc, char *argv[])
{
int ret = 0;
time_t t1, t2;
print_tz ();
t1 = time (&t2);
printf ("t1 = %ld, t2 = %ld\n", t1, t2);
print_tz ();
struct timeval tv;
struct timezone tzp;
ret = gettimeofday (&tv, (void*) &tzp);
if (ret == -1)
perror("gettimeofday");
printf ("sizeof (suseconds_t) = %d, sizeof (struct timeval) = %d, ret %d, tv.sec = %ld, tv.usec = %ld\n",
sizeof (suseconds_t), sizeof (struct timeval), ret, tv.tv_sec, tv.tv_usec);
printf ("minuteswest = %d, dsttime = %d\n", tzp.tz_minuteswest, tzp.tz_dsttime);
print_tz ();
struct tm *tm1 = gmtime (&t1);
print_tm (tm1);
print_tz ();
struct tm *tm2 = localtime (&t2);
print_tm (tm2);
print_tz ();
time_t t3 = mktime (tm1);
printf ("t3 = %ld\n", t3);
print_tz ();
printf ("from asctime: %s", asctime (tm1));
print_tz ();
printf ("from ctime: %s", ctime (&t1));
print_tz ();
return 0;
}
上面的 demo 演示了在各個時間例程呼叫後的時區資訊 (print_tz),以便觀察是否間接呼叫了 tzset,先來看 Darwin 上執行的結果:
> ./time
tzname[0] = , tzname[1] = , timezone = 0, daylight = 0
t1 = 1679811210, t2 = 1679811210
tzname[0] = , tzname[1] = , timezone = 0, daylight = 0
sizeof (suseconds_t) = 4, sizeof (struct timeval) = 16, ret 0, tv.sec = 1679811210, tv.usec = 909062
minuteswest = -480, dsttime = 0
tzname[0] = CST, tzname[1] = CDT, timezone = -28800, daylight = 1
2023-03-26 06:13:30 (week day 7) (year day 85) (daylight saving time 0)
tzname[0] = CST, tzname[1] = CDT, timezone = -28800, daylight = 1
2023-03-26 14:13:30 (week day 7) (year day 85) (daylight saving time 0)
tzname[0] = CST, tzname[1] = CDT, timezone = -28800, daylight = 1
t3 = 1679782410
tzname[0] = CST, tzname[1] = CDT, timezone = -28800, daylight = 1
from asctime: Sun Mar 26 06:13:30 2023
tzname[0] = CST, tzname[1] = CDT, timezone = -28800, daylight = 1
from ctime: Sun Mar 26 14:13:30 2023
tzname[0] = CST, tzname[1] = CDT, timezone = -28800, daylight = 1
從輸出中可以看到:
由於後五個輸出一致,為防止相互干擾,可以通過調整呼叫順序,或手動遮蔽其它呼叫來觀察輸出,結論是一致的。
需要注意的一點是,mktime 和 asctime 的結果是正確的,這是因為它們使用了 gmtime 的返回值,將其作為本地時間處理了,這直接導致 t3 比 t1 小了 28800 秒。
> ./time
tzname[0] = GMT, tzname[1] = GMT, timezone = 0, daylight = 0
t1 = 1679810748, t2 = 1679810748
tzname[0] = GMT, tzname[1] = GMT, timezone = 0, daylight = 0
sizeof (suseconds_t) = 8, sizeof (struct timeval) = 16, ret 0, tv.sec = 1679810748, tv.usec = 451237
minuteswest = 0, dsttime = 0
tzname[0] = GMT, tzname[1] = GMT, timezone = 0, daylight = 0
2023-03-26 06:05:48 (week day 7) (year day 85) (daylight saving time 0)
tzname[0] = CST, tzname[1] = CDT, timezone = -28800, daylight = 1
2023-03-26 14:05:48 (week day 7) (year day 85) (daylight saving time 0)
tzname[0] = CST, tzname[1] = CST, timezone = -28800, daylight = 0
t3 = 1679810748
tzname[0] = CST, tzname[1] = CST, timezone = -28800, daylight = 0
from asctime: Sun Mar 26 14:05:48 2023
tzname[0] = CST, tzname[1] = CST, timezone = -28800, daylight = 0
from ctime: Sun Mar 26 14:05:48 2023
tzname[0] = CST, tzname[1] = CST, timezone = -28800, daylight = 0
上面是在 linux 系統上執行的結果,和 Darwin 有以下不同:
其中 mktime 在使用 gmtime 的結果作為輸入後,居然得到了和 time 一樣的結果,實在是匪夷所思,導致後面的 asctime 結果也跟著出錯。轉念一想,是否是因為 gmtime 和 localtime 返回了同一塊靜態儲存區呢?加入下面的一行程式碼印證:
printf ("gmt %p, local %p\n", tm1, tm2);
新紀錄檔顯示果然如此:
gmt 0x7f2206a8cda0, local 0x7f2206a8cda0
而在 Darwin 上則不同:
gmt 0x7facbce04330, local 0x7facbcc058d0
看來 Darwin 確實做的要稍好一些,不同介面返回了不同靜態緩衝區。不過對於這種靜態物件,能不快取還是不要快取了,免的同型別的相互覆蓋,下面是 linux 改進後的輸出:
> ./time
tzname[0] = GMT, tzname[1] = GMT, timezone = 0, daylight = 0
t1 = 1679814314, t2 = 1679814314
tzname[0] = GMT, tzname[1] = GMT, timezone = 0, daylight = 0
sizeof (suseconds_t) = 8, sizeof (struct timeval) = 16, ret 0, tv.sec = 1679814314, tv.usec = 70725
minuteswest = 0, dsttime = 0
tzname[0] = GMT, tzname[1] = GMT, timezone = 0, daylight = 0
2023-03-26 07:05:14 (week day 7) (year day 85) (daylight saving time 0)
tzname[0] = CST, tzname[1] = CDT, timezone = -28800, daylight = 1
2023-03-26 15:05:14 (week day 7) (year day 85) (daylight saving time 0)
tzname[0] = CST, tzname[1] = CST, timezone = -28800, daylight = 0
t3 = 1679785514
tzname[0] = CST, tzname[1] = CST, timezone = -28800, daylight = 0
from asctime: Sun Mar 26 07:05:14 2023
tzname[0] = CST, tzname[1] = CST, timezone = -28800, daylight = 0
from ctime: Sun Mar 26 15:05:14 2023
tzname[0] = CST, tzname[1] = CST, timezone = -28800, daylight = 0
mktime 終於正常了。至於 linux gmtime 是否呼叫了 tzset 的問題,留待以後瀏覽 glibc 原始碼再行確認。
在沒有定義 TZ 環境變數時,會查詢當前的系統時區設定。系統時區表示方式隨系統不同而不同:
時區一般在安裝系統時進行設定,也可以在系統設定面板中更改。在某些沒有 GUI 的場景中 (遠端 ssh),也可以通過 tzselect 來更改時區:
> tzselect
Please identify a location so that time zone rules can be set correctly.
Please select a continent or ocean.
1) Africa
2) Americas
3) Antarctica
4) Arctic Ocean
5) Asia
6) Atlantic Ocean
7) Australia
8) Europe
9) Indian Ocean
10) Pacific Ocean
11) none - I want to specify the time zone using the Posix TZ format.
#? 5
Please select a country.
1) Afghanistan 18) Israel 35) Palestine
2) Armenia 19) Japan 36) Philippines
3) Azerbaijan 20) Jordan 37) Qatar
4) Bahrain 21) Kazakhstan 38) Russia
5) Bangladesh 22) Korea (North) 39) Saudi Arabia
6) Bhutan 23) Korea (South) 40) Singapore
7) Brunei 24) Kuwait 41) Sri Lanka
8) Cambodia 25) Kyrgyzstan 42) Syria
9) China 26) Laos 43) Taiwan
10) Cyprus 27) Lebanon 44) Tajikistan
11) East Timor 28) Macau 45) Thailand
12) Georgia 29) Malaysia 46) Turkmenistan
13) Hong Kong 30) Mongolia 47) United Arab Emirates
14) India 31) Myanmar (Burma) 48) Uzbekistan
15) Indonesia 32) Nepal 49) Vietnam
16) Iran 33) Oman 50) Yemen
17) Iraq 34) Pakistan
#? 9
Please select one of the following time zone regions.
1) Beijing Time
2) Xinjiang Time
#? 1
The following information has been given:
China
Beijing Time
Therefore TZ='Asia/Shanghai' will be used.
Local time is now: Sun Mar 12 17:37:12 CST 2023.
Universal Time is now: Sun Mar 12 09:37:12 UTC 2023.
Is the above information OK?
1) Yes
2) No
#? 1
You can make this change permanent for yourself by appending the line
TZ='Asia/Shanghai'; export TZ
to the file '.profile' in your home directory; then log out and log in again.
Here is that TZ value again, this time on standard output so that you
can use the /usr/bin/tzselect command in shell scripts:
Asia/Shanghai
根據提示一步步選擇就可以了,注意這個命令執行後時區並沒有變更,它只是根據使用者選擇的地區提供了 TZ 環境變數的內容,後續還需要使用者手動設定一下,最終還是走的環境變數的方式,畢竟這種方式有優先順序,能影響最終的結果。如果不想設定環境變數,也直接更改系統檔案內容 (Ubuntu) 或軟連結指向 (CentOS/Darwin),這種需要提權,必需有管理員許可權才可以。
CentOS 和 Darwin 上的時區檔案為二進位制,可以通過 zdump 檢視:
> zdump -v /usr/share/zoneinfo/Asia/Shanghai
/usr/share/zoneinfo/Asia/Shanghai -9223372036854775808 = NULL
/usr/share/zoneinfo/Asia/Shanghai -9223372036854689408 = NULL
/usr/share/zoneinfo/Asia/Shanghai Mon Dec 31 15:54:16 1900 UTC = Mon Dec 31 23:59:59 1900 LMT isdst=0 gmtoff=29143
/usr/share/zoneinfo/Asia/Shanghai Mon Dec 31 15:54:17 1900 UTC = Mon Dec 31 23:54:17 1900 CST isdst=0 gmtoff=28800
/usr/share/zoneinfo/Asia/Shanghai Sat Apr 12 15:59:59 1919 UTC = Sat Apr 12 23:59:59 1919 CST isdst=0 gmtoff=28800
/usr/share/zoneinfo/Asia/Shanghai Sat Apr 12 16:00:00 1919 UTC = Sun Apr 13 01:00:00 1919 CDT isdst=1 gmtoff=32400
/usr/share/zoneinfo/Asia/Shanghai Tue Sep 30 14:59:59 1919 UTC = Tue Sep 30 23:59:59 1919 CDT isdst=1 gmtoff=32400
/usr/share/zoneinfo/Asia/Shanghai Tue Sep 30 15:00:00 1919 UTC = Tue Sep 30 23:00:00 1919 CST isdst=0 gmtoff=28800
/usr/share/zoneinfo/Asia/Shanghai Fri May 31 15:59:59 1940 UTC = Fri May 31 23:59:59 1940 CST isdst=0 gmtoff=28800
/usr/share/zoneinfo/Asia/Shanghai Fri May 31 16:00:00 1940 UTC = Sat Jun 1 01:00:00 1940 CDT isdst=1 gmtoff=32400
/usr/share/zoneinfo/Asia/Shanghai Sat Oct 12 14:59:59 1940 UTC = Sat Oct 12 23:59:59 1940 CDT isdst=1 gmtoff=32400
/usr/share/zoneinfo/Asia/Shanghai Sat Oct 12 15:00:00 1940 UTC = Sat Oct 12 23:00:00 1940 CST isdst=0 gmtoff=28800
/usr/share/zoneinfo/Asia/Shanghai Fri Mar 14 15:59:59 1941 UTC = Fri Mar 14 23:59:59 1941 CST isdst=0 gmtoff=28800
/usr/share/zoneinfo/Asia/Shanghai Fri Mar 14 16:00:00 1941 UTC = Sat Mar 15 01:00:00 1941 CDT isdst=1 gmtoff=32400
/usr/share/zoneinfo/Asia/Shanghai Sat Nov 1 14:59:59 1941 UTC = Sat Nov 1 23:59:59 1941 CDT isdst=1 gmtoff=32400
/usr/share/zoneinfo/Asia/Shanghai Sat Nov 1 15:00:00 1941 UTC = Sat Nov 1 23:00:00 1941 CST isdst=0 gmtoff=28800
/usr/share/zoneinfo/Asia/Shanghai Fri Jan 30 15:59:59 1942 UTC = Fri Jan 30 23:59:59 1942 CST isdst=0 gmtoff=28800
/usr/share/zoneinfo/Asia/Shanghai Fri Jan 30 16:00:00 1942 UTC = Sat Jan 31 01:00:00 1942 CDT isdst=1 gmtoff=32400
/usr/share/zoneinfo/Asia/Shanghai Sat Sep 1 14:59:59 1945 UTC = Sat Sep 1 23:59:59 1945 CDT isdst=1 gmtoff=32400
/usr/share/zoneinfo/Asia/Shanghai Sat Sep 1 15:00:00 1945 UTC = Sat Sep 1 23:00:00 1945 CST isdst=0 gmtoff=28800
/usr/share/zoneinfo/Asia/Shanghai Tue May 14 15:59:59 1946 UTC = Tue May 14 23:59:59 1946 CST isdst=0 gmtoff=28800
/usr/share/zoneinfo/Asia/Shanghai Tue May 14 16:00:00 1946 UTC = Wed May 15 01:00:00 1946 CDT isdst=1 gmtoff=32400
/usr/share/zoneinfo/Asia/Shanghai Mon Sep 30 14:59:59 1946 UTC = Mon Sep 30 23:59:59 1946 CDT isdst=1 gmtoff=32400
/usr/share/zoneinfo/Asia/Shanghai Mon Sep 30 15:00:00 1946 UTC = Mon Sep 30 23:00:00 1946 CST isdst=0 gmtoff=28800
/usr/share/zoneinfo/Asia/Shanghai Mon Apr 14 15:59:59 1947 UTC = Mon Apr 14 23:59:59 1947 CST isdst=0 gmtoff=28800
/usr/share/zoneinfo/Asia/Shanghai Mon Apr 14 16:00:00 1947 UTC = Tue Apr 15 01:00:00 1947 CDT isdst=1 gmtoff=32400
/usr/share/zoneinfo/Asia/Shanghai Fri Oct 31 14:59:59 1947 UTC = Fri Oct 31 23:59:59 1947 CDT isdst=1 gmtoff=32400
/usr/share/zoneinfo/Asia/Shanghai Fri Oct 31 15:00:00 1947 UTC = Fri Oct 31 23:00:00 1947 CST isdst=0 gmtoff=28800
/usr/share/zoneinfo/Asia/Shanghai Fri Apr 30 15:59:59 1948 UTC = Fri Apr 30 23:59:59 1948 CST isdst=0 gmtoff=28800
/usr/share/zoneinfo/Asia/Shanghai Fri Apr 30 16:00:00 1948 UTC = Sat May 1 01:00:00 1948 CDT isdst=1 gmtoff=32400
/usr/share/zoneinfo/Asia/Shanghai Thu Sep 30 14:59:59 1948 UTC = Thu Sep 30 23:59:59 1948 CDT isdst=1 gmtoff=32400
/usr/share/zoneinfo/Asia/Shanghai Thu Sep 30 15:00:00 1948 UTC = Thu Sep 30 23:00:00 1948 CST isdst=0 gmtoff=28800
/usr/share/zoneinfo/Asia/Shanghai Sat Apr 30 15:59:59 1949 UTC = Sat Apr 30 23:59:59 1949 CST isdst=0 gmtoff=28800
/usr/share/zoneinfo/Asia/Shanghai Sat Apr 30 16:00:00 1949 UTC = Sun May 1 01:00:00 1949 CDT isdst=1 gmtoff=32400
/usr/share/zoneinfo/Asia/Shanghai Fri May 27 14:59:59 1949 UTC = Fri May 27 23:59:59 1949 CDT isdst=1 gmtoff=32400
/usr/share/zoneinfo/Asia/Shanghai Fri May 27 15:00:00 1949 UTC = Fri May 27 23:00:00 1949 CST isdst=0 gmtoff=28800
/usr/share/zoneinfo/Asia/Shanghai Sat May 3 17:59:59 1986 UTC = Sun May 4 01:59:59 1986 CST isdst=0 gmtoff=28800
/usr/share/zoneinfo/Asia/Shanghai Sat May 3 18:00:00 1986 UTC = Sun May 4 03:00:00 1986 CDT isdst=1 gmtoff=32400
/usr/share/zoneinfo/Asia/Shanghai Sat Sep 13 16:59:59 1986 UTC = Sun Sep 14 01:59:59 1986 CDT isdst=1 gmtoff=32400
/usr/share/zoneinfo/Asia/Shanghai Sat Sep 13 17:00:00 1986 UTC = Sun Sep 14 01:00:00 1986 CST isdst=0 gmtoff=28800
/usr/share/zoneinfo/Asia/Shanghai Sat Apr 11 17:59:59 1987 UTC = Sun Apr 12 01:59:59 1987 CST isdst=0 gmtoff=28800
/usr/share/zoneinfo/Asia/Shanghai Sat Apr 11 18:00:00 1987 UTC = Sun Apr 12 03:00:00 1987 CDT isdst=1 gmtoff=32400
/usr/share/zoneinfo/Asia/Shanghai Sat Sep 12 16:59:59 1987 UTC = Sun Sep 13 01:59:59 1987 CDT isdst=1 gmtoff=32400
/usr/share/zoneinfo/Asia/Shanghai Sat Sep 12 17:00:00 1987 UTC = Sun Sep 13 01:00:00 1987 CST isdst=0 gmtoff=28800
/usr/share/zoneinfo/Asia/Shanghai Sat Apr 16 17:59:59 1988 UTC = Sun Apr 17 01:59:59 1988 CST isdst=0 gmtoff=28800
/usr/share/zoneinfo/Asia/Shanghai Sat Apr 16 18:00:00 1988 UTC = Sun Apr 17 03:00:00 1988 CDT isdst=1 gmtoff=32400
/usr/share/zoneinfo/Asia/Shanghai Sat Sep 10 16:59:59 1988 UTC = Sun Sep 11 01:59:59 1988 CDT isdst=1 gmtoff=32400
/usr/share/zoneinfo/Asia/Shanghai Sat Sep 10 17:00:00 1988 UTC = Sun Sep 11 01:00:00 1988 CST isdst=0 gmtoff=28800
/usr/share/zoneinfo/Asia/Shanghai Sat Apr 15 17:59:59 1989 UTC = Sun Apr 16 01:59:59 1989 CST isdst=0 gmtoff=28800
/usr/share/zoneinfo/Asia/Shanghai Sat Apr 15 18:00:00 1989 UTC = Sun Apr 16 03:00:00 1989 CDT isdst=1 gmtoff=32400
/usr/share/zoneinfo/Asia/Shanghai Sat Sep 16 16:59:59 1989 UTC = Sun Sep 17 01:59:59 1989 CDT isdst=1 gmtoff=32400
/usr/share/zoneinfo/Asia/Shanghai Sat Sep 16 17:00:00 1989 UTC = Sun Sep 17 01:00:00 1989 CST isdst=0 gmtoff=28800
/usr/share/zoneinfo/Asia/Shanghai Sat Apr 14 17:59:59 1990 UTC = Sun Apr 15 01:59:59 1990 CST isdst=0 gmtoff=28800
/usr/share/zoneinfo/Asia/Shanghai Sat Apr 14 18:00:00 1990 UTC = Sun Apr 15 03:00:00 1990 CDT isdst=1 gmtoff=32400
/usr/share/zoneinfo/Asia/Shanghai Sat Sep 15 16:59:59 1990 UTC = Sun Sep 16 01:59:59 1990 CDT isdst=1 gmtoff=32400
/usr/share/zoneinfo/Asia/Shanghai Sat Sep 15 17:00:00 1990 UTC = Sun Sep 16 01:00:00 1990 CST isdst=0 gmtoff=28800
/usr/share/zoneinfo/Asia/Shanghai Sat Apr 13 17:59:59 1991 UTC = Sun Apr 14 01:59:59 1991 CST isdst=0 gmtoff=28800
/usr/share/zoneinfo/Asia/Shanghai Sat Apr 13 18:00:00 1991 UTC = Sun Apr 14 03:00:00 1991 CDT isdst=1 gmtoff=32400
/usr/share/zoneinfo/Asia/Shanghai Sat Sep 14 16:59:59 1991 UTC = Sun Sep 15 01:59:59 1991 CDT isdst=1 gmtoff=32400
/usr/share/zoneinfo/Asia/Shanghai Sat Sep 14 17:00:00 1991 UTC = Sun Sep 15 01:00:00 1991 CST isdst=0 gmtoff=28800
/usr/share/zoneinfo/Asia/Shanghai 9223372036854689407 = NULL
/usr/share/zoneinfo/Asia/Shanghai 9223372036854775807 = NULL
也可以指定相對路徑或國家縮寫,如 zdump -v Asia/Shanghai
或 zdump -v PRC
,輸出資訊一致。看起來檔案內容中包含了該時區對應的夏時制起始時間,怪不得檔案尺寸各不相等。這或許就是 Darwin 中 gettimeofday 返回當前時間是否處於夏時制的依據,關於夏時制,請參考下節。
不加 -v 選項呼叫 zdump,會返回時區的當前時間:
> zdump America/New_York
America/New_York Sun Mar 26 03:31:01 2023 EDT
> zdump PRC
PRC Sun Mar 26 15:31:05 2023 CST
> date
Sun Mar 26 15:31:10 CST 2023
可以看到和 date 命令的輸出有些許差別。如果時區不合法或沒找到,通通返回 GMT 時間。
夏時制也稱夏令時 (Daylight Saving Time),直譯過來就是日光節約時間制。這是因為北半球夏季時白天變長、夜晚變短,有人認為通過推行夏時制可以有效利用天光,節約晚上能源消耗。
具體操作就是,在進入夏季某天后,統一將時鐘調快一小時,此時早上七點將變為早上八點,提早開始上班上學,晚上五點將變為晚上六點,提早開始下班放學。即通過讓人早起早睡來達到多利用天光的目的,而且統一調整時間制後,學校、公司都不用調整了,省去了很多不一致的地方。到某個夏季結束的一天,再統一將時鐘調慢一小時,人們又可以晚起晚睡了,自此時間恢復到往常一樣。
我國曾實行過六年的夏時制 (1986-1991),發現對社會節約用電效果有限,另外還有其它弊端,例如切換夏時制後睡眠不足導致的車禍、列車時刻表的調整、全國一個時區帶來的偏遠地區時差更大等等問題,最終放棄了這一做法。歐盟也在 2021 年投票廢棄了夏時制,目前在執行夏時制的比較大的國家就剩美國、加拿大、澳大利亞等。
再來複習一下文章開關的關係圖:
其中虛線部分表示受夏時制影響,POSIX 時間例程中的 time、gettimeofday 不考慮夏時制,否則 Epoch 憑空多了 3600 或少了 3600 是什麼鬼。下面舉個例子:
> date
Sun Mar 26 16:00:40 CST 2023
> export TZ=America/New_York
> date
Sun Mar 26 04:00:51 EDT 2023
> zdump America/New_York
America/New_York Sun Mar 26 04:00:55 2023 EDT
已知美國紐約在 2023-03-12 已進入夏時制,持續直到 11-05:
> zdump -v America/New_York | grep 2023
...
America/New_York Sun Mar 12 06:59:59 2023 UTC = Sun Mar 12 01:59:59 2023 EST isdst=0
America/New_York Sun Mar 12 07:00:00 2023 UTC = Sun Mar 12 03:00:00 2023 EDT isdst=1
America/New_York Sun Nov 5 05:59:59 2023 UTC = Sun Nov 5 01:59:59 2023 EDT isdst=1
America/New_York Sun Nov 5 06:00:00 2023 UTC = Sun Nov 5 01:00:00 2023 EST isdst=0
...
那日期 2023-03-26 應該處於夏時制期間,時間理應調慢一小時,即中國東 8 區與美國西 4 區之差再加一小時——13 小時時差才對,而實際仍只有 12 小時時差 (16:00 vs 4:00)。上面的 demo 在 linux 和 Darwin 上執行結果一致。
下面再來考慮一下其它日期例程是否夏時制敏感,為了說明問題,保留上例中 export TZ=America/New_York
設定,注意執行這個例子和當前系統時間也有關係 (必需是在所在區域的夏時制範圍內):
> ./time
tzname[0] = GMT, tzname[1] = GMT, timezone = 0, daylight = 0
t1 = 1679819691, t2 = 1679819691
tzname[0] = GMT, tzname[1] = GMT, timezone = 0, daylight = 0
sizeof (suseconds_t) = 8, sizeof (struct timeval) = 16, ret 0, tv.sec = 1679819691, tv.usec = 695922
minuteswest = 0, dsttime = 0
tzname[0] = GMT, tzname[1] = GMT, timezone = 0, daylight = 0
2023-03-26 08:34:51 (week day 7) (year day 85) (daylight saving time 0)
tzname[0] = EST, tzname[1] = EDT, timezone = 18000, daylight = 1
2023-03-26 04:34:51 (week day 7) (year day 85) (daylight saving time 1)
tzname[0] = EST, tzname[1] = EDT, timezone = 18000, daylight = 1
t3 = 1679837691
tzname[0] = EST, tzname[1] = EDT, timezone = 18000, daylight = 1
from asctime: Sun Mar 26 09:34:51 2023
tzname[0] = EST, tzname[1] = EDT, timezone = 18000, daylight = 1
from ctime: Sun Mar 26 04:34:51 2023
tzname[0] = EST, tzname[1] = EDT, timezone = 18000, daylight = 1
發現幾點有趣的變化:
好好分析一下第三條現象,asctime 列印的 tm1 結構體是 gmtime 返回的,不應該受夏時制影響才對,那將 asctime 和 ctime 的輸入引數替換為 localtime 返回的 tm2 和 t2 會如何呢?
from asctime: Sun Mar 26 05:03:16 2023
tzname[0] = EST, tzname[1] = EDT, timezone = 18000, daylight = 1
from ctime: Sun Mar 26 05:03:16 2023
tzname[0] = EST, tzname[1] = EDT, timezone = 18000, daylight = 1
結論是完全沒影響。那上面突然增加的 1 小時怎麼解釋呢?難不成是 mktime 修改了 tm 結構體?增加下面的程式碼用於驗證:
time_t t3 = mktime (&tm1);
printf ("t3 = %ld\n", t3);
print_tm (&tm1);
print_tz ();
printf ("from asctime: %s", asctime (&tm1));
print_tz ();
printf ("from ctime: %s", ctime (&t3));
print_tz ();
return 0;
在 mktime 後列印 tm1 的內容,並將 asctime 和 ctime 的引數指向 mktime 的結果,新的紀錄檔如下:
> ./time
tzname[0] = GMT, tzname[1] = GMT, timezone = 0, daylight = 0
t1 = 1679821804, t2 = 1679821804
tzname[0] = GMT, tzname[1] = GMT, timezone = 0, daylight = 0
sizeof (suseconds_t) = 8, sizeof (struct timeval) = 16, ret 0, tv.sec = 1679821804, tv.usec = 289229
minuteswest = 0, dsttime = 0
tzname[0] = GMT, tzname[1] = GMT, timezone = 0, daylight = 0
2023-03-26 09:10:04 (week day 7) (year day 85) (daylight saving time 0)
tzname[0] = EST, tzname[1] = EDT, timezone = 18000, daylight = 1
2023-03-26 05:10:04 (week day 7) (year day 85) (daylight saving time 1)
tzname[0] = EST, tzname[1] = EDT, timezone = 18000, daylight = 1
t3 = 1679839804
2023-03-26 10:10:04 (week day 7) (year day 85) (daylight saving time 1)
tzname[0] = EST, tzname[1] = EDT, timezone = 18000, daylight = 1
from asctime: Sun Mar 26 10:10:04 2023
tzname[0] = EST, tzname[1] = EDT, timezone = 18000, daylight = 1
from ctime: Sun Mar 26 10:10:04 2023
tzname[0] = EST, tzname[1] = EDT, timezone = 18000, daylight = 1
果然是 mktime 做的手腳!將 tm1 結構體中的 tm_hour 增加了 1 小時,看起來是受 tm_isdst 影響了。然而 tm1 的 tm_isdst 值為 0,不應該影響 mktime 的結果,神奇,懷疑是有 tzset 在內部被呼叫了。下面是另外一些嘗試:
對於 ctime 的神奇表現簡直是匪夷所思,一個小小的 time_t 中無法包含任何關於夏時制的資訊;如果通過全域性變數,那麼將 mktime 都註釋掉了仍能增加 1 小時,這讓人上哪講理去。
同樣的現象出在 mktime 身上,如果傳遞的是 localtime 的結果,則 mktime 不會增加 1 小時,後續的 ctime 也不會增加,可見他們的問題是一致的——傳遞 gmtime 的結果到 mktime 可能會有意想不到的結果,最好不要這樣做。
最終結論是,當正常使用時間例程時,它們都不受夏時制影響;如果錯誤的將 gmtime 結果傳遞給 mktime,則 mktime 和 ctime 會受夏時制影響自動增加 1 小時。後者受影響的規律還沒有摸清楚,留待後面瀏覽 mktime / ctime 原始碼時給出解釋。
以上現象在 Darwin 上也能復現。最後上一張 linux 上 strace 的輸出:
> strace ./time |& less
...
brk(NULL) = 0x257d000
brk(0x259e000) = 0x259e000
brk(NULL) = 0x259e000
open("/usr/share/zoneinfo/America/New_York", O_RDONLY|O_CLOEXEC) = 3
fstat(3, {st_mode=S_IFREG|0644, st_size=3535, ...}) = 0
fstat(3, {st_mode=S_IFREG|0644, st_size=3535, ...}) = 0
mmap(NULL, 4096, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0x7fa3b07b7000
read(3, "TZif2\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\5\0\0\0\5\0\0\0\0"..., 4096) = 3535
lseek(3, -2260, SEEK_CUR) = 1275
read(3, "TZif2\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\6\0\0\0\6\0\0\0\0"..., 4096) = 2260
close(3) = 0
munmap(0x7fa3b07b7000, 4096) = 0
write(1, "tzname[0] = GMT, tzname[1] = GMT"..., 979tzname[0] = GMT, tzname[1] = GMT, timezone = 0, daylight = 0
t1 = 1679823212, t2 = 1679823212
tzname[0] = GMT, tzname[1] = GMT, timezone = 0, daylight = 0
sizeof (suseconds_t) = 8, sizeof (struct timeval) = 16, ret 0, tv.sec = 1679823212, tv.usec = 189083
minuteswest = 0, dsttime = 0
...
可以看到在設定了 TZ 環境變數的情況下,時區檔案仍被開啟以確認夏時制的起始結束範圍。
time_t 表示的 Epoch 適合計算機儲存、計算,但對人並不友好。將它們轉換為人能理解的日期時間需要藉助於以下例程:
char *asctime(const struct tm *tm);
char *ctime(const time_t *timep);
size_t strftime(char *s, size_t max, const char *format, const struct tm *tm);
其中 asctime 和 ctime 前面已經介紹過,它們分別將 strut tm 和 time_t 轉換為固定的時間格式。strftime 用於將 strut tm 轉換為任意使用者指定的格式,類似於 printf 做的工作。
其中 s 和 max 引數指定了輸出快取區,如果生成的字串長度 (包含結尾 null) 大於 max,則返回 0;否則返回生成的字串長度 (不包含結尾 null)。
讓我們再回顧一下開頭的關係圖:
strftime 和 strptme 互逆,asctime 生成的 string 也可以通過 strptime 轉換回 struct tm,但沒有直接從 string 轉換到 time_t 的途徑,也沒有直接從 time_t 生成格式化字串的路徑。
下面是對 format 引數的說明:
格式 | 說明 | 範例 |
%Y | 年 | 2023 |
%C | 年/100 | 20 |
%y | 年%100: [00-99] | 23 |
%G | ISO 基於周的年 | 2023 |
%g | ISO 基於周的年%100 | 23 |
%m | 月: [01-12] | 04 |
%b / %h | 月名縮寫 | Apr |
%B | 月名全寫 | April |
%w | 日in周: [0-6],週日: 0 | 6 |
%u | ISO 日in周: [1-7],週日: 7 | 7 |
%a | 日in周縮寫 | Sun |
%A | 日in周全寫 | Sunday |
%d | 日in月: [01-31],前導零 | 02 |
%e | 日in月: [1-31],前導空格 | 2 |
%j | 日in年: [001-366] | 092 |
%U | 星期日週數: [00-53] | 14 |
%W | 星期一週數: [01-53] | 13 |
%V | ISO 週數: [01-53] | 13 |
%D / %x | %m/%d/%y | 04/02/23 |
%F | %Y-%m-%d | 2023-04-02 |
%H | 24 時制小時: [00-23] | 17 |
%I | 12 時制小時: [01-12] | 05 |
%M | 分: [00-59] | 38 |
%S | 秒: [00-60] | 31 |
%T / %X | 24 時制時間: %H:%M:%S | 17:38:31 |
%R | %H:%M | 17:38 |
%p | AM/PM | PM |
%r | 12 時制時間: %I:%M:%S %p | 05:38:31 PM |
%c | 日期和時間 | Sun Apr 2 17:38:31 2023 |
%z | ISO UTC 偏移量 | +0800 |
%Z | 時區名 | CST |
%n | 換行符 | |
%t | 水平製表符 | |
%% | 百分號 | % |
大部分選項是直接明瞭的,有幾個需要單獨解釋下:
下面的程式碼可以用來測試任何格式化選項:
#include "../apue.h"
#include <sys/time.h>
#include <time.h>
void print_tm (struct tm* t)
{
printf ("%04d-%02d-%02d %02d:%02d:%02d (week day %d) (year day %d) (daylight saving time %d)\n",
t->tm_year + 1900,
t->tm_mon + 1,
t->tm_mday,
t->tm_hour,
t->tm_min,
t->tm_sec,
t->tm_wday,
t->tm_yday + 1,
t->tm_isdst);
}
void my_strftime (char const* fmt, struct tm* t)
{
char buf[64] = { 0 };
int ret = strftime (buf, sizeof (buf), fmt, t);
printf ("[%02d] '%s': %s\n", ret, fmt, buf);
}
int main (int argc, char *argv[])
{
int ret = 0;
time_t now = time (NULL);
printf ("now = %ld\n", now);
struct tm *t = localtime (&now);
print_tm (t);
printf ("year group:\n");
my_strftime ("%Y", t);
my_strftime ("%C", t);
my_strftime ("%y", t);
my_strftime ("%G", t);
my_strftime ("%g", t);
printf ("month group:\n");
my_strftime ("%m", t);
my_strftime ("%b", t);
my_strftime ("%h", t);
my_strftime ("%B", t);
printf ("day group:\n");
my_strftime ("%w", t);
my_strftime ("%u", t);
my_strftime ("%a", t);
my_strftime ("%A", t);
my_strftime ("%d", t);
my_strftime ("%e", t);
my_strftime ("%j", t);
printf ("week group:\n");
my_strftime ("%U", t);
my_strftime ("%W", t);
my_strftime ("%V", t);
printf ("date group\n");
my_strftime ("%D", t);
my_strftime ("%x", t);
my_strftime ("%F", t);
printf ("time group\n");
my_strftime ("%H", t);
my_strftime ("%k", t);
my_strftime ("%I", t);
my_strftime ("%l", t);
my_strftime ("%M", t);
my_strftime ("%S", t);
my_strftime ("%T", t);
my_strftime ("%X", t);
my_strftime ("%R", t);
my_strftime ("%p", t);
my_strftime ("%P", t);
my_strftime ("%r", t);
my_strftime ("%c", t);
my_strftime ("%s", t);
printf ("timezone group\n");
my_strftime ("%z", t);
my_strftime ("%Z", t);
printf ("common group\n");
my_strftime ("%n", t);
my_strftime ("%t", t);
my_strftime ("%%", t);
return 0;
}
下面是程式碼的典型輸出:
> ./timeprintf
now = 1680431880
2023-04-02 18:38:00 (week day 0) (year day 92) (daylight saving time 0)
year group:
[04] '%Y': 2023
[02] '%C': 20
[02] '%y': 23
[04] '%G': 2023
[02] '%g': 23
month group:
[02] '%m': 04
[03] '%b': Apr
[03] '%h': Apr
[05] '%B': April
day group:
[01] '%w': 0
[01] '%u': 7
[03] '%a': Sun
[06] '%A': Sunday
[02] '%d': 02
[02] '%e': 2
[03] '%j': 092
week group:
[02] '%U': 14
[02] '%W': 13
[02] '%V': 13
date group
[08] '%D': 04/02/23
[08] '%x': 04/02/23
[10] '%F': 2023-04-02
time group
[02] '%H': 18
[02] '%k': 18
[02] '%I': 06
[02] '%l': 6
[02] '%M': 38
[02] '%S': 00
[08] '%T': 18:38:00
[08] '%X': 18:38:00
[05] '%R': 18:38
[02] '%p': PM
[02] '%P': pm
[11] '%r': 06:38:00 PM
[24] '%c': Sun Apr 2 18:38:00 2023
[10] '%s': 1680431880
timezone group
[05] '%z': +0800
[03] '%Z': CST
common group
[01] '%n':
[01] '%t':
[01] '%%': %
範例中演示了另外一些非標準擴充套件,例如 %s 展示時間對應的 Epoch 值,在 linux 和 darwin 上都是被支援的。
回顧上面的關係圖,strftime 是受時區和夏時制影響的 (標紅部分),下面通過匯出 TZ 環境變數來驗證:
> date
Wed Apr 5 16:28:12 CST 2023
> export TZ=America/New_York
> date
Wed Apr 5 03:28:17 EDT 2023
> ./timeprintf
now = 1680679740
2023-04-05 03:29:00 (week day 3) (year day 95) (daylight saving time 1)
year group:
[04] '%Y': 2023
[02] '%C': 20
[02] '%y': 23
[04] '%G': 2023
[02] '%g': 23
month group:
[02] '%m': 04
[03] '%b': Apr
[03] '%h': Apr
[05] '%B': April
day group:
[01] '%w': 3
[01] '%u': 3
[03] '%a': Wed
[09] '%A': Wednesday
[02] '%d': 05
[02] '%e': 5
[03] '%j': 095
week group:
[02] '%U': 14
[02] '%W': 14
[02] '%V': 14
date group
[08] '%D': 04/05/23
[08] '%x': 04/05/23
[10] '%F': 2023-04-05
time group
[02] '%H': 03
[02] '%k': 3
[02] '%I': 03
[02] '%l': 3
[02] '%M': 29
[02] '%S': 00
[08] '%T': 03:29:00
[08] '%X': 03:29:00
[05] '%R': 03:29
[02] '%p': AM
[02] '%P': am
[11] '%r': 03:29:00 AM
[24] '%c': Wed Apr 5 03:29:00 2023
[10] '%s': 1680679740
timezone group
[05] '%z': -0400
[03] '%Z': EDT
common group
[01] '%n':
[01] '%t':
[01] '%%': %
新增紐約時區後,strftime 生成的時間與北京時間差了 13 個小時,除去時區跨度 12 個小時 (+8 & -4),還有 1 小時是夏時制引發的。通過 %z 和 %Z 的輸出可以觀察到時區的變數。對於夏時制,strftime 沒有提供對應的 format 引數,所以不好直接確認,只能通過時間差值來間接確認。
char *strptime(const char *s, const char *format, struct tm *tm);
strptime 是 strftime 的逆操作,藉助 format 引數解析輸入字串 s,並將結果儲存在引數 tm 中,它的返回值有如下幾種場景:
它的 format 引數和 strftime 幾乎完全一致,除以下幾點:
仍以上面的程式碼為例,如果想檢視任意時間的 format 引數效果,可以增加時間引數並通過 strptime 做解析:
int main (int argc, char *argv[])
{
int ret = 0;
struct tm *t = NULL;
if (argc == 1)
{
time_t now = time (NULL);
printf ("now = %ld\n", now);
t = localtime (&now);
}
else if (argc == 2)
{
static struct tm tmp = { 0};
char const* ptr = strptime (argv[1], "%F %T", &tmp);
if (ptr == NULL)
{
printf ("parse time %s failed\n", argv[1]);
return -1;
}
if (*ptr != NULL)
{
printf ("strptime ret:[%d] %s\n", ptr-argv[1], ptr);
}
t = &tmp;
}
else
{
printf ("Usage: ./timeprintf [YYYY-MM-DD HH:MM:SS]\n");
exit (1);
}
...
}
和之前的區別在於,當用戶給定一個額外引數時,嘗試使用 strptime 進行解析,如果成功,將解析結果用於後續的 strftime 時間引數。預設按 YYYY-MM-DD HH:MM:SS 格式解析:
> ./timeprintf "2023-01-01 10:00:00"
2023-01-01 10:00:00 (week day 0) (year day 1) (daylight saving time 0)
year group:
[04] '%Y': 2023
[02] '%C': 20
[02] '%y': 23
[04] '%G': 2022
[02] '%g': 22
...
注意需要將整個日期時間引數用引號括起來,不然會被 shell 解析為兩個引數。
這裡使用今年第一天來驗證 %g / %G 的輸出,可以看到,因為這天仍屬於 2022 的最後一週,所以它們都返回了 2022。
有的人或許有疑問,經 strptime 解析的時間和 localtime 返回的完全一致嗎?下面做個試驗:
> ./timeprintf > out.1
> ./timeprintf "2023-04-05 16:31:00" > out.2
> diff out.1 out.2
1d0
< now = 1680683460
46c45
< [05] '%z': +0800
---
> [05] '%z': +0000
可以看到,除時區偏移沒解析成功外,其它欄位確實相符 (沒帶引數的 timeprintf 使用的時間也是 16:31;00),這也比較好理解,畢竟提供給 strptime 的字串沒帶時區資訊,如果修改 format 資訊帶上時區呢?
char const* ptr = strptime (argv[1], "%F %T %Z", &tmp);
下面就來試一試:
> ./timeprintf "2023-04-05 16:31:00 CST" > out.2
> diff out.1 out.2
1c1
< now = 1680683460
---
> strptime ret:[20] CST
46c46
< [05] '%z': +0800
---
> [05] '%z': +0000
看起來是沒什麼改善,特別是額外增加的時區名稱 (%Z) 沒有被解析。換 %z 試試:
> ./timeprintf "2023-04-05 16:31:00 +0800" > out.2
> diff out.1 out.2
1d0
< now = 1680683460
這次成功了!再次切換到紐約時間:
> ./timeprintf "2023-04-05 16:31:00 -0400" > out.2
> diff out.1 out.2
1d0
< now = 1680683460
46c45
< [05] '%z': +0800
---
> [05] '%z': -0400
除了 %z 欄位受影響,其它都沒變,特別是 %s 欄位一點影響也沒有,不應該啊。改用 TZ 環境變數嘗試:
> export TZ=America/New_York
> ./timeprintf "2023-04-05 16:31:00 -0400" > out.2
> diff out.1 out.2
1d0
< now = 1680683460
44c43
< [10] '%s': 1680683460
---
> [10] '%s': 1680730260
46,47c45,46
< [05] '%z': +0800
< [03] '%Z': CST
---
> [05] '%z': -0400
> [03] '%Z': EST
這回正常了,看來 strptime 也受時區影響,那它受不受夏時制影響呢?很簡單,將上面 %s 輸出的 Epoch 用當前時區檢視一下就明瞭:
> unset TZ
> date --date=@1680730260
Thu Apr 6 05:31:00 CST 2023
兩者相差 13 小時 (0405 16:31 -> 0406 05:31),中間有夏時制 1 小時的差值,因此受夏時制影響。
最後補充一下,date 命令使用的 format 與 strftime 也基本相同,除以下幾點:
例如:
> date +%z
+0800
> date +%:z
+08:00
> date +%::z
+08:00:00
> date +%:::z
+08
> date +%-z
+800
> date +%_z
+800
> date +%0z
+0800
另外 mac date 與 unix date 語法差異較大,可能考之前在"閏秒"一節末尾的說明。
前面介紹的都是時間的獲取與展示,如果想要設定時間,需要使用另外的例程:
int settimeofday(const struct timeval *tv, const struct timezone *tz);
引數與 gettimeofday 一致。
另外如果只是想計算時間差,儘量不要使用 time 或 gettimeofday,因為它們都會受到使用者對系統時間設定的影響。
int clock_getres(clockid_t clk_id, struct timespec *res);
int clock_gettime(clockid_t clk_id, struct timespec *tp);
int clock_settime(clockid_t clk_id, const struct timespec *tp);
這種場景應當使用 clock_gettime 來獲取相對時間,clk_id 通常有以下幾種選擇:
CLOCK_BOOTTIME 為 linux 拓展,Darwin 上也有其它拓展,為保持可移植性,一般選取 CLOCK_MONOTONIC。clock_getres 是用來獲取對應時鐘型別能夠提供的時間精確度,res 引數儲存其精確度。在設定或休眠的時候,時間值也應該是這個精確度的倍數。clock_settime 是用來設定對應時鐘的時間,不過有些時鐘是不能被設定的。
clock_gettime 返回的 timespec 與 gettimeofday 返回的 timeval 有所不同:
struct timespec {
time_t tv_sec; /* seconds */
long tv_nsec; /* nanoseconds */
};
第一個欄位都是 Epoch,第二個欄位為納秒,比 timeval 的毫秒精度要高不少。
針對 timeval,linux 系統提供了一系列例程來支援時間的計算:
void timeradd(struct timeval *a, struct timeval *b, struct timeval *res);
void timersub(struct timeval *a, struct timeval *b, struct timeval *res);
void timerclear(struct timeval *tvp);
int timerisset(struct timeval *tvp);
int timercmp(struct timeval *a, struct timeval *b, CMP);
比直接手動處理進位、借位方便了不少,不過它們不屬於 POSIX 標準。
最後本文介紹的大多例程需要返回一個內部的靜態記憶體的指標,不是可重入的,既不執行緒安全,也不訊號安全,在"時區->TZ 環境變數"一節的例子中已經體驗過了。為此,POSIX 提供了可重入版本的時間例程:
char *asctime_r(const struct tm *tm, char *buf);
char *ctime_r(const time_t *timep, char *buf);
struct tm *gmtime_r(const time_t *timep, struct tm *result);
struct tm *localtime_r(const time_t *timep, struct tm *result);
將使用使用者提供的引數 (buf/result) 代替內部的靜態記憶體區,如果給定的引數為 NULL,則回退到不帶 _r 字尾的版本;如果不為 NULL,則返回這個引數,以便與非重入版本相容。
[1]. 徹底弄懂GMT、UTC、時區和夏令時
[2]. 格林尼治標準時間
[3]. 協調世界時
[4]. 國家授時中心閏秒公告
[5]. 應對linux下的閏秒
[6]. 聊聊閏秒以及模擬閏秒
[7]. 「閏秒」會對 IT 行業造成多大影響?有什麼好的解決方法?
[8]. mac date命令
[9]. linux系統date命令(時間戳與日期相互轉換)
[10]. 修改系統時區 /etc/localtime
[11]. [Linux] 設定系統時區
[12]. Linux 系統設定 : zdump 命令詳解
[13]. 那些年,我國也實行過夏時制
[14]. tzset 的作用
[15]. 自頂向下地聊聊C++的時間處理和chrono庫
[16]. 雙時製為何在歐洲仍難廢除?
[17]. gettimeofday、clockgettime 以及不同時鐘源的影響
[18]. Linux時間型別、函數和休眠函數
本文來自部落格園,作者:goodcitizen,轉載請註明原文連結:https://www.cnblogs.com/goodcitizen/p/unix_date_time_api_relationship_in_one_picture.html