计算机中几个与时间相关的概念
世界标准时
时间与人类的生活息息相关,可时间本身是连续的且不存在刻度,因此,人类引入了世界标准时的概念用以统一时间计量。度量时间意味着需要将时间转换成离散的,即使用计量单位表示时间。不同的标准时使用的度量标准不同:即对计量单位的定义标准不同。
以下是关于计量单位秒的两种定义标准,分别是:
根据地球自转和公转
地球自转,且围绕太阳公转。根据相对运动的原理,以地球为参照物时,太阳是围绕地球运动的。因此,把太阳连续两次穿过地球表面某一个定点的经线(子午线)所需的时间定为一天,即24个小时,换算可得到秒的时长。
比如格林尼治时间(Greenwich Mean Time,GMT)将太阳两次横穿格林尼治子午线所需的时长定为一天。
这种定义标准显然更符合人类习惯,但是由于地球公转轨迹是一个椭圆,意味着地球公转速度是不均匀的,且地球自转的速度正在缓慢减速,换言之,GMT时间在缓慢地变长。因此,GMT时间不再作为标准时间,取而代之的是UTC时间。
采用原子时秒
原子时秒,由原子钟导出,简言之,是以铯-133的振荡频率来定义秒。由于GMT时间存在不均匀性和低精度性,自1867年起,世界标准时改用原子时作为基本的时间计量系统。
协调世界时(Universal Time Coordinated,UTC),就是采用的这种定义标准。
定义秒这一计量单位后,向下可以进一步细分为毫秒、微妙和纳秒等,向上则可以组合成分钟、小时、日、月和年等概念。
几个与时间相关的概念
由于时间就像是一条没有起点和终点的直线,除了给出计量单位,比如秒,还需要一个基准点(epoch)作为度量的起始参考点。
以下是计算机系统中的几个与时间相关的概念,它们采用的基准点也不尽相同。
系统时钟
计算机系统中一般都存在两种系统时钟,二者的区别在于采用的基准点不同,分别是:
以系统启动时刻为基准点
我们常说的系统时钟就是指以系统启动时刻为基准点的计时系统。顾名思义,这种系统时钟的从系统启动的那一刻从零开始计时。
这种系统时钟的好处在于它是独立的,即不需要与其他系统进行时间同步。因为在通信的过程中,我们大多数时候关注的是相对时间的概念,因此使用这种系统时间即可。
以下的Mac系统中获取系统时间的两种方式:
Mac系统独有的函数
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17int64_t GetMacSystemTimeInNanos {
static mach_timebase_info_data_t timebase;
if (timebase.denom == 0) {
// Get the timebase if this is the first we run.
// Recommond by Apple's QA1398
assert(mach_timebase_info(&timebase) == KERN_SUCCESS && "Not Reached.");
}
// Use timebase to convert absolute time tick uints into nanoseconds
const auto mul = [](uint64_t a, uint32_t b) -> int64_t {
assert(b != 0);
assert(a <= std::numeric_limits<int64_t>::max() / b);
return utils::numeric::checked_static_cast<int64_t>(a * b);
};
// mach_absolute_time is a CPU/Bus dependent function that
// returns a value based on the number of 'ticks' since the system started up.
return mul(mach_absolute_time(), timebase.numer) / timebase.denom;
}兼容POSIX系统
1
2
3
4
5
6
7
8
9constexpr int64_t kNumNanosecsPerSec = 1'000'000'000;
int64_t GetPosixSystemTimeInNanos {
int64_t ticks = -1;
struct timespec ts;
// CLOCK_MONOTONIC: returns a value based on the number of 'ticks' since the system started up, independently.
clock_gettime(CLOCK_MONOTONIC, &ts);
return static_cast<int64_t>(ts.tv_sec) * kNumNanosecsPerSec +
static_cast<int64_t>(ts.tv_nsec);
}
Linux/Unix epoch
在中Linux/Unix系统使用的系统时间称之为POSIX time,其采用的时间基准点是1970年1月1日0点0分0秒(UTC)。即POSIX time表示的是自UTC 1970年1月1日0点0分0秒以来经过的秒数,包括小数秒,但忽略润秒。
以下是两种获取POSIX time的方式:
调用gettimeofday函数
1
2
3
4
5
6int64_t GetUTCTimeInMicros() {
struct timeval time;
gettimeofday(&time, nullptr);
// Convert from second (1.0) and microsecond (1e-6).
return static_cast<int64_t>(time.tv_sec) * kNumMicrosecsPerSec + time.tv_usec;
}调用clock_gettime函数
1
2
3
4
5
6
7
8constexpr int64_t kNumNanosecsPerSec = 1'000'000'000;
GetUTCTimeInNanos() {
struct timespec ts;
// Use CLOCK_REALTIME_COARSE is faster.
clock_gettime(CLOCK_REALTIME, &ts);
return static_cast<int64_t>(ts.tv_sec) * kNumNanosecsPerSec +
static_cast<int64_t>(ts.tv_nsec);
}
建议使用第一种方式,原因在于gettimeofday调用使用的是vsyscall,即不需要经过内核进程切换即可直接读取已预设好的系统时间,没有线程切换的开销,效率更高,其实现原理是:
系统内核在每次调用时间中断时会把当前的系统时间写在某个固定的位置,然后通过mmap机制映射到用户空间,调用该函数时只需在用户空间读取对应位置的数值即可,不涉及线程切换。
NOTE:关于vsyscall的实现原理:
传统的系统调用方式是通过INT 0x80中断/SYSTEMCALL,这种方式会造成内核空间和用户空间的上下文切换,因此效率较低。为此,Intel和AMD两家芯片巨头分别实现了sysenter/sysexit 和 syscall/ sysret,即快速系统调用指令。为了解决硬件层面的兼容问题,Linux实现了vsyscall机制,软件层面统一调用vsyscall来加速系统调用。
vsyscall是一种用于加速特定系统调用的一种机制,以减少系统调用的开销,适用于一些触发频繁且进行只读操作的系统调用。比如获取系统时间,这个系统调用并不会向内核提供参数,而仅是从内核中读取数据。
相较之下,clock_gettime的本身的执行就很耗时,其实现原理是:
直接进入内核空间读时钟设备的内存映射,加之用户空间和内存空间切换等操作,效率很低。但如果clock_gettime第一个参数使用CLOCK_REALTIME_COARSE,即获取一个粗略的系统时间,则比较高效。
NTP
网络时间协议(Network Time Protocol ,NTP),是一种用于同步同一网络下不同计算机系统的时钟的网络协议,采用分组交换的方式实现。NTP采用Marzullo算法来选择准确的时间服务器,旨在将同一网络下所有的计算机的UTC时间同步到几毫米的误差内。比如,NTP可以将互联网内的时间误差维持下几十毫秒以下,局域网内的时间误差维持下1毫秒左右。但是,不对称路由和拥塞控制可能导致超过100毫秒的误差。
NTP的时间基准点是:1900年1月1日0点0分0秒(UTC)。
NTP的格式如下所示,共64 bits,包括两个秒和小数秒两个部分,各32 bits:
1
2
3
4
5
6
7// NTPTime
struct NTPTime {
uint32_t seconds; // 秒,32 bits
uint32_t fractions; // 小数秒,32 bits
}
// NTP中秒和小数秒的转换关系:1秒 = 2^32小数秒
constexpr uint64_t kFractionsPerSecond = 0x100000000; // 2^32
时间转换
系统时间转UTC时间
此处的系统时间是指以系统启动时间为基准点的计时系统,下同。把时间比喻成一条直线,UTC时间的起点的1970年1月1日0点0分0秒,而系统时间则是系统启动时间。因此,任一时刻的UTC时间减去同一时刻的系统时间即为二者的差值,且这个差值在当前系统关闭之前都是相同的,即为定值。以下是二者转换的伪代码:
1
2
3
4// 计算一次即可,因此可设为常量
const int64_t utc_and_system_time_diff = curr_utc_time - curr_system_time;
// 任一时刻系统时间转换成UTC时间
int64_t utc_time_x = system_time_x + utc_and_system_time_diff;UTC时间转NTP时间
UTC时间和NTP时间除了基准点不同之外,二者表示时间的格式也不同。以下是转换代码:
1
2
3
4
5
6
7
8
9
10
11
12
13// 从UTC时间基准点(1970年1月1日0点0分0秒)到NTP时间基准点(1900年1月1日0点0分0秒)
// 之间的间隔的秒数
constexpr uint32_t kNtpJan1970Sec = 2'208'988'800UL;
// 秒转微秒
constexpr int64_t kNumMicrosecsPerSec = 1'000'000;
// UTC时间转NTP时间,时间单位为微秒
int64_t ntp_time_us = utp_time_us + kNtpJan1970Sec * kNumMicrosecsPerSec;
// NTP格式转换
NTPTime ntp_time;
// Convert seconds to uint32 through uint64 for a well-defined cast.
ntp_time.seconds = static_cast<uint64_t>(ntp_time_us / kNumMicrosecsPerSec);
// A wrap around, which will happen in 2036, is expected for NTP time.
ntp_time.fractions = (ntp_time_us % kNumMicrosecsPerSec) * kFractionsPerSecond / kNumMicrosecsPerSec;系统时间转NTP时间
结合上述两种转换,系统时间转NTP时间的步骤是:系统时间→UTC时间→NTP时间。
参考资料
Linux时间子系统之(一):时间的基本概念 - ArnoldLu - 博客园