分类:
2011-09-11 11:14:32
一个中断不过是硬件在它需要处理器的关注时发出的信号。Linux 处理中断的方式非常类似它处理用户空间信号的方式。在大多数情况下, 驱动只需要为它的设备中断注册一个处理函数,并且在中断到来时进行正确地处理。 当然, 在这个简单图像之下有一些复杂; 特别地, 中断处理有些受限于它们能够进行的动作, 这是它们如何运行而导致的结果.
由于中断处理函数自身的特性,它们与其他的代码并行运行。因此,它们不可避免地引起并发问题和对数据结构与硬件的竞争。透彻理解并发控制技术(见第5章),对于中断来说,非常重要。
1、设置(安装、注册)中断处理函数(interrupt handler)
如果你想"看到"中断的效果,光使硬件设备产生一中断信号是不足够,还需要在系统中设置一个中断处理函数。如果Linux内核没有被告知要处理某一中断,那么内核只是简单地确认(收到了中断)并忽略它。
Linux内核维护着一个类似于I/O端口注册表的中断信号线注册表。模块在使用中断前,要先请求一个中断通道(或者IRQ中断请求),并在用完后释放它。有时驱动程序可能还需要与其他驱动共享中断线。下面的函数声明在
int request_irq(unsigned int irq,
irqreturn_t (*handler)(int, void *, struct pt_regs *),
unsigned long flags,
const char *dev_name,
void *dev_id);
void free_irq(unsigned int irq, void *dev_id);
说明:
1)、unsigned int irq:请求的中断号
2)、irqreturn_t (*handler)(int, void *, struct pt_regs *):中断处理函数指针(后面我们会详细讨论这个函数的参数和返回值)
3)、unsigned long flags:指定中断管理相关的位掩码选项。flags 中可以设置的位如下:
SA_INTERRUPT
表示这是一个快速中断处理,快速中断处理函数执行时会禁止当前处理器上的中断(详见1.3节)。
SA_SHIRQ
表示可以多设备共享该中断(详见第5节)。
SA_SAMPLE_RANDOM
表示该中断能够被/dev/random和/dev/urandom使用的加密池利用。读取这些设备在时返回真正的随机数,这样的设计可以帮助应用软件选择用于加密的安全密钥。而这些随机数是从一个由各种随机事件贡献的加密池中提取的。如果你的设备以真正随机的时间产生中断,你应当设置这个标志;相反,你的中断是可预测的,就不要设置该标志了(因为它无论如何不会对系统加密有贡献)。另外,可能受攻击者影响的设备不应当设置这个标志,例如,网络驱动易遭受从外部预测数据包时间,因此,它不应当设置该位(详见 drivers/char/random.c 的注释)。
4)、const char *dev_name:该中断号的拥有者(一般就是设备名),用在/proc/interrupts来显示中断的拥有者(见下1.1小节)
5)、void *dev_id:用于共享中断线的指针。它是一个唯一标识,一般用在释放中断线时,同时还可能被驱动用来指向它自己的私有数据区(来标识哪个设备正在中断)。如果中断没有被共享,dev_id 可以设置为NULL,但推荐用它指向设备的数据结构。
6)、request_irq的返回值:成功则返回0;否则返回一个负的错误码。通常,所请求的中断线已被另一个驱动占用时,该函数返回-EBUSY。
中断处理函数可以在驱动初始化时设置,也可以在设备第一次打开时设置。但推荐在设备第一次打开时调用request_irq设置中断处理函数,这样可以更有效地利用和贡献有限的中断号。这样,调用free_irq的位置就是设备最后一次被关闭时,在硬件被告知不要再中断处理器之后。这个方法的缺点是必须为每一个设备维护一个打开计数。
以下是设置中断处理函数的例子
i386 和x86_64体系定义了一个函数来检查一个中断线是否可用:
if (short_irq >= 0)
{ /* 为中断号short_irq设置中断处理函数short_interrupt,该中断为快中断,并不支持中断共享 */
result = request_irq(short_irq, short_interrupt,
SA_INTERRUPT, "short", NULL);
if (result) {
printk(KERN_INFO "short: can't get assigned irq %i\n",
short_irq);
short_irq = -1;
} else { /* 使能并口中断 */
outb(0x10,short_base+2);
}
}
int can_request_irq(unsigned int irq, unsigned long flags);
若中断线可用,该函数返回一个非零值。但注意,即便can_request_irq成功,并不代表后面的request_irq一定会成功,因为检查和后面的设置并不是一个原子操作。
1.1、/proc 接口
当硬件中断到达处理器时,内核会递增该中断对应的内部计数器,这一方法可用来检查设备是否按预期工作。读取/proc/interrupts可以获取系统的中断报告。这是我博创2410s开发板某一时刻/proc/interrupts文件的内容:
root@lingd-arm2410s:/proc# cat interrupts
CPU0
18: 878 s3c-ext0 NE2000
25: 0 s3c s3c2410-wdt
30: 134105 s3c S3C2410 Timer Tick
32: 0 s3c s3c2410-lcd
42: 88 s3c ohci_hcd:usb1
43: 0 s3c s3c2410-i2c
70: 331 s3c-uart0 s3c2410-uart
71: 3684 s3c-uart0 s3c2410-uart
Err: 0
第一列是IRQ号,这里你可以看出/proc/interrupts 只显示系统当前设置了中断处理函数的中断号。第二列表示CPU0共处理了多少个中断(在其他系统中可能CPU1、CPU2等等)。最后1列是该中断号的拥有者,就是传递给request_irq的dev_name参数,从这一列可以看出中断号是否被共享了(即有多个拥有者)。
/proc/stat记录了几个关于系统活动的底层统计信息,包括(但不限于)自系统启动以来收到的中断数。/proc/stat文件每一行以一字符串开始,该字符串为该行的关键字:intr标志是中断计数。以下是我博创2410s开发板某一时刻/proc/stat文件的内容:
cpu 75 0 720 67657 1 614 14 0 0
cpu0 75 0 720 67657 1 614 14 0 0
intr 143743 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 910 0 0 0 0 0 0 0 0 0 0 0 138164 0 0 0 0 0 0 0 0 0 0 0 88 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 371 3814 0 0 0 0 0 396 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
ctxt 20231
btime 0
processes 950
procs_running 2
procs_blocked 0
intr行:第一个数是所有中断的总数,而其他的每一个数代表一个中断号的中断计数, 从中断号0开始(包括系统所有中断号的计数,即便中断号没有设置中断处理函数)。
1.2、自动检测 IRQ 号
决定设备要使用哪个IRQ线是驱动在初始化时最有挑战性的问题之一。驱动需要信息来正确设置中断处理函数。自动检测中断号是一个驱动可用性的基本需求。
有时自动探测依赖于设备的一些缺省属性(如I/O寄存器基地址)。以下是典型的并口中断号探测代码:
if (short_irq < 0) /* not yet specified: force the default on */
switch(short_base) {/* 依据I/O寄存器基地址决定中断号 */
case 0x378: short_irq = 7; break;
case 0x278: short_irq = 2; break;
case 0x3bc: short_irq = 5; break;
}
有时还会允许用户在加载驱动时覆盖缺省值:
insmod ./short.ko irq=x
有些设备有能力告知驱动它们要使用的中断号。在这个情况下,驱动通过从设备的I/O 端口或者PCI 配置空间读一个状态字节来获取中断号。这时自动探测中断号只是探测设备,而无需做额外的工作来探测中断。
然而,并不是每个设备都对程序员那么友好,有时还是需要对它们做一些探测工作。这个工作也非常简单: 驱动告知设备产生中断并观察发生了什么. 如果一切顺利,只有一个中断线被激活。尽管探测在理论上简单的,但实际的实现并不简单,我们有2种方法可以用来实现探测: 调用内核定义的辅助函数和DIY探测。
1.2.1、调用内核定义的辅助函数
Linux 内核提供了一个底层接口来探测中断号,且只能在非共享中断模式下工作,包括2 个函数,在
/* probe_irq_on返回一个可分配的中断的位掩码. 驱动必须保留它返回的位掩码, 并且在后面传递给 probe_irq_off. 在这个调用之后, 驱动应当安排它的设备产生至少一次中断 */
unsigned long probe_irq_on(void);
/* 在设备已请求一个中断后, 驱动调用probe_irq_off, 将之前由 probe_irq_on 返回的位掩码作为参数传递给probe_irq_off. probe_irq_off 返回在"probe_on"之后发出的中断号. 如果没有中断发生, 返回 0 (因此, IRQ 0是不能探测的, 但是没有用户设备能够在任何支持的体系上使用它). 如果产生了多次中断(模糊的探测), probe_irq_off返回一个负值 */
int probe_irq_off(unsigned long);/*
* 自动探测IRQ的一般步骤:
* 1. 清除或者禁止设备的内部中断
* 2. 必要时,打开所有中断
* 3. irqs = probe_irq_on(); // 接收所有未分配的空闲的IRQs
* 4. 使能设备,并使设备发起中断
* 5. 然后延迟一小会,等待设备中断
* 6. irq = probe_irq_off(irqs); // 获取设备中断号, 0=none, negative=multiple
* 7. 处理设备被挂起的中断
* 8. 必要时,重复该过程
*/
注意:程序员应当在调用probe_irq_on之后,使能设备的中断,而在调用probe_irq_off之前,禁止设备中断。另外,你必须记住在 probe_irq_off 之后服务设备中挂起的中断。
ldd3中short 模块演示了如何使用这样的探测。如果你加载模块使用probe=1,下列代码被执行来探测你的中断线,如果并口的管脚9和10连接在一起:
|
探测可能是一个比较耗时的任务。因此,在模块初始化时探测中断线是最好的,只需探测一次,不管你是在设备打开时设置中断处理函数还是在模块初始化函数中设置中断处理函数。
大部分体系定义了这些探测函数( 即便它们是空的 )来简化设备驱动的移植。
1.2.2、 Do-it-yourself (DIY)探测
驱动自己DIY探测也不是很难。DIY探测与前面描述的相同: 使能所有未使用的中断,接着等待并观察发生什么。正如我们队设备地了解,通常地一个设备就使用那三四个设备常用的IRQ中的一个,只探测这些IRQ使我们不必探测所有IRQ就能够探测到正确IRQ号。
short驱动假定3、5、7和9是唯一可能的IRQ值。这些数实际上是并口设备允许你选择的数。
|
中断处理函数:
|
1.3、快速和慢速中断处理函数
老版本的Linux内核尽了很大努力来区分"快速"和"慢速"中断。快速中断是那些能够很快处理的中断,而处理慢速中断会消耗更长的时间。在处理慢速中断时处理器会重新使能中断,以避免快速中断被延时过长。
在现代内核中,快速和慢速中断的大部分部分已经消失,仅剩下: 快速中断(那些使用SA_INTERRUPT的)执行时禁止当前处理器上所有其他中断。注意,其他的处理器仍然能够处理中断。除非你有充足的理由——必须在禁止其他中断情况下运行你的中断处理函数,否则你不应当使用SA_INTERRUPT。
1.3.1、x86上中断处理内幕
这个描述是从2.6内核源码的arch/i386/kernel/irq.c、arch/i386/kernel/ apic.c、arch/i386/kernel/entry.S、arch/i386/kernel/i8259.c和include/asm-i386/hw_irq.h推导得出的。尽管基本概念相同,但硬件细节与其他体系不同.
汇编语言文件entry.S包含了底层中断处理的代码。在所有情况下,这个代码将中断号压栈并跳转到一个称为do_IRQ公共段(在irq.c中定义)。do_IRQ 做的第一件事是确认中断以便中断控制器能够继续其他事情。接着它获取给定IRQ号的自旋锁,阻止任何其他CPU处理这个IRQ。然后清除几个状态位(包括IRQ_WAITING),并查找该IRQ的处理函数。如果没有找到,就什么都不做,释放自旋锁,处理任何挂起的软件中断,最后do_IRQ返回。
实际的中断处理函数由函数handle_IRQ_event调用。如果该中断是是慢速的(没有设置SA_INTERRUPT),硬件会重新使能中断,并调用中断处理函数。接着就是一些清理工作、运行软件中断以及回到正常的工作。"正常的工作"很可能已经由于中断而改变了(如处理者可能唤醒一个进程),因此从中断中返回的最后可能会发生的事情是重新调度处理器。
探测IRQ是通过为每个当前缺乏中断处理函数的IRQ设置IRQ_WAITING状态来完成。当中断发生时,因为没有注册中断处理函数,do_IRQ仅是清除IRQ_WAITING然后返回。当probe_irq_off被调用时,只需要搜索没有IRQ_WAITING设置的IRQ。
2、实现中断处理函数
中断处理函数唯一的特别之处是中断处理函数只在中断时运行。因此,它会有一些限制:
1)、中断处理函数不能与用户空间传递数据,因为它不在进程上下文执行。
2)、中断处理函数也不能做任何可能引起睡眠的事情,例如调用wait_event,使用除GFP_ATOMIC之外任何东西来分配内存,或者加锁一个信号量。
3)、中断处理函数不能调用schedule()。
中断处理函数的作用:告知相关的硬件设备中断已收到,并根据该中断的含义读/写数据。中断处理函数的第一步常常包括清除设备的一个中断标志位,大部分硬件设备不再产生中断直到"中断挂起"位被清除。这要根据你的硬件原理决定,这一步可能需要在最后做而不是开始;这里没有通吃的规则。有些设备不需要这一步,因为它们没有一个"中断挂起"位;但这样的设备比较少。
中断处理的典型任务:唤醒睡眠在设备上的进程,如果中断通知它们等待的事件已经发生,如新数据到达。
不管是快速或慢速中断,程序员应编写执行时间尽可能短的中断处理函数。如果需要进行长时间计算,最好的方法是使用一个tasklet或者workqueue在一个更安全的时间来调度计算任务(第4节会介绍如何延时任务)。
2.1、 中断处理函数的参数和返回值
传递给中断处理函数的参数有irq、dev_id和regs:
1)、int irq:中断号,在打印log消息是可能有用的。
2)、void *dev_id:是一种用户数据类型。传递给request_irq的void* dev_id参数,会在发生中断时,作为参数传回给中断处理函数。我们常常传递一个指向设备数据结构的指针给dev_id,这样一个管理若干相同设备的驱动在中断处理函数中不需要任何额外的代码,就能找出哪个设备产生了当前的中断事件。
这个参数在中断处理函数中的典型应用:
|
static void sample_open(struct inode *inode, struct file *filp)
{
struct sample_dev *dev = hwinfo + MINOR(inode->i_rdev);
request_irq(dev->irq, sample_interrupt,
0 /* flags */, "sample", dev /* dev_id */);
/*....*/
return 0;
}
3)、struct pt_regs *regs:为进入中断状态前的处理器上下文的快照。很少用到,在Linux2.6.24.7内核里已无这个参数。
中断处理函数应当返回一个值指示是否真正有一个中断需要处理。如果中断处理函数发现设备确实需要处理,应当返回IRQ_HANDLED;否则返回IRQ_NONE。宏TRQ_RETVAL用于产生中断处理函数的返回值:
|
如果你能够处理中断,x为非零。中断处理函数的返回值被内核用来检测和禁止假中断。如果无法确定设备是否产生了中断,则应当返回IRQ_HANDLED。
2.2、使能和禁止中断
有时设备驱动必须禁止中断一段时间(希望地短)。并且在驱动持有一个自旋锁时,必须禁止中断,来避免死锁。注意,应尽可能避免禁止中断应当,即便在设备驱动中,并且不能将禁止中断作为驱动的互斥机制。
2.2.1、禁止单个中断
有时(但是很少)驱动需要禁止一个特定的中断。不鼓励这么做,并且不能禁止共享的中断线(在现代的系统中, 共享中断是很常见的)。内核提供了3个函数(声明在
/* disable_irq禁止给定的中断,如果当前该IRQ还有中断处理函数在运行,disable_irq会等待该中断处理函数运行结束。如果调用disable_irq的线程持有中断处理函数需要的任何资源(例如自旋锁),系统可能会死锁 */
void disable_irq(int irq);
/* disable_irq_nosync禁止给定的中断并立刻返回(可能引入竞争情况) */
void disable_irq_nosync(int irq);
void enable_irq(int irq);
调用任一函数可能会更新可编程控制器(PIC)中特定irq的掩码,从而实现禁止或使能跨所有处理器的特定 RQ。这些函数可以嵌套调用:如果对某一IRQ连续调用2次disable_irq,需要为该IRQ调用2 次enable_irq才能真正重新使能该IRQ。可以在中断处理函数中调用这些函数,但是在处理某一IRQ时再使能它常常不是一个好做法。
2.2.2、禁止所有中断
在2.6内核,可用以下2个函数(定义在
/* local_irq_save在保存当前中断状态到flags后禁止当前处理器所有中断. 注意, flags是直接传递, 不是通过指针. */
void local_irq_save(unsigned long flags);
/* local_irq_disable禁止当前处理器所有中断,但不保存状态; 只有在你确定中断没有在别处被禁止时,使用local_irq_disable. 不象disable_irq, local_irq_disable 不跟踪多次调用. 如果调用链中有多个函数需要禁止中断, 应该使用local_irq_save. */
void local_irq_disable(void);
/* local_irq_restore打开中断,并恢复由local_irq_save存储于flags的中断状态 */
void local_irq_restore(unsigned long flags); /* local_irq_enable 无条件打开中断 */
void local_irq_enable(void);
在2.6内核,没有方法可以全局禁止整个系统上的所有中断。
3、顶半部和底半部
中断处理的一个主要问题是如何在中断处理函数中进行耗时的任务。常常为了响应一个设备中断需要完成大量的工作,但是中断处理需要很快完成并且不使中断阻塞太久。
Linux (连同许多其他系统)通过将中断处理分为2半来解决这个问题。所谓的顶半部是实际响应中断的函数——使用request_irq注册的那个中断处理函数;而底半部是由顶半部调度,并在稍后一个更安全的时间执行的函数。两者最大的不同是:在底半部执行时所有中断都打开了(这就是所谓的它在一个更安全的时间运行)。在典型的场景中,顶半部保存设备数据到一个设备特定的缓存,并调度它的底半部,然后退出(这个操作非常快)。底半部接着进行任何其他需要的工作,例如唤醒进程、启动另一个I/O操作等等。这么做的好处是:在底半部还在工作的期间,顶半部可以继续为新到的中断服务。
Linux 内核有2个不同的机制(都在第7章介绍了)可用来实现底半部处理:
1)、tasklet 常常是底半部处理的首选机制;它非常快,但是所有的tasklet代码必须是原子的。
2)、工作队列,它可能有一个更高的延迟,但是允许睡眠。
4、共享中断
Linux内核在所有总线上支持共享中断,甚至是那些(例如 ISA 总线)传统上不支持共享中断的。2.6内核的设备驱动应当使用共享中断,如果目标硬件支持该操作模式。
4.1、设置支持共享中断的中断处理函数
共享中断也是通过request_irq来设置中断处理函数,但它与非共享的有2点不同:
1)、request_irq时,flags参数中必须指定SA_SHIRQ位
2)、dev_id 参数必须是唯一的。dev_id可以是任何模块地址空间的指针,但是不能为NULL
内核为每个中断号维护一个共享中断的中断处理函数列表,而dev_id就是用来区分不同中断处理函数的标识符。如果有2个驱动在同一个中断号上设置NULL作为它们的标识符,在卸载时可能就乱了、在中断到来时引发内核oops。由于这个理由,如果在设置共享中断时传了一个NULL的dev_id ,内核会大声抱怨。当请求一个共享的中断时,如果满足以下任一条件,request_irq 成功:
1)、中断线空闲
2)、所有已经注册该中断线的中断处理函数都设置了SA_SHIRQ
当有2个以上的驱动共享同一中断线时,并且硬件在这条线上中断了处理器,内核为这个中断调用每个已注册的中断处理函数,并传递给每个中断处理函数它注册时的dev_id。因此,共享中断的中断处理函数必须能够识别它自己的中断,并且在意识到不是自己的设备发起中断时快速退出(返回IRQ_NONE)。
没有探测函数可给共享中断的时使用,因为标准的探测机制只有在设备要使用的中断线是空闲时才有效。
释放中断处理函数时还是使用 free_irq,此时dev_id 参数用来决定要释放的是该共享中断的中断处理函数列表中的哪个函数。
注意,共享中断的驱动:它不能使用enable_irq或者disable_irq;否则其他共享这条线的设备就无法正常工作了。即便短时间禁止中断,另一设备也可能产生延时而为设备和其用户带来问题。所以程序员必须记住:他的驱动并不是独占这个IRQ,它的行为应当比独占这个中断线更加"社会化"。
5、中断驱动的I/O
当与驱动管理的硬件传输数据时,可能会因为某种原因而延迟。驱动编写者应当实现缓存机制。
一个好的缓存机制产生了中断驱动的I/O,一个输入缓存在中断时填充,并由读取设备的进程从缓存中取走数据;一个输出缓存由写设备的进程填充,并在中断时将发送给设备(典型例子串口uart)。
为正确地进行中断驱动的数据传送,硬件应当能够按照以下语义产生中断:
输入:当新数据到达且准备好被处理器获取时,设备中断处理器;
输出:当设备准备好接受新数据或者确认一个成功的数据传送时,设备中断处理器。