简介: 本文试图完整地描述 Linux 系统中 C 语言编程中的时间问题。主要内容包括应用程序中的时间编程方法;时钟硬件简介;Glibc 时间函数的实现以及 Linux 内核对时间的支持和实现原理。这是第二部分,探讨时钟硬件和 GlibC 时间函数的实现。
熟悉了基本的编程方法之后,我们的兴趣就在于,计算机如何实现这一切的呢?在那些应用层 API 和底层系统硬件之间,操作系统和库函数究竟做了些什么?
首先看下 Linux 时间处理的一般过程:
图 1. 时间处理过程
应用程序部分已经在第一部分详细介绍过了,在第二部分我将介绍硬件和 GlibC 相关实现的一些概况。
PC 机里常见的时钟硬件有以下这些。
RTC (Real Time Clock,实时时钟)
人们需要知道时间的时候,可以看看钟表。计算机系统中钟表类似的硬件就是外部时钟。它依靠主板上的电池,在系统断电的情况下,也能维持时钟的准确性。计算机需要知道时间的时候,就需要读取该时钟。
在 x86 体系中,这个时钟一般被称为 Real Time Clock。RTC 是主板上的一个 CMOS 芯片,比如 Motorola 146818,该芯片独立于 CPU 和其他芯片,可以通过 0x70 和 0x71 端口操作 RTC。RTC 可以周期性地在 IRQ 8 上触发中断,但精度很低,从 2HZ 到 8192HZ。
以 Motorola 146818 为例,软件可以通过 I/O 指令读写以下这些值:
图 2. Motorola 146818
可以看到,RTC 能提供精确到秒的实时时间值。
TSC (Time Stamp Counter)
CPU 执行指令需要一个外部振荡器产生时钟信号,从 CLK 管脚输入。x86 提供了一个 TSC 寄存器,该寄存器的值在每次收到一个时钟信号时加一。比如 CPU 的主频为 1GHZ,则每一秒时间内,TSC 寄存器的值将增加 1G 次,或者说每一个纳秒加一次。x86 还提供了 rtdsc 指令来读取该值,因此 TSC 也可以作为时钟设备。TSC 提供了比 RTC 更高精度的时间,即纳秒级的时间精度。
PIT (Programmable Interval Timer)
PIT 是 Programmable Interval Timer 的缩写,该硬件设备能定时产生中断。早期的 PIT 设备是 8254,现在多数可以集成在 Intel 的 I/O Control Hub 电路中,可以通过端口 0x40~0x43 访问 PIT。系统利用 PIT 来产生周期性的时钟中断,时钟中断通过 8259A 的 IRQ0 向 CPU 报告。它的精度不高,其入口 clock 的频率为 1MHz,理论上能产生的最高时钟频率略小于 0.5MHz。实际系统往往使用 100 或者 1000Hz 的 PIT。
HPET (High Precision Event Timer)
PIT 的精度较低,HPET 被设计来替代 PIT 提供高精度时钟中断(至少 10MHz)。它是由微软和 Intel 联合开发的。一个 HPET 包括了一个固定频率的数值增加的计数器以及 3 到 32 个独立的计时器,这每一个计时器有包涵了一个比较器和一个寄存器(保存一个数值,表示触发中断的时机)。每一个比较器都比较计数器中的数值和寄存器中的数值,当这两个数值相等时,将产生一个中断。
APIC Timer (Advanced Programmable Interrupt Controller Timer)
APIC ("Advanced Programmable Interrupt Controller") 是早期 PIC 中断控制器的升级,主要用于多处理器系统,用来支持复杂的中断控制以及多 CPU 之间的中断传递。APIC Timer 集成在 APIC 芯片中,用来提供高精度的定时中断,中断频率至少可以达到总线频率。系统中的每个 CPU 上都有一个 APIC Timer,而 PIT 则是由系统中所有的 CPU 共享的。Per CPU 的 Timer 简化了系统设计,目前 APIC Timer 已经集成到了所有 Intel x86 处理器中。
以上这些硬件仅仅是 x86 体系结构下常见的时间相关硬件,其他的体系结构如 mips、arm 等还有它们常用的硬件。这么多的硬件令人眼花缭乱,但其实无论这些硬件多么复杂,Linux 内核只需要两种功能:
- 一是定时触发中断的功能;
- 另一个是维护和读取当前时间的能力。
一些硬件提供了中断功能,一些硬件提供了读取时间的功能,还有一些硬件则能够提供两种功能。下表对上面描述过的硬件进行了一个简单的总结:
表 1. 时钟硬件汇总表
设备 | 中断功能 | 读取时间功能 | Per CPU | 备注 |
---|---|---|---|---|
RTC | Y | Y | N | Linux 不使用其中断功能 |
TSC | N | Y | Y | |
PIT | Y | N | N | 虽然支持 one-shot 中断,但配置 one-shot 的延迟较大,无法应用于高精度时间操作 |
HPET | Y | Y | N | |
APIC Timer | Y | N | Y |
也许您已经发现,这些硬件提供的功能非常简单,为了满足应用程序的各种各样的需求,Linux 内核和 C 标准库还需要做很多工作,才能让我们使用诸如 gettimeofday()、setitimer() 等函数进行时间相关的操作。
我们在第一部分已经详细介绍了标准 C 库中关于时间函数的用法。表 2 罗列了一些主要的 API。
表 2. 应用层时间 API 分类
分类 | API names |
---|---|
获取和设置“实时时间” | time(),gettimeofday(),clock_gettime(), ftime(),stime(),settimeofday() |
时间格式转换 | ctime(),asctime(),gmtime(),localtime(),mktime(),strftime(),strptime() |
定时器 | getitimer(),setitimer(),timer_create().timer_delete() timer_gettime(),timer_settime().timer_getoverrun() |
本文力图简短,无法对上表中的每一个 API 进行详细分析。幸运的是,我们只需要研究几个典型 API 的实现,便可以举一反三,了解其他 API 的大致实现思想。
第一个典型 API 是 time(),我们参考 GlibC2.13 版本的实现。
清单 1.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 系统调用,来返回时间值。同样,如果我们查看 gettimeofday() 等很多 API,将会发现它们也是仅仅调用了 Linux 的系统调用来完成指定的功能。根据我的分析,下面这些函数都是直接调用了 Linux 的系统调用来完成工作:
表 3. 时间 API 及其系统调用
C API | 相应的系统调用 |
---|---|
time() | sys_time |
gettimeofday() | sys_gettimeofday |
clock_gettime() | sys_clock_gettime |
stime() | sys_stime |
settimeofday() | sys_settimeofday |
getitimer() | sys_getitimer |
setitimer(), | sys_setitimer |
timer_delete() | sys_timer_delete |
timer_gettime(), | sys_timer_gettime |
timer_settime(). | sys_timer_settime |
timer_getoverrun() | sys_timer_getoverrun |
ftime() 在 Glibc 中的代码实现在 sysdeps/unix/bsd/ftime.c,因为在 Linux 系统中 ftime 系统调用已经过时了,目前如果还有调用 ftime() 的应用程序 GLibc 将用 gettimeofday() 来模拟,具体代码如下:
清单 2,ftime 的 GlibC 实现
int ftime (timebuf) struct timeb *timebuf; { struct timeval tv; struct timezone tz; if (__gettimeofday (&tv, &tz) < 0) //调用 gettimeofday return -1; timebuf->time = tv.tv_sec; timebuf->millitm = (tv.tv_usec + 500) / 1000; if (timebuf->millitm == 1000) { ++timebuf->time; timebuf->millitm = 0; } timebuf->timezone = tz.tz_minuteswest; timebuf->dstflag = tz.tz_dsttime; return 0; } |
多数 GLibC 中的时间函数只是对系统调用的简单封装,不过 timer_create 要算是一个特例,虽然它的大部分功能都是通过系统调用 sys_timer_create 完成的。但是如果 GlibC 发现 timer 的到期通知方式被设置为 SIGEV_THREAD 时,Glibc 需要自己完成一些辅助工作,因为内核无法在 Timer 到期时启动一个新的线程。
考察文件 nptl\sysdeps\unix\sysv\linux\timer_create.c,可以看到 GLibc 发现用户需要启动新线程通知时,会自动调用 pthread_once 启动一个辅助线程(__start_helper_thread),用 sigev_notify_attributes 中指定的属性设置该辅助线程。
然后 Glibc 启动一个普通的 POSIX Timer,将其通知方式设置为:SIGEV_SIGNAL | SIGEV_THREAD_ID。这样就可以保证内核在 timer 到期时通知辅助线程。通知的 Signal 号为 SIGTIMER,并且携带一个包含了到期函数指针的数据。这样,当该辅助 Timer 到期时,内核会通过 SIGTIMER 通知辅助线程,辅助线程可以在信号携带的数据中得到用户设定的到期处理函数指针,利用该指针,辅助线程调用 pthread_create() 创建一个新的线程来调用该处理函数。这样就实现了 POSIX 的定义。
综上所述,除了少数 API(比如 timer_create),需要 GLibC 做部分辅助工作之外,大部分 GLibC API 的工作可以总结为:调用相应的系统调用。
还有一些 API 我们还没有分析,即那些时间格式转换函数。这些函数的功能是将一个时间值转换为人类容易阅读的形式,因此这些函数的实现完全是在 GlibC 中完成,而无需内核的系统调用。下面我们看一看 ctime() 吧:
清单 3,ctime 的 GlibC 实现
char * ctime (const time_t *t) { return asctime (localtime (t));} |
localtime() 和 asctime() 的实现都比较复杂,但归根结底是进行复杂的格式转换,时区转换计算等等。这些工作都是完全在 GlibC 内部实现的,无须内核参与。感兴趣的读者可以仔细研究 Glibc time 目录下的 localtime.c、tzset.c 等具体实现。
在这一部分中,我们首先了解到了一些硬件时钟设备的简单知识。无论这些设备本身如何复杂和不同,它们只提供两个主要功能:计时功能和定时中断功能。要想利用这两个基本功能来满足应用的需求似乎还有很多工作,比如:如何衡量实时时间和 CPU 时间?
通过对 GlibC 的简单分析,我们也看到库函数实际上把复杂问题统统交给了内核。GlibC 仅仅是一个中转站,把用户请求发给内核。因此想了解更多,我们必须进入内核。
在接下来的第三部分和第四部分,我们将介绍 Linux 内核的时间系统。
学习
- 本系列 第 1 部分:Linux 应用层的时间编程,探讨应用开发中的时间编程问题。
- 维基百科上关于 HPET 的介绍,详细介绍了 HPET 的细节和产生历史。
- 维基百科上关于 PIT 的介绍,介绍了 PIT 的细节。
- 51CTO 上的文章,Linux 内部的时钟处理机制全面剖析,详细介绍了 Linux 的时钟硬件和内核相关处理。
- 维基百科上关于 APIC 的介绍,详细解说了 APIC 的细节。
- 下载 GlibC2.13 的代码,可以更清楚地了解实现细节。
- 在 developerWorks Linux 专区寻找为 Linux 开发人员(包括 Linux 新手入门)准备的更多参考资料。