Linux中断和定时器
备注:本文主要摘录自《Linux设备驱动开发详解》一书。
导读:
本章主要讲解Linux设备驱动编程中的中断和定时器处理。由于中断服务程序的执行并不存在于进程上下文中,所以要求中断服务程序的时间要尽量短。因此,Linux在中断处理中引入了顶半部和低半部分离的机制。另外,内核对时钟的处理也采用中断方式,而内核软件定时器最终依赖于时钟中断。
所谓中断是指CPU在执行程序的过程中,出现了某些突发事件急待处理,CPU必须暂停当前程序的执行,转去处理突发事件,处理完毕后又返回原程序被中断的位置继续执行。
根据中断的来源,中断可分为内部中断和外部中断,内部中断的中断源来自CPU内部(软件中断指令、溢出、除法等等),外部中断的中断源来自CPU外部,由外设提出请求。
根据中断是否可以屏蔽,中断可分为可屏蔽中断和不可屏蔽中断(NMI),可屏蔽中断可以通过中断控制寄存器等方法被屏蔽,屏蔽后,该中断不再得到响应,而不可屏蔽中断不能被屏蔽。
根据中断入口跳转方法的不同,中断可以分为向量中断和非向量中断。采用向量中断的CPU通常为不同的中断分配不通过的中断号,当检测到某个中断号的中断到来后,就自动跳转到与该中断对应的地址执行。不同中断号的中断有不同的入口地址。非向量中断的多个中断共享一个入口地址,进入该入口地址后,再通过软件判断中断标志来识别具体是哪个中断。也就是说,向量中断由硬件提供中断服务程序入口地址,非向量中断由软件提供中断服务程序入口地址。
一个典型的非向量中断服务程序代码清单如下所示,它先判断中断源,然后调用不同中断源的中断服务程序。
irq_handler()
{
...
int int_src = read_int_status(); /* 读硬件的中断相关寄存器 */
switch (int_src) { /* 判断中断源 */
case DEV_A:
dev_a_handler();
break;
case DEV_B:
dev_b_handler();
break;
...
default:
break;
}
}
定时器在硬件上也依赖中断来实现,如下图所示典型的嵌入式微处理器内部可编程间隔定时器的工作原理,它接收一个时钟输入,当时钟脉冲到来时,将目前计数值增加1并与预先设置的计数值(计数目标)比较,如相等,证明计数周期满,并产生定时器中断且复位目前计数值。
在ARM多核处理器里最常用的中断控制器时GIC(Generic Interrupt Controller),如下图所示,它支持3种类型的中断。
SGI(Software Generated Interrupt):软件产生的中断,可以用于多核的核间通信,一个CPU可以通过写GIC的寄存器给另一个CPU产生中断。多核调度用的IPI_WAKEUP、IPI_TIMER、IPI_RESCHEDULE、IPI_CALL_FUNC、IPI_CALL_FUNC_SINGLE、IPI_CPU_STOP都是由CPU产生的。
PPI(Private Peripheral Interrupt):某个CPU私有外设的中断,这类外设的中断只能发给绑定的那个CPU。
SPI(Shared Peripheral Interrupt):共享外设的中断,这类外设的中断可以路由到任何一个CPU。
对于SPI类型的中断,内核可以通过如下API设定中断触发的CPU核:
extern int irq_set_affinity (unsigned int irq, const struct cpumask *m);
在ARM Linux默认情况下,中断都是CPU0产生的,比如,我们可以通过如下代码把中断irq设定到CPU i上去:
irq_set_affinity(irq, cpumask_of(i));
中断处理程序架构:
设备的中断会打断内核进程中的正常调度和运行,系统对更高吞吐率的追求势必要求中断服务程序尽量短小精悍。但是,这个良好的愿望往往与现实并不吻合。在大多数真实的系统当中,当中断到来时,要完成的工作往往并不会是短小的,它可能要进行较大量的耗时处理。
下图描述了Linux内核的中断处理机制。为了在中断执行时间尽量短和中断处理需要完成的工作量大之间找到一个平衡点,Linux将中断处理程序分解为两个半部分:顶半部和低半部。
顶半部用于完成尽量少的比较紧急的功能,它往往只是简单地读取寄存器中的中断状态,并在清除中断标志后就进行“登记中断”的工作。“登记中断”意味着将底半部处理程序挂到该设备的底半部执行队列中去。这样,顶半部执行的速度就会很快,从而可以服务更多的中断请求。
现在,中断处理工作的重心就落在了底半部的头上,需要用它来完成中断事件的绝大多数任务。底半部几乎做了中断处理程序所有的事情,而且可以被新的中断打断,这也是底半部和顶半部的最大不同,因为顶半部往往被设计成不可中断。底半部相对来说并不是非常紧急的,而且相对比较耗时,不在硬件中断服务程序中执行。
尽管顶半部、底半部的结合能够改善系统的响应能力,但是,僵化地认为Linux设备驱动中的中断处理一定要分为两个半部则是不对的。如果中断要处理的工作本身很少,则完quan可以直接在顶半部quan部完成。
在Linux中,查看/proc/interrupts文件可以获得系统中中断的统计信息,并能统计出每一个中断号上的中断在每个CPU上发生的次数,具体如下图所示。
Linux中断编程:
申请和释放中断
在Linux设备驱动中,使用中断的设备需要申请和释放对于的中断,并分别使用内核提供的request_irq()和free_irq()函数。
1、申请irq
int request_irq(unsigned int irq, irq_handler_t handler, unsigned long flags, const char * name, void *dev);
irq是要申请的硬件中断号。
handler是向系统登记的中断处理函数(顶半部),是一个回调函数,中断发生时,系统调用这个函数,dev参数将被传递给它。
flags是中断处理的属性,可以指定中断的触发方式以及处理方式。在触发方式方面,可以是IRQF_TRIGGER_RISING、IRQF_TRIGGER_FALLING、IRQF_TRIGGER_HIGH、IRQF_TRIGGER_LOW等。在处理方法方面,若设置了IRQF_SHARED,则表示多个设备共享中断,dev是要传递给中断服务程序的私有数据,一般设置为这个设备的设备结构体或者NULL。
request_irq()返回0表示申请中断成功,返回-EINVAL表示中断号无效或者处理函数指针为NULL,返回-EBUSY表示中断已经被占用且不能共享。
顶半部handler的类型irq_handler_t定义为:
typedef irqreturn_t (*irq_handler_t) (int, void*);
typedef int irqreturn_t;
2、释放irq
与request_irq()相对应的函数为free_irq()函数,free_irq()的原型为:
void free_irq(unsigned int irq, void *dev_id);
free_irq()中参数的定义与request_irq()相同。
使能和屏蔽中断:
下列3个函数用于屏蔽一个中断源:
void disable_irq(unsigned int irq);
void disable_irq_nosync(unsigned int irq);
void enable_irq(unsigned int irq);
disable_irq_nosync()和disable_irq()的区别在于前者立即返回,而后者等待目前的中断处理完成。由于disable_irq()会等待指定的中断被处理完成,因此如果在n号中断的顶半部调用disable_irq(n),会引起系统的死锁,这种情况下,只能调用disable_irq_nosync(n)。
下列两个函数(或宏,具体实现依赖于CPU的体系结构)将屏蔽CPU内的所有中断:
#define local_irq_save(flags)...
#define local_irq_disable(void);
前者会将目前的中断状态保留在flags中(注意flags为unsigned long 类型,被直接传递,而不是通过指针),后者直接禁止中断而不保存状态。
与上述两个禁止中断对应的恢复中断的函数是:
#define local_irq_restore(flags) .....
void local_irq_enable(void);
以上各以local_开头的方法的作用范围是本CPU内。
底半部机制:
Linux实现底半部的机制主要有tasklet、工作队列、软中断和线程化irq。
1、tasklet
tasklet的使用较简单,它的执行上下文是软中断,执行时机通常是顶半部返回的时候。我们只需要定义tasklet及其处理函数,并将两者关联则可,例如:
void my_tasklet_func(unsigned long); //定义一个处理函数
DECLARE_TASKLET(my_tasklet, my_tasklet_func, data);
/* 定义一个tasklet结构体my_tasklet,与my_tasklet_func(data)函数相关联*/
代码DECLARE_TASKLET(my_tasklet, my_tasklet_func, data)实现了定义名称为my_tasklet的tasklet,并将其与my_tasklet_func()函数绑定,而传入这个函数的参数是data。
在需要调度tasklet的时候引用一个tasklet_schedule()函数就能使系统在适当的时候进行调用运行:
tasklet_schedule(my_tasklet);
使用tasklet作为底半部处理中断的设备驱动程序模板如代码清单所示:
/* 定义 tasklet 和底半部函数并将它们关联 */
void xxx_do_tasklet(unsigned long);
DECLARE_TASKLET(xxx_tasklet, xxx_do_tasklet, 0);
/* 中断处理底半部 */
void xxx_do_tasklet(unsigned long)
{
...
}
/* 中断处理顶半部 */
irqreturn_t xxx_interrupt(int irq, void *dev_id)
{
...
tasklet_schedule(&xxx_tasklet);
...
}
/* 设备驱动模块加载函数 */
int __init xxx_init(void)
{
...
/* 申请中断 */
result = request_irq(xxx_irq, xxx_interrupt,0, "xxx", NULL);
...
return IRQ_HANDLED;
}
/* 设备驱动模块卸载函数 */
void __exit xxx_exit(void)
{
...
/* 释放中断 */
free_irq(xxx_irq, xxx_interrupt);
...
}
上述程序在模块加载函数中申请中断,并在模块卸载函数中释放它。对应于xxx_irq的中断处理程序被设置为xxx_interrupt()函数,在这个函数中,tasklet_schedule(&xxx_tasklet);调度被定义的tasklet函数xxx_do_tasklet()在适当的时候执行。
2、工作队列
工作队列的使用方法和tasklet非常相似,但是工作队列的执行上下文是内核线程,因此可以调度和休眠。下面的代码用于定义一个工作队列和一个底半部执行函数:
struct work_struct my_wq; //定义一个工作队列
void my_wq_func(struct work_struct *work); //定义一个处理函数
通过INIT_WORK()可以初始化这个工作队列并将工作队列与处理函数绑定:
/* 初始化工作队列并将其与处理函数绑定 */
INIT_WORK(&my_wq, my_wq_func);
与tasklet_schedule()对应的用于调度工作队列执行的函数为schedule_work(),如:
schedule_work(&my_wq); //调度工作队列执行
与代码清单10.2对应的使用工作队列处理中断底半部的设备驱动程序模板如代码清单10.3所示(仅包含与中断相关的部分)。
/* 定义工作队列和关联函数 */
struct work_struct xxx_wq;
void xxx_do_work(struct work_struct *work);
/* 中断处理底半部 */
void xxx_do_work(struct work_struct *work)
{
...
}
/* 中断处理顶半部 */
irqreturn_t xxx_interrupt(int irq, void *dev_id)
{
...
schedule_work(&xxx_wq);
...
return IRQ_HANDLED;
}
/* 设备驱动模块加载函数 */
int xxx_init(void)
{
...
/* 申请中断 */
result = request_irq(xxx_irq, xxx_interrupt,0, "xxx", NULL);
/* 初始化工作队列 */
INIT_WORK(&xxx_wq, xxx_do_work);
...
}
/* 设备驱动模块卸载函数 */
void xxx_exit(void)
{
...
/* 释放中断 */
free_irq(xxx_irq, xxx_interrupt);
...
}
与代码清单10.2不同的是,上述程序在设计驱动模块加载函数中增加了初始化工作队列的代码。
工作队列早期的实现是在每个CPU核上创建一个worker内核线程,所有在这个核上调度的工作都在该worker线程中执行,其并发性显然差强人意。在Linux2.6.36以后,转而实现了“Concurrency-managed workqueues”,简称cmwq,cmwq会自动维护工作队列的线程池以提高并发性,同时保持了API的向后兼容。
3、软中断
软中断(Softirq)也是一种传统的底半部处理机制,它的执行时机通常是顶半部返回的时候,tasklet是基于软中断实现的,因此也运行于软中断上下文。
在Linux内核中,用softirq_action结构体表征一个软中断,这个结构体包含软中断处理函数指针和传递给该函数的参数。使用open_softirq()函数可以注册软中断对应的处理函数,而raise_softirq()函数可以触发一个软中断。
软中断和tasklet运行于软中断上下文,仍然属于原子上下文的一种,而工作队列则运行于进程上下文。因此,在软中断和tasklet处理函数中不允许睡眠,而在工作队列处理函数中运行睡眠。
local_bh_disable(void)和local_bh_enable(void)是内核中用于禁止和使能软中断及tasklet底半部机制的函数。
内核中采用softirq的地方包括HI_SOFTIRQ、TIMER_SOFTIRQ、NET_TX_SOFTIRQ、NET_RX_SOFTIRQ、BLOCK_SOFTIRQ、TASKLET_SOFTIRQ等,一般来说,驱动的编写者不会也不宜直接使用softirq。
在第九章异步通知所基于的信号也类似于中断,现在,总结一下硬中断、软中断和信号的区别:硬中断是外部设备对CPU的中断,软中断是中断底半部的一种处理机制,而信号则是由内核对某个进程的中断。在涉及系统调用的场合,人们也常说软中断(例如ARM为swi)陷入内核,此时软中断的概念是指由软件指令引发的中断,和我们这个地方说的softirq是两个完quan不同的概念,一个是software,一个是soft。
需要特别说明的是,软中断以及基于软中断的tasklet如果在某段时间内大量出现的话,内核会把后续软中断放入ksoftirqd内核线程中执行。总的来说,中断优先级高于软中断,软中断又高于任何一个线程。软中断适度线程化,可以缓解高负载情况下系统的响应。
4、threaded_irq
在内核中,除了可以通过request_irq()、devm_request_irq()申请中断以外,还可以通过request_threaded_irq()和devm_request_threaded_irq()申请。这两个函数的原型为:
request_threaded_irq(unsigned int irq, irq_handler_t handler,
irq_handler_t thread_fn,
unsigned long flags, const char *name, void *dev);
devm_request_threaded_irq(struct device *dev, unsigned int irq,
irq_handler_t handler, irq_handler_t thread_fn, unsigned long irqflags,
const char *devname, void *dev_id);
由此可见,它们比request_irq()、devm_request_irq()多了一个参数thread_fn。用这两个API申请中断的时候,内核会为相应的中断号分配一个对应的内核线程。注意这个线程只针对这个中断号,如果其他中断也通过request_threaded_irq()申请,自然会得到新的内核线程。
参数handler对应的函数执行于中断上下文,thread_fn参数对应的函数则执行于内核线程。如果handler结束的时候,返回值是IRQ_WAKE_THREAD,内核会调度对应线程执行thread_fn对应的函数。
request_threaded_irq()和devm_request_threaded_irq()支持在irqflags中设置IRQF_ONESHOT标记,这样内核会自动帮助我们在中断上下文中屏蔽对应的中断号,而在内核调用thread_fn执行后,重新使能该中断号。对于我们无法再上半部清除中断的情况,IRQF_ONESHOT特别有用,避免了中断服务程序一退出,中断就洪泛的情况。
handler参数可以设置为NULL,这种情况下,内核会用默认的irq_default_primary_handler()代替handler,并会使用IRQF_ONESHOT标记。irq_default_primary_handler()定义为:
static irqreturn_t irq_default_primary_handler(int irq, void *dev_id)
{
return IRQ_WAKE_THREAD;
}
中断共享:
多个设备共享一根硬件中断线的情况在实际的硬件系统中广泛存在,Linux支持这种中断共享。下面是中断共享的使用方法。
1、共享中断的多个设备在申请中断时,都应该使用IRQF_SHARED标志,而且一个设备以IRQF_SHARED申请某个中断成功的前提是该中断未被申请,或该中断虽然被申请了,但是之前申请该中断的所有设备也都以IRQF_SHARED标志申请该中断。
2、尽管内核模块可访问的quan局地址都可以作为request_irq(..., void *dev_id)的最后一个参数dev_id,但是设备结构体指针显然是可传入的最佳参数。
3、在中断到来时,会遍历执行共享此中断的所有中断处理程序,直到某一个函数返回IRQ_HANDLED。在中断处理程序顶半部,应根据硬件寄存器的信息比照传入的dev_id参数迅速地判断是否为本设备的中断,若不是,应迅速返回IRQ_NONE,如下图所示。
代码清单10.8给出了使用共享中断的设备驱动程序的模板(仅包含与共享中断机制相关的部分)。
/* 中断处理顶半部 */
irqreturn_t xxx_interrupt(int irq, void *dev_id)
{
...
int status = read_int_status(); /* 获知中断源 */
if(!is_myint(dev_id,status)) /* 判断是否为本设备中断 */
return IRQ_NONE; /* 不是本设备中断, 立即返回 */
/* 是本设备中断, 进行处理 */
...
return IRQ_HANDLED; /* 返回 IRQ_HANDLED 表明中断已被处理 */12
}
/* 设备驱动模块加载函数 */
int xxx_init(void)
{
...
/* 申请共享中断 */
result = request_irq(sh_irq, xxx_interrupt,
IRQF_SHARED, "xxx", xxx_dev);
...
}
/* 设备驱动模块卸载函数 */
void xxx_exit(void)
{
...
/* 释放中断 */
free_irq(xxx_irq, xxx_interrupt);
...
}
内核定时器:
内核定时器编程
软件意义上的定时器最终要依赖硬件定时器来实现,内核在时钟中断发生后检测各定时器是否到期,到期后的定时器处理函数将作为软件中断的底半部执行。实质上,时钟中断处理程序会唤起TIMER_SOFTIRQ软中断,运行当前处理器上到期的所有定时器。
在Linux设备驱动编程中,可以利用Linux内核中提供的一组函数和数据结构来完成定时触发工作或者完成某个周期性的事务。这组函数和数据结构使得驱动工程师在多数情况下不用关心具体的软件定时器究竟对应着怎样的内核和硬件行为。
Linux内核所提供的用于操作定时器的数据结构和函数如下。
1、timer_list
在Linux内核中,timer_list结构体的一个实例对应一个定时器,代码清单如下所示。
struct timer_list {
/*
* All fields that change during normal runtime grouped to the
* same cacheline
*/
struct list_head entry;
unsigned long expires;
struct tvec_base *base;
void (*function)(unsigned long);
unsigned long data;
int slack;
#ifdef CONFIG_TIMER_STATS
int start_pid;
void *start_site;
char start_comm[16];
#endif
#ifdef CONFIG_LOCKDEP
struct lockdep_map lockdep_map;
#endif
};
在定时器期满后,其中void (*function)(unsigned long);成员将被执行,而data成员则是出入其中的参数,unsigned long expires;则是定时器到期的时间(jiffies)。
如下代码定义一个名为my_timer的定时器:
struct timer_list my_timer;
2、初始化定时器
init_timer是一个宏,它的原型等价于:
void init_timer(struct timer_list * timer);
上述init_timer()函数初始化timer_list的entry的next为NULL,并给base指针赋值。
TIMER_INITIALIZER(_function, _expires, _data)宏用于赋值定时器结构体的function、expires、data和base成员,这个宏等价于:
#define TIMER_INITIALIZER(_function, _expires, _data) { \
.function = (_function), \
.expires = (_expires), \
.data = (_data), \
.base = &boot_tvec_bases, \
}
DEFINE_TIMER(_name, _function, _expires, _data)宏是定义并初始化定时器成员的“快捷方式”,这个宏定义为:
#define DEFINE_TIMER(_name, _function, _expires, _data) \
struct timer_list _name = \
TIMER_INITIALIZER(_function, _expires, _data)
此外,setup_timer()也可用于初始化定时器并赋值其成员,其源代码为:
static inline void setup_timer(struct timer_list * timer,
void (*function)(unsigned long),unsigned long data)
{
timer->function = function;
timer->data = data;
init_timer(timer);
}
3、增加定时器
void add_timer(struct timer_list *timer);
上述函数用于注册内核定时器,将定时器加入到内核动态定时器链表中。
4、删除定时器
int del_timer(struct timer_list *timer);
上述函数用于删除定时器。
int del_timer_sync(struct timer_list *timer)是del_timer()的同步版,在删除一个定时器时需等待其被处理完,因此该函数的调用不能发生在中断上下文中。
5、修改定时器的expire
int mod_timer(struct timer_list * timer, unsigned long expires);
上述函数用于修改定时器的到期时间,在新的被传入的expires到来后才会执行定时器函数。
下面的代码清单给出了一个完整的内核定时器使用模板,在大多数情况下,设备驱动都如这个模板那样使用定时器。
/* xxx 设备结构体 */
struct xxx_dev {
struct cdev cdev;
...
timer_list xxx_timer; /* 设备要使用的定时器 */
};
/* xxx 驱动中的某函数 */
xxx_func1( … )
{
struct xxx_dev *dev = filp->private_data;
...
/* 初始化定时器 */
init_timer(&dev->xxx_timer);
dev->xxx_timer.function = &xxx_do_timer;
dev->xxx_timer.data = (unsigned long)dev;
/* 设备结构体指针作为定时器处理函数参数 */
18:dev->xxx_timer.expires = jiffies + delay;
/* 添加(注册) 定时器 */
dd_timer(&dev->xxx_timer);
...
}
/* xxx 驱动中的某函数 */
xxx_func2( … )
{
...
/* 删除定时器 */
del_timer (&dev->xxx_timer);
...
}
/* 定时器处理函数 */
static void xxx_do_timer(unsigned long arg)
{
struct xxx_device *dev = (struct xxx_device *)(arg);
...
/* 调度定时器再执行 */
39:dev->xxx_timer.expires = jiffies + delay;
add_timer(&dev->xxx_timer);
...
}
从代码清单中第18、39行可以看出,定时器的到期时间往往是在目前jiffies的基础上添加一个延时,若为HZ,则表示延迟1s。
在定时器处理函数中,在完成相应的工作后,往往会延后expires并将定时器再次添加到内核定时器链表中,以便定时器能再次被触发。
此外,Linux内核支持tickless和NO_HZ模式后,内核也包括对hrtimer(高精度定时器)的支持,它可以支持到微秒级别的精度。内核也定义了hrtimer结构体,hrtimer_set_expires()、hrtimer_start_expires()、hrtimer_forward_now()、hrtimer_restart()等类似的API来完成hrtimer的设置、时间推移以及到期回调。
内核中延迟的工作delayed_work
对于周期性的任务,除了定时器以外,在Linux内核中还可以利用一套封装的很好的快捷机制,其本质是利用工作队列和定时器实现,这套快捷机制就是delayed_work,delayed_work结构体的定义如下代码清单所示:
struct delayed_work {
struct work_struct work;
struct timer_list timer;
struct workqueue_struct *wq;
int cpu;
};
我们可以通过如下函数调度一个delayed_work在指定的延时后执行:
int schedule_delayed_work(struct delayed_work * dwork, unsigned long delay);
当指定的delay到来时,delayed_work结构体中的work成员work_func_t类型成员func()会被执行。work_func_t类型定义为:
typedef void (*work_func_t)(struct work_struct *work);
其中,delay参数的单位是jiffies,因此一种常见的用法如下:
schedule_delayed_work(&work, msecs_to_jiffies(poll_interval));
msecs_to_jiffies()用于将毫秒转化为jiffies。
如果要周期性地执行任务,通常会在delayed_work的工作函数中再次调用schedule_delayed_work(),周而复始。
如下函数用来取消delayed_work:
int cancel_delayed_work(struct delayed_work * work);
int cancel_delayed_work_sync(struct delayed_work * work);
内核延时
短延迟
在Linux内核中提供了下列3个函数以分别进行纳秒、微秒和毫秒延迟:
void ndelay(unsigned long nsecs);
void udelay(unsigned long usecs);
void mdelay(unsigned long msecs);
上述延迟的实现原理本质上是忙等待,它根据CPU频率进行一定次数的循环。有时候,人们在软件中进行下面的延迟:
void delay(unsigned int time)
{
while(time--);
}
ndelay()、udelay()和mdelay()函数的实现方式原理与此类似。内核在启动时,会运行一个延迟循环校准,计算出lpj,内核启动时会打印如下类似信息:
Calibrating delay loop... 530.84 BogoMIPS (lpj=1327104)
如果我们直接在bootloader传递给内核bootargs中设置lpj=1327104,则可以省掉这个校准的过程,节省约百毫秒级的开机时间。
毫秒延时(以及更大的秒延时)已经比较大了,在内核中,最好不要直接使用mdelay()函数,这将耗费CPU资源,对于毫秒级以上的延时,内核提供了下述函数:
void msleep(unsigned int msecs);
unsigned long msleep_interruptible(unsigned int msecs);
void ssleep(unsigned int seconds);
上述函数将使得调用它的进程睡眠参数指定的时间为msecs,msleep()、ssleep()不能被打断,而msleep_interruptible()则可以被打断。
长延迟
在内核中进行延迟的一个很直观的方法时比较当前的jiffies和目标jiffies,直到未来的jiffies达到目标jiffies。代码清单如下所示给出了使用忙等待先延迟100个jiffies在延迟2s的实例。
/* 延迟 100 个 jiffies */
unsigned long delay = jiffies + 100;
while(time_before(jiffies, delay));
/* 再延迟 2s */
unsigned long delay = jiffies + 2*Hz;
while(time_before(jiffies, delay));
与time_before()对应的还有一个time_after(),它们在内核中定义为(实际上只是将传入的未来时间jiffies和被调用时的jiffies进行一个简单的比较):
#define time_after(a,b) \
(typecheck(unsigned long, a) && \
typecheck(unsigned long, b) && \
((long)(b) - (long)(a) < 0))
#define time_before(a,b) time_after(b,a)
为了防止在time_before()和time_after()的比较过程中编译器对jiffies的优化,内核将其定义为volatile变量,这将保证每次都会重新读取这个变量。因此volatile更多的作用还是避免这种合并。
睡着延迟
睡着延迟无疑是比忙等待更好的方式,睡着延迟是在忙等待的时间到来之前进程处于睡眠状态,CPU资源被其他进程使用。schedule_timeout()可以使当前任务休眠至指定的jiffies之后再重新被调度执行,msleep()和msleep_interruptible()在本质上都是依靠包含了schedule_timeout()的schedule_timeout_uninterruptible()和chedule_timeout_interruptible()来实现的,代码清单如下所示:
void msleep(unsigned int msecs)
{
unsigned long timeout = msecs_to_jiffies(msecs) + 1;
while (timeout)
timeout = schedule_timeout_uninterruptible(timeout);
}
unsigned long msleep_interruptible(unsigned int msecs)
{
unsigned long timeout = msecs_to_jiffies(msecs) + 1;
while (timeout && !signal_pending(current))
timeout = schedule_timeout_interruptible(timeout);
return jiffies_to_msecs(timeout);
}
实际上,schedule_timeout()的实现原理是向系统添加一个定时器,在定时器处理函数中唤醒与参数对应的进程。
如下代码清单中调用schedule_timeout_uninterruptible()和schedule_timeout_interruptible(),这两个函数的区别在于前者在调用chedule_timeout()之前置进程状态为TASK_INTERRUPTIBLE,后者置进程状态为TASK_UNINTERRUPTIBLE,代码如下所示:
signed long __sched schedule_timeout_interruptible(signed long timeout)
{
__set_current_state(TASK_INTERRUPTIBLE);
return schedule_timeout(timeout);
}
signed long __sched schedule_timeout_uninterruptible(signed long timeout)
{
__set_current_state(TASK_UNINTERRUPTIBLE);
return schedule_timeout(timeout);
}
另外,下面两个函数可以将当期进程添加到等待队列中,从而在等待队列上睡眠。当超时发生时,进程被唤醒(后者可以在超时前被打断):
sleep_on_timeout(wait_queue_head_t *q, unsigned long timeout);
interruptible_sleep_on_timeout(wait_queue_head_t*q, unsigned long timeout);
总结
Linux的中断处理分为两个半部,顶半部处理紧急的硬件操作,底半部处理不紧急的耗时操作。tasklet和工作队列都是调度中断底半部的良好机制,tasklet基于软件中断实现。内核定时器也依靠软中断实现。
内核中的延时可以采用忙等待或睡眠等待,为了充分利用CPU资源,使系统有更好的吞吐性能,在对延迟时间的要求并不是很精确的情况下,睡眠等待通常是值得推荐的,而ndelay()、udelay()忙等待机制在驱动中通常是为了配合硬件上的短时延迟要求。
转载于:https://my.oschina.net/cht2000/blog/1217812
上一篇: linux crontab运行原理