上一章大概介绍了WDM/WDF的驱动模型,
链接:https://blog.csdn.net/o0xwh_93150o/article/details/104213348
这一章我们主要来看一下驱动程序中的IO请求是如何处理的。
其实驱动程序的主要功能也就是负责处理IO请求,其中大部分IO请求是在派遣函数中处理的。用户模式下的所有对驱动程序的IO请求,全部由操作系统转化为一个叫做IRP(IO request package)的数据结构,不同的IRP会被“派遣”到不同的派遣函数(dispatch function)中。IRP的处理机制类似windows应用程序中的“消息处理”机制,驱动程序接收到不同类型的IRP后,会进入不同的派遣函数。
首先,IRP拥有两个基本的属性,一个是MajorFunction,另一个则是MinorFunction,分别记录IRP的主类型和子类型。操作系统根据MajorFunction将IRP派遣到不同的派遣函数中,然后再判断这个IRP属于哪种MinorFunction。如下所示:
//创建设备,CreateFile时触发
DriverObject->MajorFunction[IRP_MJ_CREATE] = HelloDispatchCreate;
//关闭设备,CloseHandle时触发
DriverObject->MajorFunction[IRP_MJ_CLOSE] = HelloDispatchClose;
//对设备进行WriteFile时触发
DriverObject->MajorFunction[IRP_MJ_WRITE] = HelloDispatchWrite;
//对设备进行ReadFile时触发
DriverObject->MajorFunction[IRP_MJ_READ] = HelloDispatchRead;
当然,IRP的类型远不止这四种,对于其他没有设置的IRP类型,系统会默认将这些IRP类型与IoInvalidDeviceRequest函数关联。大部分的IRP都源于文件IO处理Win32 API,例如CreateFile与ReadFile。假如我们对这个类型的IRP不需要特别处理的话,只需要在派遣函数中直接返回success就可以了。如下:
NTSTATUS status = STATUS_SUCCESS;
//设置irp的完成状态
pIrp->IoStatus.Status = status;
//设置irp操作的实际字节数
pIrp->IoStatus.Infomation = 0;
//结束irp请求时需调用IoCompleteRequest
IoCompleteRequest(pIrp, IO_NO_INCREMENT);
return status;
在驱动程序设置完这些IRP之后,是不是就可以使用用户应用程序向该驱动发送IO请求了呢?其实并不是如此,我们都知道,在用户态假如要操作这个设备,CreateFile时必要的一个参数,就是设备句柄,但是在用户态,其实是看不到内核态设备的句柄的,例如该设备的句柄为“\\Devices\\HelloDevice0”,这个句柄仅能被内核模式下的其他驱动所看见,所以我们需要创建一个符号链接,让用户态的程序可以得到这个句柄,类似“\\DosDevices\\HelloDevice0_LINK”这样。如下实例:
#define MYWDF_KDEVICE L“\\Devices\\HelloDevice0”
#define MYWDF_LINKNAME L“\\DosDevices\\HelloDevice0_LINK”
// 创建设备
WDF_OBJECT_ATTRIBUTES_INIT_CONTEXT_TYPE(&object_attribs, DEVICE_CONTEXT_CTRL);
//为设备首先分配内存
device_init = WdfControlDeviceInitAllocate(WdfDeviceGetDriver(Device), &SDDL_DEVOBJ_SYS_ALL_ADM_RWX_WORLD_RW_RES_R);
RtlInitUnicodeString(&ustring, MYWDF_KDEVICE);
//将设备名字存入device_init中
status = WdfDeviceInitAssignName(device_init, &ustring);
if (!NT_SUCCESS(status)) {
//
}
//创建一个controldevice,用于创建符号链接
status = WdfDeviceCreate(&device_init, &object_attribs, &control_device);
if (!NT_SUCCESS(status)) {
//
}
RtlInitUnicodeString(&ustring, MYWDF_LINKNAME);
//创建符号链接
status = WdfDeviceCreateSymbolicLink(control_device, &ustring);
if (!NT_SUCCESS(status)) {
//
}
这样,你的设备就可以被用户态的程序所使用了,接下来,再在用户态使用CreateFIle打开这个符号链接就可以了。驱动程序中我们已经设置好了Write和Read的派遣函数,只要使用WriteFile和ReadFile,就可以触发这两个IRP回调函数了,这个就不再举例子了。
当然,有时候应用程序并不需要真的向这个设备进行读和写操作,它可能仅仅需要“告知”驱动程序,我想要做什么,驱动程序可以在自己的会话空间里就足够处理,这时,微软为我们准备了另一个Win32 API:DeviceIoControl。这个API会在内部使操作系统创建一个IRP_DEVICE_CONTROL类型的IRP,你只需要在驱动中设置这个类型IRP的回调函数就可以了。
DeviceIoControl的函数原型如下:
BOOL WINAPI DeviceIoControl(
_In_ HANDLE hDevice,
_In_ DWORD dwIoControlCode,
_In_opt_ LPVOID lpInBuffer,
_In_ DWORD nInBufferSize,
_Out_opt_ LPVOID lpOutBuffer,
_In_ DWORD nOutBufferSize,
_Out_opt_ LPDWORD lpBytesReturned,
_Inout_opt_ LPOVERLAPPED lpOverlapped);
其中第二个参数dwIoControlCode,就是IO控制码,即IOCTL值,用来告诉驱动程序,我需要进行哪种类型的交互,当然,前提是这个IOCTL值是驱动程序预先设置好已知的,我们可以用CTL_CODE这个宏来定义一组IOCTL值。CTL_CODE的定义为(DeviceType,Function,Method,Access),DeviceType为设备对象的类型,这个类型应该与创建设备(IoCreateDevice)时的类型匹配,如FILE_DEVICE_XX这样的宏;Function为驱动程序定义的IOCTL宏,0x0000-0x7FFFF为微软保留,0x800-0xFFF为程序员自定义;Method为操作模式,应使用如下四种之一,METHOD_BUFFERED(使用缓冲区操作),METHOD_IN_DIRECT(使用直写操作),METHOD_OUT_DIRECT(使用直写操作),METHOD_NEITHER(使用其他方式操作);Access为访问权限,如果没有特殊要求,一般使用FILE_ANY_ACCESS。如下是一个实例:
#define IOCTL_HELLO CTL_CODE(\
FILE_DEVICE_UNKNOWN, \
0x999, \
METHOD_BUFFERED, \
FILE_ANY_ACCESS)
这里说一下关于METHOD_BUFFERED,使用缓冲内存模式的IOCTL,要避免在驱动中直接访问用户模式下的内存地址。在这种模式下,DeviceIoControl的大致流程是这样的,定义CTL_CODE宏->得到当前堆栈(IoGetCurrentIrpStackLocation)->得到输入缓冲区大小(DeviceIoControl.InputBufferLength)->得到输出缓冲区大小(DeviceIoControl.OutputBufferLength)->得到IOCTL码(DeviceIoControl.IoControlCode)->switch处理该IOCTL->设置IRP状态->结束IRP请求(IoCompleteRequest)。
关于驱动程序的IO请求就说到这里,下一章的话,我们就直接进入实战篇吧~拿一个WDF的总线驱动来分析一下。