您的位置:首页 > 运维架构 > Linux

LINUX内核之中断(4)--时间管理

2012-08-02 09:58 295 查看
1. 内核中的时间概念
时间管理在内核占有非常重要的地位。相对于事件驱动而言,内核中有大量函数都是基于时间驱动的。
注意相对时间和绝对时间之间的区别。
周期性产生的事件与推迟执行的事件之间的差别。周期性产生的事件都是由系统定时器驱动的。系统定时器是一种可编程硬件芯片,它以固定频率产生中断,也称定时器中断。而动态定时器是一种用来推迟执行程序的工具。
系统定时器以某种频率自行触发时钟中断、该频率可以通过编程预定,称为节拍率(tick
rate)。
连续两次时钟中断的时间间隔就称为节拍(tick),它等于节拍率分之一秒。内核就是靠这种已知的时钟间隔来计算墙上时间和系统运行时间。墙上时间也就是实际时间,对用户空间的应用程序来说最重要。系统运行时间是自系统开始所经过的时间,对用户空间和内核都很有用。
 
2. 节拍率

系统定时器频率是通过静态预定义的,也就是HZ(赫兹),在系统启动时按照HZ值对硬件进行设置。体系不同,HZ值也不同。内核在文件<asm/param.h>中定义了HZ的实际值,节拍率等于HZ,周期为1/HZ秒
 
2.1. 理想的HZ值

提供节拍率意味着时钟产生得更加频繁,中断处理程序耶会更加频繁地执行:
1)
更高的时钟中断解析度可提高时间驱动事件的解析度
2)
提供了时间驱动事件的准确度,比如依赖定时值执行的系统调用,进程抢占,资源消耗和系统运行时间等的测量
      

3. jiffies

全局变量jiffies用来记录自系统启动以来产生的节拍的总数。启动时,内核将该变量初始化为0,此后,每次时钟中断处理程序都会增加该变量的值。因为一秒内时钟中断的次数等于HZ,所以jiffies一秒内增加的值也就为HZ。系统运行时间以秒为单位计算,就等于jiffies/HZ
      
jiffies定义于文件<linux/jiffies.h>中:
extern unsigned long volatile jiffies;
代码经常需要设置一些将来的时间:
unsigned long time_stamp = jiffies; //现在
unsigned long next_tick = jiffies + 1; //从现在开始1个节拍
unsigned long later = jiffies + 1; //从现在开始5秒
 
3.1. jiffies的内部表示

jiffies变量总是无符号长整型(unsigned
long),因此,在32位体系结构上是32位,在64位体系结构体上是64。
第二个变量定义在<linux/jiffies.h>
extern u64 jiffies_64;
在x86上位于arch/i386/kernel/vmlinux.lds.S中,然后用jiffies_64变量的初始值覆盖jiffies变量:
jiffies = jiffies_64;
因此,jiffies取整个64位jiffies_64变量的低32位。
 
3.2. jiffies的回绕

和任何C整型一样,当jiffies变量的值超过它的最大存放范围后就会发生溢出。对于32位无符号长整型,最大值是2^32-1。如果节拍计数达到了最大值还要继续增加的话,她的值会回绕到0。
下面是一个回绕的例子:
unsigned long timeout = jiffies + HZ/2; 
/*0.5秒超时*/
if(timeout > jiffies){
/*没有超时,很好*/
}else{
      
       /*超时了,发生错误*/
}
如果发生回绕,if判断语句的结果刚好相反。幸好,内核提供了四个宏来帮助比较节拍计数,它们能正确处理节拍回绕情况,定义在<linux/jiffies.h>中:
#define time_after (unknown, known) ((long)(known) – (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参数是需要对比的值。
宏time_after(unknown, known),当时间unknown超过指定的known时,返回真,否则返回假;宏time_befor(unknown,
known),当时间unknown没超过指定的known时,返回真,否则返回假;后面两个宏类似。
前面的例子可以改为时钟-回绕-安全(timer-wraparound-safe)的版本:
unsigned long timeout = jiffies + HZ/2; 
/*0.5秒超时*/
if(time_before (jiffies ,timeout )){
/*没有超时,很好*/
}else{
      
       /*超时了,发生错误*/
}
 
3.3. 用户空间和HZ

在2.6以前的内核中,如果改变内核中HZ的值会给用户空间中某些程序造成异常结果,这是因为内核是以节拍数/秒的形式给用户空间导出这个值的。如果内核修改了HZ值,用户空间并不知道新的HZ值。
要避免上面的错误,内核必须更改所有导出的jiffies值。因而内核定义了USER_HZ来代表用户空间看到的HZ值。内核可以使用宏jiffies_to_clock_t将一个由HZ表示的节拍计数转换成一个由USER_HZ表示的节拍计数。该宏的用法取决于USER_HZ是否为HZ的整数倍或相反。当是整数倍时,形式相当简单:
#define jiffies_to_clock_t(x) ((x)/(HZ/USER_HZ))
内核使用函数jiffies_64_to_clock_t将64位的jiffies值的单位从HZ转换为USER_HZ。
用户空间希望HZ=USER_HZ,但是如果它们不相等,则由宏完成转换
 
4. 硬时钟和定时器

体系结构提供了两种设备进行计时:一种是系统定时器,另一种是实时时钟。
 
4.1. 实时时钟

实时时钟(RTC)是用来持久存放系统时间的设备,即便是系统关闭后,它也可以靠主板上微型电池提供的电力保持系统的计时。在PC体系结构中,RTC和CMOS集成在一起,而且RTC的运行和BIOS的保存设置都是通过同一个电池供电的。
当系统启动时,内核通过读取RTC来初始化墙上时间,该时间存放在xtime变量中。实时时钟最主要的的作用仍是在启动时初始化xtime变量
 
4.2. 系统定时器

系统定时器是在内核定时机制中提供一种周期性触发中断机制。在0x86体系结构中,主要采用可编程中断时钟(PIT)。
5. 时钟中断处理程序

时钟中断处理程序可以划分为两个部分:体系结构相关部分和体系结构无关部分。
与体系结构相关的例程作为系统定时器的中断处理程序而注册到内核中,以便产生时钟中断时,他能够相应的运行。
处理程序最低限度完成如下工作:
1)
获得xtime_lock锁,以便对访问jiffies_64和墙上时间xtime进行保护
2)
需要时应答或重新设置系统时钟
3)
周期性地使用墙上时间更新实时时钟
4)
调用体系结构无关的时钟例程:do_timer()
中断服务程序主要通过调研与体系结构无关的例程do_timer执行如下工作:
1)
给jiffies_64变量增加1
2)
更新新资源消耗的统计值,比如当前进程所消耗的系统时间和用户时间
3)
执行已经到期的动态定时器
4)
执行scheduler_tick函数
5)
更新墙上时间,该时间存放在xtime变量中
6)
计算平均负载值
因为上述工作分别都由单独的函数负责完成,所以do_timer()例程的执行代码如下:
void do_timer(struct pt_regs *regs)
{
             
jiffies_64++;
             
update_proccess_times(user_mdoe(regs));
             
update_times();
}
user_mode()宏查询处理器寄存器regs的状态。如果时钟中断发生在用户空间,他返回1;如果发生在内核模式,则返回0。Uodate_proccess_times函数根据时钟中断产生的位置,为用户或对体系进行相应的时间更新。
void update_proccess_times(int user_tick)
{
             
struct task_struct *p = current;
             
int cpu = smp_processor_id();
             
int system = user_tick^1;
             
update_one_proccess(p, user_tick, system, cpu);
             
run_local_timers();
             
scheduler_tick(user_tick, system);
}

update_one_proccess函数的作用是更新进程时间。它的实现时相当细致,通过分支判断,将user_tick和system加到进程相应的计数上。
/*更新恰当的时间计数器,给其中一个jiffy*/
p->utime+=user;
p->stime+=system;
注意:内核对进程进行时间计数时,是根据中断发生时处理器所处的模式进行分类的,它把上一个tick全部算给进程。这也是内核应该采用高频率的另一个原因
接着的run_local_timer函数标记了一个软中断去处理所有到期的定时器。
最后scheduler_tick函数负责减少当前运行进程的时间片计数值并且在需要时设置need_resched标志。
当update_proccess_timer函数返回后,do_timer函数接着会调用update_times函数更新墙上时钟。
Void update_time()
{
             
unsigned long ticks;
             
ticks = jiffies – wall_jiffies;
             
if(ticks){
             
       wall_jiffies+=ticks;
                    
update_wall_time(ticks);
}
last_time_offset = 0;
calc_laod(ticks);
}
ticks是记录最近一次更新新发生的节拍数。通常情况下ticks等于1。但是时钟中断也有可能丢失,在中断长时间被禁止的情况下,就会出现这种现象(这是一个bug)。Update_wall_time函数更新储存墙上时间的xtime,最后调用calc_load更新载入平均值,到此update_times执行完毕。
do_timer函数执行完毕后返回与体系结构有关的中断处理程序,继续执行后面的工作,释放xtime_lock锁,然后退出。
 
6. 实际时间

当前实际时间(墙上时间)定义在文件kernel/time.c中:
struct timespaec xtime;
timespec数据结构定义在文件<linux/time.h>中:
struct timespec{
      
       time_t tv_sec;        /*秒*/
      
       long tv_nsec;         /*纳秒*/
};
xtime.tv_sec以秒为单位,存放着自1970年7月1日(UTC)以来经过的时间,1970年1月1日称为纪元。Xtime.tv_nsec记录自上一秒开始经过的纳秒数。
读写xitime变量需要使用xtime_lock锁,该锁不是普通自旋锁而是一个seqlock锁。
更新xtime首先要申请一个seqlock锁:
wite_seqlock(&xtime_lcok);
/*更新xtime*/
wite_sequnlock(&xtime_lock);
读取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*(100 0000/HZ);
sec = xtime.tv_sec;
usec += (xtime.tv_nsec/1000);

}while(read_seqretry(&xtime_lock,seq));
该循环不断重复,直到读者确认读取数据时没有写操作介入。如果发现循环期间有时钟中断处理程序更新xtime,那么read_seqretry函数返回无效序列号,继续循环等待。
从用户空间取得墙上时间的主要接口是gettimeofday,在内核对应的系统调用为sys_gettimeofday:
asmlinkage long sys_gettimeofday(struct timeval *val *tv, struct timezone *tz)
{
      
       if(likely(tv)){
             
       struct timeval ktv;
             
       do_gettimeofday(&ktv);
             
       if(copy_to_user(tv, &ktv,sizeof(ktv)))
                    
       return –EFAULT;
}
if(unlikely(tz)){
      
       if(copy_to_user(tz,&sys_tz,sizeof(ys_tz)))
             
       return –EFAULT;
}
return 0;
}
如果用户提供的tv参数非空,那么与体系结构相关的do_gettimeofday函数将被调用。该函数执行的就是上面提到的循环读取xtime的操作。如果tz参数非空,该函数吧系统时区返回用户。如果在给用户空间拷贝墙上时间或时区时发生错误,该函数返回-EFAULT。
系统调用settimeofday来设置当前时间,它需要具有CAP_STS_TIME权限。
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息