时间,在我们的现实生活中有着重要的作用,有了时间,我们才知道目前需要做些什么,该做些什么;有了时间,一切工作才能有条不紊的进行,可以说现实世界里我们一切的活动都是由时间来驱动的。
而时间在计算机的世界里也扮演着类似的角色,它是所有工作任务的基准,中断机制,进程线程调度等等都离不开时间,时间的重要性可见一般。
因此我们需要时间,需要管理时间,此篇文章主要就是来与大家介绍一下计算机里的时间与时间管理和储存时间的时钟。有关计算机时间的时钟有多种,在这儿介绍两种比较重要的两种:实时时钟,可编程间隔计数器。
实时时钟
实时时钟RTC,Real Time Clock,这个时钟可以永久的存放系统时间,也就是说它在系统关闭,没有电源的情况下也能继续工作。这里说的是没有计算机的电源(那大个儿电池),电子设备要工作那肯定还是需要电源的,这个电源是主板上的一个微型电池,计算机断电后实时时钟RTC就靠它来供电继续工作,持续对系统计时。这也就是为什么关机重新启动后时间还是正确的原因。
墙上时间(xtime)
RTC有专门的时间与日历寄存器组,来表示时间与日历的具体信息,系统启动时,会读取其中的信息来初始化墙上时间,The Wall Time,可以理解为就我们平常所使用的时间,只不过它是以总秒数来表示的。至于为什么叫这个名儿呢?我猜测是因为以前的时钟经常挂在墙上,所以叫墙上时间。
Linux里专门有个全局变量来存放这个时间,xtime,其结构定义如下:
xtime.tv_sec 是自1970年7月1日到现在经过的秒数,而tv_nsec是自上一秒到现在经过的纳秒数。
这又相关到另一个结构体timeval,与其很像,内核中实际的实现也是跟timespec息息相关的,有的版本就是根据timespec实现的,其结构如下:
tv_sec字段同上,tv_usec是自上一秒到现在经过的微秒数,精度为微秒。
这两个函数本质上没有区别,只是精度有所变化。
可编程间隔计数器
可编程间隔定时器,Programmable Interval Timer,它有着三个计数器,其作用如下:
-
计数器0:周期性的产生时钟中断信号
-
计数器1:控制着DRAM的定时刷新
-
计数器2:连接计算机的内部扬声器,传输不同频率的方波让扬声器发出不同音调的声音。
咱们这儿只说说计数器0,它的主要作用就是周期性的通过中断控制器发送中断信号(IRQ0),产生一次时钟中断,也叫一次时钟滴答,1 秒产生的时钟滴答次数叫做时钟滴答的频率。
频率越高,时钟中断次数越多,处理器花在中断处理上的时间也就越多,但也不是说频率越低就越好,频率低各任务执行时的频率也越低,不利于各任务之间的相互协作与处理。
以前的系统频率多设为100Hz,现多数提高到1000Hz,由此看出适当提高频率是利大于弊的,不然也不会提高,而且在现代的计算机上1000Hz的频率并不会造成难以接受的负担,归根结底是现代计算机的处理能力太强了。
时钟中断
PIT要正式工作,周期性地发送中断信号得先初始化,设置好相应属性才行。PIT的初始化是在系统启动时进行的,在Linux下是 init/main.c 中的 start_kernel 函数调用各种初始化函数 对硬件进行初始化,其中就包括了PIT。
初始化完成后,每隔一段时间PIT就会通过中断控制器8259A向CPU发送中断请求,CPU响应后中断控制器就继续发送中断向量号给CPU。然后CPU根据中断向量号去IDT中找时钟中断服务程序,期间,CPU会自动压栈ss, esp, eflags, cs, eip 5个寄存器(如果特权级发生变化),流程图再放一次加深印象:
上一篇关于中断的文章讲到此处,后面的中断服务程序略过了,本文就以时钟中断为例详细的说明中断服务程序的过程。
大家在上篇可能发现了在压栈时,怎么会只压栈5个寄存器来(有特权级变化,加上错误码后6个)保存上下文呢,那只是CPU检查特权级时自动压入的。发生了中断怎么可能只压栈这么点寄存器,那还怎么保存上下文呢?在Linux里,任务的上下文保存主要是在中断服务程序里进行的。大致流程如下,分为三步:
- 入口函数
上文中最终找到的中断服务程序其实并不是实际的处理函数,而是一个入口函数,通常是用汇编编写的,因为会涉及到寄存器压栈的操作,直接使用汇编语言来写比较方便。大致所做的事情如下:
压栈,真正的保护原任务的上下文,这是接着CPU自动压栈进行的。实际的linux没有将那么多的寄存器压进栈里,只压了部分必需要保存的寄存器,在这里本着简单,只需理清过程的原则,直接pushad将所有的通用寄存器给压进栈里。所以说现在栈中的情况应是如下样子:
- 实际的中断处理函数
这个函数在Linux里名为do_timer(),do_timer里面会做很多事情,我们只了解需要了解的部分
-
将变量jiffies加1,更新xtime
-
调度,任务切换
注:Linux0.11版本中,jiffies在上述的入口函数就处理加 1 了,后面版本有提及在do_timer中实现。
- 出口函数
出口函数:它也是在入口函数中被调用,即最后一条语句:jmp intr_exit,此函数可以看作入口函数前面一部分的逆过程,就是出栈,回复原任务的上下文(如果没有调度还是原任务的话)。
系统时间维护
时钟中断处理程序需要完成的一项重要任务便是维护系统时间,内核需要有严格的时间观念来驱动任务执行,那如何维护时间呢?
这就是上面提到的将jiffies加1,更新xtime,xtime在RTC中已做过介绍,那什么是jiffies呢?
jiffies,英文单词jiffy的负数,意为一会儿,瞬间,它是Linux里的一个全局变量,表示系统自启动以来发生的时钟滴答总数。
jiffies和xtime都是拿来表示时间的,xtime更多用于用户通过系统调用获取时间,而内核代码更多使用jiffies。jiffies表示时间滴答次数,就像是系统的脉搏,给各任务打着拍子,内核运行所用到的时间基本都以它为标准。就比如更新 xtime 的函数也是依据jiffies来更新xtime的值。
所以对于时钟中断处理函数来说,jiffies变量的更新可以说是最紧急的,需要立刻执行更新,而xtime不那么急迫可放到下半部分执行。
可这又会引发一个问题,Linux中断的下半部分执行的时间是不确定的,那怎么保证xtime的更新是正确的呢?
为解决这个问题,Linux里专门设置了一个全局变量wall_jiffies,它表示 xtime 上一次更新时jiffies的值,更新 xtime 的函数是update_times(),它会根据两者的差值来跟新xtime,这不问题就完美解决了。可以看看实际源码,还是很好理解:
Linux属于unix类操作系统,unix类操作系统的当前时间和日期都采用同一种计时方式:
读取RTC或者用户自己输入时间作为基准,然后根据时间滴答次数也就是jiffies变量的值来计算当前时间(这里猜测:用户输入自己时间后jiffies应该会清零重新计数的,不然计算处的结果肯定不对,但没找到相关资料验证,诸位抱歉)
调度
上述系统时间维护所讲就是我们需要了解时钟中断干得第一件事,还有一件事是调度,任务切换。
调度,简单来说就是如果一个进程或者说线程的时间片用完了,则被换下处理器,调度另一个进程或线程上处理器执行。进程线程调度的水很深,下次详细讲,在这了解知道有这回事儿就好。
时钟中断总结
时钟中断的大致过程就是这样,隐去了很多细节,但过程这条线应该还是很清楚的,在这儿再缕缕当作总结:
本文主要讲述了计算机里面的时间,时间不管对于我们人来说还是对于计算机来说都是极其重要的,它是所有工作任务的基础,所以我们必须维护好它。
再然后本着简单可行的原则说了说时钟中断的过程,旨在理清过程,隐去了许多细节,本人水平不高是一方面的原因,但操作系统的复杂多变也是客观的事实。若与诸位了解的不一样还请见谅海涵;若有错误还请批评指正。
最后,愿大家都能成为时间管理大师,生活有条不紊的向前进行。
喜欢本文的朋友还请点赞,关注公众号Rand_cs可获取更多关于系统的精品文章,还有关于计算机的各类电子书,总能找到你需要的,赶快关注吧