名詞縮寫: api 應用程式接口application program interface abi 應用系統二進制接口application binary interface
裝置驅動是作業系統的一部分,它能夠通過一些特定的程式設計接口便于硬體裝置的使用,這樣軟體就可以控制并且運作那些裝置了。因為每個驅動都對應不同的作業系統,是以你就需要不同的 linux、windows 或 unix 裝置驅動,以便能夠在不同的計算機上使用你的裝置。這就是為什麼當你雇傭一個驅動開發者或者選擇一個研發服務商提供者的時候,檢視他們為各種作業系統平台開發驅動的經驗是非常重要的。
驅動開發的第一步是了解每個作業系統處理它的驅動的不同方式、底層驅動模型、它使用的架構、以及可用的開發工具。例如,linux 驅動程式模型就與 windows 非常不同。雖然 windows 提倡驅動程式開發和作業系統開發分别進行,并通過一組 abi 調用來結合驅動程式和作業系統,但是 linux 裝置驅動程式開發不依賴任何穩定的 abi 或 api,是以它的驅動代碼并沒有被納入核心中。每一種模型都有自己的優點和缺點,但是如果你想為你的裝置提供全面支援,那麼重要的是要全面的了解它們。
在本文中,我們将比較 windows 和 linux 裝置驅動程式,探索不同的架構,api,建構開發和分發,希望讓您比較深入的了解如何開始為每一個作業系統編寫裝置驅動程式。
<a target="_blank"></a>
windows 裝置驅動程式的體系結構和 linux 中使用的不同,它們各有優缺點。差異主要受以下原因的影響:windows 是閉源作業系統,而 linux 是開源作業系統。比較 linux 和 windows 裝置驅動程式架構将幫助我們了解 windows 和 linux 驅動程式背後的核心差異。
雖然 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),它提供一個通用的接口到實體裝置。
相比于 windows 裝置驅動,linux 裝置驅動架構根本性的不同就是 linux 沒有一個标準的驅動模型也沒有一個幹淨分隔的層。每一個裝置驅動都被當做一個能夠自動的從核心中加載和解除安裝的子產品來實作。linux 為即插即用裝置和電源管理裝置提供一些方式,以便那些驅動可以使用它們來正确地管理這些裝置,但這并不是必須的。
模式輸出那些它們提供的函數,并通過調用這些函數和傳入随意定義的資料結構來溝通。請求來自檔案系統或網絡層的使用者應用,并被轉化為需要的資料結構。子產品能夠按層堆疊,在一個子產品進行處理之後,另外一個再處理,有些子產品提供了對一類裝置的公共調用接口,例如 usb 裝置。
linux 裝置驅動程式支援三種裝置:
實作一個位元組流接口的字元character裝置。
用于存放檔案系統和處理多位元組資料塊 io 的塊block裝置。
用于通過網絡傳輸資料包的網絡network接口。
linux 也有一個硬體抽象層(hal),它實際扮演了實體硬體的裝置驅動接口。
linux 和 windows 驅動 api 都屬于事件驅動類型:隻有當某些事件發生的時候,驅動代碼才執行——當使用者的應用程式希望從裝置擷取一些東西,或者當裝置有某些請求需要告知作業系統。
在 windows 上,驅動被表示為 <code>driverobject</code> 結構,它在 <code>driverentry</code> 函數的執行過程中被初始化。這些入口點也注冊一些回調函數,用來響應裝置的添加和移除、驅動解除安裝和處理新進入的 irp。當一個裝置連接配接的時候,windows 建立一個裝置對象,這個裝置對象在裝置驅動後面處理所有應用請求。
相比于 windows,linux 裝置驅動生命周期由核心子產品的 <code>module_init</code> 和<code>module_exit</code> 函數負責管理,它們分别用于子產品的加載和解除安裝。它們負責注冊子產品來通過使用核心接口來處理裝置的請求。這個子產品需要建立一個裝置檔案(或者一個網絡接口),為其所希望管理的裝置指定一個數字識别号,并注冊一些當使用者與裝置檔案互動的時候所使用的回調函數。
windows 裝置驅動在新連接配接裝置時是由回調函數 <code>adddevice</code> 通知的。它接下來就去建立一個裝置對象device object,用于識别該裝置的特定的驅動執行個體。取決于驅動的類型,裝置對象可以是實體裝置對象physical device object(pdo),功能裝置對象function device object(fdo),或者過濾裝置對象filter device object(fido)。裝置對象能夠堆疊,pdo 在底層。
裝置對象在這個裝置連接配接在計算機期間一直存在。<code>deviceextension</code> 結構能夠被用于關聯到一個裝置對象的全局資料。
裝置對象可以有如下形式的名字 <code>\device\devicename</code>,這被系統用來識别和定位它們。應用可以使用 <code>createfile</code> api 函數來打開一個有上述名字的檔案,獲得一個可以用于和裝置互動的句柄。
然而,通常隻有 pdo 有自己的名字。未命名的裝置能夠通過裝置級接口來通路。裝置驅動注冊一個或多個接口,以 128 位全局唯一辨別符(guid)來标示它們。使用者應用能夠使用已知的 guid 來擷取一個裝置的句柄。
在 linux 平台上,使用者應用通過檔案系統入口通路裝置,它通常位于 <code>/dev</code> 目錄。在子產品初始化的時候,它通過調用核心函數 <code>register_chrdev</code> 建立了所有需要的入口。應用可以發起 <code>open</code> 系統調用來擷取一個檔案描述符來與裝置進行互動。這個調用後來被發送到回調函數,這個調用(以及将來對該傳回的檔案描述符的進一步調用,例如<code>read</code>、<code>write</code> 或<code>close</code>)會被配置設定到由該子產品安裝到 <code>file_operations</code> 或者<code>block_device_operations</code>這樣的資料結構中的回調函數。
裝置驅動子產品負責配置設定和保持任何需要用于操作的資料結構。傳送進檔案系統回調函數的<code>file</code> 結構有一個 <code>private_data</code> 字段,它可以被用來存放指向具體驅動資料的指針。塊裝置和網絡接口 api 也提供類似的字段。
雖然應用使用檔案系統的節點來定位裝置,但是 linux 在内部使用一個主裝置号major numbers和次裝置号minor numbers的概念來識别裝置及其驅動。主裝置号被用來識别裝置驅動,而次裝置号由驅動使用來識别它所管理的裝置。驅動為了去管理一個或多個固定的主裝置号,必須首先注冊自己或者讓系統來配置設定未使用的裝置号給它。
目前,linux 為主次裝置對major-minor pairs使用一個 32 位的值,其中 12 位配置設定主裝置号,并允許多達 4096 個不同的裝置。主次裝置對對于字元裝置和塊裝置是不同的,是以一個字元裝置和一個塊裝置能使用相同的裝置對而不導緻沖突。網絡接口是通過像 eth0 的符号名來識别,這些又是差別于主次裝置的字元裝置和塊裝置的。
linux 和 windows 都支援在使用者級應用程式和核心級驅動程式之間傳輸資料的三種方式:
緩沖型輸入輸出buffered input-output它使用由核心管理的緩沖區。對于寫操作,核心從使用者空間緩沖區中拷貝資料到核心配置設定的緩沖區,并且把它傳送到裝置驅動中。讀操作也一樣,由核心将資料從核心緩沖區中拷貝到應用提供的緩沖區中。
直接型輸入輸出direct input-output 它不使用拷貝功能。代替它的是,核心在實體記憶體中釘死一塊使用者配置設定的緩沖區以便它可以一直留在那裡,以便在資料傳輸過程中不被交換出去。
記憶體映射memory mapping 它也能夠由核心管理,這樣核心和使用者空間應用就能夠通過不同的位址通路同樣的記憶體頁。
支援緩沖型 i/o 是 wdm 的内置功能。緩沖區能夠被裝置驅動通過在 irp 結構中的<code>associatedirp.systembuffer</code> 字段通路。當需要和使用者空間通訊的時候,驅動隻需從這個緩沖區中進行讀寫操作。
windows 上的直接 i/o 由記憶體描述符清單memory descriptor lists(mdl)介導。這種半透明的結構是通過在 irp 中的 <code>mdladdress</code> 字段來通路的。它們被用來定位由使用者應用程式配置設定的緩沖區的實體位址,并在 i/o 請求期間釘死不動。
在 windows 上進行資料傳輸的第三個選項稱為 <code>method_neither</code>。 在這種情況下,核心需要傳送使用者空間的輸入輸出緩沖區的虛拟位址給驅動,而不需要确定它們有效或者保證它們映射到一個可以由裝置驅動通路的實體記憶體位址。裝置驅動負責處理這些資料傳輸的細節。
linux 提供許多函數例如,<code>clear_user</code>、<code>copy_to_user</code>、<code>strncpy_from_user</code> 和一些其它的用來在核心和使用者記憶體之間進行緩沖區資料傳輸的函數。這些函數保證了指向資料緩存區指針的有效,并且通過在記憶體區域之間安全地拷貝資料緩沖區來處理資料傳輸的所有細節。
然而,塊裝置的驅動對已知大小的整個資料塊進行操作,它可以在核心和使用者位址區域之間被快速移動而不需要拷貝它們。這種情況是由 linux 核心來自動處理所有的塊裝置驅動。塊請求隊列處理傳送資料塊而不用多餘的拷貝,而 linux 系統調用接口來轉換檔案系統請求到塊請求中。
最終,裝置驅動能夠從核心位址區域配置設定一些存儲頁面(不可交換的)并且使用<code>remap_pfn_range</code> 函數來直接映射這些頁面到使用者程序的位址空間。然後應用能擷取這些緩沖區的虛拟位址并且使用它來和裝置驅動交流。
windows 是一個閉源作業系統。microsoft 提供 windows 驅動程式工具包以友善非 microsoft 供應商開發 windows 裝置驅動。工具包中包含開發、調試、檢驗和打包 windows 裝置驅動等所需的所有内容。
windows 驅動模型windows driver model(wdm)為裝置驅動定義了一個幹淨的接口架構。windows 保持這些接口的源代碼和二進制的相容性。編譯好的 wdm 驅動通常是前向相容性:也就是說,一個較舊的驅動能夠在沒有重新編譯的情況下在較新的系統上運作,但是它當然不能夠通路系統提供的新功能。但是,驅動不保證後向相容性。
和 windows 相對比,linux 是一個開源作業系統,是以 linux 的整個源代碼是用于驅動開發的 sdk。沒有驅動裝置的正式架構,但是 linux 核心包含許多提供了如驅動注冊這樣的通用服務的子系統。這些子系統的接口在核心頭檔案中描述。
盡管 linux 有定義接口,但這些接口在設計上并不穩定。linux 不提供有關前向和後向相容的任何保證。裝置驅動對于不同的核心版本需要重新編譯。沒有穩定性的保證可以讓 linux 核心進行快速開發,因為開發人員不必去支援舊的接口,并且能夠使用最好的方法解決手頭的這些問題。
當為 linux 寫樹内in-tree(指目前 linux 核心開發主幹)驅動程式時,這種不斷變化的環境不會造成任何問題,因為它們作為核心源代碼的一部分,與核心本身同步更新。然而,閉源驅動必須單獨開發,并且在樹外out-of-tree,必須維護它們以支援不同的核心版本。是以,linux 鼓勵裝置驅動程式開發人員在樹内維護他們的驅動。
windows 驅動程式工具包為 microsoft visual studio 添加了驅動開發支援,并包括用來建構驅動程式代碼的編譯器。開發 windows 裝置驅動程式與在 ide 中開發使用者空間應用程式沒有太大的差別。microsoft 提供了一個企業 windows 驅動程式工具包,提供了類似于 linux 指令行的建構環境。
windows 對于驅動程式的開發有良好的文檔支援。windows 驅動程式工具包包括文檔和示例驅動程式代碼,通過 msdn 可獲得關于核心接口的大量資訊,并存在大量的有關驅動程式開發和 windows 底層的參考和指南。
linux 沒有提供裝置驅動程式的指定樣本,但現有生産級驅動程式的源代碼可用,可以用作開發新裝置驅動程式的參考。
linux 和 windows 都有可用于追蹤調試驅動程式代碼的日志機制。在 windows 上将使用 <code>dbgprint</code> 函數,而在 linux 上使用的函數稱為 <code>printk</code>。然而,并不是每個問題都可以通過隻使用日志記錄和源代碼來解決。有時斷點更有用,因為它們允許檢查驅動代碼的動态行為。互動式調試對于研究崩潰的原因也是必不可少的。
windows 通過其核心級調試器 <code>windbg</code> 支援互動式調試。這需要通過一個串行端口連接配接兩台機器:一台計算機運作被調試的核心,另一台運作調試器和控制被調試的作業系統。windows 驅動程式工具包包括 windows 核心的調試符号,是以 windows 的資料結構将在調試器中部分可見。
linux 還支援通過 <code>kdb</code> 和 <code>kgdb</code> 進行的互動式調試。調試支援可以内置到核心,并可在啟動時啟用。之後,可以直接通過實體鍵盤調試系統,或通過串行端口從另一台計算機連接配接到它。kdb 提供了一個簡單的指令行界面,這是唯一的在同一台機器上來調試核心的方法。然而,kdb 缺乏源代碼級調試支援。kgdb 通過串行端口提供了一個更複雜的接口。它允許使用像 gdb 這樣标準的應用程式調試器來調試 linux 核心,就像任何其它使用者空間應用程式一樣。
在 windows 上安裝的驅動程式,是由被稱為為 inf 的文本檔案描述的,通常存儲在<code>c:\windows\inf</code> 目錄中。這些檔案由驅動供應商提供,并且定義哪些裝置由該驅動程式服務,哪裡可以找到驅動程式的二進制檔案,和驅動程式的版本等。
當一個新裝置插入計算機時,windows 通過檢視已經安裝的驅動程式并且選擇适當的一個加載。當裝置被移除的時候,驅動會自動解除安裝它。
在 linux 上,一些驅動被建構到核心中并且保持永久的加載。非必要的驅動被建構為核心子產品,它們通常是存儲在 <code>/lib/modules/kernel-version</code> 目錄中。這個目錄還包含各種配置檔案,如 <code>modules.dep</code>,用于描述核心子產品之間的依賴關系。
雖然 linux 核心可以在自身啟動時加載一些子產品,但通常子產品加載由使用者空間應用程式監督。例如,<code>init</code> 程序可能在系統初始化期間加載一些子產品,<code>udev</code> 守護程式負責跟蹤新插入的裝置并為它們加載适當的子產品。
windows 為裝置驅動程式提供了穩定的二進制接口,是以在某些情況下,無需與系統一起更新驅動程式二進制檔案。任何必要的更新由 windows update 服務處理,它負責定位、下載下傳和安裝适用于系統的最新版本的驅動程式。
所有 windows 裝置驅動程式在 windows 加載它們之前必須被數字簽名。在開發期間可以使用自簽名證書,但是分發給終端使用者的驅動程式包必須使用 microsoft 信任的有效證書進行簽名。供應商可以從 microsoft 授權的任何受信任的證書頒發機構擷取軟體出版商證書software publisher certificate。然後,此證書由 microsoft 交叉簽名,并且生成的交叉證書用于在發行之前簽署驅動程式包。
linux 核心也能配置為在核心子產品被加載前校驗簽名,并禁止不可信的核心子產品。被核心所信任的公鑰集在建構時是固定的,并且是完全可配置的。由核心執行的檢查,這個檢查嚴格性在建構時也是可配置的,範圍從簡單地為不可信子產品發出警告,到拒絕加載有效性可疑的任何東西。
如上所示,windows 和 linux 裝置驅動程式基礎設施有一些共同點,例如調用 api 的方法,但更多的細節是相當不同的。最突出的差異源于 windows 是由商業公司開發的封閉源作業系統這個事實。這使得 windows 上有好的、文檔化的、穩定的驅動 abi 和正式架構,而在 linux 上,更多的是源代碼做了一個有益的補充。文檔支援也在 windows 環境中更加發達,因為 microsoft 具有維護它所需的資源。
另一方面,linux 不會使用架構來限制裝置驅動程式開發人員,并且核心和産品級裝置驅動程式的源代碼可以在需要的時候有所幫助。缺乏接口穩定性也有其作用,因為它意味着最新的裝置驅動程式總是使用最新的接口,核心本身承載較小的後向相容性負擔,這帶來了更幹淨的代碼。
了解這些差異以及每個系統的具體情況是為您的裝置提供有效的驅動程式開發和支援的關鍵的第一步。我們希望這篇文章對 windows 和 linux 裝置驅動程式開發做的對比,有助于您了解它們,并在裝置驅動程式開發過程的研究中,将此作為一個偉大的起點。
原文釋出時間為:2017-11-09
本文來自雲栖社群合作夥伴“linux中國”