一個SMMU記憶體訪問異常的問題

yooooooo發表於2024-08-10

最近碰到棘手的問題: 乙太網進行iperf測試時, 發生了SMMU (System Memory Management Unit)訪問異常導致核心崩潰. 原本只是內部測試發現, 後面在試驗車上也機率性的出現. 問題發生的機率還不小. 很嚴重. 只能先從頭把一些基本概念與流程梳理清楚. 好在最後還是找到了原因並解決了. 鬆了口氣, 才有時間把整個問題的來龍去脈細細的總結下, 算是一個SMMU相關問題的案例.

首先來看看問題的發生的背景.

問題背景

問題發生在利用iperf做網路效能測試的時候, 測試系統(採用高通8155平臺, 內建一個EMAC晶片, 最高支援1Gbps速率)作為客戶端:

iperf -c 172.20.2.33 -p 8989 -f m -R

這裡加-R參數列示客戶端作為資料接收方(奇怪的是, 測試不加-R引數就不會有問題, 這也說明只有在接收資料的過程才會出現問題), 而服務端是傳送方:

iperf -s -p 8989 -f m 

這麼測試幾十個小時就很快出現了, 抓取到的問題堆疊如下. 前面的日誌是SMMU相關的暫存器狀態列印, 後面是核心呼叫堆疊.

[53480.526297] arm-smmu 15000000.apps-smmu: FAR    = 0x00000000a2a2a000
[53480.533192] arm-smmu 15000000.apps-smmu: PAR    = 0x0000000000000000
[53480.540466] arm-smmu 15000000.apps-smmu: FSR    = 0x40000402 [TF W SS ]
[53480.547750] arm-smmu 15000000.apps-smmu: TTBR0  = 0x000f00035bbaa000
[53480.554990] arm-smmu 15000000.apps-smmu: TTBR1  = 0x000f000000000000
[53480.562192] arm-smmu 15000000.apps-smmu: SCTLR  = 0x00c000e7 ACTLR  = 0x00000000
[53480.570572] arm-smmu 15000000.apps-smmu: CBAR  = 0x0001f300
[53480.576741] arm-smmu 15000000.apps-smmu: MAIR0   = 0xf404ff44 MAIR1   = 0x00000000
[53480.585071] arm-smmu 15000000.apps-smmu: Unhandled context fault: iova=0xa2a2a000, cb=14, fsr=0x40000402, fsynr0=0x7e0013, fsynr1=0x0
[53480.597668] arm-smmu 15000000.apps-smmu: soft iova-to-phys=0x0000000000000000
[53480.605440] arm-smmu 15000000.apps-smmu: SOFTWARE TABLE WALK FAILED! Looks like 15000000.apps-smmu accessed an unmapped address!
[53480.617935] arm-smmu 15000000.apps-smmu: hard iova-to-phys (ATOS) failed
[53480.625147] arm-smmu 15000000.apps-smmu: SID=0x3c0
[53480.630332] arm-smmu 15000000.apps-smmu: Unhandled arm-smmu context fault!
[53480.638310] ------------[ cut here ]------------
[53480.638324] kernel BUG at /home/jenkins/.jenkins/workspace/SourceCode/kernel/msm-4.14/drivers/iommu/arm-smmu.c:1762!
[53480.649128] [KERN Warning] ERROR/WARN forces debug_lock off!
[53480.649135] [KERN Warning] check backtrace:
[53480.649151] CPU: 0 PID: 319 Comm: irq/386-arm-smm Tainted: G S         O    4.14.170+ #2
[53480.649160] Hardware name: Qualcomm Technologies, Inc. SA8155P v2 PM8150 ADP-STAR model-D55 (DT)
[53480.649171] Call trace:
[53480.649215]  dump_backtrace+0x0/0x1f4
[53480.649226]  show_stack+0x20/0x2c
[53480.649241]  dump_stack+0xe4/0x134
[53480.649255]  debug_locks_off+0x54/0x88
[53480.649268]  oops_enter+0x14/0x20
[53480.649275]  die+0x38/0x16c
[53480.649284]  bug_handler+0x50/0x88
[53480.649293]  brk_handler+0x6c/0xb4
[53480.649301]  do_debug_exception+0x7c/0x114
[53480.649309]  el1_dbg+0x18/0x74
[53480.649321]  arm_smmu_context_fault+0x8e0/0x944
[53480.649332]  irq_thread_fn+0x2c/0x70
[53480.649340]  irq_thread+0xc0/0x144
[53480.649350]  kthread+0x128/0x138
[53480.649357]  ret_from_fork+0x10/0x18
[53480.649367] Internal error: Oops - BUG: 0 [#1] PREEMPT SMP

從堆疊來看大致可以瞭解到, 這是由於SMMU監測到某個模組非法的訪問DMA地址後, 引起了核心崩潰. 那麼, 為何有這個錯誤SMMU訪問錯誤? 這個錯誤又是哪個模組導致的? 是在什麼情況下引起的SMMU記憶體錯誤了? 這不得不從SMMU本身說起.

什麼是SMMU

簡單來說, SMMU(System Memory Management Unit)是ARM為外設訪問系統RAM提供了一種類似於MMU的虛擬記憶體訪問機制, 外設可以透過DMA直接訪問RAM, 而無需CPU的干預. 如此, 外設可以透過一個虛擬的地址即可訪問實體地址(可以不連續), 做到了不同外設之間IO地址空間的彼此獨立與隔離. 因此, SMMU也通常被稱為IOMMU(Input/Output MMU).

下圖是從ARM SMMU Spec手冊裡的一張SMMU簡圖: SMMU為裝置與RAM之間構建了一個裝置虛擬地址(IOVA)與實體地址之間的對映關係, 每次執行DMA資料傳輸的時候, 都要透過SMMU將IOVA地址翻譯成對應的實體地址.

image

那麼對於裝置驅動來說, 如何使用SMMU了? 不妨來看下SMMU相關的API.

  • arm_iommu_create_mapping: 配置裝置所要使用的VA(Virtual Address, 虛擬地址)的範圍
  • arm_iommu_attach_device: 將分配好的VA地址範圍與裝置繫結, 並開啟SMMU地址轉換
  • dma_map_single/dma_unmap_single: 分配/去除某個DMA地址, 這種方式是非同步的, 常用於一次性傳輸的場景(傳輸完成後DMA的對映即解除了)
  • dma_alloc_coherent/dma_free_coherent: 一致性(consistent), 同步(synchronous)的DMA記憶體分配方法, 確保CPU與裝置的資料始終是同步的, 一般用於需要常駐記憶體的一些資料

這裡不對IOMMU的程式碼做深入分析了. 有關IOMMU相關的流程可以參考核心程式碼:

  • kernel/drivers/iommu: SMMU驅動, 用於配置SMMU, 為裝置驅動提供介面
  • kernel/arch/arm64/mm: 與平臺相關的SMMU的頁表分配的實現
    有了這些SMMU的基礎知識, 我們就來分析下最開始那個問題.

SMMU訪問異常問題分析

繼續來看下問題的日誌. 堆疊的前面一部分是有關SMMU的狀態暫存器:

  • FSR(Fault Status Register)表示SMMU錯誤的型別(轉換/許可權等), 這裡的值0x40000402 [TF W SS ], 說明是一個寫操作時引起的頁表訪問錯誤

  • FAR(Fault Address Register): 表示發生錯誤的IO虛擬地址

  • PAR(Physical Address Register): 發生錯誤時查詢到的實體地址, 這裡是全0, 說明相應的IOVA地址沒有對映

  • TTBRm(Translation Table Base Address):

    • TTBR0: 儲存Translation Table0的基地址
    • TTBR1: 儲存Translation Table1的基地址

重點看下如下兩行日誌, 我們可以知道發生記憶體對映異常的IOVA地址是0xa2a2a000, 對應的SID是0x3c0(SID是對應裝置使用SMMU對映記憶體時的標識),SID一般在裝置樹DTS的配置中指定的.

[53480.585071] arm-smmu 15000000.apps-smmu: Unhandled context fault: iova=0xa2a2a000, cb=14, fsr=0x40000402, fsynr0=0x7e0013, fsynr1=0x0
 ....
[53480.625147] arm-smmu 15000000.apps-smmu: SID=0x3c0

檢視核心的DTS配置, iommus這個對應了裝置節點SMMU的配置;可以看到發生問題的裝置正是乙太網:

emac_emb_smmu: emac_emb_smmu {
	compatible = "qcom,emac-smmu-embedded";
	iommus = <&apps_smmu 0x3C0 0x0>;
	qcom,iova-mapping = <0x80000000 0x40000000>;
};

理清楚這些SMMU的日誌只是第一步, 但是對於為何會發生SMMU訪問異常還是毫無頭緒. 這個只能透過閱讀驅動原始碼弄清楚乙太網網路卡資料的接收流程才能一步步揭開迷霧了.

對於目前的乙太網網路卡來說, 一般採用ring buffer(環形緩衝區)的形式來接收資料; 驅動在初始化的時候為網路卡的ring buffer預分配DMA記憶體, 用於接收資料. 總體來收, 網路卡的資料接收流程有如下三個步驟:

EMAC DMA Process

  • 網路卡需要傳資料時, 獲取到當前的緩衝區對應的DMA記憶體地址(IOVA)後, 透過SMMU向對應的RAM地址傳輸資料
  • 傳送完成後, 透過中斷告知驅動有資料需要接收
  • CPU接收到中斷後, 驅動會把DMA的對映解除, 資料交由CPU處理; 接著驅動把對應的資料傳送到協議棧繼續處理

那麼, 問題來了, SMMU是何時收到DMA訪問異常錯誤的了? 是在第三個步驟, 驅動解除DMA地址對映後, 有地方再次嘗試使用該DMA地址導致的嗎? 從驅動的邏輯來看, 每次傳送完成, DMA地址與RAM地址解除對映後, 沒有地方會再次嘗試獲取該DMA地址了(對應buffer的DMA地址已經置空). 退一步說, 如果是驅動使用的時候發生的問題, 那麼異常的堆疊應該會列印出來, 但是現在只有SMMU相關的日誌.

所以, 問題的源頭只能是在網路卡透過SMMU往對應的DMA地址傳送資料的時候, 就是說如果網路卡給DMA傳輸資料的大小超過了預分配的buffer的大小的話, SMMU會發現對應的DMA地址沒有對映到實體地址, 從而報錯. 解決問題的辦法也很簡單, 只需要把buffer大小由原來的1538修改為2048(2kb)就可以了:

-#define DWC_ETH_QOS_ETH_FRAME_LEN (ETH_FRAME_LEN + ETH_FCS_LEN + VLAN_HLEN + PADDING_ISSUE)
+#define DWC_ETH_QOS_ETH_FRAME_LEN (1<<11)

修改後再次驗證, 問題不再出現. 但這裡有個問題, DMA的buffer大小為何設定成2kb而不是其他如4kb了? 這個實際跟乙太網網路卡(EMAC)本身的設計有關, 一般乙太網的一幀資料是一個MTU(一般是1500, 如果有VLAN資料, 則會多4個位元組), 但為何網路卡傳輸的一幀資料會超過設定的MTU大小, 這個目前諮詢了供應商仍然沒有得到答案(供應商懷疑是傳送端給到的一幀資料超過了最大的MTU 1538, 這個結論仍然值得懷疑).

從高通給的一些問題案例來說, 一般SMMU都是由於需要傳輸的資料大小與實際的buf大小不一致導致的. 總的說來, SMMU的問題看起來十分棘手, 但只要把基本的概念與原理弄清楚, 把程式碼流程梳理完整, 解決這類問題並不是件十分困難的事情.

相關文章