`gmtime()` 会在闰秒时将秒数报告为 60 秒吗?

Will `gmtime()` report seconds as 60 when in a leap second?

我在 TZ=UTC 有一个服务器 运行,我有这样的代码:

time_t t = time(NULL);
struct tm tm;
gmtime_r(&t, &tm);

问题是 tm.tm_sec == 60 服务器何时在闰秒内?

例如,如果我在以下时间跨度内:

1998-12-31T23:59:60.00 - 915 148 800.00
1998-12-31T23:59:60.25 - 915 148 800.25
1998-12-31T23:59:60.50 - 915 148 800.50
1998-12-31T23:59:60.75 - 915 148 800.75
1999-01-01T00:00:00.00 - 915 148 800.00

gmtime() return tm == 1998-12-31T23:59:60 time_t = 915148800 并且,一旦超过闰秒, return tm == 1999-01-01T00:00:00 相同 time_t?

这个问题绝对没有简单的答案。为了在有闰秒的时候有 60 秒,你需要 1) OS 中的一些东西来知道有一个闰秒,以及 2) 你使用的 C 库也知道跳秒,并用它做点什么。

很多 OSes 和图书馆没有。

我发现的最好的是 Linux 内核与 gpsd 和 ntpd 结合使用的现代版本,使用 GPS 接收器作为时间参考。 GPS 在其系统数据流中通告闰秒,并且 gpsd、ntpd 和 Linux 内核可以在闰秒发生时保持 CLOCK_TAI,并且系统时钟也是正确的。我不知道 glibc 是否对闰秒做了明智的事情。

在其他 UNIX 上,您的里程会有所不同。相当。

Windows是*******灾区。例如,C# 中的 DateTime class 不知道历史闰秒。下次收到网络时间更新时,系统时钟将跳转 1 秒。

The question is will tm.tm_sec == 60 when the server is within a leap second?

没有。在典型的 UNIX 系统上,time_t 计算自纪元(1970-01-01 00:00:00 GMT)以来的 non-leap 秒数。因此,将 time_t 转换为 struct tm 将始终生成 tm_sec 值介于 0 和 59 之间的时间结构。

time_t 计算中忽略闰秒使得可以将 time_t 转换为 human-readable date/time 而无需完全了解该时间之前的所有闰秒。它还可以在将来明确转换 time_t 值;包括闰秒将使这成为不可能,因为闰秒的存在在未来 6 个月后才为人所知。

UNIX 和 UNIX-like 系统倾向于通过几种方式处理闰秒。最典型的是:

  1. 闰秒重复一个 time_t 值。 (这是严格解释标准的结果,但会导致很多应用程序出现故障,因为看起来时间倒退了。)

  2. 系统时间 运行 稍微慢一些,从闰秒到 "smear" 闰秒的时间跨度更大。 (此解决方案已被许多大型云平台采用,包括 Google and Amazon。它避免了任何本地时钟不一致,代价是受影响的系统在此期间与 UTC 不同步达半秒。)

  3. 系统时间设置为TAI。由于这不包括闰秒,因此不需要闰秒处理。 (这种情况很少见,因为它会使系统与世界大部分地区的 UTC 系统不同步几秒钟。但对于与外界几乎没有联系的系统来说,这可能是一个可行的选择,因此无法得知即将到来的闰秒。)

  4. 系统完全不知道闰秒,但其 NTP 客户端会在闰秒使系统时钟与正确时间相差一秒后更正时钟。 (This is what Windows does.)

简短的回答是,不,实际上 gmtime_r 永远不会用 60 填充 tm_sec。这是不幸的,但不可避免。

根本问题是,根据 Posix 标准,time_t 是自 1970 年 1 月 1 日 UTC 以来的秒数 假设没有闰秒.

在最近的闰秒中,进度是这样的:

1483228799    2016-12-31 23:59:59
1483228800    2017-01-01 00:00:00

是的,那里应该有一个闰秒,23:59:60。但是 14832287991483228800 之间没有可能的 time_t 值。

我知道 gmtime 变体 return 的两种方法,时间以 :60 结尾:

  1. 您可以 运行 您的 OS 时钟不是 UTC,通常是 TAI 或 TAI-10,并使用 so-called "right"时区转换为 UTC(或本地时间)进行显示。有关此问题的一些讨论,请参阅 this web page

  2. 您可以使用 clock_gettime() 并定义一个新的 clkid 值,也许是 CLOCK_UTC,它通过使用故意非规范化的 struct timespec 来解决 time_t 问题] 必要时的值。例如,获取14832287991483228800之间的时间值的方法是将tv_sec设置为1483228799,将tv_nsec设置为1000000000。有关详细信息,请参阅 this web page

方式 #1 工作得很好,但没有人使用它,因为没有人想要 运行 他们的内核时钟在除应该的 UTC 以外的任何地方。 (你最终会遇到诸如文件系统时间戳之类的问题,以及像 tar 这样嵌入了这些时间戳的程序。)

方式 2 是个绝妙的主意,IMO,但据我所知,它从未在已发布的 OS 中实现。 (碰巧,我有一个 Linux 的工作实现,但我还没有发布我的工作。)为了方法 #2 工作,你需要一个新的 gmtime 变体,也许 gmtime_ts_r,它接受 struct timespec 而不是 time_t.


附录:我刚刚重读了你的问题标题。你问,"Will gmtime() report 60 for seconds when the server is on a Leap Second?" 我们可以用 "yes, but" 来回答这个问题,但免责声明是因为大多数服务器不能正确表示闰秒期间的时间,所以它们 永远不会 "on" 一个闰秒。


附录 2:我忘了提到方案 #1 似乎更适合当地时间——也就是说,当你调用 localtime 变体之一时——比 UTC 时间和 gmtime。很明显,localtime 执行的转换受 TZ 环境变量设置的影响,但 TZgmtime 有任何影响还不清楚。我观察到一些 gmtime 实现受 TZ 的影响,因此可以根据 "right" 区域进行闰秒,而有些则不能。特别是 GNU glibc seems to pay attention to the leap second information in a "right" zone if TZ specifies one, whereas the gmtime in the IANA tzcode distribution 中的 gmtime 没有。

他们问题的另一个角度是拥有一个 'know so' 关于闰秒的库。大多数图书馆不这样做,因此严格来说,您从 gmtime 等函数获得的答案在闰秒期间是不准确的。此外,时差计算通常会产生跨越闰秒的不准确结果。例如,昨天在同一 UTC 时间提供给您的 time_t 的值比今天的值正好小 86400 秒,即使实际上有闰秒。

天文学界已经解决了这个问题。这是 SOFA Library that has proper time routines within. See their manual (PDF),关于时间尺度的部分。如果成为您软件的一部分并保持最新(每个新的闰秒都需要一个新版本),您就可以进行准确的时间计算、转换和显示。

POSIX 指定 time_t "Seconds Since the Epoch" 值和 broken-down (struct tm) 时间之间的关系TAI,所以本质上(关于闰秒附近应该发生什么的一些歧义),POSIX time_t 值是 UT1,而不是 UTC,gmtime 的结果反映了这一点。确实没有办法适应或改变它与现有规范和基于它们的现有软件兼容。

几乎可以肯定,正确的前进方式是将 Google 对 leap second smearing 所做的工作与在 "smeared UTC" 和 "actual UTC" 之间来回转换的标准化公式相结合24 小时内 window 的时间(因此也是 TAI),围绕闰秒和执行这些转换的 API。

我在 www.cplusplus.com 读到这篇关于 gmtime 的文章:"Uses the value pointed by timer to fill a tm structure with the values that represent the corresponding time, expressed as a UTC time (i.e., the time at the GMT timezone)"。

所以有矛盾。 UTC 具有绝对恒定长度的秒,因此需要闰秒,而 GMT 具有恰好 86,400 秒的天数,长度略有不同。 gmtime() 不能同时在 UTC 和 GMT 下工作。

当我们被告知 gmtime () returns "UTC assuming no leap seconds" 时,我会假设这意味着 GMT。这意味着没有闰秒记录,这意味着时间慢慢偏离 UTC,直到差异大约 0.9 秒并且 UTC 中添加了闰秒,但 GMT 中没有。这对开发人员来说很容易处理,但不太准确。

一种替代方法是让秒数保持不变,直到接近闰秒,然后调整大约 1000 秒左右的闰秒长​​度。它也很容易处理,大部分时间 100% 准确,有时 1000 秒的秒长误差为 0.1%。

第二种选择是有固定的秒数,有闰秒,然后忘记它们。因此 gmtime() 将连续两次 return 同一秒,从 x 秒 0 纳秒到 x 秒 999999999 纳秒,然后再次从 x 秒 0 纳秒到 x 秒 999999999 纳秒,然后到 x+1 秒。这会引起麻烦。

当然有另一个时钟将 return 精确的 UTC,包括闰秒,精确秒,将是有用的。要将 "seconds since epoch" 转换为年、月、日、小时、分钟、秒,需要了解自纪元以来的所有闰秒(如果处理纪元之前的时间,则为纪元之前)。一个时钟将 return 保证准确的格林威治标准时间,没有闰秒和秒数,这些秒数几乎但不是完全恒定的时间。