英语

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()。 通常,应避免使用这些 API,而应让 pci_alloc_irq_vectors() 限制向量的数量。 如果您对向量计数有合法的特殊用例,我们可能需要重新考虑该决定并添加一个 pci_nr_irq_vectors() 辅助函数,该函数透明地处理 MSI 和 MSI-X。

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 子系统有一个专用的 C 文件,用于其导出的设备驱动程序 API — drivers/pci/msi/api.c。 导出了以下函数

int pci_enable_msi(struct pci_dev *dev)

在设备上启用 MSI 中断模式

参数

struct pci_dev *dev

要操作的 PCI 设备

描述

旧设备驱动程序 API,用于在设备上启用 MSI 中断模式并分配单个中断向量。 成功后,分配的向量 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 设备

描述

旧设备驱动程序 API,用于在设备上禁用 MSI 中断模式,释放之前分配的中断向量,并恢复 INTx 仿真。 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,否则为其他错误。

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 向量的最大期望数量

描述

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

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/MSI-X 中断域上的中断

参数

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 设备

描述

旧设备驱动程序 API,用于在设备上禁用 MSI-X 中断模式,释放之前分配的中断向量,并恢复 INTx。 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,而纯 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 适配器重置后很有用。

bool pci_msi_enabled(void)

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

参数

void

无参数

返回

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