应用层对设备的同步与异步操作

以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

IRP的同步的更多相关文章

  1. 《Windows驱动开发技术详解》之IRP的同步

    应用程序对设备的同步异步操作: 大部分IRP都是由应用程序的Win32 API函数发起的.这些Win32 API本身就支持同步和异步操作.例如,ReadFile.WriteFile和DeviceIoC ...

  2. ASP.NET 中的 Async/Await 简介

    本文转载自MSDN 作者:Stephen Cleary 原文地址:https://msdn.microsoft.com/en-us/magazine/dn802603.aspx 大多数有关 async ...

  3. ASP.NET 上的 Async/Await 简介

    原文链接 大多数有关 async/await 的在线资源假定您正在开发客户端应用程序,但在服务器上有 async 的位置吗?可以非常肯定地回答“有”.本文是对 ASP.NET 上异步请求的概念性概述, ...

  4. Windows的同步I/O和异步I/O

    同步I/O操作 执行步骤 1.        程序通过FileStream对象来打开磁盘文件,然后调用Read方法(内部调用Win32 ReadFile函数),从文件中读取数据.这时,线程从托管代码转 ...

  5. IRP小结 0x01 IRP & IO_STACK_LOCATION(结合WRK理解)

    写博客整理记录一下IRP相关的知识点,加深一下印象. 所有的I/O请求都是以IRP的形式提交的.当I/O管理器为了响应某个线程调用的的I/O API的时候,就会构造一个IRP,用于在I/O系统处理这个 ...

  6. CPU与IRP的一些相关函数

    VOID KiAdjustIrpCredits ( VOID )其中 Number = KeNumberProcessors;Prcb = KiProcessorBlock[Index]; 多核情况下 ...

  7. 内核事件KEVENT(同步)

    转载请您注明出处:http://www.cnblogs.com/lsh123/p/7358702.html 一.驱动程序与驱动程序的事件交互    IoCreateNotificationEvent ...

  8. Windows内核原理-同步IO与异步IO

    目录 Windows内核原理-同步IO与异步IO 背景 目的 I/O 同步I/O 异步I/O I/O完成通知 总结 参考文档 Windows内核原理-同步IO与异步IO 背景 在前段时间检查异常连接导 ...

  9. 64位内核开发第十二讲,进程监视,ring3跟ring0事件同步.

    一丶同步与互斥详解,以及实现一个进程监视软件. 1.用于线程同步的 KEVENT 事件很简单分别分为 事件状态. 以及事件类别. 事件状态: 有信号 Signaled 无信号 Non-signaled ...

随机推荐

  1. MyBatis_关联关系查询

    一.关联查询 当查询的内容涉及到具有关联关系的多个表时,就需要使用关联查询.根据表与表间的关联关系的不同.关联查询分为四种: 一对一关联查询: 一对多关联查询: 多对一关联查询: 多对多关联查询: 二 ...

  2. 注册Azure AD 应用程序

    作者:陈希章 发表于2017年3月22日 在此前的文章中,我给大家介绍了分别用Graph 浏览器以及第三方工具(POSTMAN)快速体验Microsoft Graph的功能,其中有一个重要的环节就是, ...

  3. spring mvc获取绝对路径的几种方法

    1.首先如果是在一个controller方法中,则很简单,直接用下面语句. @RequestMapping("categoryHome") public ModelAndView ...

  4. dubbo服务治理中间件,zookeeper注册中心

    对传统项目架构进行拆分: 集群概念: 面向服务分布式架构: 服务层提供被注册的对象需要实现序列化接口Serializable: 配置表现层和服务层: 依赖包: 服务层: <!-- 定义dubbo ...

  5. DDOS学习笔记(《破坏之王-DDOS攻击与防范深度剖析》)

           最近花了点时间把<破坏之王-DDOS攻击与防范深度剖析>看了一遍,坦白来说,这本书比较浅显,可以说是入门书,当然对于我这种对DDOS一知半解的人来说,也是一本不错的书,起码我 ...

  6. 4.python迭代器生成器装饰器

    容器(container) 容器是一种把多个元素组织在一起的数据结构,容器中的元素可以逐个地迭代获取,可以用in, not in关键字判断元素是否包含在容器中.通常这类数据结构把所有的元素存储在内存中 ...

  7. APP之构架自己的webapi框架

    当我们学习到一定程度的时候,我们会想要去深入了解代码底层的东西,也更想拥有一个属于自己的框架.本文可能成为编写一个webapi框架的开端.有研究MVC框架的朋友会发现,mvc框架的路由MvcRoute ...

  8. UVALive 4490 Help Bubu

    题目大意:有n本书,高度值域为8,现可以把k本书拿出来再放进去,相邻的.高度相同的书算作一块,最小化块的个数.n=100. 强烈建议大家不要在做完区间DP后做别的DP题:区间DP是整体考虑,而一般DP ...

  9. bzoj 1996: [Hnoi2010]chorus 合唱队

    Description Input Output Sample Input 4 1701 1702 1703 1704 Sample Output 8 HINT Source 因为只会在区间的两端进行 ...

  10. bzoj 4817: [Sdoi2017]树点涂色

    Description Bob有一棵n个点的有根树,其中1号点是根节点.Bob在每个点上涂了颜色,并且每个点上的颜色不同.定义一条路 径的权值是:这条路径上的点(包括起点和终点)共有多少种不同的颜色. ...