1. 程式人生 > >驅動開發(13)IRP 的非同步完成和 CancelRoutine

驅動開發(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. 引數1是要設定取消例程的 IRP 指標。
  2. 引數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;
}

效果圖: