PCIe學習筆記之MSI/MSI-x中斷及代碼分析

本文基於linux 5.7.0, 平臺是arm64

1. MSI/MSI-X概述

PCIe有三種中斷,分別爲INTx中斷,MSI中斷,MSI-X中斷,其中INTx是可選的,MSI/MSI-X是必須實現的。

1.1 什麼是MSI中斷?

MSI, message signal interrupt, 是PCI設備通過寫一個特定消息到特定地址,從而觸發一個CPU中斷。特定消息指的是PCIe總線中的Memory Write TLP, 特定地址一般存放在MSI capability中。

和傳統的INTx中斷相比,MSI中斷有以下幾個優點:
(1) 基於引腳的傳統中斷會被多個設備所共享,中斷共享時,如果觸發了中斷,linux需要一一調用對應的中斷處理函數,這樣會有性能上的損失,而MSI不存在共享的問題。
(2) 設備向內存寫入數據,然後發起引腳中斷, 有可能會出現CPU收到中斷時,數據還沒有達到內存。 而使用MSI中斷時,產生中斷的寫不能越過數據的寫,驅動可以確信所有的數據已經達到內存。
(3) 多功能的PCI設備,每一個功能最多隻有一箇中斷引腳,當具體的事件產生時,驅動需要查詢設備才能知道是哪一個事件產生,這樣會降低中斷的處理速度。而一個設備可以支持32個MSI中斷,每個中斷可以對應特定的功能。

1.2 什麼是MSI-X中斷?

MSI-x是MSI的擴展和增強。MSI有它自身的侷限性,MSI最多支持32箇中斷,且要求中斷向量連續, 而MSI-x沒有這個限制,且支持的中斷數量更多。此外,MSI-X的中斷向量信息並不直接存儲在capability中,而是在一塊特殊Memory中.

MSI和MSI-X的規格對比:

MSI MSI-X
中斷向量數 32 2048
中斷號約束 必須連續 可以隨意分配
MSI信息存放 capability寄存器 MSI-X Table(BAR空間)

總之,PCIe設備在提交MSI中斷請求時,都是向MSI/MSI-X Capability結構中的Message Address的地址寫Message Data數據,從而組成一個存儲器寫TLP,向處理器提交中斷請求。

在arm64中,MSI/MSI-X對應的是LPI中斷, 在之前的文章【ARM GICv3 ITS介紹及代碼分析】有介紹過,外設通過寫GITS_TRANSLATER寄存器,可以發起LPI中斷, 所以相應的,如果在沒有使能SMMU時,MSI的message address指的就是ITS_TRANSLATER的地址。

2. MSI/MSI-X capability

2.1 MSI capability

MSI Capability的ID爲5, 共有四種組成方式,分別是32和64位的Message結構,32位和64位帶中斷Masking的結構。
以帶bit mask的capability register爲例:
在這裏插入圖片描述

Capability ID :記錄msi capability的ID號,固定爲0x5.
next pointer: 指向下一個新的Capability寄存器的地址.
Message Control Register: 存放當前PCIe設備使用MSI機制進行中斷請求的狀態和控制信息
在這裏插入圖片描述
MSI enable控制MSI是否使能,Multiple Message Capable表示設備能夠支持的中斷向量數量, Multi Message enable表示實際使用的中斷向量數量, 64bit Address Capable表示使用32bit格式還是64bit格式。

Message Address Register: 當MSI enable時,保存中斷控制器種接收MSI消息的地址。
Message Data Register: 當MSI enable時,保存MSI報文的數據。
Mask Bits: 可選,Mask Bits字段由32位組成,其中每一位對應一種MSI中斷請求。
Pending Bits: 可選,需要與Mask bits配合使用, 可以防止中斷丟失。當Mask bits爲1的時候,設備發送的MSI中斷請求並不會發出,會將pending bits置爲1,當mask bits變爲0時,MSI會成功發出,pending位會被清除。

2.2 MSI-X capability

MSI-x的capability寄存器結構和MSI有一些差異:
在這裏插入圖片描述
Capability ID:記載MSI-X Capability結構的ID號,其值爲0x11
Message Control: 存放當前PCIe設備使用MSI-x機制進行中斷請求的狀態和控制信息
在這裏插入圖片描述
MSI-x enable,控制MSI-x的中斷使能 ;
Function Mask,是中斷請求的全局Mask位,如果該位爲1,該設備所有的中斷請求都將被屏蔽;如果該位爲0,則由Per Vector Mask位,決定是否屏蔽相應的中斷請求。Per Vector Mask位在MSI-X Table中定義;
Table Size, 存放MSI-X table的大小
Table BIR:BAR Indicator Register。該字段存放MSI-X Table所在的位置,PCIe總線規範規定MSI-X Table存放在設備的BAR空間中。該字段表示設備使用BAR0 ~ 5寄存器中的哪個空間存放MSI-X table。
Table Offset: 存放MSI-X Table在相應BAR空間中的偏移。
PBA(Pending Bit Array) BIR: 存放Pending Table在PCIe設備的哪個BAR空間中。在通常情況下,Pending Table和MSI-X Table存放在PCIe設備的同一個BAR空間中。
PBA Offset: 該字段存放Pending Table在相應BAR空間中的偏移。

通過Table BIR和Table offset知道了MSI-Xtable在哪一個bar中以及在bar中的偏移,就可以找到對應的MSI-X table。
查找過程如下:
在這裏插入圖片描述
查找到的MSI-X table結構:
在這裏插入圖片描述
MSI-X Table由多個Entry組成,其中每個Entry與一箇中斷請求對應。
除了msg data和msg addr外,還有一個vector control的參數,表示PCIe設備是否能夠使用該Entry提交中斷請求, 類似MSI的mask位。

3. 確認設備的MSI/MSI-X capability

lspci -v可以查看設備支持的capability, 如果有MSI或者MSI-x或者message signal interrupt的描述,並且這些描述後面都有一個enable的flag, “+”表示enable,"-"表示disable。

[root@localhost linux]# lspci -s 00:16.0 -v
00:16.0 PCI bridge: VMware PCI Express Root Port (rev 01) (prog-if 00 [Normal decode])
	Flags: bus master, fast devsel, latency 0, IRQ 32
	Bus: primary=00, secondary=0b, subordinate=0b, sec-latency=0
	I/O behind bridge: 00005000-00005fff
	Memory behind bridge: fd300000-fd3fffff
	Prefetchable memory behind bridge: 00000000e7900000-00000000e79fffff
	Capabilities: [40] Subsystem: VMware PCI Express Root Port
	Capabilities: [48] Power Management version 3
	Capabilities: [50] Express Root Port (Slot+), MSI 00
	Capabilities: [8c] MSI: Enable+ Count=1/1 Maskable+ 64bit+
	Kernel driver in use: pcieport
	Kernel modules: shpchp

4. 設備怎麼使用MSI/MSI-x中斷?

傳統中斷在系統初始化掃描PCI bus tree時就已自動爲設備分配好中斷號, 但是如果設備需要使用MSI,驅動需要進行一些額外的配置。
當前linux內核提供pci_alloc_irq_vectors來進行MSI/MSI-X capablity的初始化配置以及中斷號分配。

int pci_alloc_irq_vectors(struct pci_dev *dev, unsigned int min_vecs,
                unsigned int max_vecs, unsigned int flags);

函數的返回值爲該PCI設備分配的中斷向量個數。
min_vecs是設備對中斷向量數目的最小要求,如果小於該值,會返回錯誤。
max_vecs是期望分配的中斷向量最大個數。
flags用於區分設備和驅動能夠使用的中斷類型,一般有4種:

#define PCI_IRQ_LEGACY		(1 << 0) /* Allow legacy interrupts */
#define PCI_IRQ_MSI		(1 << 1) /* Allow MSI interrupts */
#define PCI_IRQ_MSIX		(1 << 2) /* Allow MSI-X interrupts */
#define PCI_IRQ_ALL_TYPES   (PCI_IRQ_LEGACY | PCI_IRQ_MSI | PCI_IRQ_MSIX)

PCI_IRQ_ALL_TYPES可以用來請求任何可能類型的中斷。
此外還可以額外的設置PCI_IRQ_AFFINITY, 用於將中斷分佈在可用的cpu上。
使用示例:

 i = pci_alloc_irq_vectors(dev->pdev, min_msix, msi_count, PCI_IRQ_MSIX | PCI_IRQ_AFFINITY);

與之對應的是釋放中斷資源的函數pci_free_irq_vectors(), 需要在設備remove時調用:

void pci_free_irq_vectors(struct pci_dev *dev);

此外,linux還提供了pci_irq_vector()用於獲取IRQ number.

int pci_irq_vector(struct pci_dev *dev, unsigned int nr);

5. 設備的MSI/MSI-x中斷是怎樣處理的?

5.1 MSI的中斷分配pci_alloc_irq_vectors()

深入理解下pci_alloc_irq_vectors()
pci_alloc_irq_vectors() --> pci_alloc_irq_vectors_affinity()

int pci_alloc_irq_vectors_affinity(struct pci_dev *dev, unsigned int min_vecs,
				   unsigned int max_vecs, unsigned int flags,
				   struct irq_affinity *affd)
{
	struct irq_affinity msi_default_affd = {0};
	int msix_vecs = -ENOSPC;
	int msi_vecs = -ENOSPC;

	if (flags & PCI_IRQ_AFFINITY) {                        
		if (!affd)
			affd = &msi_default_affd;
	} else {
		if (WARN_ON(affd))
			affd = NULL;
	}

	if (flags & PCI_IRQ_MSIX) {
		msix_vecs = __pci_enable_msix_range(dev, NULL, min_vecs,
						    max_vecs, affd, flags);               ------(1)
		if (msix_vecs > 0)
			return msix_vecs;
	}

	if (flags & PCI_IRQ_MSI) {
		msi_vecs = __pci_enable_msi_range(dev, min_vecs, max_vecs,
						  affd);                             ----- (2)
		if (msi_vecs > 0)
			return msi_vecs;
	}

	/* use legacy IRQ if allowed */
	if (flags & PCI_IRQ_LEGACY) {
		if (min_vecs == 1 && dev->irq) {
			/*
			 * Invoke the affinity spreading logic to ensure that
			 * the device driver can adjust queue configuration
			 * for the single interrupt case.
			 */
			if (affd)
				irq_create_affinity_masks(1, affd);
			pci_intx(dev, 1);                                 ------ (3)
			return 1;
		}
	}

	if (msix_vecs == -ENOSPC)                
		return -ENOSPC;
	return msi_vecs;
}

(1) 先確認申請的是否爲MSI-X中斷

__pci_enable_msix_range()
	+-> __pci_enable_msix()
		+-> msix_capability_init()
			+-> pci_msi_setup_msi_irqs()

msix_capability_init會對msi capability進行一些配置。
關鍵函數pci_msi_setup_msi_irqs, 會創建msi irq number:

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);      
	if (domain && irq_domain_is_hierarchy(domain))
		return msi_domain_alloc_irqs(domain, &dev->dev, nvec);

	return arch_setup_msi_irqs(dev, nvec, type);
}

這裏的irq_domain獲取的是pcie device結構體中定義的dev->msi_domain.
這裏的msi_domain是在哪裏定義的呢?
在drivers/irqchip/irq-gic-v3-its-pci-msi.c中, kernel啓動時會:

its_pci_msi_init()
	+-> its_pci_msi_init()
		+-> its_pci_msi_init_one()
			+-> pci_msi_create_irq_domain(handle, &its_pci_msi_domain_info,parent)

pci_msi_create_irq_domain中會去創建pci_msi irq_domain, 傳遞的參數分別是its_pci_msi_domain_info以及設置parent爲its irq_domain.
所以現在邏輯就比較清晰:
在這裏插入圖片描述
gic中斷控制器初始化時會去add gic irq_domain, gic irq_domain是its irq_domain的parent節點,its irq_domain中的host data對應的pci_msi irq_domain.

        gic irq_domain --> irq_domain_ops(gic_irq_domain_ops)
              ^                --> .alloc(gic_irq_domain_alloc)
              |
        its irq_domain --> irq_domain_ops(its_domain_ops)
              ^                --> .alloc(its_irq_domain_alloc)
              |                --> ...
              |        --> host_data(struct msi_domain_info)
              |            --> msi_domain_ops(its_msi_domain_ops)
              |                --> .msi_prepare(its_msi_prepare)
              |            --> irq_chip, chip_data, handler...
              |            --> void *data(struct its_node)

pci_msi irq_domain對應的ops:

static const struct irq_domain_ops msi_domain_ops = {
        .alloc          = msi_domain_alloc,
        .free           = msi_domain_free,
        .activate       = msi_domain_activate,
        .deactivate     = msi_domain_deactivate,
};

回到上面的pci_msi_setup_msi_irqs()函數,獲取了pci_msi irq_domain後, 調用msi_domain_alloc_irqs()函數分配IRQ number.

msi_domain_alloc_irqs()
	// 對應的是its_pci_msi_ops中的its_pci_msi_prepare
	+-> msi_domain_prepare_irqs()
	// 分配IRQ number
	+-> __irq_domain_alloc_irqs()

msi_domain_prepare_irqs()對應的是its_msi_prepare函數,會去創建一個its_device.
__irq_domain_alloc_irqs()會去分配虛擬中斷號,從allocated_irq位圖中取第一個空閒的bit位作爲虛擬中斷號。

至此, msi-x的中斷分配已經完成,且msi-x的配置也已經完成。

(2) 如果不是MSI-X中斷, 再確認申請的是否爲MSI中斷, 流程與MSI-x類似。
(3) 如果不是MSI/MSI-X中斷, 再確認申請的是否爲傳統intx中斷

5.2 MSI的中斷註冊

kernel/irq/manage.c

request_irq()
    +-> __setup_irq()
    	+-> irq_activate()
   			+-> msi_domain_activate()
   				// msi_domain_info中定義的irq_chip_write_msi_msg
        		+-> irq_chip_write_msi_msg()
        			// irq_chip對應的是pci_msi_create_irq_domain中關聯的its_msi_irq_chip
            		+-> data->chip->irq_write_msi_msg(data, msg);
            				+-> pci_msi_domain_write_msg()

從這個流程可以看出,MSI是通過irq_write_msi_msg往一個地址發一個消息來激活一箇中斷。

參考資料

PCIe掃盲——中斷機制介紹(MSI-X
PCIe體系結構導讀
MSI/MSI-X Capability結構
GIC ITS 學習筆記(一)
Documentation/PCI/MSI-HOWTO.txt

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章