名詞縮寫:
API 應用程序接口(Application Program Interface )
ABI 應用系統二進制接口(Application Binary Interface)
設備驅動是操作系統的一部分,它能夠通過一些特定的編程接口便于硬件設備的使用,這樣軟件就可以控制并且運行那些設備了。因為每個驅動都對應不同的操作系統,所以你就需要不同的 Linux、Windows 或 Unix 設備驅動,以便能夠在不同的計算機上使用你的設備。這就是為什么當你雇傭一個驅動開發者或者選擇一個研發服務商提供者的時候,查看他們為各種操作系統平臺開發驅動的經驗是非常重要的。
驅動開發的第一步是理解每個操作系統處理它的驅動的不同方式、底層驅動模型、它使用的架構、以及可用的開發工具。例如,Linux 驅動程序模型就與 Windows 非常不同。雖然 Windows 提倡驅動程序開發和操作系統開發分別進行,并通過一組 ABI 調用來結合驅動程序和操作系統,但是 Linux 設備驅動程序開發不依賴任何穩定的 ABI 或 API,所以它的驅動代碼并沒有被納入內核中。每一種模型都有自己的優點和缺點,但是如果你想為你的設備提供全面支持,那么重要的是要全面的了解它們。
在本文中,我們將比較 Windows 和 Linux 設備驅動程序,探索不同的架構,API,構建開發和分發,希望讓您比較深入的理解如何開始為每一個操作系統編寫設備驅動程序。
1. 設備驅動架構
Windows 設備驅動程序的體系結構和 Linux 中使用的不同,它們各有優缺點。差異主要受以下原因的影響:Windows 是閉源操作系統,而 Linux 是開源操作系統。比較 Linux 和 Windows 設備驅動程序架構將幫助我們理解 Windows 和 Linux 驅動程序背后的核心差異。
1.1. Windows 驅動架構
雖然 Linux 內核分發時帶著 Linux 驅動,而 Windows 內核則不包括設備驅動程序。與之不同的是,現代 Windows 設備驅動程序編寫使用 Windows 驅動模型(WDM),這是一種完全支持即插即用和電源管理的模型,所以可以根據需要加載和卸載驅動程序。
處理來自應用的請求,是由 Windows 內核的中被稱為 I/O 管理器的部分來完成的。I/O 管理器的作用是是轉換這些請求到 I/O 請求數據包(IO Request Packets)(IRP),IRP 可以被用來在驅動層識別請求并且傳輸數據。
Windows 驅動模型 WDM 提供三種驅動, 它們形成了三個層:
過濾(Filter)驅動提供關于 IRP 的可選附加處理。
功能(Function)驅動是實現接口和每個設備通信的主要驅動。
總線(Bus)驅動服務不同的配適器和不同的總線控制器,來實現主機模式控制設備。
一個 IRP 通過這些層就像它們經過 I/O 管理器到達底層硬件那樣。每個層能夠獨立的處理一個 IRP 并且把它們送回 I/O 管理器。在硬件底層中有硬件抽象層(HAL),它提供一個通用的接口到物理設備。
1.2. Linux 驅動架構
相比于 Windows 設備驅動,Linux 設備驅動架構根本性的不同就是 Linux 沒有一個標準的驅動模型也沒有一個干凈分隔的層。每一個設備驅動都被當做一個能夠自動的從內核中加載和卸載的模塊來實現。Linux 為即插即用設備和電源管理設備提供一些方式,以便那些驅動可以使用它們來正確地管理這些設備,但這并不是必須的。
模式輸出那些它們提供的函數,并通過調用這些函數和傳入隨意定義的數據結構來溝通。請求來自文件系統或網絡層的用戶應用,并被轉化為需要的數據結構。模塊能夠按層堆疊,在一個模塊進行處理之后,另外一個再處理,有些模塊提供了對一類設備的公共調用接口,例如 USB 設備。
Linux 設備驅動程序支持三種設備:
實現一個字節流接口的字符(Character)設備。
用于存放文件系統和處理多字節數據塊 IO 的塊(Block)設備。
用于通過網絡傳輸數據包的網絡(Network)接口。
Linux 也有一個硬件抽象層(HAL),它實際扮演了物理硬件的設備驅動接口。
2. 設備驅動 API
Linux 和 Windows 驅動 API 都屬于事件驅動類型:只有當某些事件發生的時候,驅動代碼才執行——當用戶的應用程序希望從設備獲取一些東西,或者當設備有某些請求需要告知操作系統。
2.1. 初始化
在 Windows 上,驅動被表示為 DriverObject 結構,它在 DriverEntry 函數的執行過程中被初始化。這些入口點也注冊一些回調函數,用來響應設備的添加和移除、驅動卸載和處理新進入的 IRP。當一個設備連接的時候,Windows 創建一個設備對象,這個設備對象在設備驅動后面處理所有應用請求。
相比于 Windows,Linux 設備驅動生命周期由內核模塊的 module_init 和 module_exit 函數負責管理,它們分別用于模塊的加載和卸載。它們負責注冊模塊來通過使用內核接口來處理設備的請求。這個模塊需要創建一個設備文件(或者一個網絡接口),為其所希望管理的設備指定一個數字識別號,并注冊一些當用戶與設備文件交互的時候所使用的回調函數。
2.2. 命名和聲明設備
在 Windows 上注冊設備
Windows 設備驅動在新連接設備時是由回調函數 AddDevice 通知的。它接下來就去創建一個設備對象(device object),用于識別該設備的特定的驅動實例。取決于驅動的類型,設備對象可以是物理設備對象(Physical Device Object)(PDO),功能設備對象(Function Device Object)(FDO),或者過濾設備對象(Filter Device Object )(FIDO)。設備對象能夠堆疊,PDO 在底層。
設備對象在這個設備連接在計算機期間一直存在。DeviceExtension 結構能夠被用于關聯到一個設備對象的全局數據。
設備對象可以有如下形式的名字 DeviceDeviceName,這被系統用來識別和定位它們。應用可以使用 CreateFile API 函數來打開一個有上述名字的文件,獲得一個可以用于和設備交互的句柄。
然而,通常只有 PDO 有自己的名字。未命名的設備能夠通過設備級接口來訪問。設備驅動注冊一個或多個接口,以 128 位全局唯一標識符(GUID)來標示它們。用戶應用能夠使用已知的 GUID 來獲取一個設備的句柄。
在 Linux 上注冊設備
在 Linux 平臺上,用戶應用通過文件系統入口訪問設備,它通常位于 /dev 目錄。在模塊初始化的時候,它通過調用內核函數 register_chrdev 創建了所有需要的入口。應用可以發起 open 系統調用來獲取一個文件描述符來與設備進行交互。這個調用后來被發送到回調函數,這個調用(以及將來對該返回的文件描述符的進一步調用,例如 read、write 或close)會被分配到由該模塊安裝到 file_operations 或者 block_device_operations這樣的數據結構中的回調函數。
設備驅動模塊負責分配和保持任何需要用于操作的數據結構。傳送進文件系統回調函數的 file 結構有一個 private_data 字段,它可以被用來存放指向具體驅動數據的指針。塊設備和網絡接口 API 也提供類似的字段。
雖然應用使用文件系統的節點來定位設備,但是 Linux 在內部使用一個主設備號(major numbers)和次設備號(minor numbers)的概念來識別設備及其驅動。主設備號被用來識別設備驅動,而次設備號由驅動使用來識別它所管理的設備。驅動為了去管理一個或多個固定的主設備號,必須首先注冊自己或者讓系統來分配未使用的設備號給它。
目前,Linux 為主次設備對(major-minor pairs)使用一個 32 位的值,其中 12 位分配主設備號,并允許多達 4096 個不同的設備。主次設備對對于字符設備和塊設備是不同的,所以一個字符設備和一個塊設備能使用相同的設備對而不導致沖突。網絡接口是通過像 eth0 的符號名來識別,這些又是區別于主次設備的字符設備和塊設備的。
2.3. 交換數據
Linux 和 Windows 都支持在用戶級應用程序和內核級驅動程序之間傳輸數據的三種方式:
緩沖型輸入輸出(Buffered Input-Output)它使用由內核管理的緩沖區。對于寫操作,內核從用戶空間緩沖區中拷貝數據到內核分配的緩沖區,并且把它傳送到設備驅動中。讀操作也一樣,由內核將數據從內核緩沖區中拷貝到應用提供的緩沖區中。
直接型輸入輸出(Direct Input-Output) 它不使用拷貝功能。代替它的是,內核在物理內存中釘死一塊用戶分配的緩沖區以便它可以一直留在那里,以便在數據傳輸過程中不被交換出去。
內存映射(Memory mapping) 它也能夠由內核管理,這樣內核和用戶空間應用就能夠通過不同的地址訪問同樣的內存頁。
Windows 上的驅動程序 I/O 模式
支持緩沖型 I/O 是 WDM 的內置功能。緩沖區能夠被設備驅動通過在 IRP 結構中的 AssociatedIrp.SystemBuffer 字段訪問。當需要和用戶空間通訊的時候,驅動只需從這個緩沖區中進行讀寫操作。
Windows 上的直接 I/O 由內存描述符列表(memory descriptor lists)(MDL)介導。這種半透明的結構是通過在 IRP 中的 MdlAddress 字段來訪問的。它們被用來定位由用戶應用程序分配的緩沖區的物理地址,并在 I/O 請求期間釘死不動。
在 Windows 上進行數據傳輸的第三個選項稱為 METHOD_NEITHER。 在這種情況下,內核需要傳送用戶空間的輸入輸出緩沖區的虛擬地址給驅動,而不需要確定它們有效或者保證它們映射到一個可以由設備驅動訪問的物理內存地址。設備驅動負責處理這些數據傳輸的細節。
Linux 上的驅動程序 I/O 模式
Linux 提供許多函數例如,clear_user、copy_to_user、strncpy_from_user 和一些其它的用來在內核和用戶內存之間進行緩沖區數據傳輸的函數。這些函數保證了指向數據緩存區指針的有效,并且通過在內存區域之間安全地拷貝數據緩沖區來處理數據傳輸的所有細節。
然而,塊設備的驅動對已知大小的整個數據塊進行操作,它可以在內核和用戶地址區域之間被快速移動而不需要拷貝它們。這種情況是由 Linux 內核來自動處理所有的塊設備驅動。塊請求隊列處理傳送數據塊而不用多余的拷貝,而 Linux 系統調用接口來轉換文件系統請求到塊請求中。
最終,設備驅動能夠從內核地址區域分配一些存儲頁面(不可交換的)并且使用 remap_pfn_range 函數來直接映射這些頁面到用戶進程的地址空間。然后應用能獲取這些緩沖區的虛擬地址并且使用它來和設備驅動交流。
3. 設備驅動開發環境
3.1. 設備驅動框架
Windows 驅動程序工具包
Windows 是一個閉源操作系統。Microsoft 提供 Windows 驅動程序工具包以方便非 Microsoft 供應商開發 Windows 設備驅動。工具包中包含開發、調試、檢驗和打包 Windows 設備驅動等所需的所有內容。
Windows 驅動模型(Windows Driver Model)(WDM)為設備驅動定義了一個干凈的接口框架。Windows 保持這些接口的源代碼和二進制的兼容性。編譯好的 WDM 驅動通常是前向兼容性:也就是說,一個較舊的驅動能夠在沒有重新編譯的情況下在較新的系統上運行,但是它當然不能夠訪問系統提供的新功能。但是,驅動不保證后向兼容性。
Linux 源代碼
和 Windows 相對比,Linux 是一個開源操作系統,因此 Linux 的整個源代碼是用于驅動開發的 SDK。沒有驅動設備的正式框架,但是 Linux 內核包含許多提供了如驅動注冊這樣的通用服務的子系統。這些子系統的接口在內核頭文件中描述。
盡管 Linux 有定義接口,但這些接口在設計上并不穩定。Linux 不提供有關前向和后向兼容的任何保證。設備驅動對于不同的內核版本需要重新編譯。沒有穩定性的保證可以讓 Linux 內核進行快速開發,因為開發人員不必去支持舊的接口,并且能夠使用最好的方法解決手頭的這些問題。
當為 Linux 寫樹內(in-tree)(指當前 Linux 內核開發主干)驅動程序時,這種不斷變化的環境不會造成任何問題,因為它們作為內核源代碼的一部分,與內核本身同步更新。然而,閉源驅動必須單獨開發,并且在樹外(out-of-tree),必須維護它們以支持不同的內核版本。因此,Linux 鼓勵設備驅動程序開發人員在樹內維護他們的驅動。
3.2. 為設備驅動構建系統
Windows 驅動程序工具包為 Microsoft Visual Studio 添加了驅動開發支持,并包括用來構建驅動程序代碼的編譯器。開發 Windows 設備驅動程序與在 IDE 中開發用戶空間應用程序沒有太大的區別。Microsoft 提供了一個企業 Windows 驅動程序工具包,提供了類似于 Linux 命令行的構建環境。
Linux 使用 Makefile 作為樹內和樹外系統設備驅動程序的構建系統。Linux 構建系統非常發達,通常是一個設備驅動程序只需要少數行就產生一個可工作的二進制代碼。開發人員可以使用任何 IDE,只要它可以處理 Linux 源代碼庫和運行 make ,他們也可以很容易地從終端手動編譯驅動程序。
3.3. 文檔支持
Windows 對于驅動程序的開發有良好的文檔支持。Windows 驅動程序工具包包括文檔和示例驅動程序代碼,通過 MSDN 可獲得關于內核接口的大量信息,并存在大量的有關驅動程序開發和 Windows 底層的參考和指南。
Linux 文檔不是描述性的,但整個 Linux 源代碼可供驅動開發人員使用緩解了這一問題。源代碼樹中的 Documentation 目錄描述了一些 Linux 的子系統,但是有幾本書介紹了關于 Linux 設備驅動程序開發和 Linux 內核概覽,它們更詳細。
Linux 沒有提供設備驅動程序的指定樣本,但現有生產級驅動程序的源代碼可用,可以用作開發新設備驅動程序的參考。
3.4. 調試支持
Linux 和 Windows 都有可用于追蹤調試驅動程序代碼的日志機制。在 Windows 上將使用 DbgPrint 函數,而在 Linux 上使用的函數稱為 printk。然而,并不是每個問題都可以通過只使用日志記錄和源代碼來解決。有時斷點更有用,因為它們允許檢查驅動代碼的動態行為。交互式調試對于研究崩潰的原因也是必不可少的。
Windows 通過其內核級調試器 WinDbg 支持交互式調試。這需要通過一個串行端口連接兩臺機器:一臺計算機運行被調試的內核,另一臺運行調試器和控制被調試的操作系統。Windows 驅動程序工具包包括 Windows 內核的調試符號,因此 Windows 的數據結構將在調試器中部分可見。
Linux 還支持通過 KDB 和 KGDB 進行的交互式調試。調試支持可以內置到內核,并可在啟動時啟用。之后,可以直接通過物理鍵盤調試系統,或通過串行端口從另一臺計算機連接到它。KDB 提供了一個簡單的命令行界面,這是唯一的在同一臺機器上來調試內核的方法。然而,KDB 缺乏源代碼級調試支持。KGDB 通過串行端口提供了一個更復雜的接口。它允許使用像 GDB 這樣標準的應用程序調試器來調試 Linux 內核,就像任何其它用戶空間應用程序一樣。
4. 設備驅動分發
4.1. 安裝設備驅動
在 Windows 上安裝的驅動程序,是由被稱為為 INF 的文本文件描述的,通常存儲在 C:WindowsINF 目錄中。這些文件由驅動供應商提供,并且定義哪些設備由該驅動程序服務,哪里可以找到驅動程序的二進制文件,和驅動程序的版本等。
當一個新設備插入計算機時,Windows 通過查看已經安裝的驅動程序并且選擇適當的一個加載。當設備被移除的時候,驅動會自動卸載它。
在 Linux 上,一些驅動被構建到內核中并且保持永久的加載。非必要的驅動被構建為內核模塊,它們通常是存儲在 /lib/modules/kernel-version 目錄中。這個目錄還包含各種配置文件,如 modules.dep,用于描述內核模塊之間的依賴關系。
雖然 Linux 內核可以在自身啟動時加載一些模塊,但通常模塊加載由用戶空間應用程序監督。例如,init 進程可能在系統初始化期間加載一些模塊,udev 守護程序負責跟蹤新插入的設備并為它們加載適當的模塊。
4.2. 更新設備驅動
Windows 為設備驅動程序提供了穩定的二進制接口,因此在某些情況下,無需與系統一起更新驅動程序二進制文件。任何必要的更新由 Windows Update 服務處理,它負責定位、下載和安裝適用于系統的最新版本的驅動程序。
然而,Linux 不提供穩定的二進制接口,因此有必要在每次內核更新時重新編譯和更新所有必需的設備驅動程序。顯然,內置在內核中的設備驅動程序會自動更新,但是樹外模塊會產生輕微的問題。 維護最新的模塊二進制文件的任務通常用 DKMS 來解決:這是一個當安裝新的內核版本時自動重建所有注冊的內核模塊的服務。
4.3. 安全方面的考慮
所有 Windows 設備驅動程序在 Windows 加載它們之前必須被數字簽名。在開發期間可以使用自簽名證書,但是分發給終端用戶的驅動程序包必須使用 Microsoft 信任的有效證書進行簽名。供應商可以從 Microsoft 授權的任何受信任的證書頒發機構獲取軟件出版商證書(Software Publisher Certificate)。然后,此證書由 Microsoft 交叉簽名,并且生成的交叉證書用于在發行之前簽署驅動程序包。
Linux 內核也能配置為在內核模塊被加載前校驗簽名,并禁止不可信的內核模塊。被內核所信任的公鑰集在構建時是固定的,并且是完全可配置的。由內核執行的檢查,這個檢查嚴格性在構建時也是可配置的,范圍從簡單地為不可信模塊發出警告,到拒絕加載有效性可疑的任何東西。
5. 結論
如上所示,Windows 和 Linux 設備驅動程序基礎設施有一些共同點,例如調用 API 的方法,但更多的細節是相當不同的。最突出的差異源于 Windows 是由商業公司開發的封閉源操作系統這個事實。這使得 Windows 上有好的、文檔化的、穩定的驅動 ABI 和正式框架,而在 Linux 上,更多的是源代碼做了一個有益的補充。文檔支持也在 Windows 環境中更加發達,因為 Microsoft 具有維護它所需的資源。
另一方面,Linux 不會使用框架來限制設備驅動程序開發人員,并且內核和產品級設備驅動程序的源代碼可以在需要的時候有所幫助。缺乏接口穩定性也有其作用,因為它意味著最新的設備驅動程序總是使用最新的接口,內核本身承載較小的后向兼容性負擔,這帶來了更干凈的代碼。
了解這些差異以及每個系統的具體情況是為您的設備提供有效的驅動程序開發和支持的關鍵的第一步。我們希望這篇文章對 Windows 和 Linux 設備驅動程序開發做的對比,有助于您理解它們,并在設備驅動程序開發過程的研究中,將此作為一個偉大的起點。