Chinaunix首页 | 论坛 | 博客
  • 博客访问: 3003658
  • 博文数量: 674
  • 博客积分: 17881
  • 博客等级: 上将
  • 技术积分: 4849
  • 用 户 组: 普通用户
  • 注册时间: 2010-03-17 10:15
文章分类

全部博文(674)

文章存档

2013年(34)

2012年(146)

2011年(197)

2010年(297)

分类: LINUX

2010-08-19 19:08:55

       时间管理在内核中占有非常重要的地位。相对于事件驱动,内核中有大量的函数都是基于时间驱动的。

      内核必须管理系统的运行时间以及当前的日期和时间。

      周期产生的事件都是由系统定时器驱动的。系统定时器是一种可编程硬件芯片,它已固定频率产生中断。该中断就是所谓的定时器中断,它所对应的中断处理程序负 责更新系统时间,还负责执行需要周期性运行的任务。系统定时器和时钟中断处理程序是Linux系统内核管理机制中的中枢。

10.1   内核中的时间概念

       硬件为内核提供了一个系统定时器用以计算流逝的时间,系统定时器以某种频率自行触发时钟中断,该频率可以通过编程预定,称节拍率。当时钟中断发生时,内核 就通过一种特殊中断处理程序对其进行处理。内核知道连续两次时钟中断的间隔时间。这个间隔时间称为节拍(tick),内核就是靠这种已知的时钟中断来计算 墙上时间和系统运行时间。墙上时间即实际时间,内核提供了一组系统调用以获取实际日期和实际时间。系统运行时间——自系统启动开始所经过的时间——对用户 和内核都很有用,因为许多程序都必须清楚流逝过的时间。

10.2 节拍率

      系统定时器频率是通过静态预处理定义的,也就是HZ,在系统启动时按照Hz对硬件进行设置。体系结构不同,HZ的值也不同。内核在文 件中定义了HZ的实际值,节拍率就是HZ,周期为1/HZ。i386的节拍率为1000,其它体系结构(包括 ARM)的节拍率多数都等于100。

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;     /* 定时器内部值,用户不要使用 */

};

       内核提供了一组与定时器相关的接口用来简化管理定时器的操作。所有这些接口都声明在文件中,大多数接口都在kernel/timer.c中获得实现。

     创建定时器时需要先定义它: 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   短延迟

        有时内核代码(通常也是驱动程序)不但需要很短暂的延迟(比时钟节拍还短)而且还要求延迟的时间按很精确。这种情况多发生在和硬件同步时,内核提供了两个 可以处理微秒和毫秒级别的延迟函数,它们都定义在中,可以看到它们并不使用jiffies:

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面两种状态之一,否则任务不会睡眠。调用代码绝对不能持有锁(因为持有锁的任务是不能睡眠的)。

       当任务被重新调度时,将返回代码进入睡眠前的位置继续执行。

阅读(930) | 评论(0) | 转发(1) |
给主人留下些什么吧!~~