zoco

计算机中的时间是什么

2020-06-20


ぎょうき前几天问我,计算机时间咋实现的,为什么写程序的时候能获取到准确的时间,而且全世界的时间都一致。

把我问住了,最先想到的是联网获取的,但是没网怎么办?然后想到是不是系统计算的,那么关机怎么办?难道计算机里有电池?可是电池没电怎么办?

总不可能是计算机感受到地球的自传速度自己算的吧……所以简单整理一下,先看下 Linux 时间处理的一般过程:

NlePhT.png

应用层

(以下都基于X86体系)

从应用层面,这里我直接用C语言来获取时间。

#include <time.h>
int main () {
    time_t time_raw_format;
    time(&time_raw_format); // get current time
    printf("time is [%d]\n", time_raw_format);
    //use ctime format time
    printf("The current local time: %s", ctime(&time_raw_format));
    return 0;
}
time is [1592624759]
The current local time: Sat Jun 20 11:45:59 2020

很好,时间和我手机上的一模一样。

这里出现的1592624759叫做时间戳,是从 1970年1月1日0点到当前的秒数。一会儿解释下为什么从这个时间开始。

目前我们得到的时间是一个数字,无论精度如何,它代表的仅是一个差值。比如精度为秒的 time() 函数,返回一个 time_t 类型的整数。假设当前时间为2020年6月20日上午11点45分59秒,那么 time_t 的值为:1592624759。即距离1970年1月1日0点,我们已经过去了1592624759秒。(这里的1970年1月1日零点是格林威治时间,而不是北京时间。)我们下面讨论的时间如果不特别说明都是格林威治时间,也叫GMT时间,或者UTC时间。

那么这个时间从哪来的呢?计算机如何实现这一切的呢?在那些应用层 API 和底层系统硬件之间,操作系统和库函数究竟做了些什么?

GlibC层

Glibc是 GNU 发布的libc库,即c 运行库 。glibc是 linux系统 中最底层的 api ,几乎其它任何运行库都会依赖于glibc。glibc除了封装 linux 操作系统所提供的 系统服务 外,它本身也提供了许多其它一些必要功能服务的实现。

看一下time的GlibC实现

time_t time (time_t *t) {
    INTERNAL_SYSCALL_DECL (err);
    time_t res = INTERNAL_SYSCALL (time, err, 1, NULL);
    return res;
}

可以看到,GlibC的time()函数只是调用了time系统调用,来返回时间值。

内核层

时间系统的工作需要软硬件以及操作系统的互相协作,内核自身的正常运行也依赖于时钟系统。Linux 是一个典型的分时系统,CPU 时间被分成多个时间片,这是多任务实现的基础。Linux 内核依赖 tick,即时钟中断来进行分时。

为了满足应用和内核自己的需求,内核时间系统必须提供以下三个基本功能:

  1. 提供系统 tick 中断(驱动调度器,实现分时)
  2. 维护系统时间
  3. 维护软件定时器

早期的 Linux 时间系统

在 Linux 2.6.16 之前,内核只支持低精度时钟。内核围绕着 tick 时钟来实现所有的时间相关功能。Tick 是一个定期触发的中断,一般由 PIT (Programmable Interrupt Timer) 提供,大概 10ms 触发一次 (100HZ),精度很低。在这个简单体系结构下,内核如何实现三个基本功能?

第一大功能:提供 tick 中断。

以 x86 为例,系统初始化时选择一个能够提供定时中断的设备 (比如 Programmable Interrupt Timer, PIT),配置相应的中断处理 IRQ 和相应的处理例程。当硬件设备初始化完成后,便开始定期地产生中断,这便是 tick 了。非常简单明了,需要强调的是 tick 中断是由硬件直接产生的真实中断。

第二大功能:维护系统时间。

RTC (Real Time Clock) 有独立的电池供电,始终保存着系统时间。Linux 系统初始化时,读取 RTC,得到当前时间值。 读取 RTC 是一个体系结构相关的操作,对于 x86 机器,定义在 arch\x86\kernel\time.c 中。可以看到最终的实现函数为 mach_get_cmos_time,它直接读取 RTC 的 CMOS 芯片获得当前时间。如前所述,RTC 芯片一般都可以直接通过 IO 操作来读取年月日等时间信息。得到存储在 RTC 中的时间值之后,内核调用 mktime () 将 RTC 值转换为一个距离 Epoch(既 1970 年元旦)的时间值。此后直到下次重新启动,Linux 不会再读取硬件 RTC 了。

$ cat /proc/driver/rtc

rtc_time	: 12:59:52
rtc_date	: 2020-06-20
alrm_time	: 12:53:51
alrm_date	: 2019-09-01
alarm_IRQ	: no
alrm_pending	: no
update IRQ enabled	: no
periodic IRQ enabled	: no
periodic IRQ frequency	: 1024
max user IRQ frequency	: 64
24hr		: yes
periodic_IRQ	: no
update_IRQ	: no
HPET_emulated	: yes
BCD		: yes
DST_enable	: no
periodic_freq	: 1024
batt_status	: okay

虽然内核也可以在每次需要的得到当前时间的时候读取 RTC,但这是一个 IO 调用,性能低下。实际上,在得到了当前时间后,Linux 系统会立即启动 tick 中断。此后,在每次的时钟中断处理函数内,Linux 更新当前的时间值,并保存在全局变量 xtime 内。比如时钟中断的周期为 10ms,那么每次中断产生,就将 xtime 加上 10ms。

当应用程序通过 time 系统调用需要获取当前时间时,内核只需要从内存中读取 xtime 并返回即可。就这样,Linux 内核提供了第二大功能,维护系统时间。

第三大功能:软件定时器

能够提供可编程定时中断的硬件电路都有一个缺点,即同时可以配置的定时器个数有限。但现代 Linux 系统中需要大量的定时器:内核自己需要使用 timer,比如内核驱动的某些操作需要等待一段给定的时间,或者 TCP 网络协议栈代码会需要大量 timer;内核还需要提供系统调用来支持 setitimer 和 POSIX timer。这意味着软件定时器的需求数量将大于硬件能够提供的 timer 个数,内核必须依靠软件 timer。

简单的软件 timer 可以通过 timer 链表来实现。需要添加新 timer 时,只需在一个全局的链表中添加一个新的 Timer 元素。每次 tick 中断来临时,遍历该链表,并触发所有到期的 Timer 即可。但这种做法缺乏可扩展性:当 Timer 的数量增加时,遍历链表的花销将线形增加。如果将链表排序,则 tick 中断中无须遍历列表,只需要查看链表头即可,时间为 O(1),但这又导致创建新的 Timer 的时间复杂度变为 O(n),因为将一个元素插入排序列表的时间复杂度为 O(N)。这些都是可行但扩展性有限的算法。在 Linux 尚未大量被应用到服务器时,系统中的 timer 个数不多,因此这种基于链表的实现还是可行的。

但随着 Linux 开始作为一种服务器操作系统,用来支持网络应用时,需要的 timer 个数剧增。一些 TCP 实现对于每个连接都需要 2 个 Timer,此外多媒体应用也需要 Timer,总之 timer 的个数达到了需要考虑扩展性的程度。

Timer 的三个操作:添加 (add_timer)、删除 (del_timer) 以及到期处理(tick 中断)都对 timer 的精度和延迟有巨大影响,timer 的精度和延迟又对应用有巨大影响。例如,add_timer 的延迟太大,那么高速 TCP 网络协议就无法实现。为此,从 Linux2.4 开始,内核通过一种被称为时间轮的算法来保证 add_timer()、del_timer() 以及 expire 处理操作的时间复杂度都为 O(1)。

关于Timer的详细算法这里就不赘述。

硬件层

只X86就有非常多时间相关的硬件,比如RTC (Real Time Clock)、TSC (Time Stamp Counter)、PIT (Programmable Interval Timer)、HPET (High Precision Event Timer)、APIC Timer (Advanced Programmable Interrupt Controller Timer)等等。

这里就拿RTC 来简单说一下:

RTC (Real Time Clock,实时时钟)

人们需要知道时间的时候,可以看看钟表。计算机系统中钟表类似的硬件就是外部时钟。它依靠主板上的电池,在系统断电的情况下,也能维持时钟的准确性。计算机需要知道时间的时候,就需要读取该时钟。 在 x86 体系中,这个时钟一般被称为 Real Time Clock。RTC 是主板上的一个 CMOS 芯片,比如 Motorola 146818,该芯片独立于 CPU 和其他芯片,可以通过 0x70 和 0x71 端口操作 RTC。RTC 可以周期性地在 IRQ 8 上触发中断,但精度很低,从 2HZ 到 8192HZ。

简单的说,RTC 能提供精确到秒的实时时间值。

找到了张RTC的电路图:

NlemH1.png

其中Vbatt时由安装的一颗3V左右(一般为2.8V~3.3V)的纽扣电池(一般都为CR2302,提供最大10mA的电流)提供,这颗电池的作用时在机器移除AC电源后, RTC电路仍然可以正常工作。当这颗电池的电压不足的时候,就会出现RTC时间不准确。

结论

简单画了张图:

NleYDA.png

开机引导时:使用硬件时钟设置系统时钟。Linux系统开机时间会更新到/etc/adjtime

系统运行时:系统正常运行后,由系统内核独自运行系统时钟。

时钟同步服务:需要联网,大部分操作系统都带有联网后能够和网络上的NTP服务器同步系统时钟,修正系统时间 的准确性

系统关机时:会使用系统时钟设置硬件时钟,更新硬件时钟。

注:服务器系统开机后会连续运行数月,甚至数年。硬件时间与软件时间会出现差异,一般以系统时间为准。

NTP协议:

NletHI.png

为什么时间从1970年开始

原因很简单,Unix就是在那个时代产生的,1969 年发布的雏形,最早是基于硬件 60Hz 的时间计数。

1971年底出版的《Unix Programmer’s Manual》里定义的 Unix Time 是以 1971年1月1日00:00:00 作为起始时间,每秒增长 60。考虑到 32 位整数的范围,如果每秒 60 个数字,则两年半就会循环一轮,于是改成以秒为计数单位。循环周期有136年之长,就不在乎起始时间是 1970 还是 1971 年,遂改成人工记忆、计算比较方便的1970年。

于是Unix 的世界开启了 “纪元”,Unix 时间戳也就成为了一个专有名称。

最后

最后还是不得不重复想一下,时间到底是什么?这是一个非常哲学和科学的问题了,历史上无数的人企图证明他,解释他,但结果让试图思考它的人感到迷茫。

好在Linux中的时间终究是可以理解的。我更相信所谓的时间不过是我们对这个世界运行原理的一种编码,而真正的时间就在那里。

“流动”的并非时间,而是各种穿过我们称之为空间的静止场和在此静止场中移动的物体。时间只是我们用来测量运动的方式。