1. 内核中的时间
硬件为内核提供时钟,由它触发时钟中断,内核对该中断进行处理。
内核提供API给用户查询墙上时钟和单调时钟。
2. 节拍率:HZ
定义在<asm/param.h>
,节拍率和体系架构有关。
提高节拍率,可提高时钟精度,以让更多的依赖时钟的操作更精确,但是会让系统负担过重(因为要处理更多的中断)。
3. jiffies
一个全局变量,记录系统启动以来产生的节拍数,定义在<linux/jiffies.h>
:
extern unsigned long volatile jiffies;
extern u64 jiffies_64; // For 64-bit machine
3.1. 内部表示
32位机器上,通过API读取的结果jiffies
是jiffies_64
后32位的结果(使用64位存储是为了保证不溢出),也可以显式读取全部64位;
64位机器上,2个变量一样。
3.2. 回绕
可利用宏操作处理回绕:
time_after/before(unknown, known)
time_after/before_eq(unknown, known)
这里unknown
通常是jiffies
值。
3.3. 用户空间和HZ
用户空间看到的HZ
由USER_HZ
定义,可能和内部的HZ
不一样,这时候需要转换,通过jiffies_to_clock_t(time)
转换。
4. 硬时钟和定时器
4.1. 实时时钟
一个持久存放系统时间的设备。系统启动后,内核读取它初始化墙上时间,存于xtime
中。
4.2. 系统定时器
提供了一种周期性触发中断的机制,它能以固定频率产生时钟中断。
5. 时钟中断处理程序
通常处理的工作有:
- 获得
xtime_lock
锁,以访问jiffies_64
和保护xtime
墙上时钟 - 应答和重新设置系统时钟
- 周期性使用墙上时钟更新实时时钟
- 调用体系无关的时钟例程
tick_periodic()
,并最后释放锁
而tick_periodic()
执行以下工作:
- 给
jiffies_64
加1 - 更新资源消耗统计值
- 执行已到期的动态定时器(通过软中断)
- 执行
scheduler_tick()
函数,以便于调度 - 更新墙上时间,存于
xtime
中 - 计算平均负载值
这里,一个节拍时间,运行的进程不唯一。
6. 墙上时钟
定义在kernel/time/timekeeping.c
中:
struct timespec xtime;
struct timespec {
_kernel_time_t tv_sec; /*second, from 1970/01/01*/
long tv_nsec; /*nanosecond, count from the last second*/
}
读写它需要使用xtime_lock
,它是一个顺序锁。
用户可通过gettimeofday()
获取墙上时间,对应系统调用是sys_gettimeofday()
,定义在kernel/time.c
。
7. 定时器
7.1. 使用
定时器为struct timer_list
,定义在<linux/timer.h>
中:
struct timer_list {
struct list_head entry; /*Timer list entry*/
unsigned long expires; /*Expire time ticks in jiffies*/
void (*function)(unsigned long); /*Timer procedure function*/
unsigned long data; /*Procedure function argument*/
struct tvec_t_base_s *s; /*Timer internal value*/
}
使用定义器的操作接口定义在<linux/timer.h>
中,可参考使用。
7.2. 定时器竞争条件
定时器和当前执行代码是异步的,可能存在竞争条件,因此:
- 不能通过
del_timer()
&add_timer()
序列修改计时器,而得使用del_timer_sync
- 删除定时器时,优先使用
del_timer_sync()
- 重点保护中断处理程序的共享数据
7.3. 实现定时器
时钟中断后,会执行定时器,而它作为软中断在下半部的中断上下文执行。
具体调用update_process_timers()
,任何随即调用run_local_timers()
。
所有定时器以链表形式存储,遍历耗时,因此内核将定时器以超时时间划分为5组,时间接近时,定时器随组下移,这样可以减少遍历的负担。
8. 延迟执行
8.1. 忙等待
循环等到节拍数为0即可:
unsigned long timeout = jiffies + X;
while (time_before(jiffies, timeout)) ;
volatile
关键字:每次读写都经过主内存,编译器不会对其读写进行任何优化。
8.2. 短延迟
处理ms,us,ns的延迟,内核提供API,在<linux/delay.h>
和<asm/delay.h>
中提供:
void u/n/mdelay(unsigned long u/n/msecs)
除非需要精确的延迟,这些都不要使用,否则性能会受很大影响。(因为这也是忙等待)
8.3. schedule_timeout()
更理想的方案,它会让进程睡眠到指定延迟时间后重新运行。使用方式:
set_current_state()
设置睡眠的状态(不设置则不会睡眠)schedule_timeout()
设置延迟,单位为jiffies
它是实现即一个定时器的简单应用:
- 创建一个定时器,并设置超时时间
- 注册处理函数:它会唤醒当前进程
- 激活定时器,并调用
schedule()
。由于进程已被设置了睡眠状态,调度程序就不会让其继续运行,而会选择其它进程运行
schedule_timeout()
:有schedule()
同样的功能,并可指定一个超时时间,当超时时,等待队列上的任务都会被唤醒。(代码可能需要检查被唤醒的原因)