作者简介:
程磊,某手机大厂系统开发工程师,阅码场荣誉总编辑,最大的爱好是钻研Linux内核基本原理。
一、时间概念解析
1.1 时间使用的需求
1.2 时间体系的要素
1.3 时间的表示维度
1.4 时钟与走时
1.5 时间需求之间的关系
二、时间子系统的硬件基础
2.1 时钟硬件类型
2.2 x86平台上的时钟
2.3 ARM平台上的时钟
三. 时间子系统的软件架构
3.1 系统时钟的设计
3.2 系统时钟的实现
3.3 动态tick与定时器
3.4 用户空间API的实现
四. 总结回顾
一、时间概念解析我们住在空间里,活在时间中。时间对我们来说是既熟悉又陌生。熟悉是因为我们每天都在时间的驱动下忙碌着,陌生是因为我们从来没有停下来认真思考过时间是什么。今天我们先从对时间的使用需求开始说起。
1.1 时间使用的需求我们对使用时间有三种需求:知时、定时和计时。知时就是我们需要知道现在的时间是多少,表达方式是时分秒、年月日。定时是我们需要在某个时间点被告知,时间点可以是相对的或者绝对的,告知可以是一次性的或者是周期性的,比如每天早上7:30叫我起床,是绝对时间点周期性告知,每隔10分钟向我汇报一次情况,是相对时间点周期性告知。计时是我们需要知道某件事从开始到结束一共花了多少时间,比如大学运动会1000米赛跑,裁判在运动员起跑时按一下计时器,结束时再按一下计时器,得出某运动员跑一千米用了3分50秒。
1.2 时间体系的要素为了达到知时的目的,我们首先需要建立时间体系的概念。时间体系由三个要素构成,1时间原点、2时间基本单位、3时间是否会暂停。我们把每天用的这个时间叫做自然时间,自然时间在计算机里面也叫做真实时间(Real Time),注意Real Time在这里是真实时间的意思,而不是实时的意思。自然时间有时候也会被叫做墙钟时间(wall clock time),或者简略为墙上时间(wall time),小时候家里墙上用挂钟来看时间的同学立马就能明白了。对自然时间建立的时间体系并不是唯一的,可以有不同的时间原点和时间基本单位。我们现在使用的公元纪年,它的时间原点是耶稣出生的那一年的一月一号零时零分零秒。其实我们也可以使用黄帝纪年,那现在就是5000多年了,也可以把建国的时间当做时间原点,那现在就是70几年。公元纪年的时间基本单位是秒,好在全球的秒都是一样的,没有出现什么中秒、美秒、欧秒的区分,不然换算来换算去就会很麻烦。自然时间不会暂停,计算机里面的有些时间体系可能会暂停,这个我们后面再讲。我们再来总结一下,现在全世界使用的自然时间体系是公元纪年,其时间原点是耶稣诞生当年的一月一号零时零分零秒,其时间基本单位是秒,时间流逝不会暂停。这就特别好,大家都是在同一个时间体系下生活,这样讨论时间就很方便,不用来回转换了。如果不同国家使用的时间体系都不相同,时间体系的原点不同,时间基本单位也不相同,那相互之间来回转换时间就会非常麻烦。
1.3 时间的表示维度接下来我们说一下时间的表示维度,注意是时间的表示维度,不是时间的维度,时间本身的维度是一维的。如果我告诉你说现在的时间是六百三十七亿六千五百七十九万多秒,你是不是会一脸懵逼,反应不过来。虽然时间的基本单位是秒,但是我们如果直接用秒来表示时间,那将非常难以理解和记忆。为此我们建立了多层级的时间表示维度,60秒是一分钟,60分钟是一个小时,24小时是一天,365天是一年。然后我们说今天是某年某月某日,具体时间是几时几分几秒,就非常方便了,很便于我们人类使用理解。对于人类来说时间精确到秒就足够使用了,但是对于科学研究来说还需要更高的精度,于是我们把1秒的1/1000叫做毫秒,1毫秒的1/1000叫做微秒,1微秒的1/1000叫做纳秒。这样时间的表示维度就很丰富了,便于我们在不同的情况下使用。那么计算机中的时间表示维度是多少呢?人类善于理解多维度的时间表示,但是计算机却善于处理单维度的时间表示。但是计算机用单维度的时间表示却有个问题,如果用秒作为基本单位,那么精度显然达不到,如果用纳秒作为基本单位的话,数值又太大。所以计算机中的时间采用的是两层表示维度,超过1秒的时间用秒表示,不够一秒的时间用纳秒表示,每10亿纳秒向前进位一秒。这样计算机中时间处理就非常方便了。
1.4 时钟与走时想要实现知时的目的我们就需要有工具,这个工具就叫做时钟(clock),有了时钟我们就能够快速准确地知道自然时间。下面我们来给时钟下一个定义。时钟,包括硬件的、软件的、机械的、电子的,都是用来追踪和记录自然时间流逝的工具。下面我们再来说一个动词,走时,大家一听这个词可能会不知道是啥意思。我再来说一句话,这个表走时非常精准,大家立马就明白了是啥意思。我们再给走时下个定义,走时,是时钟追踪和记录时间流逝的动作。为什么在这里要说个走时的概念呢,因为有了走时的概念,后面的很多东西都能很轻松地讲清楚。
1.5 时间需求之间的关系我们再来看一下知时、计时、定时三者之间的关系。先说知时和计时,其实两者之间是可以相互转化的。知时可以转化为计时,我们在事情开始的时候记录一下时间,在事情结束的时候记录一下时间,两者之间的时间差值就是计时。计时也可以转化为知时,把计时的起点设置为某一个时间体系的时间原点,那么计时的结果就是知时的结果。计时是时间原点不特定的知时,知时是时间原点特定的计时。知时的结果是一个时间点,它是当前时间点到时间原点的一个时间段。计时的结果是时间段,它是相对于计时原点的时间点。明白了知时和计时之间的关系对于我们理解后面计算机的具体做法有很大的帮助。
下面我们再来看一下定时和知时、计时之间的关系。由于知时、计时可以相互转换,所以它们可以放在一起讨论同定时的关系。定时是需要知时、计时的支持的,如果没有知时、计时,那么就没法定时。绝对定时用知时作为基础时间比较方便,相对定时用计时作为基础时间比较方便。当然反过来也是可以的,因为知时计时是可以相互转化的。还有一点就是定时可以用来作为时钟实现走时的方法,这个在计算机时间管理的实现中就有所体现。
二、时间子系统的硬件基础在生活中我们有各种各样的时钟来满足我们对时间的需求。比如以前家里常用的座钟、挂钟,个人也会戴个机械手表或者电子手表,这些时钟既能知时也能定时(有闹钟功能),知时本身也能转化为计时。所以一个时钟就能满足我们对时间的所有需求。在有些场合比如大学运动会时,会有专门的计时器,在比赛开始之前把计时器清零,比赛开始的时候按下开始,计时器开始走时,然后每当有一个人达到终点的时候按一下计时,计时器就会把当时的时间记下来,当所有人都跑完的时候按下结束,计时器停止走时。然后回看计时器就可以看到每个人跑完一千米的用时了。这种专用的计时器用来计时就非常方便。
现在家里有座钟、挂钟的人已经非常少了,戴手表的人也非常少了,大家基本都是用手机来看时间。手机不仅桌面上有时间显示,里面还有个时钟App,它和以前的时钟功能差不多,而且更强大。时钟App里面不仅能看时间(知时),还能定闹钟(绝对时间定时),里面还有一个计时器功能,实际上是倒计时,倒计时的本质是相对时间定时。里面还有一个秒表的功能,和我们前面说的运动会计时器的功能是一样的,所以秒表是个专业的计时器。所以手机上的时钟App完美得实现了我们对时间的所有需求。
手机实际上就是个计算机系统,而且安卓手机用的还是Linux内核。时钟App所实现的功能需要Linux内核的支持,内核时间子系统的实现需要有硬件的支持。
2.1 时钟硬件类型计算机里面一共有三类时钟硬件,分别是真时钟RTC(Real Time Clock)、定时器Timer、计时器Counter。RTC相当于是手表、座钟,定时器相当于是闹钟,计时器相当于是运动会中的计时器。注意是三类时钟硬件,而不是三个,某一类时钟可能有多个不同的硬件,某一个时钟硬件也可能实现多种不同的时钟类型。
计算机中还有其它的时钟类型,比如晶振时钟,是驱动CPU运行的周期信号,用来触发和同步CPU内部的操作,我们常说某CPU是多少GHz,就是说这个时钟晶振每秒向CPU发送多少信号(大概如此,实际上比较复杂,还有倍频什么的,这里就不讨论了)。晶振时钟一般在CPU内部,有些嵌入式CPU的晶振在外部。时钟晶振在软件层不可见。还有一些设备也有自己的时钟,还有相应的驱动可以控制它。由于这些时钟都和时间子系统没有关系,所以本文中就不讨论它们了。
不同平台的时钟硬件各有不同,下面我们就来分别说说。
2.2 x86平台上的时钟真时钟RTC,在x86上的硬件实现也叫做RTC,和CMOS(计算机中有很多叫做CMOS的东西,但是是不同的概念,此处的CMOS是指BIOS设置保存数据的地方)是放在一起的。由于在关机后都需要供电,所以两者放在了一起,由一个纽扣电池供电。所以有时候也会被人叫做CMOS时钟。
定时器Timer,在UP时代是PIT(Programmable Interval Timer),它以固定时间间隔向CPU发送中断信号。PIT可以在系统启动时设置每秒产生多少个定时器中断,一般设置是100,250,300,1000,这个值叫做HZ。到了SMP时代,PIT就不适用了,此时有多种不同的定时器。有一个叫做Local APIC Timer的定时器,它是和中断系统相关的。中断系统有一个全局的IO APIC,有NR_CPU个Local APIC,一个Local APIC对应一个CPU。所以在每个Local APIC都安装一个定时器,专门给自己对应的CPU发送定时器中断,就很方便。还有一个定时器叫做HPET(High Precision Event Timer),它是Intel和微软共同研发的。它不仅是个定时器,而且还有计时器的功能。HPET不和特定的CPU绑定,所以它可以给任意一个CPU发中断,这点和Local APIC Timer不同。
计时器Counter,RTC或者定时器虽然也可以实现计时器的目的,但是由于精度太差,所以系统都有专门的计时器硬件。计时器一般都是一个整数寄存器,以特定的时间间隔增长,比如说1纳秒增加1,这样两次读它的值就可以算出其中的时间差,而且精度很高。x86上最常用的计时器叫做TSC(Time Stamp Counter),是个64位整数寄存器。还有一个计时器叫做ACPI PMT(ACPI Power Management Timer),但是它是一个设备寄存器,需要通过IO端口来读取。而TSC是CPU寄存器,可以直接读取,读取速度就非常快。
2.3 ARM平台上的时钟暂略
三. 时间子系统的软件架构当我们知道了我们明白什么、我们有什么、我们想要什么的时候,我们就会知道我们应该怎么做。
从第一章我们明白了时间的基本概念,从第二章我们知道了我们有RTC、计时器、定时器三类底层硬件,从第三章和第四章我们知道了我们需要什么,那么我们就会很容易的分析出我们应该怎么做。
3.1 系统时钟的设计在用户空间和内核空间都有知时的需求,而底层又有RTC硬件,这样看来知时的需求很好实现啊,直接访问RTC硬件就可以了。这么做行吗?我们来分析一下。首先RTC是个外设,访问RTC要走IO端口,而这相对来说是个很慢的操作。其次RTC的精度不够,有的RTC精度是秒,有的是毫秒,这显然是不够用的。最后系统要实现很多时间体系,直接访问RTC灵活性也不够。所以直接访问RTC是一个很差的设计,那么该怎么实现知时的需求呢?
我们先来回忆一下时钟和走时的定义。
我们用机械手表来解释一个这个概念。手表里面有发条,发条的变化是在追踪时间的流逝,然后发条通过齿轮把时间的变化记录在表盘的时针、分针、秒针上,这样我们就可以看到现在的时间是多少了。
我们再来回忆一下知时和计时之间的关系。知时是原点特定的计时,计时是原点不特定的知时,知时和计时可以相互转化。知时相减就是计时,给计时一个特定的原点就是知时。计算机上既有RTC也有计时器,RTC虽然又慢精度又低,但是计时器又快精度又高啊。计时器的精度可以达到1纳秒或者几纳秒,而且计时器大部分都是通过寄存器访问的,速度非常快的。给计时器的起点一个确定的时间点,它就是RTC了啊。于是乎方案就出来了:Linux提出了系统时钟的概念,它是一个软件时钟,相应的把RTC叫做硬件时钟。系统时钟是用一个变量xtime记录现在的时间点,xtime的初始值用RTC来初始化,这样就只用访问RTC一次就可以了,然后xtime的值随着计时器的增长而增长。xtime的值的更新有两种情况,一种是调度器tick的时候从计时器更新一下,一种是读xtime的时候从计时器更新一下。对于这个时钟,计时器就相当于是发条,调度器tick就相当于是齿轮,xtime就相当于是时针、分针、秒针,一个软件时钟就这么设计好了。
Linux中用来实现系统时钟的软件体系叫做The Linux Timekeeping Architecture。如果我们把Timekeeping翻译成“时间维护”,感觉意思好像不到位。好在我们前面讲了“走时”的概念,把Timekeeping翻译成“走时”的话,一下子就觉得意思到了。后面我们就用“Linux走时框架”这个词了。在Linux走时框架中有三个基本概念:1.走时器(struct timekeeper),用来记录一些基本数据,包括系统时钟的当前时间值和其它全局时间体系的一些数据;2.时钟源(struct clocksouce),是对计时器硬件的一种抽象;3.时钟事件设备(struct clock_event_device),是对定时器硬件的一种抽象。这三个对象相互配合共同构成了系统时钟。
系统可能会有很多计时器硬件和定时器硬件。在系统启动时每个硬件都会初始化并注册自己。注册完之后系统会选择一个最佳的时钟源作为走时器的时钟源,选择一个最佳的时钟事件设备作为更新系统时钟的设备。系统启动时会去读取RTC的值来初始化系统时钟的值,然后时钟事件设备不断产生周期性的定时器事件,在定时器事件处理函数中会读取时钟源的值,再减去上一次读到的值,得到时间差,这个时间差就是系统时钟应该前进的时间值,把这个值更新到走时器中,并相应更新其它时间体系的值。系统时钟就是按照这种方式不断地在走时。系统时钟除了在启动时和休眠唤醒时会去读取RTC的值,其它时间都不会和RTC交换,两者各自独立地走时,互不影响。
用户空间API读取和设置的时间是系统时钟,和硬件时钟RTC没有关系。如果要读写RTC的话,需要用ioctl RTC_SET_TIME对/dev/rtc进行操作。stime、settimeofday设置的系统时钟,不会更改到RTC上,系统重启后更改就消失了。通过/dev/rtc修改的硬件时间也不会更改到系统时间上,只有系统重启后才会反映到系统时钟上。对此有一个系统命令hwclock,它不仅可以修改RTC,也可以在两者之间进行同步。hwclock --hctosys 把硬件时钟同步到系统时钟,hwclock --systohc把系统时钟同步到硬件时钟。事实上我们发现用settimeofday修改的系统时钟在系统重启后生效了,并没有丢失,这是为什么呢?是因为系统默认的关机脚本里面会执行hwclock --systohc,把系统时钟同步到硬件时钟,所以我们修改的系统时钟才不会丢失。
3.2 系统时钟的实现暂略
推荐阅读:
http://www.wowotech.net/timer_subsystem/time-subsyste-architecture.html
http://www.wowotech.net/timer_subsystem/timekeeping.html
http://www.wowotech.net/timer_subsystem/clocksource.html
http://www.wowotech.net/timer_subsystem/clock-event.html
3.3 动态 与定时器低精度定时器是内核在早期就有的定时器接口,它的实现是靠调度器tick来驱动的。高精度定时器是随着硬件和软件的发展而产生的。调度器tick的HZ(每秒tick多少次)是可以配置,它的配置选项有4个,100,、250、300、1000,也即是说每次tick的间隔是10ms、4ms、3.3ms、1ms。所以用调度器tick来驱动低精度定时器是很合适的,tick的精度能满足低精度定时器的精度。但是用调度器tick来驱动高精度定时器就不合适了,因为这样高精度定时器的精度最多是1ms,达不到纳秒的级别,这样就算不上是高精度定时器了。所以对于高精度定时器来说,情况就正好反了过来,高精度定时器直接用硬件实现,然后创建一个软件高精度定时器来模拟调度器tick。也就是说,对于只有低精度定时器的系统来说,是调度器tick驱动低精度定时器;对于有高精度定时器的系统来说,是高精度定时器驱动调度器tick,这个调度器tick再去驱动低精度定时器。
内核的低精度定时器接口和高精度定时器接口都是一次性的,不是周期性的。通过一次性的定时器可以实现周期性的定时器,方法是在每次定时器到期时再设置下一次的定时器,一直这样就形成了周期性的。这里说的是定时器接口的一次性和周期性,而不是定时器硬件。下面我们再来看看定时器硬件是一次性的还是周期性的。定时器硬件本身可以是一次性的也可以是周期性的,也可以两种模式都存在,由内核选择使用哪一种。对于低精度定时器来说,它的定时器硬件可以是一次性的也可以是周期性的,由于调度器tick是周期性的,所以它的底层硬件就是周期性的。低精度定时器的精度最多是1ms,也就是定时器中断做多一秒有1000次,这对于系统来说是可以承受的。但是对于高精度定时器来说,理论上它的定时器硬件也可以是周期性的。但是如果它的定时器硬件是周期性的,由于它的精度最多可以达到1纳秒,也就是说1纳秒要发生一次定时器中断,每秒发生10亿次。这对于系统来说是不可承受的,而且并不是每纳秒都有定时器事件要处理,所以大部分定时器中断是没有用的。如果我们把1纳秒1次中断改为1微妙,1微妙1次中断不就可以大大减少中断的数量嘛,但是这样定时器的精度就是1微妙,达不到1纳秒的要求了。所以对于高精度定时器,底层的定时器硬件就只能是一次性的了。每次定时器事件到来的时候再去查看一下下一个最近的定时器事件什么时候到期,然后再去设置一下定时器硬件。这样高精度定时器就可以一直运行下去了。但是我们的调度器tick也需要定时器中断,而且是周期性的,怎么办?好办,创建一个到期时间为1ms的高精度定时器,每次到期的时候再设置一下继续触发,这样就形成了一个1000HZ周期性的定时器事件,就可以驱动调度器tick。
下面我们讲一下定时器和调度器tick的初始化过程,以x86为例。系统启动时会先初始化timekeeping。然后hpet注册自己,hpet既有定时器也有计时器,hpet定时器会成为系统定时器,hpet计时器会成为timekeeper的时钟源。后面tsc计时器也会注册自己,并成为最终的时钟源。Local APIC Timer定时器也会注册自己,并成为最终的per CPU tick device。hpet最终只能做broadcast 定时器了。系统在每次run local timer的时候都会检测一下,如果不支持高精度定时器,就尝试切换到动态tick模式,如果支持高精度定时器就切换到高精度定时器模式,此模式下会尝试切换到动态tick模式。当高精度定时器和动态tick设置成功之后,Local APIC Timer会运行在一次性模式,调度器tick是由一个叫做sched_timer的高精度定时器驱动的。每次定时器到期时都会reprogram next event。
3.4 用户空间API的实现用户空间API的实现文件如下表所示,具体实现细节就不再展开解释了,大家搜索SYSCALL_DEFINE可以快速找到函数实现的地方。
四. 总结回顾通过前面的介绍,我们了解了时间的基本概念,知道了计算机中实现时间子系统的基础硬件,学会了时间的用户空间API和内核接口,明白了时间子系统的设计原理。下面我们画个图总结一下: