MSI和MSIX中断详解以及驱动分析

当前数据中心服务器,内存,显卡,网卡等设备均通过PCIe总线与CPU相连,而PCIe设备使用的最多的就是MSI和MSI-X中断。MSI中断是通过在内存写入信息来触发的一种消息中断类型,其中内存地址由硬件设备和系统协商决定。而MSI-X中断是在MSI中断的基础上扩展的一种消息类型,主要目的是解决MSI中断存在的一些限制。两者区别如下

类型MSIMSI-X
中断数322048
中断号必须连续可以任意分配
中断信息存放于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中断。 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值