如何在Linux中使用Java或C++获得当前TAI时间(以毫秒为单位)?
我需要这一点的原因是能够在很长一段时间内(以年为单位)准确地获取时间戳,并且仍然能够对它们进行比较,而不必担心闰秒。在一闰秒内可以进行多次测量,并且所有测量都必须是明确的、单调递增的和线性递增的。这将是一个专用的Linux服务器。这是一个科学项目,需要大约0.5秒的精度。
我目前不想投资GPS计时器,希望使用NTP来pool.NTP.org,以保持系统时钟正常运行。
我研究了以下解决方案:
Java 8或ThreeTen项目获得TAIInstant的唯一方法是使用Instant,然后对其进行转换,根据规范,"根据UTC-SLS,从Instant的转换在闰秒附近不会完全准确。"这本身并不是什么大不了的事(事实上,使用UTC-SLS也是可以接受的)。然而,在Instant类中使用now()似乎只是System.currentTimeMillis()的包装器,这让我认为在闰秒期间,时间仍然是模糊的,项目实际上不会给我TAI时间。Java 8规范还规定:
使用JSR-310 API实现Java时间尺度不是需要提供亚秒精度的任何时钟,或者进展单调或平稳。因此,实施不需要实际执行UTC-SLS转换或以其他方式注意闰秒。
使用右/?时区这看起来是可行的,但我不确定实现是否足够智能,可以在闰秒内继续工作,或者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这样做的问题是,即使计算机重新启动,时间戳也需要保持有效和可比较。
CLOCK_REALTIME
和CLOCK_TAI
返回相同的值,因为内核参数tai_offset
为零。
使用adjtimex(timex tmx)
进行检查并读取值。我认为如果它足够新(>4.2.6
)并且有一个闰秒文件,ntpd
会设置它。它也可以从上游服务器获得,但我还无法验证。调用adjtimex()
可以在以root身份运行时手动设置tai_offset
。adjtimex
需要一个新的ish man
页面来查看要设置的参数。我的debian man
页面太旧了,但命令有效。
除了正确接受的答案外,我还提到免费的Java库Time4J(最低版本v4.1)作为可能的解决方案,因为
- 我写它是为了填补Java世界中的一个空白(
java.time
不能做到所有) - 到目前为止给出的其他答案只涉及C++(但您也要求使用Java)
- 它的工作原理与@user3427419描述的原理相同
它使用基于System.nanoTime()
的单调时钟,但甚至允许通过接口TickProvider
进行自定义实现。为了进行校准,您可以使用net.time4j.SystemClock.MONOTONIC
,也可以使用名为SntpConnector
的SNTP时钟,它只需要一些简单的配置即可连接到您想要的任何NTP时间服务器。由于内置的闰秒表,Time4J甚至可以在本月底以ISO-8601表示法或任何时区的格式化本地时间戳字符串(使用i18n模块)向您显示宣布的闰秒。
时钟的重新校准(在NTP的情况下-重新连接)是可能的,这意味着时钟可以适应中间时间调整(尽管我强烈建议在测量期间或闰秒期间不要这样做)。尽管在某些情况下,SNTP时钟的这种重新连接通常会导致时间倒退,但Time4J试图应用平滑算法(如果在时钟配置中激活)以确保单调行为。详细文档可在线获取。
示例:
// 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)
一些注意事项:
我建议将此代码作为简单批处理程序调用的子程序编写,以避免主程序依赖于互联网连接。这个批处理文件最终将调用具有上述系统属性的主程序。如果设置此属性,则闰秒将从指定的文件中读取,并且任何最终可用的tzdata模块都将停止以产生任何并发的闰秒信息。
我需要它的原因是能够准确地获取时间戳在很长一段时间内(大约几年)仍然能够比较它们,而不用担心闰秒。这是可能的在一个闰秒内进行多次测量测量需要是明确的、单调递增的,并且线性增加。
那么你的设计是次优的。你不能利用时间,然后以某种方式干预闰秒。事实上,这种情况经常出现,人们也陷入了使用挂钟进行时间戳测量的陷阱。
- 时间戳程序的开始并将其与计数器关联
- 使用计数器以固定间隔进行顺序测量
- 根据需要对新计数器值进行时间戳,以保持数据充分同步
如果你在第二秒可能发生的1秒(午夜!)内避免了时间戳,你就可以自由回家了,因为这些时间可以稍后调整。
现在,如果你坚持使用没有计数器的TAI,你所需要的只是一个需要考虑闰秒的表。然后只使用单调时间。还有一些库可以为你做这件事,但它们可能已经过时了,所以你必须自己维护它们,
http://skarnet.org/software/skalibs/libstddjb/tai.html
您必须实现基于C++std::steady_clock或类似功能的TAI时钟。要同步您的TAI时钟,您可以依靠GPS或NTP。
NTP的选项TAI:您的TAI实现需要有关闰秒的知识。NTP协议或参考资源可能是当前和未来闰秒的最可靠来源。
选项TAI从GPS:GPS时钟有一个固定的偏移到TAI,你不必干扰闰秒