使用同步或异步的方式完成 I/O 访问和操作(Windows核心编程)
0x01 Windows 中对文件的底层操作
- Windows 为了方便开发人员操作 I/O 设备(这些设备包括套接字、管道、文件、串口、目录等),对这些设备的差异进行了隐藏,所以开发人员在使用这些设备时不必关心使用的哪一种设备,只需要调用 CreateFile 这一个函数打开设备的操作即可
- CreateFile 这个函数功能强大,不仅可以打开 I/O 设备、限制文件的访问、创建临时文件、定制缓存、甚至可以对 I/O 进行异步操作

0x02 同步方式访问和操作 I/O 设备
- 那么打开文件之后怎么操作文件中的数据呢,可以使用 ReadFile 和 WriteFile 两个函数,下面是读取文件的例子
#include <Windows.h>
#include <iostream>
#include <stdio.h>
#include <strsafe.h>
using namespace std;
DWORD WINAPI Create_File(WCHAR *FileName);
VOID WINAPI ErrorCodeTransformation(DWORD ErrorCode);
int main(int argc, char **argv)
{
WCHAR FileName[12] = TEXT("D:\\post.txt");
DWORD res = Create_File(FileName);
// 打印错误函数
if (res != TRUE) ErrorCodeTransformation(res);
return 0;
}
DWORD WINAPI Create_File(WCHAR *FileName)
{
// 以只读方式打开 D 盘中的文件,并且独占该文件的访问
DWORD LastError; HANDLE file;
file = CreateFile(FileName, GENERIC_READ, 0, NULL, OPEN_EXISTING, NULL, NULL);
// 如果错误返回错误代码
LastError = GetLastError();
if (file == INVALID_HANDLE_VALUE) return LastError;
// 查询文件大小
LARGE_INTEGER FileSize = { 0 };
GetFileSizeEx(file, &FileSize);
cout << "[*] 打开文件成功" << endl;
cout << "[*] 文件大小为: " << FileSize.LowPart << " 字节" << endl;
// 判断文件大小是否小于 4096 个字节,太大就不打印了
if (FileSize.LowPart < 4096)
{
// 读取文件当中的内容
DWORD dwNumBytes;
BYTE *Buffer = (BYTE *)malloc(FileSize.LowPart);
ReadFile(file, Buffer, FileSize.LowPart, &dwNumBytes, NULL);
cout << "[*] 文件内容: " << endl << Buffer << endl;
}
// 关闭文件句柄
CloseHandle(file);
return TRUE;
}
- 这一种方式就是同步 I/O 访问,也是菜鸟经常使用的一种方式。首先使用 CreateFile 打开 D 盘的文件,并且规定第二个参数为 GENERIC_READ 第五个参数为 OPEN_EXISTING,意思是以只读和独占方式打开文件,然后使用 GetFileSizeEx 获取文件的大小为之后读取文件做铺垫,最后如果文件小于 4096 就使用 ReadFile 函数打开文件,ReadFile 函数第二个参数是读取的数据存放的缓冲区,第三个参数表示读取的数据的大小
0x03 异步方式访问和操作 I/O 设备
- 以同步方式访问和操作 I/O 接口的好处是非常的方便,因为只需要等待 I/O 操作完成就可以了,但是如果 I/O 操作变多缺点也随之而来,每次进行 I/O 操作时线程都会等待操作完成,更何况与计算机的大多数操作相比,I/O 操作是最慢的,会浪费掉大量的时间,不利于程序的伸缩性。这样的话异步 I/O 的优势得以显现,异步 I/O 并不会让线程等待,线程可以干其他的事情,等到异步 I/O 操作完成时应用程序就会接收到一个通知,通知 I/O 读写操作已经完成了,这样就可以处理剩下的工作

- 通过 CreateFile 函数就可以很轻易的以异步方式打开 I/O 设备,但是在异步 I/O 完成时通过什么样的手段才能接收到 I/O 完成的通知呢,目前有 4 中方法 (1) 触发设备内核对象 (2) 使用可提醒的 I/O (3)触发事件内核对象 (4) 使用 I/O 完成端口,其中使用 I/O 完成端口获取通知的方式是最好的,同时也是最复杂的
4 种获取异步 I/O 完成通知的方式,难度随序号顺序逐级增加
0x03 -> (1) 以触发设备内核对象的方式获取异步 I/O 完成通知
- 以触发设备内核对象来获取异步 I/O 通知(将上面同步方式的代码稍作修改即可):
DWORD WINAPI Create_File(WCHAR *FileName)
{
// 以只读方式打开 D 盘中的文件,并且独占该文件的访问,倒数第二个参数表示以异步方式进行 I/O 操作
DWORD LastError; HANDLE file;
file = CreateFile(FileName, GENERIC_READ, 0, NULL, OPEN_EXISTING, FILE_FLAG_OVERLAPPED, NULL);
// 如果错误返回错误代码
LastError = GetLastError();
if (file == INVALID_HANDLE_VALUE) return LastError;
// 查询文件大小
LARGE_INTEGER FileSize = { 0 };
GetFileSizeEx(file, &FileSize);
cout << "[*] 打开文件成功" << endl;
cout << "[*] 文件大小为: " << FileSize.LowPart << " 字节" << endl;
// 判断文件大小是否小于 4096 个字节,太大就不打印了
if (FileSize.LowPart < 4096)
{
// 读取文件当中的内容
BOOL res; DWORD dwNumBytes; OVERLAPPED overlapped = { 0 };
// 设置文件偏移,0 表示从文件开头读取数据,10 表示从文件第 10 个字节读取文件内容
overlapped.Offset = 0;
BYTE *Buffer = (BYTE *)malloc(FileSize.LowPart);
// 以异步方式读取文件中的内容
res = ReadFile(file, Buffer, FileSize.LowPart, &dwNumBytes, &overlapped);
LastError = GetLastError();
// 之后就可以干别的事了
// 满足条件说明异步 I/O 操作成功
if (res == FALSE && (LastError == ERROR_IO_PENDING))
{
// 等待异步 I/O 完成通知
WaitForSingleObject(file, INFINITE);
cout << "[*] 文件内容: " << endl << Buffer << endl;
}
else
{
// 异步操作失败则返回错误代码
return LastError;
}
}
// 关闭文件句柄
CloseHandle(file);
return TRUE;
}
- 从以上代码可以看出,CreateFile 函数如果想以异步方式打开文件,必须向倒数第二个参数传递 FILE_FLAG_OVERLAPPED 标志,之后使用 WaitForSingleObject 函数等待通知即可,当然在这之前可以干别的事
0x03 -> (2) 以触发事件内核对象方式获取异步 I/O 完成通知
- 什么是事件内核对象,事件内核对象是内核模式下同步线程的一种方式,事件内核对象有两种状态,分别为触发态和非触发态,当异步 I/O 没有完成时为非触发态,当异步 I/O 对象完成时为触发态,这也就是为什么事件内核对象可以获取异步 I/O 通知。修改过的代码如下所示:
DWORD WINAPI Create_File(WCHAR *FileName)
{
// 以只读方式打开 D 盘中的文件,并且独占该文件的访问,倒数第二个参数表示以异步方式进行 I/O 操作
DWORD LastError; HANDLE file;
file = CreateFile(FileName, GENERIC_READ, 0, NULL, OPEN_EXISTING, FILE_FLAG_OVERLAPPED, NULL);
// 如果错误返回错误代码
LastError = GetLastError();
if (file == INVALID_HANDLE_VALUE) return LastError;
// 查询文件大小
LARGE_INTEGER FileSize = { 0 };
GetFileSizeEx(file, &FileSize);
cout << "[*] 打开文件成功" << endl;
cout << "[*] 文件大小为: " << FileSize.LowPart << " 字节" << endl;
// 判断文件大小是否小于 4096 个字节,太大就不打印了
if (FileSize.LowPart < 4096)
{
// 读取文件当中的内容
BOOL res; DWORD dwNumBytes; OVERLAPPED overlapped = { 0 };
// 将 overlapped 结构体中的 hEvent 成员绑定一个事件内核对象
overlapped.Offset = 0; overlapped.hEvent = CreateEvent(NULL, TRUE, FALSE, NULL);
BYTE *Buffer = (BYTE *)malloc(FileSize.LowPart);
// 以异步方式读取文件中的内容
res = ReadFile(file, Buffer, FileSize.LowPart, &dwNumBytes, &overlapped);
LastError = GetLastError();
// 满足条件说明异步 I/O 操作成功
if (res == FALSE && (LastError == ERROR_IO_PENDING))
{
// 等待异步 I/O 完成通知
WaitForSingleObject(overlapped.hEvent, INFINITE);
cout << "[*] 文件内容: " << endl << Buffer << endl;
}
else
{
return LastError;
}
}
// 关闭文件句柄
CloseHandle(file);
return TRUE;
}
- 以上代码相对于以触发内核对象方式获取异步 I/O 完成通知的方式只改了两个部分,第一个是将 overlapped 结构体中的 hEvent 成员绑定一个事件内核对象,该事件内核对象是自动重置且处于未触发状态;第二个是将 WaitForSingleObject 函数等待的句柄变为了 overlapped.hEvent,看起来好像比上一个复杂一些
0x03 -> (3) 使用可提醒的 I/O 获取异步 I/O 完成通知
- 使用可提醒的 I/O 获取异步 I/O 完成通知相对于上面两种方式要更为复杂,同时也显得高大上许多,因为借助了 APC 队列。当发出一个异步 I/O 请求时,系统会将其添加到调用线程的 APC 队列当中,当异步 I/O 完成之后会调用回调函数,相当于接收了通知,示例代码如下
#include <Windows.h>
#include <iostream>
#include <stdio.h>
#include <strsafe.h>
using namespace std;
DWORD WINAPI Create_File(WCHAR *FileName);
VOID WINAPI ErrorCodeTransformation(DWORD ErrorCode);
VOID WINAPI CompletionRoutine(DWORD dwError, DWORD dwNumByte, OVERLAPPED *po);
BYTE *Buffer;
int main(int argc, char **argv)
{
WCHAR FileName[12] = TEXT("D:\\post.txt");
DWORD res = Create_File(FileName);
// 打印错误函数
if (res != TRUE) ErrorCodeTransformation(res);
return 0;
}
DWORD WINAPI Create_File(WCHAR *FileName)
{
DWORD LastError; HANDLE file;
file = CreateFile(FileName, GENERIC_READ, 0, NULL, OPEN_EXISTING, FILE_FLAG_OVERLAPPED, NULL);
LastError = GetLastError();
if (file == INVALID_HANDLE_VALUE) return LastError;
LARGE_INTEGER FileSize = { 0 };
GetFileSizeEx(file, &FileSize);
cout << "[*] 打开文件成功" << endl;
cout << "[*] 文件大小为: " << FileSize.LowPart << " 字节" << endl;
// 读取文件当中的内容
BOOL res; OVERLAPPED overlapped = { 0 };
overlapped.Offset = 0;
Buffer = (BYTE *)malloc(FileSize.LowPart);
// 以异步方式读取文件中的内容
res = ReadFileEx(file, Buffer, FileSize.LowPart, &overlapped, &CompletionRoutine);
// 将线程设置为可提醒状态
SleepEx(0, TRUE);
// 关闭文件句柄
CloseHandle(file);
return TRUE;
}
// 回调函数
VOID WINAPI CompletionRoutine(DWORD dwError, DWORD dwNumByte, OVERLAPPED *po)
{
cout << "[*] 文件内容: " << endl << Buffer << endl;
}
// 如果返回错误,可调用此函数打印详细错误信息
VOID WINAPI ErrorCodeTransformation(DWORD ErrorCode)
{
LPVOID lpMsgBuf; LPVOID lpDisplayBuf; DWORD dw = ErrorCode;
// 将错误代码转换为错误信息
FormatMessage(FORMAT_MESSAGE_ALLOCATE_BUFFER | FORMAT_MESSAGE_FROM_SYSTEM | FORMAT_MESSAGE_IGNORE_INSERTS,
NULL, dw, MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT), (LPTSTR)&lpMsgBuf, 0, NULL
);
lpDisplayBuf = (LPVOID)LocalAlloc(LMEM_ZEROINIT, (lstrlen((LPCTSTR)lpMsgBuf) + 40) * sizeof(TCHAR));
StringCchPrintf((LPTSTR)lpDisplayBuf, LocalSize(lpDisplayBuf), TEXT("错误代码 %d : %s"), dw, lpMsgBuf);
// 弹窗显示错误信息
MessageBox(NULL, (LPCTSTR)lpDisplayBuf, TEXT("Error"), MB_OK);
LocalFree(lpMsgBuf); LocalFree(lpDisplayBuf); ExitProcess(dw);
}
- 结合以上程序总结出使用可提醒的 I/O 需要具被这么几个条件:(1) 线程必须设置为可提醒状态 (2) 必须有回调函数 (3) 必须使用 ReadFileEx 而非 ReadFile,因为只有 ReadFileEx 才可以传入回调函数(好像是废话)
- 当使用 ReadFileEx 开始进行异步 I/O 操作时,传入了 CompletionRoutine 作为回调函数,同时系统会将这个异步 I/O 请求添加到 APC 队列中去,之后使用 SleepEx 函数将当前线程设置为可提醒状态(其他可提醒函数也可以替代 SleepEx),当异步 I/O 操作完成之后,回调函数就会被调用,从而打印出文件中的数据,打印结果如下:

0x03 -> (4) 使用 I/O 完成端口获取异步 I/O 完成通知
- 接收异步 I/O 的最后一种方式就是使用 I/O 完成端口(大佬极力推荐,菜鸟表示无力),这是所有接收 I/O 通知方式中最复杂的一个,毕竟 Windows 团队花了数年的时间研究创建了这个机制,这个机制真的很强大,上到可以加速处理网络请求,下到可以增强 I/O 访问速度。来看看使用这套机制需要哪些函数:
(1) CreateIoCompletionPort:创建 I/O 完成端口或者将一个 I/O 完成端口与设备相绑定 (2) GetQueuedCompletionStatus:用于等待 I/O 完成端口等待队列中处理完的 I/O 操作
- 函数很简单,下面来看一个例子:
#include <process.h>
#include <Windows.h>
#include <iostream>
using namespace std;
DWORD WINAPI IOTestFun(PWCHAR FileName);
unsigned __stdcall ThreadFun(void* pvParam);
// 打开的文件设备的句柄
HANDLE file;
// 文件的大小
DWORD Size;
// ReadFile 读取文件的缓冲区
PBYTE BufferFile;
// 创建的 I/O 完成端口的句柄
HANDLE IOPort;
int main(int argc, char **argv)
{
WCHAR FileName[12] = TEXT("D:\\post.txt");
IOTestFun(FileName);
return 0;
}
DWORD WINAPI IOTestFun(PWCHAR FileName)
{
// 打开 D盘 post.txt 文件
DWORD LastError;
// FILE_FLAG_OVERLAPPED 参数表示以异步方式打开文件
file = CreateFile(FileName, GENERIC_READ, 0, NULL, OPEN_EXISTING, FILE_FLAG_OVERLAPPED, NULL);
LastError = GetLastError();
// 获取文件的大小
LARGE_INTEGER FileSize = { 0 };
GetFileSizeEx(file, &FileSize);
Size = FileSize.LowPart;
// 用于 ReadFile 读取文件数据的缓冲区,下面会使用到
BufferFile = (PBYTE)malloc(Size);
// 创建 IO 完成端口并且绑定设备 file
ULONG_PTR ptr = 0;
// CreateIoCompletionPort 最后一个参数表示同时只有两个线程被唤醒
IOPort = CreateIoCompletionPort(INVALID_HANDLE_VALUE, NULL, 0, 2);
// 将 IO 完成端口 IOPort 绑定设备 file
CreateIoCompletionPort(file, IOPort, ptr, 0);
// 创建 10 个线程并且立刻执行
HANDLE Threads[10];
for (size_t i = 0; i < 10; i++)
{
Threads[i] = (HANDLE)_beginthreadex(NULL, 0, ThreadFun, (PVOID)&i, CREATE_SUSPENDED, NULL);
ResumeThread(Threads[i]);
}
// 创建 10 个 IO 读取操作
for (size_t i = 0; i < 10; i++)
{
// overlapped.Offset = 0 表示从文件中的开头读取文件
OVERLAPPED overlapped = { 0 }; overlapped.Offset = 0;
ReadFile(file, BufferFile, Size, NULL, &overlapped);
}
// 后续等待清理工作
WaitForMultipleObjects(10, Threads, TRUE, INFINITE);
for (size_t i = 0; i < 10; i++)
{
CloseHandle(Threads[i]);
}
CloseHandle(file);
return TRUE;
}
unsigned __stdcall ThreadFun(void* pvParam)
{
DWORD NumberByte; ULONG_PTR ptr; LPOVERLAPPED lapped = { 0 };
// 等待 I/O 操作完成
GetQueuedCompletionStatus(IOPort, &NumberByte, &ptr, &lapped, INFINITE);
// 打印文件内容
cout << "文件内容: " << BufferFile << endl;
return 0;
}
注:这个程序将 CreateIoCompletionPort 函数拆开来使用,先创建后绑定,方便理解
- 以上这个程序是干什么的呢,首先使用 CreateFile 打开 D 盘的 post.txt 文件设备,之后创建 I/O 完成端口并绑定这个文件设备,然后创建 10 个线程立刻执行,最后使用 ReadFile 循环 10 次读取文件当中的内容,打印结果如下:

- 值得注意的是 CreateFile 是以异步方式打开文件的,且每个线程的一开始都会使用 GetQueuedCompletionStatus 将线程变为等待状态,由于我们循环了 10 次读取文件的操作,而每一次读取文件的时间都很长,只要有一次读取文件成功,就会唤醒 10 个线程中的两个去处理剩下的工作,也就是将文件内容打印出来,那为什么是 10 个线程中的 2 个呢,因为在使用 CreateIoCompletionPort 函数时将最后一个参数传递的是 2,表示同时只有两个线程去处理剩下的工作
注:这个理解起来确实很复杂,可以想象为开始循环 10 次使用 ReadFile 读取文件内容时,I/O 完成端口会将这 10 个 I/O 请求按顺序放入一个队列中,并且此时有 10 个线程使用 GetQueuedCompletionStatus 函数等待,每当队列中的一个请求完成也就是 ReadFile 读取文件成功时就会从队列中出来,同时等待的线程中的 1 个也会被唤醒处理剩下的工作。需要知晓的是唤醒的线程是随机的,就像小鸟母亲叼着虫子喂小鸟,健壮的小鸟才会抢到食物
-参考资料:Windows 核心编程
使用同步或异步的方式完成 I/O 访问和操作(Windows核心编程)的更多相关文章
- windows核心编程 - 线程同步机制
线程同步机制 常用的线程同步机制有很多种,主要分为用户模式和内核对象两类:其中 用户模式包括:原子操作.关键代码段 内核对象包括:时间内核对象(Event).等待定时器内核对象(WaitableTim ...
- windows核心编程---第九章 同步设备IO与异步设备IO之同步IO
同步设备IO 所谓同步IO是指线程在发起IO请求后会被挂起,IO完成后继续执行. 异步IO是指:线程发起IO请求后并不会挂起而是继续执行.IO完毕后会得到设备的通知.而IO完成端口就是实现这种通知的很 ...
- windows核心编程---第八章 使用内核对象进行线程同步
使用内核对象进行线程同步. 前面我们介绍了用户模式下线程同步的几种方式.在用户模式下进行线程同步的最大好处就是速度非常快.因此当需要使用线程同步时用户模式下的线程同步是首选. 但是用户模式下的线程同步 ...
- 【windows核心编程】 第八章 用户模式下的线程同步
Windows核心编程 第八章 用户模式下的线程同步 1. 线程之间通信发生在以下两种情况: ① 需要让多个线程同时访问一个共享资源,同时不能破坏资源的完整性 ② 一个线程需要通知其他线程 ...
- 用户模式下的线程同步的分析(Windows核心编程)
线程同步 同一进程或者同一线程可以生成许多不同的子线程来完成规定的任务,但是多个线程同时运行的情况下可能需要对某个资源进行读写访问,比如以下这个情况:创建两个线程对同一资源进行访问,最后打印出这个资源 ...
- Windows核心编程 第八章 用户方式中线程的同步(上)
第8章 用户方式中线程的同步 当所有的线程在互相之间不需要进行通信的情况下就能够顺利地运行时, M i c r o s o f t Wi n d o w s的运行性能最好.但是,线程很少能够在所有的时 ...
- Windows核心编程 第八章 用户方式中线程的同步(下)
8.4 关键代码段 关键代码段是指一个小代码段,在代码能够执行前,它必须独占对某些共享资源的访问权.这是让若干行代码能够"以原子操作方式"来使用资源的一种方法.所谓原子操作方式,是 ...
- windows核心编程---第七章 用户模式下的线程同步
用户模式下的线程同步 系统中的线程必须访问系统资源,如堆.串口.文件.窗口以及其他资源.如果一个线程独占了对某个资源的访问,其他线程就无法完成工作.我们也必须限制线程在任何时刻都能访问任何资源.比如在 ...
- Windows核心编程:第10章 同步设备IO与异步设备IO
Github https://github.com/gongluck/Windows-Core-Program.git //第10章 同步设备IO与异步设备IO.cpp: 定义应用程序的入口点. // ...
随机推荐
- .net 开源模板引擎jntemplate 教程:基础篇之语法
一.基本概念 上一篇我们简单的介绍了jntemplate并写了一个hello world(如果没有看过的,点击查看),本文将继续介绍jntemplate的模板语法. 我们在讲解语法前,首先要了解一下标 ...
- Typora学习
Markdown学习总结 标题的使用格式 # 一阶标题 或者 ctrl + 1 ## 二阶标题 或者 ctrl + 2 ### 三阶标题 或者 ctrl + 3 #### 四阶标题 或者 ctrl + ...
- 不用任何框架,Java 就能实现定时任务的 3 种方法!
是的,不用任何框架,用我们朴素的 Java 编程语言就能实现定时任务. 今天,栈长就介绍 3 种实现方法,教你如何使用 JDK 实现定时任务! 1. sleep 这也是我们最常用的 sleep 休眠大 ...
- k8s 日志收集之 EFK
如今越来越多的应用部署在容器之中,如何收集日志也是一个很重要的问题.服务出问题了,排查问题需要给开发看日志.服务一般会在多个不同的 pod 中,一个一个的登进去看也的确不方便.业务数据统计也需要日志. ...
- HDU_3359 Kind of a Blur 【浮点型高斯消元+BFS】
一.题目 Kind of a Blur 二.分析 题目读起来挺费劲的. 主要就是要求一个矩阵,其中每个点及其于这个的曼哈顿距离小于D的点的值总和的平均值就是新生成的矩阵. 给定新生成的矩阵,求初始矩阵 ...
- 数据搬运组件:基于Sqoop管理数据导入和导出
本文源码:GitHub || GitEE 一.Sqoop概述 Sqoop是一款开源的大数据组件,主要用来在Hadoop(Hive.HBase等)与传统的数据库(mysql.postgresql.ora ...
- Xshell(远程)连接不上linux服务器(防火墙介绍)
一.原因 远程(ssh)连接不上linux服务器的大多数原因都是因为本地服务器的防火墙策略导致的,因此我们想ssh远程能够连接上服务器,有两种方法: 修改防火墙策略 关闭防火墙 二.防火墙服务介绍 1 ...
- 安装RPM包或者源码包
RPM工具 RPM他是以一种数据库记录的方式将我们所需要的套件安装到linux主机的一套管理程序关于RPM各个选项的含义如下-i:表示安装-v:表示可视化-h:表示安装进度在安装RPM包时,常用的附带 ...
- [源码分析] 消息队列 Kombu 之 Hub
[源码分析] 消息队列 Kombu 之 Hub 0x00 摘要 本系列我们介绍消息队列 Kombu.Kombu 的定位是一个兼容 AMQP 协议的消息队列抽象.通过本文,大家可以了解 Kombu 中的 ...
- vim下在插件emmet
试了很多种方法,结果都没有用,最后找到了完美的解决方法! 1.方式1 1.1下载emmet并解压 1.2 cd /home/debian8/Downloads/emmet-vim-master/plu ...