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, 100, NULL, &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, 10, NULL, &overRead);
BYTE bWriteBuf[10];
OVERLAPPED overWrite = {0};
overWrite.Offset = 0;
overWrite.hEvent = CreateEvent(...);
ReadFile(hFile, bReadBuf, 10, NULL, &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优劣
-
回调函数 可提醒I/O必须创建一个回调函数,这使代码实现更加复杂。需要使用全局变量。 -
线程问题 发出I/O请求的线程必须处理完成的通知,线程需要对每个I/O请求作出响应,由于不存在负载均衡机制,所以程序伸缩性不太好。
I/O完成端口
经过上面的讨论我们终于可以讨论我们今天的主角I/O完成端口了。I/O完成端口背后的理论是并发运行的线程数必须要有个上限,如果线程数大于cpu数量,系统就必须花时间来执行线程上下文切换。这个开销则会很大。I/O完成端口的设计初衷就是配合线程池来使用的。下期将讨论Windows的线程池。
-
创建完成端口
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 -