当前位置: 首页 > news >正文

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 过程:

  1. A 地址为 PCI Bus Address,表示在PCI总线上的地址,CPU并不能通过总线地址A(位于BAR范围内,例如上面网卡设备 bar3 的起始总线地址是0x9a018000 )直接访问总线上的PCI设备。
  2. B 地址是 物理地址,位于物理内存的MMIO Space,可以通过 " cat /proc/iomem | grep -i pci " 查找到所有PCIE 设备的 物理地址空间范围
  3. C 地址为虚拟地址,可以直接被CPU使用,即被驱动程序访问。
  4. PCI Host Bridge 负责进行 物理地址B 和 总线地址A 之间的转换
  5. ioremap 负责将 物理地址B 映射成 虚拟地址 C,这样CPU就可以通过 readl,writel 来访问总线地址A位于的总线地址空间了

四、Capability、Bar、MSI-X Table 关系图

  1. 左侧展示了MSI-X CAP 对应的数据结构信息,其存储在PCIE终端设备的配置空间
  2. 右侧展示了 MSI-X table,PBA 对应的具体数据结构信息,其存储于PCIE设备的片上内存(寄存器)
  3. 中间部分展示了 MSIX Table 和 PBA 对应的PCI总线地址起始位置,以及总共的IO Memory大小(16K)

http://www.xdnf.cn/news/556831.html

相关文章:

  • 自动化测试核心知识梳理与 Java 代码详解
  • 基于正点原子阿波罗F429开发板的LWIP应用(3)——Netbiosns功能
  • 嵌入式培训之系统编程(一)标准IO、文件操作
  • Liquid Wire 柔性应变传感器:金属凝胶导体 | 仿生肌肉长度监测 | 高精度动作控制
  • 特定领域 RAG中细调嵌入模型能否提升效果?
  • IVX:重构 AI 原生开发范式,让模型调用成为指尖艺术​
  • PostgreSQL简单使用
  • 深入浅出人工智能:机器学习、深度学习、强化学习原理详解与对比!
  • 【深度学习-Day 14】从零搭建你的第一个神经网络:多层感知器(MLP)详解
  • 第六天的尝试
  • 服务器部署1Panel
  • 證券行業證券交易系統開發方案
  • 基于SpringBoot+Vue的学籍管理系统的设计与实现
  • Kubernetes在线练习平台深度对比:KillerCoda与Play with Kubernetes
  • 【开源工具】文件夹结构映射工具 | PyQt5实现多模式目录复制详解
  • 【鸿蒙开发】Hi3861学习笔记- MQTT通信
  • 统一端点管理(UEM):定义、优势与重要性
  • 从零开始:Python 从0到1轻松入门
  • 易路 AI 招聘:RPA+AI 颠覆传统插件模式,全流程自动化实现效率跃迁
  • 物业收费智能化:如何实现账单零差错自动生成?
  • SpringBean模块(三)具有生命周期管理能力的类(1)AutowireCapableBeanFactory
  • DOS常用命令及dos运行java
  • 协程+Flow:现代异步编程范式,替代RxJava的完整实践指南
  • NVIDIA Earth-2 AI 天气模型 DLI 课程:解锁全球风云的未来之匙
  • 4大AI智能体平台,你更适合哪一个呐?
  • 第六部分:第三节 - 路由与请求处理:解析顾客的点单细节
  • ⭐️白嫖的阿里云认证⭐️ 第二弹【课时3:大模型辅助内容生产场景】for 「大模型Clouder认证:利用大模型提升内容生产能力」
  • 基于YOLO11深度学习的变压器漏油检测系统【Python源码+Pyqt5界面+数据集+安装使用教程+训练代码】【附下载链接】
  • 通过 API 获取 1688 平台店铺所有商品信息的完整流程
  • Vue+eElement ui el-input输入框 type=number 输入无效。赋值输入框也不显示(问题已解决)