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核心编程)的更多相关文章

  1. windows核心编程 - 线程同步机制

    线程同步机制 常用的线程同步机制有很多种,主要分为用户模式和内核对象两类:其中 用户模式包括:原子操作.关键代码段 内核对象包括:时间内核对象(Event).等待定时器内核对象(WaitableTim ...

  2. windows核心编程---第九章 同步设备IO与异步设备IO之同步IO

    同步设备IO 所谓同步IO是指线程在发起IO请求后会被挂起,IO完成后继续执行. 异步IO是指:线程发起IO请求后并不会挂起而是继续执行.IO完毕后会得到设备的通知.而IO完成端口就是实现这种通知的很 ...

  3. windows核心编程---第八章 使用内核对象进行线程同步

    使用内核对象进行线程同步. 前面我们介绍了用户模式下线程同步的几种方式.在用户模式下进行线程同步的最大好处就是速度非常快.因此当需要使用线程同步时用户模式下的线程同步是首选. 但是用户模式下的线程同步 ...

  4. 【windows核心编程】 第八章 用户模式下的线程同步

    Windows核心编程 第八章 用户模式下的线程同步 1. 线程之间通信发生在以下两种情况: ①    需要让多个线程同时访问一个共享资源,同时不能破坏资源的完整性 ②    一个线程需要通知其他线程 ...

  5. 用户模式下的线程同步的分析(Windows核心编程)

    线程同步 同一进程或者同一线程可以生成许多不同的子线程来完成规定的任务,但是多个线程同时运行的情况下可能需要对某个资源进行读写访问,比如以下这个情况:创建两个线程对同一资源进行访问,最后打印出这个资源 ...

  6. Windows核心编程 第八章 用户方式中线程的同步(上)

    第8章 用户方式中线程的同步 当所有的线程在互相之间不需要进行通信的情况下就能够顺利地运行时, M i c r o s o f t Wi n d o w s的运行性能最好.但是,线程很少能够在所有的时 ...

  7. Windows核心编程 第八章 用户方式中线程的同步(下)

    8.4 关键代码段 关键代码段是指一个小代码段,在代码能够执行前,它必须独占对某些共享资源的访问权.这是让若干行代码能够"以原子操作方式"来使用资源的一种方法.所谓原子操作方式,是 ...

  8. windows核心编程---第七章 用户模式下的线程同步

    用户模式下的线程同步 系统中的线程必须访问系统资源,如堆.串口.文件.窗口以及其他资源.如果一个线程独占了对某个资源的访问,其他线程就无法完成工作.我们也必须限制线程在任何时刻都能访问任何资源.比如在 ...

  9. Windows核心编程:第10章 同步设备IO与异步设备IO

    Github https://github.com/gongluck/Windows-Core-Program.git //第10章 同步设备IO与异步设备IO.cpp: 定义应用程序的入口点. // ...

随机推荐

  1. Linux安全模型中的3A

    3A Authentication : 认证 验证用户身份 Authorization : 授权 不同用户设置不同权限 Accouting | Audition : 审计 Linux 验证用户身份 U ...

  2. 设计模式之抽象工厂模式(Abstract Factory Pattern)

    一.抽象工厂模式的由来 抽象工厂模式,最开始是为了解决操作系统按钮和窗体风格,而产生的一种设计模式.例如:在windows系统中,我们要用windows设定的按钮和窗体,当我们切换Linux系统时,要 ...

  3. Netty源码 reactor 模型

    翻阅源码时,我们会发现netty中很多方法的调用都是通过线程池的方式进行异步的调用, 这种  eventLoop.execute 方式的调用,实际上便是reactor线程.对应项目中使用广泛的NioE ...

  4. 【odoo14】odoo 14 Development Cookbook【目录篇】

    网上已经有大佬翻译过odoo12并且在翻译odoo14了.各位着急的可以自行搜索下... 这本书是为了让自己从odoo12转odoo14学习.也是为了锻炼下自己... odoo 14 Developm ...

  5. 【关系抽取-R-BERT】加载数据集

    认识数据集 Component-Whole(e2,e1) The system as described above has its greatest application in an arraye ...

  6. PTE 准备之 Personal introduction

    Task strategies Be prepared! This is your opportunity to give the admissions officers a first impres ...

  7. 技术分享PPT整理(一):Bootstrap基础与应用

    最近在复习的时候总感觉有些知识点总结过,但是翻了一下博客没有找到,才想起来有一些内容是放在部门的技术分享里的,趁这个时候跳了几篇相对有价值的梳理一下,因为都是PPT,所以内容相对零散,以要点和图片为主 ...

  8. P1047_校门外的树(JAVA语言)

    题目描述 某校大门外长度为L的马路上有一排树,每两棵相邻的树之间的间隔都是1米. 我们可以把马路看成一个数轴,马路的一端在数轴0的位置,另一端在L的位置: 数轴上的每个整数点,即0,1,2,-,L都种 ...

  9. 《Selenium自动化测试实战:基于Python》之 Selenium IDE插件的安装与使用

    第3章  Selenium IDE插件的安装与使用 京东:https://item.jd.com/13123910.html 当当:http://product.dangdang.com/292045 ...

  10. RPC 框架设计

    RPC 框架设计 初识 RPC 服务化有什么好处? 防止代码拷贝 防止底层复杂性的扩散 防止公共库的耦合 保证 SQL 的质量,能够解除数据库的耦合 什么是 RPC RPC:Remote Proced ...