内核中有大量函数都是基于时间驱动的.例如,屏幕刷新.
10.1 内核中的时间概念
节拍(tick):连续两次时钟中断的间隔时间,谓之节拍.
10.2 节拍率:HZ
把上面的节拍取倒数就是节拍率(HZ).内核在文件<asm/param.h>中定义了HZ的实际值,节拍率等于HZ,周期为1/HZ秒.例如:
I386体系结构在include/asm-i386/param.h中对HZ值定义如下:
#define HZ 1000
即i386体系结构中系统定时器频率为1000HZ,也就是说每秒钟1000次.
HZ在内核中起着举足轻重的地位.比如HZ=100的处理器,执行粒度为10毫秒,意味着系统中的周期事件最快为每10毫秒运行一次,而不可能有更高的精度.假定有一个正在运行的进程,它的时间片只剩下2毫秒了,此时如果调度程序要求抢占该进程,然后去运行另一个新进程,在这2毫秒内是不可能发生抢占的.实际上,对于频率为100HZ的时钟来说,最坏要在10毫秒后,当下一个时钟中断(100HZ对应的一个时钟中断为10毫秒)到来时才可以进行抢占.
10.3 jiffies
全局变量jiffies用来记录自系统启动以来产生的节拍总数.1秒内时钟中断次数等于HZ,jiffies一秒内增加的值就就是HZ.系统的运行时间以秒为单位计算,就等于jiffies/HZ.
Jiffies定义于<linux/jiffies.h>中:
Extern unsigned long volatile jiffies;
将秒转换成jiffies:
(seconds * HZ)
将jiffies转换成以秒为单位的时间:
(jiffies/HZ)
相对而言,内核中将秒转换为jiffies用得多一些,比如代码经常需要设置一些将来的时间:
Unsigned long time_stamp = jiffies; //现在
Unsigned long next_tick = jiffies+1; //下一节拍
Unsigned long later = jiffies + 5*HZ; //从现在起5秒
10.4 硬时钟和定时器
体系结构提供了两种设备进行计时--一种是前面讨论的系统定时器;另一种是实时时钟(RTC).
系统启动时,内核通过读RTC初始化墙上时间,并存放在xtime变量中.
10.5 时钟中断处理程序
时钟中断处理程序分两部分:体系结构相关部分和体系结构无关部分.
体系结构相关部分:
作为系统定时器的中断处理程序注册到内核中,以便在产生时钟中断时,它能够相应运行.最低限度的工作如下:
.获得xtime_lock锁,以便对访问jiffies_64和墙上时间xtime进行保护;
.需要时应答或重新设置系统时钟;
.周期性地使用墙上时间更新实时时钟;
.调用体系结构无关的时钟例程:do_timer().
中断服务程序主要通过调用与体系结构无关的例程do_timer()执行下面的工作:
.给jiffies_64变量增加1;
.更新资源的消耗的统计值,比如当前进程所消耗的系统时间和用户时间;
.执行到期的动态定时器;
.执行scheduler_tick()函数;
.更新墙上时间,该时间存放在xtime变量中;
.计算平均负载值
上述的各个动作离不开函数do_timer()函数.其示意代码如下:
void do_timer(struct pt_regs *regs)
{
jiffies_64++;
update_process_times(user_mode(regs));
update_times();
}
void update_process_times(int user_tick)
{
struct task_struct *p = current;
int cpu = smp_processor_id(), system = user_tick ^ 1;
update_one_process(p, user_tick, system, cpu);
run_local_timers();
scheduler_tick(user_tick, system);
}
void run_local_timers(void)
{
raise_softirq(TIMER_SOFTIRQ);
}
因此,定时器其实也是靠软中断实现的.
10.6 实际时间
当前实际时间(墙上时间)定义在文件kernel/timer.c中,记录于变量xtime变量里面:
Struct timespec xtime;
Timespec数据结构定义在文件<linux/time.h>中,如下:
Struct timespec
{
Time_t tv_sec;
Long tv_nsec;
}
读写xtime变量需要使用xtime_lock锁,该锁是一个seqlock锁.
写xtime变量:
Write_seqlock(&xtime_lock);
/*更新xtime变量*/
Write_sequnlock(&xtime_lock);
读xtime变量:
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));
对于实际时间,采用seq锁十分合理,因为实际时间会被很多程序调用.
内核也会对变量xtime使用,例如在文件系统的实现代码中存放的访问时间戳(创建、存取、修改等)需要使用xtime变量.
10.7 定时器
LINUX下的定时器并不周期运行,超时后自动销毁.
10.7.1 结构体
Struct timer_list
{
Struct list_head entry; //定时器链表的入口
Unsigned long expires; //以jiffies为单位的定时值
Spinlock_t lock; //保护定时器的锁
Void (*function)(unsigned long); //定时器处理函数
Unsigned long data; //传给定时器处理函数的长整形参数
Struct tvec_t_base_s *base; //定时器内部值,用户不要使用
}
[注:]此结构体不宜过于深入理解.
定义:
Struct timer_list my_timer;
初始化:
Init_timer(&my_timer);
填充用户信息:
My_timer.expires = jiffies + delay; //jiffies的值大于等于my_timer.expires
My_timer.data = 0; //传给定时器处理函数的参数
My_timer.function = my_function; //定时器处理函数的参数
定时器处理函数形式:
Void my_timer_function(unsigned long data);
激活定时器:
Add_timer(&my_timer);
[注:]一般来说,定时器都在超时后马上执行,但是也有可能被推迟到下一次时钟节拍时才能运行,所以不能用定时器来实现任何硬件实时任务.
更改已经激活的定时器超时时间:
Mod_timer(&my_timer,jiffies+new_delay);
停止定时器:
Del_timer(&my_timer);
Del_timer_sync(&my_timer); //不能在中断上下文使用,确保同步.优先使用
10.8 延迟执行
上述的定时器或前面讲述的下半部机制可以实现延迟执行的动作.下面再总结几种推迟执行的方案:
10.8.1 忙等待
使用的示意代码如下,但是这种方案并不推荐使用:
Unsigned long delay = jiffies + 10;
While(time_before(jiffies,delay))
;
循环不断执行,直到jiffies大于delay为止.这里为10个节拍,对于HZ=1000的体系结构而言,耗时10毫秒.
作为上述方案的改进方案:
Unsigned long delay = jiffies + 5*HZ;
Whiel(time_before(jiffies,delay))
Cond_resched();
Cond_resched()函数将调度一个新程序投入运行.
[注:]延迟执行不管在哪种情况下都不应该在持有锁或禁止中断时发生.
10.8.2 短延迟
有时内核代码需要很短的延迟而且要求也比较精准.这时候就不能基于时钟节拍来实现,因为像HZ=100的体系结构(如ARM),一个时钟节拍就是10毫秒.对于一些时序仿真是达不了要求的.LINUX为很短且精准的延迟提供了如下函数:
Void udelay(unsigned long usecs);
Void mdelay(unsigned long msecs);
[注:]非不得已,不要在持有锁或禁止中断时用忙等待,会使系统响应速度和性能大打折扣.
[阅读材料]:
BogoMIPS:
BogoMIPS值记录处理器在给定时间忙循环执行次数.其记录的是处理器在空闲时速度有多快!该值存放在变量loops_per_jiffy中,可以从文件/proc/cpuinfo中读到它.内核启动时复用calibrate_delay()计算loops_per_jiffy值.如内核启动时我们可以看到以下字样:
Detectd 1004.932 MHz processor.
Calibrating delay loop ... 1990.65 BogoMIPS
10.8.3 schdule_timeout()
Schdule_timeout()函数的延迟方法会让需要延迟执行的任务睡眠到指定的延迟时间耗尽再重新运行.是基于jiffies的.使用示意代码如下:
Set_current_state(TASK_INTERRUPTIBLE);
Schedule_timerout(s*HZ);
Set_current_state()函数只能接受TASK_INTERRUPTIBLE和TASK_UNINTERRUPTIBLE,否则不能唤醒.
10.8.4 等待队列
当然,也可以把需要延迟的进程推到等待队列上去.
10.8.5 也可以使用第九单的完成变量来实现.