C 中时间、时区、时令

问题场景

发送业务时间戳时,调用 localtime 时消耗太大,改用 gmtime + 8小时的时区偏移来计算,是否合理?

localtime 的流程

Code Debugging with GDB - part 4: Basic Debug Glibc Source Code | Ethanol's blog (ethanol1310.github.io)

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日处理。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 160,026评论 4 364
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,655评论 1 296
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 109,726评论 0 244
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 44,204评论 0 213
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,558评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,731评论 1 222
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,944评论 2 314
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,698评论 0 203
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,438评论 1 246
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,633评论 2 247
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,125评论 1 260
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,444评论 3 255
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,137评论 3 238
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,103评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,888评论 0 197
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,772评论 2 276
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,669评论 2 271

推荐阅读更多精彩内容