前言
网络上大部分的讲解IOCP模型文章都比较断章取义,要么是这里冒出一个术语,那边出来一个不知名的名词。
本文主要是给那些暂时还无太多的Windows编程基础的人阅读,里面解释了一些相应的前驱知识。比如管道、重叠I/O模型等等。
如果你已经对这些了如指掌了,可以直接忽略本文——因为本文是给那些初学者看的。
不过即使是给初学者看的,很多概念只是提个大概,让读者心里有个印象而已。更进一步的详细知识还是需要读者自行翻阅相关资料。
前驱知识
管道
管道(PIPE)是用于进程间通信的一段共享内存。创建管道的进程称为管道服务器,连接到一个管道的进程称为管道客户机。一个进程在向管道写入数据之后,另一个进程就可以从管道的另一端将其读出来。
管道分两种,匿名管道和命名管道。
匿名管道
匿名管道是在父进程和子进程间单向传输数据的一种未命名管道,只能在本地计算机中使用,而不能用于网络间通信。
匿名管道由 CreatePipe()
函数创建。该函数在创建匿名管道的同时返回两个句柄:读句柄和写句柄。其原型如下:
BOOL CreatePipe(
PHANDLE hReadPipe,
PHANDLE hWritePipe,
LPSECURITY_ATTRIBUTES lpPipeAttributes,
DWORD nSize
);
其中
hReadPipe
为指向读句柄的指针,hWritePipe
为指向写句柄的指针;lpPipeAttributes
为指向安全属性的指针;最后的nSize
为管道大小,若为0
则由系统来决定。
匿名管道不支持异步读写操作。
命名管道
命名管道是在管道服务器和一台或多台管道客户机之间进行单向或者双向通信的一种命名的管道。一个命名管道的所有实例都共享同一个管道名,但是每一个实例都拥有独立的缓存和句柄,并且为 客户机 - 服务器
通信提供一个分离的管道。
命名管道可以在同一台计算机的不同进程之间或者跨越一个网络的不同计算机的不同进程间进行有连接的可靠数据通信。如果连接中断,连接双方都能立即受到连接断开的信息。
每个命名管道都有一个唯一的名字,以区分存在于系统的命名对象列表中的其它命名管道。管道服务器在调用 CreateNamedPipe()
函数创建管道的一个或多个实例时为其指定了名称。对于管道客户机,则是在调用 CreateFile()
或 CallNamedPipe()
函数在连接一个命名管道实例时对管道名进行指定。
命名管道对其标识采用 UNC格式
:
\\Server\Pipe\[Path]Name
其中第一部分 \\Server
指定了服务器的名字,命名管道服务就在此服务器创建。其字符串部分可以为一个小数点(表示本机)、星号(当前网络字段)、域名或者是一个真正的服务;第二部分是一个不可变化的硬编码字符串;第三部分 \[Path]Name
则使应用程序可以唯一定义及标识一个命名管道的名字,而且可以设置多级目录。
管道服务器首次调用 CreateNamedPipe()
函数时,使用 nMaxInstance
参数指定了能同时存在的管道实例的最大数目。服务器可以重复调用 CreateNamedPipe()
函数去创建新的管道实例,直至达到设定的最大实例数。
下面给出 CreateNamedPipe()
的函数原型:
HANDLE CreateNamedPipe(
LPCTSTR lpName,
DWORD dwOpenMode,
DWORD dwPipeMode,
DWORD nMaxInstance,
DWORD nOutBufferSize,
DWORD nInBufferSize,
DWORD nDefaultTimeOut,
LPSECURITY_ATTRIBUTES lpSecurityAttributes
);
这里的
lpName
就是所谓的管道名称指针了,dwOpenMode
为管道打开的模式(用来指示管道在创建好之后,它的传输方向、I/O控制以及安全模式),dwPipeMode
为管道模式,nMaxInstance
正如之前所说的是最大的管道实例数,nOutBufferSize
为输出缓存的大小,nInBufferSize
为输入缓存的大小,nDefaultTimeOut
为超时设置,最后的lpSecurityAttributes
为安全属性的指针。
CreateFile, ReadFile等API
CreateFile()
这个函数可以创建或者打开一个对象的句柄,凭借此句柄我们就可以控制这些对象:
- 控制台对象
- 通信资源对象
- 目录对象(只能打开)
- 磁盘设备对象
- 文件对象
- 邮槽对象
- 管道对象
函数原型:
HANDLE CreateFile(
LPCTSTR lpFileName,
DWORD dwDesiredAccess,
DWORD dwShareMode,
LPSECURITY_ATTRIBUTES lpSecurityAttributes,
DWORD dwCreationDisposition,
DWORD dwFlagsAndAttributes,
HANDLE hTemplateFile
);
参数解析
lpFileName: 一个指向无终结符的字符串指针,用来指明要创建或者打开的对象的名字。
dwDesiredAccess: 指明对象的控制模式。一个应用程序可以包含读控制、写控制、读/写控制、设备查询控制。
dwShareMode: 指定对象的共享模式。如果
dwShareMode == 0
则表示是互斥使用的。如果CreateFile
打开成功,则别的程序只能等到当前程序关闭对象句柄CloseHandle
后才能再打开或者使用。lpSecurityAttributes: 一个指向
SECURITY_ATTRIBUTES
结构对象的指针,决定返回的句柄是否被子进程所继承。如果lpSecurityAttributes
参数为NULL
,句柄就不能被子进程继承。dwCreationDisposition: 指明当打开的对象存在或不存在的时候各需要怎么样去处理。
dwFlagsAndAttributes: 指定文件属性和标志。
hTemplateFile: 把具有
GENERIC_READ
权限的句柄指定为一个模板文件。这个模板文件提供了文件属性和扩展属性,用于创建文件。返回值
如果调用成功,返回值是一个打开文件的句柄。
如果调用之前文件已经存在,且
dwCreationDisposition
参数为CREATE_ALWAYS
或者OPEN_AWAYS
,用GetLastError
返回ERROR_ALREADY_EXISTS
(即使调用成功也会返回这个值)。如果调用之前不存在GetLastError
返回0
。如果调用失败,返回值是
INVALID_HANDLE_VALUE
。要进一步了解出错原因,调用GetLastError
。
CloseHandle()
用于关掉一个打开的对象句柄。
函数原型如下:
BOOL CloseHandle(
HANDLE hObject
);
ReadFile()
ReadFile()
函数从文件指针指定的位置读取数据。读操作完毕之后,文件指针将根据实际读出的数据自动进行调整,除非文件句柄是以 OVERLAPPED
属性值打开的。如果是以 OVERLAPPED
打开的I/O,应用程序就需要自己手动调整文件指针。
这个函数被设计成兼有同步和异步操作。 ReadFileEx()
函数则设计成只支持异步操作,异步操作允许应用程序在读文件期间可以同时进行其它的操作。
函数原型:
BOOL ReadFile(
HANDLE hFile,
LPVOID lpBuffer,
DWORD nNumberOfBytesToRead,
LPDWORD lpNumberOfBytesRead,
LPOVERLAPPED lpOverlapped
);
参数解析
hFile: 文件句柄(必须具有
GENERIC_READ
访问权限)。lpBuffer: 用来接收从文件中读出的数据的缓冲区。
nNumberOfBytesToRead: 指明要读取的字节总数。
lpNumberOfBytesRead: 一个变量指针,用来存储实际传输的字节总数。
ReadFile
在做所有事情(包括错误检查)之前,先将这个值赋为0
。当ReadFile
从一个命名管道上返回TRUE
时这个参数为0
,说明消息管道另一端调用WriteFile
时设置的nNumberOfBytesToWrite
参数为0
。如果lpOverlapped
不是NULL
,lpNumberOfBytesRead
可以设置为NULL
。如果是一个Overlapped
形式的读操作,我们可以动用GetOverlappedResult
函数来获得传输的实际字节数。如果hFile
关联的是一个完成端口(I/O Completion Port),那么可以调用GetQueuedCompletionStatus
函数来获得传输的实际字节数。如果完成端口被占用,而你用的是一个用于释放内存的回调例程,对于lpOverlapped
参数指向的OVERLAPPED
结构体来说,为这个参数指定NULL
可以避免重新分配内存时发生内存泄露。内存泄露会导致返回这个参数值时是一个非法值。lpOverlapped: 一个指向
OVERLAPPED
结构体的指针。如果hFile
是以FILE_FLAG_OVERLAPPED
方式获得的句柄,这个结构是必须的,不能为NULL
(否则函数会在错误的时刻报告读操作已经完成了)。这时,读操作在由OVERLAPPED
中Offset
成员指定的偏移地址开始读,并且在实际完成读操作之前就返回了。在这种情况下,ReadFile
返回FALSE
,GetLastError
报告的错误类型是ERROR_IO_PENDING
。这允许调用进程继续其它工作直到读操作完成。OVERLAPPED
结构中的事件将会在读操作完成时被使用。返回值
有如下任一种情况发生都会导致函数返回:
- 在管道另一端的写操作完成后。
- 请求的字节数传输完毕。
- 发生错误。
如果函数正确,返回非零。
如果返回值是非零但接受的字节数为
0
,那么可能是文件指针在读操作期间超出了文件的end
位置。然而如果文件以FILE_FLAG_OVERLAPPED
方式打开,lpOverlapped
参数不为NULL
,文件指针在读操作期间超出了文件的end
位置,那么返回值肯定是FALSE
,GetLastError
返回的错误是ERROR_HANDLE_EOF
。
WriteFile
可以以同步或异步方式向一个对象句柄中写数据。
函数原型:
BOOL WriteFile(
HANDLE hFile,
LPCVOID lpBuffer,
DWORD nNumberOfBytesToWrite,
LPDWORD lpNumberOfBytesWritten,
LPOVERLAPPED lpOverlapped
);
其它信息与 ReadFile
极其相似,可以参考 ReadFile
。
Winsock重叠I/O模型
重叠I/O模型的概念
当调用 ReadFile()
和 WriteFile()
时,如果最后一个参数 lpOverlapped
设置为 NULL
,那么线程就阻塞在这里,知道读写完指定的数据后,它们才会返回。这样在读写大文件的时候,很多时间都浪费在等待 ReadFile()
和 WriteFile()
的返回上面。如果 ReadFile()
和 WriteFile()
是往管道里面读写数据,那么有可能阻塞更久,导致程序性能下降。
为了解决这个问题,Windows引进了重叠I/O的概念,它能够同时以多个线程处理多个I/O。其实你自己开多个线程也可以处理多个I/O,但是系统内部对I/O的处理在性能上有很大的优化。它是Windows下实现异步I/O的最常用的方式。
Windows为几乎全部类型的文件提供这个工具:磁盘文件、通信端口、命名管道和套接字。通常,使用 ReadFile()
和 WriteFile()
就可以很好地执行重叠I/O。
重叠模型的核心是一个重叠数据结构。若想以重叠方式使用文件,必须用 FILE_FLAG_OVERLAPPED
标志打开它,例如:
HANDLE hFile = CreateFile(
lpFileName,
GENERIC_READ | GENERIC_WRITE,
FILE_SHARE_READ | FILE_SHARE_WRITE,
NULL,
OPEN_EXISTING,
FILE_FLAG_OVERLAPPED,
NULL
);
如果没有规定该标志,则针对这个文件(句柄),重叠I/O是不可用的。如果设置了该标志,当调用 ReadFile()
和 WriteFile()
操作这个文件(句柄)时,必须为最后一个参数提供 OVERLAPPED
结构:
// WINBASE.H
typedef struct _OVERLAPPED {
DWORD Internal;
DWORD InternalHigh;
DWORD Offset;
DWORD OffsetHigh;
HANDLE hEvent;
} OVERLAPPED, *LPOVERLAPPED;
头两个32位的结构字 Internal
和 InternalHigh
由系统内部使用。