定时器¶
定时器基于 tick,是根据系统 jiffies 来触发的,精度相对比较低。利用定时器,我们可以设定在未来的某个时刻,触发一个特定的事件。
节拍率和jiffies变量¶
系统定时器的频率被称为节拍率(tick rate),产生的两次时钟中断的间隔就被称为节拍(tick),它等于节拍率分之一秒。内核通过时钟中断间隔来计算系统运行时间。
定时器频率是通过静态预处理定义的,在系统启动时被设置,该值与体系结构相关。如果设置为 1000,那么每秒就产生 1000 次中断(每 1ms 产生一次)。
定时器频率应设置为一个理想的值,高 HZ 可以提高系统的性能,使得由时间驱动的事件更为精确。但同时也带来了额外的系统负担,因为处理器会被更频繁地打断去执行时钟中断处理程序。
全局变量jiffies
用来记录系统启动以来产生的节拍总数。内核在启动时将该变量初始化为0,此后每产生一次时钟中断该值就+1。因为一秒内时钟中断的次数等于 HZ,所以jiffies
一秒内增加的值也就为 HZ。系统运行时间以秒为计,其值等于 jiffies/HZ。
jiffies
变量被定义为unsigned long
类型。在32位体系结构上是32位。如果时钟频率为 100HZ,那么 497 天后会溢出。如果是64位体系结构,可以放心地认为它不会溢出。
当jiffies
变量的值溢出后再继续增加的话,它会回绕至0。内核使用time_after
、time_after_eq
、time_before
和time_before_eq
宏来判断两个时间值的关系。
为了解决jiffies
变量的溢出问题,内核引入了jiffies_64
变量,这是一个64位的无符号整数。在64位架构上,这两个变量是一个值;但在32位架构上,对64位值的访问不是原子的。这意味着读取64位值的高32位和低32位时,变量的值可能会发生更新而导致获取错误的值。因此在32位架构上不能对64位的值直接读取,必须使用一个特殊的辅助函数get_jiffies_64
。
定时器的使用¶
一个定时器是用struct timer_list
结构体来表示的:
struct timer_list {
struct hlist_node entry;
unsigned long expires;
void (*function)(struct timer_list *);
u32 flags;
......
};
entry:所有的定时器会根据到期的时间被分配到一组链表中的一个
expires:定时器的到期时间,以jiffies为单位
function:定时器到期时要执行的毁掉函数
flags:标志位
函数 | 描述 |
---|---|
DEFINE_TIMER | 定义一个timer |
timer_setup | 初始化timer |
add_timer | 添加一个timer |
add_timer_on | 将timer添加到指定CPU上 |
mod_timer | 修改到期时间 |
del_timer | 停止timer |
del_timer_syn | 等待timer执行完毕再停止 |
延迟执行¶
内核代码往往需要推迟某些任务的执行,这种推迟通常发生在等待硬件完成某些工作,而且等待时间往往非常短。内核提供了多种延迟方法来处理延迟请求。
忙等待¶
最简单的延迟方法就是忙等待。该方法仅仅在想要延迟的时间是节拍的整数倍,或者精确率要求不高的情况下才可以使用。忙等待的实现非常简单——在一个循环中不断等待直到希望的时钟节拍数耗尽,比如:
该循环将不断执行,直到jiffies
大于delay为止。这是一种低效的办法, 因为处理器除了等待不会做任何事情。
让出处理器¶
忙等待大大浪费了系统的性能,一种解决办法是在不需要CPU的情况下主动释放:
超时执行¶
如果驱动程序使用等待队列来等待其他一些事件,而我们同时希望在特定时间段中运行,可以使用wait_event_timeout()
或者wait_event_interruptible_timeout()
函数。调用上述函数会使进程进入等待状态,直到某个条件为真:
long wait_event_timeout(wait_queue_head_t q, int condition, long timeout);
long wait_event_interruptible_timeout(wait_queue_head_t q, int condition, long timeout);
这两个函数的区别是,第一个函数不可中断,而第二个函数可以响应信号。两个函数都会在给定的等待队列上休眠,在超时到期时返回。如果有其他进程在等待队列上调用了wake_up()
函数,也会使得进程从等待队列中被唤醒。
对于无需等待特定事件而延迟的情况,可以使用schedule_timeout()
函数:
timeout是用jiffies
表示的延迟时间。schedule_timeout()
函数要求预先设置进程状态:set_current_state(TASK_INTERRUPTIBLE)
。
注意,由于schedule_timeout()
函数需要调用调度程序,所以调用它的代码必须保证能够睡眠。也就是调用函数必须位于进程上下文中,且不能持有锁。
当任务被重新调度时,将返回代码进入睡眠前的位置继续执行。如果任务提前被唤醒,那么定时器被撤销。
短延迟¶
https://www.kernel.org/doc/html/v6.6/timers/timers-howto.html
使用短延迟时需要注意自己的代码是否位于原子上下文中。
原子上下文:
必须使用*delay函数族,这些函数忙等待足够的循环周期以实现所需的延迟:
ndelay(unsigned long nsecs)
udelay(unsigned long usecs)
mdelay(unsigned long msecs)
首选udelay,非PC设备可能无法满足ns的精度。mdelay也不建议使用。
非原子上下文:
应该使用*sleep函数族,这些函数底层机制都不同:
- 由忙等待支持:
udelay(unsigned long usecs)
- 由高精度定时器支持:
usleep_range(unsigned long min, unsigned long max)
- 由jiffies支持:
msleep(unsigned long msecs)
休眠"几"微秒(< 10us):使用udelay
休眠~微秒或小毫秒(10us ~ 20ms):使用usleep_range
休眠更长毫秒(10ms+):使用msleep
灵活的休眠(任意延迟,不可中断):使用fsleep