系统定时器是一种可编程硬件芯片,能以固定频率产生中断,也就是定时器中断,其对应的中断处理程序负责更新系统时间,也负责执行需要周期性运行的任务。
系统定时器和时钟中断处理程序是Linux系统内核管理机制的中枢。动态定时器是一种推迟执行程序的工具。
一、内核中的时间概念
内核必须在硬件的帮助下才能计算和管理时间。硬件为内核提供一个系统定时器用以计算流逝的时间。系统定时器以某种频率自行触发(击中hitting或射中popping)时钟中断,该频率可以通过编程预定,称节拍率(tick rate)。当时钟中断发生时,内核通过特殊的中断处理程序进行处理。节拍率对内核是可知的,所以内核知道连续两次时间中断的间隔时间,也就是节拍(tick),等于1/节拍率(秒)。
- 墙上时间:实际时间
- 系统运行时间:自系统启动开始所经过的时间
二、节拍率:HZ
节拍率是通过静态预处理定义的,也就是HZ(赫兹),在系统启动时按照HZ值对硬件进行设置。大多数体系结构的节拍率是可调的。
提高节拍率的好处:
- 更高的时钟中断解析度可以提高时间驱动事件的解析度
- 提高时间驱动事件的准确度
高HZ的优势(也就是高时钟中断解析度和准确度带来的好处):
- 内核定时器能以更高的频度和准确度运行
- 依赖定时值执行的系统调用以更高的精度运行,提高性能
- 对如资源消耗和系统运行时间等的测量会有更精细的解析度
- 提高进程抢占的准确度
高HZ的劣势:HZ越高,时钟中断频率越高,系统负担越重。
三、jiffies
全局变量jiffies用于记录自系统启动以来的节拍的总数。
jiffies的定义:
extern unsigned long volatile jiffies;
//关键词volatile指示编译器在每次访问变量时都重新从主内存中获得,而不是通过寄存器中的变量别名来访问。
在32位体系结构上unsigned long只有32位,很容易会溢出。因此增加了一个64位的变量:
extern u64 jiffies_64; // 可通过get_jiffies_64()函数访问
ld脚本用于连接主内核映像,然后用jiffies_64变量覆盖jiffies。因此,对于32位系统,jiffies取jiffies_64的低32位,因为大多数代码使用jiffies存放流失的时间,因此只关心低32位;时间管理代码使用整个64位jiffies_64,避免溢出。对于64位系统,jiffies和jiffies_64是同一个变量。
因为32位体系结构不能原子地一次访问64位变量中的两个32位数值,因此在读取jiffies时需要用xtime_lock锁对jiffies变量进行锁定。
当jiffies变量的值发生溢出,如果节拍计数还要继续增加,jiffies的值就会回绕到0。内核提供了宏定义来处理:
#define time_after(unkonwn, known) ((long)(known) - (long)(unknown) < 0)
#define time_before(unkonwn, known) ((long)(unknown) - (long)(known) < 0)
#define time_after_eq(unkonwn, known) ((long)(unknown) - (long)(known) >= 0)
#define time_before(unkonwn, known) ((long)(known) - (long)(unknown) >= 0)
内核定义了USER_HZ来代表用户空间看到的HZ值。
jiffies_to_clock_t(unsigned long)//将HZ表示的节拍计数转化称USER_HZ表示的节拍计数
jiffies_64_to_clock_t(unsigned long)
四、硬时钟和定时器
实时时钟(RTC):用于持久存放系统时间的设备,即便系统关闭后,也可以靠主板上的微型电池提供电力保持系统的计时。当系统启动时,内核通过读取RTC来初始化墙上时间,存放在xtime变量中。
系统定时器提供一种周期性触发中断机制。
五、时钟中断处理程序
时钟中断处理程序可以划分为两部分:
- 体系结构相关部分
- 体系结构无关部分
与体系结构相关的例程作为系统定时器的中断处理程序注册到内核中,以便产生时钟中断时运行。其执行:
- 获得xtime_lock锁,以便对访问jiffies_64和xtime进行保护
- 需要时应答或重设系统时钟
- 周期性使用墙上时间更新实时时钟
- 调用体系结构无关的时钟例程tick_periodic()
- 释放xtime_lock锁
tick_periodic()执行:
- jiffies_64加1
- 更新资源消耗的统计值
- 执行以及到期的动态定时器
- 执行sheduler_tick()函数
- 更新墙上时间,存放在xtime变量中
- 计算平均负载值
六、实际(墙上)时间
墙上时间保存在xtime变量中:
struct timespec {
_kernel_time_t tv_sec; //秒,19700101以来经过的s数
long tv_nsec; //ns,上一秒开始经过的ns数
};
struct timespec xtime;
获取墙上时间的系统调用为:
int gettimeofday(struct timeval*tv,struct timezone *tz )
七、定时器
定时器(也称动态定时器、内核定时器)是管理内核流逝的时间的基础。由结构timer_list表示:
struct timer_list {
struct list_head entry;
unsigned long expires; //以jiffies为单位的定时值
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 =
my_timer.function =
my_timer.data =
// 激活定时器
add_timer(&my_timer);
//修改定时器超时时间
mod_timer(&my_timer, jiffies+new_delay);
//停止定时器
del_timer(&my_timer);
del_timer_sync(&my_timer);
内核在时钟中断发生后执行定时器,定时器作为软中断在下半部上下文执行。
八、延迟执行
8.1 忙等待
忙等待是最简单的延迟方法,仅仅在想要延迟的时间是节拍的整数倍,或精确率要求不高时才可以使用。
8.2 短延迟
有时内核需要很短的延迟,而且还要求延迟的时间很准确,就需要延迟函数:
void udelay(unsigned long usecs); //us
void ndelay(unsigned long nsecs);//ns
void mdelay(unsigned long msecs);//ms
BogoMIPS值记录处理器在给定时间内忙循环执行的次数。udelay函数根据指定的延迟时间在1s中的占比,就能决定需要进行多少次循环就可以达到要求的推迟时间。
8.3 schedule_timeout()
schedule_timeout()会让需要延迟的任务睡眠到指定的延迟时候耗尽再重新运行。当指定时间到期后,内核唤醒被延迟的任务并将其重新放回到运行队列中。schedule_timeout()调用之前,必须将任务置成TASK_INTERRUPTIBLE或TASK_UNINTERRUPTIBLE状态,否则不会休眠。调用schedule_timeout的代码必须处于进程上下文,并且不能持有锁。