目录
一:概述
虽然有时候只需使用I/O端口就能控制设备了,但大多数实际的设备都要更复杂一些。设备必须与外界交互,通常包括磁盘旋转、磁带移动、跨电缆的远距离数据传输等。很多外界交互需要花费多个处理器周期才能完成,速度要比处理器慢得多。让处理器等待外部事件完成几乎总是不可取的,因此必须有一种方式,让设备在发生事件时,或事件处理完成时通知处理器。 而这种通知的方式就是中断。
中断是设备给处理器发送的一个信号,以便让处理器留意和处理。Linux处理中断的方式与其用户空间处理信号的方式基本相同。对于大多数驱动程序来说,只需为其设备的中断注册一个处理程序,并在中断到达时正确地处理它们即可。当然,在这个简单的逻辑之下,还隐藏一些复杂性;特别是,由于中断处理程序的运行方式不用,中断处理程序所能执行的操作也有一定的限制。
如果没有真正的硬件设备来产生中断,就很难演示中断的使用。因此,本文中使用的示例代码是与并口(parallel port)一起工作的。在现代硬件中,这种端口已经开始变得稀缺,但幸运的是,大多数人在他们的系统上仍然能够获得可用的端口(实际上在现在电脑上,并口已被USB接口所取代,没有并口的情况下,可以用一根USB转并口线代替)。我们将用一个称为“short”的示例;来演示并口中断的产生和处理。示例名字之所以叫 “short”, 实际上不是 short int 的意思,它是(Simple Hardware Operations and Raw Tests)的简写, 以提醒我们它可以处理中断。
不过,在进入主题之前,我需要提醒大家注意一点。中断处理程序的本质是与其他代码同时运行。因此,它们不可避免地会引发并发问题以及数据结构和硬件的争用问题。如果不熟悉Linux并发技术的朋友,我建议你回头查下并发的相关资料。因为在中断工作时,对并发控制技术的理解至关重要。
二:启用中断
并口可以触发中断。打印机利用这一功能通知打印机驱动程序,准备接受缓冲区中的下一个字符。
与大多数设备一样,并口在不启用中断之前实际上不会产生中断;并口标准规定,设置端口 2 的第 4 位(0x37a、0x27a 或其他)可启用中断。在模块初始化时,short例子会通过outb来设置该位。
一旦启用中断,并口的第 10 针电信号(即所谓的 ACK 位)就会从低电平变为高电平时产生中断。强制接口产生中断的最简单方法(除非将打印机连接到端口)是将并行连接器的第 9 针和第 10 针连接在一起。
三:注册中断处理程序
如果你想真正 “看到 ”中断的产生,仅仅在硬件中启动中断是不够的,还必须在Linux系统中配置一个中断处理程序。如果没有被告知Linux 内核要待你的中断,那么它只会忽略它。
在Linux系统中,中断线是一种宝贵且有限的资源,尤其是在只有 15 或 16 条中断线的情况下。内核保存着一个中断线注册表,类似于 I/O 端口注册表。模块在使用中断线(或 IRQ,表示中断请求)之前,应先请求该中断线,并在使用完毕后释放该中断线。在许多情况下,模块还需要与其他驱动程序共享中断线,在<linux/interrupt.h> 中声明了中断线注册函数,释放中断线函数:
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);
request_irq 函数的返回值值要么是 0(表示成功),要么是负(表示错误代码)。函数返回 -EBUSY 表示另一个驱动程序已经在使用请求的中断线,这种情况并不少见。函数的参数如下:
unsigned int irq
表示请求的中断(线)编号。
irqreturn_t (*handler)(int, void *, struct pt_regs *)
表示中断处理程序的函数指针。本章稍后将讨论该函数的参数及其返回值。
unsigned long flags
表示一个与中断管理相关的掩码(稍后描述)。
const char *dev_name
传递给 request_irq 的字符串, 用于在 /proc/interrupts 中显示中断的所有者(参见下一节)。
void* dev_id
表示共享中断线的指针。它是一个唯一标识符,在释放中断线时使用,也可被驱动程序用于指向其私有数据区(用于识别正在中断的设备)。如果中断不是共享的,dev_id 可以设置为 NULL,但无论如何,使用该项指向设备结构体是个好主意。我们将在后面一节中看到 dev_id 的实际用途。
标志位(flags)可设置的位数如下:
SA_INTERRUPT
表示 “快速 ”中断处理程序。快速处理程序是在当前处理器禁用中断的情况下执行的(“快速和慢速处理程序 ”一节将介绍该主题)。
SA_SHIRQ
表示中断可在设备间共享。共享的概念在 “中断共享 ”一节中概述。
SA_SAMPLE_RANDOM
该位表示生成的中断可以为 /dev/random 和 /dev/urandom 使用的熵池作出贡献。这些设备在读取时会返回真正的随机数,旨在帮助应用软件选择安全的加密密钥。这些随机数是从由各种随机事件贡献的熵池中提取的。如果设备在真正随机的时间产生中断,则应设置此标志。相反,如果您的中断是可预测的(例如,帧捕捉器的垂直消隐),则不值得设置该标记--反正它也不会对系统熵做出贡献。可能受攻击者影响的设备不应设置此标记;例如,网络驱动程序可能会受到来自外部的可预测数据包定时的影响,因此不应为熵池做出贡献。更多信息,请参阅 drivers/char/random.c 中的注释。
中断处理程序可以在驱动程序初始化时安装,也可以在设备首次打开时安装。虽然在模块的初始化函数中安装中断处理程序听起来是个好主意,但通常并非如此,尤其是在设备不共享中断的情况下。因为中断线路的数量是有限的,所以不能浪费它们。电脑中的设备数量很容易超过中断数量。如果一个模块在初始化时请求一个 IRQ,它将阻止任何其他驱动程序使用该中断,即使持有该中断的设备从未被使用过。如果在设备打开时请求中断,则可以共享中断线。
调用 request_irq 的正确位置是设备首次打开时,即在设备产生中断之前。调用 free_irq 的位置是设备最后一次关闭时,即设备被告知不再产生中断之后。这种技术的缺点是需要保持每个设备的打开次数,以便知道何时可以禁用中断。
尽管进行了上述讨论,但 short 示例还是在加载时请求中断。因此,short 在其初始化函数(short_init)中请求中断,而不是像真正的驱动程序那样在 short_open 中请求中断。
if (short_irq >= 0) {
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 { /* actually enable it -- assume this *is* a parallel port */
outb(0x10,short_base+2);
}
}
值得一提的是,i386 和 x86_64 体系结构定义了一个查询中断线是否可用的函数:
int can_request_irq(unsigned int irq, unsigned long flags);
如果中断的尝试分配成功,该函数将返回一个非零值。
四:自动检测中断(线)号
对于驱动程序来说&#x