證與排障指南)
做驅(qū)動(dòng)調(diào)試的人大概都經(jīng)歷過(guò)這種場(chǎng)面設(shè)備驅(qū)動(dòng)跑得好好的某一刻數(shù)據(jù)突然錯(cuò)亂日志里干干凈凈用崩潰轉(zhuǎn)儲(chǔ)一看內(nèi)核某塊結(jié)構(gòu)體被一段完全不相干的十六進(jìn)制數(shù)據(jù)糊住了。查到最后往往是設(shè)備發(fā)出的 DMA 地址寫(xiě)錯(cuò)了或者描述符里的地址字段被固件改花了——沒(méi)有 IOMMU 的時(shí)候總線上的 DMA 請(qǐng)求就是全裸奔的設(shè)備愛(ài)往哪寫(xiě)就往哪寫(xiě)內(nèi)存管理單元完全管不了它。Linux 6.6 內(nèi)核里 IOMMU 子系統(tǒng)的 DMA 重映射DMA Remapping就是專(zhuān)門(mén)收拾這個(gè)問(wèn)題的一層它把設(shè)備看到的地址空間和 CPU 看到的物理地址空間徹底切開(kāi)設(shè)備發(fā)出的每一個(gè)地址都要先過(guò)這張翻譯表。這篇內(nèi)容圍繞 IOMMU 的 DMA 重映射原理展開(kāi)同時(shí)把 6.6 上怎么打開(kāi)、怎么觀察、怎么驗(yàn)證、怎么排坑一次講透適合正在寫(xiě) PCIe 驅(qū)動(dòng)、做設(shè)備直通、或者被 DMA 越界搞得睡不著的同行參考。1. IOMMU 到底在解決什么問(wèn)題從一次指針踩內(nèi)存說(shuō)起1.1 沒(méi)有 IOMMU 時(shí)設(shè)備的 DMA 是完全不設(shè)防的先把最基礎(chǔ)的一層說(shuō)清楚。CPU 訪問(wèn)內(nèi)存要經(jīng)過(guò) MMU頁(yè)表決定哪塊虛擬地址對(duì)應(yīng)哪塊物理頁(yè)越界就缺頁(yè)異常。但設(shè)備不一樣設(shè)備發(fā)起 DMA 時(shí)把地址丟到總線上由內(nèi)存控制器直接取數(shù)或者寫(xiě)數(shù)這條路默認(rèn)繞開(kāi)了 MMU。也就是說(shuō)一塊 PCIe 采集卡只要往描述符里寫(xiě)一個(gè)0xffff_8880_0000_0000這樣的地址硬件就會(huì)老老實(shí)實(shí)往那個(gè)物理位置寫(xiě)數(shù)據(jù)中間沒(méi)有任何審批環(huán)節(jié)。我在某實(shí)驗(yàn)室見(jiàn)過(guò)一塊自定義采集卡FPGA 側(cè)的地址位寬算錯(cuò)了一位描述符里的目標(biāo)地址被左移了一位結(jié)果驅(qū)動(dòng)程序填給它的緩沖區(qū)地址整體偏移。沒(méi)開(kāi) IOMMU 的機(jī)器上這個(gè)偏移會(huì)隨機(jī)打壞相鄰的 slab 對(duì)象現(xiàn)象是跑幾個(gè)小時(shí)之后文件系統(tǒng)報(bào)錯(cuò)或者某個(gè)完全無(wú)關(guān)的進(jìn)程收到 SIGBUS。這種問(wèn)題最難查的地方在于故障點(diǎn)和根因之間隔著幾百毫秒甚至幾秒崩潰現(xiàn)場(chǎng)和元兇毫無(wú)關(guān)聯(lián)。打開(kāi) IOMMU 之后同樣的錯(cuò)誤地址會(huì)立刻觸發(fā)一次 IOMMU 缺頁(yè)內(nèi)核打印出一行包含設(shè)備標(biāo)識(shí)和出錯(cuò)地址的 fault 日志驅(qū)動(dòng)直接被判定為行為異常。故障從偶發(fā)玄學(xué)變成穩(wěn)定復(fù)現(xiàn)這個(gè)價(jià)值遠(yuǎn)比性能優(yōu)化重要。所以我一直跟人說(shuō)IOMMU 首先是一個(gè)調(diào)試工具和隔離邊界其次才是一個(gè)地址翻譯器。1.2 三層價(jià)值隔離、翻譯、訪問(wèn)控制把 IOMMU 的能力拆開(kāi)看其實(shí)是三件相互支撐的事。第一是地址翻譯設(shè)備用一套地址內(nèi)核里叫 IOVAI/O Virtual Address硬件按頁(yè)表翻成物理地址PA。第二是隔離每個(gè)設(shè)備嚴(yán)格說(shuō)是每個(gè) IOMMU group有自己獨(dú)立的地址空間A 設(shè)備映射的區(qū)域 B 設(shè)備看不見(jiàn)也摸不著哪怕兩塊卡是同型號(hào)、同一個(gè)驅(qū)動(dòng)。第三是訪問(wèn)控制每一個(gè)頁(yè)表項(xiàng)都帶權(quán)限位讀、寫(xiě)、執(zhí)行是分開(kāi)的設(shè)備只能做被顯式允許的操作。這三層疊起來(lái)才支撐了后來(lái)一堆上層玩法設(shè)備直通給虛擬機(jī)時(shí)把整個(gè) group 的地址空間交給客戶(hù)機(jī)管內(nèi)核里的vfio靠它做用戶(hù)態(tài)驅(qū)動(dòng)容器場(chǎng)景下做設(shè)備隔離內(nèi)核自己的DMA API靠它在 32 位設(shè)備上做地址窗口收斂bounce buffer 那一套。很多人以為 IOMMU 就是為了虛擬化其實(shí)虛擬化只是最出名的一個(gè)消費(fèi)者日常驅(qū)動(dòng)開(kāi)發(fā)里它天天都在干活。1.3 為什么叫重映射而不是映射這個(gè)用詞值得掰一下因?yàn)樗苯訉?duì)應(yīng)硬件結(jié)構(gòu)。設(shè)備發(fā)出的地址在 IOMMU 硬件里叫輸入地址經(jīng)過(guò)查表后得到輸出地址再送到內(nèi)存控制器。翻譯過(guò)程是進(jìn)來(lái)一個(gè)地址出去另一個(gè)地址所以叫 remapping。而 CPU 側(cè)的 MMU 我們習(xí)慣叫 mapping因?yàn)樗菑臎](méi)有物理歸屬的虛擬地址建立起映射關(guān)系。落到代碼上這個(gè)差別體現(xiàn)得很明顯CPU 側(cè)你操作的是vm_area_struct和頁(yè)表是申請(qǐng)地址→缺頁(yè)→填頁(yè)表的懶惰路徑IOMMU 側(cè)是先顯式建映射→再告訴設(shè)備可以用這個(gè)地址沒(méi)有缺頁(yè)這回事ATS/PRI那套另說(shuō)屬于設(shè)備主動(dòng)問(wèn)路后面章節(jié)會(huì)提。理解了這個(gè)不對(duì)稱(chēng)你就能明白為什么 DMA 映射的 API 必須成對(duì)出現(xiàn)dma_map_single/dma_unmap_single中間那段窗口硬件已經(jīng)準(zhǔn)備好了用完必須還回去否則 IOVA 會(huì)漏。2. DMA 重映射的核心數(shù)據(jù)結(jié)構(gòu)與地址翻譯鏈路2.1 設(shè)備、group、domain三個(gè)概念別搞混剛接觸這塊的人最容易在這三個(gè)詞上打轉(zhuǎn)。我用一個(gè)類(lèi)比說(shuō)清楚設(shè)備是具體的網(wǎng)卡、采集卡、NVMe 盤(pán)group是一批無(wú)法相互隔離的設(shè)備集合硬件上它們共用同一個(gè)翻譯單元或者同一條通往內(nèi)存的路徑內(nèi)核只能把它們當(dāng)成一個(gè)整體來(lái)管domain是真正承載頁(yè)表的那塊地址空間一個(gè) domain 里可以掛多個(gè) group。關(guān)鍵點(diǎn)在于內(nèi)核默認(rèn)策略是一個(gè) group 一個(gè) domain同一 group 里的設(shè)備共享同一套頁(yè)表。這就意味著如果同一個(gè) group 里有一塊你不信任的設(shè)備它會(huì)和同組的可信設(shè)備平起平坐。做過(guò)直通的人應(yīng)該都踩過(guò)這個(gè)坑——想把某塊網(wǎng)卡單獨(dú)丟給虛擬機(jī)結(jié)果發(fā)現(xiàn)它和板載的一個(gè) USB 控制器同組只能整組一起交出去或者用pci-stub提前把同組設(shè)備都占住。在 6.6 里查看分組很簡(jiǎn)單for g in /sys/kernel/iommu_groups/*; do echo $(basename $g) for d in $g/devices/*; do echo $(basename $d) $(lspci -nns $(basename $d) 2/dev/null | cut -c1-80) done done分組結(jié)果完全由固件描述決定ACPI 表或者設(shè)備樹(shù)里的描述決定了哪些設(shè)備歸到一組內(nèi)核只是照著執(zhí)行。所以有些分組看起來(lái)莫名其妙那是固件的鍋不是內(nèi)核的鍋。2.2 頁(yè)表結(jié)構(gòu)從根表一路走到最后一頁(yè)x86 平臺(tái)上的 DMA 重映射硬件ACPI 里描述為 DMAR它的翻譯鏈路大致是三層結(jié)構(gòu)先按設(shè)備標(biāo)識(shí)找到根表項(xiàng)再按總線號(hào)找到上下文表項(xiàng)上下文表項(xiàng)里存著真正的多級(jí)頁(yè)表基址。真正做地址翻譯的部分和 CPU 的分頁(yè)結(jié)構(gòu)思路一致可以是三級(jí)、四級(jí)甚至五級(jí)具體級(jí)別在上下文表項(xiàng)里寫(xiě)明。另一類(lèi)主流平臺(tái)用的表述叫 SMMU結(jié)構(gòu)換了個(gè)名字但思想一樣設(shè)備按流 ID 查到一張表里的表項(xiàng)表項(xiàng)指向另一層配置配置里指定用哪套頁(yè)表、哪個(gè)地址空間。你在設(shè)備樹(shù)里給設(shè)備節(jié)點(diǎn)寫(xiě)一條描述 IOMMU 歸屬的屬性?xún)?nèi)核初始化時(shí)就能把這條鏈路串起來(lái)。這個(gè)多層結(jié)構(gòu)的存在直接導(dǎo)致了兩個(gè)工程后果。第一初始化階段必須先把整條鏈路建好包括分配頁(yè)表內(nèi)存、填根表和上下文表這段代碼在內(nèi)核啟動(dòng)早期跑調(diào)試起來(lái)很麻煩。第二頁(yè)表級(jí)別可以按設(shè)備能力協(xié)商一塊只支持 32 位地址的設(shè)備給它配的頁(yè)表級(jí)別低IOVA 窗口自然也小這直接決定了你能給這臺(tái)設(shè)備分配多大的 DMA 緩沖區(qū)。后面第 4 節(jié)講參數(shù)計(jì)算時(shí)會(huì)用到這一點(diǎn)。2.3 IOVA 分配器地址不是隨便給的頁(yè)表建好了還得決定給設(shè)備什么 IOVA。這塊在 6.6 里是dma-iommu這層在管核心是一個(gè)按地址范圍分配的管理器內(nèi)部用紅黑樹(shù)記錄已占用的區(qū)間再配一層 per-CPU 的小緩存來(lái)加速短小分配的復(fù)用。為什么要有 per-CPU 緩存因?yàn)?DMA 映射調(diào)用極其頻繁網(wǎng)卡每發(fā)一個(gè)包、磁盤(pán)每做一次 I/O 都要映射一次如果每次都去紅黑樹(shù)上做插入刪除鎖競(jìng)爭(zhēng)會(huì)直接把性能打下來(lái)。緩存的做法是unmap 時(shí)把釋放的 IOVA 區(qū)間按大小檔位存進(jìn)當(dāng)前 CPU 的緩存數(shù)組下次分配同檔位時(shí)直接從數(shù)組里取取不到再去全局樹(shù)里找。實(shí)測(cè)下來(lái)這個(gè)緩存能干掉相當(dāng)可觀的一筆分配開(kāi)銷(xiāo)代價(jià)是 IOVA 的復(fù)用延后了一點(diǎn)對(duì)于調(diào)試來(lái)說(shuō)你可能會(huì)看到某些 IOVA 長(zhǎng)時(shí)間沒(méi)被回收。默認(rèn)情況下64 位設(shè)備拿到的 IOVA 窗口通常是 48 位寬度也就是一個(gè)非常大的地址空間。這個(gè)寬度不是拍腦袋定的它來(lái)自設(shè)備的coherent_dma_mask和dma_mask設(shè)備支持多寬的地址內(nèi)核就據(jù)此外推 IOMMU 能給多寬的窗口同時(shí)受 IOMMU 硬件本身支持的最大寬度約束。如果你的驅(qū)動(dòng)里把dma_mask設(shè)成 32 位恭喜你給自己套了一個(gè) 4GB 的籠子映射壓力大時(shí)很快會(huì)撞墻。2.4 權(quán)限位、緩存一致性與 IOTLB頁(yè)表項(xiàng)里的權(quán)限位不用多說(shuō)讀、寫(xiě)分開(kāi)內(nèi)核在dma_map_*時(shí)按數(shù)據(jù)傳輸方向設(shè)置。這里有個(gè)細(xì)節(jié)值得提DMA_FROM_DEVICE方向下建的是可寫(xiě)權(quán)限設(shè)備要往內(nèi)存寫(xiě)而不是只讀反過(guò)來(lái)DMA_TO_DEVICE建的是可讀。剛接觸的人經(jīng)常反著理解以為方向是CPU 要干什么其實(shí)是設(shè)備要干什么。一致性方面同一塊內(nèi)存 CPU 和設(shè)備都能訪問(wèn)緩存就成了麻煩。一致性架構(gòu)x86 常見(jiàn)的那種硬件自己保證非一致性架構(gòu)上就得軟件動(dòng)手dma_map_single在映射前要把 CPU 緩存里對(duì)應(yīng)的數(shù)據(jù)刷到內(nèi)存dma_unmap_single之后要讓 CPU 緩存失效否則 CPU 讀到的是舊值。這一套在 IOMMU 之上照樣要做IOMMU 管的是地址翻譯和權(quán)限不管緩存。最后是 IOTLB。頁(yè)表改了不等于硬件立刻知道IOMMU 內(nèi)部有自己的地址翻譯緩存翻譯結(jié)果會(huì)被緩存起來(lái)。改了映射必須讓緩存失效否則設(shè)備還在用舊的翻譯結(jié)果訪問(wèn)已經(jīng)回收的內(nèi)存。這就是嚴(yán)格模式和延遲模式的分界點(diǎn)嚴(yán)格模式下每次 unmap 立刻發(fā)失效指令安全但慢延遲模式下把要失效的區(qū)間攢起來(lái)攢到一定量或者遇到必須同步的點(diǎn)再一起發(fā)快但有窗口風(fēng)險(xiǎn)。# 查看當(dāng)前是否嚴(yán)格模式不同小版本參數(shù)名可能微調(diào)以?xún)?nèi)核文檔為準(zhǔn) cat /proc/cmdline | tr \n | grep -E iommu3. Linux 6.6 里的那幾條關(guān)鍵路徑3.1 啟動(dòng)階段固件描述怎么變成 domain內(nèi)核啟動(dòng)早期IOMMU 的初始化分兩步走。第一步是探測(cè)硬件、解析固件描述把每一條哪個(gè)設(shè)備歸哪套翻譯硬件管的關(guān)系記錄下來(lái)形成設(shè)備到 group 的對(duì)應(yīng)關(guān)系。這一步在內(nèi)存管理初始化之前就要做完一部分因?yàn)楹竺娴?DMA 一致性分配可能馬上就需要翻譯能力。第二步是惰性建域。內(nèi)核不會(huì)在啟動(dòng)時(shí)給所有設(shè)備都建 domain而是等設(shè)備真正被驅(qū)動(dòng) probe、第一次調(diào)用 DMA 映射時(shí)才建。這個(gè)設(shè)計(jì)很務(wù)實(shí)一臺(tái)機(jī)器上可能有幾十個(gè)設(shè)備大部分根本沒(méi)驅(qū)動(dòng)管全建一遍純屬浪費(fèi)頁(yè)表內(nèi)存。你在日志里看到的那些初始化行只說(shuō)明翻譯硬件就緒了不代表每個(gè)設(shè)備都已經(jīng)有自己的地址空間。這里有個(gè)排查技巧開(kāi)機(jī)日志里搜 IOMMU 相關(guān)的行能看到翻譯單元被識(shí)別、支持的頁(yè)表級(jí)別、以及是否有設(shè)備因?yàn)槊枋鰶_突被跳過(guò)。如果某塊設(shè)備明明在卻查不到它的 group八成是固件里的描述沒(méi)覆蓋到它或者是熱插拔槽位的描述寫(xiě)得有問(wèn)題。3.2 驅(qū)動(dòng) probe 之后dma_map_single 里的四層調(diào)用這是整條鏈路里最值得逐層看的一段。你的驅(qū)動(dòng)調(diào)用dma_map_single(dev, ptr, size, dir)往下依次是通用 DMA 層先看這個(gè)設(shè)備有沒(méi)有 IOMMU domain有就走 IOMMU 路徑?jīng)]有就退化成直接的物理地址映射這個(gè)退化路徑叫 direct mapping需要swiotlb兜底處理地址不可達(dá)的情況。走 IOMMU 路徑時(shí)進(jìn)入dma-iommu這層先做頁(yè)對(duì)齊處理你傳進(jìn)來(lái)的地址和長(zhǎng)度幾乎不可能是頁(yè)對(duì)齊的這層會(huì)算出覆蓋范圍并想好頭尾兩頁(yè)怎么處理。分配 IOVA從分配器里拿一段足夠大的連續(xù)地址注意這里要的是連續(xù) IOVA物理上可以不連續(xù)。落頁(yè)表調(diào)用iommu_map把這批 IOVA 和對(duì)應(yīng)的物理頁(yè)逐個(gè)建立映射頁(yè)大小會(huì)盡量往大頁(yè)上湊能用一個(gè) 2MB 頁(yè)就不用 512 個(gè) 4KB 頁(yè)這對(duì)減少 TLB 壓力和頁(yè)表內(nèi)存都很有意義。最終返回給你的是 IOVA你把它寫(xiě)進(jìn)設(shè)備寄存器或者描述符。注意返回的這個(gè)地址絕對(duì)不能當(dāng)指針用CPU 直接解引用它是未定義行為因?yàn)樗辉?CPU 的地址空間里。順手貼一個(gè)最小驗(yàn)證模塊的核心片段方便你在自己的板子上跑一遍#include linux/module.h #include linux/pci.h #include linux/dma-mapping.h static int __init dmatest_init(void) { struct pci_dev *pdev; void *buf; dma_addr_t dma; u64 mask DMA_BIT_MASK(48); pdev pci_get_device(PCI_ANY_ID, PCI_ANY_ID, NULL); if (!pdev) return -ENODEV; if (dma_set_mask_and_coherent(pdev-dev, mask)) dev_warn(pdev-dev, 48-bit mask not accepted\n); buf dma_alloc_coherent(pdev-dev, 4096, dma, GFP_KERNEL); if (!buf) return -ENOMEM; pr_info(iommu probe: va%px dma%pad\n, buf, dma); dma_free_coherent(pdev-dev, 4096, buf, dma); pci_dev_put(pdev); return 0; }跑起來(lái)看dma打印出來(lái)的值。如果機(jī)器上 IOMMU 生效這個(gè)值幾乎不會(huì)等于__pa(buf)如果兩者相等說(shuō)明走的是 direct mapping 或者身份映射IOMMU 實(shí)際沒(méi)在翻譯。3.3 iommu_map 內(nèi)部頁(yè)大小拆分與批量映射進(jìn)到iommu_map之后核心動(dòng)作是先算這一段能用的最大頁(yè)是多少。硬件會(huì)給一個(gè)支持的頁(yè)大小位圖映射的 IOVA 和物理地址又各有對(duì)齊要求兩頭一取得出這一段里能用大頁(yè)的范圍剩下的邊角用 4KB 頁(yè)補(bǔ)齊。這個(gè)拆分邏輯是性能的關(guān)鍵也是坑的來(lái)源。舉個(gè)具體例子你要映射 4MB 加 4KB 的緩沖區(qū)起始地址只對(duì)齊到 4KB。那么開(kāi)頭那一小段只能是 4KB 頁(yè)中間能湊出連續(xù)的 2MB 對(duì)齊區(qū)域就用 2MB 頁(yè)末尾再補(bǔ)。這樣拼出來(lái)的頁(yè)表?xiàng)l目數(shù)遠(yuǎn)少于全用 4KB 頁(yè)TLB 命中也更好。iommu_map_sg走的是同一套邏輯只是把散列表一次性批量處理省掉了多次調(diào)用的開(kāi)銷(xiāo)網(wǎng)卡這類(lèi)做多段 DMA 的設(shè)備全靠它。6.6 里這塊還在演進(jìn)中屬性結(jié)構(gòu)逐步統(tǒng)一老的按設(shè)備單獨(dú)實(shí)現(xiàn)映射回調(diào)的方式正被按域分配的分頁(yè)接口替代。這意味著如果你在維護(hù)一個(gè)廠內(nèi)私有的 IOMMU 驅(qū)動(dòng)升級(jí)到 6.6 附近時(shí)要留意接口變化別照著老版本的樣板抄。3.4 unmap 的時(shí)機(jī)比 map 更容易出事的地方映射寫(xiě)起來(lái)簡(jiǎn)單回收才是見(jiàn)功力的地方。dma_unmap_single做三件事清頁(yè)表項(xiàng)、把 IOVA 還給分配器、按模式?jīng)Q定是否立刻發(fā)失效指令。任何一步出問(wèn)題都有明確癥狀。漏了 unmapIOVA 就一直被占著跑久了分配器找不到連續(xù)區(qū)間驅(qū)動(dòng)申請(qǐng)緩沖區(qū)開(kāi)始失敗日志里會(huì)出現(xiàn) DMA API 調(diào)試相關(guān)的告警或者分配失敗。這個(gè)在長(zhǎng)時(shí)間壓測(cè)里才暴露所以很多驅(qū)動(dòng)的問(wèn)題是在跑了一兩天之后才冒出來(lái)的。更陰險(xiǎn)的是時(shí)序錯(cuò)位設(shè)備還在往緩沖區(qū)寫(xiě)數(shù)據(jù)你的代碼已經(jīng)把 unmap 做了IOVA 隨后被分配給另一個(gè)設(shè)備。這時(shí)候總線上還飛著舊設(shè)備的寫(xiě)請(qǐng)求落到新設(shè)備的緩沖區(qū)里數(shù)據(jù)就被污染了。這類(lèi)問(wèn)題表現(xiàn)為隨機(jī)數(shù)據(jù)錯(cuò)亂且只在負(fù)載高的時(shí)候出現(xiàn)。規(guī)避辦法很樸素必須確保設(shè)備已經(jīng)停止對(duì)該緩沖區(qū)的訪問(wèn)讀完成寄存器、等中斷、做一次同步屏障才能 unmap。延遲模式會(huì)讓這個(gè)窗口更長(zhǎng)調(diào)試環(huán)境建議先開(kāi)嚴(yán)格模式穩(wěn)定之后再考慮性能優(yōu)化。4. 實(shí)操打開(kāi) IOMMU 之后怎么觀察和驗(yàn)證4.1 內(nèi)核命令行參數(shù)與生效判斷先確認(rèn)硬件和固件是否就緒。開(kāi)機(jī)日志里搜翻譯單元的初始化信息能看到它被識(shí)別、支持的最大地址寬度、頁(yè)大小位圖。如果完全沒(méi)有相關(guān)行先查固件里對(duì)應(yīng)的開(kāi)關(guān)有沒(méi)有打開(kāi)x86 平臺(tái)上這個(gè)開(kāi)關(guān)有時(shí)在芯片組配置里名字不一定顯眼。內(nèi)核參數(shù)方面常用的幾個(gè)iommuon強(qiáng)制啟用iommupt讓設(shè)備默認(rèn)走身份映射只建一張等價(jià)映射性能幾乎無(wú)損隔離性大部分丟失iommu.strict1強(qiáng)制嚴(yán)格模式iommu.passthrough0禁用透?jìng)?。這里要提醒一句參數(shù)名在不同小版本上偶有調(diào)整動(dòng)手前翻一下隨內(nèi)核源碼附帶的命令行參數(shù)文檔比在網(wǎng)上抄別人的配置靠譜。判斷是否真的生效最直接的辦法是看設(shè)備的 DMA 地址和物理地址是否相同也就是上面那段模塊代碼的輸出。第二直接的辦法是看/sys/kernel/debug/iommu/devices/下面有沒(méi)有這個(gè)設(shè)備的條目——6.x 里加的這個(gè)調(diào)試接口相當(dāng)好用里面能看到它當(dāng)前掛的域類(lèi)型、是否啟用 ATS、以及身份映射狀態(tài)。我第一次用它排查一個(gè)以為開(kāi)了其實(shí)沒(méi)開(kāi)的直通問(wèn)題時(shí)五分鐘就定位到了。4.2 用 tracepoint 看每一次映射的真實(shí)參數(shù)調(diào)性能或者查泄漏光看日志不夠得上 tracepoint。IOMMU 子系統(tǒng)埋了幾個(gè)關(guān)鍵點(diǎn)映射、解映射、批量映射都能記錄下域指針、IOVA、物理地址、長(zhǎng)度、權(quán)限。# 打開(kāi)映射相關(guān)的事件 cd /sys/kernel/debug/tracing echo 1 events/iommu/enable echo 1 tracing_on # 跑一段你的業(yè)務(wù) sleep 5 echo 0 tracing_on cat trace | head -50從輸出里能直接讀出三件事映射長(zhǎng)度分布大部分是 256 字節(jié)還是 4KB反映你的緩沖區(qū)設(shè)計(jì)是否合理、IOVA 是否集中在低地址說(shuō)明窗口開(kāi)小了或者分配器壓力大、以及 map 和 unmap 的數(shù)量是否匹配不匹配就是泄漏。我用這套方法抓到過(guò)一個(gè)很典型的問(wèn)題某驅(qū)動(dòng)的接收路徑每包都映射一次小緩沖區(qū)日志里 unmap 次數(shù)比 map 少了不到千分之一??粗黄鹧鄣钠钆芤煌砩暇屠鄯e出幾萬(wàn)個(gè)沒(méi)回收的 IOVA最后分配大塊連續(xù)區(qū)間失敗。查出來(lái)是錯(cuò)誤分支上少寫(xiě)了一行 unmap。4.3 觀察點(diǎn)整理從文件系統(tǒng)到日志觀察目標(biāo)位置關(guān)鍵信息設(shè)備分組情況/sys/kernel/iommu_groups/哪些設(shè)備共用一套翻譯能否獨(dú)立直通設(shè)備當(dāng)前域狀態(tài)/sys/kernel/debug/iommu/devices/域類(lèi)型、是否身份映射、ATS 狀態(tài)每次映射的詳細(xì)參數(shù)events/iommu/下的 tracepointIOVA、物理地址、長(zhǎng)度、權(quán)限缺頁(yè)與訪問(wèn)越權(quán)內(nèi)核日志設(shè)備標(biāo)識(shí)、出錯(cuò)地址、訪問(wèn)類(lèi)型映射泄漏與誤用內(nèi)核日志需開(kāi) DMA API 調(diào)試長(zhǎng)度不一致、方向不一致、提前釋放這張表建議存下來(lái)。我排查 IOMMU 相關(guān)問(wèn)題基本就是從上往下過(guò)一遍先看分組對(duì)不對(duì)再看域建沒(méi)建再看映射有沒(méi)有異常最后看有沒(méi)有硬件報(bào)錯(cuò)順序亂了容易鉆牛角尖。4.4 嚴(yán)格模式與延遲模式的實(shí)際代價(jià)性能這塊給個(gè)量級(jí)參考具體數(shù)字跟平臺(tái)強(qiáng)相關(guān)別照抄。在一臺(tái)普通雙路服務(wù)器上跑萬(wàn)兆網(wǎng)卡的小包收發(fā)嚴(yán)格模式下每個(gè)包一次映射一次失效吞吐比延遲模式低一截某些配置下差距比較明顯換成大包或者批量提交的場(chǎng)景差距迅速縮小因?yàn)槭е噶畹臄偙⌒?yīng)出來(lái)了。我的建議是分環(huán)境對(duì)待開(kāi)發(fā)和功能驗(yàn)證階段一律開(kāi)嚴(yán)格模式讓錯(cuò)誤盡早暴露生產(chǎn)環(huán)境如果壓測(cè)通過(guò)、驅(qū)動(dòng)行為規(guī)范可以切延遲模式換性能。但前提是驅(qū)動(dòng)真的守規(guī)矩——所有 DMA 緩沖區(qū)的生命周期都有明確的同步點(diǎn)。我見(jiàn)過(guò)為了追性能直接切延遲模式結(jié)果把一個(gè)本來(lái)就存在的時(shí)序 bug 放大了十倍的情況那種排查成本完全不值得。提示切模式之前先做一輪長(zhǎng)時(shí)間壓力測(cè)試重點(diǎn)看接收方向的數(shù)據(jù)完整性這個(gè)方向的問(wèn)題最容易被延遲失效掩蓋。5. 常見(jiàn)問(wèn)題與排查速查表5.1 三類(lèi)典型故障的定位路徑第一類(lèi)是開(kāi)了 IOMMU 之后設(shè)備直接不工作。現(xiàn)象是驅(qū)動(dòng) probe 成功但第一次 DMA 就報(bào)錯(cuò)或者數(shù)據(jù)全零。先查這個(gè)設(shè)備的 DMA 地址寬度如果驅(qū)動(dòng)把dma_mask設(shè)成了 32 位而設(shè)備的實(shí)際緩沖區(qū)又申請(qǐng)?jiān)?4GB 以上映射就會(huì)失敗或者觸發(fā) bounce buffer。其次查設(shè)備是否被分到了一個(gè)奇怪的 group導(dǎo)致它的域里沒(méi)有預(yù)期的映射。最后確認(rèn)固件描述里這個(gè)設(shè)備確實(shí)歸 IOMMU 管——有些平臺(tái)對(duì)某些根端口默認(rèn)不做翻譯。第二類(lèi)是跑一段時(shí)間后隨機(jī)出錯(cuò)。這是最典型的延遲失效或者生命周期問(wèn)題。把嚴(yán)格模式打開(kāi)復(fù)現(xiàn)一次如果嚴(yán)格模式下問(wèn)題消失基本可以確定是失效時(shí)機(jī)或者緩沖區(qū)回收時(shí)機(jī)的問(wèn)題。再用 tracepoint 對(duì)齊 map/unmap 的時(shí)間戳和業(yè)務(wù)日志找出哪個(gè)緩沖區(qū)的釋放早于設(shè)備停止訪問(wèn)。第三類(lèi)是性能比不開(kāi) IOMMU 差很多。先分清是映射開(kāi)銷(xiāo)還是失效開(kāi)銷(xiāo)。映射開(kāi)銷(xiāo)大的特征是 CPU 占用高、映射次數(shù)多優(yōu)化方向是合并小映射、用散列表批量映射、復(fù)用緩沖區(qū)失效開(kāi)銷(xiāo)大的特征是延遲模式的收益明顯優(yōu)化方向是調(diào)整同步粒度。還有一種容易被忽略的情況映射時(shí)頁(yè)大小沒(méi)湊上大頁(yè)全是 4KB 頁(yè)導(dǎo)致 TLB 壓力大這個(gè)從 tracepoint 的長(zhǎng)度分布能看出來(lái)。5.2 排查速查表現(xiàn)象可能原因快速驗(yàn)證處理方式第一次 DMA 就失敗地址寬度設(shè)置過(guò)窄打印dma_mask與緩沖區(qū)地址按設(shè)備能力放開(kāi)到 48 位或更高數(shù)據(jù)隨機(jī)錯(cuò)亂提前 unmap 或延遲失效切嚴(yán)格模式復(fù)現(xiàn)補(bǔ)同步點(diǎn)確認(rèn)設(shè)備停止后再釋放運(yùn)行數(shù)小時(shí)后分配失敗IOVA 泄漏對(duì)比 map/unmap 計(jì)數(shù)檢查所有錯(cuò)誤分支的回收映射成功但設(shè)備讀到舊數(shù)據(jù)CPU 緩存未刷單步調(diào)試確認(rèn) map 前的操作檢查方向參數(shù)是否傳錯(cuò)設(shè)備訪問(wèn)直接觸發(fā) fault映射范圍沒(méi)覆蓋全看 fault 日志里的地址檢查長(zhǎng)度與頁(yè)對(duì)齊處理想直通卻整組被牽連group 內(nèi)設(shè)備不可分查分組目錄提前占住同組其他設(shè)備性能斷崖式下跌全小頁(yè)映射或嚴(yán)格模式看映射長(zhǎng)度分布合并映射、調(diào)整模式5.3 幾個(gè)只有踩過(guò)才知道的細(xì)節(jié)邊界頁(yè)的處理。你傳進(jìn)去的緩沖區(qū)地址沒(méi)對(duì)齊、長(zhǎng)度也不是頁(yè)整數(shù)倍時(shí)dma-iommu這層需要保證覆蓋范圍。它可能會(huì)對(duì)首尾頁(yè)做特殊處理也可能直接要求調(diào)用方保證對(duì)齊。不同版本的策略有差異穩(wěn)妥的做法是自己在驅(qū)動(dòng)里就按頁(yè)對(duì)齊申請(qǐng)緩沖區(qū)別把麻煩丟給底層。我吃過(guò)一次虧一個(gè) 64 字節(jié)的映射跨越了頁(yè)邊界尾部那一小段落到了相鄰的映射區(qū)域里兩個(gè)設(shè)備互相踩查了兩天才定位。方向參數(shù)不是裝飾品。傳錯(cuò)方向不會(huì)立刻報(bào)錯(cuò)但頁(yè)表權(quán)限建錯(cuò)了某些平臺(tái)上設(shè)備會(huì)收到訪問(wèn)拒絕某些平臺(tái)上則完全不報(bào)錯(cuò)只是緩存維護(hù)做反了。后一種最要命表現(xiàn)為數(shù)據(jù)延遲一個(gè)周期才更新看起來(lái)像硬件時(shí)序問(wèn)題。身份映射不等于沒(méi)開(kāi)。iommupt或者某些驅(qū)動(dòng)主動(dòng)請(qǐng)求身份映射時(shí)設(shè)備看到的就是物理地址隔離性基本沒(méi)有但翻譯鏈路還在。排查時(shí)別看到地址相等就斷定 IOMMU 沒(méi)工作得去看域類(lèi)型。熱插拔設(shè)備要重新看分組。上電順序不同固件給的分組可能不一樣一個(gè)槽位冷啟動(dòng)和熱插拔之后可能落到不同的組里。做直通方案時(shí)不能假設(shè)分組固定。調(diào)試階段把 DMA API 調(diào)試選項(xiàng)打開(kāi)。這個(gè)選項(xiàng)會(huì)額外檢查每次映射和釋放的參數(shù)一致性代價(jià)是性能下降但能在開(kāi)發(fā)期抓出大量低級(jí)錯(cuò)誤包括長(zhǎng)度不一致、方向不一致、重復(fù)釋放。上線前關(guān)掉即可。我在實(shí)際使用中的體會(huì)是IOMMU 這層?xùn)|西的價(jià)值不在于它有多復(fù)雜而在于它把設(shè)備的錯(cuò)誤行為從不可觀測(cè)變成了可觀測(cè)。以前遇到 DMA 越界只能靠縮小范圍、加內(nèi)存屏障、打點(diǎn)猜現(xiàn)在一行 fault 日志直接告訴你是哪個(gè)設(shè)備、訪問(wèn)了哪個(gè)地址、是讀還是寫(xiě)。把第 4 節(jié)那幾個(gè)觀察點(diǎn)用熟絕大部分和 DMA 相關(guān)的疑難雜癥都能在半小時(shí)內(nèi)鎖定方向剩下的只是修驅(qū)動(dòng)的問(wèn)題。