分类: LINUX
2010-08-19 19:08:55
时间管理在内核中占有非常重要的地位。相对于事件驱动,内核中有大量的函数都是基于时间驱动的。
内核必须管理系统的运行时间以及当前的日期和时间。
周期产生的事件都是由系统定时器驱动的。系统定时器是一种可编程硬件芯片,它已固定频率产生中断。该中断就是所谓的定时器中断,它所对应的中断处理程序负 责更新系统时间,还负责执行需要周期性运行的任务。系统定时器和时钟中断处理程序是Linux系统内核管理机制中的中枢。
10.1 内核中的时间概念
硬件为内核提供了一个系统定时器用以计算流逝的时间,系统定时器以某种频率自行触发时钟中断,该频率可以通过编程预定,称节拍率。当时钟中断发生时,内核 就通过一种特殊中断处理程序对其进行处理。内核知道连续两次时钟中断的间隔时间。这个间隔时间称为节拍(tick),内核就是靠这种已知的时钟中断来计算 墙上时间和系统运行时间。墙上时间即实际时间,内核提供了一组系统调用以获取实际日期和实际时间。系统运行时间——自系统启动开始所经过的时间——对用户 和内核都很有用,因为许多程序都必须清楚流逝过的时间。
10.2 节拍率
系统定时器频率是通过静态预处理定义的,也就是HZ,在系统启动时按照Hz对硬件进行设置。体系结构不同,HZ的值也不同。内核在文
件
10.3 jiffies
全局变量jiffies用来记录自系统启动以来产生的节拍的总数。启动时,内核将该变量初始化为0,此后,每次时钟中断处理程序都会增加该变量的值。
10.3.1 jiffies的内部表示
jiffies变量总是无符号长整数(unsigned long),因此,在32位体系结构上是32位,在时钟频率为100的情况下,497天后会溢出,如果频率是1000,49.7天后会溢出。
10.3.2 jiffies的回绕
当jiffies变量的值超过它的最大存放范围后就会发生溢出,在溢出前,定时器节拍计数最大为4294967295,如果节拍数达到了最大值后还要继续增加的话,它的值会回绕到0。回绕会引起许多问题,下面的宏可以正确的处理节拍计数回绕的情况:
#define time_after(unknown, known) ((long)(konwn) - (long)(unknown) < 0)
#define time_before(unknown, known) ((long)(unknown) - (long)(known) < 0)
#define time_after_eq(unknown, known) ((long)(unknown) - (long)(known) >=0)
#define time_before_eq(unknown, known) ((long)(known) - (long)(unknown)>= 0)
其中unknown参数通常是jiffies, known参数通常是参数需要对比的值。
10.3.3 用户空间和HZ
在2.6以前的内核中,如果改变内核中HZ的值会给用户空间中某些程序造成异常结果。这是因为内核是以节拍数/秒的形式给用户空间导出这个值的,在这个接 口稳定了很长一段时间后应用程序便逐渐依赖于这个特定的HZ值了。所以如果在内核中更改了HZ的定义值,就打破了用户空间的常量关系——用户空间并不知道 新的HZ值。
要想避免上面的错误,内核必须更改所有导出的jiffies值。因而内核定义了USER_HZ来代表用户空间看到的值。对于ARM体系结构,HZ = USR_HZ。
10.4 硬实钟和定时器
体系结构提供了两种设备进行计时——一种是我们前面讨论过的系统定时器,另一种是实时时钟。实时时钟(RTC)是用来持久存放系统时间的设备,即便系统关 闭后,它可以靠主板上的微型电池提供的电力保持系统的计时。当系统启动时,内核通过读取RTC来初始化墙上时间,该时间存放在xtime变量中。系统定时 器是内核定时机制中最为重要的角色。尽管不同体系结构中的定时器实现不尽相同,但是系统定时器的根本思想没有区别——提供一种周期性触发中断机制。
10.5 时钟中断处理程序
下面我们看一下时钟中断处理程序是如何实现的。时钟中断处理程序可以划分为两个部分:体系结构相关部分和体系结构无关部分。与体系结构相关的例程作为系统 定时器的中断处理程序而注册到内核中,以便在产生时钟中断时,它能够相应的运行。虽然处理程序的具体工作依赖于特定的体系结构,但是绝大多数处理程序至少 要执行如下工作:
(1)获得xtime_lock锁,以便对访问jiffies_64和墙上时间xtime进行保护。
(2)需要时应答或重新设置系统时钟。
(3)周期性的使用墙上时间更新实时时钟。
(4)调用体系结构无关的例程:do_timer。do_timer执行下面的工作:
给jiffies_64变量增加1
更新资源消耗的统计值,比如当前进程所消耗的系统时间和用户时间。
执行已经到期的动态定时器。
执行scheduler_tick()函数。
更新墙上时间,该时间存放在xtime变量中。
10.6 实际时间
当前实际时间(墙上时间)定义在文件kernel/timer.c中:
struct timespec xtime;
timespec数据结构定义在文件
struct timespec{ time_t tv_sec; /* 秒 */
long tv_nsec; /* 纳秒 */
}; 其中,xtime.tv_sec以秒为单位,存放着自1970年7月1日以来经过的时间。xtime.tv_nsec记录了自上一秒开始经过的纳秒数。读 写xtime变量需要使用xtime_lock锁,它是一个seq锁。读取xtime时要使用read_seqbegin()和 read_seqretry()函数:
do {
unsigned long lost;
seq = read_seqbegin(&xtime_lock);
usec = timer->get_offset();
lost = jiffies->wall_jiffies;
if(lost) usec+=lost *(1000000/HZ);
sec = xtime.tv_sec;
usec += (xtime.tv_nsec/1000);
} while(read_seqretry(&xtime_lock,seq));
该循环不断重复,直到读者确认读取数据时没有写操作介入。如果发现循环期间有时钟中断处理程序更新xtime,那么read_seqretry()函数就返回无效序列号,继续循环等待。从用户空间取得墙上时间的主要接口是gettimeofday()。
10.7 定时器
定时器,有时也称为动态定时器或内核定时器——是管理内核时间的基础。定时器的使用很简单。只需要执行一些初始化工作,设置一个超时时间,指定超时发生后 执行的函数,然后激活定时器就可以了。指定的函数将在定时器到期时自动执行。定时器并不周期运行,它在超时后就自行销毁,这也正是这种定时器被称为动态定 时器的一个原因。
10.7.1 使用定时器
定时器由结构timer_list表示,定义在文件
struct timer_list {
struct list_head entry; /* 定时器链表的入口 */
unsigned long expiers; /* 以jiffies为单位的定时器 */
spinlock_t lock; /* 保护定时器的锁 */
void ( * function)(unsigned long); /* 定时器处理函数 */
unsigned long data; /* 传给处理函数的长整形参数 */
struct tvec_t_base_s *base; /* 定时器内部值,用户不要使用 */
};
内核提供了一组与定时器相关的接口用来简化管理定时器的操作。所有这些接口都声明在文件
创建定时器时需要先定义它: struct timer_list my_timer;
初始化定时器数据结构,初始化必须在使用其它 定时器管理函数 对定时器进行操作之前完成。
init_timer(&my_timer); 然后就可以填充结构中需要的值了。
my_timer.expires = jiffies + delay; /* 定时器超时时的节拍数 */
my_timer.data = 0; /* 给定时器处理函数传入0值 */
my_timer.function = my_function; /* 定时器超时时调用的函数 */
my_timer.expires表示超时时间,它是以节拍为单位的绝对计数值。如果当前jiffies计数等于或大于它,处理函数开始执行。处理函数必须符合下面的函数原形:
void my_timer_function(unsigned long data);
data参数使我们可以利用一个处理函数注册多个定时器,只需通过该参数就能区别它们。
激活定时器:add_timer(&my_timer);
有时可能需要更改已经激活的定时器超时时间,所以内核通过函数mod_timer()来实现该功能,该函数可以改变指定的定时器超时时间:
mod_timer(&my_timer, jiffies+new_delay);
mod_timer()函数也可以操作那些已经初始化,但还没有被激活的定时器,它会同时激活它。一旦从mod_timer()函数返回,定时器都将被激活而且设置了新的定时值。
如果需要在定时器超时前停止定时器,可以使用del_timer()函数:
del_timer(&my_timer); 或 del_timer_sync()(不能在中断上下文中使用)
10.8 延迟执行
10.8.1 忙等待
最简单的延迟方法是忙等待(或者说是忙循环)。但这种方法仅仅适用于延迟的时间是节拍的整数倍,或者精确度要求不高时。更好的方法是在代码等待时,允许内核重新调度执行其他任务:
unsigned long delay = jiffies + 5*HZ;
while(time_before(jiffies, delay)) cond_resched();
cond_resched()函数将调度一个新程序投入运行,但它只有在设置完need_resched标志后,才能生效。延迟执行不管在哪种情况下都不应该在持有锁或禁止中断时发生。
10.8.2 短延迟
有时内核代码(通常也是驱动程序)不但需要很短暂的延迟(比时钟节拍还短)而且还要求延迟的时间按很精确。这种情况多发生在和硬件同步时,内核提供了两个
可以处理微秒和毫秒级别的延迟函数,它们都定义在
void udelay(unsigned long usecs)
void mdelay(unsigned long msecs)
经验证明,不要使用udelay()函数处理超过1毫秒的延迟。此时使用mdelay()更为安全。
10.8.3 schedule_timeout()
更理想的延迟执行方法是使用schedule_timeout()函数,用法如下:
set_current_state(TASK_INTERRUPTIBLE); /* 将任务设置为可中断睡眠状态 */
schedule_timeout(s *HZ); /* 小睡一会,s秒后唤醒 */
唯一的参数是延迟的相对时间,单位为jiffies。上例中将相应的任务推入可中断睡眠队列(注意了,这里的进入睡眠队列,就意味着可以去执行其他 任务了),睡眠s秒。在调用schedule_timeout()函数前必须首先将任务设置成TASK_INTERRUPTILE和 TASK_UNINTERRUPTIBLE面两种状态之一,否则任务不会睡眠。调用代码绝对不能持有锁(因为持有锁的任务是不能睡眠的)。
当任务被重新调度时,将返回代码进入睡眠前的位置继续执行。