很明顯的,驅動程序和客戶應用程序經常需要進行數據交換,但我 們知道驅動程序和客戶應用程序可能不在同一個地址空間,因此操 作系統必須解決兩者之間的數據交換。這就就設計到設備的I/O緩衝策略。
讀寫請求的I/O緩衝策略
前面說到通過設置Device對象的Flag可以選擇控制處理讀寫請求的 I/O緩衝策略。下面對這些緩衝策略分別做一介紹。
1、 緩衝I/O(DO_BUFFERED_IO)
在讀寫請求的一開始,I/O管理器檢查用戶緩衝區的可訪問性,然 後分配與調用者的緩衝區一樣大的非份頁池,並把它的地址放在 IRP的AssociatedIrp.SystemBuffer域中。驅動程序就利用這個域來進行實際數據的傳輸。
對於IRP_MJ_READ讀請求,I/O管理器還把IRP的UserBuffer域設置 成調用者緩衝區的用戶空間地址。當請求完成時,I/O管理器利用 這個地址將數據從驅動程序的系統空間拷貝回調用者的緩衝區。對 於IRP_MJ_WRITE寫請求,UserBuffer被設置爲NULL,並把用戶緩衝 區的數據拷貝到系統緩衝區中。
2、 直接I/O(DO_DIRECT_IO)
I/O管理器首先檢查用戶緩衝區的可訪問性,並在物理內存中鎖定 它。然後它爲該緩衝區創建一個內存描述表(MDL),並把MDL的地址 存放在IRP的MdlAddress域中。AssociatedIrp.SystemBuffer和 UserBuffer都被設置爲NULL。驅動程序可以調用函數 MmGetSystemAddressForMdl得到用戶緩衝區的系統空間地址,從而 進行數據操作。這個函數將調用者的緩衝區映射到非份頁的地址空 間。驅動程序完成I/O請求後,系統自動從系統空間解除緩衝區的 映射。
3、 這兩種方法都不是
這種情況比較少用,因爲這需要驅動程序自己來處理緩衝問題。 I/O管理器僅把調用者緩衝區的用戶空間地址放到IRP的UserBuffer 域中。我們並不推薦這種方式。
IOCTL緩衝區的緩衝策略
IOCTL請求涉及來自調用者的輸入緩衝區和返回到調用者的輸出 緩衝區。爲了理解IOCTL請求,我們先來看看WIN32 API DeviceIoControl函數的原型。
BOOL DeviceIoControl (
HANDLE hDevice, // 設備句柄
DWORD dwIoControlCode, // IOCTL請求操作代碼
LPVOID lpInBuffer, // 輸入緩衝區地址
DWORD nInBufferSize, // 輸入緩衝區大小
LPVOID lpOutBuffer, // 輸出緩衝區地址
DWORD nOutBufferSize, // 輸出緩衝區大小
LPDWORD lpBytesReturned, // 存放返回字節數的指針
LPOVERLAPPED lpOverlapped // 用於同步操作的Overlapped結構體指針
);
IOCTL請求有四種緩衝策略,下面一一介紹。
1、 輸入輸出緩衝I/O(METHOD_BUFFERED)
I/O管理器首先分配一個非份頁池,它足夠大地存放調用者的輸 入或輸出緩衝區(不管哪個更大)。非份頁緩衝區的地址放在IRP的AssociatedIrp.Sys temBuffer域中,然後把IOCTL的輸入數據拷貝 到這個非份頁緩衝區中,並把IRP的UserBuffer域設置成調用者輸 出緩衝區的用戶空間地址。當驅動程序完成IOCTL請求時,I/O管理 器將這個非份頁緩衝區中的數據拷貝到調用者的輸出緩衝區。 注意這裏同一個非份頁池同時用於輸入和輸出緩衝區,因此驅動 程序在向緩衝區寫東西之前應該把輸入的所有數據讀出來。
2、 直接輸入緩衝輸出I/O(METHOD_IN_DIRECT)
I/O管理器首先檢查調用者輸入緩衝區的可訪問性,並在物理內存 中將其鎖定。然後爲該輸入緩衝區創建一個MDL,並把指定該MDL的 指針存放到IRP的MdlAddress域中。 同時,I/O管理器還在非份頁池中分配一輸出緩衝區,並把這個緩衝 區的地址存放在IRP的AssociatedIrp.SystemBuffer域中,並把IRP 的UserBuffer域設置成調用者輸出緩衝區的用戶空間地址。當驅動 程序完成IOCTL請求時,I/O管理器將非份頁緩衝區中的數據拷貝到 調用者的輸出緩衝區。
3、 緩衝輸入直接輸出I/O(METHOD_OUT_DIRECT)
I/O管理器首先檢查調用者輸出緩衝區的可訪問性,並在物理內存中 將其鎖定。然後爲該輸出緩衝區創建一個MDL,並把指定該MDL的指針 存放到IRP的MdlAddress域中。同時,I/O管理器還在非份頁池中分配一輸入緩衝區,並把這個緩衝區的地址存放在IRP的AssociatedIrp.SystemBuffer域中, 同時把 調用者用戶輸入緩衝區中的數據拷貝到系統緩衝區中,並把IRP的 UserBuffer域設置爲NULL。
4、 上面三種方法都不是(METHOD_NEITHER)
I/O管理器把調用者的輸入緩衝區的地址放到IRP當前I/O堆棧單元的Parameters.Devi ceIoControl.Type3InputBuffer域中,把輸出緩衝 區的地址存放到IRP的UserBuffer域中。這兩個地址都是用戶空間地 址。
從上面的說明可以看出,在執行緩衝I/O時,I/O管理器將在非份頁池 中分配內存,如果調用者的緩衝區比較大時,分配的非份頁池也將 比較大。非份頁池是系統比較寶貴的資源,因此,如果調用者的緩 衝區比較大時,我們一般採用直接I/O的方式(例如磁盤讀寫請求等), 這樣不僅節省系統資源,另一方面由於省去了I/O管理器在系統緩衝 區和調用者緩衝區之間的數據拷貝,也提高了效率,這對存在大量 數據傳送的驅動程序尤其明顯。
可以注意到DDK中的Samples下,幾乎所有的例程的讀寫請求都是直 接I/O的,而對於IOCTL請求則是緩衝區I/O的居多。
下面以changerDisk的IRP_MJ_DEVICE_CONTROL例程中的一段程序來 加強IOCTL緩衝策略的認識和用法。在該例中所有的IOCTL請求的緩 衝策略都是METHOD_BUFFERED。
NTSTATUS
ChangerDiskDeviceControl(
PDEVICE_OBJECT DeviceObject,
PIRP Irp
)
{
PIO_STACK_LOCATION irpStack = IoGetCurrentIrpStackLocation(Irp);
ULONG ControlCode;
ULONG InputLength, OutputLength;
PVOID InputBuffer, OutputBuffer;
…
ControlCode = irpStack->Parameters.DeviceIoControl.IoControlCode;
// IOCTL請求功能代碼存放在IRP當前I/O堆棧單元的
// Parameters.DeviceIoControl.IoControlCode中。;
InputLength = irpStack->Parameters.DeviceIoControl.InputBufferLength;
// 輸入緩衝區的大小存放在IRP當前I/O堆棧單元的
// Parameters.DeviceIoControl.InputBufferLength中。
OutputLength = irpStack->Parameters.DeviceIoControl.OutputBufferLength;
// 輸出緩衝區的大小存放在IRP當前I/O堆棧單元的
// Parameters.DeviceIoControl. OutputBufferLength中。
InputBuffer = OutputBuffer = Irp->AssociatedIrp.SystemBuffer;
// 由於本例中所有的IOCTL請求都是輸入輸出緩衝的,所以輸入輸出緩衝區 // 的地址就是IRP的AssociatedIrp.SystemBuffer域。
….
}