天天看點

MSIHOWTO

來自核心文檔/Documentation/pci/MSI-HOWTO.txt

1. 什麼是MSI

MSI全稱Message Signaled Interrupt。

當裝置向一個特殊位址寫入時,會向CPU産生一個中斷,即也MSI中斷。

MSI能力最初在PCI 2.2裡定義,在PCI 3.0裡被強化,使得每個中斷都可以單獨控制。

PCI 3.0還引入了MSI-X能力,相比MSI,每個裝置可以支援更多的中斷,并且可以獨立配置。

裝置可以同時支援MSI和MSI-X,但同一時刻隻能使能其中一種。

2. 為什麼使用MSI

與傳統引腳中斷相比,有三個方面的優勢。

基于引腳的PCI中斷經常在幾個裝置間共享,核心必須調用與該中斷相關的每一個中斷處理函數,降低了效率。MSI不是共享的,是以不存在這個問題。

當裝置向記憶體寫入資料,然後發起引腳中斷時,有可能在CPU收到中斷時,資料還未到達記憶體(在PCI-PCI橋後的裝置更有可能如此)。為了保證資料已達 記憶體,中斷處理程式必須輪詢産生該中斷的裝置的一個寄存器,PCI事務保序規則會確定所有資料到達記憶體後,寄存器才會傳回值。

使用MSI時,産生中斷的寫不能越過資料寫,因而避免了這個問題。當中斷産生時,驅動可以确信所有資料已經到達記憶體。

PCI的每個功能裝置隻支援一個基于引腳的中斷,驅動常常需要查詢裝置來确定發生的事件,降低了中斷處理的效率。通過MSI,一個裝置可以支援多個中斷,這樣可以為不同的使用不同的中斷。比如:

     1. 給不常發生的事件(如錯誤)指定獨立的中斷,這樣驅動可以正常中斷路徑進行更有效的處理。

     2. 給網卡的每個封包隊列或者存儲控制器的每個端口配置設定中斷。

3. 如何使用MSI

PCI裝置初始化為使用基于引腳的中斷,裝置驅動需要将裝置配置為使用MSI或MSI-X。不是所有的裝置可以完整支援MSI,下面有些API就直接傳回失敗,是以仍然使用基于引腳的中斷。

3.1 核心提供MSI支援

核心需要配置CONFIG_PCI_MSI以支援MSI或者MSI-X,該配置是否可以配置受架構和其它一些配置的影響。

3.2 使用MSI

大部分工作在PCI層的驅動裡完成,隻需要請求PCI層為裝置設定MSI能力即可。

3.2.1 pci_enable_msi

int pci_enable_msi(struct pci_dev *dev)

這個調用隻給裝置配置設定一個中斷,不管裝置支援多少個MSI中斷。裝置會從基于引腳中斷模式切換為MSI模式。dev->irq會賦予一個新的代表MSI的編号。

在驅動需要在調用request_irq()之前調用這個接口。因為打開MSI中斷的同時會禁用引腳中斷IRQ,驅動是以不會收到舊的中斷。

3.2.2 pci_enable_msi_block

int pci_enable_msi_block(struct pci_dev *dev, int count)

這個接口是pci_enable_msi的變種,它可以請求多個MSI中斷。MSI規範要求中斷必須以2的幂配置設定,最多為2^5(32)。

如果函數傳回0,則表示成功配置設定了不少于驅動請求的中斷數量(可能會大于請求數量)。該函數打開裝置的MSI,并将中斷組的最小編号賦給dev->irq,該裝置的中斷範圍為dev->irq至dev->irq + count - 1。

如果函數傳回負值,表示裝置無法提供更多的中斷,驅動不應試圖再去請求。如果傳回的是正值,這個值會小于count,表示目前最配置設定的最大中斷數量。對這于兩種情況,函數都不會更新irq值,裝置也不會切換到MSI模式。

裝置驅動必須要正确處理上述第二種情況。在中斷數量不夠的情況下,有的裝置尚可工作,驅動就應再次調用pci_enable_msi_block()。不過由于一些限制,第二次調用未必也可以成功。

3.2.3 pci_disable_msi

void pci_disable_msi(struct pci_dev *dev)

該函數的功能是撤消pci_enable_msi()或pci_enable_msi_block()的工作。它恢複dev->irq為引腳中斷号,釋放此前配置設定的MSI。中斷号之後有可能配置設定給其它裝置,是以驅動不應保留dev->irq的值。

驅動必須調用free_irq()以釋放之前request_irq()配置設定的中斷号。否則會産生BUG_ON(),裝置将繼續保持MSI使能,并洩露中斷向量。

3.3 使用MSI-X

MSI-X能力比MSI更為靈活,它支援2048個中斷,每個中斷都可以單獨控制。要支援這種能力,驅動必須使用結構數組struct msix_entry。

[cpp]

view plain

copy

print

?

  1. struct msix_entry {  
  2.      u16 vector;     /* kernel uses to write alloc vector */  
  3.      u16 entry;     /* driver uses to specify entry */  
  4. }  

這個定義允許裝置以分散的方式使用中斷(比如使用中斷3和1027,隻需配置設定兩個數組元素)。驅動負責填充數組各元素的entry部分,以使核心為其配置設定中斷。不能給多個entry賦予相同的編号。

3.3.1 pci_enable_msix

int pci_enable_msix(struct pci_dev *dev, struct msix_entry *entries, int nvec)

該函數向PCI子系統請求配置設定nvec個MSI中斷。入參entries指向結構數組,元素個數不少于nvec。函數傳回0表示調用成功,裝置被切換至 MSI-X中斷模式,數組元素中的vector字段也被填充為中斷号。驅動接下來為每個需要使用的vector調用request_irq()。

如果函數傳回負值,則表示出錯,驅動不應再從該裝置申請配置設定MSI-X中斷。如果傳回的是正值,則表示最大可以配置設定的中斷數。

該函數與pci_enable_msi()不同之處在于它不修改dev->irq,一旦MSI-X使能後,dev->irq這個中斷号不會再産生中斷。驅動需要記錄已配置設定的MSI-X中斷号,以保證後續的資源釋放。

一般來說,驅動在裝置初始化時調用一次該函數。

由于種種原因,核心有可能無法提供驅動所要求的中斷數量,一個好的驅動應該能夠處理可變數量的MSI-X中斷。下面是一個例子:

[cpp]

view plain

copy

print

?

  1. static int foo_driver_enable_msix(struct foo_adapter *adapter, int nvec)  
  2. {  
  3.      while (nvec >= FOO_DRIVER_MINIMUM_NVEC)  
  4.      {  
  5.           rc = pci_enable_msix(adapter->pdev, adapter->msix_entries, nvec);  
  6.           if (rc > 0)  
  7.                nvec = rc;  
  8.           else  
  9.                return rc;  
  10.      }  
  11.      return -ENOSPC;  
  12. }  

3.3.2 pci_disable_msix

void pci_disable_msix(struct pci_dev *dev)

這個函數的作用是撤銷pci_enable_msix()的工作,它釋放之前配置設定的MSI中斷。同樣,釋放的中斷号後續可能會配置設定給其它裝置,驅動不應再記錄使用這些中斷号。

在調用這個函數之前,驅動必須調用free_irq()以釋放request_irq()配置設定的中斷号,否則會産生BUG_ON,裝置将維持在MSI使能的狀态,并洩漏中斷向量。

3.3.3 MSI-X表

MSI-X能力指定了一個BAR及BAR内偏移量用于通路MSI-X表,這個位址由PCI子系統映射,驅動不應該直接通路。如果驅動想屏蔽或者開啟一個中斷,應該調用disable_irq()/enable_irq()。

3.4 處理同時支援MSI和MSI-X能力的裝置

如果裝置同時支援MSI和MSI-X,則可以運作在MSI模式或者MSI-X模式下,但不能同時運作,這是PCI規則的要求,是以PCI層也進行了 限制。在MSI-X使能的情況下調用pci_enable_msi()或者在MSI使能的情況下調用pci_enable_msix()将産生錯誤。如果 裝置驅動在運作時希望在MSI和MSI-X之間切換,它必須先停止裝置,然後将其切換為引腳中斷模式,然後再通過pci_enable_msi()或 pci_enable_msix()進入MSI或MSI-X模式。這種操作并不常見,在開發過程中用于調試/測試。

3.5 使用MSI的考慮

3.5.1 選擇MSI-X和MSI

如果裝置同時支援MSI-X和MSI能力,應優先考慮使用MSI-X。MSI-X支援1~2048間任意數量的中斷,而MSI隻支援32個中斷(并 且必須是2的冪)。MSI中斷必須是連續配置設定的,系統不能像MSI-X那樣配置設定這麼多的中斷向量。在某些平台上,MSI中斷隻能發送給一個CPU 組,MSI-X中斷可以發給不同的CPU。

3.5.2 spinlock

多數驅動為每個裝置定義了一個spinlock,在中斷處理函數中取鎖,對于引腳中斷或者單個MSI中斷,不需要禁用中斷(Linux保證同一個中斷不會 重入)。如果裝置使用了多個中斷,驅動在持鎖期間必須禁用中斷,否則在裝置産生另一個中斷時,驅動會遞歸取鎖進而産生死鎖。

有兩個解決方法,一個是使用spin_lock_irqsave()或spin_lock_irq(),另一個是在request_irq()調用時指定IRQF_DISABLED,核心會在禁用中斷的環境下完成整個中斷處理過程。

如果MSI中斷處理程式不在整個過程中持鎖,使用第一種方法是最好的。如果想避免在中斷禁用/使能狀态間切換,則選擇第二種方法。

3.6 如何得知裝置的MSI/MSI-X已經使能

使用lspci -v。有些裝置會顯示"MSI"、"Message Signaled Interrupts"或者"MSI-X"能力,使能的會在前面顯示+,禁用的會顯示"-"。

4. MSI quirks

一些PCI晶片或裝置不支援MSI,PCI子系統提供了三種方法禁用MSI:

     1. 全局禁用

     2. 禁用特定橋之下的所有裝置

     3. 禁用某個裝置

4.1 全局禁用

有些host晶片不能正确支援MSI,如果廠家在ACPI FADT表中明确了,Linux會自動禁用MSI。有些單闆沒有在這個表包含這樣的資訊,需要自己檢測,這些都列在drivers/pci /quriks.c中的quirk_disable_all_msi()中了。

如果單闆在MSI支援上有問題,可以在核心指令參數裡加上pci=nomsi以禁用所有裝置的MSI。

4.2 禁用特定橋之下的所有裝置

有些PCI橋不能在總線間正确地傳遞MSI,這種情況必須禁用該橋之下所有裝置的MSI。

有些橋允許通過配置空間的某些位來使能MSI。在可能的情況下,Linux會盡量打開host晶片的MSI支援。如果某個橋片Linux并不識别,而你确定它可以使用MSI,可以通過下面的指令打開MSI支援。

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

$bridge是橋的PCI位址(比如0000:00:0e.0)。

要禁用MSI,使用echo 0即可。

4.3 禁用某個裝置

如果某些裝置已知在MSI實作上有問題,一般是在裝置驅動裡處理,如果有必要,也可以在quirk裡處理。

4.4 裝置MSI被禁用的原因查找

除上述情況外,還有很多原因會導緻一個裝置的MSI沒有使能,第一步應該仔細檢查dmesg,看MSI有沒有使能,還要檢查CONFIG_PCI_MSI配置有沒有打開。

通過lspci -t可以檢視裝置之上的橋,讀取/sys/bus/pci/devices/*/msi_bus看MSI是否使能了。

檢查裝置驅動是否支援MSI,比如是否調用了pci_enable_msi(), pci_enable_msix()或者pci_enable_msi_block()等等。