【原创】《Linux设备驱动程序》学习之循序渐进 --- 时间、延迟及延缓操作


【原创】《Linux设备驱动程序》学习之循序渐进 --- 时间、延迟及延缓操作


第七章 --- 时间、延迟及延缓操作

度量时间差

定时器中断由系统定时硬件以规律地间隔产生; 这个间隔在启动时由内核根据 HZ 值来编程, HZ 是一个体系依赖的值, 在 <linux/param.h>中定义或者它所包含的一个子平台文件中.作为一个通用的规则, 即便如果你知道 HZ 的值, 在编程时你应当从不依赖这个特定值. 

如果想改变系统时钟中断发生的频率,可以通过修改HZ来进行。

每次发生一个时钟中断, 一个内核计数器的值递增. 这个计数器在系统启动时初始化为 0, 因此它代表从最后一次启动以来的时钟嘀哒的数目. 这个计数器是一个 64-位 变量( 即便在 32-位的体系上)并且称为 jiffies_64. 但是, 驱动编写者正常地存取 jiffies 变量, 一个 unsigned long, 或者和 jiffies_64 是同一个或者它的低有效位. 使用 jiffies 常常是首选, 因为它更快, 并且再所有的体系上存取 64-位的 jiffies_64 值不必要是原子的.  

使用 jiffies 计数器 

这个计数器和来读取它的实用函数位于 <linux/jiffies.h>, 尽管你会常常只是包含 <linux/sched.h>, 它会自动地将 jiffies.h 拉进来. 不用说, jiffies 和 jiffies_64 必须当作只读的. 

无论何时你的代码需要记住当前的 jiffies 值, 可以简单地存取这个 unsigned long 变量, 它被声明做 volatile 来告知编译器不要优化内存读. 你需要读取当前的计数器, 无论何时你的代码需要计算一个将来的时间戳, 如下面例子所示: 

#include <linux/jiffies.h> 
unsigned long j, stamp_1, stamp_half, stamp_n; 
 
j = jiffies; /* read the current value */ 
stamp_1 = j + HZ; /* 1 second in the future */ 
stamp_half = j + HZ/2; /* half a second */ 
stamp_n = j + n * HZ / 1000; /* n milliseconds */ 
为比较你的被缓存的值( 象上面的 stamp_1 ) 和当前值, 你应当使用下面一个宏定义:  
#include <linux/jiffies.h> 
int time_after(unsigned long a, unsigned long b); 
int time_before(unsigned long a, unsigned long b); 
int time_after_eq(unsigned long a, unsigned long b); 
int time_before_eq(unsigned long a, unsigned long b);
如果你需要以一种安全的方式知道 2 个 jiffies 实例之间的差, 你可以使用同样的技巧: diff = (long)t2 - (long)t1;. 
你可以转换一个 jiffies 差为毫秒, 一般地通过: msec = diff * 1000 / HZ; 

有时, 你需要与用户空间程序交换时间表示, 它们打算使用 struct timeval 和 struct timespec 来表示时间.

内核输出 4 个帮助函数来转换以 jiffies 表达的时间值, 到和从这些结构:  
#include <linux/time.h>  
unsigned long timespec_to_jiffies(struct timespec *value); 
void jiffies_to_timespec(unsigned long jiffies, struct timespec *value); 
unsigned long timeval_to_jiffies(struct timeval *value); 
void jiffies_to_timeval(unsigned long jiffies, struct timeval *value);

存取这个 64-位 jiffy 计数值不象存取 jiffies 那样直接. 而在 64-位 计算机体系上, 这 2 个变量实际上是一个, 存取这个值对于 32-位 处理器不是原子的. 这意味着你可能读到错误的值如果这个变量的两半在你正在读取它们时被更新. 极不可能你会需要读取这个 64-位 计数器, 但是万一你需要, 你会高兴地得知内核输出了一个特别地帮助函数, 为你完成正确地加锁:  
#include <linux/jiffies.h>  
u64 get_jiffies_64(void);

对 HZ 值的用户可用的唯一证据是时钟中断多快发生, 如在 /proc/interrupts 所显示的. 例如, 你可以获得 HZ, 通过用在 /proc/uptime 中报告的系统 uptime 除这个计数值. 

处理器特定的寄存器 

作为回应, CPU 制造商引入一个方法来计数时钟周期, 作为一个容易并且可靠的方法来测量时间流失. 因此, 大部分现代处理器包含一个计数器寄存器, 它在每个时钟周期固定地递增一次. 现在, 资格时钟计数器是唯一可靠的方法来进行高精度的时间管理任务.

无论是否寄存器可以被清零, 我们强烈不鼓励复位它, 即便当硬件允许时. 毕竟, 在任何给定时间你可能不是这个计数器的唯一用户; 在一些支持 SMP 的平台上, 例如, 内核依赖这样一个计数器来在处理器之间同步. 因为你可以一直测量各个值的差, 只要差没有超过溢出时间, 你可以通过修改它的当前值来做这个事情不用声明独自拥有这个寄存器. 

获知当前时间 

内核代码能一直获取一个当前时间的表示, 通过查看 jifies 的值. 常常地, 这个值只代表从最后一次启动以来的时间, 这个事实对驱动来说无关, 因为它的生命周期受限于系统的 uptime. 如所示, 驱动可以使用 jiffies 的当前值来计算事件之间的时间间隔(例如, 在输入驱动中从单击中区分双击或者计算超时). 简单地讲, 查看 jiffies 几乎一直是足够的, 当你需要测量时间间隔. 如果你需要对短时间流失的非常精确的测量, 处理器特定的寄存器来帮忙了( 尽管它们带来严重的移植性问题 ). 

 有一个内核函数转变一个墙上时钟时间到一个 jiffies 值:  
#include <linux/time.h>  
unsigned long mktime (unsigned int year, unsigned int mon, 
 unsigned int day, unsigned int hour, 
 unsigned int min, unsigned int sec);  

虽然你不会一定处理人可读的时间表示, 有时你需要甚至在内核空间中处理绝对时间. 为此, <linux/time.h> 输出了 do_gettimeofday 函数.

延迟执行

一件要考虑的重要的事情是你需要的延时如何与时钟嘀哒比较, 考虑到 HZ 的跨各种平台的范围. 那种可靠地比时钟嘀哒长并且不会受损于它的粗粒度的延时, 可以利用系统时钟. 每个短延时典型地必须使用软件循环来实现. 在这 2 种情况中存在一个灰色地带. 在本章, 我们使用短语" long " 延时来指一个多 jiffy 延时, 在一些平台上它可以如同几个毫秒一样少, 但是在 CPU 和内核看来仍然是长的. 

忙等待

如果你想延时执行多个时钟嘀哒, 允许在值中某些疏忽, 最容易的( 尽管不推荐 ) 的实现是一个监视 jiffy 计数器的循环. 

更坏的是, 当你进入循环时如果中断碰巧被禁止, jiffies 将不会被更新, 并且 while 条件永远保持真. 运行一个抢占的内核也不会有帮助, 并且你将被迫去击打大红按钮.

让出处理器 

如我们已见到的, 忙等待强加了一个重负载给系统总体; 我们乐意找出一个更好的技术. 想到的第一个改变是明确地释放 CPU 当我们对其不感兴趣时. 这是通过调用调度函数而实现地, 在 <linux/sched.h> 中声明:  
while (time_before(jiffies, j1)) {     schedule(); }

超时 

到目前为止所展示的次优化的延时循环通过查看 jiffy 计数器而不告诉任何人来工作. 但是最好的实现一个延时的方法, 如你可能猜想的, 常常是请求内核为你做. 有 2 种方法来建立一个基于 jiffy 的超时, 依赖于是否你的驱动在等待其他的事件. 
如果你的驱动使用一个等待队列来等待某些其他事件, 但是你也想确保它在一个确定时间段内运行, 可以使用 wait_event_timeout 或者 wait_event_interruptible_timeout:  
#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 表示)到后返回. 因注意超时值表示要等待的 jiffies 数, 不是一个绝对时间值. 此, 它们实现一个限定的睡眠不会一直睡下去.

短延时

内核函数 ndelay, udelay, 以及 mdelay 对于短延时好用, 分别延后执行指定的纳秒数, 微秒数或者毫秒数. 它们的原型是:  
#include <linux/delay.h> 
void ndelay(unsigned long nsecs); 
void udelay(unsigned long usecs); 
void mdelay(unsigned long msecs); 

这些函数的实际实现在 <asm/delay.h>, 是体系特定的, 并且有时建立在一个外部函数上. 每个体系都实现 udelay, 但是其他的函数可能或者不可能定义; 如果它们没有定义, <linux/delay.h> 提供一个缺省的基于 udelay 的版本. 

重要的是记住这 3 个延时函数是忙等待; 其他任务在时间流失时不能运行. 因此, 它们重复, 尽管在一个不同的规模上, jitbusy 的做法. 因此, 这些函数应当只用在没有实用的替代时. 有另一个方法获得毫秒(和更长)延时而不用涉及到忙等待. 文件 <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 并且在"内核定时器"一节中详细介绍. 

为能够被执行, 多个动作需要进程上下文. 当你在进程上下文之外(即, 在中断上下文), 你必须遵守下列规则: 
  没有允许存取用户空间. 因为没有进程上下文, 没有和任何特定进程相关联的到用户空间的途径. 
  这个 current 指针在原子态没有意义, 并且不能使用因为相关的代码没有和已被中断的进程的联系. 
  不能进行睡眠或者调度. 原子代码不能调用 schedule 或者某种 wait_event, 也不能调用任何其他可能睡眠的函数. 例如, 调用 kmalloc(..., GFP_KERNEL) 是违犯规则的. 旗标也必须不能使用因为它们可能睡眠. 
内核代码能够告知是否它在中断上下文中运行, 通过调用函数 in_interrupt(), 它不要参数并且如果处理器当前在中断上下文运行就返回非零, 要么硬件中断要么软件中断. 

无论何时你使用 in_interrupt(), 你应当真正考虑是否 in_atomic 是你实际想要的. 2 个函数都在 <asm/hardirq.h> 中声明. 

也值得了解在一个 SMP 系统, 定时器函数被注册时相同的 CPU 来执行, 为在任何可能的时候获得更好的缓存局部特性. 因此, 一个重新注册它自己的定时器一直运行在同一个 CPU. 
不应当被忘记的定时器的一个重要特性是, 它们是一个潜在的竞争条件的源, 即便在一个单处理器系统. 这是它们与其他代码异步运行的一个直接结果. 因此, 任何被定时器函数存取的数据结构应当保护避免并发存取, 要么通过原子类型( 在第 5 章的"原子变量"一节) 要么使用自旋锁( 在第 5 章讨论 ). 

内核定时器的实现

定时器的实现被设计来符合下列要求和假设: 
  定时器管理必须尽可能简化. 
  设计应当随着激活的定时器数目上升而很好地适应. 
  大部分定时器在几秒或最多几分钟内到时, 而带有长延时的定时器是相当少见. 
  一个定时器应当在注册它的同一个 CPU 上运行. 

Tasklets 机制 

另一个有关于定时问题的内核设施是 tasklet 机制. 它大部分用在中断管理(我们将在第 10 章再次见到). 

一个 tasklet 存在为一个时间结构, 它必须在使用前被初始化. 初始化能够通过调用一个特定函数或者通过使用某些宏定义声明结构: 

#include <linux/interrupt.h>  
struct tasklet_struct { 
 /* ... */ 
 
void (*func)(unsigned long); 
 unsigned long data; 
}; 
void tasklet_init(struct tasklet_struct *t, 
 void (*func)(unsigned long), unsigned long data); 
DECLARE_TASKLET(name, func, data); 
DECLARE_TASKLET_DISABLED(name, func, data);
tasklet 在 kernel/softirq.c 中实现.

工作队列 

工作队列是, 表面上看, 类似于 taskets; 它们允许内核代码来请求在将来某个时间调用一个函数. 
两者之间关键的不同是 tasklet 执行的很快, 短时期, 并且在原子态, 而工作队列函数可能有高周期但是不需要是原子的. 每个机制有它适合的情形. 
工作队列有一个 struct workqueue_struct 类型, 在 <linux/workqueue.h> 中定义. 一个工作队列必须明确的在使用前创建, 使用一个下列的 2 个函数:  
struct workqueue_struct *create_workqueue(const char *name); 
struct workqueue_struct *create_singlethread_workqueue(const char *name); 

如果你使用 create_workqueue, 你得到一个工作队列它有一个专用的线程在系统的每个处理器上. 在很多情况下, 所有这些线程是简单的过度行为; 如果一个单个工作者线程就足够, 使用 create_singlethread_workqueue 来代替创建工作队列。

共享队列 

一个设备驱动, 在许多情况下, 不需要它自己的工作队列. 如果你只偶尔提交任务给队列, 简单地使用内核提供的共享的, 缺省的队列可能更有效.

快速参考

计时

#include <linux/param.h> 
HZ  
HZ 符号指定了每秒产生的时钟嘀哒的数目. 
#include <linux/jiffies.h> 
volatile unsigned long jiffies; 
u64 jiffies_64; 
jiffies_64 变量每个时钟嘀哒时被递增; 因此, 它是每秒递增 HZ 次. 内核代码几乎常常引用 jiffies, 它在 64-位平台和 jiffies_64 相同并
且在 32-位平台是它低有效的一半. 
int time_after(unsigned long a, unsigned long b); 
int time_before(unsigned long a, unsigned long b); 
int time_after_eq(unsigned long a, unsigned long b); 
int time_before_eq(unsigned long a, unsigned long b); 
这些布尔表达式以一种安全的方式比较 jiffies, 没有万一计数器溢出的问题和不需要存取 jiffies_64. 
u64 get_jiffies_64(void); 
获取 jiffies_64 而没有竞争条件. 
#include <linux/time.h> 
unsigned long timespec_to_jiffies(struct timespec *value); 
void jiffies_to_timespec(unsigned long jiffies, struct timespec *value); 
unsigned long timeval_to_jiffies(struct timeval *value); 
void jiffies_to_timeval(unsigned long jiffies, struct timeval *value); 
在 jiffies 和其他表示之间转换时间表示.

#include <asm/msr.h> 
rdtsc(low32,high32); 
rdtscl(low32); 
rdtscll(var32); 
x86-特定的宏定义来读取时戳计数器. 它们作为 2 半 32-位来读取, 只读低一半, 或者全部读到一个 long long 变量. 
#include <linux/timex.h> 
cycles_t get_cycles(void); 
以平台独立的方式返回时戳计数器. 如果 CPU 没提供时戳特性, 返回 0. 
#include <linux/time.h> 
unsigned long mktime(year, mon, day, h, m, s); 
返回自 Epoch 以来的秒数, 基于 6 个 unsigned int 参数. 
void do_gettimeofday(struct timeval *tv); 
返回当前时间, 作为自 Epoch 以来的秒数和微秒数, 用硬件能提供的最好的精度. 在大部分的平台这个解决方法是一个微秒或者更好, 尽管一些平台只提供 jiffies 精度. 
struct timespec current_kernel_time(void); 
返回当前时间, 以一个 jiffy 的精度. 

延迟

#include <linux/wait.h> 
long wait_event_interruptible_timeout(wait_queue_head_t *q, condition, signed long timeout); 
使当前进程在等待队列进入睡眠, 安装一个以 jiffies 表达的超时值. 使用 schedule_timeout( 下面) 给不可中断睡眠. 
#include <linux/sched.h> 
signed long schedule_timeout(signed long timeout); 
调用调度器, 在确保当前进程在超时到的时候被唤醒后. 调用者首先必须调用 set_curret_state 来使自己进入一个可中断的或者不可中断的睡眠状态. 
#include <linux/delay.h> 
void ndelay(unsigned long nsecs); 
void udelay(unsigned long usecs); 

void mdelay(unsigned long msecs); 
引入一个整数纳秒, 微秒和毫秒的延迟. 获得的延迟至少是请求的值, 但是可能更多. 每个函数的参数必须不超过一个平台特定的限制(常常是几千). 
void msleep(unsigned int millisecs); 
unsigned long msleep_interruptible(unsigned int millisecs); 
void ssleep(unsigned int seconds); 
使进程进入睡眠给定的毫秒数(或者秒, 如果使 ssleep). 

内核定时器 

#include <asm/hardirq.h> 
int in_interrupt(void); 
int in_atomic(void); 
返回一个布尔值告知是否调用代码在中断上下文或者原子上下文执行. 中断上下文是在一个进程上下文之外, 或者在硬件或者软件中断处理中. 原子上下文是当你不能调度一个中断上下文或者一个持有一个自旋锁的进程的上下文. 
#include <linux/timer.h> 
void init_timer(struct timer_list * timer); 
struct timer_list TIMER_INITIALIZER(_function, _expires, _data); 
这个函数和静态的定时器结构的声明是初始化一个 timer_list 数据结构的 2 个方法. 
void add_timer(struct timer_list * timer); 
注册定时器结构来在当前 CPU 上运行. 
int mod_timer(struct timer_list *timer, unsigned long expires); 
改变一个已经被调度的定时器结构的超时时间. 它也能作为一个 add_timer 的替代. 
int timer_pending(struct timer_list * timer); 
宏定义, 返回一个布尔值说明是否这个定时器结构已经被注册运行. 
void del_timer(struct timer_list * timer); 
void del_timer_sync(struct timer_list * timer); 

从激活的定时器链表中去除一个定时器. 后者保证这定时器当前没有在另一个 CPU 上运行. 

Tasklets 机制 

#include <linux/interrupt.h> 
DECLARE_TASKLET(name, func, data); 
DECLARE_TASKLET_DISABLED(name, func, data); 
void tasklet_init(struct tasklet_struct *t, void (*func)(unsigned long), unsigned long data); 
前 2 个宏定义声明一个 tasklet 结构, 而 tasklet_init 函数初始化一个已经通过分配或其他方式获得的 tasklet 结构. 第 2 个 DECLARE 宏标识这个 tasklet 为禁止的. 
void tasklet_disable(struct tasklet_struct *t); 
void tasklet_disable_nosync(struct tasklet_struct *t); 
void tasklet_enable(struct tasklet_struct *t); 
禁止和使能一个 tasklet. 每个禁止必须配对一个使能( 你可以禁止这个 tasklet 即便它已经被禁止). 函数 tasklet_disable 等待 tasklet 终
止如果它在另一个 CPU 上运行. 这个非同步版本不采用这个额外的步骤. 
void tasklet_schedule(struct tasklet_struct *t); 
void tasklet_hi_schedule(struct tasklet_struct *t); 
调度一个 tasklet 运行, 或者作为一个"正常" tasklet 或者一个高优先级的. 当软中断被执行, 高优先级 tasklets 被首先处理, 而正常 
tasklet 最后执行. 
void tasklet_kill(struct tasklet_struct *t); 
从激活的链表中去掉 tasklet, 如果它被调度执行. 如同 tasklet_disable, 这个函数可能在 SMP 系统中阻塞等待 tasklet 终止, 如果它当前在另一个 CPU 上运行. 

工作队列 

#include <linux/workqueue.h> 
struct workqueue_struct; 
struct work_struct; 
这些结构分别表示一个工作队列和一个工作入口. 
struct workqueue_struct *create_workqueue(const char *name); 
struct workqueue_struct *create_singlethread_workqueue(const char *name); 
void destroy_workqueue(struct workqueue_struct *queue); 

创建和销毁工作队列的函数. 一个对 create_workqueue 的调用创建一个有一个工作者线程在系统中每个处理器上的队列; 相反, 
create_singlethread_workqueue 创建一个有一个单个工作者进程的工作队列. 
DECLARE_WORK(name, void (*function)(void *), void *data); 
INIT_WORK(struct work_struct *work, void (*function)(void *), void *data); 
PREPARE_WORK(struct work_struct *work, void (*function)(void *), void *data); 
声明和初始化工作队列入口的宏. 
int queue_work(struct workqueue_struct *queue, struct work_struct *work); 
int  queue_delayed_work(struct  workqueue_struct  *queue,  struct  work_struct  *work,  unsigned 
long delay); 
从一个工作队列对工作进行排队执行的函数. 
int cancel_delayed_work(struct work_struct *work); 
void flush_workqueue(struct workqueue_struct *queue); 
使用 cancel_delayed_work 来从一个工作队列中去除入口; flush_workqueue 确保没有工作队列入口在系统中任何地方运行. 
int schedule_work(struct work_struct *work); 
int schedule_delayed_work(struct work_struct *work, unsigned long delay); 
void flush_scheduled_work(void); 
使用共享队列的函数.

原文链接:

posted @ 2014-07-12 20:42  GengLUT  阅读(281)  评论(0编辑  收藏  举报