Linux如何获取时间

背景

这几天和盛哥聊到了系统调用的开销,有些是io,有些是内存,突然提到了尽管像获取时间这种调用,有些对性能要求极高的系统也会做针对性优化。
连”获取时间“都要优化,nginx不就是这么处理么?为什么要这样处理?时间这个基础性的信息底层到底是怎样呢,而高级语言Java/C这些获取时间的方式又是怎么和底层交互的?这个问题激起了我极大的兴趣,希望能通过对“时间”的技术细节做层梳理和探索,加深对操作系统以及Java语言的理解。以生产环境服务器ubuntu为例,设定以下目标

  • C/Java是如何获取当前时间
  • Linux是怎么维护和衡量时间
  • java获取当前时间的原理,是否存在性能问题
  • nginx对获取时间做了什么优化

C/Java是如何获取当前时间

The Linux Programming Interface 上看,获取时间最常用的函数是 gettimeofday,常见的中间件也是用该函数么?

1
2
3
#include <sys/time.h>
int gettimeofday(struct timeval *tv, struct timezone *tz);
Returns 0 on success, or1 on error

Redis

扫了下redis的命令,发现redis有个time command. 调用该command能够返回当前服务器的时间戳,从redis源码
)上看,redis也是调用了gettimeofday获取当前的时间

1
2
3
4
5
6
7
8
9
10
void timeCommand(client *c) {
struct timeval tv;

/* gettimeofday() can only fail if &tv is a bad address so we
* don't check for errors. */
gettimeofday(&tv,NULL);
addReplyArrayLen(c,2);
addReplyBulkLongLong(c,tv.tv_sec);
addReplyBulkLongLong(c,tv.tv_usec);
}

Java

Java呢,是怎么获取时间的, System.currentTimeMillis()是java中获取当前时间的基础,java.lang.Date对象的构造也是通过调用System.currentTimeMillis()获取当前时间戳进行初始化。

System源码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
/**
* Returns the current time in milliseconds. Note that
* while the unit of time of the return value is a millisecond,
* the granularity of the value depends on the underlying
* operating system and may be larger. For example, many
* operating systems measure time in units of tens of
* milliseconds.
*
* <p> See the description of the class <code>Date</code> for
* a discussion of slight discrepancies that may arise between
* "computer time" and coordinated universal time (UTC).
*
* @return the difference, measured in milliseconds, between
* the current time and midnight, January 1, 1970 UTC.
* @see java.util.Date
*/
public static native long currentTimeMillis();

好吧,该方法是一个native方法,那就只能继续往jvm源码上看,从jvm源码上看,java获取时间戳最终也是通过gettimeofday获取,殊途同归。

jvm源码os_linux.cpp

1
2
3
4
5
6
jlong os::javaTimeMillis() {
timeval time;
int status = gettimeofday(&time, NULL);
assert(status != -1, "linux error");
return jlong(time.tv_sec) * 1000 + jlong(time.tv_usec / 1000);
}

好,继续往下一个问题

Linux是怎么维护和衡量时间

总的来说,现代操作系统主板上会有个Real Time Clock,记录着当前的时间,通过主板上的电池CMOS Battery维持,假如电池没电了,那我们启动后时间就会出现不正常的情况,通过api去获取时间当然的也会出现相应的问题问题。假如时间出现问题,可以通过手动或者自动的方式校准。

服务器启动的时候就会读取当前时间,保存到kernel,记T1
同时启动TSC,什么是TSC?简单来说就是一个64bit的寄存器,记录着服务器启动以来cpu的cycle,每个cycle的时间就是1/CPU频率,例如cpu 2GHZ, 那么就表示一个cycle的时间是0.5ns
所以获取当前时间的方式就是

1
当前时间 = T1 + TSC * 0.5  //时间的精度为1ns

除了TSC, Linux还有其他方式维护时间,但由于目前主流服务器上都是TSC,所以其他方式的不详述
如何查看当前服务器支持的时间源

1
2
cat /sys/devices/system/clocksource/clocksource0/available_clocksource
tsc hpet acpi_pm

如何查看当前服务器使用的时间源

1
2
cat /sys/devices/system/clocksource/clocksource0/current_clocksource
tsc

Linux最常用的获取时间的接口是gettimeofday, 但除了该方法,Linux还提供了clock_gettime,该api提供了多种获取时间的方式,这里需要注意的是,gettimeofday最终也是调用了clock_gettime就是指定了clockid clock_realtime的时间, 在新版本, linux提供多了一种clockid,CLOCK_REALTIME_COARSE,从manual上看该clockid提供了了一种新的选择“need very fast, but not fine-grained timestamps”。

需要注意的,对于clockid CLOCK_MONOTONIC,这里获取的是一个单调递增的计数器,不受系统时间修改的影响,这个java也会使用到,先记着:)

clockid description
CLOCK_REALTIME System-wide clock that measures real (i.e., wall-clock) time. Setting this clock requires appropriate privileges. This clock is affected by discontinuous jumps in the system time (e.g., if the system administrator manually changes the clock), and by the incremental adjustments performed by adjtime(3) and NTP.
CLOCK_REALTIME_COARSE (since Linux 2.6.32; Linux-specific) A faster but less precise version of CLOCK_REALTIME. Use when you need very fast, but not fine-grained timestamps.
CLOCK_MONOTONIC Clock that cannot be set and represents monotonic time since some unspecified starting point. This clock is not affected by discontinuous jumps in the system time (e.g., if the system administrator manually changes the clock), but is affected by the incremental adjustments performed by adjtime(3) and NTP.
CLOCK_MONOTONIC_COARSE (since Linux 2.6.32; Linux-specific) A faster but less precise version of CLOCK_MONOTONIC. Use when you need very fast, but not fine-grained timestamps.
CLOCK_MONOTONIC_RAW (since Linux 2.6.28; Linux-specific) Similar to CLOCK_MONOTONIC, but provides access to a raw hardware-based time that is not subject to NTP adjustments or the incremental adjustments performed by adjtime(3).
CLOCK_BOOTTIME (since Linux 2.6.39; Linux-specific) Identical to CLOCK_MONOTONIC, except it also includes any time that the system is suspended. This allows applications to get a suspend-aware monotonic clock without having to deal with the complications of CLOCK_REALTIME, which may have discontinuities if the time is changed using settimeofday(2).
CLOCK_PROCESS_CPUTIME_ID (since Linux 2.6.12) Per-process CPU-time clock (measures CPU time consumed by all threads in the process).
CLOCK_THREAD_CPUTIME_ID (since Linux 2.6.12) Thread-specific CPU-time clock.

接下来,我们就来对比各种clockid的性能。
可以参考StackOverflow的这个回答,可以看出
CLOCK_REALTIME和CLOCK_REALTIME_COARSE的性能区别还是比较高,这样对于需要高性能但相对不精确的时间时还是可以选择CLOCK_REALTIME_COARSE的

time (s) => 3 cycles
ftime (ms) => 54 cycles
gettimeofday (us) => 42 cycles
clock_gettime (ns) => 9 cycles (CLOCK_MONOTONIC_COARSE)
clock_gettime (ns) => 9 cycles (CLOCK_REALTIME_COARSE)
clock_gettime (ns) => 42 cycles (CLOCK_MONOTONIC)
clock_gettime (ns) => 42 cycles (CLOCK_REALTIME)
clock_gettime (ns) => 173 cycles (CLOCK_MONOTONIC_RAW)
clock_gettime (ns) => 179 cycles (CLOCK_BOOTTIME)
clock_gettime (ns) => 349 cycles (CLOCK_THREAD_CPUTIME_ID)
clock_gettime (ns) => 370 cycles (CLOCK_PROCESS_CPUTIME_ID)
rdtsc (cycles) => 24 cycles

好吧,上面只是别人的一些经验,直接在服务器上跑测试,可以看出CLOCK_REALTIME平均执行16.15ns, coarse版本的api执行的时间为0, 说明执行的时间小于 1/3.4 ns,好了,至此已经知道Linux常用获取时间的api的效率了

服务器CPU为 3.4GHz

Method min max avg median stdev
CLOCK_REALTIME 14.00 18.00 16.15 16.00 1.24
CLOCK_REALTIME_COARSE 0.00 0.00 0.00 0.00 0.00
CLOCK_MONOTONIC 14.00 19.00 15.88 16.50 1.23
CLOCK_MONOTONIC_RAW 63.00 67.00 64.87 65.00 1.23
CLOCK_MONOTONIC_COARSE 0.00 0.00 0.00 0.00 0.00

java获取当前时间的原理,是否存在性能问题

从上面已知, System.currentTimeMillis()最终调用是到了clock_realtime,该api执行时间大概16ns.
为什么java不用coarse版本的接口呢?从网上查了一轮资料,发现在2017年其实已经有人提了这个问题,具体问题可参考Jdk Issue
简单的说,官方回复是,该执行时间目前不是瓶颈,coarse版本对于旧版本的操作系统不一定支持,需要进行各种兼容判断,会带来额外的性能开销。

java其他时间函数- System.nanoTime()

Java除了System.currentTimeMillis(),还有一个常用的获取时间api, System.nanoTime(). 这个方法容易望文生义,觉得就是返回当前时间戳的纳秒数。在服务器上执行以下代码

1
2
3
4
public static void main(String[] args) {
System.out.println(System.currentTimeMillis());
System.out.println(System.nanoTime());
}

控制台打印

1
2
1598924672863
8616575473749327

这个输出有点奇怪,假如nanoTime输出的是是距离epoch至今的纳秒数,那么理论上值应该是 System.currentTimeMillis() * 10的9次方

JDK注释上看, “can only be used to measure elapsed time”, 注释说明了该api提供的是作用是度量elapsed time, 而非某个具体的时间,该返回的时间也不受系统时间(wall clock)修改影响,例如ntp的时间矫正或者人工调整时间。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
/**
* Returns the current value of the running Java Virtual Machine's
* high-resolution time source, in nanoseconds.
*
* <p>This method can only be used to measure elapsed time and is
* not related to any other notion of system or wall-clock time.
* The value returned represents nanoseconds since some fixed but
* arbitrary <i>origin</i> time (perhaps in the future, so values
* may be negative). The same origin is used by all invocations of
* this method in an instance of a Java virtual machine; other
* virtual machine instances are likely to use a different origin.

* <p> For example, to measure how long some code takes to execute:
* <pre> {@code
* long startTime = System.nanoTime();
* // ... the code being measured ...
* long estimatedTime = System.nanoTime() - startTime;}</pre>
*

那么nanoTime有什么应用场景呢,其实在jdk里面有些类也特意使用了nanoTime.
Java有个比较经典的问题,为什么使用ScheduledThreadPoolExecutor而非Timer.
Java Concurrency in Practice 里对比Timer和ScheduledThreadPoolExecutor提到以下

Timer does have support for scheduling based on absolute, not relative time, so that tasks can be sensitive to changes in the system clock; ScheduledThreadPoolExecutor supports only relative time.

这个的意思是对于Timer的定时任务,会受到系统时间的影响,为什么呢?
因为Timer计算执行时间是通过System.currentTimeMillis(), 该时间是绝对时间,修改了系统时间,该时间相应就产生了变化

1
2
3
4
5
public void schedule(TimerTask task, long delay) {
if (delay < 0)
throw new IllegalArgumentException("Negative delay.");
sched(task, System.currentTimeMillis()+delay, 0);
}

ScheduledThreadPoolExecutor是通过nanoTime获取时间

1
2
3
4
5
6
7
long triggerTime(long delay) {
return now() +
((delay < (Long.MAX_VALUE >> 1)) ? delay : overflowFree(delay));
}
final long now() {
return System.nanoTime();
}

那么nanoTime这个api获取的是什么时间?从jvm源码上看,对于支持supports_monotonic_clock该时间源的,获取的就是CLOCK_MONOTONIC这个时间,这个时间是单调递增, 不受系统时间影响,所以ScheduledThreadPoolExecutor使用该clock source会更加的合理。

os_linux.cpp

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
jlong os::javaTimeNanos() {
if (Linux::supports_monotonic_clock()) {
struct timespec tp;
int status = Linux::clock_gettime(CLOCK_MONOTONIC, &tp);
assert(status == 0, "gettime error");
jlong result = jlong(tp.tv_sec) * (1000 * 1000 * 1000) + jlong(tp.tv_nsec);
return result;
} else {
timeval time;
int status = gettimeofday(&time, NULL);
assert(status != -1, "linux error");
jlong usecs = jlong(time.tv_sec) * (1000 * 1000) + jlong(time.tv_usec);
return 1000 * usecs;
}
}

nginx是如何做优化

简单说, nginx是通过时间缓存优化时间获取的效率,定时更新缓存,获取时间时从该缓存获取

待补充

补充信息

gettimeofday虽然是存在一定开销,但linux其实已经针对做了很多优化,其实在目前,连系统调用的开销也已经消除了,通过vsdo技术,待持续学习

引用

  1. how-do-computers-keep-track-of-time
  2. select posix clocks
  3. fast equivalent of gettimeofday
  4. HPET vs TSC
  5. the-slow-currenttimemillis