C 中時(shí)間、時(shí)區(qū)、時(shí)令

問(wèn)題場(chǎng)景

發(fā)送業(yè)務(wù)時(shí)間戳?xí)r,調(diào)用 localtime 時(shí)消耗太大,改用 gmtime + 8小時(shí)的時(shí)區(qū)偏移來(lái)計(jì)算,是否合理?

localtime 的流程

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

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)的類(lè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ǎn)寫(xiě)結(jié)構(gòu)的索引,相當(dāng)于該區(qū)段的名稱
          # tzh_leapcnt 對(duì)4b數(shù)值對(duì),閏秒發(fā)生時(shí)間,閏秒改變的秒數(shù)
          # tzh_ttisstdcnt 個(gè)轉(zhuǎn)變時(shí)間是標(biāo)準(zhǔn)時(shí)間,還是當(dāng)?shù)貢r(shí)間,與 type 對(duì)應(yīng)
          # tzh_ttisutcnt  轉(zhuǎn)變時(shí)間是不是 UT 時(shí)間,與 type 對(duì)應(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 的閏年開(kāi)始計(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ó)中央有關(guān)部門(mén)發(fā)出“在全國(guó)范圍內(nèi)實(shí)行夏時(shí)制的通知”,具體做法是:每年從四月中旬第一個(gè)星期日的凌晨2時(shí)整(北京時(shí)間),將時(shí)鐘撥快一小時(shí),即將表針由2時(shí)撥至3時(shí),夏令時(shí)開(kāi)始;到九月中旬第一個(gè)星期日的凌晨2時(shí)整(北京夏令時(shí)),再將時(shí)鐘撥回一小時(shí),即將表針由2時(shí)撥至1時(shí),夏令時(shí)結(jié)束。從1986年到1991年的六個(gè)年度,除1986年因是實(shí)行夏時(shí)制的第一年,從5月4日開(kāi)始到9月14日結(jié)束外,其它年份均按規(guī)定的時(shí)段施行。在夏令時(shí)開(kāi)始和結(jié)束前幾天,新聞媒體均刊登有關(guān)部門(mé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

中國(guó)無(wú)冬令時(shí)、夏令時(shí)區(qū)分,gmtime 同樣會(huì)將閏秒的修正計(jì)算在內(nèi),盡管當(dāng)面并沒(méi)看到時(shí)區(qū)文件中有閏秒修正。

閏秒的另一個(gè)小問(wèn)題

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.

測(cè)試

// 以時(shí)區(qū)中的轉(zhuǎn)變點(diǎn)做邊界測(cè)試,如下所示,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í)更大,但是并沒(méi)有,有待繼續(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 日志庫(kù)中格式化日期操作

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秒是無(wú)意義的,因?yàn)殚c秒會(huì)讓兩臺(tái)機(jī)器的相對(duì)時(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ū)文件無(wú)閏秒的修正,但是假設(shè)以該時(shí)間處理心跳等,會(huì)引起偶發(fā)的邏輯問(wèn)題。

2038 年問(wèn)題

2038年問(wèn)題_百度百科 (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ī)器無(wú)影響,在特殊場(chǎng)合如 webassambly 編譯中遇到過(guò)。

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號(hào),修改為11月9日處理。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容