来自内核文档/Documentation/pci/MSI-HOWTO.txt
1. 什么是MSI
MSI全称Message Signaled Interrupt。
当设备向一个特殊地址写入时,会向CPU产生一个中断,即也MSI中断。
MSI能力最初在PCI 2.2里定义,在PCI 3.0里被强化,使得每个中断都可以单独控制。
PCI 3.0还引入了MSI-X能力,相比MSI,每个设备可以支持更多的中断,并且可以独立配置。
设备可以同时支持MSI和MSI-X,但同一时刻只能使能其中一种。
2. 为什么使用MSI
与传统引脚中断相比,有三个方面的优势。
基于引脚的PCI中断经常在几个设备间共享,内核必须调用与该中断相关的每一个中断处理函数,降低了效率。MSI不是共享的,所以不存在这个问题。
当设备向内存写入数据,然后发起引脚中断时,有可能在CPU收到中断时,数据还未到达内存(在PCI-PCI桥后的设备更有可能如此)。为了保证数据已达 内存,中断处理程序必须轮询产生该中断的设备的一个寄存器,PCI事务保序规则会确保所有数据到达内存后,寄存器才会返回值。
使用MSI时,产生中断的写不能越过数据写,因而避免了这个问题。当中断产生时,驱动可以确信所有数据已经到达内存。
PCI的每个功能设备只支持一个基于引脚的中断,驱动常常需要查询设备来确定发生的事件,降低了中断处理的效率。通过MSI,一个设备可以支持多个中断,这样可以为不同的使用不同的中断。比如:
1. 给不常发生的事件(如错误)指定独立的中断,这样驱动可以正常中断路径进行更有效的处理。
2. 给网卡的每个报文队列或者存储控制器的每个端口分配中断。
3. 如何使用MSI
PCI设备初始化为使用基于引脚的中断,设备驱动需要将设备配置为使用MSI或MSI-X。不是所有的设备可以完整支持MSI,下面有些API就直接返回失败,因此仍然使用基于引脚的中断。
3.1 内核提供MSI支持
内核需要配置CONFIG_PCI_MSI以支持MSI或者MSI-X,该配置是否可以配置受架构和其它一些配置的影响。
3.2 使用MSI
大部分工作在PCI层的驱动里完成,只需要请求PCI层为设备设置MSI能力即可。
3.2.1 pci_enable_msi
int pci_enable_msi(struct pci_dev *dev)
这个调用只给设备分配一个中断,不管设备支持多少个MSI中断。设备会从基于引脚中断模式切换为MSI模式。dev->irq会赋予一个新的代表MSI的编号。
在驱动需要在调用request_irq()之前调用这个接口。因为打开MSI中断的同时会禁用引脚中断IRQ,驱动因此不会收到旧的中断。
3.2.2 pci_enable_msi_block
int pci_enable_msi_block(struct pci_dev *dev, int count)
这个接口是pci_enable_msi的变种,它可以请求多个MSI中断。MSI规范要求中断必须以2的幂分配,最多为2^5(32)。
如果函数返回0,则表示成功分配了不少于驱动请求的中断数量(可能会大于请求数量)。该函数打开设备的MSI,并将中断组的最小编号赋给dev->irq,该设备的中断范围为dev->irq至dev->irq + count - 1。
如果函数返回负值,表示设备无法提供更多的中断,驱动不应试图再去请求。如果返回的是正值,这个值会小于count,表示目前最分配的最大中断数量。对这于两种情况,函数都不会更新irq值,设备也不会切换到MSI模式。
设备驱动必须要正确处理上述第二种情况。在中断数量不够的情况下,有的设备尚可工作,驱动就应再次调用pci_enable_msi_block()。不过由于一些限制,第二次调用未必也可以成功。
3.2.3 pci_disable_msi
void pci_disable_msi(struct pci_dev *dev)
该函数的功能是撤消pci_enable_msi()或pci_enable_msi_block()的工作。它恢复dev->irq为引脚中断号,释放此前分配的MSI。中断号之后有可能分配给其它设备,因此驱动不应保留dev->irq的值。
驱动必须调用free_irq()以释放之前request_irq()分配的中断号。否则会产生BUG_ON(),设备将继续保持MSI使能,并泄露中断向量。
3.3 使用MSI-X
MSI-X能力比MSI更为灵活,它支持2048个中断,每个中断都可以单独控制。要支持这种能力,驱动必须使用结构数组struct msix_entry。
- struct msix_entry {
- u16 vector; /* kernel uses to write alloc vector */
- u16 entry; /* driver uses to specify entry */
- }
这个定义允许设备以分散的方式使用中断(比如使用中断3和1027,只需分配两个数组元素)。驱动负责填充数组各元素的entry部分,以使内核为其分配中断。不能给多个entry赋予相同的编号。
3.3.1 pci_enable_msix
int pci_enable_msix(struct pci_dev *dev, struct msix_entry *entries, int nvec)
该函数向PCI子系统请求分配nvec个MSI中断。入参entries指向结构数组,元素个数不少于nvec。函数返回0表示调用成功,设备被切换至 MSI-X中断模式,数组元素中的vector字段也被填充为中断号。驱动接下来为每个需要使用的vector调用request_irq()。
如果函数返回负值,则表示出错,驱动不应再从该设备申请分配MSI-X中断。如果返回的是正值,则表示最大可以分配的中断数。
该函数与pci_enable_msi()不同之处在于它不修改dev->irq,一旦MSI-X使能后,dev->irq这个中断号不会再产生中断。驱动需要记录已分配的MSI-X中断号,以保证后续的资源释放。
一般来说,驱动在设备初始化时调用一次该函数。
由于种种原因,内核有可能无法提供驱动所要求的中断数量,一个好的驱动应该能够处理可变数量的MSI-X中断。下面是一个例子:
- static int foo_driver_enable_msix(struct foo_adapter *adapter, int nvec)
- {
- while (nvec >= FOO_DRIVER_MINIMUM_NVEC)
- {
- rc = pci_enable_msix(adapter->pdev, adapter->msix_entries, nvec);
- if (rc > 0)
- nvec = rc;
- else
- return rc;
- }
- return -ENOSPC;
- }
3.3.2 pci_disable_msix
void pci_disable_msix(struct pci_dev *dev)
这个函数的作用是撤销pci_enable_msix()的工作,它释放之前分配的MSI中断。同样,释放的中断号后续可能会分配给其它设备,驱动不应再记录使用这些中断号。
在调用这个函数之前,驱动必须调用free_irq()以释放request_irq()分配的中断号,否则会产生BUG_ON,设备将维持在MSI使能的状态,并泄漏中断向量。
3.3.3 MSI-X表
MSI-X能力指定了一个BAR及BAR内偏移量用于访问MSI-X表,这个地址由PCI子系统映射,驱动不应该直接访问。如果驱动想屏蔽或者开启一个中断,应该调用disable_irq()/enable_irq()。
3.4 处理同时支持MSI和MSI-X能力的设备
如果设备同时支持MSI和MSI-X,则可以运行在MSI模式或者MSI-X模式下,但不能同时运行,这是PCI规则的要求,因此PCI层也进行了 限制。在MSI-X使能的情况下调用pci_enable_msi()或者在MSI使能的情况下调用pci_enable_msix()将产生错误。如果 设备驱动在运行时希望在MSI和MSI-X之间切换,它必须先停止设备,然后将其切换为引脚中断模式,然后再通过pci_enable_msi()或 pci_enable_msix()进入MSI或MSI-X模式。这种操作并不常见,在开发过程中用于调试/测试。
3.5 使用MSI的考虑
3.5.1 选择MSI-X和MSI
如果设备同时支持MSI-X和MSI能力,应优先考虑使用MSI-X。MSI-X支持1~2048间任意数量的中断,而MSI只支持32个中断(并 且必须是2的冪)。MSI中断必须是连续分配的,系统不能像MSI-X那样分配这么多的中断向量。在某些平台上,MSI中断只能发送给一个CPU 组,MSI-X中断可以发给不同的CPU。
3.5.2 spinlock
多数驱动为每个设备定义了一个spinlock,在中断处理函数中取锁,对于引脚中断或者单个MSI中断,不需要禁用中断(Linux保证同一个中断不会 重入)。如果设备使用了多个中断,驱动在持锁期间必须禁用中断,否则在设备产生另一个中断时,驱动会递归取锁从而产生死锁。
有两个解决方法,一个是使用spin_lock_irqsave()或spin_lock_irq(),另一个是在request_irq()调用时指定IRQF_DISABLED,内核会在禁用中断的环境下完成整个中断处理过程。
如果MSI中断处理程序不在整个过程中持锁,使用第一种方法是最好的。如果想避免在中断禁用/使能状态间切换,则选择第二种方法。
3.6 如何得知设备的MSI/MSI-X已经使能
使用lspci -v。有些设备会显示"MSI"、"Message Signaled Interrupts"或者"MSI-X"能力,使能的会在前面显示+,禁用的会显示"-"。
4. MSI quirks
一些PCI芯片或设备不支持MSI,PCI子系统提供了三种方法禁用MSI:
1. 全局禁用
2. 禁用特定桥之下的所有设备
3. 禁用某个设备
4.1 全局禁用
有些host芯片不能正确支持MSI,如果厂家在ACPI FADT表中明确了,Linux会自动禁用MSI。有些单板没有在这个表包含这样的信息,需要自己检测,这些都列在drivers/pci /quriks.c中的quirk_disable_all_msi()中了。
如果单板在MSI支持上有问题,可以在内核命令参数里加上pci=nomsi以禁用所有设备的MSI。
4.2 禁用特定桥之下的所有设备
有些PCI桥不能在总线间正确地传递MSI,这种情况必须禁用该桥之下所有设备的MSI。
有些桥允许通过配置空间的某些位来使能MSI。在可能的情况下,Linux会尽量打开host芯片的MSI支持。如果某个桥片Linux并不识别,而你确定它可以使用MSI,可以通过下面的命令打开MSI支持。
echo 1 > /sys/bus/pci/devices/$bridge/msi_bus
$bridge是桥的PCI地址(比如0000:00:0e.0)。
要禁用MSI,使用echo 0即可。
4.3 禁用某个设备
如果某些设备已知在MSI实现上有问题,一般是在设备驱动里处理,如果有必要,也可以在quirk里处理。
4.4 设备MSI被禁用的原因查找
除上述情况外,还有很多原因会导致一个设备的MSI没有使能,第一步应该仔细检查dmesg,看MSI有没有使能,还要检查CONFIG_PCI_MSI配置有没有打开。
通过lspci -t可以查看设备之上的桥,读取/sys/bus/pci/devices/*/msi_bus看MSI是否使能了。
检查设备驱动是否支持MSI,比如是否调用了pci_enable_msi(), pci_enable_msix()或者pci_enable_msi_block()等等。