Linux中断和中断处理程序

背景

Linux中断上半部,参见Linux中断和中断处理程序
Linux中断下半部,参见Linux中断下半部及推后执行的工作

这部分讲Linux内核中断和中断处理程序。

[======]

中断

硬件中断 -- 异步中断
中断本质上是一种电信号,由硬件设备发出,用于通知处理器特定事件。
不同设备对应不同中断,每个中断通过唯一的数字标识,称为中断请求(IRQ)线。处理器内部对其编号,也称为中断号。

异常 -- 同步中断
异常不同于中断,产生时必须考虑与处理器时钟同步。异常常称为同步中断,是处理器执行时由于编程失误而导致的错误指令(如除0),或者执行期间出现特殊情况(如缺页),必须靠内核来处理时,处理器就会产生一个异常。
而我们通常所说的是中断,是指由硬件产生的异步中断。

由于许多处理器体系结构处理异常与处理中断的方式类似,因此内核对它们的处理也类似。

软中断
工作方式类似于异步中断,区别是它是通过软件引起的中断,异步中断是由硬件引起的。

[======]

中断处理程序

响应一个特定中断时,内核会执行一个函数,称为中断处理程序(interrupt handler)或中断服务例程(interrupt service routine,ISR)。设备产生的每个中断,都有一个中断处理程序关联。一个设备可能产生多种不同的中断,那么该设备就可以对应多个中断处理程序。相应地,设备驱动程序就需要准备多个这样的函数。

Linux中断处理程序特点
Linux中,中断处理程序看起来像普通C函数,按特定类型声明,以便内核能以标准的方式传递处理程序的信息。
中断处理程序与其他内核函数的区别:中断处理程序是被内核调用来响应中断的,而它们运行于被称为中断上下文的特殊上下文中。

由于中断随时可能产生,因此中断处理程序必须随时、尽快执行,这样才能尽快响应中断,同时恢复中断代码的执行。

上下半部的对比
一般把中断处理切分为2个部分或两半:
1)上半部(top half)
中断处理程序是上半部,接收到一个中断,就立即开始执行,但只做严格时限的工作。如对接收中断进行应答或复位硬件,这些工作都是在所有中断被禁止的情况下完成的。
2)下半部(bottom half)
中断处理程序中,能被允许稍后完成的工作会推迟到下半部。Linux提供下半部的各种机制。
本章考察上半部,下一章考察下半部。

[======]

注册中断处理程序

如果设备使用中断(大部分设备这样做),那么相应的驱动程序就需要注册一个中断处理程序。

驱动程序注册并激活一个中断处理程序:

/* request_irq: 分配一条给定的中断线 */
int request_irq(unsigned int irq,  /* 要分配的中断号 */
                irqreturn_t (*handler)(int, void *, struct pt_regs *), /* 指向实际中断处理程序, 收到中断时由内核调用 */
                unsigned long irqflags, /* 标志位 */
                const char* devname, /* 中断相关的设备ASCII文本表示法 */
                void *dev_id); /* 用于共享中断线 */

参数说明:

  • irq 表示要分配的中断号。对某些设备,该值通常预先固定的;对于大部分设备,该值可以探测,或者通过编程动态决定。

  • handler 一个函数指针,指向该中断的实际中断处理程序。OS收到中断,该函数就会被调用。

  • irqflags 可以为0,也可以是下面一个或多个标志的位掩码:
    1)SA_INTERRUPT 表明给定的中断处理程序是一个快速中断处理程序(fast interrupt handler),默认不带该标志。过去,Linux将中断处理程序分为快速和慢速两种。哪些可以迅速执行但调用频率可能会很高的中断处理程序会贴上这样的标签。这样做,通常需要修改中断处理程序的行为,使得它们能尽可能快地执行。现在,加不加此标志的区别只剩一条:在本地处理器上,快速中断处理程序在禁止所有中断的情况下运行。好处是可以快速执行,不受其他中断干扰。默认没有该标志,除了正运行的中断处理程序对应那条中断线,其他所有中断都是激活的。除了时钟中断,绝大多数中断都不使用该标志。
    2)SA_SAMPLE_RANDOM 表明该设备产生的中断对内核熵池(entropy pool)有贡献。内核熵池负责提供从各种随机事件导出真正的随机数。如果指定该标志,那么来自该设备的中断时间间隔就会作为熵填充到熵池。如果你的设备以预知的速率产生中断(如系统定时器),或可能受到外部攻击(如网卡)的影响,那么就不要设置该标志。
    3)SA_SHIRQ 表明可以在多个中断处理程序之间共享中断线。在同一个给定线上注册的每个处理程序必须指定这个标志;否则,在每条线上只能有一个处理程序。

  • devname 与中断相关的设备的ASCII文本表示法。例如,PC机上键盘中断对应的这个值为“keyboard”。这些名字会被/proc/irq和/proc/interrupt文件使用,以便与用户通信。

  • dev_id 用于共享中断线。当一个中断处理程序需要释放时,dev_id将提供唯一的标志信息(cookie),以便从共享中断线的诸多中断处理程序中删除指定的那一个。如果没有该参数,那么内核不可能知道在给定的中断线上到底要删除哪个处理程序。如果无需共享中断线,那么该参数置为NULL即可;如果需要共享,那么必须传递唯一的信息。除非设备又旧又破且位于ISA总线上,那么必须支持共享中断。另外,内核每次调用中断处理程序时,都会把这个指针传递给它(中断处理程序都是预先在内核进行注册的回调函数(callback function)),不同的函数位于不同的驱动程序中,因此这些函数共享同一个中断线时,内核必须准确为它们创造执行环境,此时就可以通过该指针将有用的环境信息传递给它们了。实践中,往往通过它传递驱动程序的设备结构:这个指针是唯一的,而且有可能在中断处理程序内及设备模式中被用到。

返回值:
request_irq()执行成功时,返回0;出错时,返回非0,指定的中断处理程序不会被注册。常见错误-EBUSY,表示给定的中断线已经在使用(或当期用户或者你没有指定SA_SHIRQ)。

注意:
request_irq()可能会睡眠,因此不能在中断上下文或其他不允许阻塞的代码中调用。

request_irq()为什么会睡眠?
因为在注册过程中,内核需要在/proc/irq文件创建一个与中断对应的项。proc_mkdir()就是用来创建这个新的procfs项的,proc_mkdir()通过调用proc_create()对这个新的profs项进行设置,而proc_create()会调用kmalloc()来请求分配内存。kmalloc()是可以睡眠的。

通过request_irq()注册中断处理程序典型应用:

if (request_irq(irqn, my_interrupt, SA_SHIRQ, "my_device", dev)) { // not 0: error
    printk(KERN_ERR "mydevice:cannot register IRQ %d\n", irqn);
    return -EIO;
}

irqn 请求的中断线,my_interrupt 中断处理程序,"my_device" 设备名称,通过dev_id传递dev结构体。返回非0,则表示注册失败;返回0,表示注册成功,响应中断时处理程序会被调用。

释放中断处理程序
卸载驱动程序,需要注销相应的中断处理程序,并释放中断线。调用:

void free_irq(unsigned int irq, void* dev_id);

如果指定的中断线不是共享的,那么该函数将删除处理程序同时禁用该中断线;如果是共享的,则仅删除dev_id对应的处理程序,而这条中断线只有在删除了最后一个处理程序时才会被禁用。这也是dev_id必须唯一的原因:共享的中断线中,用来区分不同的处理程序。

必须从进程上下文中调用free_irq();

[======]

编写中断处理程序

一个典型的中断处理程序声明:

static irqreturn_t intr_handler(int irq, void *dev_id, struct pt_regs *regs);

其签名必须与request_irq()中参数handler所要求函数类型一致。

参数说明:

  • irq 该处理程序要响应的中断的中断线号。目前,没有太大用处,可能打印LOG时会用到。内核2.0之前,因为没有dev_id参数,必须通过irq才能区分使用相同中断程序、相同的中断处理程序的多个设备。如,具有多个相同类型硬盘驱动控制器的计算机。
  • dev_id 与中断处理程序注册时传递给request_irq()的参数dev_id必须一致。如果该值具有唯一确定性(建议采用这样的值,以便支持共享),那么它就相当于一个cookie,可以用来区分共享同一个中断处理程序的多个设备。另外,dev_id也可能指向一个中断处理程序使用的一个数据结构,对每个设备而言,设备结构都是唯一的。
  • regs 指向结构的指针,包含处理中断前处理器的寄存器和状态。除了调试,其他时候很少使用。

返回值:
返回值是一个特殊类型:irqerturn_t。实际是int类型,为了与早期内核兼容,因为2.6以前实际是void类型。
可能返回2个特殊值:IRQ_NONE和IRQ_HANDLED。当中断处理程序检测到一个中断,但该中断并非注册处理函数时指定的产生源时,返回IRQ_NONE;当中断处理程序被正确调用,而且确实是它所对应的设备产生的中断时,返回IRQ_HANDLED。
返回值也可以用宏IRQ_RETVAL(x):x为非0,宏返回IRQ_HANDLED;x为0,返回IRQ_NONE。

定义说明:
中断处理程序通常标记为static,因为从来不会在其他文件被直接调用。

可重入性
Linux中中断处理程序无需重入。当一个给定的中断处理程序正在执行时,相应的中断线在所有处理器上都会被屏蔽,以防在同一个中断线上接收另一个新的中断。也就是说,同一个中断线,同一个中断处理程序,在执行完毕之前不可能同时在2个处理器上执行,加上中断处理程序不能休眠,因此无需考虑可重入性。
注意:
1)如果中断处理程序可以被中断(不是同一个中断线的中断),称为嵌套中断,或中断嵌套。
2)旧版本Linux允许中断嵌套,但2010以后提交的版本中,已经禁止了中断嵌套。参考Linux的中断可以嵌套吗? | 知乎

共享的中断处理程序

共享的和非共享的处理程序,注册和运行方式相似,差异主要有三点:
1)request_irq()的参数flgs必须设置SA_SHIRQ标志。
2)对每个注册的中断处理程序来说,dev_id参数必须唯一。指向任一设备结构的指针就可以满足这一要求;通常会选设备结构,因为它是唯一的,而且中断处理性可能会用到。不能给共享的处理程序传递NULL值。
3)中断处理程序必须能区分其设备是否真正产生中断。既需要硬件支持,也需要处理程序中有相关的处理逻辑。

所有共享中断线的驱动程序,都必须满足以上要求。否则,中断线无法共享。

中断处理程序实例

一个来自RTC驱动程序的中断处理程序,可在drivers/char/rtc.c中找到。RTC用于设置系统时钟,提供alarm或周期性定时器。

RTC驱动程序装载时,rtc_init()会被调用,对驱动程序进行初始化。
1)注册中断处理程序:

/* 对RTC_IRQ 注册rtc_interrupt */
if (request_irq(RTC_IRQ, rtc_interrupt, SA_INTERRUPT, "rtc", NULL)) {
    printk(KERN_ERR "rtc:cannot register IRQ %d\n", RTC_IRQ);
    return -EIO;
}

SA_INTERRUPT指明是fast IRQ,会禁用其他中断,该中断线也不允许共享,因此处理程序不会用到什么特殊值,传入dev_id
可以是NULL。

2)定义处理程序本身

/*
 * 很小的中断处理程序, 运行时设置了SA_INTERRUPT, 但可能与set_rtc_mmss() 调用产生冲突(rtc中断和时钟中断可以轻易在两个不同的CPU上同时运行). 因此, 我们需要rtc_lock自旋锁串行访问该芯片. 自旋锁的实现在时钟代码中, 与体系结构相关.
 * 关于set_rtc_mmss(), 参见 ./arch/XXXX/kernel/time.c
 *
 */
static irqreturn_t rtc_interrupt(int irq, void* dev_id, struct pt_regs* regs) {
    /*
     * 可以是alarm中断, 更新完成的中断或周期性中断. 我们把状态保存在rtc_irt_data的低字节中, 而把最后一次读之后所接收的中断号保存在其余字节中
     */
    spin_lock(&rtc_lock);
    {
        rtc_irq_data += 0x100;
        rtc_irq_data &= ~0xFF;
        rtc_irq_data |= (CMOS_READ(RTC_INTR_FLAGS) & 0xF0);

        if (rtc_status & RTC_TIMER_ON)
            mod_timer(&rtc_irq_timer, jiffies + HZ / rtc_freq + 2 * HZ / 100); /* 修改并激活定时器 */
    }    
    spin_unlock(&rtc_lock);

    /* 执行其余操作 */
    spin_lock(&rtc_task_lock);
    {
        if (rtc_callback)
            rtc_callback->func(rtc_callback->private_data); /* 调用预先设置好的回调函数. RTC允许预先注册一个回调函数 */
    }
    spin_unlock(&rtc_task_lock);
    wake_up_interruptible(&rtc_wait); /* 唤醒一个等待队列上的可中断任务 */

    kill_fasync(&rtc_async_queue, SIGIO, POLL_IN); /* 向用户发送可读信号 */
    return IRQ_HANDLED; /* 该中断处理程序被正确调用 */
}

[======]

中断上下文

当执行一个中断处理程序(中断上半部)或中断下半部时,内核处于中断上下文(interrupt context)中。
进程上下文是一种内核所处的模式,此时内核代表进程执行 -- 例如,执行系统调用或运行内核线程。进程上下文中,可通过current宏关联当前进程。此外,因为进程是以进程上下文的形式连接到内核中,因此,在进程上下文可以睡眠,也可以调用调度程序。

中断上下文的特性:
1)不能睡眠
中断上下文和进程没有什么关系,跟current宏也不相干(尽管它会指向被中断的进程)。因为没有进程的背景,所以中断上下文不可以睡眠,否则怎么再对它重新调度(处理器调度的是进程)?
2)严格时间限制,尽可能迅速、简短
中断上下文具有严格的时间限制,因为它打断了其他代码(进程和另一个中断处理程序)。中断上下文的代码应当迅速简洁,尽量不用循环处理繁重工作。
3)尽量把繁重工作放在下半部执行
因为中断处理程序要求迅速、简短,因此可以尽量把工作从中断处理性中分离出来,放到下半部执行。
4)中断处理程序栈有限,谨慎使用。
中断处理程序共享所中断进程的内核栈,大小是两页,i.e. 32bit体系结构上是8KB,64bit体系结构上是16KB。

[======]

中断处理机制的实现

中断处理系统的实现依赖于体系结构。
下图展示了从硬件到内核的路由的一个示例:

中断的入口点是:硬件产生了中断,处理器开始跳转到对应的一个唯一位置,在栈中保存这个号,并存放当前寄存器的值(被中断任务),开始处理中断。
中断产生到内核处理中断的步骤:
1)硬件产生中断。
2)处理器跳转到中断向量表对应的中断处理程序,栈中保存中断号及打断任务保存线程。
3)内核调用do_IRQ()。之后,大多数中断处理代码用C编写。
do_IRQ() 声明:

unsigned int do_IRQ(struct pt_regs regs);

regs 保存原始寄存器的值,也会保存中断的值。do_IRQ()提取中断号,对应x86代码:

int irq = regs.orig_eax & 0xFF;

计算出中断号后,do_IRQ()对所接收的中断进行应答,禁止这条线上的中断传递。这些操作通过调用mask_and_ack_8259A()来完成。

4)处理中断
如果中断线上已安装中断处理程序,就调用handle_IRQ_event()处理;如果没有,就直接调用ret_from_intr(),返回内核运行中断的代码。
x86上,handle_IRQ_event() :

asmlinkage int handle_IRQ_event(unsigned int irq, struct pt_regs* regs, struct irqaction* action)
{
    int status = 1;
    int retval = 0;
    
    if (!(action->flags & SA_INTERRUPT)) /* 注册的中断处理函数没设置标志, 或者注册中断处理程序时设置fast IRQ, 禁用了中断 */
        local_irq_enable(); /* 打开被关闭的中断 */
    /* 遍历action链表, 通过已注册的handler处理中断 */
    do {
        status |= action->flags;
        retval |= action->handler(irq, action->dev_id, regs);
        action = action->next;
    } while (action);

    if (status & SA_SAMPLE_RANDOM) /* 注册时指定SA_SAMPLE_RANDOM, 表明中断对内核熵池有贡献 */
        add_interrupt_randomness(irq); /* 用中断时间间隔时间为随机数产生器产生熵, 加入内核熵池 */
    local_irq_disable(); /* 禁止本地中断 */
    return retval;
}

5)调用ret_from_intr()

类似于初始入口代码,用汇编编写。用于检查重新调度释放正在挂起(意味着设置了need_resched)。如果重新调度正在挂起,并且内核正在返回用户空间(i.e. 中断了用户进程),那么schedule()被调用。如果内核正在返回内核空间(i.e. 中断了内核本身),只有preempt_count(抢占计数器)为0时,schedule()才会被调用(否则,抢占内核便是不安全的)。schedule() 返回后,或者如果没有挂起的工作,那么原来的寄存器被恢复,内核恢复到曾经中断的点。

x86 上,初始汇编例程位于arch/i386/kernel/entry.S,C方法位于arch/i386/kernel/irq.c 。
其他体系结构与此类似。

/proc/interrupts 查看中断统计信息
procfs 是一个虚拟文件系统,只存在于内核内存,一般安装于/proc目录下。在procfs中读写文件都要调用内核函数,这些函数模拟从真实文件中读或写。/proc/interrupts 文件存放系统中与中断相关的统计信息。

$ cat /proc/interrupts
            CPU0       CPU1       CPU2       CPU3       
   0:         32          0          0          0   IO-APIC    2-edge      timer
   1:         46          0         74          0   IO-APIC    1-edge      i8042
   8:          1          0          0          0   IO-APIC    8-edge      rtc0
   9:          0          0          0          0   IO-APIC    9-fasteoi   acpi
  12:       1287          0        279          0   IO-APIC   12-edge      i8042
  14:          0          0          0          0   IO-APIC   14-edge      ata_piix
  15:          0          0          0          0   IO-APIC   15-edge      ata_piix
  16:        906          0        842          0   IO-APIC   16-fasteoi   vmwgfx, snd_ens1371
  17:       7556          0       3617          0   IO-APIC   17-fasteoi   ehci_hcd:usb3, ioc0
  18:         38          0          0          0   IO-APIC   18-fasteoi   uhci_hcd:usb4
  19:        112          0         21        103   IO-APIC   19-fasteoi   eth0
  24:          0          0          0          0   PCI-MSI 344064-edge      PCIe PME, pciehp
  25:          0          0          0          0   PCI-MSI 346112-edge      PCIe PME, pciehp
  26:          0          0          0          0   PCI-MSI 348160-edge      PCIe PME, pciehp
  27:          0          0          0          0   PCI-MSI 350208-edge      PCIe PME, pciehp
  28:          0          0          0          0   PCI-MSI 352256-edge      PCIe PME, pciehp
  29:          0          0          0          0   PCI-MSI 354304-edge      PCIe PME, pciehp
  30:          0          0          0          0   PCI-MSI 356352-edge      PCIe PME, pciehp
  31:          0          0          0          0   PCI-MSI 358400-edge      PCIe PME, pciehp
  32:          0          0          0          0   PCI-MSI 360448-edge      PCIe PME, pciehp
  33:          0          0          0          0   PCI-MSI 362496-edge      PCIe PME, pciehp
  34:          0          0          0          0   PCI-MSI 364544-edge      PCIe PME, pciehp
  35:          0          0          0          0   PCI-MSI 366592-edge      PCIe PME, pciehp
  36:          0          0          0          0   PCI-MSI 368640-edge      PCIe PME, pciehp
  37:          0          0          0          0   PCI-MSI 370688-edge      PCIe PME, pciehp
  38:          0          0          0          0   PCI-MSI 372736-edge      PCIe PME, pciehp
  39:          0          0          0          0   PCI-MSI 374784-edge      PCIe PME, pciehp
  40:          0          0          0          0   PCI-MSI 376832-edge      PCIe PME, pciehp
  41:          0          0          0          0   PCI-MSI 378880-edge      PCIe PME, pciehp
  42:          0          0          0          0   PCI-MSI 380928-edge      PCIe PME, pciehp
  43:          0          0          0          0   PCI-MSI 382976-edge      PCIe PME, pciehp
  44:          0          0          0          0   PCI-MSI 385024-edge      PCIe PME, pciehp
  45:          0          0          0          0   PCI-MSI 387072-edge      PCIe PME, pciehp
  46:          0          0          0          0   PCI-MSI 389120-edge      PCIe PME, pciehp
  47:          0          0          0          0   PCI-MSI 391168-edge      PCIe PME, pciehp
  48:          0          0          0          0   PCI-MSI 393216-edge      PCIe PME, pciehp
  49:          0          0          0          0   PCI-MSI 395264-edge      PCIe PME, pciehp
  50:          0          0          0          0   PCI-MSI 397312-edge      PCIe PME, pciehp
  51:          0          0          0          0   PCI-MSI 399360-edge      PCIe PME, pciehp
  52:          0          0          0          0   PCI-MSI 401408-edge      PCIe PME, pciehp
  53:          0          0          0          0   PCI-MSI 403456-edge      PCIe PME, pciehp
  54:          0          0          0          0   PCI-MSI 405504-edge      PCIe PME, pciehp
  55:          0          0          0          0   PCI-MSI 407552-edge      PCIe PME, pciehp
  56:         97          0          0          0   PCI-MSI 1572864-edge      xhci_hcd
  57:          0          0          0          0   PCI-MSI 1572865-edge      xhci_hcd
  58:          0          0          0          0   PCI-MSI 1572866-edge      xhci_hcd
  59:          0          0          0          0   PCI-MSI 1572867-edge      xhci_hcd
  60:          0          0          0          0   PCI-MSI 1572868-edge      xhci_hcd
  61:         84          0         56          0   PCI-MSI 1130496-edge      0000:02:05.0
  62:          0          0          0          0   PCI-MSI 129024-edge      vmw_vmci
  63:          0          0          0          0   PCI-MSI 129025-edge      vmw_vmci
NMI:          0          0          0          0   Non-maskable interrupts
LOC:       4802       4029       6007       6308   Local timer interrupts
SPU:          0          0          0          0   Spurious interrupts
PMI:          0          0          0          0   Performance monitoring interrupts
IWI:          0          0          0          0   IRQ work interrupts
RTR:          0          0          0          0   APIC ICR read retries
RES:      14669      15576      16058      16864   Rescheduling interrupts
CAL:       2240       1257       1400       1781   Function call interrupts
TLB:        508        587        449        589   TLB shootdowns
TRM:          0          0          0          0   Thermal event interrupts
THR:          0          0          0          0   Threshold APIC interrupts
DFR:          0          0          0          0   Deferred Error APIC interrupts
MCE:          0          0          0          0   Machine check exceptions
MCP:          1          1          1          1   Machine check polls
HYP:          0          0          0          0   Hypervisor callback interrupts
ERR:          0
MIS:          0
PIN:          0          0          0          0   Posted-interrupt notification event
PIW:          0          0          0          0   Posted-interrupt wakeup event
  • 第1列是中断线,这里中断号为01,89,...,24~63。没有显示安装处理程序的中断线。
  • 第25列是一个接收中断数目的计数器。系统中的每个处理器都有这样的列,表明有4个这样的处理器(CPU0CPU3)。可以看到,4个处理器的时钟中断(Local timer interrupts)总共已经接收21146次中断,如果HZ为1000,代表机器已经启动21秒。
  • IO-APIC-level或IO-APIC-edge列,作为自己的中断控制器。
  • 最后一列是与该中断相关的设备名字,该名字通过参数devname提供给函数request_irq()(中断注册函数)的。如果中断线是共享的,这条中断线上注册的所有设备都会列出来。

[======]

中断控制

Linux内核提供一组接口用于操作机器上的中断状态:能用于禁止当前处理器的中断系统,屏蔽整个机器的一条中断线。例程与体系结构相关,位于<asm/system.h>,<asm/irq.h>。

为什么会要控制中断系统?
因为需要提供同步,通过禁止中断,可以确保某个中断处理程序不会抢占当前的代码。禁止中断还可以禁止内核抢占。不过,不论禁止中断 or 禁止内核抢占,都没有提供任何保护机制来防止来自其他处理器的并发访问。

禁止内核抢占:preempt_disable、preempt_enable等;

禁止和激活中断

用于禁止当前处理器上的本地中断,随后由激活:

local_irq_disable();
/* 禁止中断 */
local_irq_enable();

上面的例程存在问题,因为其实现往往是简单禁止、允许中断(x86下,local_irq_disable <=> cli指令,local_irq_enable <=> sti指令),如果在这之前已经禁止中断,调用该例程后,将无条件恢复中断。解决办法:只是恢复到禁止中断前的状态。

unsigned long flags;

local_irq_save(flags); /* 禁止中断 */
/* ... */
local_irq_restore(flags); /* 中断恢复到原来的状态 */

由于flags包含具体体系结构的数据,即中断系统的状态,因此,flags不能传递给另一个函数,必须驻留在同一栈帧中。基于此,local_irq_save和restore的调用必须在同一个函数中。

前面这些禁止和激活中断的函数,都可以在中断上下文和进程上下文中调用。

不再使用全局cli()

x86下,cli() 能禁止系统中所有处理器上的中断。如果另一个处理器调用了该方法,那么它就不得不等待,直到中断重新被激活才能继续执行
。cli()对应激活函数是sti()。
取消全局cli() 优点:
1)强制驱动程序编写者实现真正的加锁。特定目的的细粒度锁 比全局锁要快很多,也完全吻合cli()使用初衷。
2)是的很多代码更具流线型,避免代码的成簇布局。得到的中断系统更简单、易理解。

禁止指定中断线

local_irq_disable/local_irq_save 是禁止整个处理器上所有中断。某些情况下,只需要禁止某条特定中断线就足够了。这就是所谓屏蔽条(masking out)一条中断线。Linux提供4个接口:

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()禁止指定中断线向所有处理器传递。
只有当前正在执行的所有处理程序完成后, disable_irq()才能返回。因此,调用者不仅要确保不在指定线上传递新的中断,同时还要确保所有已经开始执行的处理程序已全部退出。
disable_irq_nosync()不会等待当前中断处理程序执行完毕。

synchronize_irq() 等待一个特定的中断处理程序的退出。如果该处理程序正在执行,那么函数必须退出后才能返回。

使用注意事项:
1)禁止和使能指定中断线次数需配对
上面3个函数调用可以嵌套,但在一条指定的中断线上,对disable_irq()和disable_irq_nosync()的每次调用,都需要相应地调用一次enable_irq()。只有在对enable_irq()完成最后一次调用后,才真正重新激活了中断线。

2)不要在中断上下文激活当前中断线
当正在处理一条中断线时,本意并不想激活它,因此使能指定中断线时要特别小心。当某个处理程序的中断线正在处理时,默认会被屏蔽掉。

3)禁止多个中断处理程序共享的中断线是不合适的
禁止某个共享中断线,也就禁止了这条线上所有设备的中断传递。因此,新设备的驱动程序应该倾向于不使用这些接口(如PCI设备必须支持中断线共享)。

中断系统的状态

如何了解中断系统的状态,如中断当前是禁止的还是激活的?或者当前是否正处于中断上下文的执行状态中?
本小节提供这方面回答。

<asm/system.h>中
irqs_disable() 如果本地处理器上的中断系统被禁止,则返回非0;否则,返回0。
<asm/hardirq.h>中
in_interrupt() 如果内核处于中断上下文,则返回非0;如果在进程上下文,返回0。
in_irq() 如果当前正在执行中断处理程序,则返回非0;否则,返回0.

所有中断控制方法列表:

[======]

别打断我,马上结束

本章考察了硬件异步中断,中断用来打断操作系统。
1)大多数硬件都通过中断与操作系统通信,对给定硬件的驱动程序注册中断处理程序(request_irq),可以响应并处理来自相关硬件的中断。中断过程所做工作主要包括:应答、重新设置硬件,从设备拷贝数据到内存,或者从内存拷贝数据到设备,处理硬件请求,发送新硬件请求。
2)内核提供接口包括:注册、释放中断处理程序,禁止中断,屏蔽指定中断线,检查中断系统的状态。
3)对中断处理程序本身的要求:简短、迅速。将中断工作分为两半,上半部是中断处理程序;下半部执行推后的工作。

[======]

posted @ 2022-01-30 14:00  明明1109  阅读(2512)  评论(0编辑  收藏  举报