英语

4. MSI 驱动程序指南 HOWTO

作者:

Tom L Nguyen;Martine Silbermann;Matthew Wilcox

版权:

2003, 2008 Intel Corporation

4.1. 关于本指南

本指南介绍了消息信号中断 (MSI) 的基础知识、使用 MSI 而不是传统中断机制的优势、如何更改驱动程序以使用 MSI 或 MSI-X 以及在设备不支持 MSI 时尝试的一些基本诊断方法。

4.2. 什么是 MSI?

消息信号中断是设备向特殊地址写入数据,从而导致 CPU 接收到中断。

MSI 功能最初在 PCI 2.2 中指定,后来在 PCI 3.0 中得到增强,允许单独屏蔽每个中断。MSI-X 功能也随 PCI 3.0 引入。它支持每个设备比 MSI 更多的中断,并允许独立配置中断。

设备可能同时支持 MSI 和 MSI-X,但一次只能启用一个。

4.3. 为什么要使用 MSI?

使用 MSI 比传统的基于引脚的中断具有三个优势。

基于引脚的 PCI 中断通常在多个设备之间共享。为了支持这一点,内核必须调用与中断关联的每个中断处理程序,这会导致整个系统的性能下降。MSI 从不共享,因此不会出现此问题。

当设备向内存写入数据,然后引发基于引脚的中断时,中断可能在所有数据到达内存之前到达(在 PCI-PCI 桥之后的设备上,这种情况发生的可能性更大)。为了确保所有数据都已到达内存,中断处理程序必须读取引发中断的设备上的寄存器。PCI 事务排序规则要求所有数据在可以从寄存器返回值之前到达内存。使用 MSI 可以避免此问题,因为生成中断的写入不能通过数据写入,因此在引发中断时,驱动程序知道所有数据都已到达内存。

PCI 设备每个功能只能支持一个基于引脚的中断。驱动程序通常必须查询设备以找出发生了什么事件,这会减慢常见情况下中断处理的速度。使用 MSI,设备可以支持更多中断,从而允许将每个中断专门用于不同的目的。一种可能的设计是将不频繁的情况(例如错误)赋予它们自己的中断,这允许驱动程序更有效地处理正常的中断处理路径。其他可能的设计包括为网卡中的每个数据包队列或存储控制器中的每个端口分配一个中断。

4.4. 如何使用 MSI

PCI 设备初始化为使用基于引脚的中断。设备驱动程序必须设置设备以使用 MSI 或 MSI-X。并非所有机器都正确支持 MSI,对于这些机器,下面描述的 API 将简单地失败,并且设备将继续使用基于引脚的中断。

4.4.1. 包含对 MSI 的内核支持

为了支持 MSI 或 MSI-X,必须在启用 CONFIG_PCI_MSI 选项的情况下构建内核。此选项仅在某些架构上可用,并且可能取决于是否设置了其他一些选项。例如,在 x86 上,您还必须启用 X86_UP_APIC 或 SMP 才能看到 CONFIG_PCI_MSI 选项。

4.4.2. 使用 MSI

大部分繁重的工作都在 PCI 层中为驱动程序完成。驱动程序只需请求 PCI 层为此设备设置 MSI 功能。

要自动使用 MSI 或 MSI-X 中断向量,请使用以下函数

int pci_alloc_irq_vectors(struct pci_dev *dev, unsigned int min_vecs,
              unsigned int max_vecs, unsigned int flags);

该函数为 PCI 设备分配最多 max_vecs 个中断向量。它返回分配的向量数或负错误。如果设备对最小向量数有要求,则驱动程序可以传递设置为此限制的 min_vecs 参数,如果 PCI 内核无法满足最小向量数,则返回 -ENOSPC。

flags 参数用于指定设备和驱动程序可以使用哪种类型的中断 (PCI_IRQ_INTX、PCI_IRQ_MSI、PCI_IRQ_MSIX)。也可以使用方便的简写 (PCI_IRQ_ALL_TYPES) 来请求任何可能类型的中断。如果设置了 PCI_IRQ_AFFINITY 标志,pci_alloc_irq_vectors() 将在可用的 CPU 周围分散中断。

要获取传递给 request_irq()free_irq() 以及向量的 Linux IRQ 编号,请使用以下函数

int pci_irq_vector(struct pci_dev *dev, unsigned int nr);

应在使用以下函数删除设备之前释放所有分配的资源

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 向量。

MSI 或 MSI-X 中断的典型用法是分配尽可能多的向量,可能达到设备支持的上限。如果 nvec 大于设备支持的数量,它将自动限制为支持的上限,因此无需预先查询支持的向量数量。

nvec = pci_alloc_irq_vectors(pdev, 1, nvec, PCI_IRQ_ALL_TYPES)
if (nvec < 0)
        goto out_err;

如果驱动程序无法或不愿意处理可变数量的 MSI 中断,则可以通过将该数字作为 'min_vecs' 和 'max_vecs' 参数传递给 pci_alloc_irq_vectors() 函数来请求特定数量的中断

ret = pci_alloc_irq_vectors(pdev, nvec, nvec, PCI_IRQ_ALL_TYPES);
if (ret < 0)
        goto out_err;

上面描述的请求类型的最臭名昭著的示例是为设备启用单个 MSI 模式。它可以通过将两个 1 作为 'min_vecs' 和 'max_vecs' 来完成

ret = pci_alloc_irq_vectors(pdev, 1, 1, PCI_IRQ_ALL_TYPES);
if (ret < 0)
        goto out_err;

某些设备可能不支持使用传统的线路中断,在这种情况下,驱动程序可以指定只接受 MSI 或 MSI-X

nvec = pci_alloc_irq_vectors(pdev, 1, nvec, PCI_IRQ_MSI | PCI_IRQ_MSIX);
if (nvec < 0)
        goto out_err;

4.4.3. 旧版 API

不应在新代码中使用以下用于启用和禁用 MSI 或 MSI-X 中断的旧 API

pci_enable_msi()              /* deprecated */
pci_disable_msi()             /* deprecated */
pci_enable_msix_range()       /* deprecated */
pci_enable_msix_exact()       /* deprecated */
pci_disable_msix()            /* deprecated */

此外,还有一些 API 可以提供支持的 MSI 或 MSI-X 向量的数量:pci_msi_vec_count()pci_msix_vec_count()。通常,应避免使用这些,而应让 pci_alloc_irq_vectors() 限制向量的数量。如果您对向量的计数有合法的特殊用例,我们可能必须重新考虑该决定并添加一个透明地处理 MSI 和 MSI-X 的 pci_nr_irq_vectors() 辅助函数。

4.4.4. 使用 MSI 时的注意事项

4.4.4.1. 自旋锁

大多数设备驱动程序都有一个每个设备的自旋锁,该自旋锁在中断处理程序中获取。对于基于引脚的中断或单个 MSI,没有必要禁用中断(Linux 保证不会重新进入同一中断)。如果设备使用多个中断,则驱动程序必须在持有锁定时禁用中断。如果设备发送不同的中断,驱动程序将尝试递归获取自旋锁而死锁。可以通过使用 spin_lock_irqsave() 或 spin_lock_irq() 来避免此类死锁,这会禁用本地中断并获取锁(请参阅 不可靠的锁定指南)。

4.4.5. 如何判断设备是否启用了 MSI/MSI-X

使用 ‘lspci -v’(以 root 身份)可能会显示一些具有“MSI”、“消息信号中断”或“MSI-X”功能的设备。这些功能中的每一个都有一个“启用”标志,后面跟着“+”(已启用)或“-”(已禁用)。

4.5. MSI 怪癖

已知有几个 PCI 芯片组或设备不支持 MSI。PCI 堆栈提供了三种禁用 MSI 的方法

  1. 全局

  2. 在特定桥接器后面的所有设备上

  3. 在单个设备上

4.5.1. 全局禁用 MSI

某些主机芯片组根本不支持 MSI。如果我们幸运的话,制造商知道这一点,并在 ACPI FADT 表中指出了这一点。在这种情况下,Linux 会自动禁用 MSI。一些主板没有在表中包含此信息,因此我们必须自己检测它们。这些的完整列表可以在 drivers/pci/quirks.c 中的 quirk_disable_all_msi() 函数附近找到。

如果您的主板存在 MSI 问题,您可以在内核命令行上传递 pci=nomsi 以禁用所有设备上的 MSI。您最好向 linux-pci@vger.kernel.org 报告问题,包括完整的“lspci -v”,以便我们可以将怪癖添加到内核中。

4.5.2. 禁用桥接器下方的 MSI

某些 PCI 桥接器无法正确地在总线之间路由 MSI。在这种情况下,必须禁用桥接器后所有设备上的 MSI。

某些桥接器允许您通过更改其 PCI 配置空间中的某些位来启用 MSI(尤其是 Hypertransport 芯片组,如 nVidia nForce 和 Serverworks HT2000)。与主机芯片组一样,Linux 大多知道它们,如果可以,会自动启用 MSI。如果您的 Linux 不知道桥接器,您可以使用您知道的任何方法在配置空间中启用 MSI,然后通过执行以下操作在该桥接器上启用 MSI:

echo 1 > /sys/bus/pci/devices/$bridge/msi_bus

其中 $bridge 是您已启用的桥接器的 PCI 地址(例如 0000:00:0e.0)。

要禁用 MSI,请回显 0 而不是 1。更改此值应谨慎进行,因为它可能会破坏此桥接器下所有设备的 中断处理。

再次强调,如果遇到任何需要特殊处理的桥接器,请通知 linux-pci@vger.kernel.org

4.5.3. 禁用单个设备上的 MSI

已知某些设备的 MSI 实现存在缺陷。通常,这会在各个设备驱动程序中处理,但有时需要使用怪癖来处理。某些驱动程序有一个选项来禁用 MSI 的使用。虽然这对驱动程序作者来说是一种方便的解决方法,但这并不是一个好的做法,不应效仿。

4.5.4. 查找设备上禁用 MSI 的原因

从以上三节可以看出,有很多原因可能导致给定设备未启用 MSI。您的第一步应该是仔细检查 dmesg,以确定是否为您的机器启用了 MSI。您还应该检查您的 .config,以确保您已启用 CONFIG_PCI_MSI。

然后,“lspci -t”给出设备上方的桥接器列表。读取 /sys/bus/pci/devices/*/msi_bus 将告诉您是否启用 (1) 或禁用 (0) 了 MSI。如果在 PCI 根和设备之间的桥接器的任何 msi_bus 文件中发现 0,则禁用 MSI。

还值得检查设备驱动程序以查看它是否支持 MSI。例如,它可能包含对 pci_alloc_irq_vectors() 以及 PCI_IRQ_MSI 或 PCI_IRQ_MSIX 标志的调用。

4.6. 设备驱动程序 MSI(-X) API 列表

PCI/MSI 子系统为其导出的设备驱动程序 API 提供了专用的 C 文件 — drivers/pci/msi/api.c。以下函数已导出

int pci_enable_msi(struct pci_dev *dev)

在设备上启用 MSI 中断模式

参数

struct pci_dev *dev

要操作的 PCI 设备

描述

用于在设备上启用 MSI 中断模式并分配单个中断向量的旧设备驱动程序 API。成功后,分配的向量 Linux IRQ 将保存在 dev->irq 中。驱动程序必须在清理时调用 pci_disable_msi()

注意

通常应使用较新的 pci_alloc_irq_vectors() / pci_free_irq_vectors() API 对。

返回

成功时为 0,否则为 errno

void pci_disable_msi(struct pci_dev *dev)

在设备上禁用 MSI 中断模式

参数

struct pci_dev *dev

要操作的 PCI 设备

描述

用于在设备上禁用 MSI 中断模式,释放先前分配的中断向量并恢复 INTx 仿真的旧设备驱动程序 API。PCI 设备 Linux IRQ (dev->irq) 恢复为其默认的引脚断言 IRQ。这是 pci_enable_msi() 的清理配对。

注意

通常应使用较新的 pci_alloc_irq_vectors() / pci_free_irq_vectors() API 对。

int pci_msix_vec_count(struct pci_dev *dev)

获取设备上 MSI-X 中断向量的数量

参数

struct pci_dev *dev

要操作的 PCI 设备

返回

此设备上可用的 MSI-X 中断向量的数量(即,设备的 MSI-X 功能结构的“表大小”),如果设备不支持 MSI-X,则为 -EINVAL,否则为其他 errno。

int pci_enable_msix_range(struct pci_dev *dev, struct msix_entry *entries, int minvec, int maxvec)

在设备上启用 MSI-X 中断模式

参数

struct pci_dev *dev

要操作的 PCI 设备

struct msix_entry *entries

输入/输出参数,MSI-X 配置条目的数组

int minvec

所需的最少 MSI-X 向量数

int maxvec

所需的最大 MSI-X 向量数

描述

用于在设备上启用 MSI-X 中断模式并根据需要配置其 MSI-X 功能结构的旧设备驱动程序 API。传入的 entries 数组必须将其每个成员的“entry”字段设置为所需的(有效的)MSI-X 向量号,可以通过 pci_msix_vec_count() 查询有效的 MSI-X 向量号的范围。如果成功,驱动程序必须在清理时调用 pci_disable_msix()

注意

通常应使用较新的 pci_alloc_irq_vectors() / pci_free_irq_vectors() API 对。

返回

分配的 MSI-X 向量数(可能小于 maxvecs),其中此类分配的向量的 Linux IRQ 编号将保存回 entries 数组元素的“vector”字段中。如果可用的中断向量少于 minvecs,则返回 -ENOSPC。如果传递的 entries 成员的“entry”字段无效或重复,或者先前在设备上启用了普通 MSI 中断模式,则返回 -EINVAL。否则返回其他 errno。

bool pci_msix_can_alloc_dyn(struct pci_dev *dev)

查询是否支持启用 MSI-X 后动态分配

参数

struct pci_dev *dev

要操作的 PCI 设备

返回

如果支持,则为 True,否则为 false

struct msi_map pci_msix_alloc_irq_at(struct pci_dev *dev, unsigned int index, const struct irq_affinity_desc *affdesc)

在给定的 MSI-X 向量索引或任何空闲向量索引处启用 MSI-X 后分配一个 MSI-X 中断

参数

struct pci_dev *dev

要操作的 PCI 设备

unsigned int index

要分配的索引。如果 index == MSI_ANY_INDEX,则这将在 MSI-X 表中分配下一个空闲索引

const struct irq_affinity_desc *affdesc

指向亲缘性描述符结构的可选指针。否则为 NULL

返回

一个 struct msi_map

成功时,msi_map::index 包含分配的索引 (>= 0),而 msi_map::virq 包含分配的 Linux 中断号 (> 0)。

如果失败,msi_map::index 包含错误代码,并且 msi_map::virq 设置为 0。

void pci_msix_free_irq(struct pci_dev *dev, struct msi_map map)

在 PCI/MSIX 中断域上释放一个中断

参数

struct pci_dev *dev

要操作的 PCI 设备

struct msi_map map

一个描述要释放的中断的 struct msi_map

描述

撤消中断向量分配。不禁用 MSI-X。

void pci_disable_msix(struct pci_dev *dev)

在设备上禁用 MSI-X 中断模式

参数

struct pci_dev *dev

要操作的 PCI 设备

描述

用于在设备上禁用 MSI-X 中断模式、释放先前分配的中断向量并恢复 INTx 的旧设备驱动程序 API。PCI 设备 Linux IRQ (dev->irq) 恢复为其默认的引脚断言 IRQ。这是 pci_enable_msix_range() 的清理配对。

注意

通常应使用较新的 pci_alloc_irq_vectors() / pci_free_irq_vectors() API 对。

int pci_alloc_irq_vectors(struct pci_dev *dev, unsigned int min_vecs, unsigned int max_vecs, unsigned int flags)

分配多个设备中断向量

参数

struct pci_dev *dev

要操作的 PCI 设备

unsigned int min_vecs

所需的最小向量数(必须 >= 1)

unsigned int max_vecs

所需的最大向量数

unsigned int flags

以下一个或多个

  • PCI_IRQ_MSIX 允许尝试 MSI-X 向量分配

  • PCI_IRQ_MSI 允许尝试 MSI 向量分配

  • PCI_IRQ_INTX 允许尝试 INTx 中断,当且仅当 min_vecs == 1 时

  • PCI_IRQ_AFFINITY 通过将向量分散到可用的 CPU 周围来自动管理 IRQ 亲和性

描述

在设备上分配最多 max_vecs 个中断向量。 MSI-X irq 向量分配的优先级高于普通 MSI,后者的优先级高于传统的 INTx 模拟。

成功分配后,调用者应使用 pci_irq_vector() 来获取要传递给 request_threaded_irq() 的 Linux IRQ 号。驱动程序必须在清理时调用 pci_free_irq_vectors()

返回

分配的向量数(可能小于 max_vecs),如果可用的中断向量少于 min_vecs 则返回 -ENOSPC,否则返回其他错误码。

int pci_alloc_irq_vectors_affinity(struct pci_dev *dev, unsigned int min_vecs, unsigned int max_vecs, unsigned int flags, struct irq_affinity *affd)

分配具有亲和性要求的多个设备中断向量

参数

struct pci_dev *dev

要操作的 PCI 设备

unsigned int min_vecs

所需的最小向量数(必须 >= 1)

unsigned int max_vecs

所需的最大向量数

unsigned int flags

分配标志,如 pci_alloc_irq_vectors() 中所述

struct irq_affinity *affd

亲和性要求(可以为 NULL)。

描述

pci_alloc_irq_vectors() 相同,但带有额外的 affd 参数。有关更多详细信息,请查看该函数的文档以及 struct irq_affinity

int pci_irq_vector(struct pci_dev *dev, unsigned int nr)

获取设备中断向量的 Linux IRQ 号

参数

struct pci_dev *dev

要操作的 PCI 设备

unsigned int nr

设备相关中断向量索引(从 0 开始);根据中断模式具有不同的含义

  • MSI-X MSI-X 向量表中的索引

  • MSI 已启用的 MSI 向量的索引

  • INTx 必须为 0

返回

Linux IRQ 号,如果 nr 超出范围,则返回 -EINVAL

const struct cpumask *pci_irq_get_affinity(struct pci_dev *dev, int nr)

获取设备中断向量的亲和性

参数

struct pci_dev *dev

要操作的 PCI 设备

int nr

设备相关中断向量索引(从 0 开始);根据中断模式具有不同的含义

  • MSI-X MSI-X 向量表中的索引

  • MSI 已启用的 MSI 向量的索引

  • INTx 必须为 0

返回

MSI/MSI-X 向量亲和性,如果 nr 超出范围,或者在没有显式亲和性要求的情况下分配 MSI(-X) 向量(例如,通过 pci_enable_msi()pci_enable_msix_range()pci_alloc_irq_vectors(),且不带 PCI_IRQ_AFFINITY 标志),则返回 NULL。 如果设备处于传统的 INTx 模式,则返回一个表示系统启动期间所有可用 CPU 的通用 CPU ID 集。

void pci_free_irq_vectors(struct pci_dev *dev)

释放先前为设备分配的 IRQ

参数

struct pci_dev *dev

要操作的 PCI 设备

描述

撤销之前通过 pci_alloc_irq_vectors_affinity()pci_alloc_irq_vectors() 完成的中断向量分配以及可能的设备 MSI/MSI-X 启用。

void pci_restore_msi_state(struct pci_dev *dev)

在设备上恢复缓存的 MSI(-X) 状态

参数

struct pci_dev *dev

要操作的 PCI 设备

描述

将 Linux 缓存的 MSI(-X) 状态写回设备。这通常在系统恢复时或在错误恢复 PCI 适配器重置后很有用。

int pci_msi_enabled(void)

是否在系统范围内启用了 MSI(-X) 中断?

参数

void

无参数

返回

如果 MSI 没有通过 ACPI FADT、PCI 桥接器怪癖或“pci=nomsi”内核命令行选项全局禁用,则为 true。