linux内核时间管理

前言:

Linux中如何对时间进行管理?时钟节拍的概念及延时函数的用法很多同学都用不好,下面我给大家总结一下。


一,linux时钟运作机制

1,linux时钟运作机制

• 大部分PC机中有两个时钟源,分别是实时时钟(RTC)和 操作系统(OS)时钟

• 实时时钟也叫CMOS时钟,它靠电池供电,即使系统断电,也可以维持日期和时间。

• RTC和OS时钟之间的关系通常也被称作操作系统的时钟运作机制

• 不同的操作系统,其时钟运作机制也不同

linux中的时钟机制大致如下图所示

linux中时钟机制

由上图可知:

RTC是硬件时钟,它为整个计算机提供一个计时标准,是最原始最底层的时钟数据,由纽扣电池供电,系统断电后仍然在工作

OS时钟产生于PC主板上的定时/计数芯片,由操作系统控制这个芯片的工作,OS时钟的基本单位就是该芯片的计数周期,开机时操作系统取得RTC中的时间数据来初始化OS时钟,所以它只是在开机有效,由操作系统控制,已被称为软时钟或系统时钟。操作系统通过OS时钟提供给应用程序和时间有关的服务。

扩展:OS时钟其本质是一个计数器,计数器从计数初值开始,每收到一次脉冲信号,计数器减1,当减至0时,就会输出高电平或低电平,然后获取重载值重新从初值开始计数,不断循环,这样就得到一个输出脉冲,这个脉冲作用中断控制器上,产生中断信号,触发时钟中断。 

2,OS时钟中断

• OS时钟是由可编程定时/计数器产生的输出脉冲触发中断而产生的,而输出脉冲的周期叫做一个“时钟节拍”(Tick,又称滴答),(中断触发时会进入中断处理函数,使jiffies+1)

• 操作系统的“时间基准” 由设计者决定,Linux的时间基准是1970年1月1日凌晨0点

• OS时钟记录的时间就是系统时间。系统时间以“时钟节拍”为单位

•时钟中断触发的频率,由内核HZ来确定,系统启动时会按照定义的HZ值对硬件进行设置

比如对HZ的定义如下:

#define  Hz 100      

内核时间频率:表示每秒钟触发100次时钟中断,即每10ms触发一次,

   每次中断jiffies+1,,则每秒jiffies增加了100,

• Linux中用全局变量 jiffies表示系统自启动以来的时钟节拍数目(时钟中断触发的次数)

   因此系统运行的时间以s为单位计数,  就等于 jiffies/HZ

   内核启动时将该变量初始化为0,此后,每次时钟中断处理程序都会增加该变量的值,每秒钟触发中断的次数为Hz, 

3、实际时间

实际时间就是现实中钟表上显示的时间,其实内核中并不常用这个时间,主要是用户空间的程序有时需要获取当前时间,所以内核中也管理着这个时间。

实际时间的获取是在开机后,内核初始化时从RTC读取的。

内核读取这个时间后就将其放入内核中的 xtime 变量中,并且在系统的运行中不断更新这个值。

当前实际时间(墙上时间):  xtime.tv_sec以秒为单位,存放着自1970年7月1日(UTC)以来经过的时间,1970年1月1日被称为纪元。多数Unix系统的墙上时间都是基于该纪元而言的。xtime.tv_nsec记录自上一秒开始经过的纳秒数。

在<Time.h(incluce/linux)>中 

extern struct timespec xtime; 

#ifndef _STRUCT_TIMESPEC 

#define _STRUCT_TIMESPEC 

struct timespec {      /*高精度*/

     time_t  tv_sec;     /* seconds */ 

    long    tv_nsec;    /* nanoseconds 纳秒*/ 

}; 

     #endif

 从用户空间取得墙上时间的主要接口是gettimeofday(),在内核中对应的系统调用为sys_gettimeofday():

虽然内核也实现了time()系统调用,但是gettimeofday()几乎完全取代了它。C库函数也提供了墙上时间相关的库调用,比如ftime(),ctime()。

  除了更新xtime时间外,内核不会想用户空间程序那样频繁的使用xtime。但是,在文件系统的实现代码中存放访问时间戳(创建,存取,修改等)需要使用xtime。

4,时钟中断处理程序----操作系统的脉搏

每一次时钟中断的产生都触发下列几个主要的操作:

– 给jiffies变量加 1

– 更新时间和日期,既更新xtime墙上时间

– 确定当前进程在CPU 上已运行了多长时间,如果已经超过了分配给它的时间,则抢占它

– 更新资源使用统计数

– 检查定时器时间间隔是否已到,如果是,则执行它注册的函数(运行于底半部软中断中)

 以上工作每秒要发生 Hz次,也就是说PC上的时钟中断处理程序执行的频率为Hz

5、时间系统总结

1、节拍----->jiffies

又称时钟滴答,是一个全局变量,它的值在系统引导的时候初始化为0,在时钟中断初始化完成后,每次时钟中断发生,在时钟中断处理例程中都会将jiffies的值 +1。

 jiffies_64:为了解决jiffies溢出问题,更重要的是通过jiffies_64可以知道自开机以来的时间间隔。

2、节拍率---->HZ

HZ表示时钟中断发生的频率。可以在.config的配置文件中改写。1/HZ是每个jiffies+1的时间间隔。

3、通过jiffies可以进行时间的比较和时间转换

4、时间比较

     32位                                                    64位

     time_after(a,b)                                    time_after64(a,b)

     time_before(a,b)                                 time_before64(a,b)

     time_after_eq(a,b)                              time_after_eq64(a,b)

     time_before_eq(a,b)                           time_before_eq64

     time_in_range(a,b,c)                           time_in_range(a,b,c)

5、时间转换

     a、jiffies和msecs以及usecs的转换:

     unsigned int jiffies_to_msecs(const unsigned long);

     unsigned int jiffies_to_usecs(const unsigned long);

     unsigned long msecs_to_jiffies(const unsigned int m);

     unsigned long usecs_to_jiffies(const unsigned int u);

    b、jiffies和timespec以及timeval的转换

 在用户空间,应用程序更多的使用秒以及毫秒等时间形式,而在内核中多使用jiffes。

     内核定义了struct timeval 和 struct timespec 两种数据结构

     struct timespec {

               __kernel_time_t tv_sec;

               long              tv_nsec;

      }

     struct timeval {

               __kernel_time_t          tv_sec;

              __kernel_suseconds_t  tv_usec;

    }

    相互转换函数:

     unsigned long timespec_to_jiffies(const struct timespec *value);

     void jiffies_to_timespec(const unsigned long jiffies, struct timespec *value);

     unsigned long timeval_to_jiffies(const struct timeval *value);

     void jiffies_to_timeval(const unsigned long jiffies, struct timeval *value);

6、要注意的是jiffies的精度问题。

如果HZ = 1000,则jiffies增加1代表1ms。如果要用到更高精度的始终,要用其他的硬件机制。

 

二、内核短延时

Linux内核中提供了下列3个函数以分别进行纳秒、微秒和毫秒延迟:

void ndelay(unsigned long nsecs);

void udelay(unsigned long usecs);

void mdelay(unsigned long msecs);

上述延迟的实现原理本质上是忙等待,它根据CPU频率进行一定次数的循环。如果没有特殊的理由(比如在中断上下文中获取自旋锁的情况),不推荐使用这些函数延迟较长的时间,浪费CPU。

注:ndelay 和 mdelay都是基于udelay,将udelay的次数除1000就是ndelay,因此ndelay的次数为1000的整数倍才准确。

有时候,人们在软件中进行下面的延迟:

void delay(unsigned int time)

{

while(time--);

}

ndelay()、udelay()和mdelay()函数的实现方式原理与此类似。

内核在启动时,会运行一个延迟循环校准(Delay Loop Calibration),计算出lpj(Loops Per Jiffy)即处理器在一个jiffy时间内运行一个内部的延迟循环的次数,内核启动时会打印如下类似信息:

Calibrating delay loop... 530.84 BogoMIPS (lpj=1327104)

如果我们直接在bootloader传递给内核的bootargs中设置lpj=1327104,则可以省掉这个校准的过程节省约百毫秒级的开机时间。

睡着延时

毫秒时延(以及更大的秒时延)已经比较大了,在内核中,最好不要直接使用mdelay()函数,这将耗费CPU资源,对于毫秒级以上的时延,内核提供了下述函数:

void msleep(unsigned int millisecs);

unsigned long   msleep_interruptible(unsigned int millisecs);

void ssleep(unsigned int seconds);

上述函数将使得调用它的进程睡眠参数指定的时间为millisecs,msleep()、ssleep()不能被打断,而msleep_interruptible()则可以被打断。

受系统Hz以及进程调度的影响,msleep()类似函数的精度是有限的。

 

三、内核长延时

在内核中,一个直观的延时的方法是将所要延迟的时间设置的当前的jiffies加上要延迟的时间,这样就可以简单的通过比较当前的jiffies和设置的时间来判断延时的时间时候到来。针对此方法,内核中提供了简单的宏用于判断延时是否完成。

time_after(a,b);         /*如果时间a在b之后 (a>b),则返回真,否则返回0*/

time_before(a,b);      /*如果时间a在b之前 (a<b),则返回真,否则返回0*/

长延时实现举例:

/* 延迟 100 个 jiffies */

unsigned long delay = jiffies + 100;

while(time_before(jiffies, delay));

/* 再延迟 2s */

unsigned long delay = jiffies + 2*Hz;

while(time_before(jiffies, delay)); 

与time_before()对应的还有一个time_after(),它们在内核中定义为(实际上只是将传入的未来时间jiffies和被调用时的jiffies进行一个简单的比较):

#define time_after(a,b) \

(typecheck(unsigned long, a) && \

typecheck(unsigned long, b) && \

((long)(b) - (long)(a) < 0))

#define time_before(a,b) time_after(b,a)

为了防止在time_before()和time_after()的比较过程中编译器对jiffies的优化,内核将其定义为

volatile变量,这将保证每次都会重新读取这个变量。因此volatile更多的作用还是避免这种读合并。


四、让进程睡固定的时间

下面两个函数可以将当前进程添加到等待队列中,从而在等待队列上睡眠,当超时发生时,进程将被唤醒:

sleep_on_timeout(wait_queue_head_t *q, unsigned long timeout);

interrupt_sleep_on_timeout(wait_queue_head_t *q, unsigned long timeout);

the end

评论(0)