计算机中断体系三:中断路由
与简单定制的嵌入式系统不同,计算机是个开放的系统,大量的PCI设备带来了复杂的拓扑结构,PCI桥设备和扩展槽让固定的路由搭配成为了不可能。那么如何决定和设定中断以减少大量PCI设备中断共享,如何向OS报告这些中断设置信息呢?这些将是本文要重点讨论的内容。
PCI/PCIe设备构成了计算机的基础框架,本文的重点也将聚焦PCI设备的中断设置问题。在开始之前,我希望读者能阅读过前面几篇介绍PCI/PCIe和中断的文章,以便能有个共同的讨论基础。如果你还没有看过,下面是传输门:
PCI中断路由
前文我们介绍过了PIC和IOAPIC的中断引脚,那么PCI设备是如何连接到PIC/IOAPIC的中断引脚上呢?这个连接关系的原则是什么呢?
PCI设备中断
PCI spec为每个PCI设备定义了四个中断引脚,分别是INTA#,INTB#,INTC#和INTD#,如图:
如果PCI设备为单功能设备,则必须使用INTA#,对于多功能设备,各功能设备可任意接至PCI 总线的四条中断申请线INTA# - INTD# 。那么如何设定某个PCI设备的某个功能使用那个中断引脚呢?实际上这个是由PCI设备制造商决定的,并不能由主板固件后期更改。我们翻出PCI体系结构文中介绍的PCI配置空间,在其中interrupt pin会告知主板固件该func使用了哪个引脚,如图中红色圈标注:
一个PCI设备可以有8个func,而4个终端引脚意味着中断共享不可避免。PCI为了方便中断共享,定义中断是电平触发,低电平有效。中断信号与PCI CLK异步,设备一旦Assert为低,则要维持低电平状态,直到驱动程序清除这个中断请求。这样PCI中断可以为通过链的方式来共享,某个设备中断处理完毕后将其电平置高,只有该中断信号上所有的设备中断处理都结束了,该中断信号才能恢复高电平,整个中断处理才能结束。实际上中断共享不仅仅存在PCI设备各个func中,假设系统中有10个PCI设备,每个设备有4个中断引脚,要它们每个都独立意味着中断控制器需要有40个引脚。我们通过前文知道PIC只有15个引脚,IOAPIC通常有24个引脚,还有很多保留不可用,而一台计算机内部PCI设备往往超过20个。所以,中断更需要在设备之间共享。
让我们举个形象的比喻来理解中断共享的处理。想象操作系统中断处理调度器是个严厉的老师,设备中断处理例程是一群可爱的小朋友,他们被按照连接在PIC/IOAPIC引脚数分成若干排。每个小朋友手里都有个按钮,小朋友想要问问题,就按下按钮,老师回答完后他会再按一下恢复弹起状态。问题是老师是个近视眼,他看不见后面的小朋友。为了解决这个问题,在每排座位前面安装了个灯泡,当这排小朋友有人的按键被按下,灯就亮了。老师上课时,看见第2排灯亮了,他就停下来,问:“第2排第1个小朋友,是不是你要问问题?”得到肯定的答复就解答该小朋友,否定的话就问下一个,如此往复,直到该排没有人有问题,灯才灭,老师才能继续讲课。
看来在老师没有换个更好的眼镜或者做个近视矫正手术之前这是唯一的办法了。那么该如何为这些小朋友分组呢,老师把这个任务交给了班长小张。我们帮小张想想,怎么分配才能既公平又上课效率高呢?是不是平均分最好呢?假设小朋友都很乖,这样是最好的办法,但是小张知道班里有个话痨小朋友--小明,他什么都不懂但问题很多,他会频繁提问。假设他被分在第3排最后一个,老师每次看到第3排灯亮了,都从第一个小朋友问,经过了好久才问到小明,效率好低,小明每次很久才轮到他也很不满,上课节奏整体也被拖慢了!
怎么办?开除小明吗?可惜小明是校董的儿子,绝对不能少!让小明坐第一个位置吗?校规规定座位是流水席,每排都先来先坐,再说让小明又高问题又多,谁坐他后面别想问问题了,太不公平。看来只有给小明开小灶了,让他单独坐一排!
世界上没有绝对的公平,在资源捉襟见肘时会叫的孩子有奶吃在我们天朝还少吗?班长小张也只有做些妥协了。我们的计算机固件和主板硬件工程师在很多时候就扮演了左右为难的小张的角色,在硬件的限制下,尽量保证系统的响应时间。他们基于一些简单的原则:
A. 公平:尽量减少中断共享。
B. 效率:紧急或者频繁的设备可以独占中断
同时要充分理解PC系统中绝大多数设备都是单功能设备,所以仅使用INTA#信号,很少使用INTB#和INTC#信号,而INTD#信号更是极少使用。映射INTA#~INTD#到PIC/IOAPIC IRQ的机制称为“Swizzling”,它在不同情况下实现机制有很大不同,我们分别来看看。
2
PCI/PCIe扩展插槽
对主板上的PCI扩展插槽,用户插入什么设备,插在哪个槽内都不能在出厂时确定。我们这里要尽量考虑平衡原则和效率原则。我们将所有插槽的INTA#~INTD#分成四组串联起来如何?这样离得最近的Slot 1高兴了,每个都是我优先!万一有个用户把重要的网卡插在slot 4,效率会严重下降。在充分考虑到PCI设备绝大多数都是单功能设备(仅使用INTA#信号,很少使用INTB#和INTC#信号,而INTD#信号更是极少使用),PCI SIG推荐PCI to PCI bridge后slot连接关系应该组成如下图:
即Slot1 INTA#->Slot2 INT B#->Slot3 INTC#->Slot4 INTD#等等。这样,当然slot 1还是占些小便宜,但其他slot也有很大机会独占某个中断线(想想为什么)。这种Swizzling是主板设计硬件连线决定的,不由主板固件决定,但是主板固件需要了解这些信息。
PCI桥则将它下面的转换结果INTx转化为本身的INTx,接入芯片组内部的Swizzling。PCIe的插槽是1:1对应PCIe root port,PCIe root port可以看作PCI桥,等同处理。
3
芯片组内部
PCI SIG并没有规定芯片组内部Swizzling的规则,而芯片组内部设备在出厂时就已经确定了,芯片是不是可以hard code一个中断路由关系呢?实际上有些芯片组就是这么做的,在芯片硬件说明书中注明各种PCI设备INTx到IRQ的关系表,BIOS只要照着报告给OS就可以了。而Intel芯片组提供了更灵活的方式,BIOS可以根据需要设置中断路由,以适应不同的应用市场。不同的芯片组设置方法不同,有些给出了几组应用场景,BIOS可以根据需要选择一种,而在很早开始南桥芯片组ICH/PCH就给出了一种更灵活的方式,就是我们前文提到的PIRQ。简单来说就是在ICH/PCH内部加入了几个新的寄存器:IR寄存器组。IR寄存器组用于设定芯片组内部PCI设备中断INTA#-H# 连接到具体PIC/IOAPIC的哪个引脚上。这样BIOS就可以根据面向市场不同重新绘制中断路由图了,好方便!
OS接口
班长小张终于给小朋友排好了座位,他怎么把这些信息告诉各个老师呢?一个个告诉太麻烦,不如把所有情况打印成一张表贴子讲台上。我们的固件也是通过表告诉OS的,这里有两种表。
MP table
有感于DOS阶段中断设置的混乱,微软在推出Win95时联合Intel提出了PCI Interrupt Routing Table的数据结构,它的作用就是用来描述在使用8259中断控制器的系统下,PCI中断的路由关系。它和其他一些表构成了传统的MP table。它只在PIC模式下起作用,而PIC模式已经被淘汰,所以它的具体结构我们这里略过。这里要特别指出的是,固件在提交PIRQ table时要同步更新PCI配置空间的Interrupt line寄存器,如图篮圈部分:
2
ACPI table
在APIC模式下,固件应该通过ACPI的_PRT(PCI Routing Table) method返回主板上无论硬件还是固件设定的中断路由信息。ACPI spec有示例,但内容比较单薄。我们来看一个Intel开源硬件平台Minnowboard MAX固件是怎么处理的。源码在:
https://github.com/tianocore/edk2-platforms/blob/minnowboard-max-udk2015/Vlv2DeviceRefCodePkg/AcpiTablesPCAT/PciTree.asl
_PRT代码如下:
Method(_PRT,0)
{
If(PICM) {Return(AR00)} // APIC mode
Return (PR00) // PIC Mode
} // end _PRT
这里判断如果是PIC模式,返回PR00,如果是APIC模式返回AR00表。PR00和AR00其实是同一个中断路由关系的两个view,其实质内容是一致的。我们单看一下AR00就好:
Name(AR00, Package()
{
// SD Host #0 - eMMC
Package() {0x0010FFFF, 0, 0, 16 },
// SD Host #1 - SDIO
Package() {0x0011FFFF, 0, 0, 17 },
// SD Host #2 - SD Card
Package() {0x0012FFFF, 0, 0, 18 },
// SATA Controller
Package() {0x0013FFFF, 0, 0, 19 },
// xHCI Host
Package() {0x0014FFFF, 0, 0, 20 },
// Low Power Audio Engine
Package() {0x0015FFFF, 0, 0, 21 },
// USB OTG
Package() {0x0016FFFF, 0, 0, 22 },
//
// MIPI-HSI
Package() {0x0017FFFF, 0, 0, 23 },
//
// LPSS2 DMA
// LPSS2 I2C #4
Package() {0x0018FFFF, 0, 0, 17 },
// LPSS2 I2C #1
// LPSS2 I2C #5
Package() {0x0018FFFF, 2, 0, 19 },
// LPSS2 I2C #2
// LPSS2 I2C #6
Package() {0x0018FFFF, 3, 0, 18 },
// LPSS2 I2C #3
// LPSS2 I2C #7
Package() {0x0018FFFF, 1, 0, 16 },
// SeC
Package() {0x001AFFFF, 0, 0, 21 },
//
// High Definition Audio Controller
Package() {0x001BFFFF, 0, 0, 22 },
//
// EHCI Controller
Package() {0x001DFFFF, 0, 0, 23 },
// LPSS DMA
Package() {0x001EFFFF, 0, 0, 19 },
// LPSS I2C #0
Package() {0x001EFFFF, 3, 0, 16 },
// LPSS I2C #1
Package() {0x001EFFFF, 1, 0, 17 },
// LPSS PCM
Package() {0x001EFFFF, 2, 0, 18 },
// LPSS I2S
// LPSS HS-UART #0
// LPSS HS-UART #1
// LPSS SPI
// LPC Bridge
//
// SMBus Controller
Package() {0x001FFFFF, 1, 0, 18 },
//
。。。。
}
这里的16,18,19..25就是上文介绍过的GSI(还记得GSI和IRQ的关系吗?)。通过这张大表,OS才能确定主板PCI设备和IRQ的连接关系。
其他中断信息
除了中断路由表之外,还有很多和中断相关的信息也需要通过ACPI向OS报告
1. LAPIC和IOAPIC
固件需要在ACPI的MADT表里向OS报告所有的LAPIC和IOAPIC。LAPIC默认映射到物理地是0xFEE00000(想想为什么不会互相冲突?),不需要报告,只要LAPIC ID即可。需要特别说明的是,因为APIC ID不一定从0开始,也不一定连续,所以其值要动态枚举而得。同时由于很多OS在调度processor时是挨个调度的,而HT的两个thread的APIC ID往往连续,同时被调度效能大大降低,Intel推荐HT的两个thread隔开报告。如果多个CPU socket,情况又有变化,这是系统调优的手段之一,这里不再赘述。
IOAPIC的地址和ID都由固件指定,必选准确的在MADT里向OS报告,每个IOAPIC的GSI偏移量也要报告。
2. SCI
在Intel的芯片组中,SCI通常缺省占用了中断9。它的值可以在芯片组的寄存器中修改,并需要通过FADT报告OS。因为这是个特殊指定的中断,所以在MADT中还要通过interrupt override节予以保留。
结语
终于结束了中断的介绍,希望大家在通读了三篇文章能有所收获。