当前数据中心服务器,内存,显卡,网卡等设备均通过PCIe总线与CPU相连,而PCIe设备使用的最多的就是MSI和MSI-X中断。MSI中断是通过在内存写入信息来触发的一种消息中断类型,其中内存地址由硬件设备和系统协商决定。而MSI-X中断是在MSI中断的基础上扩展的一种消息类型,主要目的是解决MSI中断存在的一些限制。两者区别如下
类型 | MSI | MSI-X |
中断数 | 32 | 2048 |
中断号 | 必须连续 | 可以任意分配 |
中断信息 | 存放于capbility寄存器 | MSIX-table,存放BAR空间 |
下面将对MSI和MSI-X中断进行分析,最后会以笔者在工作中使用的MSI中断出现的单中断问题进行研究。
1.MSI中断
下图为 MSI中断的Capabilities结构。
MSI Capability 的ID是5,共有四种组成方式,分别是32位和64位的Message结构。
Capability ID:记录msi capibility的ID号,固定为0x5
Next Cap Ptr:指向下一个新的Capability寄存器的地址
Message Address:系统软件配置此寄存器分配给PCI设备,CPU可产生中断的寄存器的地址;
Message Data: 系统软件配置此寄存器为符合CPU中断解析规则的内容,系统会在初始化MSI中断的时候写入中断向量号到Message Data。当PCIe设备需要上送中断时,就将Meeage Data中的中断向量号写入Message Address中。
Message Control Register:存放当前PCIe设备使用MSI机制进行中断请求的状态和控制信息。
MSI enable控制msi是否使能,Multiple Message Capable表示设备能够支持的中断向量数量,Multi Message表示实际使用的中断数量。可见MSI中断数量最大支持32哥。
以笔者的驱动设备,查看PCIe设备的MSI 的Capabilities。可见MSI的 Capabilities相对偏移为0x50。
对于网卡驱动,如果需要使用MSI中断,就需要通过probe函数进行MSI中断的申请,同时在open函数中完成中断的使能;
1.驱动调用接口申请msi中断:ret = pci_alloc_irq_vectors(pdev, msi_num + 1, msi_num + 1, PCI_IRQ_MSI);
查看该函数调用关系:
pci_alloc_irq_vectors
pci_alloc_irq_vectors_affinity
__pci_enable_msi_range
msi_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
查看_pci_write_msi_msg函数,对于msi中断来说,它负责配置msi的message control,message addr,message data。一般message data的数据为中断向量,message data的数据有什么作用呢,它的主要作用是当PCIe设备需要上报中断时,会将message的data的数据写入message addr上,这样系统感知到message addr上数据有变化时,则会知道将中断传递给哪一个中断向量上。
void __pci_write_msi_msg(struct msi_desc *entry, struct msi_msg *msg)
{
......
else {
int pos = dev->msi_cap;
u16 msgctl;
pci_read_config_word(dev, pos + PCI_MSI_FLAGS, &msgctl);
msgctl &= ~PCI_MSI_FLAGS_QSIZE;
msgctl |= entry->msi_attrib.multiple << 4;
pci_write_config_word(dev, pos + PCI_MSI_FLAGS, msgctl);
pci_write_config_dword(dev, pos + PCI_MSI_ADDRESS_LO,
msg->address_lo);
if (entry->msi_attrib.is_64) {
pci_write_config_dword(dev, pos + PCI_MSI_ADDRESS_HI,
msg->address_hi);
pci_write_config_word(dev, pos + PCI_MSI_DATA_64,
msg->data);
} else {
pci_write_config_word(dev, pos + PCI_MSI_DATA_32,
msg->data);
}
/* Ensure that the writes are visible in the device */
pci_read_config_word(dev, pos + PCI_MSI_FLAGS, &msgctl);
}
......
}
2.使能MSI中断:devm_request_irq。
2.MSI-X中断
与MSI Capability寄存器相比,MSI-X Capability寄存器使用一个数组存放Message Address字段和Message Data字段,而不是将这两个字段放入Capability寄存器中,这个数组称为MSI-X Table。从而当PCIe设备使用MSI-X机制时,每一个中断请求可以使用独立的Message Address字段和Message Data字段。可以通过lspci 查看msix table位于哪个bar空间以及相对偏移多少。
由上图可知,该PCIe网卡设备开放了30个MSI-X中断(驱动如果只需要30个以下的中断,那么对于网卡设备开放了30个中断也是可以的),并且中断已经使能,其MSI-X table位于bar 0空间处,相对偏移为0x4000,PBA也位于bar 0处,相对偏移在0xa000.
Message address和Message Upper address字段存放的是MSI-X memory write请求需要使用的地址。
Message Data字段存放的是MSI-X memory write请求需要使用的data。该地址和CPU的架构相关,是使能MSI-X时,系统软件写入的。
下图表示MSI-X table以及PBA表存放的位置,不一定在bar 0上也有可能在其他的bar上,主要看PCIe设备如何分配。
对于网卡驱动,如果需要使用msix中断,就需要通过probe函数进行msix中断的申请,同时在open函数中完成中断的使能;
1.通过pci_msix_vec_count获取当前pcie设备支持的msix中断总数,如果需要申请的msix中断小于支持的总数,则会有异常;
if (num_vectors > pci_msix_vec_count(pdev))
{
return -1;
}
2.调用pci_enable_msix_range函数分配msix table。
struct msix_entry {
u16 vector; /* kernel uses to write allocated vector */
u16 entry; /* driver uses to specify entry, OS writes */
};
msix_entries = devm_kzalloc(ctrl->dev, (sizeof(struct msix_entry) * num_vectors), GFP_KERNEL);
for (i = 0; i < num_vectors; i++)
{
msix_entries[i].entry = i;
}
total_vecs = pci_enable_msix_range(pdev, msix_entries, num_vectors, num_vectors);
if(total_vecs < num_vectors)
{
ret = total_vecs;
goto no_msix;
}
3.根据msix_entry中的vector申请irq,这个步骤主要是将msix table的controller位打开,使得设备能够上送中断。
irq_num = msix_entries[0].vector;
memset(name, 0, sizeof(name));
sprintf(name, "%s:%s-%d", RESOURCE_NAME, "dev", 0);
ret = devm_request_irq(dev, irq_num, irq_handler, 0, name, dev);
查看pci_enable_msix_range函数的调用栈,可见最后会调用__pci_write_msi_msg函数进行初始化。
__pci_write_msi_msg+1
msi_domain_activate+108
__irq_domain_activate_irq+85
irq_domain_activate_irq+45
__msi_domain_alloc_irqs+471
msi_domain_alloc_irqs+23
msix_capability_init+805
pci_enable_msix_range+339
查看一下该函数关于MSI-X部分。
void __pci_write_msi_msg(struct msi_desc *entry, struct msi_msg *msg)
{
......
else if (entry->msi_attrib.is_msix) {
void __iomem *base = pci_msix_desc_addr(entry);
u32 ctrl = entry->msix_ctrl;
bool unmasked = !(ctrl & PCI_MSIX_ENTRY_CTRL_MASKBIT);
if (entry->msi_attrib.is_virtual)
goto skip;
/*
* The specification mandates that the entry is masked
* when the message is modified:
*
* "If software changes the Address or Data value of an
* entry while the entry is unmasked, the result is
* undefined."
*/
if (unmasked)
pci_msix_write_vector_ctrl(entry, ctrl | PCI_MSIX_ENTRY_CTRL_MASKBIT);
writel(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);
if (unmasked)
pci_msix_write_vector_ctrl(entry, ctrl);
/* Ensure that the writes are visible in the device */
readl(base + PCI_MSIX_ENTRY_DATA);
}
......
}
可见该函数关于MSI-X 部分主要是将地址写入MSI-X table表中的upper-addry以及lower-addr,同时将msg-data写入 MSI-X table表中的DATA部分。
那么msg-data又是从哪里获取呢。这里以irq_gcc_v3_its.c中文件进行说明(针对arm架构的服务器),可见它会把中断向量号传给msg->data,从而写入MSI-Xtable表中DATA部分。
static void its_irq_compose_msi_msg(struct irq_data *d, struct msi_msg *msg)
{
struct its_device *its_dev = irq_data_get_irq_chip_data(d);
struct its_node *its;
u64 addr;
its = its_dev->its;
addr = its->get_msi_base(its_dev);
msg->address_lo = lower_32_bits(addr);
msg->address_hi = upper_32_bits(addr);
msg->data = its_get_event_id(d);
iommu_dma_compose_msi_msg(irq_data_get_msi_desc(d), msg);
}
static inline u32 its_get_event_id(struct irq_data *d)
{
struct its_device *its_dev = irq_data_get_irq_chip_data(d);
return d->hwirq - its_dev->event_map.lpi_base;
}
3.MSI单中断分析
笔者在一个项目中使用了MSI中断,但在申请MSI中断的时候,发现只能申请一个,查看该PCIe设备的能力发现是具有支持32个MSI中断的能力的,那么笔者判断无法申请MSI多中断的原因不在硬件,而可能在操作系统的系统架构上。
本部分源码均来源于linux-5.15
驱动调用接口申请msi中断:ret = pci_alloc_irq_vectors(pdev, msi_num + 1, msi_num + 1, PCI_IRQ_MSI);
查看该函数调用栈:
pci_alloc_irq_vectors
pci_alloc_irq_vectors_affinity
__pci_enable_msi_range
msi_capability_init
pci_msi_setup_msi_irqs
最终定位到pci_msi_setup_msi_irqs函数,该函数的实现依赖于CONFIG_CPI_MSI_IRQ_DOMAIN.如果定义了CONFIG_PCI_MSI_IRQ_DOMAIN,则pci_msi_setup_msi_irqs进入上面的分支实现。查看ubuntu下的linux-header文件,CONFIG_PCI_MSI_IRQ_DOMAIN是定义的。
dev_get_msi_domain函数根据是否定义CONFIG_GENERIC_MSI_IRQ_DOMAIN,返回dev->msi_domain,或者NULL.
pci_msi_setup_msi_irqs最终会调用msi_domain_alloc_irqs和arch_setup_msi_irqs中一个。现将这两个函数分别分析。
#ifdef CONFIG_PCI_MSI_IRQ_DOMAIN
static int pci_msi_setup_msi_irqs(struct pci_dev *dev, int nvec, int type)
{
struct irq_domain *domain;
domain = dev_get_msi_domain(&dev->dev); /*获取domain*/
if (domain && irq_domain_is_hierarchy(domain))
return msi_domain_alloc_irqs(domain, &dev->dev, nvec);
return arch_setup_msi_irqs(dev, nvec, type);
}
static void pci_msi_teardown_msi_irqs(struct pci_dev *dev)
{
struct irq_domain *domain;
domain = dev_get_msi_domain(&dev->dev);
if (domain && irq_domain_is_hierarchy(domain))
msi_domain_free_irqs(domain, &dev->dev);
else
arch_teardown_msi_irqs(dev);
}
#else
#define pci_msi_setup_msi_irqs arch_setup_msi_irqs
#define pci_msi_teardown_msi_irqs arch_teardown_msi_irqs
#endif
而msi_domain_alloc_irqs的函数调用如下
msi_domain_alloc_irqs
ops->domain_alloc_irqs(domain, dev, nvec)
msi_domain_prepare_irqs(domain, dev, nvec, &arg);
__irq_domain_alloc_irqs
最终会调用msi_domain_prepare_irqs函数,重点看ops->msi_check函数实现。
int pci_msi_domain_check_cap(struct irq_domain *domain,
struct msi_domain_info *info, struct device *dev)
{
struct msi_desc *desc = first_pci_msi_entry(to_pci_dev(dev));
/* Special handling to support __pci_enable_msi_range() */
if (pci_msi_desc_is_multi_msi(desc) &&
!(info->flags & MSI_FLAG_MULTI_PCI_MSI))
return 1;
else if (desc->msi_attrib.is_msix && !(info->flags & MSI_FLAG_PCI_MSIX))
return -ENOTSUPP;
return 0;
}
最终指引到了激动人心的地方,MSI_FLAG_MULTI_MSI这个标志,如果info->flags没有多中断标志,则返回msi中断数量为1.
那么MSI_FLAG_MULTI_PCI_MSI标志如何产生的呢。
可见MSI_FLAG_MULTI_PCI_MSI来自pcie 的controller驱动。这表明,对于有些PCIe 的controller驱动并不支持MSI多中断。
最终笔者在考虑到有些主板可能无法申请到MSI多中断,从而最终决定全都使用MSI-X中断。