最近解决一个关于Linux中断的问题,把相关机制整理了一遍,记录在此。
不同的外部设备、不同的体系结构、不同的OS其中断实现机制都有差别,本文对应的OS为linux3.4版本,外部设备为PCI设备、系统为X86。
中断让外设能够通知CPU他需要获得服务(让CPU执行指定的中断服务例程ISR)。为了达到这个目的,首先要为中断执行做好准备,完成初始化相关的操作。包括:
1、 初始化中断控制器等相关器件(OS初始化过程中完成);
2、 配置并使能外部设备(比如使用pci_enable_msix),得到irq号;在这个操作过程中,内核需要完成的大致操作是:
1、 确定该中断的执行CPU,并在对应CPU上建立vector和irq号的对应关系(利用全局per-cpu变量vector_irq),配置中断控制器(I/OAPIC、PIR等),可能还需要设置外部设备(比如设置MSI
Capacity registers);
2、 为对应的irq_desc初始化正确的handle_irq接口(通用逻辑接口);
3、 为对应的irq_desc初始化正确的底层chip操作接口。
3、 使用request_irq号为该中断号指定一个服务例程;
完成了以上的初始化操作,在外设中断到来的时候,为该中断指定的ISR(Interrupt Service Routines)就能得到执行,这个执行过程大致如下:
1、 外设根据各自的配置,产生中断信号或者中断消息(MSI,INT# message)。
2、 中断控制器从外设获取中断电信号或者中断消息,把它翻译为vector(CPU使用这个参数来决定是谁发生了中断,要如何处理)并提交到CPU。
3、 对X86系统,CPU利用从中断控制器获取到的vector为索引,查询IDT (interrupt descriptor table)得到该中断的处理接口(对linux,是在entry_64.s中定义的函数common_interrupt接口)并执行。
4、 在linux定义的common_interrupt接口中,执行完中断执行环境建立后,会进入generic interrupt layer执行,其首先通过vector查找到irq和对应的irq_desc结构,并执行该结构的handle_irq接口,这个接口就是generic interrupt layer的通用逻辑接口,比如handle_edge_irq/handle_level_irq等;在中断执行的通用逻辑接口中,会通过irq_desc::action调用外设指定的ISR。
在linux中可以通过/proc/interrupts查看当前系统中所有中断的统计信息,在/proc/irq/xxx(中断号)下面,可以看到该中断的详细信息。
这里的描述很多来自INTEL的文档《Intel Software developer’s Manual, system programming guide》和《PCI Express System Architecture》
中断控制器的功能是:把外设的中断信号,转换成CPU能够明白的vector,并完成中断执行控制,确保在合适的时机把中断提交给CPU执行。对这部分内容,《interrupt in linux》有详细的描述。
1、 8259A:
每个8259A有8个管脚,每个管脚对应其连接的CPU的IDT中的一个vector,单独使用8259A,其硬件连线就决定了对设备vector的使用。典型的场景是使用两个8259A级联,理论最多16个中断号(就是ISA IRQs),实际能提供对15个中断线的处理(master的IRQ2用于连接slave),其具体的分配见下图。
2、 PIR:
用于完成输入的信号到输出信号的映射。在下图中PIR被用于完成多个PCI设备的INT#信号到8259A对应引脚的路由。对应这种连接方式,在PCI设备初始化的时候,OS会根据BISO提供的信息设置PIR,把INT#路由到O0-O3中正确的管脚,从而体现到8259A的正确管脚(对应了vector),这样INT#信号就被转换为vector并提交到CPU。由于可能有较多的PCI设备,而PIR的输入/出错管脚有限,所以连接到相同输入关键的INT#会共享一个中断。
3、 I/O APIC
每个I/O APIC提供24个管脚,能够和外部设备的中断线连接,每个管脚都可以通过配RTE(Redirection table entry)配置对应的vector。其功能是:把外部设备的中断请求,翻译为local APIC的interrupt message,并按照配置的vector,发送给指定的local APIC处理(在SMP系统,存在多个CPU,也就有多个local APIC)。通常的配置方式是:第一个I/O APIC的前16个管脚,配置来处理之前的ISA IRQs,其它外设比如PCI设备,则直接使用其他管脚连接。
4、 local APIC
其负责处理IPI(inter-process interrupt)、直接连接的中断处理、接收和处理interrupt message,每个CPU有自己的local APIC。
对应I/O APIC和local APIC的组合,其连接方式见下图
针对X86中断控制器硬件和linux对这些硬件的初始化,在《interrupt in linux》中有很详细的描述。
Local APIC的处理过程
每个local APIC对应了一个CPU。其处理interrupt message的过程如下:
1、 判断该中断的destination是否为当前APIC,如果不是则忽略,否则继续处理
2、 如果是SMI/NMI/INIT/ExtINT, or SIPI(这些中断都负责特殊的系统管理任务,外设一般不会使用)被直接送到CPU执行,否则执行下一步。
3、 设置Local APIC 的IRR寄存器的对应bit位。
4、 如果该中断优先级高于当前CPU正在执行的中断,且当前CPU没有屏蔽中断(按照X86和LINUX的实现,这时是屏蔽了中断的),则该高优先级中断会中断当前正在执行的中断(置ISR位,并开始执行),低优先级中断会在高优先级中断完成后继续执行,否则只有等到当前中断执行完成(写了EOI寄存器)后才能开始执行下一个中断。
5、 在CPU可以处理下一个中断的时候,从IRR中选取最高优先级的中断,清0 IRR中的对应位,并设置ISR中的对应位,然后ISR中最高优先级的中断被发送到CPU执行(如果其它优先级和屏蔽检查通过)。
6、 CPU执行中断处理例程,在合适的时机(在IRET指令前)通过写EOI寄存器来确认中断处理已经完成,写EOI寄存器会导致local APIC清理ISR的对应bit,对于level trigged中断,还会向所有的I/O APIC发送EOI message,通告中断处理已经完成。
说明:
1、 关于Local APIC的IRR和ISR
寄存器interrupt request register (IRR) 和 in-service register (ISR),都是256bit寄存器,每个bit对应一个中断(其中[0-15]不能使用,SMI/NMI/INIT/ExtINT/SIPI的发送和执行不经过ISR和IRR) 。IRR中保存的是已经被local APIC接纳但是还没有开始执行的中断;ISR中保持的是当前正在执行但是还没有完成的中断。
2、 中断优先级
对应通过local APIC发送到CPU的中断,按照其vector进行优先级排序:
优先级=vector/16
数值越大,优先级越高。由于local APIC允许的vector范围为[16,255],而X86系统预留了[0,31]作为系统保留使用的vector,实际的用户定义中断的优先级的取值范围为[2,15],在每个优先级内部,vector的值越大,优先级越高。
Local APIC中还有一个关于中断优先级的寄存器TPR(task priority register)寄存器:用于确定打断线程执行需要的中断优先级级别,只有优先级高于设置值的中断才会被CPU执行 (SMI/NMI/INIT/ExtINT, or SIPI不受限制),也就是除了特殊中断外,优先级低于TPR指定值的中断将被忽略。
3、 中断的pending
对于同一个vector,如果有多次中断请求,可能IRR和ISR对应的bit位都被置位,也就是对同一个vector,local APIC可以pending两个中断,其后的即使有多处,也会被合并为一个执行。
4、 中断执行时机
中断的执行总是在指令边界开始(只有一个特殊的exception:abort在外,出现了这个中断,系统基本上也就完蛋了),也就是中断不可能打断指令的执行。
CPU对中断和异常的处理
相关概念
1、 vector(中断向量)
vector是一个整数,在X86CPU上,使用vector对中断(interrupt,外部设备产生)和异常(exception,CPU在程序执行中产生)统一编号,每个CPU核心内部,中断/异常和vector所以一一对应的;但是在各个不同的CPU核心上,相同的vector可以对应不同的中断(至少对于linux的设置,异常还是使用相同的vector)。
vector的取值范围为[0,255],其中[0,31]被系统保留使用(多数作为异常的vector),其余的可供外设中断使用(系统设备比如local APIC也占用了部分[32,255]这个范围的vector)。
2、 IDT(interrupt descriptor table)
X86 CPU采用一个有256个元素的数组来描述中断/异常,该数组的index为vector;其内容包括了三种gate descriptor,用于描述一个中断/异常的处理接口;这个数组就是IDT,CPU在收到中断请求的时候,就利用vector获取到对应的中断处理接口描述并执行。
3、 可屏蔽中断
通过CPU INTR管脚/local APIC接收到的中断是可屏蔽中断,这些中断能够通过清零EFLAGS的IF来屏蔽(CLI指令)。通过INT n指令生成的中断即使使用了和外部中断一样的vector,也是不可屏蔽的;同样CPU运行过程中同步产生的trap、fault、abort等异常也是不可屏蔽的。
4、 NMI
NMI是不可屏蔽中断(不可通过IF标志屏蔽),是通过CPU的NMI管脚发出的中断或者通过delivery mode为NMI的方式提交的中断。NMI中断在执行前,CPU不仅会屏蔽其它中断,也会屏蔽NMI中断,直到NMI中断处理执行完成(IRET指令被执行)。使用INT 2指令虽然能执行NMI中断处理函数,但是相关硬件不会介入,也就是没有相关的屏蔽NMI中断的操作。
CPU执行中断的过程
1、 利用vector,查IDT得到中断描述符;
2、 如果中断发生在用户态,会首先执行stack switch切换到内核态执行;
3、 依次保存EFLAGS CS IP到当前栈,如果需要(有error code的异常),把error code PUSH到当前栈。并把IF/TF位清零屏蔽可屏蔽中断;至此,CPU完成了中断处理程序执行环境的建立。
4、 执行中断描述符定义的中断处理入口(IDT中指定地址的代码);
5、 根据环境执行不同的中断退出方式,比如执行现场调度操作(retint_careful和retint_kernel),最终都会执行IRET指令;至此,中断执行完成。
异常的执行过程类似,只不过异常在执行前不会把IF位清零,只清零TF位。
本部分的很多内容来自《PCI Interrupts for x86 Machines under FreeBSD》和《PCI Express®
Base Specification Revision 3.0》和《PCI Express System Architecture》。
PCI设备的中断有两种模式:一种是INT#模式,一种是MSI模式。
INT#模式
每个PCI设备用四个中断信号,对应INTA#、INTB# INTC#、INTD#,这些中断信号采用level trigger 的方式并且为低电平有效,PCI设备通过拉低对应的信号来assert对应的中断,并在ISR访问PCI设备的指定寄存器deassert该中断。
中断线和X86系统的连接
这里存在两种常见连接模式,一种是使用老的8259A+PIR的系统,一种是使用新的I/O APIC的系统。
对于使用8259A的系统:PCI的中断线连接到一个可编程的PIR设备,再通过该设备连接到8259A(见X86中断控制器一章的图);对于采用I/OAPIC的系统,可以使用以下的连接方式,同样这里只画出了一个中断线,同时根据不同的系统配置可能存在多个I/OAPIC。除了采用直接的中断引脚连接,PCI还支持virtual INT#,使用INT# message(Assert INT# message和deassert INT# message)的方式来使用INT#信号。
INT#模式的局限
1、 中断数量有限且不方便扩展:每个物理的PCI设备,最多只有4个中断但是至少能支持8个function,且系统中可能存在多个PCI设备,不得不使用中断共享的模式,影响使用性能。
2、 同步问题:由于INT#中断采用的是side channel,中断信号和数据本身存在不同步的问题:可能在中断到达的时候,对应的数据没有达到,为了处理这个问题,一般采用“读刷新”的做法,也就是在使用该设备写入到X86的数据之前,ISR先对这个设备进行一次读操作来确保相关数据已经写入完成,比如读PCI设备的中断状态寄存器等。
MSI/MSI-X模式
在这种模式下,PCI设备通过和数据DMA一样的通道来完成中断处理,通过向特定地址空间(系统FSB Interrupt存储器空间)发起一个写操作来发起中断。该写操作的地址和数据信息在PCI设备初始化MSI功能的时候已经填写到MSI Capacity registers(MSI模式)/MSI-X table(MSI-X)中(对X86,这个地址空间是FEE00000H开始的地址空间,其实就是local APIC寄存器映射的地址空间),地址信息保存在Message address register,其中包含了目标CPU信息和FSB Interrupt存储器空间;数据中包含了该MSI中断对应的vector,保存在Message data register中。 MCH(memory control hub)截获这个写操作,转换为FSB interrupt message并向各个CPU核心广播,local APIC接收并处理这个消息,最终触发CPU的中断处理过程。使用这种机制,中断的数量不受PIR/ IOAPIC等各种器件管脚数量的限制,MSI可以支持32个中断,而MSI-X可以达到2048个;中断的传递相当直接,省略了中断路由的过程;并且能直接从interrupt message中获取vector信息,减少了交互过程。