定时测量是由基于固定频率振荡器和计数器的几个硬件电路完成的。
一、时钟和定时器电路
在80x86体系结构上,内核必须显示地与几种时钟和定时器电路打交道。时钟电路同时用于跟踪当前时间和产生精确的时间度量。定时器电路由内核编程,所以它们以固定的、预先定义的频率发出中断。这样的周期性中断对于实现内核和用户程序使用的软定时器都是至关重要的。
1.1、实时时钟(RTC)
Linux只用RTC来获取时间和日期。
1.2、时间戳计数器(TSC)
算出CPU实际频率的任务是在系统初始化期间完成的。calibrate_tsc()函数通过计算一个大约在5ms的时间间隔内所产生的时钟信号的个数来算出CPU实际频率。通过适当地设置可编程间隔定时器的一个通道来产生这个时间常量。
1.3、可编程间隔定时器(Programmable Interval Timer PIT)
PIT通过发出一个特殊的中断,叫做时钟中断(timer interrupt)来通知内核又一个时间间隔过去了。与闹钟的一个区别是,PIT永远以内核确定的固定频率不停地发出中断。
时钟中断的频率取决于硬件体系结构。较慢的机器,其节拍大约为10ms(每秒产生100次时钟中断),而较快的机器的节拍大约1ms(每秒产生1000或1024次时钟中断)。
HZ产生每秒时钟中断的近似个数,也就是时钟中断的频率。
1.4、CPU本地定时器
CPU本地定时器是一种能够产生单步中断或周期性中断的设备,它类似于方才描述的可编程间隔定时器,不过,还是有几点区别:
* APIC计数器是32位,而PIC计数器是16位;因此,可以对本地定时器编程来产生很低频率的中断(计数器存放中断发生前必须经过的节拍数)。
* 本地APIC定时器把中断只发送给自己的处理器,而PIT产生一个全局性中断,系统中的任一CPU都可以对其处理。
* APIC定时器是基于总线时钟信号的。每隔1,2,4,8,16,32,64或128总线时钟信号到来时对该定时器进行递减可以实现对其编程的目的。相反,PIT有其自己的内部时钟振荡器,可以更灵活地编程。
1.5、高精度事件定时器(HPET)
1.6、ACPI电源管理定时器
二、Linux计时体系结构
Linux必定执行与定时相关的操作,例如,内核周期性地:
* 更新自系统启动以来所经过的时间。
* 更新时间和日期。
* 确定当前进程在每个CPU上已运行了多长时间,如果已经超过了分配给它的时间,则抢占它。
* 更新资源使用统计数。
* 检查每个软定时器的时间间隔是否已到。
多处理器机器所有的计时体系结构与单处理器机器所具有的稍有不同:
* 在单处理器系统上,所有的计时活动都是由全局定时器(可以是可编程间隔定时器也可以是高精度事件定时器)产生的中断触发的。
* 在多处理器系统上,所有普通的活动都是由全局定时器产生的中断触发的,而具体CPU的活动(像监控当前运行进程的执行时间)是由本地APIC定时器产生的中断触发的。
Linux的计时体系结构还依赖于时间戳计数器(TSC)、ACPI电源管理定时器、高精度事件定时器(HPET)的可用性。内核使用两个基本的计时函数:一个保持当前最新的时间,另一个计算在当前秒内走过的纳秒数。有几种不同的方式获得后一个值:如果CPU有TSC或HPET,就可以用一些更精确的方法;在其他情况下,使用精确性差一点的方法。
2.1、计时体系结构的数据结构
2.1.1、定时器对象
定时器对象是timer_opts类型的一个描述符。
2.1.2、jiffies变量
jiffies变量是一个计数器,用来记录自系统启动以来产生的节拍总数。每次时钟中断发生时(每个节拍)它便加1。
2.1.3、xtime变量
xtime变量存放当前时间和日期;它是一个timespec类型的数据结构,该结构有两个字段:
tv_sec:存放自1970年1月1日(UTC)午夜以来经过的秒数。
tv_nsec:存放自上一秒开始经过的纳秒数。
xtime变量通常是每个节拍更新一次,也就是说,大约每秒更新1000次。用户程序从xtime变量获得当前时间和日期。内核也经常引用它,例如,在更新节点时间戳时引用。
2.2、单处理器系统上的计时体系结构
在单处理器系统上,所有与定时有关的活动都是由IRQ线0上的可编程间隔定时器产生的中断触发的。同样,在Linux中,某些活动都尽可能在中断产生后立即执行,而其余的活动延迟。
2.2.1、初始化阶段
在内核初始化期间,time_init()函数被用来建立计时体系结构,它通常执行如下操作:
1、初始化xtime变量。利用get_cmos_time()函数从实时时钟上读取自1970年1月1日(UTC)午夜以来经过的秒数。设置xtime的tv_nsec字段,这样使得即将发生的jiffies变量溢出与tv_sec字段保持一致,也就说,它将落到秒的范围内。
2、初始化wall_to_monotonic变量。
3、如果内核支持HPET,它将调用hpet_enable()函数来确认ACPI固件是否探测到了该芯片并将它的寄存器映射到了内存地址空间中。
4、调用select_timer()来挑选系统中可利用的最好的定时器资源,并设置cur_timer变量指向该定时器资源对应的定时器对象的地址。
5、调用setup_irq(0, &irq0)来创建与IRQ0相应的中断门,IRQ0引脚线连接着系统时钟中断源(PIT或HPET)。irq0变量被静态定义如下:
struct irqaction irq0 = {timer_interrupt, SA_INTERRUPT, 0, "timer", NULL, NULL);
从现在起,timer_interrupt()函数将会在每个节拍到来时被调用,而中断被禁止,因为IRQ0主描述符的状态字段中的SA_INTERRUPT标志被置位。
2.2.2、时钟中断处理程序
P237
2.3、多处理器系统上的计时体系结构
多处理器系统可以依赖两种不同的时钟中断源:可编程间隔定时器或高精度事件定时器产生的中断,以及CPU本地定时器产生的中断。
一个CPU本地时钟中断触发涉及本地CPU的计时活动,例如监视当前进程的运行时间和更新资源使用统计数。
2.3.1、初始化阶段
全局时钟中断处理程序由time_init()函数初始化。
2.3.2、全局时钟中断处理程序
2.3.3、本地时钟中断处理程序
该处理程序执行系统中与特定CPU相关的计时活动,即监管内核代码并检测当前进程在特定CPU上已经运行了多长时间。
三、更新时间和日期
用户程序从xtime变量中获得当前时间和日期。内核必须周期性地更新该变量,才能使它的值保持相当的精确。
全局时钟中断处理程序调用update_times()函数更新xtime变量的值。
四、更新系统统计数
内核在与定时相关的其他任务中必须周期性地收集若干数据用于:
* 检查运行进程的CPU资源限制。
* 更新与本地CPU工作负载有关的统计数。
* 计算平均系统负载。
* 监管内核代码。
4.1、更新本地CPU统计数
P241
4.2、记录系统负载
update_times()在每个节拍都要调用calc_load()函数来计算处于TASK_RUNNING或TASK_UNINTERRUPTIBLE状态的进程数,并用这个数据更新平均系统负载。
4.3、监管内核代码
Linux包含一个被称作readprofiler的最低要求的代码监管器,Linux开发者用其发现内核在内核态的什么地方花费时间。
监管器基于非常简单的蒙特卡洛算法:在每次时钟中断发生时,内核确定该中断是否发生在内核态;如果是,内核从堆栈取回中断发生前的eip寄存器的值,并用这个值揭示中断发生前内核正在做什么。最后,采样数据积聚在“热点”上。
4.4、检查非屏蔽中断(NMI)监视器
当NMI中断处理程序检测到一个CPU冻结时,就会敲响所有的钟:它把引起恐慌的信息记录在系统日志文件中,转储该CPU寄存器的内容和内核栈(内核OOP)的内容,最后杀死当前进程。这就为内核开发者提供了发现错误的机会。
五、软定时器和延迟函数
定时器是一种软件功能,即允许在将来的某个时刻,函数在给定的时间间隔用完时被调用。超时(time-out)表示与定时器相关的时间间隔已经用完的那个时刻。
相对来说,实现一个定时器并不难。每个定时器都包含一个字段,表示定时器将需要多长时间到期。每个字段的初值就是jiffies的当前值加上合适的节拍数。这个字段的值不再改变。每当内核检查定时器时,就把这个到期字段值和当前这一刻jiffies的值相比较,当jiffies大于或等于这个字段存放的值时,定时器到期。
Linux考虑两种类型的定时器,即动态定时器(dynamic timer)和间隔定时器(interval timer)。第一种类型由内核使用,而间隔定时器可以由进程在用户态创建。
有关Linux定时器的警告:因为对定时器函数的检查总是由可延迟函数进行,而可延迟函数被激活以后很长时间才能被执行,因此,内核不能确保定时器函数正好在定时到期时开始执行,而只能保证在适当的时间执行它们,或者假定延迟到几百毫秒之后执行它们。因此,对于必须严格遵守定时时间的那些实时应用而言,定时器并不适合。
5.1、动态定时器
动态定时器被动态地创建和撤销,对当前活动动态定时器的个数没有限制。
动态定时器存放在下列timer_list结构中:
struct timer_list {
struct list_head entry;
unsigned long expires;
spinlock_t lock;
unsigned long magic;
void (*function)(unsigned long);
unsigned long data;
tvec_base_t *base;
};
function字段包含定时器到期时执行函数的地址。data字段指定传递给定时器函数的参数。正是由于data字段,就可以定义一个单独的通用函数来处理多个设备驱动程序的超时问题,在data字段可以存放设备ID,或其他有意义的数据,定时器函数就可以用这些数据区分不同的设备。
expires字段给出定时器到期时间,时间用节拍数表示,其值为系统启动以来所经过的节拍数。当expires的值小于或等于jiffies的值时,就说明计数器到期或终止。
entry字段用于将软定时器插入双向循环链表中,该链表根据定时器expires字段的值将它们分组存放。
为了创建并激活一个动态定时器,内核必须:
1、如果需要,创建一个新的timer_list对象,比如说设为t。这可以通过以下几种方式来进行:
在代码中中定义一个静态全局变量。
在函数内定义一个全局变量:在这种情况下,这个对象存放在内核堆栈中。
在动态分配的描述符中包含这个对象。
2、调用init_timer(&t)函数初始化这个对象。实际是把t.base指针字段置为NULL并把t.lock自旋锁设为“打开”。
3、把定时器到期时激活函数的地址存入function字段。如果需要,把传递给函数的参数值存入data字段。
4、如果动态定时器还没有被插入到链表中,给expires字段赋一个合适的值并调用add_timer(&t)函数把t元素插入到合适的链表中。
5、否则,如果动态定时器已经被插入到链表中,则调用mod_timer()函数来更新expires字段,这样也能将对象插入到合适的链表中。
在定时器函数内删除定时器是一种良好的习惯做法。
在Linux2.6中,动态定时器需要CPU来激活,也就是说,定时器函数总会在第一个执行add_timer()或稍后执行mod_timer()函数的那同一个CPU上运行。不过,del_timer()及与其类似的函数能使所有动态定时器无效,即使该定时器并不依赖于本地CPU激活。
5.1.1、动态定时器与竞争条件
如果一个定时器作用于可丢弃的资源,那么在释放资源前停止定时器。
del_timer_sync()函数从链表中删除定时器,然后检查定时器函数是否还在其他CPU上运行,如果是,函数就等待,直到定时器函数结束。该函数相当复杂,而且执行速度慢。
del_timer_sync()函数相当复杂,而且执行速度慢,因为它必须小心考虑这种情况:定时器函数重新激活它自己。如果内核开发者知道定时器函数从不重新激活定时器,她就能使用更简单更快速的del_singleshot_timer_sync()函数来使定时器无效,并等待直到定时器函数结束。
定时器函数在SMP上的安全实现是通过每个timer_list对象包含的lock自旋锁达到的:每当内核必须访问动态定时器的链表时,就禁止中断并获取这个自旋锁。
5.1.2、动态定时器的数据结构
把expires值划分成不同的大小,并允许动态定时器从大expires值的链表到小expires值的链表进行有效的过滤。此外,在多处理器系统中活动的动态定时器集合被分配到各个不同的CPU中。
5.1.3、动态定时器处理
5.2、动态定时器应用之一:nanosleep()系统调用
5.3、延迟函数
udelay()和ndelay()函数。
六、与定时测量相关的系统调用
6.1、time()和gettimeofday()系统调用
用户态下的进程通过以下几个系统调用获得当前时间和日期:
time():返回从1970年1月1日午夜(UTC)开始所走过的秒数。
gettimeofday():返回从1970年1月1日午夜(UTC)开始所走过的秒数及在前一秒内走过的微妙数,这个值存放在数据结构timeval中。
6.2、adjtimex()系统调用
通常把系统配置成能在常规基准上运行时间同步协议,例如网络定时协议(NTP),以在每个节拍逐渐地调整时间。在Linux中,这个实用程序依赖于adjtimex()系统调用。
6.3、setitmer()和alarm()系统调用
间隔定时器由以下两个方面来刻画:
* 发送信号所必须的频率,或者如果只需要产生一个信号,则频率为空。
* 在下一个信号被产生以前所剩余的时间。
6.4、与POSIX定时器相关的系统调用
POSIX定时器比传统间隔定时器更灵活、更可靠。它们之间有两个显著区别:
* 当传统间隔定时器到期时,内核会发送一个SIGALRM信号给进程来激活定时器。而当一个POSIX定时器到期时,内核可以发送各种信号给整个多线程应用程序,也可以发送给单个指定的线程。内核还能在应用程序的某个线程上强制执行一个通告器函数,或者甚至什么也不做(这取决于处理时间的用户态函数库)。
* 如果一个传统间隔定时器到期了很多次但用户态进程不能接收SIGALRM信号(例如由于信号被阻塞或者进程不处于运行态),那么只有第一个信号被接收到,其他所有SIGALRM信号都丢失了。对于POSIX定时器来说会发生同样的情况,但进程可以调用timer_getoverrun()系统调用来得到自第一个信号产生以来定时器到期的次数。
阅读(890) | 评论(0) | 转发(0) |