Windows API笔记(一)内核对象
Windows API笔记(二)进程和进程间通信、进程边界
Windows API笔记(三)线程和线程同步、线程局部存储
Windows API笔记(四)win32内存结构
Windows API笔记(五)虚拟内存
Windows API笔记(六)内存映射文件
Windows API笔记(七)堆
Windows API笔记(八)文件系统
Windows API笔记(九)窗口消息
Windows API笔记(十)动态链接库
Windows API笔记(十一)设备I/O
文章目录
常见设备和用处:
设备 | 常见的用处 |
---|---|
文件 | 任意数据的持久化存储 |
目录 | 属性和文件压缩 |
逻辑磁盘 | 格式化 |
物理磁盘 | 分区表访问 |
串行口 | 通过电话线传输数据 |
并行口 | 向打印机传输数据 |
邮件槽 | 一对多的传输数据,通常通过网络向一台Windows机器传输 |
命名管道 | 一对一的传输数据,通常通过网络向一台Windows机器传输 |
无名管道 | 在同一机器上一对一的传输数据(不经过网络) |
套接字 | 数据传输的数据流,通常通过网络向任一支持套接字的计算机传输(不一定运行Windows) |
控制台 | 文本窗口屏幕缓冲区 |
Win32尽力对软件开发人员屏蔽了设备的不同,即一旦你打开一个设备,不管你同什么设备通信,Win32的读写数据的函数都是一样的。尽管有些Win32函数不管什么设备都能读写数据,设备和设备之间还是不同的。例如,对串行口设备波特率是有意义的,而当使用命名管道时,波特率则毫无意义。
1. 打开和关闭设备
要进行任何形式的I/O,必须首先打开所需的设备,并得到它的句柄。如何得到设备的句柄取决于不同的设备,和打开它们的函数:
HANDLE CreateFileA(
_In_ LPCSTR lpFileName,
_In_ DWORD dwDesiredAccess,
_In_ DWORD dwShareMode,
_In_opt_ LPSECURITY_ATTRIBUTES lpSecurityAttributes,
_In_ DWORD dwCreationDisposition,
_In_ DWORD dwFlagsAndAttributes,
_In_opt_ HANDLE hTemplateFile
)
在《Windows API笔记(一)内核对象》已经写过Create**函数。
设备 | 打开设备的函数 |
---|---|
文件 | CreateFile(lpFileName是路径名或UNC路径名) |
目录1 | CreateFile(lpFileName是目录名或UNC目录名,使用FILE_FLAG_BACKUP_SEMANTICS) |
逻辑盘2 | CreateFile(lpFileName="\.\x:"); |
物理盘3 | CreateFile(lpFileName="\.\PHYSICALDRIVEx") |
串行口 | CreateFile(lpFileName=“COM*”) |
并行口 | CreateFile(lpFileName=“LPT*”) |
邮件槽服务端 | CreateMailslot(lpFileName="\.\mailslot\mailslotname") |
邮件槽客户端 | CreateFile(lpFileName="\servername\mailslot\mailslotname") |
命名管道服务端 | CreateNamedPipe(lpFileName="\.\pipe\pipename") |
命名管道客户端 | CreateFile(lpFileName="\servename\pipe\pipename") |
无名管道客户端和服务端 | CreatePipe |
套接字 | socket,accept,AcceptEx |
控制台 | CreateConsoleScreenBuffer,GetStdHandle |
- 如果在调用CreateFile时使用FILE_FLAG_BACKUP_SEMANTICS标志,Windows允许你打开一个目录,这使得你可以改变目录的属性(普通/隐藏等)和它的时间戳;
- 如果以“\.\x:”(x是一个驱动器符)的形式指定一个字符串,Windows允许你打开一个逻辑盘。例如,要打开D盘,你可以指定“\.\D:”,打开一个盘使得你可以格式化或确定它的介质类型;
- 如果你以“\.\PHYSICALDRIVEx”(x是一个物理盘符)的形式指定一个字符串,Windows允许你打开一个物理盘。例如,要读写用户的第一个物理硬盘的物理扇区,你可以指定“\.\PHYSICALDRIVE0”。打开一个物理盘允许你直接访问硬盘的分区表。打开物理盘存在潜在的危险。对盘的不正确的写可能使操作系统的文件系统无法访问盘的内容。
所有这些函数都返回标识设备的一个32/64位句柄,通过将句柄传递给不同的操作函数来与设备通信。例如,你可以调用SetCommConfig来设置一个串行口的波特率:
BOOL SetCommConfig(HANDLE hCommDev,LPCOMMCONFIG lpCC,DWORD dwSize);
当完成设备操作后,必须关闭它,对于大多数设备,可以调用CloseHandle:
BOOL CloseHandle(HANDLE hObject);
如果设备是一个套接字,必须调用closesocket:
int closesocket(SOCKET s);
通过GetFileType来查看句柄的设备类型:
DWORD GetFileType(HANDLE hFile);
标志 | 描述 |
---|---|
FILE_TYPE_UNKONWN | 未知设备类型 |
FILE_TYPE_DISK | 磁盘 |
FILE_TYPE_CHAR | 字符文件,一般是LPT设备或控制台 |
FILE_TYPE_PIPE | 命名或无命名管道 |
1.1 CreateFile
HANDLE CreateFile(
_In_ LPCSTR lpFileName,
_In_ DWORD dwDesiredAccess,
_In_ DWORD dwShareMode,
_In_opt_ LPSECURITY_ATTRIBUTES lpSecurityAttributes,
_In_ DWORD dwCreationDisposition,
_In_ DWORD dwFlagsAndAttributes,
_In_opt_ HANDLE hTemplateFile
)
参数名 | 说明 | 取值范围 |
---|---|---|
lpFileName | 标识设备类型和设备的某个示例; 如果打开文件,可以传递_MAX_PATH(定义为260)个字符的lpFilePath路径名。还可以使用CreateFileW(Unicode版本)并在路径名前加上“\\?\”来超越这一限制。如果加上这些字符,CreatW会移除这些字符,并允许传递一个长度约为32000字符长的lpFileName路径。不过字符必须使用绝对路径,不允许出现"."和".."。 | |
dwDesiredAccess | 设置设备访问权限 | 0 不想读写设备数据,只改变设备的配置属性,如文件的时间戳 |
GENERIC_READ 只读 | ||
GENERIC_WRITE 只写 | ||
GENERIC_READ\|GENERIC_WRITE 读写 | ||
dwShareMode | 设置设备共享权限 | 0 独占,如果设备已打开,CreateFile失败;若打开成功,在关闭前不允许再CreateFile |
FILE_SHARE_READ 只读,如果设备已为写打开,CreateFile失败;如果打开成功,之后以写调用CreateFile将会失败 | ||
FILE_SHARE_WRITE 只写,如果设备已为读打开,CreateFile失败;如果打开成功,将来要为读而调用的CreateFile会失败 | ||
FILE_SHARE_READ|FILE_SHARE_WRITE 可读可写,如果设备已被独占打开,CreateFile失败;如果打开成功,将来为读写而调用的CreateFile会失败 | ||
lpSecurityAttributes | 指向一个SECURITY_ATTRIBUTES结构,设定设备关联的内核对象的安全信息和返回的句柄是否可继承(允许子进程访问设备),如果想使用默认的安全属性和不让句柄继承,可设置为NULL | |
dwCreationDisposition | 文件打开方式 | CREATE_NEW 创建一个新文件,如果已有同名文件,则失败 |
CREATE_ALWAYS 不管同名文件是否存在都创建一个新文件,存在则覆盖 | ||
OPEN_EXITING 打开一个已存在的文件,不存在则失败,不是打开文件的设备时,应使用此选项 | ||
OPEN_ALWAYS 打开一个文件,如果文件不存在则创建 | ||
TRUNCATE_EXISTING 打开一个已存在的文件,清空文件;如果文件不存在则失败;GENERIC_WRITE标志必须同该标志一起使用 | ||
dwFlagsAndAttributes | 微调同设备的通信,如果设备是文件,还能设置文件的属性;标志可以告诉系统你打算如何访问设备,系统就可以优化它的缓存算法来帮助你的程序工作更有效 | FILE_FLAG_NO_BUFFERING 与缓冲有关,通过系统会使用缓冲预读数据,提供读写性能,但是会浪费内存;此选项可以关闭缓冲 |
FILE_FLAG_SEQUENTIAL_SCAN 与缓冲有关,顺序访问文件,系统会预读数据,减少访问次数,提供程序速度;设置了FILE_FLAG_NO_BUFFERING则此选项无效 | ||
FILE_FLAG_RANDOM_ACCESS 与缓冲有关,不要预读文件数据;设置了FILE_FLAG_NO_BUFFERING则此选项无效 | ||
FILE_FLAG_WRITE_THROUGH 与缓冲有关,使文件的缓冲无效,不缓冲写入数据,降低数据丢失的可能性 | ||
FILE_FLAG_DELETE_ON_CLOSE 系统在关闭文件后删除文件,经常同FILE_ATTRIBUTE_TEMPORARY属性一起使用,用于创建临时文件;当文件被多个进程打开后,系统会等所有打开句柄都被关闭后再删除它 | ||
FILE_FLAG_BACKUP_SEMANTICS 用于备份或恢复软件 | ||
FILE_FLAG_POSIX_SEMANTICS 使用POSIX规则来访问文件,大小写敏感 | ||
FILE_FLAG_OVERLAPPED 异步访问设备;打开设备的默认方式是同步I/O(不指定FILE_FLAG_OVERLAPPED)。同步I/O从文件读取数据时,程序被挂起,等待数据被读完。一旦数据读取完成,程序重新获得控制,继续执行 | ||
FILE_ATTRIBUTE_NORMAL 不设置任何属性,单独使用才有效 | ||
FILE_ATTRIBUTE_READONLY 只读,程序可以读文件,不能写或删除 | ||
FILE_ATTRIBUTE_SYSTEM 文件是操作系统的一部分,或者被操作系统独占使用 | ||
FILE_ATTRIBUTE_COMPRESSED 文件或目录是压缩的 | ||
FILE_ATTRIBUTE_OFFLINE 文件存储在,但它的数据被移到离线的存储种,该标志对层次存储系统是有用的 | ||
FILE_ATTRIBUTE_TEMPORARY 临时文件,文件系统努力把文件的数据保存在RAM而不是磁盘,来使访问时间尽可能短;当RAM存储不下时会转移到磁盘 | ||
FILE_ATTRIBUTE_ARCHIVE 存档文件,应用程序使用该标志来对文件标记为备份或删除;当CreateFile创建一个新文件时,该标志自动被设置 | ||
hTemplateFile | 属性标志模板文件句柄,标识一个打开的文件句柄或为NULL,如果是文件句柄,CreateFile将完全忽略dwFlagsAndAttributes参数,而使用hTemplateFile文件句柄相同的属性 | |
返回值 | 成功返回文件句柄,失败返回INVALID_HANDLE_VALUE |
#include <Windows.h>
#include <stdio.h>
int main()
{
// 创建/打开文件
DWORD dwDesiredAccess = GENERIC_WRITE | GENERIC_READ; // 可读可写
DWORD dwShareMode = 0; // 0 不共享,独占,其他进程将不能再次打开文件
LPSECURITY_ATTRIBUTES lpSecurityAttributes = NULL; // 使用默认的安全属性,句柄不可继承
DWORD dwCreationDisposition = OPEN_ALWAYS; // 文件打开方式
DWORD dwFlagsAndAttributes = FILE_FLAG_DELETE_ON_CLOSE | FILE_ATTRIBUTE_TEMPORARY; // 创建临时文件,数据尽量写到RAM,不写入磁盘,提高速度;程序关闭后文件将自动删除
HANDLE handle = CreateFile("test.txt", dwDesiredAccess, dwShareMode, lpSecurityAttributes, dwCreationDisposition, dwFlagsAndAttributes, NULL);
if (INVALID_HANDLE_VALUE == handle)
{
printf("CreateFile Failed %ld\n", handle);
}
// 独占,再次打开将失败
// HANDLE handle2 = CreateFile("test.txt", dwDesiredAccess, dwShareMode, lpSecurityAttributes, dwCreationDisposition, dwFlagsAndAttributes, NULL);
// if (INVALID_HANDLE_VALUE == handle2)
// {
// printf("CreateFile 2 Failed %ld\n", handle2);
// }
system("pause");
}
2. 使用文件设备
- 定位文件指针
- 改变文件大小
- 解锁和枷锁文件种的区域(记录锁)
2.1 定位文件指针
移动指针位置
DWORD
SetFilePointer(
_In_ HANDLE hFile,
_In_ LONG lDistanceToMove,
_Inout_opt_ PLONG lpDistanceToMoveHigh,
_In_ DWORD dwMoveMethod
);
- hFile:文件句柄
- lDistanceToMove:移动位置,低位
- lpDistanceToMoveHigh:移动位置,高位
- dwMoveMethod:移动方式(起点),FILE_BEGIN、FILE_CURRENT、FILE_END
- 成功将返回lDistanceToMove的值,失败返回0xFFFFFFFF
2.2 设置文件尾
设置当前文件位置(offset)为文件尾。可以改变当前文件的大小。
BOOL SetEndOffFile(HANDLE hFile);
强制一个文件为1024字节长:
#include <Windows.h>
int main()
{
// 创建/打开文件
DWORD dwDesiredAccess = GENERIC_WRITE | GENERIC_READ; // 可读可写
DWORD dwShareMode = 0; // 0 不共享,独占,其他进程将不能再次打开文件
LPSECURITY_ATTRIBUTES lpSecurityAttributes = NULL; // 使用默认的安全属性,句柄不可继承
DWORD dwCreationDisposition = OPEN_ALWAYS; // 文件打开方式
DWORD dwFlagsAndAttributes = FILE_ATTRIBUTE_NORMAL; // 不设置属性
HANDLE handle = CreateFile("test.txt", dwDesiredAccess, dwShareMode, lpSecurityAttributes, dwCreationDisposition, dwFlagsAndAttributes, NULL);
if (INVALID_HANDLE_VALUE == handle)
{
printf("CreateFile Failed %ld\n", handle);
}
SetFilePointer(handle, 1024, NULL, FILE_BEGIN); // 定位文件
SetEndOfFile(handle); // 设置当前位置为文件尾
CloseHandle(handle); // 关闭文件句柄
}
在文件夹中可以查看文件长度。
2.3 加锁和解锁文件的区域
- LockFile
- UnLockFile
- LockFileEx
FILE_SHARE_READ和FILE_SHARE_WRITE影响的是整个文件,而文件加锁只影响文件的一部分,可以精确控制文件的读写权限。
加锁后其他进程将不能对文件进行读写。
加锁:
BOOL
LockFile(
_In_ HANDLE hFile,
_In_ DWORD dwFileOffsetLow,
_In_ DWORD dwFileOffsetHigh,
_In_ DWORD nNumberOfBytesToLockLow,
_In_ DWORD nNumberOfBytesToLockHigh
);
- hFile:文件句柄
- dwFileOffsetLow:加锁起始位置的低位
- dwFileOffsetHigh:加锁起始位置的高位
- nNumberOfBytesToLockLow:加锁的内容长度的低位
- nNumberOfBytesToLockHigh:加锁的内容长度的高位
加锁后再次加锁将失败。
解锁:
BOOL
UnlockFile(
_In_ HANDLE hFile,
_In_ DWORD dwFileOffsetLow,
_In_ DWORD dwFileOffsetHigh,
_In_ DWORD nNumberOfBytesToUnlockLow,
_In_ DWORD nNumberOfBytesToUnlockHigh
);
解锁的参数必须与加锁的参数完全一致。
FlockFileEx:
可以允许其他进程从加锁的区域读数据
BOOL
LockFileEx(
_In_ HANDLE hFile,
_In_ DWORD dwFlags,
_Reserved_ DWORD dwReserved,
_In_ DWORD nNumberOfBytesToLockLow,
_In_ DWORD nNumberOfBytesToLockHigh,
_Inout_ LPOVERLAPPED lpOverlapped
);
- dwFlags:缺省时为共享锁,LOCKFILE_EXCLUSIVE_LOCK为独占锁;若区域被锁定,默认将会一直等待,LOCKFILE_FAIL_IMMEDIATELY将会立即返回
- dwReserved:预留参数,默认为0
- lpOverlapped:用到OVERLAPPED结构的Offset、OffsetHigh参数,用于指定锁定起始位置;LockFileEx只有nNumberOfBytesToLock*参数,设置锁定区域的大小,没有直接设置锁定区域地址的参数
OVERLAPPED结构
typedef struct _OVERLAPPED {
ULONG_PTR Internal;
ULONG_PTR InternalHigh;
union {
struct {
DWORD Offset; // 当前LockFileEx只用到了Offset、OffsetHigh两个元素
DWORD OffsetHigh;
} DUMMYSTRUCTNAME;
PVOID Pointer;
} DUMMYUNIONNAME;
HANDLE hEvent;
} OVERLAPPED, *LPOVERLAPPED;
测试,同时2次或以上允许此程序进行测试:
#include <Windows.h>
#include <stdio.h>
int main()
{
char buf[100] = {0};
OVERLAPPED overlapped;
// BOOL bRet;
// 文件加锁
// FILE_SHARE_* 影响的是整个文件,而文件加锁只影响文件的一部分
// 创建/打开文件
DWORD dwDesiredAccess = GENERIC_WRITE | GENERIC_READ; // 可读可写
DWORD dwShareMode = FILE_SHARE_READ | FILE_SHARE_WRITE; // 读写共享
LPSECURITY_ATTRIBUTES lpSecurityAttributes = NULL; // 使用默认的安全属性,句柄不可继承
DWORD dwCreationDisposition = OPEN_ALWAYS; // 文件打开方式
DWORD dwFlagsAndAttributes = FILE_ATTRIBUTE_NORMAL; // 不设置属性
HANDLE handle = CreateFile("test2.txt", dwDesiredAccess, dwShareMode, lpSecurityAttributes, dwCreationDisposition, dwFlagsAndAttributes, NULL);
if (INVALID_HANDLE_VALUE == handle)
{
printf("CreateFile Failed %ld\n", handle);
}
BOOL bRet = LockFile(handle, 100, NULL, 10, NULL); // 锁定文件 100 ~ 110 这部分记录
if (bRet)
{
printf("LockFile Success\n");
}
else
{
printf("LockFile Failed, Use LockFileEx Try Again\n");
overlapped.Offset = 100;
overlapped.OffsetHigh = NULL;
// 锁定文件失败
bRet = LockFileEx(handle, 0, 0, 10, NULL, &overlapped);
if (!bRet)
{
printf("LockFileEx Falied\n");
return -1;
}
}
SetFilePointer(handle, 100, NULL, FILE_BEGIN);
DWORD dWritten;
bRet = ReadFile(handle, &buf, 10, &dWritten, NULL);
if (bRet)
{
printf("ReadFile Success\n");
}
else
{
bRet = GetLastError();
printf("ReadFile Failed %ld\n", bRet);
goto unlock;
return -1;
}
printf("read data : %s\ninput data:\n", buf);
scanf("%s", buf);
SetFilePointer(handle, 100, NULL, FILE_BEGIN);
bRet = WriteFile(handle, buf, 10, &dWritten, NULL);
if (bRet)
{
printf("WriteFile Success\n");
}
else
{
printf("WriteFile Failed\n");
}
unlock:
// 解锁,解锁的参数必须和加锁的参数完全相同
bRet = UnlockFile(handle, 100, NULL, 10, NULL);
if (bRet)
{
printf("UnlockFile Success\n");
}
else
{
printf("UnlockFile Failed\n");
}
}
3. 同步I/O
读写设备的最简单和最常用的函数,可用于任何设备:
BOOL
ReadFile(
_In_ HANDLE hFile,
_Out_writes_bytes_to_opt_(nNumberOfBytesToRead, *lpNumberOfBytesRead) __out_data_source(FILE) LPVOID lpBuffer,
_In_ DWORD nNumberOfBytesToRead,
_Out_opt_ LPDWORD lpNumberOfBytesRead,
_Inout_opt_ LPOVERLAPPED lpOverlapped
);
BOOL
WriteFile(
_In_ HANDLE hFile,
_In_reads_bytes_opt_(nNumberOfBytesToWrite) LPCVOID lpBuffer,
_In_ DWORD nNumberOfBytesToWrite,
_Out_opt_ LPDWORD lpNumberOfBytesWritten,
_Inout_opt_ LPOVERLAPPED lpOverlapped
);
若需要同步操作设备,在CreateFile打开设备时不能设置FILE_FLAG_OVERLAPPED标志,否则被认为要对设备进行异步I/O。
- hFile:文件句柄
- lpBuffer:要读取或写入数据的用户缓冲区
- nNumberOfBytesToWrite:要读取或写入的数据长度
- lpNumberOfBytesWritten:实际读取或写入的数据长度
- lpOverlapped:同步操作时为NULL
- 返回值:成功为TURE,失败为FALSE
3.1 向设备强制刷新数据
在打开文件时,可设置缓冲数据方式;FlushFileBuffers可强迫系统把缓冲区数据写回指定设备:
BOOL
FlushFileBuffers(
_In_ HANDLE hFile
);
/*同步I/O*/
#include <Windows.h>
#include <stdio.h>
int main()
{
// 创建/打开文件
DWORD dwDesiredAccess = GENERIC_WRITE | GENERIC_READ; // 可读可写
DWORD dwShareMode = FILE_SHARE_READ | FILE_SHARE_WRITE; // 读写共享
LPSECURITY_ATTRIBUTES lpSecurityAttributes = NULL; // 使用默认的安全属性,句柄不可继承
DWORD dwCreationDisposition = OPEN_ALWAYS; // 文件打开方式
DWORD dwFlagsAndAttributes = FILE_ATTRIBUTE_NORMAL; // 不设置属性
HANDLE handle = CreateFile("sync_io.txt", dwDesiredAccess, dwShareMode, lpSecurityAttributes, dwCreationDisposition, dwFlagsAndAttributes, NULL);
if (INVALID_HANDLE_VALUE == handle)
{
printf("CreateFile Failed %ld\n", handle);
return -1;
}
char *buf = "SYNC IO TEST";
DWORD dWritten;
BOOL bRet = WriteFile(handle, buf, strlen(buf), &dWritten, NULL);
if (bRet)
{
printf("WriteFile Success\n");
}
else
{
printf("WriteFile Failed\n");
}
char buffer[100] = {0};
do
{
printf("Input Data(c quit): \n");
scanf("%s", buffer);
bRet = WriteFile(handle, buffer, strlen(buffer), &dWritten, NULL);
if (bRet)
{
printf("WriteFile Success\n");
}
else
{
printf("WriteFile Failed\n");
}
} while ('c' != buffer[0]);
// 尝试直接退出;程序异常直接退出,可能导致数据未完全写入到文件
// char *cc = NULL;
// memset(cc, 0, 100);
FlushFileBuffers(handle);
// system("pause");
CloseHandle(handle);
return 0;
}
4. 异步I/O
与计算机执行得大多数操作相比,设备I/O是最慢的之一。使用Win32的多线程结构,你可以进行异步设备I/O,即你告诉系统对设备读写数据,而同时你的应用程序的其余代码可以继续继续执行。
为了异步访问一个设备,你必须在调用CreateFile打开设备时,在dwFlagsAndAttrs参数中指定FILE_FLAG_OVERLAPPED标志。这一标志告诉系统你想要的异步地访问该设备。
Win32有4种技术用于进行异步I/O。这些技术有共同的理论基础。当进行异步I/O时,必须先向操作系统发出一个I/O请求;操作系统队列化各种I/O请求,在内部处理他们。当系统在处理I/O请求时,你的线程可以返回继续执行。
在某一时刻,操作系统处理完了你的I/O请求,需要通知你数据被发送了,数据被接收了,或发生了一个错误。你发出I/O请求的方式对4种技术来说几乎是相同的。这些技术的区别在于你被通知结果的方式。
下表简单地总结了这4种不同的技术。这些技术是按复杂程度排序的,从最易于理解和实现的(设备处理信号)到最难于理解和实现的(I/O完成端口)。
技术 | 总结 |
---|---|
使一个设备内核对象变为有信号 | 如果你要对单个设备进行多个同时的I/O操作,这种技术没有用。它允许一个线程发出I/O请求,另一个线程进行处理。 |
使一个事件内核对象变为有信号 | 允许对的那个设备进行多个同时的I/O请求。允许一个线程发出I/O请求,另一个线程进行处理。 |
告警I/O | 允许对单个设备进行多个同时的I/O请求。发出I/O请求的线程也必须处理它。 |
I/O完成端口 | 允许对单个设备进行多个同时的I/O请求。允许一个线程发出I/O请求,另一个线程进行处理。该该技术的伸缩性和灵活性都最好。 |
5. 使设备(文件)内核对象有信号
最简单的进行异步设备I/O的技术是使用设备句柄信号。要发出I/O请求,你还是使用ReadFile或WriteFile函数。
不能同时执行多个异步操作。同时从文件开始读取数据,从文件尾写入数据这种情况,不能通过等待文件句柄变为有信号来同步线程。因为信号会混乱。CreateFile和ReadFile会将设备句柄设为无信号,异步执行完后将设备句柄设为有信号。
通过WaitForSingleObjects和WaitForMultipleObjects等待句柄有信号。
先说一下GetOverlappedResult函数:
BOOL GetOverlappedResult(
HANDLE hFile, // 文件句柄,用于
LPOVERLAPPED lpOverlapped, // 重叠操作启动时指向地OVERLAPPED结构指针;若hEvent=NULL,使用hFile状态指示操作何时完成(设备内核对象有信号);hEvent!=NULL,将使用hEvent(事件内核对象有信号),【推荐】
LPDWORD lpNumberOfBytesTransferred, // 实际读取或写入地字节数
BOOL bWait // 是否等待,TRUE将一直阻塞,直到异步执行完成
);
/*使设备句柄有信号*/
#include <Windows.h>
#include <stdio.h>
int main()
{
DWORD dwRet;
// 创建/打开文件
DWORD dwDesiredAccess = GENERIC_WRITE | GENERIC_READ; // 可读可写
DWORD dwShareMode = FILE_SHARE_READ | FILE_SHARE_WRITE; // 读写共享
LPSECURITY_ATTRIBUTES lpSecurityAttributes = NULL; // 使用默认的安全属性,句柄不可继承
DWORD dwCreationDisposition = OPEN_ALWAYS; // 文件打开方式
DWORD dwFlagsAndAttributes = FILE_FLAG_OVERLAPPED; // 异步访问设备
HANDLE handle = CreateFile("async_dev_handle_io.txt", dwDesiredAccess, dwShareMode, lpSecurityAttributes, dwCreationDisposition, dwFlagsAndAttributes, NULL);
if (INVALID_HANDLE_VALUE == handle)
{
printf("CreateFile Failed %ld\n", handle);
return -1;
}
char *buf = "ASYNC IO WITH DEV HANDLE TEST";
DWORD dWritten;
// 设置读写起始位置
OVERLAPPED overlapped;
overlapped.Offset = 0; // 文件操作的offset
overlapped.OffsetHigh = 0;
overlapped.hEvent = NULL;
BOOL bRet = WriteFile(handle, buf, strlen(buf), &dWritten, &overlapped);
if (bRet)
{
printf("WriteFile Success\n");
}
else
{
dwRet = GetLastError();
printf("WriteFile Failed %ld\n", dwRet);
}
// 等待句柄有信号,用于同步,有信号说明异步执行完成
// dwRet = WaitForSingleObject(handle, INFINITE); //
// 获取异步执行结果,
bRet = GetOverlappedResult(handle, &overlapped, &dWritten, TRUE);
if (bRet)
{
printf("GetOverlappedResult Success, writen %ld bytes\n", dWritten);
}
// 更新成员后才能复用,复用必须在异步执行完后
overlapped.Internal = 0;
overlapped.InternalHigh = 0;
overlapped.Offset = 0;
overlapped.OffsetHigh = 0;
overlapped.hEvent = NULL;
char buffer[100] = {0};
DWORD dwRead;
bRet = ReadFile(handle, buffer, 10, &dwRead, &overlapped);
// 获取异步执行结果,
bRet = GetOverlappedResult(handle, &overlapped, &dwRead, TRUE);
if (bRet)
{
printf("GetOverlappedResult 2 Success, read %ld bytes\n", dwRead);
}
printf("%s\n", buffer);
return 0;
}
6. 使事件内核对象有信号
OVERLAPPED结构的hEvent标识了一个事件内核对象,需调用CreateEvent来创建它。当系统完成了一个异步I/O请求后,它检查OVERLAPPED结构的hEvent成员是否为NULL。如果hEvent不为NULL,系统就调用SetEvent使它变为有信号。系统也像以前一样把文件句柄也设为有信号。不过,如果你使用事件来判断一个文件操作何时结束,就不应该等待文件句柄对象变为有信号,而应等待事件对象。
同时执行多个异步设备I/O请求时,应为每个请求创建一个事件,并初始化每个请求的OVERLAPPED结构的hEvent成员,再调用ReadFile或WriteFile。当到达了需要与I/O请求的完成同步那一点时,只需要调用WaitForMultipleObjects,传递同每个I/O请求的OVERLAPPED结构相关联的事件句柄。这样,你就可以使用同一设备句柄,容易而可靠地进行多个同时地异步设备I/O操作。
GetOverlappedResult地fWait参数中传递TRUE,函数在内部调用WaitForSingleObjects,传递给它OVERLAPPED结构地hEvent成员。
WaitForSingleObjects返回时,事件被自动重设为无信号状态。
事件内核对象有信号与设备内核对象有信号的区别就是OVERLAPPED的hEvent元素,其他都是完全一样的。
HANDLE
CreateEvent(
_In_opt_ LPSECURITY_ATTRIBUTES lpEventAttributes, // 安全属性,与CreateFile一致
_In_ BOOL bManualReset, // 是否手动重设
_In_ BOOL bInitialState, // 初始化状态,TRUE:初始化为有信号;FALSE:初始化为无信号
_In_opt_ LPCSTR lpName // 事件名
);
若OVERLAPPED的hEvent元素不为NULL,且为自动重设信号时,WaitForSingleObject和GetOverlappedResult等待返回后都将重设hEvent为无信号状态。
/*使事件句柄有信号*/
#include <Windows.h>
#include <stdio.h>
int main()
{
DWORD dwRet;
// 创建/打开文件
DWORD dwDesiredAccess = GENERIC_WRITE | GENERIC_READ; // 可读可写
DWORD dwShareMode = FILE_SHARE_READ | FILE_SHARE_WRITE; // 读写共享
LPSECURITY_ATTRIBUTES lpSecurityAttributes = NULL; // 使用默认的安全属性,句柄不可继承
DWORD dwCreationDisposition = OPEN_ALWAYS; // 文件打开方式
DWORD dwFlagsAndAttributes = FILE_FLAG_OVERLAPPED; // 异步访问设备
HANDLE handle = CreateFile("async_dev_handle_io.txt", dwDesiredAccess, dwShareMode, lpSecurityAttributes, dwCreationDisposition, dwFlagsAndAttributes, NULL);
if (INVALID_HANDLE_VALUE == handle)
{
printf("CreateFile Failed %ld\n", handle);
return -1;
}
char *buf = "ASYNC IO WITH DEV HANDLE TEST";
DWORD dWritten;
// 设置读写起始位置
OVERLAPPED overlapped;
overlapped.Offset = 0; // 文件操作的offset
overlapped.OffsetHigh = 0;
overlapped.hEvent = CreateEvent(NULL, FALSE, FALSE, NULL); // 创建自动重设、初始无信号的事件句柄;和设备句柄信号只有这里不同
BOOL bRet = WriteFile(handle, buf, strlen(buf), &dWritten, &overlapped);
if (bRet)
{
printf("WriteFile Success\n");
}
else
{
dwRet = GetLastError();
printf("WriteFile Failed %ld\n", dwRet);
}
// 等待句柄有信号,用于同步,有信号说明异步执行完成
dwRet = WaitForSingleObject(handle, INFINITE); //
// 获取异步执行结果,
bRet = GetOverlappedResult(handle, &overlapped, &dWritten, TRUE); // 将永远阻塞,hEvent信号已经被WaitForSingleObject重置
if (bRet)
{
printf("GetOverlappedResult Success, writen %ld bytes\n", dWritten);
}
return 0;
}
7. 告警I/O
当线程创建时,系统还创建了一个队列,并把它同线程关联起来。该队列被称为异步过程调用(APC,Asynchronous Procedure Call)队列。APC队列的工作方式类似于消息队列,不过APC队列是用内核的低层实现的,而消息队列则实现在很高的层次上。因为APC队列是由内核实现的,它更有效,涉及它的操作执行得也更快。
**如何让请求得结果被队列化到线程得APC队列中?**调用ReadFileEx、WriteFileEx:
BOOL
ReadFileEx(
_In_ HANDLE hFile,
_Out_writes_bytes_opt_(nNumberOfBytesToRead) __out_data_source(FILE) LPVOID lpBuffer,
_In_ DWORD nNumberOfBytesToRead,
_Inout_ LPOVERLAPPED lpOverlapped,
_In_ LPOVERLAPPED_COMPLETION_ROUTINE lpCompletionRoutine
);
BOOL
WriteFileEx(
_In_ HANDLE hFile,
_In_reads_bytes_opt_(nNumberOfBytesToWrite) LPCVOID lpBuffer,
_In_ DWORD nNumberOfBytesToWrite,
_Inout_ LPOVERLAPPED lpOverlapped,
_In_ LPOVERLAPPED_COMPLETION_ROUTINE lpCompletionRoutine
);
当I/O请求执行完成时,它们只是被简单的加入队列,回调函数不会立即被调用。要处理线程APC队列中的项,线程必须把自己放在一个警觉状态,这意味者线程现在可以被打断了。有5个Win32函数可以使线程处于警觉状态:
DWORD
SleepEx(
_In_ DWORD dwMilliseconds,
_In_ BOOL bAlertable
);
DWORD
WaitForSingleObjectEx(
_In_ HANDLE hHandle,
_In_ DWORD dwMilliseconds,
_In_ BOOL bAlertable
);
DWORD
WaitForMultipleObjectsEx(
_In_ DWORD nCount,
_In_reads_(nCount) CONST HANDLE* lpHandles,
_In_ BOOL bWaitAll,
_In_ DWORD dwMilliseconds,
_In_ BOOL bAlertable
);
DWORD
SignalObjectAndWait(
_In_ HANDLE hObjectToSignal,
_In_ HANDLE hObjectToWaitOn,
_In_ DWORD dwMilliseconds,
_In_ BOOL bAlertable
);
DWORD
MsgWaitForMultipleObjectsEx(
_In_ DWORD nCount,
_In_reads_opt_(nCount) CONST HANDLE *pHandles,
_In_ DWORD dwMilliseconds,
_In_ DWORD dwWakeMask,
_In_ DWORD dwFlags);
前4个函数的最后一个参数是一个BOOL值,指出调用的线程是否要把自己放在警觉状态。对于MsgWaitForMultipleObjectsEx必须使用MWMD_ALERTABLE标志来让线程进入警觉状态。
告警I/O运行流程:
/*告警I/O*/
#include <Windows.h>
void ShowOverlapped(LPOVERLAPPED lpOverlapped)
{
printf("Internal %ld, InternalHigh %ld, Offset %ld, OffsetHigh %ld\n",
lpOverlapped->Internal,
lpOverlapped->InternalHigh,
lpOverlapped->Offset,
lpOverlapped->OffsetHigh);
}
// 异步执行结果回调函数
VOID WINAPI Alert(
_In_ DWORD dwErrorCode,
_In_ DWORD dwNumberOfBytesTransfered,
_Inout_ LPOVERLAPPED lpOverlapped)
{
printf("alert ErrorCode %ld, Transfered %ld \n", dwErrorCode, dwNumberOfBytesTransfered);
ShowOverlapped(lpOverlapped);
}
int main()
{
DWORD dwRet;
// 创建/打开文件
DWORD dwDesiredAccess = GENERIC_WRITE | GENERIC_READ; // 可读可写
DWORD dwShareMode = FILE_SHARE_READ | FILE_SHARE_WRITE; // 读写共享
LPSECURITY_ATTRIBUTES lpSecurityAttributes = NULL; // 使用默认的安全属性,句柄不可继承
DWORD dwCreationDisposition = OPEN_ALWAYS; // 文件打开方式
DWORD dwFlagsAndAttributes = FILE_FLAG_OVERLAPPED; // 异步访问设备
HANDLE handle = CreateFile("async_dev_handle_io.txt", dwDesiredAccess, dwShareMode, lpSecurityAttributes, dwCreationDisposition, dwFlagsAndAttributes, NULL);
if (INVALID_HANDLE_VALUE == handle)
{
printf("CreateFile Failed %ld\n", handle);
return -1;
}
char *buf = "ASYNC IO WITH DEV HANDLE TEST";
DWORD dWritten;
// 设置读写起始位置
OVERLAPPED overlapped;
overlapped.Offset = 0; // 文件操作的offset
overlapped.OffsetHigh = 0;
overlapped.hEvent = NULL; // 事件句柄
WriteFileEx(handle, buf, strlen(buf), &overlapped, Alert);
// 设置读写起始位置
OVERLAPPED overlapped_read;
overlapped_read.Offset = 0; // 文件操作的offset
overlapped_read.OffsetHigh = 0;
overlapped_read.hEvent = NULL; // 事件句柄
char read_buf[20];
ReadFileEx(handle, read_buf, 20, &overlapped_read, Alert);
// 等待告警I/O
SleepEx(10000, TRUE);
printf("%s\n", read_buf);
// MsgWaitForMultipleObjectsEx
system("pause");
}
手动添加APC
除了由内核将异步I/O的结果写入APC,我们也可以自己手动将回调写入APC,而且可以跨线程和进程写入APC。
// 手动写入APC队列至指定线程
DWORD
QueueUserAPC(
_In_ PAPCFUNC pfnAPC, // 回调函数
_In_ HANDLE hThread, // APC所属的目标线程
_In_ ULONG_PTR dwData // 回调函数的参数
);
测试代码:
/*手动写入APC*/
#include <Windows.h>
VOID WINAPI APCFunc(DWORD dwParam)
{
printf("APCFunc dwParam = %ld\n", dwParam);
}
int main()
{
HANDLE thnd = GetCurrentThread(); // 获取当前线程句柄
QueueUserAPC(APCFunc, thnd, 100); // 手动添加APC到当前线程
SleepEx(10000, TRUE);
return 0;
}
告警I/O请求的缺点是发出I/O请求的线程也必须是处理结果的线程。在一些服务器的应用程序中,这不是很好的结构。如果I/O请求完成,能让另一个线程来处理数据会很好。I/O完成端口就能解决这个问题。
8. I/O完成端口
一个服务应用程序的结构可以由两种方式:
- 在串行模式下,单个线程等待一个客户发出请求(通常是通过网络)。当来了请求后,线程醒来处理客户的请求。
- 在并行模式下,单个线程等待客户发出请求,而后创建新线程来处理请求。当线程处理客户请求时,起初的线程循环回去等待另一个客户请求。处理客户请求的线程处理完毕后终结。
串行模式的问题在于它不能处理好多个同时的请求。如果两个客户同时做出请求,一次只能处理一个请求,第二个请求必须等待第一个请求处理完。涉及成使用串行模型的服务不能利用SMP机器。
由于串行模式的局限性,并发模型就非常普通了。在并发模型中,对每个客户请求抖创建一个模型。其优点在于等待请求的线程只需做很少的工作。大多数时间中,该线程都在休眠。当来了一个客户请求后,线程唤醒,床一个新线程来处理请求,而后等待下一个客户请求,服务程序的伸缩性很好,能容易地利用SMP计算机。所以,当你使用并发模型时,通过升级硬件(添加另一个CPU),服务应用程序地性能可以提高。
当线程数较多时,Windows NT内核需要花费大量时间来转换运行线程地上下文,线程就没有得到很多CPU时间来工作。
I/O完成端口地理论基础是并行运行地线程数据必须由一个上限,即由500个同时地客户请求,并不意味着有500个运行地线程。但并发运行的合适的线程是多少呢?如果一台机器有两个CPU,那么有多于两个的可运行线程就没有意义了。一旦可运行的线程数据超过CPU的数目,系统就不得不花费时间来进行线程上下文的切换,这将会浪费宝贵的CPU周期。
并行模型的另一个低效之处是为每个客户请求创建了一个新线程。创建线程比起创建进程来开销要小,但也远不是没有开销。如果当应用程序初始化时创建了一个线程池,而这些线程在应用程序执行期间是空闲的,应用程序的性能就能进一步提高。I/O完成端口就是用线程池。
I/O完成端口可能是Win32提供的最复杂的内核对象。要创建I/O完成端口,应调用CreateIoCompletionPort。
创建完成端口和将设备关联至完成端口(前三个参数在关联文件句柄时有用,最后一个参数用于创建完成端口时有用;也可以在创建I/O完成端口时向它关联一个设备):
HANDLE // 返回I/O完成端口句柄
CreateIoCompletionPort(
_In_ HANDLE FileHandle, // 需要关联到完成端口的文件句柄(用于关联文件句柄时不为NULL,用于创建完成端口时为NULL)
_In_opt_ HANDLE ExistingCompletionPort, // 关联的完成端口句柄(用于关联文件句柄时不为NULL,用于创建完成端口时为NULL)
_In_ ULONG_PTR CompletionKey, // 关联的文件句柄的完成键(用于创建完成端口时为0)
_In_ DWORD NumberOfConcurrentThreads // 并发线程数(用于关联文件句柄时为0,创建完成端口时不为0)
);
当创建一个I/O完成端口时,内核实际创建了5个不同的数据结构:
- 设备列表
- I/O完成队列(FIFO)
- 等待的线程队列(LIFO)
- 释放线程队列
- 暂停线程队列
当一个设备的异步I/O请求完成时,系统检查该设备是否关联了一个完成端口。如果是,系统就向该完成端口的I/O完成队列中加入完成的I/O请求项。
可以向一个设备发出一个I/O请求而不向I/O完成端口的队列中加入一个I/O完成项。例如有时你向一个套接字传输数据,但并不关心数据是否真的被传送了。给异步I/O操作的参数 Overlapped.hEvent |=1 赋值可实现。关闭该事件前不要忘记重设它的低位字节:CloseHandle(Overlapped.hEvent & ~1)
当应用程序初始化时,它应该创建I/O完成端口,而后应该创建一个线程池来处理客户请求。现在的问题在于池中应该有多少个线程。一个标准的答案是计算机上的CPU数目乘以2。池中的所有这些线程应该执行同一个线程函数。一般来说,该线程函数执行一些初始化后进入一个循环,该循环在服务进程终止时才结束。在循环中,线程使自己睡眠来等待完成端口的设备I/O请求的完成。这是通过调用GetQueuedCompletionStatus来实现的:
BOOL
GetQueuedCompletionStatus(
_In_ HANDLE CompletionPort, // 要监视的完成端口
_Out_ LPDWORD lpNumberOfBytesTransferred, // 传送完成的字节数(返回参数)
_Out_ PULONG_PTR lpCompletionKey, // 关联的设备的完成键
_Out_ LPOVERLAPPED* lpOverlapped, // 返回异步执行结果OVERLAPPED
_In_ DWORD dwMilliseconds // 超时时间
);
GetQueuedCompletionStatus将使当前线程进入等待的线程队列,线程是按后进先出(LIFO)方式被唤醒的。GetQueuedCompletionStatus使调用线程进入睡眠,直到指定的完成端口的I/O完成队列中出现了一项或直到超时(由参数dwMilliseconds设定)。
名称 | 数据结构 | 表项增加 | 表现删除 | 备注 |
---|---|---|---|---|
设备列表 | {设备句柄(hDev),完成键(dwCompletionKey)}集合 | 调用CreateIoCompletionPort添加关联设备 | 关闭设备句柄CloseHandle | |
I/O完成队列 | FIFO { dwBytesTranssfered,dwCompletionKey,pOverlapped,dwError}集合 | 内核完成异步I/O后,将结果写入 | 调用GetQueuedCompletionStatus获取数据 | |
I/O等待线程队列 | 调用GetQueuedCompletionStatus将当前线程添加至I/O等待线程队列 | GetQueuedCompletionStatus返回时 | ||
I/O释放线程队列 | GetQueuedCompletionStatus返回时或挂起的线程调用函数醒来,当前队列添加至I/O释放线程队列 | 线程调用函数挂起,或再次调用GetQueuedCompletionStatus | ||
I/O暂停线程队列 | 线程调用函数挂起 | 挂起的线程调用函数醒来 |
8.1 使用I/O完成端口异步读写文件
/*I/O完成端口测试 文件读写*/
#include <Windows.h>
#include <stdio.h>
// 封装OVERLAPPED用于数据传输
typedef struct _FILE_IO_OVERLAPPED
{
OVERLAPPED overlapped;
DWORD *transferred;
char *buffer;
} FILE_IO_OVERLAPPED, *PFILE_IO_OVERLAPPED;
/*
创建IO完成端口
dwNumberOfConcurrentThreads 同时能运行的最多线程数,0 缺省运行的并发数是CPU数
返回 I/O完成端口句柄
*/
HANDLE CreateNewCompletionPort(DWORD dwNumberOfConcurrentThreads)
{
// 无LPSECURITY_ATTRIBUTES参数,因为完成端口只应用于一个进程内
return CreateIoCompletionPort(INVALID_HANDLE_VALUE, NULL, 0, dwNumberOfConcurrentThreads);
}
/*
关联设备,向已有的完成端口的设备列表中加入一项。
hCompPort : 关联的完成端口句柄
hDevice : 需要关联的设备句柄
dwCompKey : 完成键
*/
BOOL AssociateDeviceWithCompletionPort(HANDLE hCompPort, HANDLE hDevice, DWORD dwCompKey)
{
HANDLE h = CreateIoCompletionPort(hDevice, hCompPort, dwCompKey, 0);
return (h == hCompPort);
}
// 工作线程运行函数
DWORD WINAPI ServerThread(LPVOID lParam)
{
HANDLE IocpHandle = (HANDLE)lParam;
DWORD dwTrans;
PFILE_IO_OVERLAPPED overlapped;
ULONGLONG cptKey;
while (TRUE)
{
// printf("Start GetQueuedCompletionStatus\r\n");
// 扫描完成端口的队列是否有请求存在,存在则返回
BOOL bOk = GetQueuedCompletionStatus(IocpHandle, &dwTrans, (PULONG_PTR)&cptKey, (LPOVERLAPPED *)&overlapped, INFINITE);
if (!bOk)
{
printf("err %ld\r\n", GetLastError());
continue;
}
if (dwTrans == 0)
{
printf("dwTrans = 0\r\n");
continue;
}
DWORD tid = GetCurrentThreadId();
printf("Current Thread Id : %ld, key = %ld , write data length %ld , %s\n", tid, cptKey, dwTrans, overlapped->buffer);
}
return 0;
}
int main()
{
// 创建/打开文件
DWORD dwDesiredAccess = GENERIC_WRITE | GENERIC_READ; // 可读可写
DWORD dwShareMode = FILE_SHARE_READ | FILE_SHARE_WRITE; // 读写共享
LPSECURITY_ATTRIBUTES lpSecurityAttributes = NULL; // 使用默认的安全属性,句柄不可继承
DWORD dwCreationDisposition = OPEN_ALWAYS; // 文件打开方式
DWORD dwFlagsAndAttributes = FILE_FLAG_OVERLAPPED; // 异步访问设备
HANDLE hWriter = CreateFile("iocp_file.txt", dwDesiredAccess, dwShareMode, lpSecurityAttributes, dwCreationDisposition, dwFlagsAndAttributes, NULL);
if (INVALID_HANDLE_VALUE == hWriter)
{
printf("CreateFile Failed %ld\n", hWriter);
return -1;
}
HANDLE hCompPort = CreateNewCompletionPort(4);
AssociateDeviceWithCompletionPort(hCompPort, hWriter, (DWORD)hWriter);
// 创建4个处理线程
CreateThread(NULL, 0, ServerThread, hCompPort, 0, 0);
char *buf = "SYNC IO TEST";
DWORD dWritten;
// PFILE_IO_OVERLAPPED overlapped = new FILE_IO_OVERLAPPED;
// memset(&overlapped->overlapped, 0, sizeof(overlapped->overlapped));
// overlapped->transferred = &dWritten;
// BOOL bRet = WriteFile(hWriter, buf, strlen(buf), &dWritten, (LPOVERLAPPED)overlapped);
// if (bRet)
// {
// printf("WriteFile Success\n");
// }
// else
// {
// printf("WriteFile Failed\n");
// }
char buffer[100] = {0};
DWORD dwRead;
PFILE_IO_OVERLAPPED overlapped = new FILE_IO_OVERLAPPED;
memset(&overlapped->overlapped, 0, sizeof(overlapped->overlapped));
overlapped->transferred = &dwRead;
overlapped->buffer = buffer;
BOOL bRet = ReadFile(hWriter, buffer, strlen(buf), &dwRead, (LPOVERLAPPED)overlapped);
if (bRet)
{
printf("ReadFile Success, read %ld bytes\n", dwRead);
}
else
{
printf("WriteFile Failed %ld , Error %ld\n", dwRead, GetLastError());
}
system("pause");
}
8.2 模拟I/O完成端口请求
手动向指定的I/O完成队列中加入一个完成I/O请求:
BOOL
PostQueuedCompletionStatus(
_In_ HANDLE CompletionPort, // 关联的完成端口
_In_ DWORD dwNumberOfBytesTransferred, // 传送完成的数据个数
_In_ ULONG_PTR dwCompletionKey, // 关联设备的完成键
_In_opt_ LPOVERLAPPED lpOverlapped // 异步的OVERLAPPED结构
);
/*I/O完成端口测试*/
#include <Windows.h>
#include <stdio.h>
// #include <iostream>
// 封装OVERLAPPED用于数据传输
typedef struct _PER_IO_DATA
{
OVERLAPPED overlapped;
char buffer[100];
} PER_IO_DATA, *PPER_IO_DATA;
// 工作线程运行函数
DWORD WINAPI ServerThread(LPVOID lParam)
{
HANDLE IocpHandle = (HANDLE)lParam;
DWORD dwTrans;
PPER_IO_DATA overlapped;
ULONGLONG cptKey;
while (TRUE)
{
// printf("Start GetQueuedCompletionStatus\r\n");
// 扫描完成端口的队列是否有请求存在,存在则返回
BOOL bOk = GetQueuedCompletionStatus(IocpHandle, &dwTrans, (PULONG_PTR)&cptKey, (LPOVERLAPPED *)&overlapped, INFINITE);
if (!bOk)
{
printf("err %ld\r\n", GetLastError());
continue;
}
if (dwTrans == 0)
{
printf("dwTrans = 0\r\n");
continue;
}
DWORD tid = GetCurrentThreadId();
printf("Current Thread Id : %ld, key = %ld , read data length %ld , %s\n", tid, cptKey, dwTrans, overlapped->buffer);
}
return 0;
}
int main()
{
// 创建完成端口
HANDLE IocpHandle = CreateIoCompletionPort(INVALID_HANDLE_VALUE, NULL, 0, 0);
// 创建4个处理线程
CreateThread(NULL, 0, ServerThread, IocpHandle, 0, 0);
CreateThread(NULL, 0, ServerThread, IocpHandle, 0, 0);
CreateThread(NULL, 0, ServerThread, IocpHandle, 0, 0);
CreateThread(NULL, 0, ServerThread, IocpHandle, 0, 0);
DWORD transffered = 11;
// Sleep(1000 * 2);
ULONGLONG CptKey = 120;
// 手动发送数据至IOCP
for (int i = 0; i < 100; i++)
{
PPER_IO_DATA overlapped = new PER_IO_DATA;
memset(&overlapped->overlapped, 0, sizeof(overlapped->overlapped));
sprintf(overlapped->buffer, "hello i'm %d", i);
transffered = strlen(overlapped->buffer);
// 写入一条数据,模拟I/O完成端口请求
BOOL bRet = PostQueuedCompletionStatus(IocpHandle, transffered, CptKey, &overlapped->overlapped);
if (!bRet)
{
printf("write failed %ld\r\n", GetLastError());
break;
}
else
{
printf("write success\r\n");
}
}
system("pause");
return 0;
}
9. 判断I/O请求是否完成
Win32 SDK中定义了一个HasOverlappedIoCompleted的宏:
#define HasOverlappedIoCompleted(lpOverlapped) (((DWORD)(lpOverlapped)->Internal) != STATUS_PENDING)
该宏允许应用程序快速地查看一个指定的I/O请求是否完成。如果请求还未完成,则返回FALSE;否则返回TRUE。
10. 取消未完成的I/O请求
CancelIO(HANDLE hFile)可以取消当前线程对某个设备发出的所有未完成操作:
BOOL
CancelIo(
_In_ HANDLE hFile
);
该函数只能取消调用线程所发出的请求,不能取消其他线程发出的请求。如果函数失败,则返回FALSE。
如果线程是使用ReadFileEx或WriteFileEx发出I/O请求的,CancelIo使得线程的APC队列中被加入一项。当线程处于警觉状态时,回调函数被使用错误码ERROR_OPERATION_ABORTED调用。
如果该设备完成的I/O请求被加入到一个I/O完成端口的队列中,队列中就会增加一项,GetQueuedCompletionStatus将返回;对GetLastError的调用返回ERROR_OPERATION_ABORTED。
对一个有未完成的I/O请求的设备调用CloseHandle会取消I/O请求。