Linux内核-定时器和时间管理

Posted by keys961 on March 27, 2019

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读取的结果jiffiesjiffies_64后32位的结果(使用64位存储是为了保证不溢出),也可以显式读取全部64位;

64位机器上,2个变量一样。

3.2. 回绕

可利用宏操作处理回绕:

  • time_after/before(unknown, known)
  • time_after/before_eq(unknown, known)

这里unknown通常是jiffies值。

3.3. 用户空间和HZ

用户空间看到的HZUSER_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()同样的功能,并可指定一个超时时间,当超时时,等待队列上的任务都会被唤醒。(代码可能需要检查被唤醒的原因)