如何获取当前TAI时间?

How to obtain current TAI time?

如何使用 Java 或 C++ 在 Linux 中获取以毫秒为单位的当前 TAI 时间?

我需要它的原因是能够在很长一段时间内(以年为单位)准确地获取时间戳,并且仍然能够比较它们,而不用担心闰秒。闰秒期间可能会发生多次测量,并且所有测量都需要明确、单调递增和线性递增。这将是一个专用的 Linux 服务器。这是一个科学项目,需要大约 0.5 秒的精度。

我目前不希望投资 GPS 计时器,希望使用 NTP pool.ntp.org 以保持系统时钟正常运行。

我研究了以下解决方案:

Java 8 或三十工程 获得 TAIInstant 的唯一方法是使用 Instant 然后转换它,根据规范,"Conversion from an Instant will not be completely accurate near a leap second in accordance with UTC-SLS." 这本身并不是什么大问题(事实上,使用 UTC-SLS 也是可以接受的).不过在 Instant class 中使用 now() 似乎也只是对 System.currentTimeMillis() 的包装,这让我觉得在闰秒期间,时间仍然会模糊,项目将实际上不给我TAI时间。 Java 8 规范还声明:

Implementations of the Java time-scale using the JSR-310 API are not required to provide any clock that is sub-second accurate, or that progresses monotonically or smoothly. Implementations are therefore not required to actually perform the UTC-SLS slew or to otherwise be aware of leap seconds.

使用权/?时区 这似乎可行,但我不确定该实现是否足够智能以在闰秒期间继续工作,或者 System.currentTimeMillis() 是否会给 TAI 时间。换句话说,底层实现是否仍然使用 UTC,从而在闰秒期间给出一个模糊的时间,然后将其转换为 TAI,或者使用正确的/时区实际上总是使用 System.currentTimeMillis() 与 TAI 一起工作(即即使在闰秒期间)?

使用CLOCK_TAI 我尝试在 Linux 内核中使用 CLOCK_TAI,但在我的测试中发现它与 CLOCK_REALTIME 完全相同: 代码:

#include <iostream>
#include <time.h>

long sec(int clock)
{
    struct timespec gettime_now;
    clock_gettime(clock, &gettime_now);
    return gettime_now.tv_sec;
}

int main()
{
    std::cout << sec(0) << std::endl;       // CLOCK_REALTIME
    std::cout << sec(1) << std::endl;       // CLOCK_MONOTONIC
    std::cout << sec(11) << std::endl;      // CLOCK_TAI

    return 0;
}

输出很简单:

1427744797
6896
1427744797

使用CLOCK_MONOTONIC 这样做的问题是,即使计算机重新启动,时间戳也需要保持有效和可比性。

The reason I need this is to be able to accurately take timestamps over a long period of time (on the order of years) and still be able to compare them, without worrying about leap seconds. It is possible for multiple measurements to take place during a leap second and all measurements need to be unambiguous, monotonically increasing, and linearly increasing.

那么你的设计不是最理想的。你不能使用时间然后以某种方式干预闰秒。这实际上经常出现,人们陷入了使用挂钟进行时间戳测量的相同陷阱。

  1. 程序开始的时间戳并将其与计数器相关联
  2. 使用计数器以固定间隔进行连续测量
  3. 根据需要为新计数器值添加时间戳,以保持数据充分同步

如果您避免在可能出现闰秒的那 1 秒(午夜!)添加时间戳,您就可以在家自由,因为这些可以在以后进行调整。

现在,如果您坚持使用没有计数器的 TAI,您所需要的只是一个 table 需要计算的闰秒。然后只需使用单调时间。还有一些库可以为您做这件事,但是它们可能已经过时了,所以您必须自己维护它们,

http://skarnet.org/software/skalibs/libstddjb/tai.html

您必须实现基于 C++ std::steady_clock 或类似语言的 TAI 时钟。要同步您的 TAI 时钟,您可以依靠 GPS 或 NTP。

NTP 的选项 TAI:您的 TAI 实施需要有关闰秒的知识。 NTP 协议或 referenced resources 可能是当前和未来闰秒最可靠的来源。

来自 GPS 的选项 TAI:GPS 时钟与 TAI 有固定的偏移量,您不必搞乱闰秒

CLOCK_REALTIMECLOCK_TAIreturn一样,因为内核参数tai_offset为零。

使用adjtimex(timex tmx)检查并读取值。我认为如果 ntpd 足够新 (>4.2.6) 并且有闰秒文件,它会设置它。它也可能能够从上游服务器获取它,但我无法验证。当运行 为root 时,调用adjtimex() 可以手动设置tai_offsetadjtimex 需要一个新的 man 页面来查看要设置的参数。我的 debian man 页面太旧但命令有效。

除了正确接受的答案外,我还会提到免费的 Java 库 Time4J (min version v4.1) 作为可能的解决方案,因为

  • 我写它是为了填补Java世界的空白(java.time不能全部),
  • 到目前为止给出的其他答案只谈论C++(但你也要求Java),
  • 它的工作原理与@user3427419 描述的相同。

它使用基于 System.nanoTime() 的单调时钟,但甚至允许通过接口 TickProvider 进行自定义实现。出于校准目的,您可以使用 net.time4j.SystemClock.MONOTONIC,或者使用名为 SntpConnector 的 SNTP 时钟,它只需要一些简单的配置即可连接到您想要的任何 NTP 时间服务器。多亏了内置的闰秒-table,Time4J 甚至可以在本月底向您展示宣布的闰秒——采用 ISO-8601 表示法,甚至采用任何时区的格式化本地时间戳字符串(使用i18n-模块).

时钟的重新校准(在 NTP 的情况下 - 重新连接)是可能的,这意味着时钟可以适应中间时间调整(尽管我强烈建议不要在测量期间或闰秒期间这样做)。虽然这种 SNTP 时钟的重新连接通常会在某些情况下导致时间倒退,但 Time4J 尝试应用平滑算法(如果在时钟配置中激活)以确保单调行为。提供详细文档 online.

示例:

// Step 0: configure your clock
String ntpServer = "ptbtime1.ptb.de";
SntpConnector clock = new SntpConnector(ntpServer);

// Step 1: Timestamp start of the program and associate it with a counter
clock.connect(); 

// Step 2: Use the counter for sequential measurements at fixed intervals
Moment m = clock.currentTime();
System.out.println(m); // possible output = 2015-06-30T23:59:60,123456789Z

// Step 3: Timestamp new counter value(s) as necessary to keep your data adequately synced
clock.connect();

我怀疑是否有任何基于 C++ 的解决方案更简单。更多代码演示也可以在DZone.

上研究

更新(评论中问题的答案):

如何自动下载给定的 IETF 资源以获得新的闰秒并将其转换为特定于 Time4J 的格式的稍微简化的解决方案可能如下所示:

URL url = new URL("https://www.ietf.org/timezones/data/leap-seconds.list");
BufferedReader br =
    new BufferedReader(
        new InputStreamReader(url.openStream(), "US-ASCII"));
String line;
PlainDate expires = null;
Moment ntpEpoch = PlainTimestamp.of(1900, 1, 1, 0, 0).atUTC();
List<PlainDate> events = new ArrayList<PlainDate>();

try {
    while ((line = br.readLine()) != null) {
        if (line.startsWith("#@")) {
            long expraw = Long.parseLong(line.substring(2).trim());
            expires = ntpEpoch.plus(
              expraw, TimeUnit.SECONDS)
            .toZonalTimestamp(ZonalOffset.UTC).toDate();
            continue;
        } else if (line.startsWith("#")) {
            continue; // comment line
        }

        // this works for some foreseeable future
        long epoch = Long.parseLong(line.substring(0, 10)); 

        // this is no leap second 
        // but just the official introduction of modern UTC scale
        if (epoch == 2272060800L) {
            continue;
        }

        // -1 because we don't want to associate 
        // the leap second with the following day
        PlainDate event = 
          ntpEpoch.plus(epoch - 1, TimeUnit.SECONDS)
                  .toZonalTimestamp(ZonalOffset.UTC).toDate();
        events.add(event); // we don't assume any negative leap seconds here for simplicity
    }
} finally {
    br.close();
}

// now let's write the result into time4j-format
// use a location relative to class path of main program (see below)
String path = "C:/work/leapseconds.txt"; 
Writer writer = new FileWriter(new File(path));
String sep = System.getProperty("line.separator");

try {
    for (PlainDate event : events) {
        writer.write(event + ", +" + sep);
    }
    writer.write("@expires=" + expires + sep);
} finally {
    writer.close();
}

System.out.println(
  "Leap second file was successfully written from IETF-resource.");

// And finally, we can start the main program in a separate process
// with the system property "net.time4j.scale.leapseconds.path"
// set to our leapsecond file path (must be relative to class path)

一些注意事项:

我建议将此代码编写为简单批处理程序调用的子程序,以避免主程序依赖于 Internet 连接。该批处理文件最终将调用具有上述系统 属性 的主程序。如果你设置这个 property 那么闰秒将从那里指定的文件中读取,然后任何最终可用的 tzdata-module 将停止产生任何并发的闰秒信息。