传统上,外设向 CPU 发送中断请求时,会通过物理总线(PCB上的丝印)向中断控制器发送电信号,中断控制器再进一步将中断信号发送到 CPU,这也是通过物理上与 CPU 引脚连接实现的。这就带来了一个问题,CPU 或 中断控制器的物理引脚总是有限的,设备却是无限的,想要让每个设备都物理上连接到中断控制器是不现实的,所以必须通过中断线复用/共享的方式来实现上述要求。在中断共享的情况下,CPU 收到一个中断信号,需要尝试运行所有共享该中断号的 ISR,这必然带来了性能上的损耗。而 MSI (Message Signaled Interrupt)中断通过基于消息的方式传递中断,从而避免了上述问题。
什么是 MSI
MSI 消息信号中断,通过使用消息信号而不是硬件中断引脚来通知处理器有中断请求。本质上,消息就是设备向一个特定地址的写入的特定数据:
- 当设备需要中断时,它会生成一个消息,通常是向特定内存地址写入一个特定的数据值。
- 处理器和中断控制器监控这些特定内存地址,当检测到写入操作时,会将其解释为中断请求。
MSI 功能最初在 PCI 2.2 中指定,后来在 PCI 3.0 中得到增强,以允许每个中断单独被屏蔽。MSI-X 功能也随 PCI 3.0 引入。它每个设备支持的中断比 MSI 更多,并允许中断被独立配置。
设备可能同时支持 MSI 和 MSI-X,但一次只能启用其中一个。
使用 MSI 的好处
与传统基于引脚的重点,基于消息信号的 MSI/MSI-X 中断有如下优点:
无需共享:基于引脚的 PCI 中断通常在多个设备之间共享。为支持这一点,内核必须调用与每个中断相关联的中断处理程序,这导致整个系统的性能降低。MSI 从不共享,因此不会出现此问题。
更多中断 :通常 PCI 只有一个中断引脚,但是该设备往往可能会产生多种事件,使用基于引脚的中的时,通常的做法是在中断服务程序中再次查询设备寄存器来确定设备事件,这无疑减慢了中断处理速度。使用 MSI 中断,可以为每种事件设置不同的中断(不同消息)和中断处理函数,换句话说,设备可以支持更多中断,允许每个中断专门用于不同的目的。
数据一致性 :在基于引脚的中断中,设备通过硬件中断引脚向处理器发送中断信号。然而,存在一个潜在的问题:设备在写入数据到内存后,立即触发中断信号,可能导致中断信号在所有数据完全写入内存之前到达处理器。这种情况在设备通过 PCI-PCI 桥连接时尤为明显,因为桥接设备可能引入额外的延迟。(简单描述就是,中断信号传递的速度快于数据写入内存速度)。对于 MSI 中断来说,不存在这个问题,因为中断信号也是写入内存的,这时只要保证数据和中断消息的写入顺序即可。
使用 MSI
PCI 设备被初始化为使用基于引脚的中断。设备驱动程序必须设置设备以使用 MSI 或 MSI-X。并非所有机器都能正确支持 MSI,对于那些机器,下面描述的 API 将简单地失败,设备将继续使用基于引脚的中断。
内核配置
要支持 MSI 或 MSI-X,内核必须在启用 CONFIG_PCI_MSI
选项的情况下构建。此选项仅在某些架构上可用,并且可能还取决于其他一些选项的设置。例如,在 x86 上,您还必须启用 X86_UP_APIC
或 SMP
才能看到 CONFIG_PCI_MSI
选项。
分配和释放中断向量
大部分的硬件工作是在 PCI 层为驱动程序完成的。驱动程序只需要请求 PCI 层为该设备设置 MSI 功能。
若要自动使用 MSI 或 MSI-X 中断向量,请使用以下函数,该函数为设备分配多个中断号/向量:
1 | /* 成功时返回分配的中断数量,失败返回负值 */ |
- dev:内核中,PCI 设备的抽象
- min_vecs:请求申请的中断向量的数量下限
- max_vecs:请求申请的中断向量的数量上限
- flags:指定设备和驱动程序可以使用哪种类型的中断(
PCI_IRQ_LEGACY
,PCI_IRQ_MSI
,PCI_IRQ_MSIX
)。一个方便的快捷方式PCI_IRQ_ALL_TYPES
可用于请求任何可能的中断类型。
有获取就应该有分配,在设备 remove
时,应该释放申请的中断:
1 | void pci_free_irq_vectors(struct pci_dev *dev); |
如果一个设备同时支持 MSI-X 和 MSI 功能,上述 API 将优先使用 MSI-X 设施而非 MSI 设施。MSI-X 支持 1 到 2048 之间的任意数量的中断。相比之下,MSI 被限制为最多 32 个中断(并且必须是 2 的幂)。此外,MSI 中断向量必须连续分配,因此系统可能无法为 MSI 分配与为 MSI-X 一样多的向量。
在某些平台上,MSI 中断必须全部针对同一组 CPU,而 MSI-X 中断可以全部针对不同的 CPU。
如果一个设备既不支持 MSI-X 也不支持 MSI,它将回退到单个传统 IRQ 向量。
尽可能多的分配方式
1 | nvec = pci_alloc_irq_vectors(pdev, 1, nvec, PCI_IRQ_ALL_TYPES) |
MSI 或 MSI-X 中断的典型用法是分配尽可能多的向量,可能达到设备支持的极限。如果 nvec
大于设备支持的数量,它将自动被限制为支持的极限,因此无需事先查询支持的向量数量。
固定数量的分配方式
1 | ret = pci_alloc_irq_vectors(pdev, nvec, nvec, PCI_IRQ_ALL_TYPES); |
如果不想处理可变数量的中断,那么直接使用上面这种方式即可,nvec
为固定的中断数量。
唯一中断的分配方式
1 | ret = pci_alloc_irq_vectors(pdev, 1, 1, PCI_IRQ_ALL_TYPES); |
很简单,就是只申请 1 个中断向量。
只申请 MSI/MSI-X 中断
1 | nvec = pci_alloc_irq_vectors(pdev, 1, nvec, PCI_IRQ_MSI | PCI_IRQ_MSIX); |
某些设备可能不支持使用传统的线路中断,在这种情况下,驱动程序可以指定仅 MSI 或 MSI-X。
MSI 中断向量转换到 Linux IRQ
要获取传递给 request_irq()
和 free_irq()
的 Linux IRQ 编号以及向量,要使用以下函数:
1 | int pci_irq_vector(struct pci_dev *dev, unsigned int nr); |
废弃接口
下面这些接口会在老代码中看到,但务必不要继续使用:
1 | pci_enable_msi() /* deprecated */ |
驱动编写
模块入口和出口
1 | static int __init pci_init(void) |
pci_driver 驱动对象
1 | static struct pci_driver pci_drv = { |
id_table 设备匹配表
1 | static struct pci_device_id ids[] = { |
pci 私有设备对象
保存了驱动程序会经常使用的一些成员,如设备号,字符设备,pci_dev 等,还对设备的 BAR 空间,irq 等进行保存:
1 | struct pci_private_t { |
probe 探测设备
使能 PCI 设备
按照 PCI 设备驱动的编写顺序,首先使能设备:
1 | retval = pci_enable_device(pdev); |
为 MMIO 预留空间
内存(MMIO)和 I/O 端口地址不应直接从 PCI 设备配置空间读取。应使用 pci_dev
结构中的值,因为 PCI “总线地址” 可能已被特定于架构/芯片组的内核支持重映射为 “主机物理” 地址。
1 | retval = pci_request_regions(pdev, MODULE_NAME); |
为设备数据申请空间
1 | pci_prv = kzalloc(sizeof(struct pci_private_t), GFP_KERNEL); |
申请 MSI 中断
1 | retval = irq_alloc(pci_prv); |
具体实现都在 irq_alloc
函数中,该函数首先使用上面提到的接口 pci_alloc_irq_vectors
申请 MSI 中断向量,然后将 MSI 向量转换成 Linux IRQ 号,并保存到设备数据中,如下:
1 | static int irq_alloc(struct pci_private_t *pci_prv) |
相应的,释放 IRQ 的函数如下:
1 | static void irq_free(struct pci_private_t *pci_prv) |
中断处理函数如下,只是简单打印:
1 | static irqreturn_t default_pci_isr(int irq, void *dev_id) |
映射 BAR 空间
这部分在本系列文章已经讲述过,这里不在赘述,直接看代码:
1 | retval = map_bars(pci_prv); |
其主要的实现也封装到了函数 map_bars
中,具体实现如下:
1 | static int map_sigal_bar(struct pci_private_t *pci_prv, int idx) |
相应的,也有 unmap_bars
1 | static void unmap_bars(struct pci_private_t *pci_prv) |
创建字符设备
1 | retval = create_cdev(pci_prv); |
看具体实现,比较常规:
1 | static int create_cdev(struct pci_private_t *pci_prv) |
remove 移除设备
按照和 probe
相反的顺序释放设备占用的资源:
1 | static void pci_remove(struct pci_dev *pdev) |
完整的驱动代码
和上一节的代码相同,这里直接放出所有细节,内核版本是 5.10 :
头文件
1 |
|
源文件
1 |
|