Linux 内核时间管理

  时间管理在内核中占有非常重要的地位。相对于事件驱动,内核中有大量的函数都是基于时间驱动的。内核必须管理系统的运行时间以及当前的日期和时间。

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

 

1. 内核中的时间概念

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

 

2. 节拍率

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

 

3. jiffies

      全局变量 jiffies 用来记录自系统启动以来产生的节拍的总数。启动时,内核将该变量初始化为0,此后,每次时钟中断处理程序都会增加该变量的值。因为一秒内时钟中断的次数等于Hz,所以jiffes一秒内增加的值也就为Hz,系统运行时间以秒为单位计算,就等于jiffes/Hz。jiffes=seconds*HZ

jiffes 定义在文件 linux/jiffs.h 中

extern unsigned long volatile jiffies;

关键字volatile指示编译器在每次访问变量时都重新从主内存中获得,而不是通过寄存器中的变量别名访问,从而确保前面的循环能按预期的方式执行。

 

3.1 jiffies的内部表示

       jiffies变量总是无符号长整数(unsigned long),因此,在32位体系结构上是32位,在时钟频率为100的情况下,497天后会溢出,如果频率是1000,49.7天后会溢出。

 

3.2   jiffies的回绕

      当jiffies变量的值超过它的最大存放范围后就会发生溢出,对于32位无符号长整型,最大取值为2^32-1,在溢出前,定时器节拍计数最大为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参数通常是参数需要对比的值。

 

3.3 用户空间和HZ

      在2.6以前的内核中,如果改变内核中HZ的值会给用户空间中某些程序造成异常结果。这是因为内核是以节拍数/秒的形式给用户空间导出这个值的,在这个接口稳定了很长一段时间后应用程序便逐渐依赖于这个特定的HZ值了。所以如果在内核中更改了HZ的定义值,就打破了用户空间的常量关系——用户空间并不知道新的HZ值。

      要想避免上面的错误,内核必须更改所有导出的jiffies值。因而内核定义了USER_HZ来代表用户空间看到的值。对于ARM体系结构,HZ = USR_HZ。

 

4. 硬实钟和定时器

       体系结构提供了两种设备进行计时——一种是我们前面讨论过的系统定时器,另一种是实时时钟。实时时钟(RTC)是用来持久存放系统时间的设备,即便系统关闭后,它可以靠主板上的微型电池提供的电力保持系统的计时。当系统启动时,内核通过读取RTC来初始化墙上时间,该时间存放在xtime变量中。

系统定时器是内核定时机制中最为重要的角色。尽管不同体系结构中的定时器实现不尽相同,但是系统定时器的根本思想没有区别——提供一种周期性触发中断机制。

 

5. 时钟中断处理程序

       下面我们看一下时钟中断处理程序是如何实现的。时钟中断处理程序可以划分为两个部分:体系结构相关部分和体系结构无关部分。

与体系结构相关的例程作为系统定时器的中断处理程序而注册到内核中,以便在产生时钟中断时,它能够相应的运行。虽然处理程序的具体工作依赖于特定的体系结构,但是绝大多数处理程序至少要执行如下工作:

(1)获得xtime_lock锁,以便对访问jiffies_64和墙上时间xtime进行保护。

(2)需要时应答或重新设置系统时钟。

(3)周期性的使用墙上时间更新实时时钟。

(4)调用体系结构无关的例程:do_timer。

中断服务程序主要通过调用与体系结构无关的例程do_timer执行下面的工作:

给 jiffies_64 变量增加1

更新资源消耗的统计值,比如当前进程所消耗的系统时间和用户时间。

执行已经到期的动态定时器。

执行 scheduler_tick() 函数。

更新墙上时间,该时间存放在xtime变量中。

do_timer() 函数执行完毕后返回与体系结构相关的中断处理程序,继续执行后面的工作,释放xtime_lock锁,然后退出。

以上全部工作每1/HZ秒都要发生一次,也就是说在你的PC机上时钟中断处理程序每秒执行1000次。

 

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()。

 

7. 定时器

       定时器,有时也称为动态定时器或内核定时器——是管理内核时间的基础。定时器的使用很简单。只需要执行一些初始化工作,设置一个超时时间,指定超时发生后执行的函数,然后激活定时器就可以了。指定的函数将在定时器到期时自动执行。定时器并不周期运行,它在超时后就自行销毁,这也正是这种定时器被称为动态定时器的一个原因。

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()  //(不能在中断上下文中使用)

 

8. 延迟执行

8.1 忙等待

最简单的延迟方法是忙等待(或者说是忙循环)。但这种方法仅仅适用于延迟的时间是节拍的整数倍,或者精确度要求不高时。更好的方法是在代码等待时,允许内核重新调度执行其他任务:

unsigned long delay = jiffies + 5*HZ;

while(time_before(jiffies, delay))
    cond_resched();

cond_resched() 函数将调度一个新程序投入运行,但它只有在设置完need_resched标志后,才能生效。延迟执行不管在哪种情况下都不应该在持有锁或禁止中断时发生。

 

8.2   短延迟

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

void udelay(unsigned long usecs)

void mdelay(unsigned long msecs)

经验证明,不要使用udelay()函数处理超过1毫秒的延迟。此时使用mdelay()更为安全

 

8.3   schedule_timeout()

       更理想的延迟执行方法是使用 schedule_timeout() 函数,用法如下:

set_current_state(TASK_INTERRUPTIBLE);      /* 将任务设置为可中断睡眠状态   */

schedule_timeout(s *HZ);        /* 小睡一会,s秒后唤醒   */

唯一的参数是延迟的相对时间,单位为 jiffies。上例中将相应的任务推入可中断睡眠队列(注意了,这里的进入睡眠队列,就意味着可以去执行其他任务了),睡眠s秒。在调用schedule_timeout()函数前必须首先将任务设置成TASK_INTERRUPTILE和TASK_UNINTERRUPTIBLE面两种状态之一,否则任务不会睡眠。调用代码绝对不能持有锁(因为持有锁的任务是不能睡眠的)。

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

 

你可能感兴趣的:(Linux,内核时间管理)