问题场景
发送业务时间戳时,调用 localtime 时消耗太大,改用 gmtime + 8小时的时区偏移来计算,是否合理?
localtime 的流程
localtime()
__tz_convert()
tzset_internal() # 解析 TZ 的时区设置,只处理一次
if tz == NULL, tz = /etc/localtime # 使用系统设置时区
# /etc/localtime -> /usr/share/zoneinfo/America/Los_Angeles
__tzfile_read() #解析时区文件,https://www.man7.org/linux/man-pages/man5/tzfile.5.html
fopen()
#tzh_magic = "TZif"
#tzh_version = "2" 8b 转换时间
#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 个 U32 计算日期的转变点
# tzh_timecnt 个上述转变点之前时间段内的类型
# tzh_typecnt 个 ttinfo 信息
# struct ttinfo
# int32_t tt_utoff UT 时间上增加的秒数,[-89999, 93599], -25h -> 6h
# unsigned char tt_isdst 是否设置 tm.tm_isdst, 夏令时标志
# unsigned char tt_desigidx 指向 ttinfo 之后的时区简写结构的索引,相当于该区段的名称
# tzh_leapcnt 对4b数值对,闰秒发生时间,闰秒改变的秒数
# tzh_ttisstdcnt 个转变时间是标准时间,还是当地时间,与 type 对应
# tzh_ttisutcnt 转变时间是不是 UT 时间,与 type 对应
# 时区名称 "PST8PDT,M3.2.0,M11.1.0"
rule_stdoff = -28800s
rule_dstoff = -25200s
__tzfile_compute()
# 时区偏移, -28800
# 时令偏移, 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 的闰年开始计算,年,月,日
# 加上闰秒
gmtime()
__tz_convert()
tzset_internal() # 解析 TZ 的时区设置,只处理一次
if tz == NULL, tz = /etc/localtime # 使用系统设置时区
# /etc/localtime -> /usr/share/zoneinfo/America/Los_Angeles
__tzfile_read() #解析时区文件,https://www.man7.org/linux/man-pages/man5/tzfile.5.html
fopen()
__tzfile_compute()
# 计算闰秒的偏移量
__offtime()
Asia/Shanghai 时区文件
1986年4月,中国中央有关部门发出“在全国范围内实行夏时制的通知”,具体做法是:每年从四月中旬第一个星期日的凌晨2时整(北京时间),将时钟拨快一小时,即将表针由2时拨至3时,夏令时开始;到九月中旬第一个星期日的凌晨2时整(北京夏令时),再将时钟拨回一小时,即将表针由2时拨至1时,夏令时结束。从1986年到1991年的六个年度,除1986年因是实行夏时制的第一年,从5月4日开始到9月14日结束外,其它年份均按规定的时段施行。在夏令时开始和结束前几天,新闻媒体均刊登有关部门的通告。1992年起,夏令时暂停实行。
(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
中国无冬令时、夏令时区分,gmtime 同样会将闰秒的修正计算在内,尽管当面并没看到时区文件中有闰秒修正。
闰秒的另一个小问题
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.
测试
// 以时区中的转变点做边界测试,如下所示,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 的性能瓶颈
在当前机器上,localtime 耗时 142ns,gmtime 耗时 38ns。应该是时区相关的处理耗时较多。
TODO 按照这个猜想,时区设置为夏令时的时区,localtime 耗时更大,但是并没有,有待继续分析。
附录
muduo 中时区计算
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 多线程服务器编程》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) // 缓存秒部分,只在跨秒时更新字符串中的年月日时分秒部分
{
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 多线程服务器编程》chp9.3 业务层心跳机制应规避闰秒
考虑到闰秒的影响,Tc小于1秒是无意义的,因为闰秒会让两台机器的相对时间发生跳变,可能产生报警。
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 标准 linux time() 函数返回的是 UT 时间,不计入闰秒- 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) 是计入闰秒的,因此 linux 上的时区文件无闰秒的修正,但是假设以该时间处理心跳等,会引起偶发的逻辑问题。
2038 年问题
2038年问题_百度百科 (baidu.com)
localtime(INT_MAX) = "2038-1-19 11:14:07",因此 mktime("2038-1-19 11:14:08") 就会导致 32位的 time_t 移出,通常当下的 64 位机器无影响,在特殊场合如 webassambly 编译中遇到过。
mktime 的校验
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 当各字段超出有效范围,mktime 会修改输入参数,如10月40号,修改为11月9日处理。