驅動開發(13)IRP 的非同步完成和 CancelRoutine
在之前的博文中,我們對於IRP,都是同步完成的,但是 Windows 對非同步操作很友好,我們來看看如何非同步完成 IRP 。
在應用程式中非同步訪問裝置
在開始之前,我認為有必要提一句非同步訪問裝置。在之前的博文中,與驅動通訊的程式碼都是採用同步訪問裝置的,其實所謂同步訪問,是 Win32 子系統封裝了“等待”這一過程。 Win32API 會在內部建立事件,並在向裝置傳送 I/O 請求後直接等待事件被完成,一旦驅動程式完成 I/O ,那麼就會啟用事件,從而使 Win32API 中的等待狀態結束,執行緒恢復執行。(關於“事件”物件,見上一篇博文“核心中開啟多執行緒和同步物件”)
其實,這個等待操作我們可以自己來做,就像這樣:
#include "stdafx.h"
#include<Windows.h>
int _tmain(int argc, _TCHAR* argv[])
{
//如果 CreateFile 的第 6 個引數 dwFlagsAndAttributes 被指定為 FILE_FLAG_OVERLAPPED,
HANDLE handle = CreateFile(TEXT("\\\\.\\D:\\1.txt"), GENERIC_READ, 0, NULL, OPEN_EXISTING, FILE_FLAG_OVERLAPPED | FILE_ATTRIBUTE_NORMAL, NULL );
if (handle == INVALID_HANDLE_VALUE){
MessageBoxA(0, "開啟檔案/裝置失敗", "錯誤", 0);
return 0;
}
unsigned char buffer[50] = { 0 };
OVERLAPPED over = { 0 };
HANDLE Event = CreateEvent(NULL, FALSE, FALSE, NULL);
over.hEvent = Event;
ReadFile(handle, buffer, 49, NULL, &over);
//Do somethings
WaitForSingleObject(Event, INFINITE);
for (int i = 0; i < sizeof(buffer); i++)
printf("0x%X ", buffer[i]);
CloseHandle(handle);
getchar();
return 0;
}
當然,還有一種方法非同步訪問裝置,即使用ReadFileEx/WriteFileEx,這是通過APC來實現非同步訪問的,這裡不展開了。
為什麼要說非同步訪問裝置呢,這是為了避免大家和下面的非同步完成IRP產生混淆,同時也有必要讓大家明白下 Win32 子系統對同步讀寫裝置的實現,同時,瞭解非同步訪問裝置有助於下面的對派遣函式和IRP相關內容的理解。
要非同步訪問裝置,需要得到驅動程式的支援,當應用程式呼叫 I/O 函式時,驅動程式的 Dispatch Function 會被呼叫,當 Dispatch Function 返回時,應用程式的 I/O 函式才能退出。驅動程式呼叫 IoMarkIrpPending 並返回 STATUS_PENDING 時,意味著 I/O 請求已經掛起,如果此時應用程式選擇了非同步訪問裝置,那麼 I/O 函式會退出,當 I/O 真正處理完成時(即呼叫 IoCompleteRequest ),應用程式建立的 Event 就會被激發!WaitForSingleObject 會返回。而同步訪問,其實是在 I/O 函式內部建立並等待了這個事件。
非同步完成 IRP
要非同步完成IRP,需要在派遣函式中不呼叫 IoCompleteRequest ,而是呼叫 IoMarkIrpPending 函式,同時需要派遣函式返回 STATUS_PENDING
通過上面的非同步訪問裝置我們可以發現,應用程式會建立一個 Event 物件,要使應用程式等待完成,需要啟用這個事件,這個過程不需要驅動程式自己去做的,完成IRP時如果呼叫 IoCompleteRequest , IoCompleteRequest 會自動啟用此事件。
也就是說,呼叫 IoMarkIrpPending 後,應用程式建立的事件並沒有被啟用,即,如果應用程式等待此事件(如同步讀寫),並不會使其退出等待,他的作用僅僅是使派遣函式返回,以便於驅動程式在其他地方完成 IRP 。
就像這樣,之後,此 IRP 被掛起,驅動程式可以儲存此 IRP 的指標,在未來某個必要的時刻完成他:
//in a Dispatch Function
IoMarkIrpPending(pIrp);
return STATUS_PENDING;
CancelRoutine 取消 I/O 例程
有些時候,驅動程式需要允許應用程式“取消”某個 I/O 請求。比如,應用程式程式非同步讀寫檔案時可以實現一個“終止”按鈕。這需要驅動程式的支援。如果我們希望給自己的裝置實現這種功能,就需要給 IRP 設定取消例程。應用程式通過 Win32 子系統提供的 CancelIO API來取消一個 I/O 請求,這會呼叫驅動程式設定的取消例程。
設定取消例程的核心函式是 IoSetCancelRoutine,這個函式的原型如下:
PDRIVER_CANCEL IoSetCancelRoutine(
_In_ PIRP Irp,
_In_ PDRIVER_CANCEL CancelRoutine
);
- 引數1是要設定取消例程的 IRP 指標。
- 引數2是取消例程指標。如果此引數為 NULL ,則刪除取消例程
返回值:當前 IRP 存在取消例程時返回 Irp->CancelRoutine ,否則返回 NULL 。
取消例程的原型如下:
DRIVER_CANCEL Cancel;
VOID Cancel(
_Inout_ struct _DEVICE_OBJECT *DeviceObject,
_Inout_ struct _IRP *Irp
)
{ ... }
IoCancelIrp 會在內部呼叫 IoAcquireCancelSpinLock 獲取cancel自旋鎖,因此,在取消例程中,必須呼叫 IoReleaseCancelSpinLock 函式釋放自旋鎖,否則會導致藍屏宕機。
IRP 非同步完成和 CancelRoutine 例程程式碼
最後,我們以一個例程來結束本篇博文,此例中,我們在處理”讀”的派遣函式中掛起 IRP 來演示 IRP 非同步完成,並設定 CancelRoutine ,並在取消例程中將掛起的 IRP 完成。
應用程式原始碼:
#include "stdafx.h"
#include<Windows.h>
int _tmain(int argc, _TCHAR* argv[])
{
//開啟裝置
HANDLE handle = CreateFileA("\\\\.\\MyDevice1_link", GENERIC_READ | GENERIC_WRITE, 0, NULL, OPEN_EXISTING, FILE_FLAG_OVERLAPPED | FILE_ATTRIBUTE_NORMAL, NULL);
if (handle == INVALID_HANDLE_VALUE){
MessageBoxA(0, "開啟裝置失敗", "錯誤", 0);
return 0;
}
unsigned char buffer[50] = { 0 };
DWORD len;
OVERLAPPED over = { 0 };
HANDLE Event = CreateEvent(NULL, FALSE, FALSE, NULL);
over.hEvent = Event;
if (!ReadFile(handle, buffer, 49, &len, &over)){
if (GetLastError() == ERROR_IO_PENDING){
puts("I/O is Pending");
}
}
Sleep(3000);
CancelIo(handle);
CloseHandle(handle);
return 0;
}
驅動程式原始碼:
#include <ntddk.h>
extern "C" VOID DriverUnload(PDRIVER_OBJECT pDriverObject);
extern "C" NTSTATUS DefDispatchRoutine(PDEVICE_OBJECT pDevObj, PIRP pIrp);
extern "C" NTSTATUS ReadDispatchRoutine(PDEVICE_OBJECT pDevObj, PIRP pIrp);
//我們定義的裝置擴充套件
typedef struct _DEVICE_EXTENSION {
UNICODE_STRING SymLinkName;//符號連結名
} DEVICE_EXTENSION, *PDEVICE_EXTENSION;
#pragma code_seg("INIT")
extern "C" NTSTATUS DriverEntry(PDRIVER_OBJECT pDriverObject, PUNICODE_STRING pRegistryPath)
{
DbgPrint("DriverEntry\r\n");
pDriverObject->DriverUnload = DriverUnload;//註冊驅動解除安裝函式
//註冊派遣函式
pDriverObject->MajorFunction[IRP_MJ_CREATE] = DefDispatchRoutine;
pDriverObject->MajorFunction[IRP_MJ_CLOSE] = DefDispatchRoutine;
pDriverObject->MajorFunction[IRP_MJ_WRITE] = DefDispatchRoutine;
pDriverObject->MajorFunction[IRP_MJ_READ] = ReadDispatchRoutine;
NTSTATUS status;
PDEVICE_OBJECT pDevObj;
PDEVICE_EXTENSION pDevExt;
//建立裝置名稱的字串
UNICODE_STRING devName;
RtlInitUnicodeString(&devName, L"\\Device\\MyDevice1");
//建立裝置
status = IoCreateDevice(pDriverObject, sizeof(DEVICE_EXTENSION), &devName, FILE_DEVICE_UNKNOWN, 0, TRUE, &pDevObj);
if (!NT_SUCCESS(status))
return status;
pDevObj->Flags |= DO_BUFFERED_IO;//將裝置設定為緩衝裝置
pDevExt = (PDEVICE_EXTENSION)pDevObj->DeviceExtension;//得到裝置擴充套件
//建立符號連結
UNICODE_STRING symLinkName;
RtlInitUnicodeString(&symLinkName, L"\\??\\MyDevice1_link");
pDevExt->SymLinkName = symLinkName;
status = IoCreateSymbolicLink(&symLinkName, &devName);
if (!NT_SUCCESS(status))
{
IoDeleteDevice(pDevObj);
return status;
}
return STATUS_SUCCESS;
}
extern "C" VOID DriverUnload(PDRIVER_OBJECT pDriverObject)
{
DbgPrint("DriverUnload\r\n");
PDEVICE_OBJECT pDevObj;
pDevObj = pDriverObject->DeviceObject;
PDEVICE_EXTENSION pDevExt = (PDEVICE_EXTENSION)pDevObj->DeviceExtension;//得到裝置擴充套件
//刪除符號連結
UNICODE_STRING pLinkName = pDevExt->SymLinkName;
IoDeleteSymbolicLink(&pLinkName);
//刪除裝置
IoDeleteDevice(pDevObj);
}
extern "C" NTSTATUS DefDispatchRoutine(PDEVICE_OBJECT pDevObj, PIRP pIrp)
{
DbgPrint("DefDispatchRoutine\r\n");
NTSTATUS status = STATUS_SUCCESS;
pIrp->IoStatus.Status = status;
pIrp->IoStatus.Information = 0;
IoCompleteRequest(pIrp, IO_NO_INCREMENT);
return status;
}
VOID Read_CancelIRP(PDEVICE_OBJECT DeviceObject, PIRP pIrp)
{
DbgPrint("Read_CancelIRP pIrp: 0x%X\r\n", pIrp);
//完成狀態設定為 STATUS_CANCELLED
pIrp->IoStatus.Status = STATUS_CANCELLED;
//操作位元組數
pIrp->IoStatus.Information = 0;
//完成 IRP
IoCompleteRequest(pIrp, IO_NO_INCREMENT);
//釋放 Cancel 自旋鎖
IoReleaseCancelSpinLock(pIrp->CancelIrql);
}
extern "C" NTSTATUS ReadDispatchRoutine(PDEVICE_OBJECT pDevObj, PIRP pIrp)
{
DbgPrint("ReadDispatchRoutine\r\n");
NTSTATUS status = STATUS_SUCCESS;
//得到裝置擴充套件
PDEVICE_EXTENSION pDevExt = (PDEVICE_EXTENSION)pDevObj->DeviceExtension;
//得到I/O堆疊的當前這一層,也就是IO_STACK_LOCATION結構的指標
PIO_STACK_LOCATION stack = IoGetCurrentIrpStackLocation(pIrp);
//ULONG ReadLength = stack->Parameters.Read.Length;//得到讀的長度
//ULONG ReadOffset = (ULONG)stack->Parameters.Read.ByteOffset.QuadPart;//得到讀偏移量
//DbgPrint("ReadLength: %d\r\nReadOffset: %d\r\n", ReadLength, ReadOffset);//輸出相關資訊
//PVOID buffer = pIrp->AssociatedIrp.SystemBuffer;//得到緩衝區指標
//if (ReadOffset + ReadLength > BUFFER_LENGTH){
// //如果要操作的超出了緩衝區,則失敗完成IRP,返回無效
// DbgPrint("E: The size of the data is too long.\r\n");
// status = STATUS_FILE_INVALID;//會設定使用者模式下的GetLastError
// ReadLength = 0;//設定操作了0位元組
//}
//else{
// //沒有超出,則進行緩衝區複製
// DbgPrint("OK, I will copy the buffer.\r\n");
// RtlMoveMemory(buffer, pDevExt->buffer + ReadOffset, ReadLength);
// status = STATUS_SUCCESS;
//}
IoSetCancelRoutine(pIrp, Read_CancelIRP);
IoMarkIrpPending(pIrp);
DbgPrint("IoMarkIrpPending pIrp: 0x%X\r\n", pIrp);
return STATUS_PENDING;
//pIrp->IoStatus.Status = status;//設定IRP完成狀態,會設定使用者模式下的GetLastError
//pIrp->IoStatus.Information = ReadLength;//設定操作位元組數
//IoCompleteRequest(pIrp, IO_NO_INCREMENT);//完成IRP
//return status;
}
效果圖: