GMT 是一個 時區,也指一種 時制。很久以前,科學家通過天文觀察,將一個太陽日定義為 86400 秒,以英國 Greenwich 天文臺白天平均太陽最高點作為正午 12:00,這樣一個相對長度 + 一個絕對時刻,就定義了一套絕對時間體系,也就是 GMT 體系,同時 Greenwich 所在的時區也作為 GMT+0 時區。自1924年2月5日開始,Greenwich 天文臺負責每隔一小時向全世界發放調時信息。再後來又從 GMT 升級到了 UT1,本質不變,還是基於天體測量。
UTC (Coordinated Universal Time)UTC 時間體系是基於 銫(caesium)-133 原子鐘。原子從某高能階躍遷到某低能階時,會釋放頻率非常穩定的電磁波,例如 銫-133 原子在基態的兩個超精細能級之間躍遷時,會釋放頻率為 9192631770 Hz 的電磁波,即該電磁波 1 秒中有 9192631770 個周期。那麼反過來,可定義該電磁波經過 9192631770 個周期所用的時間為 1 秒。然後以某個 GMT 時間為起點,每計數該電磁波的 9192631770 個周期為 1 秒,這種時間體系就是 UTC 時間體系。
Leap Second (閏秒)據上,可知 UTC 剛引入的時候和 GMT 時間是同步的。但是地球的自轉並不是勻速不變的(地震,地殼運動等),且整體趨勢呈緩慢減速(Tidal Acceleration 效應),而原子鐘計量的時間是勻速增加的。為了使 UTC 時間和 GMT 時間誤差不超過 0.9 秒,需要每隔一段時間(半年或一年或多年)把 UTC 時間減去 1 秒(不減這 1 秒的話,累計起來,過兩萬年,UTC 的表已經中午 12 點了,太陽才剛升起來,UT1 的表是早上 6 點)。被減去的這 1 秒叫做 Leap Second (閏秒)。有了閏秒,UTC 才和 earth rotation 保持同步。
Timezone (時區)大概在本初子午線(Prime Meridian)左右 7.5 度範圍內為 0 時區,UK 和 Greenwich 位於這個時區內,即 GMT+0 時區。
這個時區在 UTC 中被稱為 Zulu Timezone,簡記 Z。
GPS (Global Positioning System)GPS 時間也基於原子鐘,但它沒有 Leap second,不和地球自轉保持同步。GPS 時間 是相對於 UTC 時間的某一時刻開始計時的,該時刻稱為 GPS 時間的 epoch (UTC時間1980年1月6日00:00:00)。GPS 時間和 UTC 時間的每一秒開始時間被同步在 25ns 的誤差內(消除相對論誤差和設備誤差等)。到現在(20190830)為止,GPS 時間已經超前 UTC 時間 18 秒。意味著從 1980年1月6日 到今日,UTC 已經調整過 18 個閏秒。
TAI (International Atomic Time) 時間也是基於原子鐘計時,也沒有 Leap Second。TAI 時間的 Epoch 是 1958/01/01 00:00:00 UTC+0。從該 Epoch 開始截止今天(2019/09/05), UTC 已經調整過 37 個閏秒,因此 TAI 現在超前 UTC 37 秒。
Daylight Saving Time (夏令時)為了充分利用白天時間,某些地區實行夏令時。以美國為例:
每年3月的第二個周日凌晨2點,時鐘從2點撥到3點,時制記為 PST (Pacific Standard Time)。
每年11月的第一個周日凌晨2點,時鐘從2點撥到1點,時制記為 PDT (Pacific Daylight Time)。
例如 2019-03-10 01:59:59 PST 的下一秒是 2019-03-10 03:00:00 PDT
ISO Time FormatISO 8601 規定的時間格式:(由 date 和 time 兩部分組成)
Posix TimePosix Time(Unix Time/Unix TimeStamp等),指從1970-01-01 00:00:00 UTC(這個時刻稱為 Posix Time Epoch) 開始所經過的秒數(減去閏秒,和UTC時間基本一致)。根據表示的精確度的不同,Unix時間戳有秒級(time_t)、毫秒級(timeval)、納秒級(timespec)等。
Unix和UTC不完全一致,UTC規範的閏秒表示為 2016-12-31 23:59:60 UTC,而 Unix time 會重複兩個 23:59:59 時刻。並且,兩個 UTC 時刻之差的時間會計入閏秒,而兩個 Posix time 之間的時間差是不考慮閏秒的。
老版本 Linux 是以 32bit signed int 存儲 unix time 的,因此最多支持到 2038-01-19 03:14:07 UTC,下一秒變成 1901-12-13 20:45:52 UTC。因此也稱為 Year 2038 problem。因此老版本請儘快升級。
新版本 Linux 中日期和時間一般用 struct timespec 表示,它包含兩個成員:tv_sec(從1970年開始的秒數,整數) 和 tv_nsec(納秒部分)
編程接口1. 彙編級別的計時舉一個例子:Intel CPU 指令 rdtsc (read Time Stamp Counter):
It counts the number of CPU cycles since reset(last reboot)。換言之 rdtsc 的 epoch 一般是最近一次開機時間
跨平臺性差
不同核不一定值相同;如果一個 thread 被 migrate 到不同核,計數可能發生倒退。
有人說由於CPU指令亂序,該指令的執行可能發生在前面的指令之前,因此計時不準確
也有人說 rdtsc 計時器的時鐘可能會不穩定(CPU clock)
實測(CentOS7.2 x64,該伺服器已開機運行接近兩年):
用 std::chrono::steady_clock 測得的經過時間為 1年12個月08天 15:54:26
用 rdtsc + /proc/cpuinfo 中顯示的 CPU 頻率來計算:1年12個月08天 16:16:31
用 rdtsc + 實時測量的 CPU HZ 來計算:1年12個月08天 15:57:25
2. linux 系統函數/usr/include/time.h
/usr/include/linux/time.h
/usr/include/sys/time.h
/usr/include/bits/time.h
struct timeval
struct timeval {
int gettimeofday(timeval* tv, timezone*)
time_t tv_sec;
suseconds_t tv_usec;
}struct timespec
struct timezone {
int tz_minuteswest;
int tz_dsttime;
}int clock_gettime(clockid clk_id, timespec*)
這是用一個 Linux syscall
返回指定 clock_id 下,"now" 距離其 epoch 的時間 (seconds + nanoseconds)。clk_id 取值:
CLOCK_REALTIME:返回距離 Posix time epoch 的時間。實測(CentOS7.2 x64) chrono::system_clock::now() 調用了它
CLOCK_MONOTONIC:返回距離最近一次開機的時間。不計入系統 suspend 時間。實測(CentOS7.2 x64) chrono::steady_clock::now() 調用了它
返回值:0 表示正常
3. ctime#include <ctime>
std::time_t
std::time_t time(std::time_t* tloc) "posix"
std::tm* localtime(std::time_t*) "local"
std::tm* gmtime(std::time_t*) "posix"
char* ctime(std::time_t*) "local" 【整段加灰】
std::tm
struct tm {
int tm_sec; /* Seconds (0-60) */
int tm_min; /* Minutes (0-59) */
int tm_hour; /* Hours (0-23) */
int tm_mday; /* Day of the month (1-31) */ "只有mday是從1開始"
int tm_mon; /* Month (0-11) */ "呵呵"
int tm_year; /* Year - 1900 */ "years since 1900, 例如100表示2000年"
int tm_wday; /* Day of the week (0-6, Sunday = 0) */
int tm_yday; /* Day in the year (0-365, 1 Jan = 0) */
int tm_isdst; /* Daylight saving time */ "0 表示 no Daylight Saving Time"
};std::time_t std::mktime(std::tm*) "local"
char* std::asctime(std::tm*)
size_t std::strftime(buf, len, format, std::tm*)
std::put_time(tm*, format)
std 不支持 Linux strptime (char*轉tm*),用 std::get_time 代替。
4. std::chrono#include <chrono>
std::ratio 在 chrono 庫中表示秒、分、時等單位std::nano 即 std::ratio<1,1000000000>,在 chrono 中表示納秒單位
std::micro 即 std::ratio<1,1000000>,在 chrono 中表示微秒單位
std::milli 即 std::ratio<1,1000>,在 chrono 中表示毫秒單位
模板二參默認是1,ratio<60> 即 ratio<60, 1>,在 chrono 中表示1分鐘
chrono::days (C++20) 相當於 duration<int, ratio<86400>>;
chrono::hours 相當於 duration<int, ratio<3600>>;
chrono::minutes 相當於 duration<int, ratio<60>>;
chrono::seconds 相當於 duration<long, ratio<1>>
chrono::milliseconds 相當於 duration<long, std::milli>;
chrono::microseconds 相當於 duration<long, std::micro>;
chrono::nanoseconds 相當於 duration<long, std::nano>;
chrono literals (C++14起支持)using namespace std::chrono_literals;
感受一下這種方便的語法:
auto x = 1h; 相當於 auto x = chrono::hours(1);
auto x = 1min; 相當於 auto x = chrono::minutes(1);
auto x = 1s; 相當於 auto x = chrono::seconds(1);
auto x = 1ms; 相當於 auto x = chrono::milliseconds(1);
auto x = 1us; 相當於 auto x = chrono::microseconds(1);
auto x = 1ns; 相當於 auto x = chrono::nanoseconds(1);
durationduration 代表時間的長度,例如 duration(1) 表示 1 秒。
duration 是一個模板類,支持 integer 或 float 類型的時間長度,需要明確指定該類型,即 duration<int>(1) 表示 1 秒,或者 duration<double>(1.2) 表示 1.2 秒。
duration 還能代表 分鐘,需要指定模板的第二個參數:duration<int, ratio<60>>(1) 表示 1 分鐘。其中 ratio<60> 表示時間單位是 60 秒,也就是 1 分鐘。
duration::rep:即模板一參,是時間長度的類型 (例如 int, long, double 等,未提供默認類型)。要求是有符號數,從而可以表示 epoch 前的數。
duration::period:即模板二參,是時間的單位,默認是 std::ratio<1>,表示單位是秒。
count() 返回 period 的個數,返回值類型為 rep,如果 rep 是 double,則 count() 返回小數
duration_cast
(duration1) 從一種duration1轉換為另一種duration2:time_pointA time_point object expresses a point in time relative to a clock's epoch. Typical epochs include 1970-01-01 00:00:00(for system_clock) and the instant when the computer booted up(for steady_clock).
time_point::clock:時刻對應的時鐘。可以是 system_clock, steady_clock, high_resolution_clock。不同的 Clock 即不同的 epoch。
time_point::duration:指定計時的單位(period)和類型(rep),一般是 秒(ratio<1>) 和 integer(long)。
time_since_epoch():
time_point_cast
(duration1) 從一種duration1轉換為另一種duration2:重載了運算符 +, -, ++, --, >, <, == 等來方便 time_point 和 duration 的自然語義運算
system_clocksystem_clock measures Posix Time (i.e., time since UTC 1970-01-01 00:00:00, not counting leap seconds).
system_clock::duration:system_clock keeps track of the time elapsed since its epoch,duration 給出了 elapsed time 的計數單位(period)和計數數值類型(rep)。
system_clock::period:即 duration::period
system_clock::rep:即 duration::rep
system_clock::time_point:即 time_point<system_clock>,簡潔記法。
now():Get current time, 返回值類型為 system_clock::time_point
to_time_t():Convert from time_point to std::time_t
from_time_t():Convert from std::time_t to time_point
從 C++20 開始支持 cout << time_point,易於調試
結合 <thread>:std::this_thread::sleep_for(std::chrono::duration)
結合 <thread>:std::this_thread::sleep_until(std::chrono::time_point)
steady_clockClass std::chrono::steady_clock represents a monotonic(單調遞增) clock. The time points of this clock cannot decrease as physical time moves forward and the time between ticks of this clock is constant. This clock is not related to wall clock time (for example, it can be time since last reboot), and is most suitable for measuring intervals.
steady_clock::duration:steady_clock 一般實現為 記錄距離最近一次開機經過的時間(duration),或者說 steady_clock 的 epoch 是最近一次開機的時刻。duration 給出了 elapsed time 的計數單位(period)和計數數值類型(rep)。
steady_clock::period:即 duration::period
steady_clock::rep:即 duration::rep
is_steady(): always true。注意 system_clock 一定不是 steady 的:because the "system clock" can be adjusted. Such an adjustment may cause a call to now() to return a value earlier than that returned by a prior call to now(). 因此 system_clock 不適合用於非常嚴格地 measure time interval。
high_resolution_clockhigh_resolution_clock is the clock with the shortest tick period. It may be a synonym for system_clock or steady_clock. 即二者誰的精度高就選誰,實測:
5. Boost DateTime#include <boost/date_time.hpp>
Boost date_time 庫主要由3個部分組成:
boost::gregorian 表示年月日
boost::posix_time 結合 gregorian::date,表示年月日時分秒
boost::local_time 結合在 posix_time 基礎上加上時區,可以表示任意 UTC 時間
格式化輸入/輸出:boost::locale, facet, cin.imbue
boost::gregorian今年是2019年,是指公曆(或稱為陽曆,新曆,西曆,基督歷),也稱為 Gregorian Calendar(格里曆)。其 epoch 也就是 公元元年,也就是 1 AD。(往前一年是 1 BC)
using namespace boost::gregorian;
gregorian::date d{2014, 1, 23};
date day_clock::local_day() -> 獲得 posix now 並轉成 local date
date day_clock::universal_day() -> 獲得 posix now 並轉成 unix date
gregorian::date_duration dd{4}; 等同於 days dd{4};
boost::posix_timeusing namespace boost::posix_time;
time_duration td{12, 0, 0} a time_duration object of 12:00:00
另外也有 hours{h}, minutes{m}, seconds{s}, nanoseconds{ns} 都是 time_duration。
td 的成員有 td.hours(), minutes(), seconds(), total_seconds(), fractional_seconds()
ptime{date, time_duration}
ptime 由 date + time_duration 組成。
ptime.date() 返回其中的 date object
ptime.time_of_day() 返回其中的 time_duration object
ptime second_clock::local_time() -> 獲得 posix now 並轉成 local date + local time_duration
ptime second_clock::universal_time() -> 獲得 posix now 並轉成 utc date + utc time_duration
boost::local_timeusing namespace boost::local_time;
舉例辦法1. ptime -> tm -> time_t
auto now = second_clock::local_time();
auto _tm = to_tm(now);
auto _time_t = mktime(&_tm);
辦法2. ptime -> time_duration -> total_seconds()
(second_clock::universal_time() - ptime{{1970,1,1},{0,0,0}}).total_seconds()lstat("file", &sb);
ptime t{date{1970,1,1}, seconds{sb.st_mtim.tv_sec} + nanoseconds{sb.st_mtim.tv_nsec}};tm to_tm(date/ptime/time_duration/local_date_time)
date date_from_tm(tm);
ptime ptime_from_tm(tm)
ptime from_time_t(time_t)
std的tm和time_t都不能指定具體時區,因此無法轉local_date_time
Linux的tm有擴展成員:.tm_gmtoff = +28800秒, .tm_zone = "HKT"ptime pt{date{2002,01,20}, time_duration{12,0,0}};
6. C++20 date (HowardHinnant/date)
// os<<put_time/strftime %Z%z 在 Linux 得到 "CST+0800"
// os<<put_time/strftime %Z%z 在 Windows 得到 "China Standard Time+0800"
strftime(tz_str, 256, "%z", &_tm); tz_str[strlen(tz_str)-2]='\0';
time_zone_ptr tz{new posix_time_zone{tz_str}};
local_date_time dt{pt, tz};
// boost/date_time 不支持 current_zone// github HowardHinnant/date
date 和 time 支持
#include <date/date.h>
#include <date/tz.h>
using namespace date;date 級別:year_month_day 是年月日 field type class,是結構體,記錄 3 個數(年/月/日);
time 級別:time_of_day 時分秒 field type class。同時記錄了 mode (默認0代表24時制)
sys_time 是 serial-based type,是一個標量,記錄從 epoch 開始的(納?)秒數。
year_month_day簡單款:auto ymd = 2019_y/sep/1;
2019_y/mar/sun[2] 表示 March 的第二個 Sunday
複雜款:year_month_day ymd{year(2019), month(9), day(1)};
sys_time就是 system_clock 中的某個 time_point,簡稱 sys_time
sys_time<D>等價於 time_point<system_clock, D>
year_month_day 可以自動轉換成 sys_days:
sys_days x = 1970_y/jan/1;
轉換成 time_point 後可以參與 time_point 的時間運算 (例如+,-,>,<等)
floor<D>(參數是 duration 或者 time_point); 類似 duration_cast/time_point_cast<D>(xxx),只是後者的捨入方向是 towards epoch 的;而 floor 是 towards 負無窮的。
從 C++20 開始,system_clock 被明確定義為 Posix time clock,不論在 Linux 還是 Windows。在C++20之前有些混亂,Windows曾嘗試定義 system_clock 的 epoch 為 1601-01-01 00:00:00
時區支持時區支持須結合 IANA 發布的時區資料庫 tzdata.tar.gz 協同工作。
time_zonetimezone tz = locate_zone("America/Los_Angeles");
UTC+0 zone 可以用 locate_zone("UTC") 或 locate_zone("Etc/UTC") 表示。
auto tz = current_zone();
local_timelocal_time 比較特殊,雖然它是一個 time_point,但未指定 clock,不包含時區信息
zoned_timezoned_time = local_time + timezone 結構體。
make_zoned 構造一個 zoned_time。
例如系統是HK時區,下面兩個 zoned_time 相等:
make_zoned("Asia/Hong_Kong", local_days{2019_y/sep/1} + 16h);
make_zoned(current_zone(), sys_days{2019_y/sep/1} + 8h);
這是因為當地時間 16:00 HKT 相當於 posix time 8:00 UTC
Leap Second 支持在上述所有的 lib 中,只有這個 lib 支持 leap second。
由於 Leap Second 的存在,從 posix time 2015/07/01 23:59:59 到 2015/07/02 00:00:00 對應物理世界的 2 秒鐘。
對於 posix clocks(sys_clock),23:59:59 這一個時間被數了兩次。
對於 utc clocks(utc_clock),時間經過了 23:59:59 -> 23:59:60 -> 00:00:00 三個時刻。
posix time_point 的計時標量只增加了 1
utc time_point 的計時標量增加了 2
utc_timeutc_time是 Clock = utc_clock 的 time_point,即 time_point<utc_clock, D>,其中 D 可以是任意 std::duration Type
auto t0 = to_utc_time(sys_days{2015_y/jul/1) - 200ms);
其他 clock
auto t1 = to_utc_time(sys_days{2015_y/jul/1) + 200ms);
可驗證:t1 - t0 == 1400ms;本 Lib 還支持 tai_clock, gps_clock
可以用 to_gps_time(某 time_point) 進行轉換。
Formatting"%F %T" 其中 %F=%Y-%m-%d(2014-01-23),%T=%H:%M:%S(14:55:02)
"%Y%m%d %H%M%S"
"%z" 得到時區offset字符串 "+0800"
"%Z" 得到時區abbrv字符串 "HKT"
下面記 TIME = local_time/zoned_time/sys_time
cout << TIME
cout << format(fmtstr, TIME);
cout << format(locale, fmtstr, time)
istringstream("1970-01-01 05:00:00 GMT +08") >> parse("%F %T %Z %z", TIME, zone_str, tz_offset);
存在的問題Linux 需要存在目錄 ~/Downloads,最好預先解壓 tzdata2019b.tar.gz 到 ~/Downloads/tzdata
Windows 需要存在目錄 %USERPROFILE%/Downloads,且解壓 tzdata2019b.tar.gz 到 %USERPROFILE%/Downloads/tzdata,並且下載 windowsZones.xml 到 %USERPROFILE%/Downloads/tzdata。
目前沒有 utc_days 定義,utc_time
似乎不支持從 ymd 構造istringstream("1999-01-01 05:00:00 +0800") >> parse("%F %T %z", ltp, offset) 支持有問題
istringstream("05:00:00 +0800") >> parse("%T %z", ltp) 支持有問題
istringstream("05:00:00") >> parse("%T", chrono::seconds) 支持有問題
zoned_time.get_sys_time(); VS2015無法編譯
cout << sys_time; VS2015無法編譯
locate_zone(string) 不支持 "GMT+8" 或者 "UTC+8" 或者 "CST" 這種標識。北京時間只能用 locate_zone("Asia/Shanghai") 這種冗長的形式表示。
這個庫還比較新,會有一些問題。歡迎去其github頁面(howardhinnant date)提 bug。
Summary建議使用 std::chrono + HowardHinnant/date,其內在邏輯通順,語義比較自然。
ReferencesStanding on the shoulders of giants:
Jeff Garland: author of boost::chrono, boost::date_time
Howard Hinnant: https://howardhinnant.github.io/
Acknowledgements