Windows API笔记(十一)设备I/O

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路径名)
目录1CreateFile(lpFileName是目录名或UNC目录名,使用FILE_FLAG_BACKUP_SEMANTICS)
逻辑盘2CreateFile(lpFileName="\.\x:");
物理盘3CreateFile(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
  1. 如果在调用CreateFile时使用FILE_FLAG_BACKUP_SEMANTICS标志,Windows允许你打开一个目录,这使得你可以改变目录的属性(普通/隐藏等)和它的时间戳;
  2. 如果以“\.\x:”(x是一个驱动器符)的形式指定一个字符串,Windows允许你打开一个逻辑盘。例如,要打开D盘,你可以指定“\.\D:”,打开一个盘使得你可以格式化或确定它的介质类型;
  3. 如果你以“\.\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个不同的数据结构:

  1. 设备列表
  2. I/O完成队列(FIFO)
  3. 等待的线程队列(LIFO)
  4. 释放线程队列
  5. 暂停线程队列

当一个设备的异步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请求。

  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值