延迟执行
设备驱动常常需要延后一段时间执行一个特定片段的代码, 常常允许硬件完成某个任务.
长延迟
有时,驱动需要延后执行相对长时间,长于一个时钟嘀哒。
忙等待(尽量别用)
若想延迟执行若干个时钟嘀哒,精度要求不高。最容易的( 尽管不推荐 ) 实现是一个监视 jiffy 计数器的循环。这种忙等待实现的代码如下:
while (time_before(jiffies, j1)) cpu_relax(); |
对 cpu_relex 的调用将以体系相关的方式执行,在许多系统中它根本不做任何事,这个方法应当明确地避免。对于ARM体系来说:
#define cpu_relax() barrier() |
也就是说在ARM上运行忙等待相当于:
while (time_before(jiffies, j1)) ; |
这种忙等待严重地降低了系统性能。如果未配置内核为抢占式, 这个循环在延时期间完全锁住了处理器,计算机直到时间 j1 到时会完全死掉。如果运行一个可抢占的内核时会改善一点,但是忙等待在可抢占系统中仍然是浪费资源的。更糟的是, 当进入循环时如果中断碰巧被禁止, jiffies 将不会被更新, 并且 while 条件永远保持真,运行一个抢占的内核也不会有帮助, 唯一的解决方法是重启。
让出处理器
忙等待加重了系统负载,必须找出一个更好的技术:不需要CPU时释放CPU 。 这可通过调用schedule函数实现(在 <linux/sched.h> 中声明):
while (time_before(jiffies, j1)) { schedule(); } |
在计算机空闲时运行空闲任务(进程号 0, 由于历史原因也称为swapper)可减轻处理器工作负载、降低温度、增加寿命。
超时
实现延迟的最好方法应该是让内核为我们完成相应的工作。
(1)若驱动使用一个等待队列来等待某些其他事件,并想确保它在一个特定时间段内运行,可使用:
#include <linux/wait.h> long wait_event_timeout(wait_queue_head_t q, condition, long timeout); long wait_event_interruptible_timeout(wait_queue_head_t q, condition, long timeout); /*这些函数在给定队列上睡眠, 但是它们在超时(以 jiffies 表示)到后返回。如果超时,函数返回 0; 如果这个进程被其他事件唤醒,则返回以 jiffies 表示的剩余的延迟实现;返回值从不会是负值*/ |
(2)为了实现进程在超时到期时被唤醒而又不等待特定事件(避免声明和使用一个多余的等待队列头),内核提供了 schedule_timeout 函数:
#include <linux/sched.h> signed long schedule_timeout(signed long timeout); /*timeout 是要延时的 jiffies 数。除非这个函数在给定的 timeout 流失前返回,否则返回值是 0 。schedule_timeout 要求调用者首先设置当前的进程状态。为获得一个不可中断的延迟, 可使用 TASK_UNINTERRUPTIBLE 代替。如果你忘记改变当前进程的状态, 调用 schedule_time 如同调用 shcedule,建立一个不用的定时器。一个典型调用如下:*/ set_current_state(TASK_INTERRUPTIBLE); schedule_timeout (delay); |
短延迟
当一个设备驱动需要处理硬件的延迟(latency潜伏期), 涉及到的延时通常最多几个毫秒,在这个情况下, 不应依靠时钟嘀哒,而是内核函数 ndelay, udelay和 mdelay ,他们分别延后执行指定的纳秒数, 微秒数或者毫秒数,定义在 <asm/delay.h>,原型如下:
#include <linux/delay.h> void ndelay(unsigned long nsecs); void udelay(unsigned long usecs); void mdelay(unsigned long msecs); |
重要的是记住这 3 个延时函数是忙等待; 其他任务在时间流失时不能运行。每个体系都实现 udelay, 但是其他的函数可能未定义; 如果它们没有定义, <linux/delay.h> 提供一个缺省的基于 udelay 的版本。在所有的情况中, 获得的延时至少是要求的值, 但可能更多。udelay 的实现使用一个软件循环, 它基于在启动时计算的处理器速度和使用整数变量 loos_per_jiffy确定循环次数。
为避免在循环计算中整数溢出, 传递给udelay 和 ndelay的值有一个上限,如果你的模块无法加载和显示一个未解决的符号:__bad_udelay, 这意味着你调用 udleay时使用太大的参数。
作为一个通用的规则:若试图延时几千纳秒, 应使用 udelay 而不是 ndelay; 类似地, 毫秒规模的延时应当使用 mdelay 完成而不是一个更细粒度的函数。
有另一个方法获得毫秒(和更长)延时而不用涉及到忙等待的方法是使用以下函数(在<linux/delay.h> 中声明):
void msleep(unsigned int millisecs); unsigned long msleep_interruptible(unsigned int millisecs); void ssleep(unsigned int seconds) |
若能够容忍比请求的更长的延时,应使用 schedule_timeout, msleep 或 ssleep。
内核定时器
当需要调度一个以后发生的动作, 而在到达该时间点时不阻塞当前进程, 则可使用内核定时器。内核定时器用来调度一个函数在将来一个特定的时间(基于时钟嘀哒)执行,从而可完成各类任务。
内核定时器是一个数据结构, 它告诉内核在一个用户定义的时间点使用用户定义的参数执行一个用户定义的函数,函数位于 <linux/timer.h> 和 kernel/timer.c 。被调度运行的函数几乎确定不会在注册它们的进程在运行时运行,而是异步运行。实际上, 内核定时器通常被作为一个"软件中断"的结果而实现。当在进程上下文之外(即在中断上下文)中运行程序时, 必须遵守下列规则:
(1)不允许访问用户空间;
(2)current 指针在原子态没有意义;
(3)不能进行睡眠或者调度. 例如:调用 kmalloc(..., GFP_KERNEL) 是非法的,信号量也不能使用因为它们可能睡眠。
通过调用函数 in_interrupt()能够告知是否它在中断上下文中运行,无需参数并如果处理器当前在中断上下文运行就返回非零。
通过调用函数 in_atomic()能够告知调度是否被禁止,若调度被禁止返回非零; 调度被禁止包含硬件和软件中断上下文以及任何持有自旋锁的时候。
在后一种情况, current 可能是有效的,但是访问用户空间是被禁止的,因为它能导致调度发生. 当使用 in_interrupt()时,都应考虑是否真正该使用的是 in_atomic 。他们都在 <asm/hardirq.h> 中声明。
内核定时器的另一个重要特性是任务可以注册它本身在后面时间重新运行,因为每个 timer_list 结构都会在运行前从激活的定时器链表中去连接,因此能够立即链入其他的链表。一个重新注册它自己的定时器一直运行在同一个 CPU.
即便在一个单处理器系统,定时器是一个潜在的态源,这是异步运行直接结果。因此任何被定时器函数访问的数据结构应当通过原子类型或自旋锁被保护,避免并发访问。
定时器 API
内核提供给驱动许多函数来声明、注册以及删除内核定时器:
#include <linux/timer.h> struct timer_list { struct list_head entry; unsigned long expires;/*期望定时器运行的绝对 jiffies 值,不是一个 jiffies_64 值,因为定时器不被期望在将来很久到时*/ void (*function)(unsigned long); /*期望调用的函数*/ unsigned long data;/*传递给函数的参数,若需要在参数中传递多个数据项,可以将它们捆绑成单个数据结构并且将它的指针强制转换为 unsiged long 的指针传入。这种做法在所有支持的体系上都是安全的并且在内存管理中相当普遍*/ struct tvec_t_base_s *base; #ifdef CONFIG_TIMER_STATS void *start_site; char start_comm[16]; int start_pid; #endif }; /*这个结构必须在使用前初始化,以保证所有的成员被正确建立(包括那些对调用者不透明的初始化):*/ void init_timer(struct timer_list *timer); struct timer_list TIMER_INITIALIZER(_function, _expires, _data); /*在初始化后和调用 add_timer 前,可以改变 3 个公共成员:expires、function和data*/ void add_timer(struct timer_list * timer); int del_timer(struct timer_list * timer);/*在到时前禁止一个已注册的定时器*/ int del_timer_sync(struct timer_list *timer); /*如同 del_timer ,但还保证当它返回时, 定时器函数不在任何 CPU 上运行,以避免在 SMP 系统上竞态, 并且在 单处理器内核中和 del_timer 相同。这个函数应当在大部分情况下优先考虑。 如果它被从非原子上下文调用, 这个函数可能睡眠,但是在其他情况下会忙等待。当持有锁时要小心调用 del_timer_sync ,如果这个定时器函数试图获得同一个锁, 系统会死锁。如果定时器函数重新注册自己, 调用者必须首先确保这个重新注册不会发生; 这通常通过设置一个" 关闭 "标志来实现, 这个标志被定时器函数检查*/ int mod_timer(struct timer_list *timer, unsigned long expires); /*更新一个定时器的超时时间, 常用于超时定时器。也可在正常使用 add_timer时在不活动的定时器上调用mod_timer*/ int timer_pending(const struct timer_list * timer); /*通过调用timer_list结构中一个不可见的成员,返回定时器是否在被调度运行*/
|
内核定时器的实现《LDD3》介绍的比较笼统,以后看《ULK3》的时候再细细研究。
一个内核定时器还远未完善,因为它受到 jitter 、硬件中断,还有其他定时器和其他异步任务的影响。虽然一个简单数字 I/O关联的定时器对简单任务是足够的,但不合适在工业环境中的生产系统,对于这样的任务,你将最可能需要实时内核扩展(RT-Linux).