linux之 pcie MSI-X中断编程
一、前言
当前数据中心服务器,CPU基本都是基于PCIE总线和各种设备(例如,内存、显卡和网卡等)相连。而各种PCIE设备采用 MSIX(Message Signaled Interrupt eXtended - 基于消息的信号中断扩展)将中断信号发送给CPU。我们知道MSI最多支持32个中断向量号,而MSI-X中断向量数目最大为2048。那我们编程时如何获取、申请、控制这些中断向量资源呢,本文将为你揭开神秘的面纱。
二、MSI-X Capability
基于lspci 命令可以轻易获取到当前系统中PCI 设备的能力,而MSI-X 能力是PCIE设备众多能力中的一个。下面将基于网卡设备查看其 MSI-X Capability配置。
2.1 Capability配置
- 查看系统中的PCIE网卡设备
- # lspci | grep Eth
- 查看 PCIE 设备 1a:00.0 的MSI-X Capability
- # lspci -s 1a:00.0 -vvv
从上图PCIE设备的配置空间信息可以看出,该PCIE设备支持两个Bar地址空间(Region 0 & Region 3)
- Bar0:总线地址起始位置为 0x98000000, 总大小时 16MB
- Bar3:总线地址起始位置为 0x9a018000, 总大小时 32KB
- MSI-X :中断能力 为 Enable 状态,支持 129 中断向量号
- MSI-X Vector Table:位于 Bar3,起始地址或 offset 为 0
- MSI-X PBA (pending table):也位于Bar3,但起始地址为 0x00001000
下图展示了MSIX Capability在配置空间中的位置,其大小为3个DWORD,即12字节:
2.2 Capability标准
上图第二个红框关于MSI-X Capability的标准如下:
- 第1个DWORD 0x068位置:关键是 16~26 bit,存放着 Vector Table 的Size,最大为0x7FF,共11位,所以可知一个 PCIE function 最多只能支持 2的11次方,即2048个中断号。
- 第2个DWORD 0x06C位置:3~31 位是 MSI-X Table位于Bar空间的起始位置
- 第3个DWORD 0x06D位置:3~31 位是 MSI-X PBA位于Bar空间的起始位置
如何获取MSIX Capability的信息呢?下面将基于内核代码解释读取第一个DOWRD 中 Size of the MSI-X Table(即中断向量总数 Vector Number )的过程。
2.3 编程获取Capability
可以基于接口 pci_read_config_word 来读取PCIE EP(end point)设备的配置空间信息。下面代码用该接口读取MSIX Capability 的第一个DWORD 中的Message Control(16~31 位),从而获取到当前 PCIE EP 真实支持中断号数量。例如上面PCIE 网卡设备的 num_vectors 为130(129+1)。
#include <linux/pci.h>/* MSI-X registers (in MSI-X capability) */#define PCI_MSIX_FLAGS 2 /* Message Control */#define PCI_MSIX_FLAGS_QSIZE 0x07FF /* Table size */struct pci_dev *pdev = pci_dev;u16 msix_config;int num_vectors;
// pdev 为 PCIE 设备,pdev->msix_cap为设备配置空间中MSIX Capability的起始位置(PCI_MSIX_FLAGS表示读取Message Control)
// 读取到的 message control 寄存器放置于 msix_config 变量pci_read_config_word(pdev, pdev->msix_cap + PCI_MSIX_FLAGS, &msix_config);num_vectors = ((msix_config & PCI_MSIX_FLAGS_QSIZE) + 1);pr_info("MSIX: num_vectors=%d\n", num_vectors);
也可以基于PCI Driver提供的现有接口 pci_msix_vec_count (drivers/pci/msi.c),来获取给定PCIE设备支持的 中断向量数目(vector number)。
/*** pci_msix_vec_count - return the number of device's MSI-X table entries* @dev: pointer to the pci_dev data structure of MSI-X device function* This function returns the number of device's MSI-X table entries and* therefore the number of MSI-X vectors device is capable of sending.* It returns a negative errno if the device is not capable of sending MSI-X* interrupts.**/
int pci_msix_vec_count(struct pci_dev *dev)
{u16 control;if (!dev->msix_cap)return -EINVAL;pci_read_config_word(dev, dev->msix_cap + PCI_MSIX_FLAGS, &control);return msix_table_size(control);
}#define msix_table_size(flags) ((flags & PCI_MSIX_FLAGS_QSIZE) + 1)
三、MSI-X Table
MSIX Table 中存放着所有PCIE 设备的中断向量号,如下图MSIX Table位于BAR3 起始地址 0x9a018000 处,该地址对应PCIE EP 设备中的Memory 地址空间,该Memory 空间是PCIE 设备内部的寄存器空间。
3.1 table 结构
MSIX Table 中每一条Entry 代表一个中断向量,Msg Data 中包括了中断向量号,Msg Addr 中通常包含了多核CPU用于处理 中断的 Local APIC 编号。
从MSI-X table 的结构可以看出,每个entry占用4个DWORD,即16bytes,所以访问第N个Entry的地址是:n_entry_address = base address[BAR] + 16 * n
MSIX Table的结构中每一条Entry的中断信息,是什么时候产生的呢?接下来将从内核代码的角度进行分析。
3.2 Table初始化
如果你熟悉PCIE 驱动,内核会调用pci_driver 注册的 probe 函数,而probe 函数会负责中断向量的申请:
第一步:调用 pci_msix_vec_count 获取当前PCIE 设备支持的中断向量总数
第二步:调用pci_enable_msix_range 分配 MSIX Table 中每一个中断向量 Entry,获得软件可以使用的所有中断向量号,即msix_entry 的 vector 成员。
下面代码详细解释了pci_enable_msix_range 如何使用:
struct msix_entry {u32 vector; /* Kernel uses to write allocated vector */u16 entry; /* Driver uses to specify entry, OS writes */
};// 获取PCIE 设备支持的中断向量总数,pdev 为probe传入的struct pci_dev *
int num_vectors = pci_msix_vec_count(pdev);// num_vectors 为从MSIX Capability中获取到的 Table Size
// 根据PCIE 设备支持的中断向量总数,申请msix_entries 向量数组
struct msix_entry *msix_entries = kzalloc((sizeof(struct msix_entry) * num_vectors), GFP_KERNEL);// entry 的编号由驱动程序自己维护,而 vector 是具体的中断向量号,被 request_irq 使用
for (i=0; i<num_vectors; i++) {msix_entries[i].entry = i;
}// 返回申请成功的中断向量总数total_vecs ,pdev 为probe传入的struct pci_dev *,
// 2 为最小申请的中断向量数(少于2则返回-ENOSPC),num_vectors为(最大)需要申请的向量总数
total_vecs = pci_enable_msix_range(pdev, msix_entries, 2, num_vectors);
下面代码深入分析pci_enable_msix_range 在内核中的实现,你会发现MSIX Table Entry如何被写入:
probe
|--- pci_msix_vec_count
|--- pci_enable_msix_range (__pci_enable_msix_range)|---__pci_enable_msix|--- msix_capability_init|--- pci_msi_setup_msi_irqs|--- arch_setup_msi_irqs// 调用硬件(如X86)相关的接口获得IRQ Domain信息,Domain负责将硬件中断ID映射到软件的IRQ Number(vector)|--- native_setup_msi_irqs |--- [ msi_domain_alloc_irqs ][ msi_domain_alloc_irqs ]
|--- irq_domain_activate_irq ( __irq_domain_activate_irq )|--- msi_domain_activate ( domain->ops->activate )|--- irq_chip_write_msi_msg |--- pci_msi_domain_write_msg (data->chip->irq_write_msi_msg)|--- " __pci_write_msi_msg "// 在函数 [ msi_domain_alloc_irqs ] 中循环每一个中断号,最终调用 __pci_write_msi_msg
int msi_domain_alloc_irqs(struct irq_domain *domain, struct device *dev,int nvec)
{int i, ret, virq;for_each_msi_entry(desc, dev) {virq = desc->irq;// 中断号 irq_data = irq_domain_get_irq_data(domain, desc->irq);ret = irq_domain_activate_irq(irq_data, can_reserve);}
}// " __pci_write_msi_msg " 负责将每一个中断向量 Entry 写入 MSIX Table
void __pci_write_msi_msg(struct msi_desc *entry, struct msi_msg *msg)
{struct pci_dev *dev = msi_desc_to_pci_dev(entry);if (dev->current_state != PCI_D0 || pci_dev_is_disconnected(dev)) {/* Don't touch the hardware now */} else if (entry->msi_attrib.is_msix) {void __iomem *base = pci_msix_desc_addr(entry);// 将message address 和 message data 写入 MSIX tablewritel(msg->address_lo, base + PCI_MSIX_ENTRY_LOWER_ADDR);writel(msg->address_hi, base + PCI_MSIX_ENTRY_UPPER_ADDR);writel(msg->data, base + PCI_MSIX_ENTRY_DATA);}
}
另外补充一下, 初始化table, 在rapidio 驱动中, 是另一种方式:
在probe 函数中:
写配置空间的方式设置两个表;然后在
pci_enable_msix_exact():
pci_enable_msix_range()函数和上面一致了。
3.3 Table访问
MSIX Table Entry 的访问,需要借助于pci_ioremap_bar(ioremap), 将PCIE 设备Bar空间对应的设备内存(即PCIE终端设备上的Register空间)映射到主机的__iomem 类型虚拟地址,才可以被驱动程序访问。下面代码给出了MSIX Table的读取方法。
// 输入输入参数:struct pci_dev *pdev
void __iomem *bar3;// 将 iomem* 强制转换为 u32 *,则msi_tbl_addr 每次+1,就偏移 4 bytes
u32 *msi_tbl_addr = (u32*) bar3;
bar3 = pci_ioremap_bar(pdev, 3);u32 one_table_entry_size = sizeof(u32) * 4;
u32 ** msi_table_entry = kzalloc(one_table_entry_size * num_vectors), GFP_KERNEL);// 读取所有的 MSIX Table Entries 的方法
for (i = 0; i < num_vectors; ++i)for (j = 0; j < 4; ++j)msi_table_entry [i][j] = readl(msi_tbl_addr + i * 4 + j)// 读出第一条 MSIX Table Entry 的方法
u32 first_msi_table_entry[4] = {0};
first_msi_table_entry[0] = readl(msi_tbl_addr);
first_msi_table_entry[1] = readl(msi_tbl_addr+1);
first_msi_table_entry[2] = readl(msi_tbl_addr+2);
first_msi_table_entry[3] = readl(msi_tbl_addr+3);
下图展示了ioremap 用到的相关地址信息以及 map 过程:
- A 地址为 PCI Bus Address,表示在PCI总线上的地址,CPU并不能通过总线地址A(位于BAR范围内,例如上面网卡设备 bar3 的起始总线地址是0x9a018000 )直接访问总线上的PCI设备。
- B 地址是 物理地址,位于物理内存的MMIO Space,可以通过 " cat /proc/iomem | grep -i pci " 查找到所有PCIE 设备的 物理地址空间范围
- C 地址为虚拟地址,可以直接被CPU使用,即被驱动程序访问。
- PCI Host Bridge 负责进行 物理地址B 和 总线地址A 之间的转换
- ioremap 负责将 物理地址B 映射成 虚拟地址 C,这样CPU就可以通过 readl,writel 来访问总线地址A位于的总线地址空间了
四、Capability、Bar、MSI-X Table 关系图
- 左侧展示了MSI-X CAP 对应的数据结构信息,其存储在PCIE终端设备的配置空间
- 右侧展示了 MSI-X table,PBA 对应的具体数据结构信息,其存储于PCIE设备的片上内存(寄存器)
- 中间部分展示了 MSIX Table 和 PBA 对应的PCI总线地址起始位置,以及总共的IO Memory大小(16K)