英語

4. MSI 驅動程式指南 HOWTO

作者:

Tom L Nguyen; Martine Silbermann; Matthew Wilcox

版權:

2003, 2008 Intel Corporation

4.1. 關於本指南

本指南介紹了訊息訊號中斷 (MSI) 的基礎知識、使用 MSI 代替傳統中斷機制的優勢、如何更改驅動程式以使用 MSI 或 MSI-X 以及在裝置不支援 MSI 時嘗試的一些基本診斷方法。

4.2. 什麼是 MSI?

訊息訊號中斷是從裝置寫入到特殊地址,導致 CPU 接收到中斷。

MSI 功能最初在 PCI 2.2 中指定,後來在 PCI 3.0 中得到增強,允許單獨遮蔽每個中斷。 MSI-X 功能也隨 PCI 3.0 一起引入。 它比 MSI 支援每個裝置更多的中斷,並允許獨立配置中斷。

裝置可能同時支援 MSI 和 MSI-X,但一次只能啟用一個。

4.3. 為什麼要使用 MSI?

與傳統的基於引腳的中斷相比,使用 MSI 有三個原因可以提供優勢。

基於引腳的 PCI 中斷通常在多個裝置之間共享。 為了支援這一點,核心必須呼叫與中斷關聯的每個中斷處理程式,這會導致整個系統的效能下降。 MSI 永遠不會共享,因此不會出現此問題。

當裝置將資料寫入記憶體,然後引發基於引腳的中斷時,中斷可能在所有資料到達記憶體之前到達(對於 PCI-PCI 橋後面的裝置,這種情況更有可能發生)。 為了確保所有資料都已到達記憶體,中斷處理程式必須讀取引發中斷的裝置上的暫存器。 PCI 事務排序規則要求所有資料在可以從暫存器返回該值之前到達記憶體。 使用 MSI 可以避免此問題,因為生成中斷的寫入無法透過資料寫入,因此在引發中斷時,驅動程式知道所有資料都已到達記憶體。

PCI 裝置每個功能只能支援單個基於引腳的中斷。 通常,驅動程式必須查詢裝置以找出發生了什麼事件,從而降低了常見情況下中斷處理的速度。 使用 MSI,裝置可以支援更多中斷,從而允許每個中斷專門用於不同的目的。 一種可能的設計為不頻繁的條件(例如錯誤)提供它們自己的中斷,這允許驅動程式更有效地處理正常的中斷處理路徑。 其他可能的設計包括為網絡卡中的每個資料包佇列或儲存控制器中的每個埠提供一箇中斷。

4.4. 如何使用 MSI

PCI 裝置被初始化為使用基於引腳的中斷。 裝置驅動程式必須設定裝置以使用 MSI 或 MSI-X。 並非所有機器都正確支援 MSI,對於這些機器,下面描述的 API 將簡單地失敗,並且裝置將繼續使用基於引腳的中斷。

4.4.1. 包含對 MSI 的核心支援

為了支援 MSI 或 MSI-X,核心必須在啟用 CONFIG_PCI_MSI 選項的情況下構建。 此選項僅在某些架構上可用,並且可能取決於也設定了一些其他選項。 例如,在 x86 上,您還必須啟用 X86_UP_APIC 或 SMP 才能看到 CONFIG_PCI_MSI 選項。

4.4.2. 使用 MSI

大部分繁重的工作都是在 PCI 層中為驅動程式完成的。 驅動程式只需請求 PCI 層為此裝置設定 MSI 功能。

要自動使用 MSI 或 MSI-X 中斷向量,請使用以下函式

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

它為 PCI 裝置分配最多 max_vecs 箇中斷向量。 它返回已分配的向量數或負錯誤。 如果裝置對最小向量數有要求,驅動程式可以傳遞設定為此限制的 min_vecs 引數,如果 PCI 核心無法滿足最小向量數,則返回 -ENOSPC。

flags 引數用於指定裝置和驅動程式可以使用的中斷型別(PCI_IRQ_INTX、PCI_IRQ_MSI、PCI_IRQ_MSIX)。 一個方便的簡寫 (PCI_IRQ_ALL_TYPES) 也可用於請求任何可能的型別的中斷。 如果設定了 PCI_IRQ_AFFINITY 標誌,pci_alloc_irq_vectors() 會將中斷分散到可用的 CPU 周圍。

要獲取傳遞給 request_irq()free_irq() 的 Linux IRQ 編號和向量,請使用以下函式

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

在使用以下函式刪除裝置之前,應釋放任何已分配的資源

void pci_free_irq_vectors(struct pci_dev *dev);

如果裝置同時支援 MSI-X 和 MSI 功能,則此 API 將優先使用 MSI-X 功能,而不是 MSI 功能。 MSI-X 支援 1 到 2048 之間的任何數量的中斷。 相比之下,MSI 被限制為最多 32 箇中斷(並且必須是 2 的冪)。 此外,MSI 中斷向量必須連續分配,因此係統可能無法為 MSI 分配與為 MSI-X 分配的向量一樣多的向量。 在某些平臺上,MSI 中斷必須全部以同一組 CPU 為目標,而 MSI-X 中斷可以全部以不同的 CPU 為目標。

如果裝置既不支援 MSI-X 也不支援 MSI,它將回退到單箇舊 IRQ 向量。

MSI 或 MSI-X 中斷的典型用法是分配儘可能多的向量,可能達到裝置支援的限制。 如果 nvec 大於裝置支援的數量,它將自動限制為支援的限制,因此無需事先查詢支援的向量數量

nvec = pci_alloc_irq_vectors(pdev, 1, nvec, PCI_IRQ_ALL_TYPES)
if (nvec < 0)
        goto out_err;

如果驅動程式無法或不願處理可變數量的 MSI 中斷,則可以透過將該數字作為 ‘min_vecs’ 和 ‘max_vecs’ 引數傳遞給 pci_alloc_irq_vectors() 函式來請求特定數量的中斷

ret = pci_alloc_irq_vectors(pdev, nvec, nvec, PCI_IRQ_ALL_TYPES);
if (ret < 0)
        goto out_err;

上述請求型別最臭名昭著的例子是為裝置啟用單個 MSI 模式。 這可以透過將兩個 1 作為 ‘min_vecs’ 和 ‘max_vecs’ 傳遞來完成

ret = pci_alloc_irq_vectors(pdev, 1, 1, PCI_IRQ_ALL_TYPES);
if (ret < 0)
        goto out_err;

某些裝置可能不支援使用舊的行中斷,在這種情況下,驅動程式可以指定僅接受 MSI 或 MSI-X

nvec = pci_alloc_irq_vectors(pdev, 1, nvec, PCI_IRQ_MSI | PCI_IRQ_MSIX);
if (nvec < 0)
        goto out_err;

4.4.3. 舊版 API

以下用於啟用和停用 MSI 或 MSI-X 中斷的舊 API 不應在新程式碼中使用

pci_enable_msi()              /* deprecated */
pci_disable_msi()             /* deprecated */
pci_enable_msix_range()       /* deprecated */
pci_enable_msix_exact()       /* deprecated */
pci_disable_msix()            /* deprecated */

此外,還有一些 API 用於提供支援的 MSI 或 MSI-X 向量的數量:pci_msi_vec_count()pci_msix_vec_count()。 通常,應避免使用這些 API,而應讓 pci_alloc_irq_vectors() 限制向量的數量。 如果您對向量計數有合法的特殊用例,我們可能需要重新考慮該決定並新增一個 pci_nr_irq_vectors() 輔助函式,該函式透明地處理 MSI 和 MSI-X。

4.4.4. 使用 MSI 時的注意事項

4.4.4.1. 自旋鎖

大多數裝置驅動程式都有一個每裝置自旋鎖,該鎖在中斷處理程式中獲取。 使用基於引腳的中斷或單個 MSI,無需停用中斷(Linux 保證不會重新進入相同的中斷)。 如果裝置使用多箇中斷,則驅動程式必須在持有鎖時停用中斷。 如果裝置傳送不同的中斷,驅動程式將死鎖並嘗試遞迴獲取自旋鎖。 可以透過使用 spin_lock_irqsave() 或 spin_lock_irq() 來避免這種死鎖,它們停用本地中斷並獲取鎖(請參閱 不可靠的鎖定指南)。

4.4.5. 如何判斷裝置上是否啟用了 MSI/MSI-X

使用 ‘lspci -v’(作為 root)可能會顯示一些具有“MSI”、“訊息訊號中斷”或“MSI-X”功能的裝置。 這些功能中的每一個都具有一個“啟用”標誌,後跟“+”(已啟用)或“–”(已停用)。

4.5. MSI 怪癖

已知一些 PCI 晶片組或裝置不支援 MSI。 PCI 堆疊提供了三種停用 MSI 的方法

  1. 全域性

  2. 在特定網橋後面的所有裝置上

  3. 在單個裝置上

4.5.1. 全域性停用 MSI

某些主機晶片組根本不支援 MSI。 如果我們幸運的話,製造商知道這一點並在 ACPI FADT 表中指出了它。 在這種情況下,Linux 會自動停用 MSI。 某些主機板未在表中包含此資訊,因此我們必須自己檢測它們。 這些主機板的完整列表可以在 drivers/pci/quirks.c 中的 quirk_disable_all_msi() 函式附近找到。

如果您的主機板在使用 MSI 時出現問題,您可以在核心命令列中傳遞 pci=nomsi 以停用所有裝置上的 MSI。 您最好將問題報告給 linux-pci@vger.kernel.org,包括完整的“lspci -v”,以便我們可以將怪癖新增到核心。

4.5.2. 停用網橋下方的 MSI

某些 PCI 網橋無法正確地在匯流排之間路由 MSI。 在這種情況下,必須停用網橋後面所有裝置上的 MSI。

某些網橋允許您透過更改其 PCI 配置空間中的某些位來啟用 MSI(尤其是 Hypertransport 晶片組,例如 nVidia nForce 和 Serverworks HT2000)。 與主機晶片組一樣,Linux 大多對此有所瞭解,並且如果可以,會自動啟用 MSI。 如果您有 Linux 未知的網橋,您可以使用您知道有效的任何方法在配置空間中啟用 MSI,然後透過執行以下操作在該網橋上啟用 MSI

echo 1 > /sys/bus/pci/devices/$bridge/msi_bus

其中 $bridge 是您已啟用的網橋的 PCI 地址(例如 0000:00:0e.0)。

要停用 MSI,請回顯 0 而不是 1。 更改此值應謹慎,因為它可能會中斷此網橋下方所有裝置的中斷處理。

同樣,請將需要特殊處理的任何網橋通知給 linux-pci@vger.kernel.org

4.5.3. 停用單個裝置上的 MSI

已知某些裝置具有錯誤的 MSI 實現。 通常,這是在單個裝置驅動程式中處理的,但有時需要使用怪癖來處理它。 某些驅動程式有一個選項可以停用 MSI 的使用。 雖然這對於驅動程式作者來說是一種方便的解決方法,但這不是一個好的做法,不應效仿。

4.5.4. 查詢為什麼在裝置上停用了 MSI

從上面的三個部分可以看出,有很多原因可能無法為給定的裝置啟用 MSI。 您的第一步應該是仔細檢查您的 dmesg 以確定是否為您的機器啟用了 MSI。 您還應該檢查您的 .config 以確保您已啟用 CONFIG_PCI_MSI。

然後,“lspci -t”給出了裝置上方的網橋列表。 讀取 /sys/bus/pci/devices/*/msi_bus 將告訴您是否啟用 (1) 或停用 (0) MSI。 如果在 PCI 根和裝置之間的網橋的任何 msi_bus 檔案中找到 0,則 MSI 將被停用。

還值得檢查裝置驅動程式以檢視它是否支援 MSI。 例如,它可能包含對 pci_alloc_irq_vectors() 的呼叫,其中帶有 PCI_IRQ_MSI 或 PCI_IRQ_MSIX 標誌。

4.6. 裝置驅動程式 MSI(-X) API 列表

PCI/MSI 子系統有一個專用的 C 檔案,用於其匯出的裝置驅動程式 API — drivers/pci/msi/api.c。 匯出了以下函式

int pci_enable_msi(struct pci_dev *dev)

在裝置上啟用 MSI 中斷模式

引數

struct pci_dev *dev

要操作的 PCI 裝置

描述

舊裝置驅動程式 API,用於在裝置上啟用 MSI 中斷模式並分配單箇中斷向量。 成功後,分配的向量 Linux IRQ 將儲存在 dev->irq 中。 驅動程式必須在清理時呼叫 pci_disable_msi()

注意

通常應使用較新的 pci_alloc_irq_vectors() / pci_free_irq_vectors() API 對。

返回

成功時返回 0,否則返回 errno

void pci_disable_msi(struct pci_dev *dev)

在裝置上停用 MSI 中斷模式

引數

struct pci_dev *dev

要操作的 PCI 裝置

描述

舊裝置驅動程式 API,用於在裝置上停用 MSI 中斷模式,釋放之前分配的中斷向量,並恢復 INTx 模擬。 PCI 裝置 Linux IRQ (dev->irq) 恢復為其預設的引腳斷言 IRQ。 這是 pci_enable_msi() 的清理對。

注意

通常應使用較新的 pci_alloc_irq_vectors() / pci_free_irq_vectors() API 對。

int pci_msix_vec_count(struct pci_dev *dev)

獲取裝置上的 MSI-X 中斷向量數

引數

struct pci_dev *dev

要操作的 PCI 裝置

返回

此裝置上可用的 MSI-X 中斷向量數(即,裝置的 MSI-X 功能結構“表大小”),如果裝置不支援 MSI-X,則為 -EINVAL,否則為其他錯誤。

int pci_enable_msix_range(struct pci_dev *dev, struct msix_entry *entries, int minvec, int maxvec)

在裝置上啟用 MSI-X 中斷模式

引數

struct pci_dev *dev

要操作的 PCI 裝置

struct msix_entry *entries

輸入/輸出引數,MSI-X 配置條目陣列

int minvec

MSI-X 向量的最小必需數量

int maxvec

MSI-X 向量的最大期望數量

描述

舊裝置驅動程式 API,用於在裝置上啟用 MSI-X 中斷模式並根據需要配置其 MSI-X 功能結構。 傳遞的 entries 陣列必須將其每個成員的“entry”欄位設定為所需的(有效)MSI-X 向量編號,其中有效 MSI-X 向量編號的範圍可以透過 pci_msix_vec_count() 查詢。 如果成功,驅動程式必須在清理時呼叫 pci_disable_msix()

注意

通常應使用較新的 pci_alloc_irq_vectors() / pci_free_irq_vectors() API 對。

返回

已分配的 MSI-X 向量數(可能小於 maxvecs),其中此類已分配向量的 Linux IRQ 編號儲存在 entries 陣列元素的“vector”欄位中。 如果少於 minvecs 箇中斷向量可用,則返回 -ENOSPC。 如果傳遞的 entries 成員“entry”欄位無效或重複,或者如果裝置上之前啟用了純 MSI 中斷模式,則返回 -EINVAL。 否則返回其他錯誤。

bool pci_msix_can_alloc_dyn(struct pci_dev *dev)

查詢是否支援啟用 MSI-X 後進行動態分配

引數

struct pci_dev *dev

要操作的 PCI 裝置

返回

如果支援則為 True,否則為 False

struct msi_map pci_msix_alloc_irq_at(struct pci_dev *dev, unsigned int index, const struct irq_affinity_desc *affdesc)

在啟用 MSI-X 後,在給定的 MSI-X 向量索引或任何空閒向量索引處分配 MSI-X 中斷

引數

struct pci_dev *dev

要操作的 PCI 裝置

unsigned int index

要分配的索引。 如果 index == MSI_ANY_INDEX,則此分配 MSI-X 表中的下一個空閒索引

const struct irq_affinity_desc *affdesc

指向關聯性描述符結構的可選指標。 否則為 NULL

返回

一個 struct msi_map

成功後,msi_map::index 包含已分配的索引 (>= 0),msi_map::virq 包含已分配的 Linux 中斷號 (> 0)。

失敗時,msi_map::index 包含錯誤程式碼,msi_map::virq 設定為 0。

void pci_msix_free_irq(struct pci_dev *dev, struct msi_map map)

釋放 PCI/MSI-X 中斷域上的中斷

引數

struct pci_dev *dev

要操作的 PCI 裝置

struct msi_map map

描述要釋放的中斷的 struct msi_map

描述

撤消中斷向量分配。 不停用 MSI-X。

void pci_disable_msix(struct pci_dev *dev)

在裝置上停用 MSI-X 中斷模式

引數

struct pci_dev *dev

要操作的 PCI 裝置

描述

舊裝置驅動程式 API,用於在裝置上停用 MSI-X 中斷模式,釋放之前分配的中斷向量,並恢復 INTx。 PCI 裝置 Linux IRQ (dev->irq) 恢復為其預設的引腳斷言 IRQ。 這是 pci_enable_msix_range() 的清理對。

注意

通常應使用較新的 pci_alloc_irq_vectors() / pci_free_irq_vectors() API 對。

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

分配多個裝置中斷向量

引數

struct pci_dev *dev

要操作的 PCI 裝置

unsigned int min_vecs

所需向量的最小數量(必須 >= 1)

unsigned int max_vecs

所需向量的最大數量

unsigned int flags

以下一項或多項

  • PCI_IRQ_MSIX 允許嘗試 MSI-X 向量分配

  • PCI_IRQ_MSI 允許嘗試 MSI 向量分配

  • PCI_IRQ_INTX 允許嘗試 INTx 中斷,當且僅當 min_vecs == 1

  • PCI_IRQ_AFFINITY 透過將向量分散到可用的 CPU 周圍來自動管理 IRQ 關聯性

描述

在裝置上分配最多 max_vecs 箇中斷向量。 MSI-X irq 向量分配的優先順序高於純 MSI,而純 MSI 的優先順序高於舊的 INTx 模擬。

成功分配後,呼叫者應使用 pci_irq_vector() 獲取要傳遞給 request_threaded_irq() 的 Linux IRQ 編號。 驅動程式必須在清理時呼叫 pci_free_irq_vectors()

返回

已分配向量數(可能小於 max_vecs),如果少於 min_vecs 箇中斷向量可用,則為 -ENOSPC,否則為其他錯誤。

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 pci_dev *dev

要操作的 PCI 裝置

unsigned int min_vecs

所需向量的最小數量(必須 >= 1)

unsigned int max_vecs

所需向量的最大數量

unsigned int flags

分配標誌,如 pci_alloc_irq_vectors() 中所示

struct irq_affinity *affd

關聯性要求(可以為 NULL)。

描述

pci_alloc_irq_vectors() 相同,但帶有額外的 affd 引數。 有關更多詳細資訊,請檢視該函式文件和 struct irq_affinity

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

獲取裝置中斷向量的 Linux IRQ 編號

引數

struct pci_dev *dev

要操作的 PCI 裝置

unsigned int nr

裝置相關中斷向量索引(從 0 開始); 具有不同的含義,具體取決於中斷模式

  • MSI-X MSI-X 向量表中的索引

  • MSI 啟用的 MSI 向量的索引

  • INTx 必須為 0

返回

Linux IRQ 編號,如果 nr 超出範圍,則為 -EINVAL

const struct cpumask *pci_irq_get_affinity(struct pci_dev *dev, int nr)

獲取裝置中斷向量親和性

引數

struct pci_dev *dev

要操作的 PCI 裝置

int nr

裝置相關中斷向量索引(從 0 開始); 具有不同的含義,具體取決於中斷模式

  • MSI-X MSI-X 向量表中的索引

  • MSI 啟用的 MSI 向量的索引

  • INTx 必須為 0

返回

MSI/MSI-X 向量親和性。如果 nr 超出範圍,或者 MSI(-X) 向量是在沒有明確親和性要求的情況下分配的(例如,透過 pci_enable_msi(), pci_enable_msix_range(), 或 pci_alloc_irq_vectors(),但不使用 PCI_IRQ_AFFINITY 標誌),則返回 NULL。 如果裝置處於傳統 INTx 模式,則返回表示系統啟動期間所有可用 CPU 的通用 CPU ID 集。

void pci_free_irq_vectors(struct pci_dev *dev)

釋放先前為裝置分配的 IRQ

引數

struct pci_dev *dev

要操作的 PCI 裝置

描述

撤銷中斷向量分配以及透過 pci_alloc_irq_vectors_affinity()pci_alloc_irq_vectors() 較早完成的裝置 MSI/MSI-X 啟用。

void pci_restore_msi_state(struct pci_dev *dev)

恢復裝置上快取的 MSI(-X) 狀態

引數

struct pci_dev *dev

要操作的 PCI 裝置

描述

將 Linux 快取的 MSI(-X) 狀態寫回裝置。這通常在系統恢復時或在錯誤恢復 PCI 介面卡重置後很有用。

bool pci_msi_enabled(void)

MSI(-X) 中斷是否在系統範圍內啟用?

引數

void

無引數

返回

如果 MSI 未透過 ACPI FADT、PCI 橋接器怪癖或“pci=nomsi”核心命令列選項全域性停用,則為 true。