应用层对设备的同步与异步操作
以WriteFile为例,一般的同步操作是调用WriteFile完成后,并不会返回,应用程序会在此处暂停,一直等到函数将数据写入文件中并正常返回,而异步操作则是调用WriteFile后会马上返回,但是操作系统有另一线程在继续执行写的操作,这段时间并不影响应用程序的代码往下执行,一般异步操作都有一个事件用来通知应用程序,异步操作的完成,以下图分别来表示同步和异步操作:
在调用这些函数时可以看做操作系统提供一个专门的线程来处理,然后如果选择同步,那么应用层线程会等待底层的线程处理完成后接着执行才执行后面的操作,而异步则是应用层线程接着执行后面的代码,而由操作系统来通知,因此一般来说异步相比较与同步少去了等待操作返回的过程,效率更高一些,但是选择同步还是异步,应该具体问题具体分析
同步操作设备
如果需要对设备进行同步操作,那么在使用CreateFile时就需要以同步的方式打开,这个函数的第六个参数dwFlagsAndAttributes是同步和异步操作的关键,如果给定了参数FILE_FLAG_OVERLAPPED则是异步的,否则是同步的。一旦用这个函数指定了操作方式,那么以后在使用这个函数返回的句柄进行操作时就是该中操作方式,但是这个函数本身不存在异步操作方式,一来这个函数没有什么耗时的操作,二来,如果它不正常返回,那么针对这个设备的操作也不能进行。
一般像WriteFile、ReadFile、DeviceIoControl函数最后一个参数lpOverlapped,是一个OVERLAPPED类型的指针,如果是同步操作,需要给这个参数赋值为NULL
异步操作方式
设置Overlapped参数实现同步
一般能够异步操作的函数都设置一个OVERLAPPED类型的参数,它的定义如下
typedef struct _OVERLAPPED {
ULONG_PTR Internal;
ULONG_PTR InternalHigh;
DWORD Offset;
DWORD OffsetHigh;
HANDLE hEvent;
} OVERLAPPED;
对于这个参数在使用时,其余的我们都不需要关心,一般只使用最后一个hEvent成员,这个成员是一个事件对象的句柄,在使用时,先创建一个事件对象,并设置事件对象无信号,并将句柄赋值给这个成员,一旦异步操作完成,那么系统会将这个事件设置为有信号,在需要同步的地方使用Wait系列的函数进行等待即可。
int main()
{
HANDLE hDevice =
CreateFile("test.dat",
GENERIC_READ | GENERIC_WRITE,
0,
NULL,
OPEN_EXISTING,
FILE_ATTRIBUTE_NORMAL|FILE_FLAG_OVERLAPPED,//此处设置FILE_FLAG_OVERLAPPED
NULL );
if (hDevice == INVALID_HANDLE_VALUE)
{
printf("Read Error\n");
return 1;
}
UCHAR buffer[BUFFER_SIZE];
DWORD dwRead;
//初始化overlap使其内部全部为零
OVERLAPPED overlap={0};
//创建overlap事件
//设置事件采用自动赋值的方式,且初始化为无信号,这样操作系统才能在异步操作完成时自动给其赋值为有信号
overlap.hEvent = CreateEvent(NULL,FALSE,FALSE,NULL);
//这里没有设置OVERLAP参数,因此是异步操作
ReadFile(hDevice,buffer,BUFFER_SIZE,&dwRead,&overlap);
//做一些其他操作,这些操作会与读设备并行执行
//等待读设备结束
WaitForSingleObject(overlap.hEvent,INFINITE);
CloseHandle(hDevice);
return 0;
}
使用完成函数来实现异步操作
异步函数是在异步操作完成时由操作系统调用的函数,所以我们可以在需要同步的地方等待一个同步对象,然后在异步函数中将这个对象设置为有信号。
使用异步函数必须使用带有Ex的设备操作函数,像ReadFileEx,WriteFileEx等等,Ex系列的函数相比于不带Ex的函数来说,多了最后一个参数,LPOVERLAPPED_COMPLETION_ROUTINE 类型的回调函数,这个函数的原型如下:
VOID CALLBACK FileIOCompletionRoutine(
__in DWORD dwErrorCode,
__in DWORD dwNumberOfBytesTransfered,
__in LPOVERLAPPED lpOverlapped
);
第一个参数是一个错误码,如果异步操作出错,那么他的错误码可以由这个参数得到,第二个参数是实际操作的字节数对于Write类型的函数来说这个就是实际读取的字节数,第三个是一个异步对象。在使用这个方式进行异步时Ex函数中的OVERLAPPED参数一般不需要为其设置事件句柄,只需传入一个已经清空的OVERLAPPED类型的内存地址即可。
当我们设置了该函数后,操作系统会将这个函数插入到相应的队列中,一旦完成这个异步操作,系统就会调用这个函数,Windows中将这种机制叫做异步过程调用(APC Asynchronous Produre Call);这种机制也不是一定会执行,一般只有程序进入警戒状态时才会执行,想要程序进入警戒状态需要调用带有Ex的等待函数,包括SleepEx,在其中的bAlertable设置为TRUE那么当其进入等待状态时就会调用APC队列中的函数,需要注意的是所谓的APC就是系统借当前线程的线程环境来执行我们提供的回调函数,是用当前线程环境模拟了一个轻量级的线程,这个线程没有自己的线程上下文,所以在回调函数中不要进行耗时的操作,否则一旦原始线程等到的它的执行条件而被唤醒,而APC例程还没有被执行完成的话,就会造成一定的错误。下面是使用这种方式进行异步操作的例子:
VOID CALLBACK MyFileIOCompletionRoutine(
DWORD dwErrorCode, // 对于此次操作返回的状态
DWORD dwNumberOfBytesTransfered, // 告诉已经操作了多少字节,也就是在IRP里的Infomation
LPOVERLAPPED lpOverlapped // 这个数据结构
)
{
SetEvent(lpOverlapped->hEvent);
printf("IO operation end!\n");
}
int main()
{
HANDLE hDevice =
CreateFile("test.dat",
GENERIC_READ | GENERIC_WRITE,
0,
NULL,
OPEN_EXISTING,
FILE_ATTRIBUTE_NORMAL|FILE_FLAG_OVERLAPPED,//此处设置FILE_FLAG_OVERLAPPED
NULL );
if (hDevice == INVALID_HANDLE_VALUE)
{
printf("Read Error\n");
return 1;
}
UCHAR buffer[BUFFER_SIZE];
//初始化overlap使其内部全部为零
//不用初始化事件!!
OVERLAPPED overlap={0};
//这里没有设置OVERLAP参数,因此是异步操作
overlap.hEvent = CreateEvent(NULL, FALSE, FALSE, NULL);
ReadFileEx(hDevice, buffer, BUFFER_SIZE,&overlap,MyFileIOCompletionRoutine);
//做一些其他操作,这些操作会与读设备并行执行
printf("在此处可以执行其他操作\n");
//进入alterable,只是为了有机会执行APC函数
SleepEx(1000, TRUE);
//在此处进行同步,只有当读操作完成才关闭句柄
WaitForSingleObject(overlap.hEvent, INFINITE);
CloseHandle(hDevice);
return 0;
}
在最后SleepEx让线程休眠而使其有机会执行APC例程,然后使用WaitForSingleObject来等待事件,我们在APC例程中将事件置为有信号,这样只有当异步操作完成,才会返回,利用这个可以在关键的位置实现同步,在这里按理来说可以直接用WaitForSingleObjectEx来替换这两个函数的调用,但是不知道为什么使用WaitForSingleObjectEx时,即使我没有设置为有信号的状态它也能正常返回,所以为了体现这点,我是使用了SleepEx和WaitForSingleObject两个函数。
IRP中的同步和异步操作
上述的同步和异步操作必须得到内核的支持,其实所有对设备的操作最终都会转化为IRP请求,并传递到相应的派遣函数中,在派遣函数中可以直接结束IRP,或者让派遣函数返回,在以后的某个时候处理,由于应用层会等待派遣函数返回,所以直接结束IRP的方式可以看做是同步,而先返回以后处理的方式可以看做是异步处理。
在CreateFile中没有异步的方式,所以它会一直等待派遣函数调用IoCompleteRequest结束,所以当调用CreateFile打开一个自己写的设备时需要编写一个用来处理IRP_MJ_CREATE的派遣函数,并且需要在函数中结束IRP,否则CreateFile会报错,之前本人曾经犯过这样的错误,没有为设备对象准备IRP__MJ_CREATE的派遣函数,结果CreateFile直接返回-1.
对于ReadFile和WriteFile来说,它们支持异步操作,在调用这两个函数进行同步操作时,内部会生成一个事件并等待这个事件,这个事件会和IRP一起发送的派遣函数中,当IRP被结束时,事件会被置为有信号,这样函数中的等待就可以正常返回。而异步操作就不会产生这个事件。而是使用函数中的overlapped参数,这时它内部不会等待这个事件,而由程序员自己在合适的位置等待。
而调用带有Ex的I/O函数则略有不同,他不会设置overlapped参数中的事件,而是当进入警告模式时调用提供的APC函数。
在派遣函数中可以调用IoCompleteRequest函数来结束IRP的处理或者调用IoMarkIrpPending来暂时挂起IRP,将IRP进行异步处理。该函数原型如下:
VOID
IoMarkIrpPending(
IN OUT PIRP Irp
);
下面的例子演示了如何进行IRP的异步处理
typedef struct IRP_QUEUE_struct
{
LIST_ENTRY IRPlist;
PIRP pPendingIrp;
}IRP_QUEUE, *LPIRP_QUEUE;
NTSTATUS DefaultDispatch(
IN PDEVICE_OBJECT DeviceObject,
IN PIRP Irp
)
{
NTSTATUS status;
PIO_STACK_LOCATION pIrpStack;
pIrpStack = IoGetCurrentIrpStackLocation(Irp);
switch(pIrpStack->MajorFunction)
{
case IRP_MJ_READ:
{
PLIST_ENTRY pQueueHead;
LPIRP_QUEUE pQueue;
Irp->IoStatus.Information = 0;
Irp->IoStatus.Status = STATUS_PENDING;
pQueue = (LPIRP_QUEUE)ExAllocatePoolWithTag(PagedPool, sizeof(IRP_QUEUE), TAG);
if(pQueue != NULL)
{
pQueue->pPendingIrp = Irp;
pQueueHead = (PLIST_ENTRY)(DeviceObject->DeviceExtension);
InsertHeadList(pQueueHead, &(pQueue->IRPlist));
}
IoMarkIrpPending(Irp);
return STATUS_PENDING;
}
break;
case IRP_MJ_CLEANUP:
{
PLIST_ENTRY pQueueHead;
LPIRP_QUEUE pQueue;
PLIST_ENTRY pDelete;
pQueueHead = (PLIST_ENTRY)(DeviceObject->DeviceExtension);
if(NULL != pQueueHead)
{
while(!IsListEmpty(pQueueHead))
{
pDelete = RemoveHeadList(pQueueHead);
pQueue = CONTAINING_RECORD(pDelete, IRP_QUEUE, IRPlist);
IoCompleteRequest(pQueue->pPendingIrp, IO_NO_INCREMENT);
ExFreePoolWithTag(pQueue, TAG);
pQueue = NULL;
}
}
}
default:
{
Irp->IoStatus.Information = 0;
Irp->IoStatus.Status = STATUS_SUCCESS;
IoCompleteRequest(Irp, IO_NO_INCREMENT);
return STATUS_SUCCESS;
}
break;
}
}
VOID DriverUnload(IN PDRIVER_OBJECT DriverObject)
{
UNICODE_STRING uDeviceName;
UNICODE_STRING uSymbolickName;
UNREFERENCED_PARAMETER(DriverObject);
RtlInitUnicodeString(&uDeviceName, DEVICE_NAME);
RtlInitUnicodeString(&uSymbolickName, SYMBOLIC_NAME);
IoDeleteSymbolicLink(&uSymbolickName);
IoDeleteDevice(DriverObject->DeviceObject);
DbgPrint("GoodBye World\n");
}
NTSTATUS DriverEntry(IN PDRIVER_OBJECT DriverObject, IN PUNICODE_STRING RegistryPath)
{
NTSTATUS status;
int i = 0;
PDEVICE_OBJECT pDeviceObject;
UNREFERENCED_PARAMETER(RegistryPath);
DriverObject->DriverUnload = DriverUnload;
status = CreateDevice(DriverObject, &pDeviceObject);
for(i = 0; i < IRP_MJ_MAXIMUM_FUNCTION + 1; i++)
{
DriverObject->MajorFunction[i] = DefaultDispatch;
}
DbgPrint("Hello world\n");
return status;
}
NTSTATUS CreateDevice(PDRIVER_OBJECT pDriverObject,PDEVICE_OBJECT *ppDeviceObject)
{
NTSTATUS status;
PLIST_ENTRY pIrpQueue = NULL;
UNICODE_STRING uDeviceName;
UNICODE_STRING uSymbolickName;
RtlInitUnicodeString(&uDeviceName, &DEVICE_NAME);
RtlInitUnicodeString(&uSymbolickName, SYMBOLIC_NAME);
if(NULL != ppDeviceObject)
{
status = IoCreateDevice(pDriverObject, sizeof(LIST_ENTRY), &uDeviceName, FILE_DEVICE_UNKNOWN, FILE_DEVICE_SECURE_OPEN, FALSE, ppDeviceObject);
if(!NT_SUCCESS(status))
{
return status;
}
(*ppDeviceObject)->Flags |= DO_BUFFERED_IO;
status = IoCreateSymbolicLink(&uSymbolickName, &uDeviceName);
if(!NT_SUCCESS(status))
{
IoDeleteDevice(*ppDeviceObject);
*ppDeviceObject = NULL;
return status;
}
pIrpQueue = (PLIST_ENTRY)((*ppDeviceObject)->DeviceExtension);
InitializeListHead(pIrpQueue);
return status;
}
return STATUS_UNSUCCESSFUL;
}
在上述代码中,定义一个链表用来保存未处理的IRP,然后在DriverEntry中创建一个设备对象,将链表头指针放入到设备对象的扩展中,在驱动的IRP_MJ_READ请求中,将IRP保存到链表中,然后直接调用IoMarkIrpPending,将IRP挂起。一般的IRP_MJ_CLOSE用来关闭内核创建的内核对象,对应用层来说也就是句柄,而IRP_MJ_CLEANUP用来处理被挂起的IRP,所以在这我们需要对CLEANUP的IRP进行处理,在处理它时,我们从链表中依次取出IRP,调用IoCompleteRequest直接结束并清除这个节点。对于其他类型的IRP则直接结束掉即可。
在应用层,利用异步处理的方式多次调用ReadFile,最后再IrpTrace工具中可以看到,有多个显示状态位Pending的IRP。
在处理IRP时除了调用IoCompleteRequest结束之外还可以调用IoCancelIrp来取消IRP请求。这个函数原型如下:
BOOLEAN
IoCancelIrp(
IN PIRP Irp
);
当调用这个函数取消相关的IRP时,对应的取消例程将会被执行,在DDK中可以使用函数IoSetCancelRoutine,该函数可以通过第二个参数为IRP设置一个取消例程,如果第二个参数为NULL,那么就将之前绑定到IRP上的取消例程给清除。函数原型如下:
PDRIVER_CANCEL
IoSetCancelRoutine(
IN PIRP Irp,
IN PDRIVER_CANCEL CancelRoutine
);
在调用IoCancelIrp函数时系统在内部会获取一个名为cancel的自旋锁,然后进行相关操作,但是自旋锁的释放需要自己来进行,一般在取消例程中进行释放操作。这个自旋锁可以通过函数IoAcquireCancelSpinLock来获取,通过IoReleaseCancelSpinLock来释放,下面是一个演示如何使用取消例程的例子。
//取消例程
VOID CancelReadIrp(
IN PDEVICE_OBJECT DeviceObject,
IN PIRP Irp
)
{
Irp->IoStatus.Information = 0;
Irp->IoStatus.Status = STATUS_CANCELLED;
IoCompleteRequest(Irp, IO_NO_INCREMENT);
IoReleaseCancelSpinLock(Irp->CancelIrql);
}
//IRP_MJ_READ 处理
case IRP_MJ_READ:
{
Irp->IoStatus.Information = 0;
Irp->IoStatus.Status = STATUS_PENDING;
IoSetCancelRoutine(Irp, CancelReadIrp);
IoMarkIrpPending(Irp);
return STATUS_PENDING;
}
在R3层可以利用CancelIO,来使系统调用取消例程。
这个API传入的是设备的句柄,当调用它时所有针对该设备的被挂起的IRP都会调用对应的取消例程,在这就不需要像上面那样保存被挂起的IRP,每当有READ请求过来时都会调用case里面的内容,将该IRP和取消例程绑定,每当有IRP被取消时都会调用对应的取消例程,就不再需要自己维护了。另外在取消时,系统会自己获取这个cancel自旋锁,并提升对应的IRQL,IRP所处的IRQL被保存在IRP这个结构的CancelIrql成员中,而调用IoReleaseCancelSpinLock函数释放自旋锁时需要的参数正是这个IRP对应的IRQL,所以这里直接传入Irp->CancelIrql