問題場景
發(fā)送業(yè)務(wù)時(shí)間戳?xí)r,調(diào)用 localtime 時(shí)消耗太大赞别,改用 gmtime + 8小時(shí)的時(shí)區(qū)偏移來計(jì)算畏陕,是否合理?
localtime 的流程
localtime()
__tz_convert()
tzset_internal() # 解析 TZ 的時(shí)區(qū)設(shè)置仿滔,只處理一次
if tz == NULL, tz = /etc/localtime # 使用系統(tǒng)設(shè)置時(shí)區(qū)
# /etc/localtime -> /usr/share/zoneinfo/America/Los_Angeles
__tzfile_read() #解析時(shí)區(qū)文件惠毁,https://www.man7.org/linux/man-pages/man5/tzfile.5.html
fopen()
#tzh_magic = "TZif"
#tzh_version = "2" 8b 轉(zhuǎn)換時(shí)間
#tzh_reserved = '\000' <repeats 14 times>
#num_isgmt = 6 tzh_ttisutcnt = "\000\000\000\006",
#num_isstd = 6 tzh_ttisstdcnt = "\000\000\000\006",
#num_leaps = 0 tzh_leapcnt = "\000\000\000",
#num_transitions = 186 tzh_timecnt = "\000\000\000\272",
#num_types = 6 tzh_typecnt = "\000\000\000\006",
#chars = 20 tzh_charcnt = "\000\000\000\024"}
# tzh_timecnt 個(gè) U32 計(jì)算日期的轉(zhuǎn)變點(diǎn)
# tzh_timecnt 個(gè)上述轉(zhuǎn)變點(diǎn)之前時(shí)間段內(nèi)的類型
# tzh_typecnt 個(gè) ttinfo 信息
# struct ttinfo
# int32_t tt_utoff UT 時(shí)間上增加的秒數(shù),[-89999, 93599], -25h -> 6h
# unsigned char tt_isdst 是否設(shè)置 tm.tm_isdst, 夏令時(shí)標(biāo)志
# unsigned char tt_desigidx 指向 ttinfo 之后的時(shí)區(qū)簡寫結(jié)構(gòu)的索引崎页,相當(dāng)于該區(qū)段的名稱
# tzh_leapcnt 對4b數(shù)值對鞠绰,閏秒發(fā)生時(shí)間,閏秒改變的秒數(shù)
# tzh_ttisstdcnt 個(gè)轉(zhuǎn)變時(shí)間是標(biāo)準(zhǔn)時(shí)間飒焦,還是當(dāng)?shù)貢r(shí)間蜈膨,與 type 對應(yīng)
# tzh_ttisutcnt 轉(zhuǎn)變時(shí)間是不是 UT 時(shí)間,與 type 對應(yīng)
# 時(shí)區(qū)名稱 "PST8PDT,M3.2.0,M11.1.0"
rule_stdoff = -28800s
rule_dstoff = -25200s
__tzfile_compute()
# 時(shí)區(qū)偏移荒给, -28800
# 時(shí)令偏移, 0
# 閏秒偏移丈挟,0
__offtime(t, off, tp)
d1 = (t + off) / (24 * 60 * 60)
h = (t + off) % (24 * 60 * 60) / (60 * 60)
m = (t + off) % (24 * 60 * 60) % (60 * 60) / 60
s = (t + off) % (24 * 60 * 60) % (60 * 60) % 60
wd = (4 + d1) % 7 # 1970-1-1 周四
# 從 1970 的閏年開始計(jì)算,年志电,月曙咽,日
# 加上閏秒
gmtime()
__tz_convert()
tzset_internal() # 解析 TZ 的時(shí)區(qū)設(shè)置,只處理一次
if tz == NULL, tz = /etc/localtime # 使用系統(tǒng)設(shè)置時(shí)區(qū)
# /etc/localtime -> /usr/share/zoneinfo/America/Los_Angeles
__tzfile_read() #解析時(shí)區(qū)文件挑辆,https://www.man7.org/linux/man-pages/man5/tzfile.5.html
fopen()
__tzfile_compute()
# 計(jì)算閏秒的偏移量
__offtime()
Asia/Shanghai 時(shí)區(qū)文件
1986年4月例朱,中國中央有關(guān)部門發(fā)出“在全國范圍內(nèi)實(shí)行夏時(shí)制的通知”,具體做法是:每年從四月中旬第一個(gè)星期日的凌晨2時(shí)整(北京時(shí)間)鱼蝉,將時(shí)鐘撥快一小時(shí)洒嗤,即將表針由2時(shí)撥至3時(shí),夏令時(shí)開始魁亦;到九月中旬第一個(gè)星期日的凌晨2時(shí)整(北京夏令時(shí))渔隶,再將時(shí)鐘撥回一小時(shí),即將表針由2時(shí)撥至1時(shí)洁奈,夏令時(shí)結(jié)束间唉。從1986年到1991年的六個(gè)年度,除1986年因是實(shí)行夏時(shí)制的第一年利术,從5月4日開始到9月14日結(jié)束外呈野,其它年份均按規(guī)定的時(shí)段施行。在夏令時(shí)開始和結(jié)束前幾天印叁,新聞媒體均刊登有關(guān)部門的通告被冒。1992年起军掂,夏令時(shí)暫停實(shí)行。
(gdb) p transitions[0]@29
$18 = {-2177481943, -1600675200, -1585904400, -933667200, -922093200, -908870400, -888829200, -881049600, -767869200, -745833600,
-733827600, -716889600, -699613200, -683884800, -670669200, -652348800, -650019600, 515527200, 527014800, 545162400, 558464400,
577216800, 589914000, 608666400, 621968400, 640116000, 653418000, 671565600, 684867600}
1900-12-31 23:54:17, isdst = 0.
1919-04-13 01:00:00, isdst = 1.
1919-09-30 23:00:00, isdst = 0.
1940-06-01 01:00:00, isdst = 1.
1940-10-12 23:00:00, isdst = 0.
1941-03-15 01:00:00, isdst = 1.
1941-11-01 23:00:00, isdst = 0.
1942-01-31 01:00:00, isdst = 1.
1945-09-01 23:00:00, isdst = 0.
1946-05-15 01:00:00, isdst = 1.
1946-09-30 23:00:00, isdst = 0.
1947-04-15 01:00:00, isdst = 1.
1947-10-31 23:00:00, isdst = 0.
1948-05-01 01:00:00, isdst = 1.
1948-09-30 23:00:00, isdst = 0.
1949-05-01 01:00:00, isdst = 1.
1949-05-27 23:00:00, isdst = 0.
1986-05-04 03:00:00, isdst = 1.
1986-09-14 01:00:00, isdst = 0.
1987-04-12 03:00:00, isdst = 1.
1987-09-13 01:00:00, isdst = 0.
1988-04-17 03:00:00, isdst = 1.
1988-09-11 01:00:00, isdst = 0.
1989-04-16 03:00:00, isdst = 1.
1989-09-17 01:00:00, isdst = 0.
1990-04-15 03:00:00, isdst = 1.
1990-09-16 01:00:00, isdst = 0.
1991-04-14 03:00:00, isdst = 1.
1991-09-15 01:00:00, isdst = 0.
(gdb) n
(gdb) p types[0].offset
$19 = 29143
(gdb) p types[1].offset
$20 = 32400
(gdb) p types[2].offset
$21 = 28800
中國無冬令時(shí)昨悼、夏令時(shí)區(qū)分蝗锥,gmtime 同樣會(huì)將閏秒的修正計(jì)算在內(nèi),盡管當(dāng)面并沒看到時(shí)區(qū)文件中有閏秒修正率触。
閏秒的另一個(gè)小問題
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 */
};
The members of the tm structure are:
tm_sec
The number of seconds after the minute, normally in the range 0 to 59, but can be up to 60 to allow for leap seconds.
測試
// 以時(shí)區(qū)中的轉(zhuǎn)變點(diǎn)做邊界測試玛追,如下所示,91-09-15 之后闲延,gmtime +8 與 localtime 一致
localtime 1986-05-04 03:00:01, isdst = 1.
gmtime+8 1986-05-04 02:00:01, isdst = 0.
localtime 1986-09-14 01:00:01, isdst = 0.
gmtime+8 1986-09-14 01:00:01, isdst = 0.
localtime 1987-04-12 03:00:01, isdst = 1.
gmtime+8 1987-04-12 02:00:01, isdst = 0.
localtime 1987-09-13 01:00:01, isdst = 0.
gmtime+8 1987-09-13 01:00:01, isdst = 0.
localtime 1988-04-17 03:00:01, isdst = 1.
gmtime+8 1988-04-17 02:00:01, isdst = 0.
localtime 1988-09-11 01:00:01, isdst = 0.
gmtime+8 1988-09-11 01:00:01, isdst = 0.
localtime 1989-04-16 03:00:01, isdst = 1.
gmtime+8 1989-04-16 02:00:01, isdst = 0.
localtime 1989-09-17 01:00:01, isdst = 0.
gmtime+8 1989-09-17 01:00:01, isdst = 0.
localtime 1990-04-15 03:00:01, isdst = 1.
gmtime+8 1990-04-15 02:00:01, isdst = 0.
localtime 1990-09-16 01:00:01, isdst = 0.
gmtime+8 1990-09-16 01:00:01, isdst = 0.
localtime 1991-04-14 03:00:01, isdst = 1.
gmtime+8 1991-04-14 02:00:01, isdst = 0.
localtime 1991-09-15 01:00:01, isdst = 0.
gmtime+8 1991-09-15 01:00:01, isdst = 0.
localtime 2023-03-05 22:25:16, isdst = 0.
gmtime+8 2023-03-05 22:25:16, isdst = 0.
localtime 的性能瓶頸
在當(dāng)前機(jī)器上痊剖,localtime 耗時(shí) 142ns,gmtime 耗時(shí) 38ns垒玲。應(yīng)該是時(shí)區(qū)相關(guān)的處理耗時(shí)較多陆馁。
TODO 按照這個(gè)猜想,時(shí)區(qū)設(shè)置為夏令時(shí)的時(shí)區(qū)合愈,localtime 耗時(shí)更大叮贩,但是并沒有,有待繼續(xù)分析佛析。
附錄
muduo 中時(shí)區(qū)計(jì)算
class TimeZone : public muduo::copyable
{
public:
static TimeZone UTC();
static TimeZone China(); // Fixed at GMT+8, no DST
static TimeZone loadZoneFile(const char* zonefile);
struct DateTime toLocalTime(int64_t secondsSinceEpoch, int* utcOffset = nullptr) const;
int64_t fromLocalTime(const struct DateTime&, bool postTransition = false) const;
// gmtime(3)
static struct DateTime toUtcTime(int64_t secondsSinceEpoch);
// timegm(3)
static int64_t fromUtcTime(const struct DateTime&);
struct Data;
private:
explicit TimeZone(std::unique_ptr<Data> data);
std::shared_ptr<Data> data_;
friend class TimeZoneTestPeer;
};
《linux 多線程服務(wù)器編程》chp5.2 日志庫中格式化日期操作
void Logger::Impl::formatTime()
{
int64_t microSecondsSinceEpoch = time_.microSecondsSinceEpoch();
time_t seconds = static_cast<time_t>(microSecondsSinceEpoch / Timestamp::kMicroSecondsPerSecond);
int microseconds = static_cast<int>(microSecondsSinceEpoch % Timestamp::kMicroSecondsPerSecond);
if (seconds != t_lastSecond) // 緩存秒部分益老,只在跨秒時(shí)更新字符串中的年月日時(shí)分秒部分
{
t_lastSecond = seconds;
struct DateTime dt;
dt = g_logTimeZone.toLocalTime(seconds);
int len = snprintf(t_time, sizeof(t_time), "%4d%02d%02d %02d:%02d:%02d",
dt.year, dt.month, dt.day, dt.hour, dt.minute, dt.second);
assert(len == 17); (void)len;
}
Fmt us(".%06d ", microseconds);
assert(us.length() == 8);
stream_ << T(t_time, 17) << T(us.data(), 8);
}
《linux 多線程服務(wù)器編程》chp9.3 業(yè)務(wù)層心跳機(jī)制應(yīng)規(guī)避閏秒
考慮到閏秒的影響,Tc小于1秒是無意義的寸莫,因?yàn)殚c秒會(huì)讓兩臺機(jī)器的相對時(shí)間發(fā)生跳變捺萌,可能產(chǎn)生報(bào)警。
linux 如何處理閏秒
- RF8536
- UNIX Time: The time as returned by the time() function provided by the C programming language (see Section 3 of the "System Interfaces" volume of [POSIX]). This is an integer number of seconds since the POSIX epoch, not counting leap seconds. As an extension to POSIX, negative values represent times before the POSIX epoch, using UT.
- UNIX Leap Time: UNIX time plus all preceding leap-second corrections. For example, if the first leap-second record in a TZif file occurs at 1972-06-30 23:59:60 UTC, the UNIX leap time for the timestamp 1972-07-01 00:00:00 UTC would be 78796801, one greater than the UNIX time for the same timestamp. Similarly, if the second leap-second record occurs at 1972-12-31 23:59:60 UTC, it accounts for the first leap second, so the UNIX leap time of 1972-12-31 23:59:60 UTC would be 94694401, and the UNIX leap time of 1973-01-01 00:00:00 UTC would be 94694402. If a TZif file specifies no leap-second records, UNIX leap time is equal to UNIX time.
按照 POSIX 標(biāo)準(zhǔn) linux time() 函數(shù)返回的是 UT 時(shí)間膘茎,不計(jì)入閏秒- linux clock_gettime(CLOCK_TAI) (since Linux 3.10; Linux-specific) A nonsettable system-wide clock derived from wall-clock time but ignoring leap seconds. This clock does not experience discontinuities and backwards jumps caused by NTP inserting leap seconds as CLOCK_REALTIME does.
而 clock_gettime(CLOCK_REALTIME) 是計(jì)入閏秒的桃纯,因此 linux 上的時(shí)區(qū)文件無閏秒的修正,但是假設(shè)以該時(shí)間處理心跳等披坏,會(huì)引起偶發(fā)的邏輯問題态坦。
2038 年問題
2038年問題_百度百科 (baidu.com)
localtime(INT_MAX) = "2038-1-19 11:14:07",因此 mktime("2038-1-19 11:14:08") 就會(huì)導(dǎo)致 32位的 time_t 移出棒拂,通常當(dāng)下的 64 位機(jī)器無影響伞梯,在特殊場合如 webassambly 編譯中遇到過。
mktime 的校驗(yàn)
The mktime() function modifies the fields of the tm structure as follows: tm_wday and tm_yday are set to values determined from the contents of the other fields; if structure members are outside their valid interval, they will be normalized (so that, for example, 40 October is changed into 9 November); tm_isdst is set (regardless of its initial value) to a positive value or to 0, respectively, to indicate whether DST is or is not in effect at the specified time. Calling mktime() also sets the external variable tzname with information about the current timezone. mktime 當(dāng)各字段超出有效范圍帚屉,mktime 會(huì)修改輸入?yún)?shù)谜诫,如10月40號,修改為11月9日處理涮阔。