1. 程式人生 > >Linux和windows訪問裝置的方式比較

Linux和windows訪問裝置的方式比較

LInux一直秉承著一切皆檔案的理念,但是如何把裝置當做檔案來處理呢?Windows又是如何處理裝置的呢?參照前輩的譯文

      畢業後一直在學作業系統, 有時候覺得什麼都懂了,有時候又覺得好像什麼都不懂,但總體來說自認為對作業系統實現機制的瞭解比周圍的人還是要多一些。去年曾花了幾個星期的晚上時間斷斷續續翻譯了這篇對Linux和Windows驅動架構進行比較的論文。原文在這裡

LinuxWindows裝置驅動架構比較

1. 概述

這篇論文中,我們將考查目前最為廣泛使用的兩種作業系統,即LinuxWindows系統的裝置驅動架構。為每種作業系統實現裝置驅動所需要的驅動元件將被展示並進行比較,同時也展示每種作業系統中執行

I/O到核心緩衝的驅動的實現過程。最後將以對每種作業系統為開發者所提供的開發環境和輔助設施的考查收尾。

2. 引言

現代作業系統中包含多個模組,諸如記憶體管理器、程序排程器、硬體抽象層和安全管理器。欲瞭解Windows核心的細節,可參考[Russinovich, 98],Linux核心則參考[Rusling, 99], [Beck et al, 98]。核心可被看作一個黑盒,並且它應該知道如何與現存的不同種類的以及還沒出現的更多硬體裝置進行互動。實現一個核心,使其能夠與所有被熟知的硬體裝置進行互動是可能的,但並不現實,因為這會消耗太多的系統資源,沒有必要。

核心模組化

在核心被建立的時候,並不期望它能知道如何與未出現的裝置進行互動。現代作業系統核心允許通過在執行時新增裝置驅動模組的方式來擴充套件系統功能,這個模組的功能使得核心可以與某種特定的新裝置進行互動。每個模組都提供一個例程供核心在模組被載入時進行呼叫,還有一個例程在模組被移除時呼叫。每個模組還實現各種不同的例程以便實現將資料傳送到裝置或從裝置接收資料的I/O功能,同時還有一個例程供傳送

I/O控制指令到裝置。以上所述對LinuxWindows兩種驅動架構均適用。

本論文的組織結構

本論文分成下面幾節:

l 兩種作業系統大致的驅動架構(第二節)

l 每種作業系統驅動架構元件(第三節)

l 實現一個驅動執行I/O到核心緩衝(第四節)

l 兩種作業系統為開發者提供的驅動開發環境和輔助設施(第五節)

相關工作

Windows裝置驅動架構相關文件可在Windows驅動開發包中找到,更有甚者,Walter Oney [Oney, 99] 和Chris Cant [Cant, 99] 對Windows驅動架構進行了詳細的展示。Linux裝置驅動架構則由Rubini et al [Rubini et al,01]作了很好的描述,可免費獲取。

3. 裝置驅動架構

裝置驅動通過暴露程式設計介面的方式使得應用程式和作業系統可以對裝置進行控制來達到對硬體的操作。這一節將展示當前最常用的兩個作業系統,即WindowsLinux的驅動架構,以及這些架構的起源。

Linux驅動架構的起源

Linux可以說是Unix作業系統的一個克隆,首先由Linus Travolds創造 [Linus FAQ, 02], [LinuxHQ,02]。Linux沿用了類似於Unix的系統架構。Unix系統將裝置看作是檔案系統的節點。裝置以特殊檔案節點的方式呈現在目錄中,該目錄通常包含裝置檔案系統的節點入口[Deitel, 90]。用檔案系統節點來表示裝置的目的是使得應用程式能以裝置無關的方式訪問各種裝置[Massie, 86],[Flynn et al, 97]。應用程式仍然可以通過I/O控制操作進行特定於裝置的操作。裝置由主裝置號和次社保號進行標識。主裝置號用來作為驅動陣列的索引下標,而次裝置號將相似的物理裝置歸組[Deitel, 90]。Unix有兩種型別的裝置,即字元裝置和塊裝置。字元裝置驅動管理沒有緩衝並需要順序訪問的裝置,塊裝置驅動管理那些可隨機訪問的裝置,資料以塊的方式被訪問。另外,塊裝置驅動還用到緩衝區。塊裝置必須以檔案系統節點的方式掛載之後才能被訪問[Beck et al, 98]。Linux保留了Unix的很多架構設計,區別在於,Unix系統中,每個塊裝置需要建立一個對應的字元裝置,而在Linux中,虛擬檔案系統(VFS)介面使得字元裝置和塊裝置的區分變得模糊[Beck et al, 98]。Linux還引入了第三種裝置,叫網路裝置。訪問網路裝置驅動的方式和訪問字元裝置、塊裝置的方式不同,它使用了不同於檔案系統I/O介面的一個介面集。比如socket介面,就是用來訪問網路裝置的。

Windows驅動架構的起源

1980年,微軟從貝爾實驗室獲取Unix作業系統的許可,之後作為XENIX作業系統釋出。1981年,MS DOS第一版隨著IBM PC釋出,並具有和基於XENIXUnix系統類似的驅動架構[Deitel, 90]。和Unix作業系統不同的是,這個系統內嵌了常規裝置所需的驅動程式。裝置入口不以檔案系統節點的形式呈現,而是給裝置賦予預留的名稱,如CON表示鍵盤或螢幕,PRN表示印表機,AUX表示串列埠。應用程式不能像對待檔案系統節點那樣通過開啟裝置獲取和驅動程式關聯的裝置控制代碼從而對裝置進行I/O操作。作業系統透明地將預留的裝置名稱對應到驅動程式所管理的裝置。MS DOS第二版引進了可載入驅動的概念。由於Microsoft公開了驅動架構的介面,這也促進了第三方裝置製造商生產更多裝置 [Davis, 83]。硬體製造商可以為這些新裝置提供執行時可載入到核心或從核心移除的驅動程式。

之後,Microsoft又釋出了Windows 3.1,它支援更多的裝置並使用基於MS DOS的架構。之後的Windows 9598NTMicrosoft引入了WDM(Windows Driver Mode)WDM的出現是因為Microsoft想要驅動程式程式碼和後面所有新的作業系統相容[Microsoft WDM, 02]。因此,驅動遵守WDM規範的好處是,驅動程式只需編寫一次,在Microsoft之後所有新版作業系統上使用時只需要重新編譯該驅動即可。

Windows驅動架構

Windows驅動分兩類,分別為遺留驅動和即插即用驅動。這裡我們只將重點放在PnP驅動上,所有提及的驅動都大可認為是PnP驅動。PnP驅動不需費什麼力氣就能安裝好,因此它對使用者是友好的。另外一個使驅動程式支援PnP的好處是它們只會在需要的時候被作業系統載入,因此它們不會無端的耗盡系統資源。遺留驅動是為Microsoft早期的作業系統實現的,它們的架構已經過時。WDMMicrosoft指定的標準驅動模型[Microsoft DDK, 02]。WDM驅動適用於Microsoft近期所有的作業系統(Windows 95和之後的)

WDM驅動架構

WDM驅動分三類,分別為過濾驅動、功能驅動和匯流排驅動[Oney, 01]。它們形成了圖2.3所示的棧式結構。另外,WDM驅動必須是具有PnP感知的,支援電源管理和Windows管理規範(Windows Management Instrumentation)。圖2.3顯示了各個驅動如何互動資料和訊息。一個叫I/O請求包(IRPI/O Request Package)的標準結構被用來進行通訊。任何時候,應用程式向驅動程式傳送請求時,I/O管理將建立IRP並下傳到驅動程式,驅動程式處理完畢後,“完成”這個IRP [Cant, 99]。不是所有的IRP都被下發到匯流排驅動,有些IRP被上層的驅動處理後直接返回到I/O管理器。對裝置硬體的訪問需要通過硬體抽象層。

 

Figure 2.3 The WDM Driver Architecture

Linux驅動架構

Linux下的驅動以模組的形式呈現,這些模組就是擴充套件了Linux核心功能的一個個程式碼塊[Rubini et al, 01]。模組可形成圖2.4那樣的層次結構。模組之間的通訊通過函式呼叫實現。模組在載入時,將匯出模組中所有對Linux核心所維護的符號表公開的函式,之後這些函式對所有的核心模組都是可見的。對裝置的訪問需要通過硬體抽象層,硬體抽象層的實現依賴於核心編譯時所針對的硬體平臺,如x86SPARC

 

Figure 2.4 The Linux Driver Architecture

Linux和Windows驅動架構的比較

如圖2.32.4所示,兩個作業系統有很多的相似之處。兩個系統中,驅動程式都是作為擴充套件核心功能的模組化元件。在Windows系統中,驅動層級之間的通訊是通過將IRP作為標準系統函式或驅動程式自定義函式的引數來實現,Linux下函式呼叫的引數則是根據具體的驅動而不同。Windows有單獨的核心模組來管理PnPI/O和電源,這些元件在適當的時候將IRP傳送到驅動程式。

Linux系統中,模組沒有明顯的層級關係,比如沒有區分匯流排、功能、過濾驅動。核心沒有明確定義的PnP、電源管理器以便在適當的時候將特定的資訊傳送給核心模組。核心可能會載入具有PnP、電源管理功能的核心模組,但核心模組暴露給驅動程式的介面並沒有規定。

這些功能一般會合併到新版的Linux核心中,因為Linux核心總是處於發展狀態。每當核心將資料傳送給棧式模組中的某個驅動程式時,通過這些驅動所指定的某個介面,該資料可被分享給棧式模組中的其他驅動程式。

在這兩種系統環境中,對硬體的訪問都會通過硬體抽象層介面,硬體抽象層介面根據核心編譯時所針對的特定平臺(X86SPARC)而實現。兩種架構的相同之處在於,驅動程式都是執行時可載入的模組,每個模組都包含一個入口點使核心知道從哪裡開始執行模組的程式碼。一個模組還包含這樣一些例程,即該模組所管理的裝置接收到I/O操作請求時供核心呼叫的例程。這使得核心可以嚮應用層提供裝置無關的介面。在後面的第3.3節中,將對兩種架構中的驅動元件作更深入的比較。

驅動元件

編寫驅動程式時需要對硬體裝置如何被操作有了解。比如說,幾乎所有的裝置都允許使用者讀取和寫入資料。這一節中將展示所有驅動程式都應該包含的驅動元件,同時對兩種作業系統的驅動元件進行比較,並展示如何實現一個對核心緩衝區進行I/O操作的驅動程式。本節將以對每種作業系統為驅動程式開發所提供的環境和輔助設施的考究來收尾。

Windows驅動元件

Windows驅動程式由各種不同的例程組成,其中有一些是必須的,其它的則是可選的。這一節展示所有驅動程式都必須實現的例程。Windows中的裝置驅動以一個叫DriverObject的結構體表示。用一個結構體諸如驅動物件來表示一個驅動是有必要的,因為核心實現了可被所有驅動物件使用的各種例程。這些例程對一個驅動物件進行操作,這部分內容將在下一節進行討論。

驅動程式初始化

Windows中每個裝置驅動程式都包含一個叫DriverEntry的例程。顧名思義,這個例程在驅動程式被載入時執行,驅動所管理的裝置物件的初始化也在這個例程中進行。Microsoft’s DDK [Microsoft DDK, 02] 是這樣描述的:驅動物件表示當前被載入的驅動程式,裝置物件則表示一個物理、邏輯或虛擬的裝置。一個被載入的驅動程式(用驅動物件表示)可以管理多個裝置(用裝置物件表示)。初始化過程中,裝置物件中用以指定驅動程式的解除安裝例程、新增裝置例程和分發例程都將被設定。解除安裝例程用於驅動程式被解除安裝時做一些清除操作,例如釋放從核心堆中的分配的記憶體。新增裝置例程僅在當驅動程式作為PnP驅動載入時,在DriverEntry例程之後被呼叫,而分發例程用於實現I/O操作。

AddDevice例程

PnP驅動程式需要實現AddDevice例程。在這個例程中,一個裝置物件被建立,為該裝置儲存全域性資料的空間被分配。裝置資源的分配和初始化也在這裡進行。裝置物件根據其被建立的位置而擁有不同的名稱。如果一個裝置在當前載入的驅動程式中建立並用於管理該驅動,則該裝置叫功能裝置物件(FDO)。如果一個裝置物件是由驅動棧中位於下方的驅動程式建立,則該裝置叫物理裝置物件(PDO)。如果一個裝置物件是由位於上方的驅動程式所建立,則叫過濾驅動物件(FIDO)

建立一個裝置物件

一個裝置物件對應於在AddDevice例程中呼叫I/O管理器中名為IoCreateDevice的例程所建立的裝置。對IoCreateDevice來說,最重要的是裝置物件的名稱和裝置型別。這個名稱使得應用程式和其他的核心驅動可獲取到該驅動的控制代碼,從而可進行I/O操作。裝置型別指定了驅動程式管理的裝置的型別,如儲存裝置。

全域性驅動資料

當一個裝置物件被建立時,可以將一個記憶體塊與之關聯,該記憶體塊叫DeviceExtension,也即在Windows中驅動程式資料儲存的地方。這是一個挺重要的東東,它使得在驅動程式程式碼中使用難於維護的全域性資料結構變得沒有必要。例如,要是錯誤的聲明瞭一個和全域性變數具有相同名稱的區域性變數,驅動程式編寫者會發現難以跟蹤這樣的bug。這也使得維護特定於裝置物件的資料變得簡單,尤其是當多個裝置物件存在於一個驅動程式中的時候,比如匯流排驅動程式在管理總線上出現的多個裝置的物理裝置物件時。

裝置命名

裝置的名稱可在裝置物件被建立的時候賦予,這個名稱可以用來訪問驅動的控制代碼,控制代碼又被用來進行I/O操作。Microsoft建議不要給在過濾驅動和功能驅動中建立的功能裝置物件命名。Oney [Oney, 99]指出,若一個裝置物件具有名稱,則任意使用者都能開啟裝置物件並對其進行I/O操作,即使是對非磁碟裝置驅動。這是因為Windows預設就給了非磁碟裝置物件毫無限制的訪問狀態。另外一個問題是這些名稱不需要遵循任何命名規範,指定的名稱往往不是經過挑選的。例如兩個驅動程式開發者可能給他們的裝置物件賦予相同的名稱,這樣就會引起衝突。Windows還支援另外一種裝置物件命名方式,即裝置介面。裝置介面是由128位元位構成的全域性唯一識別符號[Open Group, 97]。GUID可用Microsoft DDK中提供的工具生成,生成之後可對外發布。驅動程式通過在AddDevice例程中呼叫I/O管理器的名為IoRegisterDeviceInterface的例程註冊裝置介面。一旦註冊,驅動程式必須呼叫I/O管理器的IoSetDeviceInterfaceState例程來使能裝置介面。註冊過程中一個介面資料入口被新增到Windows登錄檔中,應用程式接著就可以訪問到。

從應用程式訪問驅動

應用程式欲對裝置驅動執行I/O操作前,必須先通過調Win32 API CreateFile獲取到裝置驅動的一個控制代碼,這個API需要裝置的路徑作為引數,如\device\devicex。具有名稱的裝置將出現在名稱空間“\\device”中,因此先前的路徑表示裝置devicexCreateFile同時需要指定對裝置的訪問標誌,如讀、寫和共享方式。對註冊了裝置介面而沒有名稱的裝置的訪問則不同於圖3.1.2.4展示的例程,它需要使用驅動程式的GUID,呼叫Win32 API SetupDiGetClassDevs獲取一個指向裝置資訊結構的控制代碼。這種方式只適用於驅動程式已經註冊了裝置介面、應用程式需要訪問裝置(叫裝置介面類)的情況。每次驅動程式呼叫I/O管理器例程IoRegisterDeviceInterface時,一個新的裝置介面類的例項就被建立。一旦應用程式獲取到了裝置資訊控制代碼,對Win32 API SetupDiEnumDeviceInterfaces的多個呼叫將會為每個裝置介面類例項返回裝置介面資料。最後,通過呼叫Win32 API SetupGetDeviceInterfaceDetail,並根據之前返回的介面資料,可為每個裝置介面類例項獲取到一個裝置路徑。接著,對感興趣的裝置,使用裝置路徑為引數呼叫CreateFile來獲取控制代碼以便執行I/O操作。

 

Figure 3.1.2.4 Obtaining a handle an application can use for I/O from a device GUID.

裝置物件棧

當PnP管理器呼叫AddDevice例程時,它其中的一個引數是來自下層驅動的一個裝置物件(PDO)。裝置物件在AddDevice例程中完成堆疊,因為發往下層驅動的IRP可被當前載入的驅動獲取到。如圖 3.1.2.5所示,裝置堆疊是通過呼叫I/O管理器例程IoAttachDeviceToDeviceStack 來完成。在呼叫IoAttachDeviceToDeviceStack時,需要一個位於棧中新建立裝置物件下方的物理裝置物件。這個例程將新建立的裝置附加到裝置棧的頂層,並將當前位於其下方的裝置物件返回,圖 3.1.2.5中下方裝置為裝置物件X。下層的物理裝置可位於新裝置下方的任何位置,而IoAttachDeviceToStack 返回的是緊鄰著當前裝置的下層裝置。

 

Figure 3.1.2.5 Attaching a device object to the top of a device object stack.

Windows應用層到核心和核心到應用層資料傳輸模式

從核心空間到使用者空間以及從使用者空間到核心空間傳送資料的模式是在裝置物件的flag域中設定。共有三種模式,分別為buffer I/Odirect I/Oneither I/O。圖3.1.2.6 闡述了這三種模式。在buffer I/O模式中,作業系統分配了一個核心緩衝區來處理請求。在寫操作中,作業系統首先驗證使用者空間提供的緩衝區,然後從使用者空間將資料拷貝到新分配的核心緩衝區,接著講核心緩衝區傳送給驅動程式。讀操作時,作業系統驗證使用者緩衝區然後將資料從新分配的核心緩衝區中拷貝到使用者緩衝區。驅動程式可通過IRPAssociatedIrp.SystemBuffer域訪問到核心緩衝區。當使用buffer I/O模式時,驅動程式通過讀取或寫入核心緩衝區來實現與應用層的通訊。

Direct I/O是用於應用層和驅動程式交換資料的第二種模式。應用層提供的緩衝區被作業系統在記憶體中鎖定,這樣它就不會被交換出去,並將被鎖定記憶體的記憶體描述列表(Memory Description ListMDL)傳送給驅動程式。記憶體描述列表是一個不透明的結構體,它的實現對驅動程式是不可見的。驅動程式之後通過MDL對使用者空間緩衝區進行DMA操作。驅動程式通過IRPMdlAddress域訪問MDL。使用direct I/O的好處是它比buffer I/O速度要快,因為不需要在使用者層和核心層之間拷貝任何資料,而是直接對使用者緩衝區進行I/O操作。

第三種I/O模式既不使用buffer也不使用MDLs,作業系統直接將使用者空間緩衝區的虛擬地址傳送給驅動程式。驅動程式在使用之前負責檢查緩衝區的有效性。此外,只有在當前執行緒上下文環境和應用程式的上下文環境一致時,使用者空間緩衝區才能被訪問,否則會出現頁錯誤,因為虛擬地址只有在應用程式所對應的程序處於啟用狀態時才有效。

 

Figure 3.1.2.6 The three ways in which data from kernel to user and user to kernel

space is exchanged.

分發例程

分發例程用來處理接收到的I/O請求包,即IRPs(I/O request packets)。當一個IRP到來(如當一個應用程式發起I/O操作)時,一個適當的例程被從驅動物件的MajorFunction域中指定的例程陣列中選出來,如圖3.1.3。這些例程在驅動程式的入口函式中被初始化。每個IRP在建立時就與一個I/O stack location結構體(用於儲存IRP的引數)關聯。這個結構體有一個域,指定了IRP需要執行的分發例程和分發例程需要的相關引數。I/O管理器根據IRP決定將IRP發往哪個分發例程。

 

Figure 3.1.3 dispatching IRP’s to dispatch routines.

因此,IRPs被路由到適當的驅動例程進而得到處理。分發例程ID如表3.1.3所示,它們作為例程陣列的索引,這個陣列是在驅動物件的MajorFunction域中指定。分發例程的名稱是驅動程式所實現的例程的名稱,這些例程都將一個IRP和該IRP被髮送到的裝置物件作為引數。

 

Table 3.1.3 Required Windows driver dispatch routines

Windows驅動程式安裝

Windows根據一個INF檔案中的安裝資訊來安裝驅動。驅動程式的編寫者負責為驅動程式提供一個INF檔案。Windows DDK提供一個叫GenInfGUI應用程式,來為驅動程式生成INF檔案。這個工具需要提供一個公司名稱和一個Windows裝置類,驅動程式將被安裝到該裝置類下。Windows為驅動程式預定義了各種不同的裝置類。從系統控制面板進入到裝置管理器面板,可看到顯示的所有按裝置類分類的已安裝驅動程式。已有的裝置類如1394PCMCIA裝置類。可在INF檔案中新增一個ClassInstall32節來新增一個自定義的裝置類。對PnP感知的裝置,還需要在INF檔案中指定一個硬體ID,在該裝置被新增到系統中時,系統將用該ID來標識裝置。硬體ID是一個標識字串,PnP管理器在裝置新增到系統中用硬體ID來標識裝置。MicrosoftWindows系統會用到的各種裝置釋出了硬體ID。硬體ID儲存在硬體裝置中,作業系統在裝置新增到系統中時從裝置讀取。一旦新裝置的INF檔案成功安裝到系統中,每當具有指定硬體ID的裝置被新增到系統中,為該裝置編寫的驅動程式都被載入,並在裝置移除時被解除安裝。

Windows獲取驅動程式使用資訊

系統控制面板上的裝置管理器給使用者提供驅動的相關資訊。它列出了所有當前已載入的驅動,每個驅動提供者的相關資訊和驅動資源使用情況。同時還顯示驅動無法載入時的失敗資訊以及錯誤碼。

Linux驅動架構元件

Linux下的裝置驅動和Windows裝置驅動的相似之處在於它們都是由一些執行I/O及控制操作的例程組成。驅動程式沒有對應的驅動物件,而是由核心直接管理。

驅動程式初始化

Linux下的美國各驅動程式包含一個驅動註冊例程和反註冊例程。驅動註冊例程類似於Windows的驅動入口例程。驅動程式編寫者使用核心定義的兩個巨集module_initmodule_exit來指定自定義的例程作為註冊和反註冊例程。

3.2.1.1. 驅動註冊和反註冊

module_init宣告的註冊例程是驅動程式被載入時第一個執行的例程。在這個例程中,用一個核心字元設備註冊例程register_chrdev註冊驅動。這個例程需要一個驅動名稱、主驅動編號(將在3.2.2節中討論)和一系列執行檔案操作的例程。其它特定於驅動的初始化也必須在這個例程中完成。反註冊函式在驅動程式被解除安裝時執行,它的主要功能是做一些清除操作。反註冊之前使用register_chrdev註冊的驅動程旭時會呼叫核心例程unregister_chrdev,並需以裝置名和主編號為引數。

裝置命名

Linux下,裝置命名使用0255的數字,叫主裝置編號。這意味著最多隻能有256個可用的裝置,也即應用程式可獲取到控制代碼的裝置。但這樣一個主裝置的每個驅動程式可以管理額外的256個裝置。這些驅動程式管理的裝置也使用0255的數字標識,叫次裝置編號。因此,應用程式可訪問多達65535(256*256)個裝置。主裝置編號賦給一些熟知的裝置,如IEEE1394的編號為171Linux核心原始碼樹中的檔案Documentation/devices.txt包含了所有主裝置編號的分配情況和編號註冊中心的聯絡地址。當前,主裝置編號240-254為實驗所用。一個驅動程式通過指定0作為主裝置編號來請求一個自動分配的主編號(若當前還有可用的主裝置編號的話)。這種指定0為主裝置編號的方式並不會有什麼問題,因為它是為null裝置預留的,而沒有一個新的驅動程式會將自己註冊為null裝置驅動。

應用程式訪問驅動

應用程式通過檔案系統入口(nodes)訪問驅動。按照慣例,驅動程式目錄為/dev。需要對驅動執行I/O操作的應用程式使用open系統呼叫來獲取某個特定驅動的控制代碼。Open系統呼叫需要一個裝置節點名稱如/dev/tty和訪問標識(flags)。獲取控制代碼之後,應用程式使用該控制代碼來呼叫其他的I/O系統呼叫如readwriteIOCTL

檔案操作

Windows下,分發例程是在驅動入口例程中設定。Linux下,這些分發例程就是所謂的檔案操作並使用結構體file_operations來表示。一個典型的驅動程式會實現如表3.2.3列出的檔案操作例程。

 

Table 3.2.3 Most commonly defined driver file operations in Linux

這些檔案操作在驅動程式註冊時指定。每當應用程式請求一個裝置控制代碼時,核心會建立一個叫file的結構體,並在某個驅動例程被呼叫時將其傳遞給驅動程式。檔案操作例程被多個使用者呼叫,每個都對應一個file結構體。File結構體有一個f_op域,這個域是一個指標,指向驅動註冊時指定的檔案操作例程集。因此,在呼叫任何一個檔案操作例程時,都可以通過改變f_op域的值來指向新的檔案操作例程集。

驅動程式全域性資料 

每當應用程式對/dev下的裝置檔案節點發起一個open系統呼叫時,應用程式從作業系統獲得裝置的一個控制代碼。這個時候驅動程式的open函式被呼叫,並給它傳遞為open系統呼叫所建立的file結構體。任何一個檔案操作例程執行時,核心都將file結構體傳遞給驅動程式。File結構體的private_data域可以是驅動程式指定的任意自定義結構體。驅動程式的私有資料通常在檔案open操作函式中被設定,即為它分配記憶體,之後在檔案的release操作函式中釋放該記憶體。File結構體的私有資料域可用來指向驅動程式的全域性資料,避免了使用全域性變數。

驅動主編號和次編號如何工作

3.2.4.1. 問題

Linux下只有一個驅動程式可通過註冊一個特定的主編號來管理一個裝置,也就是說,驅動程式的註冊只能使用一個主編號。舉個例子,存在兩個裝置節點/dev/device1(主編號4次編號1)和裝置/dev/device2(主編號4次編號2),只有一個驅動程式能夠處理應用程式對兩個節點的請求。這種限制的存在是因為Linux沒有提供一種註冊機制使得驅動程式能自注冊一個主編號和一個次編號以便能管理一個裝置。

3.2.4.2.解決辦法

· 載入一個驅動程式來管理主編號為4的裝置。這個驅動程式將自己在核心中註冊(3.2.1.1會看到這是如何完成的)

· 分別載入兩個驅動,一個管理主編號4次編號1的裝置,另一個管理主編號4次編號2的裝置。這兩個驅動程式沒有在核心中註冊,而是向管理主裝置編號4的另外一個驅動程式註冊。這個驅動程式負責實現註冊機制並跟蹤管理向它註冊的所有驅動程式。

· 應用程式開啟任意一個裝置節點(/dev/device1/dev/device2)時,註冊為管理主裝置4的驅動程式的open例程將被核心呼叫。一個用來表示被開啟裝置的file結構體作為引數傳遞給這個open例程。

· 這時候,管理裝置主編號4的驅動程式修改檔案操作函式指標(file結構體的f_op成員)來指向管理被開啟裝置的驅動程式所實現的I/O例程。應用程式開啟次裝置時,管理主編號4的驅動程式以下列方式區分:

o 一個叫inode的結構體被傳遞給open例程。這個結構體包含一個叫i_rdev的域,該域指定了open操作的目標裝置對應的主編號和次編號。核心的MINORMAJOR巨集可用來從i_rdev域提取住次編號。這個例子中,主編號為4,次編號為12。管理主編號4的驅動程式就可以通過這個資訊從它的註冊資料庫中定位到次裝置驅動程式。

使用者到核心和核心到使用者空間資料傳輸模式

Linux下,使用者到核心和核心到使用者空間的資料交換方式有三種,分別是buffer I/Odirect I/Ommap。在buffer I/O模式中,核心將資料從使用者空間拷貝到核心空間供驅動程式使用。和Windows不同,Linux沒有自動對I/O進行緩衝,而是提供訪問使用者和核心空間的例程,驅動程式使用這些例程來完成使用者和核心空間之間資料的拷貝。Direct I/O模式中,驅動程式可對使用者空間緩衝區直接讀和寫。這是通過kiobuf介面完成,它將使用者空間緩衝區對映到呼叫系統呼叫kiobuf時定義的結構體。這個操作會鎖定使用者空間緩衝區,這樣該空間不會被換出以便滿足裝置的I/O操作。第三種方式是mmap,它是由驅動程式使用mmap核心呼叫將核心的空間塊對映到使用者空間,應用程式因而可以對對映的核心記憶體進行I/O操作[Rubini et al, 01].。

Linux驅動安裝

Linux下驅動程式的安裝是將驅動檔案放置到特定的系統目錄下。在RedHat發行版中[Redhat, 02],模組位於目錄/lib/modules/kernel_versionkernel_version指定當前核心版本,如2.4.19。一個叫modules.conf的配置檔案位於系統配置檔案目錄如/etc中,這個檔案在載入模組時被核心使用。通過修改該檔案,可對某個驅動程式放置位置進行覆蓋。還可以定義其它一些模組載入選項,如驅動被載入時給它傳遞的引數。模組載入和解除安裝使用系統自帶的核心模組工具包,叫insmodmodprobermmodInsmodmodprobe將驅動程式二進位制映象載入到核心,rmmod則移除模組。另一個叫lsmod的程式列出當前所有已載入的模組。Insmod嘗試載入一個模組,若該模組依賴於其他模組,則返回一個錯誤碼。Modprobe則嘗試著滿足模組依賴關係,它試圖將當前模組所依賴的其它模組先進行載入。模組依賴關係資訊可從一個叫modules.dep的檔案獲取,該檔案位於系統模組目錄(system’s modules directory)中。在驅動程式可被應用程式訪問前,這個驅動的一個附有主次裝置編號的裝置節點(2.13.2.2.1節,Linux如何在系統中表示裝置)首先要在裝置目錄/dev中被建立。系統程式mknod就是為這個目的準備的。在建立一個裝置節點時,指定節點為字元裝置還是塊裝置是有必要的。

Linux獲取驅動使用資訊

我們時常需要獲取系統已載入驅動的狀態資訊。Linux下,proc檔案系統是用來將核心資訊嚮應用程式釋出。Proc檔案系統和其他的檔案一樣,它也包含目錄和檔案節點供應用程式訪問和執行I/O操作。Proc檔案系統中的檔案和普通檔案的區別在於,對proc檔案執行I/O操作的資料是被傳遞到核心記憶體而不是磁碟儲存。Proc檔案系統是應用程式和核心元件之間的通訊媒介。例如,讀取/proc/modules將返回當前所有已載入模組和它們的依賴關係。在獲取驅動狀態資訊和釋出驅動程式資料到應用程式時,proc檔案系統就尤為有用。

WindowsLinux驅動架構元件比較

WindowsLinux的驅動程式都是由一系列執行I/O操作的例程組成的可動態載入的模組。當載入一個模組時,核心將定位到被系統標記為驅動程式入口的例程作為驅動程式碼執行的起點。

驅動例程

兩個系統中驅動程式都具有初始化和反初始化例程。在Linux中,這兩個例程的名稱可自定義,在Windows中,初始化例程的名稱固定(DriverEntry)但反初始化例程可自定義。Windows為每個驅動程式維護一個驅動物件,驅動程式的多個例項用多個驅動物件表示。Linux下,核心為每個管理一個裝置主編號的驅動維護資訊,即每個主裝置驅動。兩個作業系統都要求驅動程式實現標準的I/O例程,Windows中叫分發例程,Linux下叫檔案操作。Linux下,可為每個應用程式獲取到的裝置控制代碼設定一個不同的檔案操作例程集。Windows下,分發例程在驅動物件的一部分,並且是一次性地在DriverEntry例程中定義。由於每個被載入的驅動都有一個驅動物件,因此不建議在應用程式使用系統呼叫請求一個控制代碼時修改驅動物件的分發例程。Windows有個叫AddDevice的例程,在PnP感知的裝置新增到系統時被PnP管理器呼叫。Linux沒有PnP管理器,也就不存在這樣一個例程。

Windows的分發例程對裝置物件和IRPs進行操作,Linux下,檔案操作針對file結構體。自定義的驅動全域性資料儲存在Windows的裝置物件中,而Linux下則儲存在file結構體中。Windows下,裝置物件在驅動載入時被建立,Linux下,file結構體是應用程式通過系統呼叫open向驅動請求控制代碼時被建立。這就意味著Linux下每個應用程式的全域性資料可儲存在file操作結構體中。Windows下,全域性資料只能出現在驅動管理的功能裝置物件(FDO)中。Windows下每個應用程式的全域性資料必須儲存在功能裝置物件(FDO)自定義結構體的列表結構中。

裝置命名

Windows下的驅動使用驅動自定義的字串命名並顯示在\\device名稱空間下。Linux下,驅動被賦予文字形式的名稱,但應用程式並不需要知道這些名稱,驅動是通過主-次編號對來標識。主-次編號的範圍是0-255,因為是用16位元位來表示主-次編號對,所以最大允許65535個裝置安裝到系統中。Linux下的裝置通過檔案系統節點供應用程式訪問。在大部分的Linux發行版中,目錄/dev包含裝置檔案系統節點。每個節點建立時帶有驅動的主編號和次編號。應用程式獲得驅動的一個控制代碼,用來對系統呼叫open的目標裝置節點進行I/O操作。Windows還有另一種驅動命名方式,是給每個驅動註冊的128GUID。應用程式訪問登錄檔,通過GUID獲得\\device名稱空間下的文字形式的名稱。這個名稱通過使用Win32 API CreateFile來獲取驅動的一個控制代碼以便進行I/O操作。

使用者-核心空間資料交換

兩種作業系統中,資料來自或去往使用者空間的方式是類似的,都允許緩衝區資料傳送,在Windows下是有I/O管理器執行,Linux下則由驅動執行。兩種作業系統都可以進行direct I/O到使用者空間緩衝區,通過鎖定使用者空間緩衝區以使得該緩衝區一直存在於實體記憶體中。這個起因是驅動程式並不總能直接訪問使用者空間緩衝區,因為它不能保證一直執行在和擁有該使用者空間緩衝區的應用程式一致的程序上下文中。應用程式有它自己的虛擬地址空間,該地址空間只在它自己的程序上下文中有效。因此,當驅動程式訪問某些應用程式的一個虛擬地址但不在該應用程式的程序上下文中時,就會訪問了無效的地址。

驅動安裝和管理

Windows驅動的安裝是通過一個叫INF檔案的文字檔案。一旦安裝之後,一個裝置的驅動程式在裝置出現在系統中時會自動被PnP管理器載入。Linux系統中,使用程式工具來載入驅動二進位制映象到核心。需要手動將一些條目新增到系統啟動檔案中,這樣驅動載入程式如modprobe就以驅動程式映象路徑或驅動程式的別名為引數執行。驅動程式的別名在檔案/etc/modules.conf中定義,modprobe等類似程式在載入驅動之前會檢視該檔案。Modules.conf中一個定義別名的條目的例子可類似於“alias sounddriver testdriver”,這是將sounddriver作為testdriver驅動二進位制映象的別名。這樣一來,使用者可通過使用標準的更簡單的名稱如sounddriver來載入音訊驅動程式而不需要知道音訊卡的某個特定驅動程式的名稱。Windows下驅動程式的狀態資訊可在裝置管理面板中看到,也可以直接從系統登錄檔中讀取相關資料。Linux下,驅動資訊可通過proc檔案系統節點獲取,如檔案/proc/module包含了已載入模組的一個列表。

一個核心緩衝驅動

這一節展示一個執行I/O操作到核心記憶體塊(虛擬磁碟)的簡單驅動程式的實現。我們將討論為使驅動程式能夠同時在WindowsLinux下工作所需要的各種元件,這樣它們所需要驅動元件的相似和不同之處也得到了突顯。驅動程式所管理的虛擬裝置如圖4.0.所示,它由若干核心記憶體塊組成。應用程式可對虛擬裝置進行I/O操作。驅動程式可以選擇某個記憶體塊和記憶體塊的偏移位置進行訪問。

 

Figure 4.0 A simple virtual device

需要的驅動元件

WindowsLinux驅動程式都將實現readwriteIOCTL驅動例程。每個作業系統所需要的例程如圖4.1.所示。驅動程式的名稱可隨意指定。圖4.1種不同作業系統的例程也可以被賦以相同的名稱,這裡只是根據平臺的慣用法來命名。

 

Figure 4.1 The Windows and Linux basic driver routines

驅動載入和解除安裝例程

Windows下,驅動載入例程DriverEntry中所執行的步驟是設定I/O分發例程,如圖4.1.1a所示。

 

Figure 4.1.1a Initialisation of a driver Object in the driver entry routine

Linux下,驅動載入例程RegisterDriver中所進行的是驅動主編號的註冊,如圖4.1.1bTagged檔案操作的初始化,只針對GCC編譯器,如圖4.1.1b,是在對結構體fops的宣告中,當然這不是ANSI C的有效語法。編譯器將使用驅動程式實現的例程名稱初始化file_operation結構體(fops)中的各個不同的域。如open是結構體一個域的名稱而Open是驅動實現的一個例程,編譯器將Open函式指標賦給open域。


Figure 4.1.1b Registration of a driver major number in Linux

Linux驅動的解除安裝程式中,已註冊驅動必須進行反註冊,如圖4.1.1c

 

Figure 4.1.1c Driver major number deregistration in Linux

驅動全域性結構

必須定義一個結構體來儲存驅動全域性資料,這些資料在驅動的各例程中被使用。對這個記憶體裝置,同樣的結構使用在WindowsLinux驅動程式中,其定義如圖4.1.2


Figure 4.1.2 Structure used to store global data for generic driver

memoryBank是包含4個記憶體塊,每個塊為1K大小的陣列。currentBank表示當前選中的記憶體塊,offsets記錄了每個記憶體塊內部的偏移量。

新增裝置例程

新增裝置例程只針對WindowsLinux沒有新增裝置例程,所有的初始化必須在驅動載入例程裡完成。WindowsaddDevice例程所執行的操作如圖4.1.3所示。在呼叫I/O管理器例程IoCreateDevice例程時,一個裝置物件被建立。供應用程式使用來獲取驅動控制代碼的介面也被建立,這通過呼叫I/O管理器例程IoRegisterDeviceInterface。這個例程的一個引數是使用系統工具guidgen手動生成的GUIDWindows下,驅動和應用程式之間的不同資料交換方式在3.1.2.6節中有說明。驅動程式通過設定裝置物件的flags(3.13.2關於裝置物件的討論)來表明其要使用的資料交換方法。這裡例子中flags被設定成使驅動使用buffered I/O方式。記憶體裝置使用的每個記憶體塊通過其中一個叫ExAllocatePool的核心記憶體分配例程來分配。這個記憶體從核心的非頁記憶體池中分配,這樣裝置的記憶體總是存在於實體記憶體中。

 

Figure 4.1.3 Operations performed in the Windows driver’s add device routine

開啟和關閉例程

Windows驅動的大部分初始化操作都已經在新增裝置例程中完成,因此不需要在開啟例程中做任何初始化。Linux下的開啟例程如圖4.1.4a所示。首先,用於儲存驅動全域性資料的記憶體被分配,然後將檔案結構體的private_data域指向該記憶體。之後記憶體裝置所使用的記憶體塊通過和Windows下完全一樣的方式分配,只是記憶體分配函式的名字不同,Windows下是ExAllocatePoolLinux下是kmalloc

 

Figure 4.1.4a Operations performed in Linux’s generic driver open routine

Linux的關閉例程中,為驅動全域性資料和記憶體裝置分配的記憶體被釋放,如圖4.1.4bWindows下,記憶體的釋放是在響應PnP移除訊息的時候,這個在本節後面會有討論。

 

Figure 4.1.4b Operations performed in Linux’s generic driver close routine

讀和寫例程

Readwrite例程將資料傳送到或取自當前選中的核心記憶體塊。Windows下,讀例程的執行如圖4.1.5a所示。要讀取資料的長度值從IRPI/O棧位置(3.1.3節什麼是I/O stack location)獲取,該域名稱為Parameters.Read.Length。所請求長度的資料將被從當前選中的記憶體塊(後面會討論應用程式通過驅動IOCTL例程選擇記憶體塊)中讀取,使用的是核心執行時例程RtlMoveMemory。RtlMoveMemory將資料從記憶體裝置的記憶體空間搬移到I/O管理器為buffered I/O分配的緩衝區,也就是IRPAssociatedIrp.SystemBuffer域。這個IRP算完成了,就通知I/O管理器驅動程式已完成IRP的處理,I/O管理器將IRP返回給其發起者。

 

Figure 4.1.5a Performing a read operation in the Windows driver

寫例程對上述記憶體搬移進行反操作,如圖4.1.5b

 

Figure 4.1.5b Performing a write operation in the Windows driver

Linux下,讀例程如圖4.1.5c所示。對驅動全域性資料的引用從檔案結構體的private_data域獲取,從全域性資料中,又獲取到對memoryBank的引用。接著資料就從這個記憶體區被傳送到使用者空間,使用核心訪問使用者空間例程copy_to_user。

 

Figure 4.1.5c Performing a read operation in the Linux driver

寫例程執行和上面同樣的操作,只是這一次資料是從使用者空間傳送到核心空間,如圖4.1.5d

 

Figure 4.1.5d Performing a write operation in the Linux driver

裝置控制例程

裝置控制例程用來設定裝置的各種狀態。應用程式使用win32例程DeviceIoControl來對驅動程式進行IOCTL呼叫。這個例程需要一個由驅動程式定義的IOCTL碼。一個IOCTL碼告訴驅動程式應用程式要執行的操作。在這個例子中,驅動實現IOCTL例程用來選擇當前記憶體塊號(current bank number)。驅動的IOCTL碼使用之前必須先被定義。WindowsIOCTL碼的定義如圖4.1.6aCTL_CODE巨集用