在前一節中,我講述了當WDM驅動程序被第一次裝入時如何初始化。通常,一個驅動程序可以被多個設備利用。WDM驅動程序有一個特殊的AddDevice函數,PnP管理器為每個設備實例調用該函數。該函數的原型如下:
NTSTATUS AddDevice(PDRIVER_OBJECT DriverObject, PDEVICE_OBJECT pdo)
{
}
|
DriverObject參數指向一個驅動程序對象,就是你在DriverEntry例程中初始化的那個驅動程序對象。pdo參數指向設備堆棧底部的物理設備對象。
對于功能驅動程序,其AddDevice函數的基本職責是創建一個設備對象并把它連接到以pdo為底的設備堆棧中。相關步驟如下:
- 調用IoCreateDevice創建設備對象,并建立一個私有的設備擴展對象。
- 寄存一個或多個設備接口,以便應用程序能知道設備的存在。另外,還可以給出設備名并創建符號連接。
- 初始化設備擴展和設備對象的Flag成員。
- 調用IoAttachDeviceToDeviceStack函數把新設備對象放到堆棧上。
下面我將詳細解釋這些步驟。
創建設備對象
調用IoCreateDevice函數創建設備對象,例如:
PDEVICE_OBJECT fdo;
NTSTATUS status = IoCreateDevice(DriverObject,
sizeof(DEVICE_EXTENSION),
NULL,
FILE_DEVICE_UNKNOWN,
FILE_DEVICE_SECURE_OPEN,
FALSE,
&fdo);
|
第一個參數(DriverObject) 就是AddDevice的第一個參數。該參數用于在驅動程序和新設備對象之間建立連接,這樣I/O管理器就可以向設備發送指定的IRP。
第二個參數是設備擴展結構的大小。正如我在本章前面講到的,I/O管理器自動分配這個內存,并把設備對象中的DeviceExtension指針指向這塊內存。
第三個參數在本例中為NULL。它可以是命名該設備對象的UNICODE_STRING串的地址。決定是否命名設備對象以及以什么名字命名還需要仔細考慮,我將在本節后面深入討論這個問題。
第四個參數(FILE_DEVICE_UNKNOWN) 是表2-4中列出的設備類型。這個值可以被設備硬件鍵或類鍵中的超越值所替代,如果這兩個鍵都含有該參數的超越值,那么硬件鍵中的超越值具有更高的優先權。對于屬于某個已存在類的設備,必須在這些地方指定正確的值,因為驅動程序與外圍系統的交互需要依靠這個值。另外,設備對象的默認安全設置也依靠這個設備類型值。
第五個參數(FILE_DEVICE_SECURE_OPEN) 為設備對象提供Characteristics標志(見表2-3)。這些標志主要關系到塊存儲設備(如軟盤、CDROM、Jaz等等)。未公開標志位FILE_AUTOGENERATED_DEVICE_NAME僅用于內部使用,并不是DDK文檔忘記提到該標志。這個參數同樣也能被硬件鍵或類鍵中的對應值超越,如果兩個值都存在,那么硬件鍵中的超越值具有更高的優先權。
第六個參數(FALSE) 指出設備是否是排斥的。通常,對于排斥設備,I/O管理器僅允許打開該設備的一個句柄。這個值同樣也能被注冊表中硬件鍵和類鍵中的值超越,如果兩個超越值都存在,硬件鍵中的超越值具有更高的優先權。
注意
排斥屬性僅關系到打開請求的目標是命名設備對象。如果你遵守Microsoft推薦的WDM驅動程序設計方針,沒有為設備對象命名,那么打開請求將直接指向PDO。PDO通常不能被標記為排斥,因為總線驅動程序沒有辦法知道設備是否需要排斥特征。把PDO標為排斥的唯一的機會在注冊表中,即設備硬件鍵或類鍵的Properties子鍵含有Exclusive超越值。為了完全避免依賴排斥屬性,你應該利用IRP_MJ_CREAT例程彈出任何有違規行為的打開請求。
第七個參數(&fdo) 是存放設備對象指針的地址,IoCreateDevice函數使用該變量保存剛創建設備對象的地址。
如果IoCreateDevice由于某種原因失敗,則它返回一個錯誤代碼,不改變fdo中的值。如果IoCreateDevice函數返回成功代碼,那么它同時也設置了fdo指針。然后我們進行到下一步,初始化設備擴展,做與創建新設備對象相關的其它工作,如果在這之后又發現了錯誤,那么在返回前應先釋放剛創建的設備對象并返回狀態碼。見下面例子代碼:
NTSTATUS status = IoCreateDevice(...);
if (!NT_SUCCESS(status))
return status;
...
if (<some other error discovered>)
{
IoDeleteDevice(fdo);
return status;
}
|
NTSTATUS狀態代碼和NT_SUCCESS宏的解釋見下一章。
為設備命名
Windows NT使用對象管理器集中管理大量的內部數據結構,包括我們討論過的驅動程序對象和設備對象。David Solomon在《Inside Windows NT, Second Edition (Microsoft Press, 1998)》的第三章“System Mechanisms”中給出了關于Windows NT對象管理器和命名空間的一個比較完整的闡述。對象都有名稱,對象管理器用一個層次化的命名空間來管理這些名稱。圖2-16是DevView顯示的頂層對象名。圖中以文件夾形式顯示的對象是目錄對象,它可以包含子目錄或常規對象,其它圖標則代表正常對象。(從這一點上看,DevView與平臺SDK中的WINOBJ工具相類似,但WINOBJ不能給出設備對象和驅動程序的相關信息)

圖2-16. 用DevView觀察命名空間
通常設備對象都把自己的名字放到\Device目錄中。在Windows 2000中,設備的名稱有兩個用途。第一個用途,設備命名后,其它內核模式部件可以通過調用IoGetDeviceObjectPointer函數找到該設備,找到設備對象后,就可以向該設備的驅動程序發送IRP。
另一個用途,允許應用程序打開命名設備的句柄,這樣它們就可以向驅動程序發送IRP。應用程序可以使用標準的CreateFile API打開命名設備句柄,然后用ReadFile、WriteFile,和DeviceIoControl向驅動程序發出請求。應用程序打開設備句柄時使用\\.\路徑前綴而不是標準的UNC(統一命名約定)名稱,如C:\MYFILE.CPP或\\FRED\C-Drive\HISFILE.CPP。在內部,I/O管理器在執行名稱搜索前自動把\\.\轉換成\??\。為了把\??目錄中的名字與名字在其它目錄(例如,在\Device目錄)中的對象相連接,對象管理器實現了一種稱為符號連接(symbolic link)的對象。
符號連接
符號連接有點象桌面上的快捷方式,符號連接在Windows NT中的主要用途是把處于列表前面的DOS形式的名稱連接到設備上。圖2-17顯示了\??目錄的部分內容,這里就有一些符號名,例如,“C:”和其它一些用DOS命名方案命名的驅動器名稱,它們被連接到\Device目錄中,而這些設備對象的真正名稱就放在\Device目錄中。符號連接可以使對象管理器在分析一個名稱時能跳到命名空間的某個地方。例如,如果我用CreateFile打開名稱為“C:\MYFILE.CPP”的對象,對象管理器將以下面過程打開該文件:
- 內核模式代碼最開始看到的名稱是\??\C:\MYFILE.CPP。對象管理器在根目錄中查找“??”。
- 找到\??目錄后,對象管理器在其中查找“C:”。它發現找到的對象是一個符號連接,所以它就用這個符號連接組成一個新的內核模式路徑名:\Device\HarddiskVolume1\MYFILE.CPP,然后析取它。
- 使用新路徑名后,對象管理器重新在根目錄中查找“Device”。
- 找到\Device目錄后,對象管理器在其中查找“HarddiskVolume1”,最后它找到一個以該名字命名的設備。

圖2-17. \??目錄和部分符號連接
現在,對象管理器要創建一個IRP,然后把它發到HarddiskVolume1設備的驅動程序。該IRP最終將使某個文件系統驅動程序或其它驅動程序定位并打開一個磁盤文件。描述文件系統驅動程序的工作過程已經超出了本書的范圍。如果我們使用設備名COM1,那么最終收到該IRP的將是\Device\Serial0的驅動程序。
用戶模式程序可以調用DefineDosDevice創建一個符號連接,如下例:
BOOL okay = DefineDosDevice(DDD_RAW_TARGET_PATH, "barf", "\\Device\\SECTEST_0");
|
圖2-17中顯示了上面調用的結果。
如果你需要在WDM驅動程序中創建一個符號連接,可以調用IoCreateSymbolicLink函數:
IoCreateSymbolicLink(linkname, targname);
|
linkname是要創建的符號連接名,targname是要連接的名字。順便說一下,對象管理器并不關心targname是否是已存在對象的名字,如果連接到一個未定義的符號名,那么訪問該符號連接將簡單地收到一個錯誤。如果你想允許用戶模式程序能超越這個連接而轉到其它地方,應使用IoCreateUnprotectedSymbolicLink函數替代上面函數。
應該命名設備對象嗎?
決定為設備對象命名之前,你應該多想一想。如果命名了設備對象,那么任何內核模式程序都可以打開該設備的句柄。另外,任何內核模式或用戶模式程序都能創建連接到該設備的符號連接,并可以使用這個符號連接打開設備的句柄。你可能允許也可能不允許這種事情發生。
是否命名設備對象的主要考慮是安全問題。當有人打開一個命名對象的句柄時,對象管理器將檢查他是否有權這樣做。當IoCreateDevice為你創建設備對象時,它也為設備對象設置了一個默認安全描述符(基于第四個參數中的設備類型)。下面是三個基本分類,I/O管理器基于這些分類來選擇安全描述符。(參考表2-4中的第二列)
- 大部分文件系統設備對象(磁盤、CD-ROM、文件、磁帶)將得到“public default unrestricted”ACL(訪問控制表)。該表對系統(SYSTEM)和管理員(administrator)之外的所有賬戶給予了SYNCHRONIZE、READ_CONTROL、FILE_READ_ATTRIBUTES、FILE_TRAVERSE訪問權限。順便說一下,文件系統設備對象就是作為CreateFile函數的目標而存在,CreateFile函數將打開一個由文件系統管理的文件。
- 磁盤設備和網絡文件系統對象將得到與文件系統對象相同的ACL,但做了一些修改。例如,任何人對命名軟磁盤設備對象都有全部訪問權,管理員有足夠的權限運行ScanDisk。(用戶模式的網絡支持DLL需要更大的權限來訪問其對應文件系統驅動程序的設備對象,這就是網絡文件系統需要與其它文件系統區別對待的原因)
- 所有其它的設備對象將得到“public open unrestricted”ACL,它允許任何有設備句柄的人不受限制地使用該設備。
可以看出,如果非磁盤設備的驅動程序在調用IoCreateDevice時給出設備對象名,那么任何人都可以讀寫這個設備,因為默認安全設置幾乎允許用戶有全部的訪問權限,而且在創建符號連接時根本不進行安全檢查。安全檢查僅發生在對設備的打開操作上,基于命名對象的安全描述符。這對于在同一堆棧中的有更嚴格安全限制的其它設備對象也是這樣。
DevView可以顯示設備對象的安全屬性。你可以通過測試一個文件系統、一個磁盤設備、或者任何其它隨機存取設備了解到我剛描述過的默認操作規則。
PDO也得到一個默認安全描述符,但這個安全描述符可能被存儲在硬件鍵或類鍵的Properties子鍵中的安全描述符超越(當兩者都存在時,硬件鍵中的超越值有更高的優先權)。即使沒有指定安全描述符超越,如果硬件鍵或類鍵的Properties子鍵中有設備類型或特征的超越值,那么I/O管理器也會基于新類型為對象構造一個新的默認安全描述符。但I/O管理器不會超越PDO上面的任何其它設備對象的安全設置。因此,由于超越的影響,你不應該命名你的設備對象。但不要失望,應用程序仍可以使用注冊的接口(interface)訪問你的設備。
關于安全問題的最后一點:當對象管理器析取對象名時,對于名字的中間部分僅需要具有FILE_TRAVERSE訪問權,它僅在最終對象名上執行全部的安全檢查。所以,假設某個設備對象可以通過\Device\SECTEST_0名或符號連接\??\SecurityTest_0名到達,那么,如果設備對象的安全描述符設置為拒絕寫,則試圖以寫方式打開\\.\SecurityTest_0的用戶模式應用程序將被阻塞。但如果應用程序試圖打開名為\\.\SecurityTest_0\ExtraStuff的對象,那么打開請求(IRP_MJ_CREATE形式)將被允許,而此時用戶對\\.\SecurityTest_0\僅有FILE_TRAVERSE權限。I/O管理器希望設備驅動程序自己去處理額外名稱部件的安全檢查。
為了避免涉及到我剛描述過的安全問題,你可以在調用IoCreateDevice時指定設備特征參數為FILE_DEVICE_SECURE_OPEN。該標志將使Windows 2000在額外名稱部件存在的情況下仍檢查調用者是否有權限打開設備句柄。
設備名稱
如果你決定命名設備對象,通常應該把對象名放在名稱空間的\Device分支中。為了命名設備對象,首先應該創建一個UNICODE_STRING結構來存放對象名,然后把該串作為調用IoCreateDevice的參數:
UNICODE_STRING devname;
RtlInitUnicodeString(&devname, L"\\Device\\Simple0");
IoCreateDevice(DriverObject, sizeof(DEVICE_EXTENSION), &devname, ...);
|
我將在下一章中討論RtlInitUnicodeString的用法。
通常,驅動程序用設備類型串后加上一個以0開始的實例號作為設備對象名(如上面的Simple0)。一般,你不希望象我上面做的那樣使用帶有硬編碼性質的名稱。你希望用串操作函數動態地合成一個名字:
UNICODE_STRING devname;
static LONG lastindex = -1;
LONG devindex = InterlockedIncrement(&lastindex);
WCHAR name[32];
_snwprintf(name, arraysize(name), L"\\Device\\SIMPLE%2.2d", devindex);
RtlInitUnicodeString(&devname, name);
IoCreateDevice(...);
|
我將在后兩章中解釋上面代碼中出現的服務函數。如上面代碼所示,從私有設備類型得出的實例號應該是一個靜態變量。
設備接口
用舊的命名方法命名設備對象,并創建一個應用程序能夠使用的符號連接,存在著兩個主要問題。命名設備對象所帶來的潛在安全問題我們已經討論過。此外,訪問設備的應用程序需要先知道設備采用的命名方案。如果你的硬件僅由你的應用程序訪問,那么不會有什么問題。但是,如果有其它公司想為你的硬件寫應用程序,并且有許多硬件公司想制作相似的設備,那么設計一個合適的命名方案是困難的。最后,許多命名方案將依賴于程序員所說的自然語言,這不是一個好的選擇。
為了解決這些問題,WDM引入了一個新的設備命名方案,該方案是語言中立的、易于擴展的、可用于許多硬件和軟件廠商,并且易于文檔化。該方案依靠一個設備接口(device interface)的概念,它基本上是軟件如何訪問硬件的一個說明。一個設備接口被一個128位的GUID唯一標識。你可以用平臺SDK中的UUIDGEN工具或者GUIDGEN工具生成GUID,這兩個工具輸出同一種數,但格式不同。這個想法就象某些工業組織聯合起來共同制定某種硬件的標準訪問方法一樣。在標準制作過程中,產生了一些GUID,這些GUID將永遠關聯到某些接口上。

圖2-18. 使用GUIDGEN生成GUID
我想接口類似于蛋白質合成器,它能制作活細胞的細胞膜。訪問特定種類設備的應用程序有自己的蛋白質合成器,它就象一把鑰匙,可以插入到所有有匹配合成器的設備驅動程序中。如圖2-19。

圖2-19. 用設備接口匹配應用程序和設備
注冊設備接口 調用IoRegisterDeviceInterface函數,功能驅動程序的AddDevice函數可以注冊一個或多個設備接口:
#include <initguid.h> <--1
#include "guids.h" <--2
...
NTSTATUS AddDevice(...)
{
...
IoRegisterDeviceInterface(pdo, &GUID_SIMPLE, NULL, &pdx->ifname); <--3
...
}
|
- 我們包含了GUIDS.H頭文件,那里定義了DEFINE_GUID宏。DEFINE_GUID通常聲明一個外部變量。在驅動程序的某些地方,我們不得不為將要引用的每個GUID保留初始化的存儲空間。系統頭文件INITGUID.H利用某些預編譯指令使DEFINE_GUID宏在已經定義的情況下仍能保留該存儲空間。
- 我使用單獨的頭文件來保存我要引用的GUID定義。這是一個好的想法,因為用戶模式的代碼也需要包含這些定義,但它們不需要那些僅與內核模式驅動程序有關的聲明。
- IoRegisterDeviceInterface的第一個參數必須是設備PDO的地址。第二個參數指出與接口關聯的GUID,第三個參數指出額外的接口細分類名。只有Microsoft的代碼才使用名稱細分類方案。第四個參數是一個UNICODE_STRING串的地址,該串用于接收設備對象的符號連接名。
IoRegisterDeviceInterface的返回值是一個Unicode串,這樣在不知道驅動程序編碼的情況下,應用程序能用該串確定并打開設備句柄。順便說一下,這個名字比較丑陋;后面例子是我在Windows 98中為Sample設備生成的名字:\DosDevices\0000000000000007#{CAF53C68-A94C-11d2-BB4A-00C04FA330A6}。
注冊過程實際就是先創建一個符號連接名,然后再把它存入注冊表。之后,當響應PnP請求IRP_MN_START_DEVICE時,驅動程序將調用IoSetDeviceInterfaceState函數“使能”該接口:
IoSetDeviceInterfaceState(&pdx->ifname, TRUE);
|
在響應這個調用過程中,I/O管理器將創建一個指向設備PDO的符號連接對象。以后,驅動程序會執行一個功能相反的調用禁止該接口(用FALSE做參數調用IoSetDeviceInterfaceState)。最后,I/O管理器刪除符號連接對象,但它保留了注冊表項,即這個名字將總與設備的這個實例關聯;但符號連接對象與硬件一同到來或消失。
因為接口名最終指向PDO,所以PDO的安全描述符將最終控制設備的訪問權限。這樣比較好,因為只有管理員才可以通過控制臺控制PDO的安全屬性。
枚舉設備接口 內核模式代碼和用戶模式代碼都能定位含有支持它們感興趣接口的設備。下面我將解釋如何在用戶模式中枚舉所有含有特定接口的設備。枚舉代碼寫起來十分冗長,最后我不得不寫一個C++類來實現。你可以在DEVICELIST.CPP和DEVICELIST.H文件中找到這些代碼,這些文件是第八章“電源管理”中WDMIDLE例子的一部分。它們聲明并實現了一個CDeviceList類,該類包含一個CDeviceListEntry對象數組。這兩個類聲明如下:
class CDeviceListEntry
{
public:
CDeviceListEntry(LPCTSTR linkname, LPCTSTR friendlyname);
CDeviceListEntry(){}
CString m_linkname;
CString m_friendlyname;
};
class CDeviceList
{
public:
CDeviceList(const GUID& guid);
~CDeviceList();
GUID m_guid;
CArray<CDeviceListEntry, CDeviceListEntry&> m_list;
int Initialize();
};
|
該類使用了CString類和CArray模板,它們都是MFC的一部分。這兩個類的構造函數僅簡單地把它們的參數復制到數據成員中:
CDeviceList::CDeviceList(const GUID& guid)
{
m_guid = guid;
}
CDeviceListEntry::CDeviceListEntry(LPCTSTR linkname, LPCTSTR friendlyname)
{
m_linkname = linkname;
m_friendlyname = friendlyname;
}
|
所有實際的工作都發生在CDeviceList::Initialize函數中。其執行過程大致是這樣:先枚舉所有接口GUID與構造函數得到的GUID相同的設備,然后確定一個“友好”名,我們希望向最終用戶顯示這個名字。最后返回找到的設備號。下面是這個函數的代碼:
int CDeviceList::Initialize()
{
HDEVINFO info = SetupDiGetClassDevs(&m_guid, NULL, NULL, DIGCF_PRESENT | DIGCF_INTERFACEDEVICE); <--1
if (info == INVALID_HANDLE_VALUE)
return 0;
SP_INTERFACE_DEVICE_DATA ifdata;
ifdata.cbSize = sizeof(ifdata);
DWORD devindex;
for (devindex = 0; SetupDiEnumDeviceInterfaces(info, NULL, &m_guid, devindex, &ifdata); ++devindex) <--2
{
DWORD needed;
SetupDiGetDeviceInterfaceDetail(info, &ifdata, NULL, 0, &needed, NULL); <--3
PSP_INTERFACE_DEVICE_DETAIL_DATA detail = (PSP_INTERFACE_DEVICE_DETAIL_DATA) malloc(needed);
detail->cbSize = sizeof(SP_INTERFACE_DEVICE_DETAIL_DATA);
SP_DEVINFO_DATA did = {sizeof(SP_DEVINFO_DATA)};
SetupDiGetDeviceInterfaceDetail(info, &ifdata, detail, needed, NULL, &did));
TCHAR fname[256]; <--4
if (!SetupDiGetDeviceRegistryProperty(info,
&did,
SPDRP_FRIENDLYNAME,
NULL,
(PBYTE) fname,
sizeof(fname),
NULL)
&& !SetupDiGetDeviceRegistryProperty(info,
&did,
SPDRP_DEVICEDESC,
NULL,
(PBYTE) fname,
sizeof(fname),
NULL)
)
_tcsncpy(fname, detail->DevicePath, 256);
CDeviceListEntry e(detail->DevicePath, fname); <--5
free((PVOID) detail);
m_list.Add(e);
}
SetupDiDestroyDeviceInfoList(info);
return m_list.GetSize();
}
|
- 該語句打開一個枚舉句柄,我們用它尋找寄存了指定GUID接口的所有設備。
- 循環調用SetupDiEnumDeviceInterfaces以尋找每個匹配的設備。
- 有兩項信息是我們需要的,接口的“細節”信息和設備實例信息。這個“細節”信息就是設備的符號名。因為它的長度可變,所以我們兩次調用了SetupDiGetDeviceInterfaceDetail。第一次調用確定了長度,第二次調用獲得了名字。
- 通過詢問注冊表中的FriendlyName鍵或DeviceDesc鍵,我們獲得了設備的“友好”名稱。
- 我們用設備符號名同時作為連接名和友好名創建了類CDeviceListEntry的一個臨時實例e。
其它全局性的設備初始化操作
在AddDevice中還需要加入其它一些步驟來初始化設備對象,下面我將按順序描述這些步驟。
初始化設備擴展
設備擴展的內容和管理全部由用戶決定。該結構中的數據成員應直接反映硬件的專有細節以及對設備的編程方式。大多數驅動程序都會在這里放入一些數據項,下面代碼聲明了一個設備擴展結構:
typedef struct _DEVICE_EXTENSION { <--1
PDEVICE_OBJECT DeviceObject; <--2
PDEVICE_OBJECT LowerDeviceObject; <--3
PDEVICE_OBJECT Pdo; <--4
UNICODE_STRING ifname; <--5
IO_REMOVE_LOCK RemoveLock; <--6
DEVSTATE devstate; <--7
DEVSTATE prevstate;
DEVICE_POWER_STATE devpower;
SYSTEM_POWER_STATE syspower;
DEVICE_CAPABILITIES devcaps; <--8
...
} DEVICE_EXTENSION, *PDEVICE_EXTENSION;
|
- 我模仿DDK中官方的結構聲明模式聲明了這個結構。
- 我們可以用設備對象中的DeviceExtension指針定位自己的設備擴展。同樣,我們有時也需要在給定設備擴展時能定位設備對象。因為某些函數的邏輯參數就是設備擴展本身(這里有設備每個實例的全部信息)。所以,我認為這里應該有一個DeviceObject指針。
- 我在一些地方曾提到過,在調用IoAttachDeviceToDeviceStack函數時,應該把緊接著你下面的設備對象的地址保存起來。LowerDeviceObject成員用于保存這個地址。
- 有一些服務例程需要PDO的地址,而不是堆棧中某個高層設備對象的地址。由于定位PDO非常困難,所以最好的辦法是在AddDevice執行時在設備擴展中保存一個PDO地址。
- 無論你用什么方法(符號連接或設備接口)命名你的設備,都希望能容易地獲得這個名字。所以,這里我用一個Unicode串成員ifname來保存設備接口名。如果你使用一個符號連接名而不是設備接口,應該使用一個有相關含義的成員名,例如“linkname”。
- 當你調用IoDeleteDevice刪除這個設備對象時,需要使用一個自旋鎖來解決同步安全問題,我將在第六章中討論同步問題。因此,需要在設備擴展中分配一個IO_REMOVE_LOCK對象。AddDevice有責任初始化這個對象。
- 你可能需要一個成員來記錄設備當前的PnP狀態和電源狀態。DEVSTATE和POWERSTATE是枚舉類型變量,我假設事先已經在頭文件中聲明了這些變量類型。我將在后面章節中討論這些狀態變量的用途。
- 電源管理的另一個部分涉及電源能力設置的恢復,設備擴展中的devcaps結構用于保存這些設置。
下面是AddDevice中的初始化語句(著重設備擴展部分的初始化):
NTSTATUS AddDevice(...)
{
PDEVICE_OBJECT fdo;
IoCreateDevice(..., sizeof(DEVICE_EXTENSION), ..., &fdo);
PDEVICE_EXTENSION pdx = (PDEVICE_EXTENSION) fdo->DeviceExtension;
pdx->DeviceObject = fdo;
pdx->Pdo = pdo;
IoInitializeRemoveLock(&pdx->RemoveLock, ...);
pdx->devstate = STOPPED;
pdx->devpower = PowerDeviceD0;
pdx->syspower = PowerSystemWorking;
IoRegisterDeviceInterface(..., &pdx->ifname);
pdx->LowerDeviceObject = IoAttachDeviceToDeviceStack(...);
}
|
初始化默認的DPC對象
許多設備使用中斷來報告操作完成。我將在第七章“讀寫數據”中討論中斷處理,其中對中斷服務例程(ISR)能做什么做了嚴格的限定。特別是ISR不能調用用于報告IRP完成的例程(IoCompleteRequest)。利用DPC(推遲過程調用)可以繞過這個限制。你的設備對象中應包含一個輔助DPC對象,它可以調度你的DPC例程,該對象應該在設備對象創建后不久被初始化。
NTSTATUS AddDevice(...)
{
IoCreateDevice(...);
IoInitializeDpcRequest(fdo, DpcForIsr);
}
|
設置緩沖區對齊掩碼
執行DMA傳輸的設備直接使用內存中的數據緩沖區工作。HAL要求DMA傳輸中使用的緩沖區必須按某個特定界限對齊,而且設備也可能有更嚴格的對齊需求。設備對象中的AlignmentRequirement域表達了這個約束,它是一個位掩碼,等于要求的地址邊界減一。下面語句可以把任何地址圈入這個界限:
PVOID address = ...;
SIZE_T ar = fdo->AlignmentRequirement;
address = (PVOID) ((SIZE_T) address & ~ar);
|
還可以把任意地址圈入下一個對齊邊界:
PVOID address = ...;
SIZE_T ar = fdo->AlignmentRequirement;
address = (PVOID) (((SIZE_T) address + ar) & ~ar);
|
在這兩段代碼中,我使用了SIZE_T把指針類型(它可以是32位也可以是64位,這取決于編譯的目標平臺)轉化成一個整型,該整型與原指針有同樣的跨度范圍。
IoCreateDevice把新設備對象中的AlignmentRequirement域設置成HAL要求的值。例如,Intel的x86芯片沒有對齊需求,所以AlignmentRequirement的默認值為0。如果設備需要更嚴格的緩沖區對齊(例如設備有總線主控的DMA能力,要求對齊數據緩沖區),應該修改這個默認值,如下:
if (MYDEVICE_ALIGNMENT - 1 > fdo->AlignmentRequirement)
fdo->AlignmentRequirement = MYDEVICE_ALIGNMENT - 1;
|
我假設你在驅動程序某處已定義了一個名為MYDEVICE_ALIGNMENT的常量,它是2的冪,代表設備的數據緩沖區對齊需求。
其它對象
設備可能還有其它一些需要在AddDevice中初始化的對象。這些對象可能包括各種同步對象,各種隊列頭(queue anchors),聚集/分散列表緩沖區,等等。事實上,在本書的其它地方討論這些對象的初始化更合適。
初始化設備標志
設備對象中有兩個標志位需要在AddDevice中初始化,并且它們在以后也不會改變,它們是DO_BUFFERED_IO和DO_DIRECT_IO標志。你只能設置并使用其中一個標志,它將決定你以何種方式處理來自用戶模式的內存緩沖區。(我將在第七章中討論這兩種緩沖模式的不同,以及你如何選擇) 由于任何在后面裝入的上層過濾器驅動程序將復制你的標志設置,所以在AddDevice中做這個選擇十分重要。如果你在過濾器驅動程序裝入后改變了設置,它們可能會不知道。
設備對象中有兩個標志位屬于電源管理范疇。與前兩個緩沖區標志不同,這兩個標志在任何時間都可以被改變。我將在第八章中詳細討論它們,但這里我先介紹一下。DO_POWER_PAGABLE意味著電源管理器將在PASSIVE_LEVEL級上向你發送IRP_MJ_POWER請求。DO_POWER_INRUSH意味著你的設備在上電時將汲取大量電流,因此,電源管理器將確保沒有其它INRUSH設備同時上電。
設置初始電源狀態
大部分設備一開始就進入全供電狀態。如果你知道你的設備的初始電源狀態,應該告訴電源管理器:
POWER_STATE state;
state.DeviceState = PowerDeviceD0;
PoSetPowerState(fdo, DevicePowerState, state);
|
電源管理的細節請見第八章。
建立設備堆
每個過濾器驅動程序和功能驅動程序都有責任把設備對象放到設備堆棧上,從PDO開始一直向上。你可以調用IoAttachDeviceToDeviceStack完成你那部分工作:
NTSTATUS AddDevice(..., PDEVICE_OBJECT pdo)
{
PDEVICE_OBJECT fdo;
IoCreateDevice(..., &fdo);
pdx->LowerDeviceObject = IoAttachDeviceToDeviceStack(fdo, pdo);
}
|
IoAttachDeviceToDeviceStack的第一個參數是新創建的設備對象的地址。第二個參數是PDO地址。AddDevice的第二個參數也是這個地址。返回值是緊接著你下面的任何設備對象的地址,它可以是PDO,也可以是其它低級過濾器設備對象。如果該函數失敗則返回一個NULL指針,因此你的AddDevice函數也是失敗的,應返回STATUS_DEVICE_REMOVED。
清除DO_DEVICE_INITIALIZING標志
在AddDevice中最后一件需要做的事是清除設備對象中的DO_DEVICE_INITIALIZING標志:
fdo->Flags &= ~DO_DEVICE_INITIALIZING;
|
當這個標志設置時,I/O管理器將拒絕任何打開該設備句柄的請求或向該設備對象上附著其它設備對象的請求。在驅動程序完成初始化后,必須清除這個標志。在以前版本的Windows NT中,大部分驅動程序在DriverEntry中創建所有需要的設備對象。當DriverEntry返回時,I/O管理器自動遍歷設備對象列表并清除該標志。但在WDM驅動程序中,設備對象在DriverEntry返回后才創建,所以I/O管理器不會自動清除這個標志,驅動程序必須自己清除它。