Linux系统中断和中断处理
1. 注册中断处理程序
(1) 中断处理程序
中断处理程序是管理硬件的驱动程序的组成部分。每一设备都有相关的驱动程序,如果设备使用中断,那么相应的驱动程序就注册一个中断处理程序。驱动程序可以通过request_irq()
函数注册一个中断处理程序(声明在文件<linux/internupt.h>中),并且激活给定的中断线,以处理中断:
/* request_irq: 分配一条给定的中断线 */
int request_irq(unsigned int irq,
irq_handler_t handler,
unsigned long flags,
const char *name,
void *dev)
-
第一个参数
irq
表示要分配的中断号。 -
第二个参数
handler
是一个指针,指向处理这个中断的实际中断处理程序。只要操作系统一接收到中断,该函数就被调用:
typedef irqreturn_t (*irq_handler_t) (int, void *);
(2) 中断处理程序标志
-
第三个参数
flags
可以为0,也可能是下列一个或多个标志的位掩码。其定义在文件<linux/interrupt.h>,在这些标志中最重要的是:a.
IRQF_DISABLED
——该标志意味着内核在处理中断处理程序本身期间要禁止所有的其他中断。如果不设置,中断处理程序可以与 除本身外的其他任何中断同时运行。多数中断处理程序是不会去设置该位的,这种用法留给希望快速执行的轻量级中断。这一标志 是SA_INTERRUPT
标志的当前表现形式,在过去的中断中用以区分“快速"和“慢速”中断。b.
IRQF_SAMPLE_RANDOM
——此标志表明这个设备产生的中断对内核熵池有贡献。内核熵池负责提供从各种随机事件导出的真正的随 机数。如果指定了该标志,那么来自该设备的中断间隔时间就会作为墒填充到熵池。如果设备以预知的速率产生中断(如系统定时 器),或者可能受外部攻击者(如联网设备)的影响,那就不要设置这个标志。c.
IRQF_TIMER
——该标志是特别为系统定时器的中断处理而准备的。d.
IRQF_SHARED
——此标志表明可以在多个中断处理程序之间共享中断线。在同一个给定线上注册的每个处理程序必须指定这个标 志;否则,在每条线上只能有一个处理程序。 -
第四个参数
name
是与中断相关的设备的ASCII文本表示。这些名字会被/proc/iq和/proc/intrrupts文件使用,以便与用户通信。 -
第五个参数
dev
用于共享中断线。当一个中断处理程序需要释放时,dev
将提供唯一的标志信息(cookic),以便从共享中断线的诸多中断处理程序中删除指定的那一个。如果无须共享中断线,那么将该参数赋为空值(NULL)。内核每次调用中断处理程序时,都会把这个指针传递给它,往往会通过它传递驱动程序的设备结构。中断处理程序都是预先在内核进行注册的回调函数,而不同的函数位于不同的驱动程序中,所以在这些函数共享同一个中断线时,内核必须准确地为它们创造执行环境,此时就通过这个指针将有用的环境信息传递给它们。
request_irq()
成功执行会返回0。如果返回非0值就表示有错误发生,指定的中断处理程序不会被注册。最常见的错误是-EBUSY
,表示给定的中断线已经在使用(或者没有指定IRQF_SHARED
)。
request_irq()
函数可能会睡眠,因此不能在中断上下文或其他不允许阻塞的代码中调用该函数。在注册的过程中,内核需要在/proc/irq文件中创建一个与中断对应的项。proc_mkdir()
就是用来创建这个新的procfs
项的。proc_mkdir()
通过调用函数proc_create()
对这个新的项进行设置,而proc_create()
会调用函数kmalloc()
来请求分配内存,kmalloc()
是可以睡眠的。
(3) 释放中断处理程序
卸载驱动程序时,需要注销相应的中断处理程序,并释放中断线,需要调用:
void free_irq(unsigned int irq, void *dev)
必须从进程上下文中调用free_irq()
。如果指定的中断线不是共享的,那么,该函数删除处理程序的同时将禁用这条中断线。如果中断线是共享的,则仅删除dev
所对应的处理程序,而这条中断线本身只有在删除了最后一个处理程序时才会被禁用。
2. 编写中断处理程序
(1) 中断处理程序声明
以下是一个中断处理程序声明,它的类型与request_irq()
参数中handler
所要求的参数类型相匹配:
static irqreturn_t intr_handler(int irq, void *dev)
-
第一个参数
irq
就是这个处理程序要响应的中断的中断号。 -
第二个参数
dev
是一个通用指针,它与在中断处理程序注册时传递给request_irq()
的参数dev
必须一致。
(2) 共享的中断处理程序
- 中断处理程序的返回值是一个特殊类型:
irqreturm_t
,实际上就是int型。中断处理程序通常会标记为static
,因为它不会被别的文件中的代码直接调用。 - 中断处理程序可能返回两个特殊的值:
IRQ_NONE
和IRQ_HANDLED
。当中断处理程序检测到一个中断,但该中断对应的设备并不是在注册处理函数期间指定的产生源时,返回IRQ_NONE
;当中断处理程序被正确调用,且确实是它所对应的设备产生了中断时,返回IRQ_HANDLED
。另外,也可以使用宏IRQ_RETVAL(val)
。如果val
为非0值,那么该宏返回IRQ_HANDLED
;否则,返回IRQ_NONE
。 - 只有在以下两种情况下才能指定
IRQF_SHARED
标志以调用request_irq()
:中断线当前未被注册,或者在该线上的所有已注册处理程序都指定了IRQF_SHARED
。共享的处理程序可以混用IRQF_DISABLED
。 - 内核接收一个中断后将依次调用在该中断线上注册的每一个处理程序。如果一个处理程序与它相关的设备并没有产生中断,那么处理程序应该立即退出。这需要硬件设备提供状态寄存器或类似机制。
(2) 中断处理程序实例
- 下面是一个实际的中断处理程序,它来自real-time clock(RTC)驱动程序,可以在drivers/char/rtc.c中找到。它是一个从系统定时器中独立出来的设备,用于设置系统时钟,提供报警器(alarm)或周期性的定时器。系统时钟的设置通常只需要向某个特定的寄存器或I/O地址写入想要的时间就可以了,而报警器或周期性定时器通常得靠中断来实现。
- RTC驱动程序装载时,
rtc_init()
函数会被调用,对这个驱动程序进行初始化。它的职责之一就是注册中断处理程序:
/* 对rtc_irq注册rtc_interrupt */
if (request_irq(rtc_irq, rtc_interrupt, IRQF_SHARED, "rtc", (void *)&rtc_port)) {
printk(KERN_ERR "rtc: cannot register IRQ %d\n", rtc_irq);
return -EIO;
}
中断号由rtc_irq
指定。这个变量用于为给定体系结构指定RTC中断。例如,在PC上,RTC位于IRQ 8;第二个参数是中断处理程 序;驱动程序的名称为“rtc”;它给dev
型参传递了一个面向每个设备的实参值。
3. 中断处理机制的实现
- 设备产生中断,通过总线把电信号发送给中断控制器。如果中断线是激活的(它们允许被屏蔽),那么中断控制器就会把中断发往处理器,处理器会立即停止它正在做的事,关闭中断系统,然后跳到内存中预定义的位置开始执行那里的代码。这个预定义的位置是由内核设置的,是中断处理程序的入口点。
- 在内核中,中断的开始于预定义入口点,这类似于系统调用通过预定义的异常句柄进入内核。对于每条中断线,处理器都会跳到对应的一个唯一的位置。这样,内核就可知道所接收中断的IRQ号。初始入口点只是在栈中保存这个号,并存放当前寄存器的值(这些值属于被中断的任务);然后,内核调用函数
do_IRQ()
。do_IRQ()
的声明如下:
unsigned int do_IRQ(struct pt_regs regs)
因为C的调用惯例是要把函数参数放在栈的顶部,因此pt_regs
结构包含原始寄存器的值,这些值是以前在汇编入口例程中保存在栈 中的。中断的值也会得以保存,所以,do_IRQ()
可以将它提取出来。
- 计算出中断号后,
do_IRQ()
对所接收的中断进行应答,禁止这条线上的中断传递。在普通PC机上,这些操作由mask_and_ack_8259A()
完成。接下来,do_IRQ()
需要确保在这条中断线上有一个有效的处理程序,而且送个程序已经启动,但是当前并没有执行,这样,do_IRQ()
就调用handle_IRQ_event()
来运行为这条中断线所安装的中断处理程序。handle_IRQ_event()
方法定义在kerme/irq/handler.c中。
/**
* handle_IRQ_event - irq action chain handler
* @irq: the interrupt number
* @action: the interrupt action chain for this irq
*
* Handles the action chain of an irq event
*/
irqreturn_t handle_IRO_event(unsigned int irq, struct irqaction *action)
{
irqreturn_t ret, retval = IRQ_NONE;
unsigned int status = 0;
if (!(action->flags & IROF_DISABLED))
local_irq_enable_in_hardirq();
do {
trace_irq_handler_entry(irq, action);
ret = action->handler(irq, action->dev_id);
trace_irq_handler_exit(irq, action, ret);
switch (ret) {
case IRQ_WAKE_THREAD:
/*
*把返回值设置为已处理,以便可疑的检查不再触发
*/
ret = IRQ_HANDLED;
/*
*捕获返回值为WAKE_THREAD的驱动程序,但是井不创建一个线程函数
*/
if (unlikely(!action->thread_fn)) {
warn_nо_thread(irq, acticn);
break;
}
/*
* 为这次中断唤醒处理线程。万一线程奔溃且被杀死,我们仅仅假装已经处理了该中
断。上述的硬件中断(hardirq)处理程序已经禁止设备中断,因此杜绝irq产生
*/
if (likely(!test_bit(IRQTF_DIED,
&action->thread_flags))) {
set_bit(IROTF_RUNTHREAD, &action->thread_flags);
wake_up_process(action->thread);
}
/* Fall through to add to randomness */
case IRQ_HANDLED:
status |= action->flags;
break;
default:
break;
}
retval |= ret;
action = action->next;
} while (action);
if(status & IRQF_SAMPLE_RANDOM)
add_interrupt_randonness(irq);
local_irq_disablel();
return retval;
}
a. 因为处理器禁止中断,这里要把它们打开,就必须在处理程序注册期间指定IRQF_DISABLED
标志。IRQF_DISABLED
表示处理程序 必须在中断禁止的情况下运行。
b. 每个潜在的处理程序在循环中依次执行。如果这条线不是共享的,第一次执行后就退出循环。否则,所有的处理程序都要被执行。 c. 如果在注册期间指定了IRQF_SAMPLE_RANDOM
标志,则调用函数add_inerrupt_randomness()
。这个函数使用中断间隔时间为随 机数产生器产生熵。
d. 将中断禁止(do_IRQ()
期望中断一直是禁止的),函数返回。回到do_IRQ()
,该函数做清理工作并返回到初始入口点,然后再从这 个入口点跳到函数ret_from_intr()
。
-
ret_from_int()
例程类似于初始入口代码,以汇编语言编写。a. 这个例程检查重新调度是否正在挂起(意味着设置了
need_resched()
)。 如果重新调度正在挂起,而且内核正在返回用户空间(中断了用户进程),那么,schedule()
被调用。如果内核正在返回内核空间(中断了内核本身),只有在preempt_count
为0时,schedule()
才会被调用,否则,抢占内核是不安全的。在schedule()
返回之后,或者如果没有挂起的工作,那么,原来的寄存器被恢复,内核恢复到曾经中断的点。b. 在x86上,初始的汇编例程位于arch/x86/kemel/entry_64.S(文件entry_32.S对应32位的x86体系架构),C方法位于arch/x86/kerme/irq.c。
4. /proc/interrupts
procfs是一个虚拟文件系统,它只存在于内核内存,一般安装于/proc目录,procfs 代码位于fs/proc中。在procfs中读写文件都要调用内核函数。与此相关的例子是/proc/interrupts文件,该文件存放的是系统中与中断相关的统计信息。提供/proc/interrupts的函数是show_interupt()
,与体系结构相关。下面是从单处理器PC上输出的信息:
CPU0
0: 3602371 XT-PIC timer
l: 3048 XT-PIC i8042
2: 0 XT-PIC cascade
4: 2689466 XT-PIC uhci-hcd, eth0
5: 0 XT-PIC EMU10K1
12: 85077 XT-PIC uhci-hcd
15: 24571 XT-PIC aic7xxx
NMI: 0
LOC: 3602236
ERR: 0
- 第1列是中断线。在这个系统中,现有的中断号为0~2、4、5、12及15。
- 第2列是一个接收中断数目的计数器。
- 第3列是处理这个中断的中断控制器。XT-PIC对应于标准的PC可编程中断控制器。在具有I/O APIC的系统上,大多数会列出IO-APIC-level或IO-APIC-edge。
- 最后一列是与这个中断相关的设备名字。这个名字是通过参数devname提供给函数
request_irq()
的。如果中断是共享的,则这条中断线上注册的所有设备都会列出来。
5. 中断控制
-
Linux内核提供了一组接口用于操作机器上的中断状态。这些接口提供了能够禁止当前处理器的中断系统,或屏蔽掉整个机器的一条中断线的能力,这些例程与体系结构相关,可以在<asn/system.h>和<asm/irq.h>中找到。
-
一般来说, 控制中断系统的原因是需要提供同步。禁止中断可以确保某个中断处理程序不会抢占当前的代码。禁止中断还可以禁止内核抢占。Linux内核代码一般都需要获取某种锁,防止来自其他处理器对共享数据的并发访问。获取这些锁的同时也伴随着禁止本地中断。锁提供保护机制,防止来自其他处理器的并发访问,而禁止中断提供保护机制,防止来自其他中断处理程序的并发访问。
(1) 禁止和激活中断
- 用于禁止当前处理器上的本地中断,随后又激活它们的语句为:
local_irq_disable();
/*禁止中断*/
local_irq_enable();
这两个函数通常以单个汇编指令来实现(这依赖于体系结构)。在x86中,local irq discablel()
是cli
指令,而local_irq_enable()
是sti
指令。cli
和sti
分别是对clear
和set
允许中断标志的汇编调用。
- 如果在调用
local_irq_discable()
例程之前已经禁止了中断,那么该例程往往会带来潜在的危险;同样相应的local_irq_cenable()
例程也存在潜在危险,因为它将无条件地激括中断,尽管这些中断可能在开始时就是关闭的。所以需要一种机制把中断恢复到以前的状态而不是简单地禁止或激活。内核中一个给定的代码路径既可以在中断激活的情况下达到,也可以在中断禁止的情况下达到,这取决于具体的调用链。随着内核的不断增长,要想知道到达函数的所有代码路径将变得越来越困难,因此,在禁止中断之前保存中断系统的状态会更加安全一些。相反,在准备激活中断时,只需把中断恢复到它们原来的状态:
unsigned long flags;
local_irq_save(flags); /* 禁止中断 */
/* ... */
local_irq_restore(flags) ; /* 中断被恢复到它们原来的状态 */
- 这些方法至少部分要以宏的形式实现,因此表面上
flags
参数(定义为unsigned long
类型)是以值传递的。该参数包含具体体系结构的数据,也就是包含中断系统的状态。至少有一种体系结构把栈信息与值相结合(SPARC),flags
不能传递给另一个函数(必须驻留在同一栈帧中)。因此,对local_irq_save()
和local_irq_restore()
的调用必须在同一个函数中进行。 - 前面的所有函数既可以在中断中调用,也可以在进程上下文中调用。
(2) 禁止指定中断线
Linux提供了四个接口屏蔽掉一条中断线,对中断的状态操作之前禁止设备中断的传递:
void disable_irq(unsigned int irq);,
void disable_irq_nosync(unsigned int irq);
void enable_irq(unsigned int irq);
void synchronize_irq(unsigned int irq);
-
前两个函数禁止中断控制器上指定的中断线,即禁止给定中断向系统中所有处理器的传递。只有在当前正在执行的所有处理程序完成后,
disable_irq()
才能返回,因此调用者不仅要确保不在指定线上传递新的中断,还要确保所有已经开始执行的处理程序已全部退出。函数disable_irq_nosync()
不会等待当前中断处理程序执行完毕。 -
函数
synchronize_irq()
等待一个特定的中断处理程序的退出。如果该处理程序正在执行,那么该函数必须退出后才能返回。 -
对这些函数的调用可以嵌套。对
disable_irq()
或disable_irq_nosync()
的每次调用,都需要相应地调用一次enable_irq()
。只有在对enable_irq()
完成最后一次调用后才真正重新激活了中断线。例如,如果disable_irq()
被调用了两次,那么直到第二次调用enable_irq()
后,才能真正地激活中断线。所有这三个函数可以从中断或进程上下文中调用,而且不会睡眠。