Windows同步设备I/O与异步设备I/O

Windows同步设备I/O与异步设备I/O

总体介绍

Windows中打开文件或者设备都可以使用CreateFile函数,Windows系统为我们封装了底层设备IO的细节让我们可以像操作文件一样操作串口并口等设备。

当我们从设备中读取或写入数据时,我们需要等待I/O设备处理完毕后才能进行下一次的读写。但是I/O设备的处理是非常耗时的。如果我们一直等待I/O设备,那程序的性能是非常糟糕的。我们希望系统在处理I/O事务时我们的程序能够继续运行执行其他任务。一种简单的方法是用多线程的方法,但是这种方案效率不高而且浪费线程资源。Mircosoft在这个领域花了数年的时间研究和测试开发出了一个非常好的机制,这种机制被称为I/O完成端口。

在讲I/O完成端口前我们需要对普通的同步I/O和异步I/O的实现有个大概认识。才能更好的理解I/O完成端口的优越性。

打开和关闭设备

目录、逻辑磁盘驱动器、物理磁盘驱动器、串口、并口 命名管道、邮件槽客户端等设备可以使用CreateFile打开;对于邮件槽服务器、套接字、控制台可用对应的函数打开这里就不复述可自行在Mircosoft官方文档查看。

这里我们重点看下CreateFile函数

HANDLE CreateFile(
  LPCSTR                lpFileName,
  DWORD                 dwDesiredAccess,
  DWORD                 dwShareMode,
  LPSECURITY_ATTRIBUTES lpSecurityAttributes,
  DWORD                 dwCreationDisposition,
  DWORD                 dwFlagsAndAttributes,
  HANDLE                hTemplateFile
)
;

可以看出CreateFile有许多参数,这给我们极大的灵活性。各参数的作用可自行查看Mircosoft官方文档,这里主要介绍dwFlagsAndAttributes这个参数如果传入FILE_FLAG_OVERLAPPED则这个I/O是以异步的方式打开的。

同步I/O设备

对设配I/O进行读写最方便时ReadFile和WriteFile 函数原型如下:

BOOL ReadFile(
  HANDLE       hFile, // 设备句柄
  LPVOID       lpBuffer, //数据缓存
  DWORD        nNumberOfBytesToRead, // 告诉设备需要读取多少字节
  LPDWORD      lpNumberOfBytesRead, // 真实读取的字节
  LPOVERLAPPED lpOverlapped // 同步I/O 此参数应该为NULL,异步I/O时需要传入LPOVERLAPPED  
)
;

BOOL WriteFile(
  HANDLE       hFile,// 设备句柄
  LPCVOID      lpBuffer,//数据缓存
  DWORD        nNumberOfBytesToWrite,// 告诉设备需要写入多少字节
  LPDWORD      lpNumberOfBytesWritten,真实写入的字节
  LPOVERLAPPED lpOverlapped// 同步I/O 此参数应该为NULL,异步I/O时需要传入LPOVERLAPPED  
)
;

异步设备I/O基础

要使用异步I/O,需要在打开设备时将CreateFile中的参数dwFlagsAndAttributes传入FILE_FLAG_OVERLAPPED标志。之后ReadFile和WriteFile在使用时会检查这个标志。

OVERLAPPED结构
typedef struct _OVERLAPPED {
  ULONG_PTR Internal; // 
  ULONG_PTR InternalHigh;
  union {
    struct {
      DWORD Offset; // 文件偏移量,访问文件时从哪里开始访问
      DWORD OffsetHigh;
    } DUMMYSTRUCTNAME;
    PVOID Pointer;
  } DUMMYUNIONNAME;
  HANDLE    hEvent;
} OVERLAPPED, *LPOVERLAPPED;

在非文件设备中Offset和OffsetHigh我们必须将这两个成员函数初始化为0,否则I/O请求会失败,这时调用GetLastError()会返回ERROR——INVALID——PARAMETER。 其他成员可自行查阅Mircosoft官方文档。

OVERLAPPED over1 = {0};
OVERLAPPED over2 = {0};
BYTE buf[100];
ReadFile(hFile, buf, 100NULL, &over1);
WriteFile(hFile, buf, 100,NULL, &over2);

以上ReadFile和WriteFile 顺序是不确定,这要看设备驱动程序如何执行,还有一个需要注意的是ReadFile和WriteFile的返回值并不能正确地判断函数是否执行成功。我们还必须调用GetLastError函数来判断。如果eadFile和WriteFile返回FALSE,但是GetLastError返回ERROR_IO_PENDING说明I/O请求已经被成功加入队列了。

接收I/O请求完成的通知

我们利用ReadFile和WriteFile发出异步I/O请求后如何知道I/O请求已经完成。MicoSoft提供四种通知方案

技术介绍
触发设备内核对象当向一个设备发出多个I/O请求的时候,这种方法没什么用。
触发事件内核对象它允许一个设备发出多个I/O请求。
使用可提醒I/O发出I/O的线程必须对结进行处理
使用I/O完成端口具有高度的伸缩性和最佳的灵活性

触发设备内核对象和触发事件内核对象主要等待的事件对象不同, 设备内核对象不能区分是读取还是写入事件。触发事件内核对象主要利用OVERLAPPED结构中的hEvent成员。 在调用ReadFile和WriteFile时将对应的时间传入,当I/O请求完成时设备驱动会触发对应的事件。例如下面这个伪代码:

 HANDLE hFile = CreateFile(_T("itempos.reg"), GENERIC_READ,
  FILE_SHARE_READ, NULL, OPEN_EXISTING, FILE_FLAG_OVERLAPPED, NULL);
    BYTE bReadBuf[10];
    OVERLAPPED overRead = {0};
    OverRead.Offset = 0;
    overRead.hEvent = CreateEvent(...);
    ReadFile(hFile, bReadBuf, 10NULL, &overRead);
    
    BYTE bWriteBuf[10];
    OVERLAPPED overWrite = {0};
    overWrite.Offset = 0;
    overWrite.hEvent = CreateEvent(...);
    ReadFile(hFile, bReadBuf, 10NULL, &overWrite);
    
    HANDLE h[2];
    h[0] = overRead.hEvent;
    h[1] = overWrite.hEvent;
    DWORD dw = WaitForMultipleObjects(2, h, FALSE, INFINITE);
    switch(dw - WAIT_OBJECT_0)
    {
    case 0// 读完成
    break;
    case 1// 写完成
    break;
    }
使用可提醒I/O

要使用可提醒I/O,同样CreateFile中的参数dwFlagsAndAttributes需要传入FILE_FLAG_OVERLAPPED标志,而且我们需要使用ReadFileEx和 WriteFileEx 函数来请求I/O;

BOOL ReadFileEx(
  HANDLE                          hFile,
  LPVOID                          lpBuffer,
  DWORD                           nNumberOfBytesToRead,
  LPOVERLAPPED                    lpOverlapped,
  LPOVERLAPPED_COMPLETION_ROUTINE lpCompletionRoutine // 完成函数
)
;

BOOL WriteFileEx(
  HANDLE                          hFile,
  LPCVOID                         lpBuffer,
  DWORD                           nNumberOfBytesToWrite,
  LPOVERLAPPED                    lpOverlapped,
  LPOVERLAPPED_COMPLETION_ROUTINE lpCompletionRoutine// 完成函数
)
;

可提醒I/O是通过线程APC队列来实现的,当I/O请求完成时系统会将完成函数插入线程的APC队列中。为让线程执行APC函数,我们要让线程变为可提醒,通过以下函数可以做到。

DWORD SleepEx(
  DWORD dwMilliseconds,
  BOOL  bAlertable
)
;

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

DWORD WaitForMultipleObjectsEx(
  DWORD        nCount,
  const HANDLE *lpHandles,
  BOOL         bWaitAll,
  DWORD        dwMilliseconds,
  BOOL         bAlertable
)
;

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

BOOL WINAPI GetQueuedCompletionStatusEx(
  _In_  HANDLE             CompletionPort,
  _Out_ LPOVERLAPPED_ENTRY lpCompletionPortEntries,
  _In_  ULONG              ulCount,
  _Out_ PULONG             ulNumEntriesRemoved,
  _In_  DWORD              dwMilliseconds,
  _In_  BOOL               fAlertable
)
;

DWORD MsgWaitForMultipleObjectsEx(
  DWORD        nCount,
  const HANDLE *pHandles,
  DWORD        dwMilliseconds,
  DWORD        dwWakeMask,
  DWORD        dwFlags
)
;

这6个函数的具体使用可以查看MicoSoft 官方文档。

可提醒I/O优劣
  1. 回调函数 可提醒I/O必须创建一个回调函数,这使代码实现更加复杂。需要使用全局变量。
  2. 线程问题 发出I/O请求的线程必须处理完成的通知,线程需要对每个I/O请求作出响应,由于不存在负载均衡机制,所以程序伸缩性不太好。

I/O完成端口

经过上面的讨论我们终于可以讨论我们今天的主角I/O完成端口了。I/O完成端口背后的理论是并发运行的线程数必须要有个上限,如果线程数大于cpu数量,系统就必须花时间来执行线程上下文切换。这个开销则会很大。I/O完成端口的设计初衷就是配合线程池来使用的。下期将讨论Windows的线程池。

  1. 创建完成端口
HANDLE WINAPI CreateIoCompletionPort(
  _In_     HANDLE    FileHandle,
  _In_opt_ HANDLE    ExistingCompletionPort,
  _In_     ULONG_PTR CompletionKey,
  _In_     DWORD     NumberOfConcurrentThreads
)
;

这个函数能够创建一个I/O完成端口并将端口和设备关联。

I/O完成端口内部维护了设备列表、I/O完成队列、等待线程队列、已释放线程列表 已暂停线程列表,这使I/O完成端口能够清楚地知道如何调用线程。对于I/O完成端口的实现下期继续。今天就到这里了。

长按图片关注我哦~~~ 后续有更加精品的内容
长按图片关注我哦~~~ 后续有更加精品的内容
- END -
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值