此文檔是基於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; |
到這裏初始化流程基本上都完了。