linux驱动程序设计10 中断与时钟

本章主要讲解Linux设备驱动编程中的中断与定时器处理。由于中断服务程序的执行并不存在于进程
上下文中,所以要求中断服务程序的时间要尽量短。因此,Linux在中断处理中引入了顶半部和底半部分
离的机制。另外,内核对时钟的处理也采用中断方式,而内核软件定时器最终依赖于时钟中断。
10.1节讲解中断和定时器的概念及处理流程。
10.2节讲解Linux中断处理程序的架构,以及顶半部、底半部之间的关系。
10.3节讲解Linux中断编程的方法,涉及申请和释放中断、使能和屏蔽中断以及中断底半部tasklet、工
作队列、软中断机制和threaded_irq。
10.4节讲解多个设备共享同一个中断号时的中断处理过程。
10.5节和10.6节分别讲解Linux设备驱动编程中定时器的编程以及内核延时的方法。
10.1 中断与定时器
所谓中断是指CPU在执行程序的过程中,出现了某些突发事件急待处理,CPU必须暂停当前程序的执
行,转去处理突发事件,处理完毕后又返回原程序被中断的位置继续执行。
根据中断的来源,中断可分为内部中断和外部中断,内部中断的中断源来自CPU内部(软件中断指
令、溢出、除法错误等,例如,操作系统从用户态切换到内核态需借助CPU内部的软件中断),外部中断
的中断源来自CPU外部,由外设提出请求。
根据中断是否可以屏蔽,中断可分为可屏蔽中断与不可屏蔽中断(NMI),可屏蔽中断可以通过设置
中断控制器寄存器等方法被屏蔽,屏蔽后,该中断不再得到响应,而不可屏蔽中断不能被屏蔽。
根据中断入口跳转方法的不同,中断可分为向量中断和非向量中断。采用向量中断的CPU通常为不同
的中断分配不同的中断号,当检测到某中断号的中断到来后,就自动跳转到与该中断号对应的地址执行。
不同中断号的中断有不同的入口地址。非向量中断的多个中断共享一个入口地址,进入该入口地址后,再
通过软件判断中断标志来识别具体是哪个中断。也就是说,向量中断由硬件提供中断服务程序入口地址,
非向量中断由软件提供中断服务程序入口地址。
一个典型的非向量中断服务程序如代码清单10.1所示,它先判断中断源,然后调用不同中断源的中断
服务程序。
代码清单10.1 非向量中断服务程序的典型结构
1 irq_handler()
2 {
3 ...
4 int int_src = read_int_status(); /* 读硬件的中断相关寄存器 */
5 switch (int_src) { /* 判断中断源 */
6 case DEV_A:
7 dev_a_handler();
8 break;
9 case DEV_B:
10 dev_b_handler();
11 break;
12 ...
13 default:
14 break;
15 }
16 ...
17}
嵌入式系统以及x86PC中大多包含可编程中断控制器(PIC),许多MCU内部就集成了PIC。如在
80386中,PIC是两片i8259A芯片的级联。通过读写PIC的寄存器,程序员可以屏蔽/使能某中断及获得中断
状态,前者一般通过中断MASK寄存器完成,后者一般通过中断PEND寄存器完成。
定时器在硬件上也依赖中断来实现,图10.1所示为典型的嵌入式微处理器内可编程间隔定时器
(PIT)的工作原理,它接收一个时钟输入,当时钟脉冲到来时,将目前计数值增1并与预先设置的计数值
(计数目标)比较,若相等,证明计数周期满,并产生定时器中断且复位目前计数值。
图10.1 PIT定时器的工作原理
在ARM多核处理器里最常用的中断控制器是GIC(Generic Interrupt Controller),如图10.2所示,它支
持3种类型的中断。
图10.2 ARM多核处理器里的GIC
SGI(Software Generated Interrupt):软件产生的中断,可以用于多核的核间通信,一个CPU可以通过
写GIC的寄存器给另外一个CPU产生中断。多核调度用的IPI_WAKEUP、IPI_TIMER、
IPI_RESCHEDULE、IPI_CALL_FUNC、IPI_CALL_FUNC_SINGLE、IPI_CPU_STOP、IPI_IRQ_WORK、
IPI_COMPLETION都是由SGI产生的。
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));

10.2 Linux中断处理程序架构
设备的中断会打断内核进程中的正常调度和运行,系统对更高吞吐率的追求势必要求中断服务程序尽
量短小精悍。但是,这个良好的愿望往往与现实并不吻合。在大多数真实的系统中,当中断到来时,要完
成的工作往往并不会是短小的,它可能要进行较大量的耗时处理。
图10.3描述了Linux内核的中断处理机制。为了在中断执行时间尽量短和中断处理需完成的工作尽量
大之间找到一个平衡点,Linux将中断处理程序分解为两个半部:顶半部(Top Half)和底半部(Bottom
Half)。
图10.3 Linux中断处理机制
顶半部用于完成尽量少的比较紧急的功能,它往往只是简单地读取寄存器中的中断状态,并在清除中
断标志后就进行“登记中断”的工作。“登记中断”意味着将底半部处理程序挂到该设备的底半部执行队列中
去。这样,顶半部执行的速度就会很快,从而可以服务更多的中断请求。
现在,中断处理工作的重心就落在了底半部的头上,需用它来完成中断事件的绝大多数任务。底半部
几乎做了中断处理程序所有的事情,而且可以被新的中断打断,这也是底半部和顶半部的最大不同,因为
顶半部往往被设计成不可中断。底半部相对来说并不是非常紧急的,而且相对比较耗时,不在硬件中断服
务程序中执行。
尽管顶半部、底半部的结合能够改善系统的响应能力,但是,僵化地认为Linux设备驱动中的中断处
理一定要分两个半部则是不对的。如果中断要处理的工作本身很少,则完全可以直接在顶半部全部完成。
其他操作系统中对中断的处理也采用了类似于Linux的方法,真正的硬件中断服务程序都应该
尽量短。因此,许多操作系统都提供了中断上下文和非中断上下文相结合的机制,将中断的耗时工作保留
到非中断上下文去执行。例如,在VxWorks中,网络设备包接收中断到来后,中断服务程序会通过
netJobAdd()函数将耗时的包接收和上传工作交给tNetTask任务去执行。
在Linux中,查看/proc/interrupts文件可以获得系统中中断的统计信息,并能统计出每一个中断号上的
中断在每个CPU上发生的次数,具体如图10.4所示。
图10.4 Linux中的中断统计信息
10.3 Linux中断编程
10.3.1 申请和释放中断
在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参数将被传递给它。
irqflags是中断处理的属性,可以指定中断的触发方式以及处理方式。在触发方式方面,可以是
IRQF_TRIGGER_RISING、IRQF_TRIGGER_FALLING、IRQF_TRIGGER_HIGH、IRQF_TRIGGER_LOW
等。在处理方式方面,若设置了IRQF_SHARED,则表示多个设备共享中断,dev是要传递给中断服务程
序的私有数据,一般设置为这个设备的设备结构体或者NULL。
request_irq()返回0表示成功,返回-EINVAL表示中断号无效或处理函数指针为NULL,返回-
EBUSY表示中断已经被占用且不能共享。
int devm_request_irq(struct device *dev, unsigned int irq, irq_handler_t handler,
unsigned long irqflags, const char *devname, void *dev_id);
此函数与request_irq()的区别是devm_开头的API申请的是内核“managed”的资源,一般不需要在出
错处理和remove()接口里再显式的释放。有点类似Java的垃圾回收机制。比如,对于at86rf230驱动,如
下的补丁中改用devm_request_irq()后就删除了free_irq(),该补丁对应的内核commit ID是652355c5。
--- a/drivers/net/ieee802154/at86rf230.c
+++ b/drivers/net/ieee802154/at86rf230.c
@@ -1190,24+1190,22@@ static int at86rf230_probe(struct spi_device *spi)
if (rc)
goto err_hw_init;
- rc = request_irq(spi->irq, irq_handler, IRQF_SHARED,
- dev_name(&spi->dev), lp);
+ rc = devm_request_irq(&spi->dev, spi->irq, irq_handler, IRQF_SHARED,
+ dev_name(&spi->dev), lp);
if (rc)
goto err_hw_init;
/* Read irq status register to reset irq line */
rc = at86rf230_read_subreg(lp, RG_IRQ_STATUS, 0xff, 0, &status);
if (rc)
- goto err_irq;
+ goto err_hw_init;
rc = ieee802154_register_device(lp->dev);
if (rc)
- goto err_irq;
+ goto err_hw_init;
return rc;
-err_irq:
- free_irq(spi->irq, lp);
err_hw_init:
flush_work(&lp->irqwork);
spi_set_drvdata(spi, NULL);
@@ -1232,7+1230,6@@ static int at86rf230_remove(struct spi_device *spi)
at86rf230_write_subreg(lp, SR_IRQ_MASK, 0);
ieee802154_unregister_device(lp->dev);
- free_irq(spi->irq, lp);
flush_work(&lp->irqwork);
if (gpio_is_valid(pdata->slp_tr))
顶半部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()相同。
10.3.2 使能和屏蔽中断
下列3个函数用于屏蔽一个中断源:
void disable_irq(int irq);
void disable_irq_nosync(int irq);
void enable_irq(int irq);
disable_irq_nosync()与disable_irq()的区别在于前者立即返回,而后者等待目前的中断处理完
成。由于disable_irq()会等待指定的中断被处理完,因此如果在n号中断的顶半部调用disable_irq(n),
会引起系统的死锁,这种情况下,只能调用disable_irq_nosync(n)。
下列两个函数(或宏,具体实现依赖于CPU的体系结构)将屏蔽本CPU内的所有中断:
#define local_irq_save(flags) ...
void local_irq_disable(void);
前者会将目前的中断状态保留在flags中(注意flags为unsigned long类型,被直接传递,而不是通过指
针),后者直接禁止中断而不保存状态。
与上述两个禁止中断对应的恢复中断的函数(或宏)是:
#define local_irq_restore(flags) ...
void local_irq_enable(void);
以上各以local_开头的方法的作用范围是本CPU内。
10.3.3 底半部机制
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作为底半部处理中断的设备驱动程序模板如代码清单10.2所示(仅包含与中断相关的部
分)。
代码清单10.2 tasklet使用模板
1/* 定义tasklet和底半部函数并将它们关联 */
2void xxx_do_tasklet(unsigned long);
3DECLARE_TASKLET(xxx_tasklet, xxx_do_tasklet, 0);
4
5/* 中断处理底半部 */
6void xxx_do_tasklet(unsigned long)
7{
8 ...
9}
10
11/* 中断处理顶半部 */
12irqreturn_t xxx_interrupt(int irq, void *dev_id)
13{
14 ...
15 tasklet_schedule(&xxx_tasklet);
16 ...
17}
18
19/* 设备驱动模块加载函数 */
20int __init xxx_init(void)
21{
22 ...
23 /* 申请中断 */
24 result = request_irq(xxx_irq, xxx_interrupt,
25 0, "xxx", NULL);
26 ...
27 return IRQ_HANDLED;
28}
29
30/* 设备驱动模块卸载函数 */
31void __exit xxx_exit(void)
32{
33 ...
34 /* 释放中断 */
35 free_irq(xxx_irq, xxx_interrupt);
36 ...
37}
上述程序在模块加载函数中申请中断(第24~25行),并在模块卸载函数中释放它(第35行)。对应
于xxx_irq的中断处理程序被设置为xxx_interrupt()函数,在这个函数中,第15行的
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所示(仅包
含与中断相关的部分)。
代码清单10.3 工作队列使用模板
1/* 定义工作队列和关联函数 */
2struct work_struct xxx_wq;
3void xxx_do_work(struct work_struct *work);
4
5/* 中断处理底半部 */
6void xxx_do_work(struct work_struct *work)
7{
8 ...
9}
10
11/*中断处理顶半部*/
12irqreturn_t xxx_interrupt(int irq, void *dev_id)
13{
14 ...
15 schedule_work(&xxx_wq);
16 ...
17 return IRQ_HANDLED;
18}
19
20/* 设备驱动模块加载函数 */
21int xxx_init(void)
22{
23 ...
24 /* 申请中断 */
25 result = request_irq(xxx_irq, xxx_interrupt,
26 0, "xxx", NULL);
27 ...
28 /* 初始化工作队列 */
29 INIT_WORK(&xxx_wq, xxx_do_work);
30 ...
31}
32
33/* 设备驱动模块卸载函数 */
34void xxx_exit(void)
35{
36 ...
37 /* 释放中断 */
38 free_irq(xxx_irq, xxx_interrupt);
39 ...
40}
与代码清单10.2不同的是,上述程序在设计驱动模块加载函数中增加了初始化工作队列的代码(第29
行)。
工作队列早期的实现是在每个CPU核上创建一个worker内核线程,所有在这个核上调度的工作都在该
worker线程中执行,其并发性显然差强人意。在Linux 2.6.36以后,转而实现了“Concurrency-managed
workqueues”,简称cmwq,cmwq会自动维护工作队列的线程池以提高并发性,同时保持了API的向后兼
容。
3.软中断
软中断(Softirq)也是一种传统的底半部处理机制,它的执行时机通常是顶半部返回的时候,tasklet
是基于软中断实现的,因此也运行于软中断上下文。
在Linux内核中,用softirq_action结构体表征一个软中断,这个结构体包含软中断处理函数指针和传递
给该函数的参数。使用open_softirq()函数可以注册软中断对应的处理函数,而raise_softirq()函数可以
触发一个软中断。
软中断和tasklet运行于软中断上下文,仍然属于原子上下文的一种,而工作队列则运行于进程上下
文。因此,在软中断和tasklet处理函数中不允许睡眠,而在工作队列处理函数中允许睡眠。
local_bh_disable()和local_bh_enable()是内核中用于禁止和使能软中断及tasklet底半部机制的函
数。
内核中采用softirq的地方包括HI_SOFTIRQ、TIMER_SOFTIRQ、NET_TX_SOFTIRQ、
NET_RX_SOFTIRQ、SCSI_SOFTIRQ、TASKLET_SOFTIRQ等,一般来说,驱动的编写者不会也不宜直
接使用softirq。
第9章异步通知所基于的信号也类似于中断,现在,总结一下硬中断、软中断和信号的区别:硬中断
是外部设备对CPU的中断,软中断是中断底半部的一种处理机制,而信号则是由内核(或其他进程)对某
个进程的中断。在涉及系统调用的场合,人们也常说通过软中断(例如ARM为swi)陷入内核,此时软中
断的概念是指由软件指令引发的中断,和我们这个地方说的softirq是两个完全不同的概念,一个是
software,一个是soft。
需要特别说明的是,软中断以及基于软中断的tasklet如果在某段时间内大量出现的话,内核会把后续
软中断放入ksoftirqd内核线程中执行。总的来说,中断优先级高于软中断,软中断又高于任何一个线程。
软中断适度线程化,可以缓解高负载情况下系统的响应。
4.threaded_irq
在内核中,除了可以通过request_irq()、devm_request_irq()申请中断以外,还可以通过
request_threaded_irq()和devm_request_threaded_irq()申请。这两个函数的原型为:
int request_threaded_irq(unsigned int irq, irq_handler_t handler,
irq_handler_t thread_fn,
unsigned long flags, const char *name, void *dev);
int 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()定义为:
/*
* Default primary interrupt handler for threaded interrupts. Is
* assigned as primary handler when request_threaded_irq is called
* with handler == NULL. Useful for oneshot interrupts.
*/
static irqreturn_t irq_default_primary_handler(int irq, void *dev_id)
{
return IRQ_WAKE_THREAD;
}
10.3.4 实例:GPIO按键的中断
drivers/input/keyboard/gpio_keys.c是一个放之四海皆准的GPIO按键驱动,为了让该驱动在特定的电路
板上工作,通常只需要修改arch/arm/mach-xxx下的板文件或者修改device tree对应的dts。该驱动会为每个
GPIO申请中断,在gpio_keys_setup_key()函数中进行。注意最后一个参数bdata,会被传入中断服务程
序。
代码清单10.4 GPIO按键驱动中断申请
1static int gpio_keys_setup_key(struct platform_device *pdev,
2 struct input_dev *input,
3 struct gpio_button_data *bdata,
4 const struct gpio_keys_button *button)
5{
6 ...
7
8 error = request_any_context_irq(bdata->irq, isr, irqflags, desc, bdata);
9 if (error < 0) {
10 dev_err(dev, "Unable to claim irq %d; error %d\n",
11 bdata->irq, error);
12 goto fail;
13 }
14 ...
15}
第8行的request_any_context_irq()会根据GPIO控制器本身的“上级”中断是否为threaded_irq来决定采
用request_irq()还是request_threaded_irq()。一组GPIO(如32个GPIO)虽然每个都提供一个中断,并
且都有中断号,但是在硬件上一组GPIO通常是嵌套在上一级的中断控制器上的一个中断。
request_any_context_irq()也有一个变体是devm_request_any_context_irq()。
在GPIO按键驱动的remove_key()函数中,会释放GPIO对应的中断,如代码清单10.5所示。
代码清单10.5 GPIO按键驱动中断释放
1static void gpio_remove_key(struct gpio_button_data *bdata)
2{
3 free_irq(bdata->irq, bdata);
4 if (bdata->timer_debounce)
5 del_timer_sync(&bdata->timer);
6 cancel_work_sync(&bdata->work);
7 if (gpio_is_valid(bdata->button->gpio))
8 gpio_free(bdata->button->gpio);
GPIO按键驱动的中断处理比较简单,没有明确地分为上下两个半部,而只存在顶半部,如代码清单
10.6所示。
代码清单10.6 GPIO按键驱动中断处理程序
1static irqreturn_t gpio_keys_gpio_isr(int irq, void *dev_id)
2{
3 struct gpio_button_data *bdata = dev_id;
4
5 BUG_ON(irq != bdata->irq);
6
7 if (bdata->button->wakeup)
8 pm_stay_awake(bdata->input->dev.parent);
9 if (bdata->timer_debounce)
10 mod_timer(&bdata->timer,
11 jiffies + msecs_to_jiffies(bdata->timer_debounce));
12 else
13 schedule_work(&bdata->work);
14
15 return IRQ_HANDLED;
16}
第3行直接从dev_id取出了bdata,这就是对应的那个GPIO键的数据结构,之后根据情况启动timer以进
行debounce或者直接调度工作队列去汇报按键事件。在GPIO按键驱动初始化的时候,通过
INIT_WORK(&bdata->work,gpio_keys_gpio_work_func)初始化了bdata->work,对应的处理函数是
gpio_keys_gpio_work_func(),如代码清单10.7所示。
代码清单10.7 GPIO按键驱动的工作队列底半部
1static void gpio_keys_gpio_work_func(struct work_struct *work)
2{
3 struct gpio_button_data *bdata =
4 container_of(work, struct gpio_button_data, work);
5
6 gpio_keys_gpio_report_event(bdata);
7
8 if (bdata->button->wakeup)
9 pm_relax(bdata->input->dev.parent);
10}
观察其中的第3~4行,它通过container_of()再次从work_struct反向解析出了bdata。原因是
work_struct本身在定义时,就嵌入在gpio_button_data结构体内。读者朋友们应该掌握Linux的这种可以到
处获取一个结构体指针的技巧,它实际上类似于面向对象里面的“this”指针。
struct gpio_button_data {
const struct gpio_keys_button *button;
struct input_dev *input;
struct timer_list timer;
struct work_struct work;
unsigned int timer_debounce; /* in msecs */
unsigned int irq;
spinlock_t lock;
bool disabled;
bool key_pressed;
};
10.4 中断共享
多个设备共享一根硬件中断线的情况在实际的硬件系统中广泛存在,Linux支持这种中断共享。下面
是中断共享的使用方法。
1)共享中断的多个设备在申请中断时,都应该使用IRQF_SHARED标志,而且一个设备以
IRQF_SHARED申请某中断成功的前提是该中断未被申请,或该中断虽然被申请了,但是之前申请该中断
的所有设备也都以IRQF_SHARED标志申请该中断。
2)尽管内核模块可访问的全局地址都可以作为request_irq(…,void*dev_id)的最后一个参数
dev_id,但是设备结构体指针显然是可传入的最佳参数。
3)在中断到来时,会遍历执行共享此中断的所有中断处理程序,直到某一个函数返回
IRQ_HANDLED。在中断处理程序顶半部中,应根据硬件寄存器中的信息比照传入的dev_id参数迅速地判
断是否为本设备的中断,若不是,应迅速返回IRQ_NONE,如图10.5所示。
图10.5 共享中断的处理
代码清单10.8给出了使用共享中断的设备驱动程序的模板(仅包含与共享中断机制相关的部分)。
代码清单10.8 共享中断编程模板
1/* 中断处理顶半部 */
2irqreturn_t xxx_interrupt(int irq, void *dev_id)
3{
4 ...
5 int status = read_int_status(); /* 获知中断源 */
6 if(!is_myint(dev_id,status)) /* 判断是否为本设备中断 */
7 return IRQ_NONE; /* 不是本设备中断,立即返回 */
8
9 /* 是本设备中断,进行处理 */
10 ...
11 return IRQ_HANDLED; /* 返回IRQ_HANDLED表明中断已被处理 */
12}
13
14/* 设备驱动模块加载函数 */
15int xxx_init(void)
16{
17 ...
18 /* 申请共享中断 */
19 result = request_irq(sh_irq, xxx_interrupt,
20 IRQF_SHARED, "xxx", xxx_dev);
21 ...
22}
23
24/* 设备驱动模块卸载函数 */
25void xxx_exit(void)
26{
27 ...
28 /* 释放中断 */
29 free_irq(xxx_irq, xxx_interrupt);
30 ...
31}
10.5 内核定时器
10.5.1 内核定时器编程
软件意义上的定时器最终依赖硬件定时器来实现,内核在时钟中断发生后检测各定时器是否到期,到
期后的定时器处理函数将作为软中断在底半部执行。实质上,时钟中断处理程序会唤起TIMER_SOFTIRQ
软中断,运行当前处理器上到期的所有定时器。
在Linux设备驱动编程中,可以利用Linux内核中提供的一组函数和数据结构来完成定时触发工作或者
完成某周期性的事务。这组函数和数据结构使得驱动工程师在多数情况下不用关心具体的软件定时器究竟
对应着怎样的内核和硬件行为。
Linux内核所提供的用于操作定时器的数据结构和函数如下。
1.timer_list
在Linux内核中,timer_list结构体的一个实例对应一个定时器,如代码清单10.9所示。
代码清单10.9 timer_list结构体
1struct timer_list {
2 /*
3 * All fields that change during normal runtime grouped to the
4 * same cacheline
5 */
6 struct list_head entry;
7 unsigned long expires;
8 struct tvec_base *base;
9
10 void (*function)(unsigned long);
11 unsigned long data;
12
13 int slack;
14
15#ifdef CONFIG_TIMER_STATS
16 int start_pid;
17 void *start_site;
18 char start_comm[16];
19#endif
20#ifdef CONFIG_LOCKDEP
21 struct lockdep_map lockdep_map;
22#endif
23};
当定时器期满后,其中第10行的function()成员将被执行,而第11行的data成员则是传入其中的参
数,第7行的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) { \
.entry = { .prev = TIMER_ENTRY_STATIC }, \
.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()也可用于初始化定时器并赋值其成员,其源代码为:
#define __setup_timer(_timer, _fn, _data, _flags) \
do { \
__init_timer((_timer), (_flags)); \
(_timer)->function = (_fn); \
(_timer)->data = (_data); \
} while (0)
3.增加定时器
void add_timer(struct timer_list * timer);
上述函数用于注册内核定时器,将定时器加入到内核动态定时器链表中。
4.删除定时器
int del_timer(struct timer_list * timer);
上述函数用于删除定时器。
del_timer_sync()是del_timer()的同步版,在删除一个定时器时需等待其被处理完,因此该函数的
调用不能发生在中断上下文中。
5.修改定时器的expire
int mod_timer(struct timer_list *timer, unsigned long expires);
上述函数用于修改定时器的到期时间,在新的被传入的expires到来后才会执行定时器函数。
代码清单10.10给出了一个完整的内核定时器使用模板,在大多数情况下,设备驱动都如这个模板那
样使用定时器。
代码清单10.10 内核定时器使用模板
1/* xxx设备结构体 */
2struct xxx_dev {
3 struct cdev cdev;
4 ...
5 timer_list xxx_timer; /* 设备要使用的定时器 */
6};
7
8/* xxx驱动中的某函数 */
9xxx_func1(…)
10{
11 struct xxx_dev *dev = filp->private_data;
12 ...
13 /* 初始化定时器 */
14 init_timer(&dev->xxx_timer);
15 dev->xxx_timer.function = &xxx_do_timer;
16 dev->xxx_timer.data = (unsigned long)dev;
17 /* 设备结构体指针作为定时器处理函数参数 */
18 dev->xxx_timer.expires = jiffies + delay;
19 /* 添加(注册)定时器 */
20 add_timer(&dev->xxx_timer);
21 ...
22}
23
24/* xxx驱动中的某函数 */
25xxx_func2(…)
26{
27 ...
28 /* 删除定时器 */
29 del_timer (&dev->xxx_timer);
30 ...
31}
32
33/* 定时器处理函数 */
34static void xxx_do_timer(unsigned long arg)
35{
36 struct xxx_device *dev = (struct xxx_device *)(arg);
37 ...
38 /* 调度定时器再执行 */
39 dev->xxx_timer.expires = jiffies + delay;
40 add_timer(&dev->xxx_timer);
41 ...
42}
从代码清单第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的设
置、时间推移以及到期回调。我们可以从sound/soc/fsl/imx-pcm-fiq.c中提取出一个使用范例,如代码清单
10.11所示。
代码清单10.11 内核高精度定时器(hrtimer)使用模板
1static enum hrtimer_restart snd_hrtimer_callback(struct hrtimer *hrt)
2{
3 ...
4
5 hrtimer_forward_now(hrt, ns_to_ktime(iprtd->poll_time_ns));
6
7 return HRTIMER_RESTART;
8}
9
10static int snd_imx_pcm_trigger(struct snd_pcm_substream *substream, int cmd)
11{
12 struct snd_pcm_runtime *runtime = substream->runtime;
13 struct imx_pcm_runtime_data *iprtd = runtime->private_data;
14
15 switch (cmd) {
16 case SNDRV_PCM_TRIGGER_START:
17 case SNDRV_PCM_TRIGGER_RESUME:
18 case SNDRV_PCM_TRIGGER_PAUSE_RELEASE:
19 ...
20 hrtimer_start(&iprtd->hrt, ns_to_ktime(iprtd->poll_time_ns),
21 HRTIMER_MODE_REL);
22 ...
23}
24
25static int snd_imx_open(struct snd_pcm_substream *substream)
26{
27 ...
28 hrtimer_init(&iprtd->hrt, CLOCK_MONOTONIC, HRTIMER_MODE_REL);
29 iprtd->hrt.function = snd_hrtimer_callback;
30
31 ...
32 return 0;
33}
34static int snd_imx_close(struct snd_pcm_substream *substream)
35{
36 ...
37 hrtimer_cancel(&iprtd->hrt);
38 ...
39}
第28~29行在声卡打开的时候通过hrtimer_init()初始化了hrtimer,并指定回调函数为
snd_hrtimer_callback();在启动播放(第15~21行SNDRV_PCM_TRIGGER_START)等时刻通过
hrtimer_start()启动了hrtimer;iprtd->poll_time_ns纳秒后,时间到snd_hrtimer_callback()函数在中断上
下文被执行,它紧接着又通过hrtimer_forward_now()把hrtimer的时间前移了iprtd->poll_time_ns纳秒,这
样周而复始;直到声卡被关闭,第37行又调用了hrtimer_cancel()取消在open时初始化的hrtimer。
10.5.2 内核中延迟的工作delayed_work
对于周期性的任务,除了定时器以外,在Linux内核中还可以利用一套封装得很好的快捷机制,其本
质是利用工作队列和定时器实现,这套快捷机制就是delayed_work,delayed_work结构体的定义如代码清单
10.12所示。
代码清单10.12 delayed_work结构体
1struct delayed_work {
2 struct work_struct work;
3 struct timer_list timer;
45
/* target workqueue and CPU ->timer uses to queue ->work */
6 struct workqueue_struct *wq;
7 int cpu;
8};
我们可以通过如下函数调度一个delayed_work在指定的延时后执行:
int schedule_delayed_work(struct delayed_work *work, 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);
10.5.3 实例:秒字符设备
下面我们编写一个字符设备“second”(即“秒”)的驱动,它在被打开的时候初始化一个定时器并将其
添加到内核定时器链表中,每秒输出一次当前的jiffies(为此,定时器处理函数中每次都要修改新的
expires),整个程序如代码清单10.13所示。
代码清单10.13 使用内核定时器的second字符设备驱动
1#include <linux/module.h>
2#include <linux/fs.h>
3#include <linux/mm.h>
4#include <linux/init.h>
5#include <linux/cdev.h>
6#include <linux/slab.h>
7#include <linux/uaccess.h>
8
9#define SECOND_MAJOR 248
10
11static int second_major = SECOND_MAJOR;
12module_param(second_major, int, S_IRUGO);
13
14struct second_dev {
15 struct cdev cdev;
16 atomic_t counter;
17 struct timer_list s_timer;
18};
19
20static struct second_dev *second_devp;
21
22static void second_timer_handler(unsigned long arg)
23{
24 mod_timer(&second_devp->s_timer, jiffies + HZ); /* 触发下一次定时 */
25 atomic_inc(&second_devp->counter); /* 增加秒计数 */
26
27 printk(KERN_INFO "current jiffies is %ld\n", jiffies);
28}
29
30static int second_open(struct inode *inode, struct file *filp)
31{
32 init_timer(&second_devp->s_timer);
33 second_devp->s_timer.function = &second_timer_handler;
34 second_devp->s_timer.expires = jiffies + HZ;
35
36 add_timer(&second_devp->s_timer);
37
38 atomic_set(&second_devp->counter, 0); /* 初始化秒计数为0 */
39
40 return 0;
41}
42
43static int second_release(struct inode *inode, struct file *filp)
44{
45 del_timer(&second_devp->s_timer);
46
47 return 0;
48}
49
50static ssize_t second_read(struct file *filp, char __user * buf, size_t count,
51 loff_t * ppos)
52{
53 int counter;
54
55 counter = atomic_read(&second_devp->counter);
56 if (put_user(counter, (int *)buf))/* 复制counter到userspace */
57 return -EFAULT;
58 else
59 return sizeof(unsigned int);
60}
61
62static const struct file_operations second_fops = {
63 .owner = THIS_MODULE,
64 .open = second_open,
65 .release = second_release,
66 .read = second_read,
67};
68
69static void second_setup_cdev(struct second_dev *dev, int index)
70{
71 int err, devno = MKDEV(second_major, index);
72
73 cdev_init(&dev->cdev, &second_fops);
74 dev->cdev.owner = THIS_MODULE;
75 err = cdev_add(&dev->cdev, devno, 1);
76 if (err)
77 printk(KERN_ERR "Failed to add second device\n");
78}
79
80static int __init second_init(void)
81{
82 int ret;
83 dev_t devno = MKDEV(second_major, 0);
84
85 if (second_major)
86 ret = register_chrdev_region(devno, 1, "second");
87 else {
88 ret = alloc_chrdev_region(&devno, 0, 1, "second");
89 second_major = MAJOR(devno);
90 }
91 if (ret < 0)
92 return ret;
93
94 second_devp = kzalloc(sizeof(*second_devp), GFP_KERNEL);
95 if (!second_devp) {
96 ret = -ENOMEM;
97 goto fail_malloc;
98 }
99
100 second_setup_cdev(second_devp, 0);
101
102 return 0;
103
104fail_malloc:
105 unregister_chrdev_region(devno, 1);
106 return ret;
107}
108module_init(second_init);
109
110static void __exit second_exit(void)
111{
112 cdev_del(&second_devp->cdev);
113 kfree(second_devp);
114 unregister_chrdev_region(MKDEV(second_major, 0), 1);
115}
116module_exit(second_exit);
117
118MODULE_AUTHOR("Barry Song <[email protected]>");
119MODULE_LICENSE("GPL v2");
在second的open()函数中,将启动定时器,此后每1s会再次运行定时器处理函数,在second的
release()函数中,定时器被删除。
second_dev结构体中的原子变量counter用于秒计数,每次在定时器处理函数中调用的atomic_inc()会
令其原子性地增1,second的read()函数会将这个值返回给用户空间。
本书配套的Ubuntu中/home/baohua/develop/training/kernel/drivers/second/包含了second设备驱动以及
second_test.c用户空间测试程序,运行make命令编译得到second.ko和second_test,加载second.ko内核模块并
创建/dev/second设备文件节点:
# mknod /dev/second c 248 0
代码清单10.14给出了second_test.c这个应用程序,它打开/dev/second,其后不断地读取自/dev/second设
备文件打开以后经历的秒数。
代码清单10.14 second设备用户空间测试程序
1#include ...
2
3main()
4{
5 int fd;
6 int counter = 0;
7 int old_counter = 0;
8
9 /* 打开/dev/second设备文件 */
10 fd = open("/dev/second", O_RDONLY);
11 if (fd != - 1) {
13 while (1) {
15 read(fd,&counter, sizeof(unsigned int));/* 读目前经历的秒数 */
16 if(counter!=old_counter) {
18 printf("seconds after open /dev/second :%d\n",counter);
19 old_counter = counter;
20 }
21 }
22 } else {
25 printf("Device open failure\n");
26 }
27}
运行second_test后,内核将不断地输出目前的jiffies值:
[13935.122093] current jiffies is 13635122
[13936.124441] current jiffies is 13636124
[13937.126078] current jiffies is 13637126
[13952.832648] current jiffies is 13652832
[13953.834078] current jiffies is 13653834
[13954.836090] current jiffies is 13654836
[13955.838389] current jiffies is 13655838
[13956.840453] current jiffies is 13656840
...
从上述内核的打印消息也可以看出,本书配套Ubuntu上的每秒jiffies大概走1000次。而应用程序将不
断输出自/dec/second打开以后经历的秒数:
# ./second_test
seconds after open /dev/second :1
seconds after open /dev/second :2
seconds after open /dev/second :3
seconds after open /dev/second :4
seconds after open /dev/second :5
...
10.6 内核延时
10.6.1 短延迟
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()函数的实现方式原理与此类似。内核在启动时,会运行一个延迟
循环校准(Delay Loop Calibration),计算出lpj(Loops Per Jiffy),内核启动时会打印如下类似信息:
Calibrating delay loop... 530.84 BogoMIPS (lpj=1327104)
如果我们直接在bootloader传递给内核的bootargs中设置lpj=1327104,则可以省掉这个校准的过程,节
省约百毫秒级的开机时间。
毫秒时延(以及更大的秒时延)已经比较大了,在内核中,最好不要直接使用mdelay()函数,这将
耗费CPU资源,对于毫秒级以上的时延,内核提供了下述函数:
void msleep(unsigned int millisecs);
unsigned long msleep_interruptible(unsigned int millisecs);
void ssleep(unsigned int seconds);
上述函数将使得调用它的进程睡眠参数指定的时间为millisecs,msleep()、ssleep()不能被打断,
而msleep_interruptible()则可以被打断。
受系统Hz以及进程调度的影响,msleep()类似函数的精度是有限的。
10.6.2 长延迟
在内核中进行延迟的一个很直观的方法是比较当前的jiffies和目标jiffies(设置为当前jiffies加上时间间
隔的jiffies),直到未来的jiffies达到目标jiffies。代码清单10.15给出了使用忙等待先延迟100个jiffies再延迟
2s的实例。
代码清单10.15 忙等待时延实例
1/* 延迟100个jiffies */
2unsigned long delay = jiffies + 100;
3while(time_before(jiffies, delay));
45
/* 再延迟2s */
6unsigned long delay = jiffies + 2*Hz;
7while(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更多的作用还是避免这种读合并。
10.6.3 睡着延迟
睡着延迟无疑是比忙等待更好的方式,睡着延迟是在等待的时间到来之前进程处于睡眠状态,CPU资
源被其他进程使用。schedule_timeout()可以使当前任务休眠至指定的jiffies之后再重新被调度执行,
msleep()和msleep_interruptible()在本质上都是依靠包含了schedule_timeout()的
schedule_timeout_uninterruptible()和schedule_timeout_interruptible()来实现的,如代码清单10.16所示。
代码清单10.16 schedule_timeout()的使用
1void msleep(unsigned int msecs)
2{
3 unsigned long timeout = msecs_to_jiffies(msecs) + 1;
4
5 while (timeout)
6 timeout = schedule_timeout_uninterruptible(timeout);
7}
8
9unsigned long msleep_interruptible(unsigned int msecs)
10{
11 unsigned long timeout = msecs_to_jiffies(msecs) + 1;
12
13 while (timeout && !signal_pending(current))
14 timeout = schedule_timeout_interruptible(timeout);
15 return jiffies_to_msecs(timeout);
16}
实际上,schedule_timeout()的实现原理是向系统添加一个定时器,在定时器处理函数中唤醒与参数
对应的进程。
代码清单10.16中第6行和第14行分别调用schedule_timeout_uninterruptible()和
schedule_timeout_interruptible(),这两个函数的区别在于前者在调用schedule_timeout()之前置进程状
态为TASK_INTERRUPTIBLE,后者置进程状态为TASK_UNINTERRUPTIBLE,如代码清单10.17所示。
代码清单10.17 schedule_timeout_interruptible()和schedule_timeout_interruptible()
1signed long __sched schedule_timeout_interruptible(signed long timeout)
2{
3 __set_current_state(TASK_INTERRUPTIBLE);
4 return schedule_timeout(timeout);
5}
6
7signed long __sched schedule_timeout_uninterruptible(signed long timeout)
8{
9 __set_current_state(TASK_UNINTERRUPTIBLE);
10 return schedule_timeout(timeout);
11}
另外,下面两个函数可以将当前进程添加到等待队列中,从而在等待队列上睡眠。当超时发生时,进
程将被唤醒(后者可以在超时前被打断):
sleep_on_timeout(wait_queue_head_t *q, unsigned long timeout);
interruptible_sleep_on_timeout(wait_queue_head_t*q, unsigned long timeout);
10.7 总结

Linux的中断处理分为两个半部,顶半部处理紧急的硬件操作,底半部处理不紧急的耗时操作。tasklet
和工作队列都是调度中断底半部的良好机制,tasklet基于软中断实现。内核定时器也依靠软中断实现。
内核中的延时可以采用忙等待或睡眠等待,为了充分利用CPU资源,使系统有更好的吞吐性能,在对
延迟时间的要求并不是很精确的情况下,睡眠等待通常是值得推荐的,而ndelay()、udelay()忙等待
机制在驱动中通常是为了配合硬件上的短时延迟要求。

猜你喜欢

转载自blog.csdn.net/oushaojun2/article/details/81347080