计算机的时间系统
时间
时间是一个非常抽象的概念,多少年来,吸引着无数科学家、物理学家、甚至哲学家花费毕生精力去解释时间的本质是什么,从宇宙大爆炸到时空相对论,从黑洞到量子力学,都能看到关于时间这个问题的身影。这里我们不探讨高深莫测的学术知识,只把目光放聚焦在计算机这个很小的范畴内。但要想清楚解释这个问题,也并非想的那么简单。我们从最简单的开始说起。想要知道时间是怎么被定义的,首先要知道「天」是怎么来的?
答案是:观察太阳。
由于地球的「自转」,人们可以看到日出日落,人们日出而作,日落而息,所以就把这一周期现象定义为「天」。地球除了自转,还在围绕太阳公转,所以公转一周就被定义为一「年」。从这些现象就能看出来,很早之前的人们,是以「天文现象」来确定时间的。
再后来,人们为了把时间定义得更「精确」,就把一天平均划分为 24 等份,这就是「时」。
同样地,把 1 小时划分 60「分钟」,1 分钟划分为 60「秒」。
这样,时间的基本单位「秒」就被定义出来了。
所以,秒与天的关系就是这样的:
1 秒 = 1 / 24 * 60 * 60 = 1 / 86400 天。
这些定义,都与「地球自转」和「太阳」息息相关。
但是,后来人们发现,地球的公转轨道并不是一个正圆,而是一个「椭圆」,也就是说公转速度是「不均匀」的,这意味着什么呢?这意味着每天的时间不是等长的,那根据天推算出的秒,自然也不是「等长」的。
很明显,这里的计算存在误差。这怎么办?聪明的人们就想到,把一年内所有天的时长加起来,然后求「平均」,得到相对固定的「天」,然后再计算得出「相对平均」的秒,这样就减小了误差。
确定了天文规律,人们开始制造「钟表」,把时间表示出来。从摆钟到机械钟,再到现代广泛使用的石英钟,钟表的制作工艺越来越高,时间精度也越来越高,现代石英钟每天的计时误差只有「千分之一秒」。
所以,在 1927 年,人们以基于「天文现象」+「钟表计时」,确立了第一套时间标准:世界时(Universal Time,简称 UT)。
但是,随着科技的发展,人类对太阳的观测越来越精准,有意思的事情发生了。人们发现,地球每天的自转速度也「不是匀速」的,地球的自转受到潮汐、地壳运动、冰川融化、地震等自然现象的影响,越来越慢!这会导致什么问题呢?
这会导致之前规定的,每年平均下来一天的时间,现在来看,也是不一样长的。例如,第 1 年算出来平均一天的时间是 23.9997 小时,第 2 年可能是 23.998 小时,第 3 年可能是 23.999 小时...
那按照 1 秒 = 1 / 86400 天的定义,每一年的「秒」,也是不一样长的。这就比较尴尬了,人们以地球自转为依据,定义出来的时间,还是不准!
你可能会想,时间有误差会有什么问题吗?人们依赖不准确的天文现象,不也生活了几个世纪么?确实,对于人们的基本生活影响其实并不大。但随着人类活动的发展,人们对于高精度的时间场景开始变得越来越多。
例如,体育赛事中百分之一秒的差距就能决定胜负,炮弹的发射要精确在千分之一秒内发生,雷达技术甚至需要精确到百万分之一秒...尤其是卫星发射、火箭试验等航天领域,对高精度的时间系统也提出了越来越高的要求!
怎么办?怎么彻底解决时间不准的问题?
聪明的科学家们开始思考,既然观测天文现象无法解决这个问题,那在微观层面能否找到比较好的解决方案吗?
这时,他们开始把目光投向了「微观世界」。
一秒到底有多长?
让我们梳理一下我们的需求。
一直以来,我们对于「秒」的定义需求,从本质上讲,就是想要一个「完全稳定」的周期,也就是说,期望每一秒都是固定「等长」的。
而以天文观测、地球自转为基础的时间测量,做不到这一点。
那在微观世界层面,是否存在一种元素,它的运动周期是「高度稳定」,不受外界环境影响的呢?
科学家们沿着这个思路开始探索...
好,现在让我们把视角下放,来到原子世界。
一个原子虽然很小,但它内部却是一个很复杂的世界。
每个原子都有一个原子核,核外分层排布着高速运转的电子,当原子受电磁辐射时,它的轨道电子可以从一个位置「跳」到另一个位置,物理学上称此为「跃迁」。
人们发现,原子内的电子发生跃迁时,原子会吸收或放出一定能量的「电磁波」,这类电磁波就是一种「周期运动」,我们也可以把它看成原子内部的「振荡」。
基于这个原理,科学家们开始不断地试验、研究,尝试寻找一种运动「周期短、高度稳定」的原子。
终于,科学家们发现确实存在这样一种原子:铯原子,它内部的振荡周期比其它原子都要更短、更稳定,而且,这个过程基本不受环境因素的干扰。
经过层层试验,科学家们认为这是目前人类在地球上可测量到的,运动周期最短、周期最稳定的元素!
之后,科学家们就以之前定义的「秒」为基础,去测量一秒内这个铯原子内部电子周期运动的「次数」,测量出来的结果为 9192631770 次(91 亿+次)。
基于此,科学家们决定「抛弃」原来基于天文测量的秒,重新定义「秒」的时长,就是这个高度稳定的运动周期。
因此,在 1967 年,国际度量衡大会决定采用,以铯原子跃迁 9192631770 个周期,所持续的时间长度定义为 1 秒!
注:这个测量原理和测量过程比较复杂,这里把这些物理细节简化了。不用太过纠结这个数值是怎么测量出来的,你只需要理解,这个微观原子内部的振荡周期是非常稳定的,它比之前根据天文现象测量出来的秒,要精确多得多。
而基于这个铯原子振荡制造出来的时钟,我们就把它称之为「原子钟」。
有了原子钟,这就意味着,原子钟输出的每一秒,都是绝对「等长」的,非常稳定,这样一来,就实现了「精准计时」!
这个精确程度可以达到多高呢?
2000 万年不差 1 秒!可见其精准程度之高。
科研技术还在发展,精密设备和测量能力也越来越高,最新的原子钟甚至可以达到 1 亿年不差 1 秒!
有了原子钟,人们基于原子钟又确立了一套新的时间标准,叫做「国际原子时」(International Atomic Time,简称 TAI)。
科学家们规定,从 1958-01-01 00:00:00 起,用原子时开始计时,它每走的一秒,都是非常精确的一秒(固定等长),实打实的一秒,完全稳定的一秒。
这个方案非常棒,至此终于解决了秒不固定长的问题。
GMT
GMT(Greenwich Mean Time), 格林威治平时(也称格林威治时间)。它规定太阳每天经过位于英国伦敦郊区的皇家格林威治天文台的时间为中午12点。是旧的世界标准时间。
它使用穿过英国伦敦格林威治天文台子午仪中心的一条经线作为零度参考线,这条线,简称格林威治子午线,此也确定了全球24小时自然时区的划分,所有时区都以和 GMT 之间的偏移量做为参考。
1972年之前,格林威治时间(GMT)一直是世界时间的标准。1972年之后,GMT 不再是一个时间标准了。
UTC
UTC(Coodinated Universal Time),协调世界时,又称世界统一时间、世界标准时间、国际协调时间。由于英文(CUT)和法文(TUC)的缩写不同,作为妥协,简称UTC。
UTC 是现在全球通用的时间标准,全球各地都同意将各自的时间进行同步协调。UTC 时间是经过平均太阳时(以格林威治时间GMT为准)、地轴运动修正后的新时标以及以秒为单位的国际原子时所综合精算而成。
在军事中,协调世界时会使用“Z”来表示。又由于Z在无线电联络中使用“Zulu”作代称,协调世界时也会被称为"Zulu time"。
UTC 由两部分构成:
- 原子时间(TAI, International Atomic Time):结合了全球400个所有的原子钟而得到的时间,它决定了我们每个人的钟表中,时间流动的速度。原子钟时间非常精确,2000万年误差不超过1秒。
- 世界时间(UT, Universal Time):也称天文时间,或太阳时,他的依据是地球的自转,我们用它来确定多少原子时,对应于一个地球日的时间长度。地球自传越来越慢,导致一天的时间越来越长。因此原子时间与世界时间需要协调。
为了兼顾基于天文测量的世界时,人类会「持续观测」世界时与这个新时钟的差距。如果发现两者相差过大时,我们就「人为」地调整一下这个时钟(加一秒或减一秒),让两者相差不超过 0.9 秒。例如,这个时钟本身比世界时走得快,经过一段时间后,如果发现两者相差越来越大,那就给这个时钟「加一秒」,让这个时钟在 23:59:59 的下一秒变为 23:59:60 秒,让它与世界时差距控制在 0.9 秒以内,这个操作过程,相当于让快的时钟稍微「等」一下走得慢的世界时。而加的这一秒,科学家把它定义为「闰秒」。
UTC就是原子时间与世界时间协调的结果。
GMT vs UTC
GMT是前世界标准时,UTC是现世界标准时。
UTC 比 GMT更精准,以原子时计时,适应现代社会的精确计时。
但在不需要精确到秒的情况下,二者可以视为等同。
每年格林尼治天文台会发调时信息,基于UTC。
夏令时
它是为节约能源而人为规定地方时间的制度。一般在天亮早的夏季人为将时间提前一小时,可以使人早起早睡,减少照明量,以充分利用光照资源,从而节约照明用电。中国曾经推行过一段时间,后放弃。很多国家也在逐渐放弃。
历法
也叫日历系统。是为了配合人们日常生活的需要,根据天象而制订的计算时间方法。根据月球环绕地球运行所订的历法称为阴历;根据太阳在不同季节的位置变化所订的历法称为阳历。我国普遍存在的对历法的两种称谓:公历,农历。
从分类标准来说,阴历是和阳历对应的,再加上阴阳历,只有这三种历法。世界上任何一种历法也跳不出这三种历法的分类。世界上通行的“公历”(新历)实际上是一种阳历,而中国传统历法的“农历”(旧历)属于阴阳历。
阳历
太阳历又称为阳历,是以地球绕太阳公转的运动周期为基础而制定的历法。但不管是太阳还是月亮,公转和自传都是不均匀的。地球的公转周期有时候一年的时间长一点,有时候一年短一点。月亮也是一样,绕着地球转的公转周期也是时长时短。
特点:
1、太阳历的历年近似等于回归年(以地球围绕太阳的公转周期为1年),一年12个月
2、月:分为大月和小月。大月:31天,小月:30天。大小月的月份和天数都是固定的。
这个“月”,实际上与朔望月无关。阳历的月份、日期都与太阳在黄道上的位置较好地符合,根据阳历的日期,在一年中可以明显看出四季寒暖变化的情况;但在每个月份中,看不出月亮的朔、望、两弦。
大小月顺口溜:一三五七八十腊 三十一天永不差;四六九和十一 三十整天记心里。
3、闰年:如今世界通行的公历就是一种阳历,平年365天,闰年366天,每四年一闰,每满百年少闰一次,到第四百年再闰,即每四百年中有97个闰年。公历的历年平均长度与回归年只有26秒之差,要累积3300年才差一日。
农历
农历是在阴历(夏历)基础上融合了阳历成分的一种阴阳合历。取月相的变化周期即朔望月为月的长度(朔月:即看不见月亮,望月:即满月,月亮正圆),加入干支历“二十四节气”成分,参考太阳回归年为年的长度,通过设置闰月以使平均历年与回归年相适应。所以,农历既有阴历又有阳历的成分。与阳历年固定在365天或366天不同的是,阴历年相比阳历年在天数上有时会相差大约10~20天。为了协调回归年与朔望月之间的天数,于是产生了阴阳历,即农历。方法是在历法中通过合理的置闰法,如“19年7闰”法(即在19个农历年中加上7个闰年。农历的平年为12个月平均有354.3672天,而闰年比平年多一个闰月,因此平均有383.8978天。)使得一年的平均天数与回归年的天数相符。因此这种历法既与月相相符,也与地球绕太阳周期运动近似符合。
1. 其年份分为平年和闰年,平年为十二个月,闰年为十三个月(设置闰年是为了与阳历和太阳回归年相适应)
2. 月份分为大月和小月,大月三十天,小月二十九天,其平均历月等于一个朔望月,大小月是哪几个月是不固定的,需要计算(设置大小月是为了与朔望月相适应)
二十四节气
现行的“二十四节气”是依据太阳在回归黄道上的位置制定,即把太阳周年运动轨迹划分为24等份,每15°为1等份,每1等份为一个节气。一岁四时,春夏秋冬各三个月,每月两个节气,每个节气均有其独特的含义。
每个节气与对应于特定的公历时间(每年相差三天以内)
二十四节气也是农历中与公历相适应的体现。
计算机中的时间
从系统中获取时间
系统的时钟类型:
CLOCK_REALTIME:指从1970年1月1日0时0点0分到现在的时间,该值会受到NTP服务的影响,系统管理员可以调整,可能会回退,也可能向前跳跃,这个时间是系统的墙上时间。
CLOCK_MONOTONIC :记录开机时到现在的时间(除去系统休眠的时间,即suspend时间),系统管理员不可调整,会受到NTP服务影响,但不会回退。
CLOCK_BOOTIME:记录从开启到现在的所有时间(包含suspend时间),系统管理员不可调整,会受到NTP服务影响,但不会回退。
CLOCK_MONOTONIC_RAW :与CLOCK_MONOTONIC意义相同,但不受NTP服务的影响。
CLOCK_REALTIME_COARSE:特征与CLOCK_REALTIME相同,提供更快的访问速度,但精确度有所降低。
CLOCK_MONOTONIC_COARSE :特征与CLOCK_MONOTONIC 相同,提供更快的访问速度,但精确度有所降低。
系统如何维护这些时间
内核维护的这些时间,每个jiffies会更新一次。jiffies记录自系统启动以来产生的时钟中断数。每个timer interrupt 会导致jiffies加1。使用 grep 'CONFIG_HZ=' /boot/config-$(uname -r) 命令可以获取系统的时间中断频率。1000HZ表示1秒钟中断1000次,那么jiffies的精度即为1ms.时钟中断由硬件产生,比如apic timer pit timer等。
开机时系统会从BIOS中读取硬件时间,写入到上述clock中
然后每个timer interrupt 会更新一次这些时间,时间更新逻辑如下:
New Clock = ((current cycle - last cycle ) * multiplier) + Old Clock
用户读取时间的实现
用户每次调用获取时间的函数时 其逻辑与系统维护时间的逻辑类似,如下:
Clock = Old Clock + delta + Adjustment
Old Clock 就是靠timer interrupt更新的。
delta就是 这次从clock source 中读取的counter与 上次读取的差值,即(current cycle - last cycle ) * multiplier
Adjustment : adjtime函数调整的时间
从函数实现逻辑中可以看到,每次用户获取时间其实是获取的最新时间,并不是timer interrupt维护的时间。
Java中如何获取时间
public static LocalDateTime now() { return now(Clock.systemDefaultZone()); } public static LocalDateTime now(ZoneId zone) { return now(Clock.system(zone)); } public static LocalDateTime now(Clock clock) { Objects.requireNonNull(clock, "clock"); final Instant now = clock.instant(); // called once ZoneOffset offset = clock.getZone().getRules().getOffset(now); return ofEpochSecond(now.getEpochSecond(), now.getNano(), offset); }
会调用到Clock.instant():
public long millis() { return System.currentTimeMillis(); } public Instant instant() { return Instant.ofEpochMilli(millis()); }
最终来到System.currentTimeMillis(),这是一个native方法,我们进入jdk源码(这里是jdk21)中看一下,打开openjdk/src/java.base/share/native/libjava/System.c:
static JNINativeMethod methods[] = { {"currentTimeMillis", "()J", (void *)&JVM_CurrentTimeMillis}, {"nanoTime", "()J", (void *)&JVM_NanoTime}, {"arraycopy", "(" OBJ "I" OBJ "II)V", (void *)&JVM_ArrayCopy}, }; #undef OBJ JNIEXPORT void JNICALL Java_java_lang_System_registerNatives(JNIEnv *env, jclass cls) { (*env)->RegisterNatives(env, cls, methods, sizeof(methods)/sizeof(methods[0])); }
可以看到 currentTimeMillis 注册的函数为 JVM_CurrentTimeMillis,我们再打开openjdk/src/hotspot/share/prims/jvm.cpp
JVM_LEAF(jlong, JVM_CurrentTimeMillis(JNIEnv *env, jclass ignored)) return os::javaTimeMillis(); JVM_END
看到这里调用了os::javaTimeMillis();,我们打开打开openjdk/src/hotspot/os/posix/os_posix.cpp:
jlong os::javaTimeMillis() { struct timespec ts; int status = clock_gettime(CLOCK_REALTIME, &ts); assert(status == 0, "clock_gettime error: %s", os::strerror(errno)); return jlong(ts.tv_sec) * MILLIUNITS + jlong(ts.tv_nsec) / NANOUNITS_PER_MILLIUNIT; }
可以看到是调用的cock_gettime,并且使用的是CLOCK_REALTIME这个clockid。这就是1970年的epoch,这里给转化为毫秒了。
Java中所有的日期、日历 、时间都是基于这个毫秒值,配合locale,timezone之类的东西加工而成。难道java中的时间都是以毫秒为单位吗?如果我想用微妙 纳秒级别的时间 不行吗?java还有一个方法是:nanoTIme();这里可以分析一下,最终看到的系统调用是:
jlong os::javaTimeNanos() { struct timespec tp; int status = clock_gettime(CLOCK_MONOTONIC, &tp); assert(status == 0, "clock_gettime error: %s", os::strerror(errno)); jlong result = jlong(tp.tv_sec) * NANOSECS_PER_SEC + jlong(tp.tv_nsec); return result; }
使用的是CLOCK_MONOTONIC这个clockid。这是记录的自系统启动以来经过的时间,因此这个时间不能转化为一个日期,只有在计算时间差时才有意义。