度量时间差:时钟中断由系统定时硬件以周期性的间隔产生,这个间隔由内核根据 HZ 值来设定,HZ 是一个体系依赖的值,在 <linux/param.h>中定义或该文件包含的某个子平台相关文件中。作为通用的规则,即便如果知道 HZ 的值,在编程时应当不依赖这个特定值,而始终使用HZ。对于当前版本,我们应完全信任内核开发者,他们已经选择了最适合的HZ值,最好保持 HZ 的默认值。
每次当时钟中断发生时,内核内部计数器的值就增加一。这个计数器在系统引导时被初始化为0。这个计数器是一个 64-位 变量( 即便在 32-位的体系上)并且称为 “jiffies_64”。但是驱动通常访问 jiffies 变量(unsigned long)(根据体系结构的不同:可能是 jiffies_64 ,可能是jiffies_64 的低32位)。使用 jiffies 是首选,因为它访问更快,且无需在所有的体系上实现原子地访问 64-位的 jiffies_64 值。
对于ARM体系结构:在<linux/param.h>文件中的定义如下:
|
也就是说:HZ 由__KERNEL__和CONFIG_HZ决定。若未定义__KERNEL__,HZ为100;否则为CONFIG_HZ。而CONFIG_HZ是在内核的根目录的.config文件中定义,并没有在make menuconfig的配置选项中出现。Linux的/arch/arm/configs/s3c2410_defconfig文件中的定义为:
|
使用jiffies计数器
使用该计数器的函数包含在<linux/jiffie.h>中,但是通常只需包含<linux/sched.h>文件。jiffies和jiffies均应被看成是只读变量。当需要记录当前 jiffies 值(被声明为 volatile 避免编译器优化内存读)时,可以简单地访问这个 unsigned long 变量。在代码需要计算未来的时间戳时,必须读取当前的计数器,如下:
#include <linux/jiffies.h>
unsigned long j,stamp_1,stamp_half,stamp_n;
j = jiffies; //读取当前值
stamp_1 = j + HZ; // 未来一秒
stamp_half =j +HZ/2; //半秒
stamp_n = j+n*HZ/1000; //未来n毫秒
这里转载
一下吧,对于这部分内容 真不是特别感兴趣,了解下吧。
以下是一些简单的工具宏及其定义:
|
用户空间的时间表述法(struct timeval 和 struct timespec )与内核表述法的转换函数:
struct timespec { unsigned long timespec_to_jiffies(struct timespec *value); |
访问jiffies_64 对于 32-位 处理器不是原子的,这意味着如果这个变量在你正在读取它们时被更新你可能读到错误的值(在分别读取高32位和低32位时)。若需要访问jiffies_64,内核有一个特别的辅助函数,为你完成适当的锁定:
|
需要注意,实际的时钟频率对用户空间来讲几乎是不可见的。用户空间程序包含param.h,HZ宏始终被扩展为100。对用户来讲,如果想知道定时器中断的确切HZ值,可以通过将/proc/interrupts获得的计数值除以/proc/uptime文件报告的系统运行时间,即可获得确切的HZ值。
处理器特定的寄存器
现代巨大多数处理器都包含一个随始终周期不断递增的计数寄存器。这个时钟计数器是完成高分辨率计时任务的唯一可靠途径。最有名的计数器寄存器是 TSC ( timestamp counter), 在 x86 的 Pentium 处理器开始引入并在之后所有的 CPU 中出现(包括 x86_64 平台)。它是一个 64-位 寄存器,计数 CPU 的时钟周期,可从内核和用户空间读取。在包含了 <asm/msr.h> (一个 x86-特定的头文件, 它的名子代表"machine-specific registers")的代码中可使用这些宏:
rdtsc(low32,high32);/*原子地读取 64-位TSC 值到 2 个 32-位 变量*/
rdtscl(low32);/*读取TSC的低32位到一个 32-位 变量*/
rdtscll(var64);/*读 64-位TSC 值到一个 long long 变量*/
/*下面的代码行测量了指令自身的执行时间:*/
unsigned long ini, end;
rdtscl(ini); rdtscl(end);
printk("time lapse: %li/n", end - ini);
一些其他的平台提供相似的功能, 并且内核头文件提供一个体系无关的功能用来代替 rdtsc,称 get_cycles(定义在 <asm/timex.h>( 由 <linux/timex.h> 包含)),原型如下:
/*由于s3c2410系列处理器上没有时钟周期计数器所以get_cycles定义如下:*/ static inline cycles_t get_cycles (void) |
延迟执行:
长延迟
有时,驱动需要延后执行相对长时间,长于一个时钟嘀哒。
若想延迟执行若干个时钟嘀哒,精度要求不高。最容易的( 尽管不推荐 ) 实现是一个监视 jiffy 计数器的循环。这种忙等待实现的代码如下:
|
这种忙等待严重地降低了系统性能。如果未配置内核为抢占式, 这个循环在延时期间完全锁住了处理器,计算机直到时间 j1 到时会完全死掉。如果运行一个可抢占的内核时会改善一点,但是忙等待在可抢占系统中仍然是浪费资源的。更糟的是, 当进入循环时如果中断碰巧被禁止, jiffies 将不会被更新, 并且 while 条件永远保持真,运行一个抢占的内核也不会有帮助, 唯一的解决方法是重启。
超时
实现延迟的最好方法应该是让内核为我们完成相应的工作。
(1)若驱动使用一个等待队列来等待某些其他事件,并想确保它在一个特定时间段内运行,可使用:
|
(2)为了实现进程在超时到期时被唤醒而又不等待特定事件(避免声明和使用一个多余的等待队列头),内核提供了 schedule_timeout 函数:
/*timeout 是要延时的 jiffies 数。除非这个函数在给定的 timeout 流失前返回,否则返回值是 0 。schedule_timeout 要求调用者首先设置当前的进程状态。为获得一个不可中断的延迟, 可使用 TASK_UNINTERRUPTIBLE 代替。如果你忘记改变当前进程的状态, 调用 schedule_time 如同调用 shcedule,建立一个不用的定时器。一个典型调用如下:*/ |
短延迟
当一个设备驱动需要处理硬件的延迟(latency潜伏期), 涉及到的延时通常最多几个毫秒,在这个情况下, 不应依靠时钟嘀哒,而是内核函数 ndelay, udelay和 mdelay ,他们分别延后执行指定的纳秒数, 微秒数或者毫秒数,定义在 <asm/delay.h>,原型如下:
|
重要的是记住这 3 个延时函数是忙等待; 其他任务在时间流失时不能运行。每个体系都实现 udelay, 但是其他的函数可能未定义; 如果它们没有定义, <linux/delay.h> 提供一个缺省的基于 udelay 的版本。在所有的情况中, 获得的延时至少是要求的值, 但可能更多。udelay 的实现使用一个软件循环, 它基于在启动时计算的处理器速度和使用整数变量 loos_per_jiffy确定循环次数。
为避免在循环计算中整数溢出, 传递给udelay 和 ndelay的值有一个上限,如果你的模块无法加载和显示一个未解决的符号:__bad_udelay, 这意味着你调用 udleay时使用太大的参数。
作为一个通用的规则:若试图延时几千纳秒, 应使用 udelay 而不是 ndelay; 类似地, 毫秒规模的延时应当使用 mdelay 完成而不是一个更细粒度的函数。
有另一个方法获得毫秒(和更长)延时而不用涉及到忙等待的方法是使用以下函数(在<linux/delay.h> 中声明):
|
若能够容忍比请求的更长的延时,应使用 schedule_timeout, msleep 或 ssleep。
内核定时器
当需要调度一个以后发生的动作, 而在到达该时间点时不阻塞当前进程,
则可使用内核定时器。内核定时器用来调度一个函数在将来一个特定的时间(基于时钟嘀哒)执行,从而可完成各类任务。
内核定时器是一个数据结构,
它告诉内核在一个用户定义的时间点使用用户定义的参数执行一个用户定义的函数,函数位于 <linux/timer.h> 和 kernel/timer.c
。被调度运行的函数几乎确定不会在注册它们的进程在运行时运行,而是异步运行。实际上,
内核定时器通常被作为一个"软件中断"的结果而实现。许多动作需要在进程上下文才能执行,当在进程上下文之外(即在中断上下文)中运行程序时, 必须遵守下列规则:
(1)不允许访问用户空间。因为没有进程上下文们,无法将任何特定进程与用户空间关联起来。另外在用户空间可能会引起调度;
(2)current 指针在原子态没有意义;
(3)不能进行睡眠或者调度。原子代码不可以调用schedule或者wait_event,也不能调用任何引起休眠的函数。例如:调用 kmalloc(..., GFP_KERNEL) 是非法的,信号量也不能使用因为它们可能睡眠。
通过调用函数
in_interrupt()能够告知是否它在中断上下文中运行,无需参数并如果处理器当前在中断上下文运行就返回非零。
通过调用函数
in_atomic()能够告知调度是否被禁止,若调度被禁止返回非零; 调度被禁止包含硬件和软件中断上下文以及任何持有自旋锁的时候。
在后一种情况, current 可能是有效的,但是访问用户空间是被禁止的,因为它能导致调度发生. 当使用
in_interrupt()时,都应考虑是否真正该使用的是 in_atomic 。他们都在 <asm/hardirq.h>
中声明。
内核定时器的另一个重要特性是任务可以注册它本身在后面时间重新运行,因为每个 timer_list
结构都会在运行前从激活的定时器链表中去连接,因此能够立即链入其他的链表。一个重新注册它自己的定时器一直运行在同一个 CPU.
即便在一个单处理器系统,定时器是一个潜在的竟态源,这是异步运行直接结果。因此任何被定时器函数访问的数据结构应当通过原子类型或自旋锁被保护,避免并发访问。
定时器 API
内核提供给驱动许多函数来声明、注册以及删除内核定时器:
|
这里给出书上的一段代码:
unsigned long j = jiffies;
为定时器函数填充数据
data->prevjiffies=j;
data->buf=buf2;
data->loops=JIT_ASYNC_LOOPS;
data->timer.data = (unsigned long)data;
data->timer.function=jit_timer_fn;
data->tmer.expire=j+tdelay;
add_timer(&data->timer);
wait_event_interruptible(data->wait,!data->loops);
内核定时器的实现:
1.内核定时器管理必须做到轻量级。
2.其设计必须在活动定时器大量增加时具有很大的伸缩性。
3.大部分定时器会在最多几秒或者几分钟到达,而很少存在长期延迟的定时器。
4.定时器应该在注册它的统一CPU上运行。
内核开发者定义了一个数据结构利用:per-CPU数据结构。timer_list结构的base字段包含了指向该结构的指针。base为NULL,表示尚未调度进行,否则该结构会告诉我么哪个CPU在运行定时器。
tasklet
tasklet类型与内核定时器:他们始终在中断期间运行,始终会在调度他们的同一CPU上运行,而且都接受一个unsigned long参数。和内核定时器不同的是,我们不能要求tasklet在某个给定时间执行。调度一个tasklet,表明我们只是希望内核选择某个其后的时间来执行给定的函数。这种机制对于中断来说尤为重要。硬件中断必须尽快处理, 但大部分的数据管理可以延后到以后安全的时间执行。 实际上, 一个 tasket, 就象一个内核定时器,
在一个"软中断"的上下文中执行(以原子模式)。软件中断是在使能硬件中断时执行异步任务的一个内核机制。
tasklet的初始化:
#include <linux/interrupt.h>
struct tasklet_struct
{
struct tasklet_struct *next;
unsigned long state;
atomic_t count;
void (*func)(unsigned long);
unsigned long data;
}
void tasklet_init(struct tasklet_struct *t,void (*func)(unsigned long),unsigned long data);
#define DECLEAR_TASKLET(name,func,data) /
struct tasklet_struct name = {
NULL, 0,
ATOMIC_INIT(0),
func, data }
#define DECLEAR_TASKLET_DISABLED(name,func,data) /
struct tasklet_struct name = {NULL,0,ATOMIC_INIT(1),func,data}
void tasklet_disable(struct tasklet_struct *t);
/*函数暂时禁止给定的 tasklet被 tasklet_schedule
调度,直到这个 tasklet 被再次被enable;若这个 tasklet 当前在运行,
这个函数忙等待直到这个tasklet退出*/
void
tasklet_disable_nosync(struct tasklet_struct *t);
/*禁用指定的tasklet,但不会等待任何正在运行的tasklet推出。该函数返回后,tasklet是禁用的,而且在重新启用之前,不会再次被调度。但是当该函数返回时,指定的tasklet可能仍在其他的CPU上运行。*/
void
tasklet_enable(struct tasklet_struct *t);
/*使能一个之前被disable的 tasklet;若这个 tasklet
已经被调度, 它会很快运行。 tasklet_enable 和tasklet_disable必须匹配调用, 因为内核跟踪每个 tasklet
的"禁止次数"*/
void
tasklet_schedule(struct tasklet_struct *t);
/*调度 tasklet 执行,如果tasklet在运行中被调度,
它在完成后会再次运行; 这保证了在其他事件被处理当中发生的事件受到应有的注意. 这个做法也允许一个 tasklet
重新调度它自己*/
void
tasklet_hi_schedule(struct tasklet_struct *t);
/*和tasklet_schedule类似,只是在更高优先级执行。当软中断处理运行时,
它处理高优先级 tasklet 在其他软中断之前,只有具有低响应周期要求的驱动才应使用这个函数,
可避免其他软件中断处理引入的附加周期*/
void
tasklet_kill(struct tasklet_struct *t);
/*确保了 tasklet
不会被再次调度来运行,通常当一个设备正被关闭或者模块卸载时被调用。如果 tasklet 正在运行, 这个函数等待直到它执行完毕。若 tasklet
重新调度它自己,则必须阻止在调用 tasklet_kill 前它重新调度它自己,也即先确保重新调度完成,才能调用tasklet_kill,如同使用 del_timer_sync*/
工作队列:
工作队列类似 taskets,允许内核代码请求在将来某个时间调用一个函数,不同在于:
1.tasklet在软件中断上下文中运行,因此所有的tasklet必须是原子的。相反工作队列函数在一个特殊内核进程的上下文中进行,工作队列可以休眠。
2.tasklet始终运行在被初始提交的同一处理器上,但这只是工作队列的默认方式。
3.内核代码可以请求队列函数的执行延迟给定的时间间隔。
关键区别在于:tasklet会在很短的时间段内很快执行,并且以原子的方式,而工作队列函数可具有更长的延迟并且不必原子化。
工作队列的类型为struct workqueue_struct,定义在<linux/workqueue.h>。在使用之前,必须显示地创建一个工作队列:
struct workqueue_struct *create_workqueue(const char *name);
struct workqueue_struct *create_singlethread_workqueue(const char *name);
每个工作队列有一个或多个专用的进程("内核线程"), 这些进程运行提交给这个队列的函数。 若使用
create_workqueue, 就得到一个工作队列它在系统的每个处理器上有一个专用的线程。在很多情况下,过多线程对系统性能有影响,如果单个线程就足够则使用
create_singlethread_workqueue 来创建工作队列。
要向一个工作队列提交一个任务,需要填充一个word_struct结构(上述只是定义wordqueue工作队列)。要使工作队列进行工作,需要将work_struct添加到workqueue_struct。
struct delayed_work { DECLARE_WORK(n, /*若在运行时需要建立 /*有 2 |
在将来的某个时间, 这个工作函数(封装在work_struct中的函数)将被传入给定的 data 值来调用。这个函数将在工作线程的上下文运行, 因此它可以睡眠
(你应当知道这个睡眠可能影响提交给同一个工作队列的其他任务) 工作函数不能访问用户空间,因为它在一个内核线程中运行, 完全没有对应的用户空间来访问。
取消一个挂起的工作队列入口项可以调用:
int cancel_delayed_work(struct delayed_work *work)
void cancel_work_sync(struct work_struct *work);
如果入口项在开始执行前被取消,则上述函数返回非零值。在调用cancel_delayed_work之后,内核会确保不会初始化给定入口项的执行。但是如果cancel_delayed_work返回0,则说明该入口项已经在其他处理器上执行了,因此cancel_delayed_work函数返回后,入口项仍在进行,为了确保在cancel_delayed_work返回0之后,工作函数不会在系统的任何地方运行,则应该随后调用以下函数:
void flush_workqueue(struct workqueue_struct *queue);
flush_workqueue返回后,任何在调用之前被提交的工作函数都不会在系统任何地方运行。
而cancel_work_sync会取消相应的work,但是如果这个work已经在运行那么cancel_work_sync会阻塞,直到work完成并取消相应的work。
当用完一个工作队列,可以去掉它,使用:
|
共享队列
使用的过程
1.建立共享队列:
static struct work_struct jiq_work;
static struct delayed_struct jiq_work_delay;
INIT_WORK(&jiq_work,jiq_print_wq);
INIT_DELAYED_WORK(&jiq_work_delay,jiq_print_wq);
2.提交工作
int schedule_work(&jiq_work);
int schedule_delayed_work(&jiq_work_delay,delay);
若需取消一个已提交给工作队列入口项, 可以使用 cancel_delayed_work和cancel_work_sync,
但刷新共享队列需要一个特殊的函数:
|
因为不知道谁可能使用这个队列,因此不可能知道 flush_schduled_work 返回需要多长时间。