I/O 完成端口实现
这篇文章是继承上篇《Windows 同步设备 I/O 与异步设备 I/O》,未读过的读者可以去看看再来看这篇文章哈。
I/O完成端口接口封装
- 创建新的I/O完成端口
I/O完成可能是最复杂的windows内核对象了,为了创建一个I/O完成端口我们需要调用CreateIoCompletionPort函数:
HANDLE WINAPI CreateIoCompletionPort(
_In_ HANDLE FileHandle,
_In_opt_ HANDLE ExistingCompletionPort,
_In_ ULONG_PTR CompletionKey,
_In_ DWORD NumberOfConcurrentThreads
);
这个函数执行两项不同的任务:它不仅会创建一个I/O完成端口,而且会将一个设备与一个I/O完成端口关联起来。为了只创建一个I/O完成端口我们可以对上面的函数进行封装:
HANDLE CreateNewCompletionPort(DWORD threadSize)
{
return (CreateIoCompletionPort(INVALID_HANDLE_VALUE, NULL, 0, threadSize));
}
这个函数只有一个参数threadSize,它调用了CreateIoCompletionPort来创建一个新I/O完成端口。为了只创建一个I/O完成端口前三个参数传入固定值INVALID_HANDLE_VALUE, NULL和0,第四个参数告诉I/O完成端在统一时间最多有多少线程处于可运行的状态。如果threadSize传入0,那么I/O端口会使用默认值也就是CPU数量。这个通常也是我们想要的。
- 将设备和I/O完成端口关联
当我们创建一个I/O完成端口的时候,系统内核实际上会创建5个不同的数据结构。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-FQJ3urmZ-1607852667079)(https://imgkr2.cn-bj.ufileos.com/2d1e1331-ee16-490a-bf10-d1c30f620267.png?UCloudPublicKey=TOKEN_8d8b72be-579a-4e83-bfd0-5f6ce1546f13&Signature=NfrwmFUykaoOYWgvNVXx1FFCWXA%253D&Expires=1607929713)]
我们封装一个函数将I/O完成端口和设备关联:
BOOL DeviceWithCopletionPort(HANDLE hCompletionPort, HANDLE hDevice, DWORD dwCompletionKey)
{
HANDLE h = CreateIoCompletionPort(hDevice, hCompletionPort, dwCompletionKey, 0);
return (h == hCompletionPort);
}
hCompletionPort 为CreateNewCompletionPort创建完成端口的句柄、hDevice为设备句柄、dwCompletionKey 为完成键当I/O完成时会返回我们设置的dwCompletionKey的值。这个值操作系统并不关心但是我们自己需要关心。
- 如何使线程等待I/O完成通知
线程通过调用GetQueuedCompletionStatus可以将自己切换到睡眠状态,来等待设备I/O请求完成并进入完成端口。
BOOL GetQueuedCompletionStatus(
HANDLE CompletionPort,
LPDWORD lpNumberOfBytesTransferred,
PULONG_PTR lpCompletionKey,
LPOVERLAPPED *lpOverlapped,
DWORD dwMilliseconds
);
CompletionPort表示线程希望对哪个完成端口进行监视。lpNumberOfBytesTransferred 为已传输的字节、lpCompletionKey为完成键、lpOverlapped 为OVERLAPPED结构地址。
使用I/O完成端口例子
- 直接上代码
//新建工程IOComplet工程, 拷贝代码到main.cpp
#include <Windows.h>
#include <stdio.h>
#include <thread>
#define VIOSERIAL_PORT_PATH L"E:\\code\\IOComplet\\Debug\\IOComplet1.pdb"
#define Max_Size 20
char buf[Max_Size];
HANDLE hPort;
HANDLE h;
HANDLE CreateNewCompletionPort(DWORD threadSize)
{
return (CreateIoCompletionPort(INVALID_HANDLE_VALUE, NULL, 0, threadSize));
}
BOOL DeviceWithCopletionPort(HANDLE hCompletionPort, HANDLE hDevice, DWORD dwCompletionKey)
{
HANDLE h = CreateIoCompletionPort(hDevice, hCompletionPort, dwCompletionKey, 0);
return (h == hCompletionPort);
}
class MyIO : public OVERLAPPED
{
public:
MyIO(){}
enum IOType
{
IO,
IO_READ,
IO_WRITE
} m_ioType;
};
void func()
{
while (true)
{
printf("while (true)\n");
DWORD dwNumBytes;
ULONG_PTR completionKey;
MyIO * myIO;
BOOL ret = GetQueuedCompletionStatus(hPort, &dwNumBytes, &completionKey,
(OVERLAPPED**)&myIO, INFINITE);
DWORD err = GetLastError();
if (ret)
{
printf("GetQueuedCompletionStatus err = %d\n", ret);
if (completionKey == MyIO::IOType::IO)
{
printf("completionKey == MyIO::IOType::IO\n");
if (myIO != NULL)
{
if (myIO->m_ioType == MyIO::IOType::IO_READ)
{
printf("myIO->m_ioType == MyIO::IOType::IO_READ\n");
MyIO myIORead;
ZeroMemory(&myIORead, sizeof(MyIO));
myIORead.m_ioType = MyIO::IOType::IO_READ;
ZeroMemory(buf, Max_Size);
ReadFile(h, buf, Max_Size, NULL, &myIORead);
}
}
}
}
printf("end\n");
}
printf("end end\n");
}
int main()
{
h = CreateFile(VIOSERIAL_PORT_PATH, GENERIC_READ | GENERIC_WRITE, 0, NULL,
OPEN_EXISTING, FILE_FLAG_OVERLAPPED, NULL); // 设备句柄
if (h == INVALID_HANDLE_VALUE)
{
printf("h = %d, error=%d\n", h, GetLastError());
}
else
{
hPort = CreateNewCompletionPort(0);
BOOL ret = DeviceWithCopletionPort(hPort, h, MyIO::IOType::IO);
if (ret == FALSE)
{
printf("DeviceWithCopletionPort err = %d\n", ret);
}
std::thread proc(func);
proc.detach();
Sleep(1000);
MyIO myIO;
myIO.hEvent = NULL;
myIO.Internal = 0;
myIO.InternalHigh = 0;
myIO.Offset = 0;
myIO.OffsetHigh = 0;
myIO.m_ioType = MyIO::IOType::IO_READ;
DWORD readSize;
BOOL retR = ReadFile(h, buf, Max_Size, &readSize, &myIO);
if (retR == FALSE)
{
DWORD errRet = GetLastError();
if (errRet == ERROR_IO_PENDING)
{
printf("retR == ERROR_IO_PENDING\n");
}
}
}
SleepEx(INFINITE, FALSE);
return 0;
}
这个简单例子是为了读者能更好理解IO端口,首先这个程序以读写打开了一个文件句柄,随后创建了I/O完成端口并将文件句柄和它关联,最后利用thread创建了一个线程等待I/O完成。在线程函数func中调用了GetQueuedCompletionStatus等待I/O完成。读者应该注意到这里MyIO类继承了OVERLAPPED添加了成员m_ioType这是为了区分I/O完成的类型。这个例子只是为了读者理解I/O完成端口,在实际使用中应该穿件一个线程池来等待I/O完成。只要读者理解I/O完成端口的原理,我想读者应该不难实现一个伸缩性好的I/O完成端口的服务程序。