此文档是基于linux-3.6.10内核代码对msix中断相关进行分析。
PCIe设备可以使用msix报文向处理器提交中断,下面首先看下PCIe设备中的MSXI Capability结构。
此结构在PCIe设备配置空间偏移0x68的位置处。
字段 |
含义 |
Capability ID |
Capability结构的ID号 |
Next Cap Ptr |
下一个Capability结构的位置 |
Message Control |
当前PCIe设置使用msix中断请求的状态和控制信息 |
MSI-X Table Offset |
存放MSI-X Table在bar空间中的偏移 |
MSI-X Table BAR Indicator |
表示设备使用BAR0~5寄存器中的哪个空间存放MSI-X table |
内核中读写PCIe配置空间的函数有:
pci_read_config_dword/pci_read_config_word/pci_read_config_byte
pci_write_config_dword/pci_write_config_word/pci_write_config_byte
◆下面分析pci_enable_msix函数的实现。
int pci_enable_msix(struct pci_dev *dev, struct msix_entry *entries, int nvec) |
pci_enable_msix函数向PCI子系统请求分配nvec个msix中断,第二个参数entries指向msix_entry结构体数组,元素个数不能少于nvec。
struct msix_entry { u32 vector; /* kernel uses to write allocated vector */ u16 entry; /* driver uses to specify entry, OS writes */ }; |
该结构体的vector字段在后面将会看到被填充为中断向量号。
pci_enable_msix函数开始会检查设备是否支持MSIX,以及支持所需的中断数量。通过检查后重点就是调用msix_capability_init函数配置MSXI Capability结构。
◆下面分析msix_capability_init函数的实现。
pos = pci_find_capability(dev, PCI_CAP_ID_MSIX); pci_read_config_word(dev, pos + PCI_MSIX_FLAGS, &control);
/* Ensure MSI-X is disabled while it is set up */ control &= ~PCI_MSIX_FLAGS_ENABLE; pci_write_config_word(dev, pos + PCI_MSIX_FLAGS, control);
/* Request & Map MSI-X table region */ base = msix_map_region(dev, pos, multi_msix_capable(control)); if (!base) return -ENOMEM; ret = msix_setup_entries(dev, pos, base, entries, nvec); if (ret) return ret; |
1、 首先调用pci_find_capability函数在pci配置空间中查找MSXI Capability的偏移位置pos,如图1所示就是0x68。
2、 读取图1中的message control,并将bit15位清零。在此函数的后面会重新设置bit15,即使能中断。
3、 调用msix_map_region函数建立MSIX Table的地址映射。此函数首先从0x6c偏移处读取值table_offset。如图1所示,此值的bit[0:2]说明了MSIX Table所在的BAR,bit[3:31]则是MSIX Table在此BAR空间内的偏移。然后使用ioremap_nocache映射此MSIX Table地址。
4、 调用msix_setup_entries函数申请nvec个msix中断对应的msi_desc结构内存,并初始化,添加到dev的msi_list链表中。
ret = arch_setup_msi_irqs(dev, nvec, PCI_CAP_ID_MSIX); if (ret) goto error; |
arch_setup_msi_irqs函数在x86价格下对应的函数为x86_setup_msi_irqs,此函数最终调用的为native_setup_msi_irqs函数。
◆下面分析native_setup_msi_irqs函数的实现。
list_for_each_entry(msidesc, &dev->msi_list, list) { irq = create_irq_nr(irq_want, node); if (irq == 0) return -1; irq_want = irq + 1; if (!irq_remapping_enabled) goto no_ir;
if (!sub_handle) { /* * allocate the consecutive block of IRTE's * for 'nvec' */ index = msi_alloc_remapped_irq(dev, irq, nvec); if (index < 0) { ret = index; goto error; } } else { ret = msi_setup_remapped_irq(dev, irq, index, sub_handle); if (ret < 0) goto error; } no_ir: ret = setup_msi_irq(dev, msidesc, irq); if (ret < 0) goto error; sub_handle++; } |
此函数的主体是循环遍历dev的msi_list链表,对前面加入的nvec个msi中断的msi_desc结构。对应于此设备的每个msix中断:
1、 首先调用create_irq_nr函数为此msix中断分配一个中断号irq。
2、 如果是此设备的第一个msix中断,调用msi_alloc_remapped_irq分配此设备的nvec个msix中断映射表项。后面的msix中断则调用msi_setup_remapped_irq函数来建立相应的中断映射。
3、 调用setup_msi_irq函数会首先填充MSIX Table中的low_addr、hi_addr和data项;为设备的每一个msix中断指定一个vectore向量号;并将MSIX Table中的内容写到前面映射的地址。
msi_alloc_remapped_irq、msi_setup_remapped_irq和setup_msi_irq三个函数内容比较多,下面逐个分析。
■msi_alloc_remapped_irq函数分析
msi_alloc_remapped_irq函数在x86价格下对应是intel_msi_alloc_irq函数。此函数中和底层硬件相关的部分没看怎么明白,也不是重点。重点在alloc_irte函数中。
alloc_irte函数代码片段:
do { for (i = index; i < index + count; i++) if (table->base[i].present) break; /* empty index found */ if (i == index + count) break; index = (index + count) % INTR_REMAP_TABLE_ENTRIES; if (index == start_index) { raw_spin_unlock_irqrestore(&irq_2_ir_lock, flags); printk(KERN_ERR "can't allocate an IRTE\n"); return -1; } } while (1); for (i = index; i < index + count; i++) table->base[i].present = 1; irq_iommu->iommu = iommu; irq_iommu->irte_index = index; irq_iommu->sub_handle = 0; irq_iommu->irte_mask = mask; |
此函数中会遍历irte表,找到nvec个连续未使用的位置,并返回位置Index。
初始化irq_iommu的irte_index和sub_handle字段。这两个字段在后面计算vector的时候会用到。
■msi_setup_remapped_irq函数分析
msi_setup_remapped_irq函数在x86架构下对应的是intel_msi_setup_irq函数。此函数重点在set_irte_irq函数中。
set_irte_irq函数代码片段:
struct irq_2_iommu *irq_iommu = irq_2_iommu(irq);
irq_iommu->iommu = iommu; irq_iommu->irte_index = index; irq_iommu->sub_handle = subhandle; irq_iommu->irte_mask = 0; |
此函数与alloc_irte的基本功能相同,区别在于:alloc_irte函数计算了index值,此值在set_irte_irq中继续赋值给irte_index字段,但是sub_handle会累加。比如一个设备申请了四个msix中断,这四个msix中断对应的irq_iommu结构的irte_index都相同,sub_handle则是按照msix中断对应的irq的大小从0到3赋值。
■setup_msi_irq函数分析
setup_msi_irq函数的代码片段:
ret = msi_compose_msg(dev, irq, &msg, -1);
irq_set_msi_desc(irq, msidesc); write_msi_msg(irq, &msg);
if (irq_remapped(irq_get_chip_data(irq))) { irq_set_status_flags(irq, IRQ_MOVE_PCNTXT); irq_remap_modify_chip_defaults(chip); }
irq_set_chip_and_handler_name(irq, chip, handle_edge_irq, "edge"); |
1、 首先看看msi_compose_msg函数。
① 调用assign_irq_vector-à__assign_irq_vector函数,此函数中首先计算vector
for_each_cpu_and(new_cpu, tmp_mask, cpu_online_mask) per_cpu(vector_irq, new_cpu)[vector] = irq; cfg->vector = vector; |
将msix中断对应的irq存入per cpu的数组vector_irq的位置vector的地方。并将vector值赋给cfg->vector,在prepare_irte函数中会将cfg->vector赋值给irte->vector。
② 填充msg结构的address_lo、address_hi和data字段。X86架构下对应的intel_compose_msi_msg函数完成此功能。
intel_compose_msi_msg函数首先调用map_irq_to_irte_handle来获取irq对应的irq_iommu结构的sub_handle和irte_index字段。
*sub_handle = irq_iommu->sub_handle; index = irq_iommu->irte_index; |
接着调用prepare_irte函数初始化IRTE结构的低64bit,set_msi_sid函数初始化IRTE结构的高64bit
irte->present = 1; irte->dst_mode = apic->irq_dest_mode; irte->trigger_mode = 0; irte->dlvry_mode = apic->irq_delivery_mode; irte->vector = vector; irte->dest_id = IRTE_DEST(dest); irte->redir_hint = 1; |
下图是IRTE结构图
dest_id字段决定了哪个cpu响应处理此中断,vector字段决定了此中断在IDT中的位置,这里的vector是由cfg->vector传过来的,在前面__assign_irq_vector函数中我们有看到vector相关的设置。
初始化完IRTE结构之后,就调用modify_irte函数将其赋值给对应的irq中断。
index = irq_iommu->irte_index + irq_iommu->sub_handle; irte = &iommu->ir_table->base[index];
msg->address_hi = MSI_ADDR_BASE_HI; msg->data = sub_handle; msg->address_lo = MSI_ADDR_BASE_LO | MSI_ADDR_IR_EXT_INT | MSI_ADDR_IR_SHV | MSI_ADDR_IR_INDEX1(ir_index) | MSI_ADDR_IR_INDEX2(ir_index); |
irq_iommu->irte_index字段对应msg->address_lo的bit[19:5],irq_iommu->sub_handle字段对应msg->data值。通过这两个值计算此中断在ir_table中的索引,进而得到IRTE结构,然后irte结构的vector字段就可以得到irq中断号了。
Msg就是MSIX Table中的一个entry,每个entry由4个4字节的成员组成。Data字段赋值为sub_handle,每个设备的多个中断对应的sub_handle值从0开始,连续递增1;address_hi字段值赋值为0;address_lo字段的值组成如下图所示:
bit[31:20]赋值为MSI_ADDR_BASE_LO宏,即是0xfee00000一个固定的值。PCI设备通过向特定地址空间(FSB Interrupt存储器空间)发起一个写操作来发起中断,x86架构下此地址空间是0xfee00000开始的地址空间,其实就是local APIC寄存器映射的地址空间。
bit[19:12]是处理中断的目标cpu的ID号。FSB Interrupt Message总线任务向不同的cpu提交中断请求,目标cpu将接收这个中断请求。
2、 调用write_msi_msg函数-à__write_msi_msg函数将msg信息写到此PCI设备的相应BAR空间位置。
base = entry->mask_base + entry->msi_attrib.entry_nr * PCI_MSIX_ENTRY_SIZE; 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); |
在前面分析msix_map_region函数时候知道entry->mask_base就是此PCI设备的MSIX Table在BAR空间内的偏移位置,PCI_MSIX_ENTRY_SIZE宏为16,对应4个4字节的成员。entry_nr对应此设备的nvec个MSIX中断的从0开始的序列号。
3、调用irq_set_chip_and_handler_name函数设置中断流控回调函数handle_edge_irq。Msix中断都是使用边沿触发的。
struct irq_desc *desc = irq_get_desc_buslock(irq, &flags, 0);
desc->handle_irq = handle; desc->name = name; |
到这里初始化流程基本上都完了。