一文讲解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);
-------------------------------------------------------------------------------------------------------------------------------------------

 【推荐阅读】

需要多久才能看完linux内核源码?

概述Linux内核驱动之GPIO子系统API接口

代码大佬的【Linux内核开发笔记】分享,前人栽树后人乘凉!

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值