windows via c/C++设备IO之接收I/O请求完成通知

  本文章转载于http://www.cnblogs.com/wz19860913/archive/2008/08/20/1272257.html,感谢原作者辛苦付出。

上一篇,讨论了如何发送I/O请求。在异步的设备I/O请求方式下,要考虑的问题就是当I/O请求完成之后,驱动程序如何通知你的应用程序。本篇主要讨论获得通知的方法。

  

   Windows提供了4种不同的技术方法来得到I/O完成的通知。

技术概要
通知一个设备内核对象当一个设备同时有多个IO请求的时候,该方法不适用。允许一个线程发送一个IO请求,另一个线程处理之。
通知一个事件内核对象允许一个设备同时有多个IO请求。允许一个线程发送一个IO请求,另一线程处理之。
警告IO允许一个设备同时有多个IO请求。必须在同一个线程中发送并处理同一个IO请求。
IO完成端口允许一个设备同时有多个IO请求。允许一个线程发送一个IO请求,另一个线程处理之。该方法伸缩性好,而且性能高。



本篇主要讨论前3种。
1、通知一个设备内核对象
    在Windows中,一个设备内核对象可以处于“已通知”或“未通知”状态。ReadFile和WriteFile在发送I/O请求之前让指定的设备内核对象处于“未通知”状态。当设备驱动程序完成了I/O请求,驱动程序将设备内核对象设置为“已通知”状态。一个线程可以查看一个异步的I/O请求是否完成,通过等待函数即可实现:WaitForSingleObject或WaitForMultipleObject等。这就意味着,这种实现的方式不是完完全全的“异步”,最终有点“同步”的味道,因为这些等待函数可能会导致线程进入阻塞状态。
可以如下地编码来使用这种方法:

//创建或打开设备内核对象,注意使用FILE_FLAG_OVERLAPPED旗标
HANDLE hFile = CreateFile(..., FILE_FLAG_OVERLAPPED, ...);
BYTE bBuffer[100];     //I/O缓冲区
OVERLAPPED o = { 0 };     //重叠结构,不要忘记初始化
o.Offset = 345;     //偏移量
BOOL bReadDone = ReadFile(hFile, bBuffer, 100, NULL, &o);   //读取数据
DWORD dwError = GetLastError();
//ReadFile返回FLASE,但是错误码dwError表明I/O即将开始
if (!bReadDone && (dwError == ERROR_IO_PENDING))

{

     //等待I/O请求完成

     WaitForSingleObject(hFile, INFINITE);

     bReadDone = TRUE;

}

if (bReadDone)

{

     // 操作成功,可以查看OVERLAPPED结构中的各个字段和缓冲区中的数据

     // o.Internal 包含了I/O错误码

     // o.InternalHigh 包含了I/O传输字节数

     // 缓冲区包含了读取的数据

}

else

{

     // 错误发生,bReadDone为FLASE,且错误码dwError指明一个错误

}
这种方法是十分简单的,实现起来十分容易,但是有一个明显的缺点,就是无法处理多个I/O请求。因为一旦一个I/O请求完成,等待函数就会返回,无法识别是哪个I/O请求完成了。

2、通知一个事件内核对象
这种方法可以处理多个同时的I/O请求。
记得OVERLAPPED结构中有一个hEvent成员吧,该成员是一个事件内核对象。
使用这种方法,你必须使用CreateEvent函数来创建一个事件内核对象,并初始化那个hEvent成员。
当一个异步I/O请求完成设备驱动程序查看OVERLAPPED中的hEvent是否为NULL,如果不是,驱动程序通过SetEvent通知该事件内核对象,同时也使得设备内核对象进入“已通知”状态。
但是,你应该等待在该事件内核对象上。
你可以让Windows不通知“文件内核对象”,这样可以少许提高一点性能,通过呼叫函数SetFileCompletionNotificationModes即可,
传递一个设备内核对象句柄和FILE_SKIP_SET_EVENT_ON_HANDLE旗标:
BOOL SetFileCompletionNotificationModes(HANDLE hFile, UCHAR uFlags);
为了处理多个I/O请求,你必须为每个I/O请求创建一个独立的事件内核对象,并将之初始化OVERLAPPED结构中的hEvent。
然后可以通过WaitForMultipleObject来等待这些事件内核对象。这种方法可以实现一个设备上的多个I/O请求的处理。可以如下编码:

//创建或打开设备,注意使用FILE_FLAG_OVERLAPPED
HANDLE hFile = CreateFile(..., FILE_FLAG_OVERLAPPED, ...);
BYTE bReadBuffer[10];     //读缓冲区
OVERLAPPED oRead = { 0 };     //定义OVERLAPPED结构,并初始化之
oRead.Offset = 0;
oRead.hEvent = CreateEvent(...);     //创建事件内核对象,与读操作相关
ReadFile(hFile, bReadBuffer, 10, NULL, &oRead);

BYTE bWriteBuffer[10] = { 0, 1, 2, 3, 4, 5, 6, 7, 8, 9 };
OVERLAPPED oWrite = { 0 };
oWrite.Offset = 10;
oWrite.hEvent = CreateEvent(...);     //另一个事件内核对象,与写操作相关
WriteFile(hFile, bWriteBuffer, _countof(bWriteBuffer), NULL, &oWrite);

//可在此执行其他操作
//......

HANDLE h[2];
h[0] = oRead.hEvent;     //与读相关的事件对象
h[1] = oWrite.hEvent;    //与写相关的事件对象
DWORD dw = WaitForMultipleObjects(2, h, FALSE, INFINITE);     //等待
switch (dw – WAIT_OBJECT_0)
{
     case 0:   //读操作完成
          break;

     case 1:   //写操作完成
          break;
}

当然,也可以把上面代码拆分成两个线程执行,上面半段为发送I/O请求的放在一个线程中,下面处理I/O请求完成的放在另一个线程中。在I/O请求完成之后,收到通知之后,可以得到有关OVERLAPPED结构的信息,通过函数GetOverlappedResult:

BOOL GetOverlappedResult(
   HANDLE      hFile,        //设备对象句柄
   OVERLAPPED* pOverlapped,  //OVERLAPPED结构指针,返回OVERLAPPED
   PDWORD      pdwNumBytes,  //返回传输的字节数
   BOOL        bWait);       //是否等到I/O结束才返回
3、警告IO
 当一个线程被创建的时候,系统也创建一个与该线程关联的队列,这个队列称为“异步过程调用”(APC)队列。
当发送一个I/O请求的时候,你可以告诉驱动程序在APC队列中加入一个记录。当I/O请求完成之后,如果线程处于“待命状态”,则该记录中的回调函数可以被调用。
让I/O请求完成的通知进入线程的APC队列,即在APC队列中添加一个I/O请求完成通知的记录,可以使用如下两个函数:
BOOL ReadFileEx(
   HANDLE      hFile,        //设备对象句柄
   PVOID       pvBuffer,     //数据缓冲区
   DWORD       nNumBytesToRead,  //预期传输的数据
   OVERLAPPED* pOverlapped,      //OVERLAPPED结构指针
   LPOVERLAPPED_COMPLETION_ROUTINE pfnCompletionRoutine);//回调函数指针
BOOL WriteFileEx(
   HANDLE      hFile,
   CONST VOID  *pvBuffer,
   DWORD       nNumBytesToWrite,
   OVERLAPPED* pOverlapped,
   LPOVERLAPPED_COMPLETION_ROUTINE pfnCompletionRoutine);
注意一下函数的最后一个参数pfnCompletionRoutine,是一个函数指针,接受一个回调函数,这个函数就是被记录到APC队列的函数,
函数头必须按如下格式书写:
VOID WINAPI CompletionRoutine(     //函数名可以任意
   DWORD       dwError,     //错误码
   DWORD       dwNumBytes,  //传输的数据
   OVERLAPPED* po);         //OVERLAPPED结构
当使用ReadFileEx和WriteFileEx函数的时候,传递回调函数的地址,
当驱动程序完成I/O请求之后,它在线程APC队列中添加一个记录,这个记录包含这个回调函数的地址和起初发送I/O请求时候的OVERLAPPED结构地址。
当线程进入“待命状态”,系统检测线程APC队列,然后调用回调函数,并设置其3个参数。
当I/O请求完成,系统不会马上调用记录在APC队列中的回调函数,因为线程可能没有进入“待命状态”。
为了调用回调函数,你必须让线程进入“待命状态”,可以通过一些带“Ex”的等待函数来完成:

DWORD SleepEx(
   DWORD dwMilliseconds,
   BOOL  bAlertable);

DWORD WaitForSingleObjectEx(
   HANDLE hObject,
   DWORD  dwMilliseconds,
   BOOL   bAlertable);

DWORD WaitForMultipleObjectsEx(
   DWORD   cObjects,
   CONST HANDLE* phObjects,
   BOOL    bWaitAll,
   DWORD   dwMilliseconds,
   BOOL    bAlertable);

BOOL SignalObjectAndWait(
   HANDLE hObjectToSignal,
   HANDLE hObjectToWaitOn,
   DWORD  dwMilliseconds,
   BOOL   bAlertable);

BOOL GetQueuedCompletionStatusEx(
   HANDLE hCompPort,
   LPOVERLAPPED_ENTRY pCompPortEntries,
   ULONG ulCount,
   PULONG pulNumEntriesRemoved,
   DWORD dwMilliseconds,
   BOOL bAlertable);

DWORD MsgWaitForMultipleObjectsEx(
   DWORD   nCount,
   CONST HANDLE* pHandles,
   DWORD   dwMilliseconds,
   DWORD   dwWakeMask,
   DWORD   dwFlags);     //使用MWMO_ALERTABLE使线程进入“待命状态”

  除了MsgWaitForMultipleObjectEx函数之外,上面其余5个函数的最后一个参数bAlertalbe,指明了是否要线程进入“待命状态”,如果需要,请传递TRUE。当你调用上面这些等待函数,并让线程进入“待命状态”,系统首先查看线程的APC队列,如果至少有一个记录在APC队列中,系统不会让你的线程进入阻塞状态,而是调用回调函数,并提供其3个参数。当回调函数返回给系统,系统再次检查APC队列中的记录,如果存在,继续调用回调函数。否则,回调函数返回给用户(即普通的返回)。
注意,如果APC队列中存在记录,那么调用上述等待函数,不会让你的线程进入阻塞状态。只有当APC队列中没有记录,调用这些函数的时候才会让线程进入阻塞状态,直到等待的内核对象为“已通知”状态或APC队列中出现记录。由于线程处于“待命状态”,
因此一点APC队列中出现一个记录,那么系统唤醒你的线程,呼叫回调函数,清空APC队列,回调函数返回,线程继续执行。这6个等待函数返回的值说明了它们是因为什么原因而返回的。如果返回WAIT_IO_COMPLETION,那么说明了你的线程继续执行,因为至少一个APC记录被处理。如果返回其他的值,那么说明这些等待函数等待的内核对象为“已通知”状态(也可能是互斥内核对象被抛弃)或者等待超时。


还有需要注意的是,系统调用APC回调函数,不是按FIFO的顺序,而是随意的。注意如下代码:
hFile = CreateFile(..., FILE_FLAG_OVERLAPPED, ...);
ReadFileEx(hFile, ..., ReadRoutine1);   //第一次读,回调函数ReadRoutine1
WriteFileEx(hFile, ..., WriteRoutine1); //第一次写,回调函数WriteRoutine1
ReadFileEx(hFile, ..., ReadRoutine2);   //第二次读,回调函数ReadRoutine2
SomeFunc();   //其他一些操作
SleepEx(INFINITE, TRUE);     //等待,线程进入“待命状态”
线程发起了3次I/O请求,并给出了3个回调函数ReadRoutine1、WriteRoutine1、ReadRoutine2。
然后线程执行SomeFunc函数,执行完成之后进入无限等待,当I/O请求结束,会调用3个APC队列中的回调函数。
需要注意的是,如果3个I/O请求都在SomeFunc函数执行的时候完成,那么回调函数的调用顺序可能不是ReadRountine1、WriteRoutine1、ReadRoutine2,这个顺序是任意的。
Windows提供了一个函数可以手动在一个线程的APC队列加入一个记录(即加入一个回调函数):
DWORD QueueUserAPC(
   PAPCFUNC  pfnAPC,     //APC回调函数指针
   HANDLE    hThread,     //线程对象句柄
   ULONG_PTR dwData);  //传递给参数pfnAPC所对应的回调函数的参数
 其中第1个参数是一个函数指针,是一个回调函数,被记录到线程的APC队列,其函数头格式如下:


VOID WINAPI APCFunc(ULONG_PTR dwParam);
 QueueUserAPC函数的第2个参数指明了你想要设置的哪个线程的APC队列。第3个参数dwData就是传递给回调函数APCFunc的参数。QueueUserAPC可以让你的线程摆脱阻塞状态,此时上述等待函数返回码为WAIT_IO_COMPLETION。


最后要讲的就是告警I/O的缺点:


告警I/O的回调函数所提供的参数较少,因此处理上下文内容只能通过全局变量来实现。
使用告警I/O,意味着发送I/O请求和处理I/O完成通知只能放在同一个线程中,如果发送多个I/O请求,该线程就不得不处理每个I/O完成通知,其他线程则会比较 空闲,这样会造成不平衡。





评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值