以下文章來源于AdriftCoreFPGA芯研社,作者CNL中子
前言
我們習(xí)慣了用 Verilog 去死磕 PCIe 的底層協(xié)議狀態(tài)機(jī)。但一旦越過硬件邊界來到操作系統(tǒng)層面,Linux 內(nèi)核是如何接管并驅(qū)動這些 PCI/PCIe 設(shè)備的呢?由于不同的 CPU 架構(gòu)實(shí)現(xiàn)了各異的芯片組,加上各種 PCI 設(shè)備自身獨(dú)特的功能需求,Linux 內(nèi)核中的 PCI 支持遠(yuǎn)比我們希望的要復(fù)雜得多。今天這篇文章,我們將從驅(qū)動開發(fā)的視角,梳理 Linux PCI 設(shè)備驅(qū)動的核心生命周期與關(guān)鍵 API。
驅(qū)動注冊和發(fā)現(xiàn)
在 Linux 中,PCI 驅(qū)動程序通過pci_register_driver()在系統(tǒng)中發(fā)現(xiàn)設(shè)備。但實(shí)際上,這個過程是反向的:當(dāng) PCI 通用代碼發(fā)現(xiàn)了一個新設(shè)備時,具有匹配描述的驅(qū)動程序才會被內(nèi)核通知 。
系統(tǒng)上電或有 PCIe 設(shè)備熱插拔時,底層的總線枚舉其實(shí)已經(jīng)完成了。Linux 的 PCI 通用代碼會去掃描物理總線,讀取每個設(shè)備的配置空間(拿到 Vendor ID, Device ID 等),把系統(tǒng)里所有的硬件設(shè)備都登記在自己的花名冊上。這時候,你的驅(qū)動程序可能還沒加載進(jìn)系統(tǒng)。
當(dāng)你的驅(qū)動程序跑起來,調(diào)用pci_register_driver()進(jìn)行注冊時,它并不是去物理總線上找設(shè)備。相反,它只是向內(nèi)核提交了一份匹配描述(也就是包含它能支持的 Vendor ID 和 Device ID 的id_table)。
內(nèi)核會拿著你提交的匹配描述,去自己早就登記好的設(shè)備花名冊里比對。一旦內(nèi)核發(fā)現(xiàn):總線上有個硬件設(shè)備的 ID,剛好和這個驅(qū)動要求的 ID 對上了;內(nèi)核就會主動觸發(fā)(通知)驅(qū)動程序里寫好的probe探測函數(shù),并把指向該硬件設(shè)備的指針(struct pci_dev *)塞給驅(qū)動 。
staticstructpci_drivermy_pci_driver={ .name ="my_pci_driver", .id_table = my_driver_id_table, .probe = my_probe_function, .remove = my_remove_function, };
匹配描述
在傳給注冊函數(shù)pci_register_driver()的struct pci_driver結(jié)構(gòu)體中,有一個名為id_table的字段,它就是一個指向驅(qū)動程序感興趣的設(shè)備 ID 表的指針。
這個 ID 表是一個struct pci_device_id類型的數(shù)組,并且必須以一個全零的條目作為結(jié)束標(biāo)志。通常建議將其定義為static const。
在實(shí)際寫代碼時,你不需要手動去填充上面所有的字段。大多數(shù)驅(qū)動程序只需要使用宏P(guān)CI_DEVICE()或PCI_DEVICE_CLASS()就可以非常方便地設(shè)置pci_device_id表了 。
staticconststructpci_device_idmy_driver_id_table[] ={
{ PCI_DEVICE(0x10EC,0x8168) },/* 使用宏,只匹配具體的 Vendor ID 和 Device ID */
{0, }/* 必須以全 0 結(jié)尾,告訴內(nèi)核數(shù)組到此為止 */
};
MODULE_DEVICE_TABLE(pci, my_driver_id_table);/* 導(dǎo)出表 */
staticstructpci_drivermy_pci_driver={
...
.id_table = my_driver_id_table,
...
};
驅(qū)動名稱(name)
這是驅(qū)動程序必不可少的身份標(biāo)識。當(dāng)你注冊驅(qū)動后,內(nèi)核會使用這個名字在 sysfs 文件系統(tǒng)中創(chuàng)建對應(yīng)的目錄(例如/sys/bus/pci/drivers/AdriftCorePCIe/)。后續(xù)如果在運(yùn)行時動態(tài)添加新的設(shè)備 ID 到驅(qū)動中,也會用到這個名字對應(yīng)的路徑。
#defineDEVICE_NAME"AdriftCorePCIe"
staticstructpci_drivermy_pci_driver={
.name = DEVICE_NAME
...
...
};
探測與初始化(probe)
這是驅(qū)動認(rèn)領(lǐng)設(shè)備的入口。
當(dāng)內(nèi)核發(fā)現(xiàn)了一個與你的驅(qū)動程序id_table匹配的、且尚未被其他驅(qū)動占有的 PCI 設(shè)備時,就會調(diào)用這個探測函數(shù)。這可能發(fā)生在執(zhí)行pci_register_driver()的過程中(如果設(shè)備已經(jīng)存在),或者在稍后插入新設(shè)備時觸發(fā)。
在這個階段,probe的核心動作包括:
接收設(shè)備指針:內(nèi)核會為每一個 ID 表匹配的設(shè)備,將一個struct pci_dev *指針傳遞給該函數(shù)。
決定是否接管:如果驅(qū)動程序決定接受并獲取該設(shè)備的所有權(quán),probe必須返回零;如果無法接管,則返回一個負(fù)數(shù)的錯誤碼。
允許睡眠上下文:probe函數(shù)始終在進(jìn)程上下文 (process context) 中被調(diào)用,因此它是允許睡眠的。這對于后續(xù)申請內(nèi)存或長時間等待硬件就緒非常關(guān)鍵。
一旦probe決定接管設(shè)備并獲得了所有權(quán),驅(qū)動程序通常需要在這個函數(shù)內(nèi)部執(zhí)行以下標(biāo)準(zhǔn)的初始化步驟:
啟用設(shè)備 (Enable the device)
請求 MMIO/IOP 資源 (Request MMIO/IOP resources)
設(shè)置 DMA 掩碼大小 (Set the DMA mask size):包含一致性 (coherent) 和流式 (streaming) DMA
分配并初始化共享控制數(shù)據(jù) (Allocate and initialize shared control data):通常使用pci_allocate_coherent()
訪問設(shè)備配置空間 (Access device configuration space):在需要的情況下執(zhí)行
注冊 IRQ 處理程序 (Register IRQ handler):通過request_irq()完成
初始化非 PCI 部分 (Initialize non-PCI):例如初始化芯片中的 LAN、SCSI 等特定功能模塊
啟用 DMA/處理引擎 (Enable DMA/processing engines)
probe就是你的驅(qū)動程序正式登臺亮相的地方。內(nèi)核把匹配的硬件交到它手上,它負(fù)責(zé)把這塊硬件通電、申請資源、配中斷、設(shè) DMA,最終完成點(diǎn)亮,讓系統(tǒng)能夠真正使用這塊硬件。
/* * 2. Probe 函數(shù):設(shè)備的接管與點(diǎn)亮
* 該函數(shù)在進(jìn)程上下文中調(diào)用,允許睡眠。
*/
staticintmy_pci_probe(structpci_dev *pdev,conststructpci_device_id *id)
{
interr;
/* 2.1 啟用 PCI 設(shè)備 (喚醒設(shè)備、分配資源、分配 IRQ 等) */
err = pci_enable_device(pdev);
if(err) {
dev_err(&pdev->dev,"Failed to enable PCI device
");
returnerr;/* 接管失敗,返回負(fù)數(shù)錯誤碼 */
}
/* 2.2 請求 MMIO/IOP 資源,防止與其他設(shè)備發(fā)生地址沖突 */
/* 注意:現(xiàn)代內(nèi)核通常使用 pci_request_regions 包裝函數(shù) */
err = pci_request_regions(pdev,"my_pci_driver");
if(err) {
dev_err(&pdev->dev,"Failed to request PCI regions
");
gotoerr_disable_device;
}
/* 2.3 設(shè)置 DMA 掩碼:聲明設(shè)備的 DMA 尋址能力 (例如支持 64 位 DMA) */
err = pci_set_dma_mask(pdev, DMA_BIT_MASK(64));
if(err) {
dev_err(&pdev->dev,"No suitable DMA available
");
gotoerr_release_regions;
}
pci_set_consistent_dma_mask(pdev, DMA_BIT_MASK(64));
/* 2.4 啟用 DMA 總線主控模式 (設(shè)置 PCI_COMMAND 寄存器中的 Bus Master 位) */
pci_set_master(pdev);
/* * 后續(xù)初始化步驟(偽代碼):
* - 映射 MMIO 寄存器空間 (pci_iomap)
* - 配置 MSI/MSI-X 中斷 (pci_alloc_irq_vectors)
* - 注冊中斷處理程序 (request_irq)
* - 初始化你的 DPU/硬件引擎狀態(tài)機(jī)
*/
dev_info(&pdev->dev,"PCI device probed successfully!
");
return0;/* 成功接管設(shè)備,返回 0 */
err_release_regions:
pci_release_regions(pdev);
err_disable_device:
pci_disable_device(pdev);
returnerr;
}
設(shè)備的正確關(guān)閉與清理
如果說probe是驅(qū)動接管設(shè)備的入場儀式,那么remove就是它的優(yōu)雅退場與資源回收大管家。
在內(nèi)核的生命周期中,remove的主要職責(zé)就是完全反向執(zhí)行probe中所做的一切,確保設(shè)備被安全關(guān)閉,并且所有占用的系統(tǒng)資源都被徹底釋放,不留任何內(nèi)存泄漏或?qū)е孪到y(tǒng)崩潰的隱患。
remove()函數(shù)會在由該驅(qū)動程序處理的設(shè)備被移除時調(diào)用。這通常發(fā)生在兩種場景:
驅(qū)動被注銷時:比如當(dāng)驅(qū)動程序退出(執(zhí)行pci_unregister_driver()),或者你通過命令行(如rmmod)卸載驅(qū)動模塊時,PCI 層會自動為該驅(qū)動處理的所有設(shè)備調(diào)用remove鉤子。
設(shè)備被物理拔出時:當(dāng)設(shè)備從支持熱插拔 (hot-pluggable) 的插槽中被手動拔出時。
和probe一樣,remove函數(shù)始終在進(jìn)程上下文 (process context)中被調(diào)用,因此它是允許睡眠 (sleep) 的。
當(dāng)模塊需要被卸載或者設(shè)備不再使用時,remove函數(shù)通常需要嚴(yán)格按照以下步驟進(jìn)行清理:
禁用設(shè)備生成中斷 (Disable the device from generating IRQs):這是第一步,必須阻止芯片產(chǎn)生新的中斷。如果不做這一步,且中斷號是與其他設(shè)備共享的,可能會引發(fā)致命的尖叫中斷 (screaming interrupt)問題 。
釋放 IRQ (Release the IRQ):調(diào)用free_irq()來注銷中斷處理程序。
停止所有 DMA 活動 (Stop all DMA activity):在嘗試釋放 DMA 控制數(shù)據(jù)之前,停止所有 DMA 操作極其重要。如果未能停止 DMA 就直接釋放內(nèi)存,可能會導(dǎo)致內(nèi)存損壞、系統(tǒng)掛起,甚至在某些芯片組上發(fā)生硬崩潰 ^^。
釋放 DMA 緩沖區(qū) (Release DMA buffers):包括流式 (streaming) 和一致性 (coherent) DMA 緩沖區(qū)的清理與解除映射。
從其他子系統(tǒng)注銷 (Unregister from other subsystems):比如解綁相關(guān)的 SCSI 或網(wǎng)絡(luò)設(shè)備 (netdev)。
禁用設(shè)備及釋放區(qū)域:
? 禁用設(shè)備對 MMIO/IO 端口地址的響應(yīng)。
? 釋放 MMIO/IOP 資源。
/* * 3. Remove 函數(shù):設(shè)備的優(yōu)雅退場與資源回收
* 必須嚴(yán)格反向執(zhí)行 probe 中的分配步驟。
*/
staticvoidmy_pci_remove(structpci_dev *pdev)
{
/* * 卸載前期的關(guān)鍵清理(偽代碼):
* - 停止設(shè)備側(cè)的數(shù)據(jù)收發(fā)與引擎運(yùn)轉(zhuǎn)
* - 停止設(shè)備產(chǎn)生中斷,并釋放 IRQ (free_irq)
* - 停止所有 DMA 活動,釋放 DMA 緩沖區(qū)
* - 解除 MMIO 空間映射 (pci_iounmap)
*/
/* 3.1 釋放 MMIO/IOP 資源區(qū)域 */
pci_release_regions(pdev);
/* 3.2 禁用 PCI 設(shè)備響應(yīng),與 pci_enable_device 對稱相反 */
pci_disable_device(pdev);
dev_info(&pdev->dev,"PCI device removed successfully.
");
}
remove就是負(fù)責(zé)擦屁股的。它必須嚴(yán)絲合縫地把probe里申請的內(nèi)存還給系統(tǒng),把注冊的中斷注銷掉,把開啟的 DMA 停下來,最后讓硬件安安靜靜地進(jìn)入關(guān)閉狀態(tài)。
整個驅(qū)動模塊的執(zhí)行
在 Linux 內(nèi)核驅(qū)動的架構(gòu)中,如果把 probe 和 remove 比作針對單個具體 PCIe 硬件的上崗和下崗,那么module_init和module_exit就是整個驅(qū)動程序模塊本身的出生和消亡。
在早期的內(nèi)核代碼中,你通常需要手動編寫這兩個函數(shù),看起來像這樣:
staticint__initmy_pci_init(void)
{
/* 向內(nèi)核的 PCI 核心注冊你的驅(qū)動結(jié)構(gòu)體 */
returnpci_register_driver(&my_pci_driver);
}
staticvoid__exitmy_pci_exit(void)
{
/* 注銷驅(qū)動,這會自動觸發(fā)所有已接管設(shè)備的 remove 函數(shù) */
pci_unregister_driver(&my_pci_driver);
}
module_init(my_pci_init);
module_exit(my_pci_exit);
驅(qū)動的出生與注冊
當(dāng)你通過 insmod 或 modprobe 命令將編譯好的 .ko 驅(qū)動模塊加載到內(nèi)核時,系統(tǒng)第一個調(diào)用的就是 module_init 宏指定的初始化函數(shù)。
在 PCIe 驅(qū)動中,它主要干一件事:向內(nèi)核注冊自己。
PCI 設(shè)備驅(qū)動程序會在其初始化期間調(diào)用 pci_register_driver(),并傳入指向描述該驅(qū)動程序的結(jié)構(gòu)體(struct pci_driver)的指針 。這就好比去內(nèi)核那里掛號,把自己的 id_table、probe 和 remove 提交給內(nèi)核。
注:module_init() 函數(shù)(以及僅由它調(diào)用的所有初始化函數(shù))應(yīng)該被標(biāo)記為 __init 屬性 。這個屬性非常巧妙,它告訴內(nèi)核:這段初始化代碼在驅(qū)動完成初始化后就可以被直接丟棄,從而節(jié)省寶貴的內(nèi)核內(nèi)存空間 。
驅(qū)動的消亡與注銷
當(dāng)你通過rmmod命令卸載驅(qū)動模塊時,內(nèi)核會調(diào)用module_exit宏指定的退出函數(shù)。
在 PCIe 驅(qū)動中,它的核心任務(wù)是:向內(nèi)核注銷自己,并引發(fā)連鎖清理。
調(diào)用核心 API:當(dāng)驅(qū)動程序退出時,它只需調(diào)用pci_unregister_driver()。
連鎖觸發(fā)remove:這是非常省心的一點(diǎn)。當(dāng)你調(diào)用注銷函數(shù)后,PCI 層會自動去尋找所有目前正由該驅(qū)動程序處理的設(shè)備,并自動為它們逐一調(diào)用remove鉤子函數(shù)。這意味著你不需要在module_exit里手動寫循環(huán)去清理設(shè)備,內(nèi)核全幫你包辦了。
屬性標(biāo)記:與初始化對應(yīng),退出函數(shù)應(yīng)該被標(biāo)記為__exit屬性。對于那些并非以模塊形式動態(tài)加載,而是直接編譯進(jìn)內(nèi)核鏡像(非模塊化)的驅(qū)動來說,帶有__exit標(biāo)記的代碼會被直接忽略,因?yàn)樗肋h(yuǎn)不會被卸載。
寫在最后
習(xí)慣了使用 Verilog 雕琢 PCIe 的底層狀態(tài)機(jī),再回過頭來看看 Linux 內(nèi)核是如何以軟件的視角接管這些硬件的,是一件非常有趣的事情。
正如我們在文中所看到的,Linux 并沒有讓驅(qū)動程序去干滿大街找設(shè)備的臟活累活。相反,它構(gòu)建了一個極其優(yōu)雅的總線-設(shè)備-驅(qū)動模型:底層總線負(fù)責(zé)枚舉和登記(發(fā)現(xiàn)硬件),驅(qū)動模塊負(fù)責(zé)提交自己的匹配描述并注冊(module_init與id_table),而內(nèi)核的 PCI Core 則扮演了紅娘的角色,精準(zhǔn)地將匹配的設(shè)備交到驅(qū)動的probe函數(shù)手中進(jìn)行點(diǎn)亮,最后在卸載時通過remove和module_exit實(shí)現(xiàn)系統(tǒng)資源的完美回收。
從 RTL 側(cè)的代碼邏輯,跨越物理層與鏈路層,最終到達(dá)操作系統(tǒng)的驅(qū)動框架,這種從底至頂?shù)耐暾暯?,能讓我們在遇到?fù)雜的系統(tǒng)級 Bug 時擁有更清晰的排查思路。硬件不只是冷冰冰的寄存器,軟件也不只是虛無縹緲的指針,它們的完美交匯,才是系統(tǒng)穩(wěn)定運(yùn)行的基石。
-
內(nèi)核
+關(guān)注
關(guān)注
4文章
1476瀏覽量
43098 -
Linux
+關(guān)注
關(guān)注
88文章
11821瀏覽量
219598 -
PCIe
+關(guān)注
關(guān)注
16文章
1483瀏覽量
88962 -
狀態(tài)機(jī)
+關(guān)注
關(guān)注
2文章
501瀏覽量
29351
原文標(biāo)題:漫談PCIe之如何理解PCIe驅(qū)動
文章出處:【微信號:HXSLH1010101010,微信公眾號:FPGA技術(shù)江湖】歡迎添加關(guān)注!文章轉(zhuǎn)載請注明出處。
發(fā)布評論請先 登錄
Linux內(nèi)核中container_of原理詳解
請問DVR RDK自帶的Linux內(nèi)核已經(jīng)包含了PCIE相關(guān)的驅(qū)動嗎?請問有沒有PC和8168通過PCIE進(jìn)行通信的例子?
在Linux內(nèi)核中添加wifi驅(qū)動
Linux的內(nèi)核教程
基于Linux內(nèi)核輸入子系統(tǒng)的驅(qū)動研究
基于Xilinx PCIe例程附帶Linux驅(qū)動的修改
快速理解什么是Linux內(nèi)核以及Linux內(nèi)核的內(nèi)容
如何使用Linux內(nèi)核實(shí)現(xiàn)USB驅(qū)動程序框架
linux內(nèi)核中的driver_register介紹
linux驅(qū)動程序如何加載進(jìn)內(nèi)核
linux內(nèi)核中通用HID觸摸驅(qū)動
如何理解Linux內(nèi)核中的PCIe驅(qū)動
評論