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. 推荐模型AutoRec:原理介绍与TensorFlow2.0实现

    1. 简介 本篇文章先简单介绍论文思路,然后使用Tensoflow2.0.Keras API复现算法部分.包括: 自定义模型 自定义损失函数 自定义评价指标RMSE 就题目而言<AutoRec: ...

  2. 2020年HTML5考试模拟题整理(一)

    1.哪个元素被称为媒体元素的子元素? 答案:<track>. <track> 标签为媒体元素(比如 <audio> and <video>)规定外部文本 ...

  3. ts装饰器的用法,基于express创建Controller等装饰器

    TS TypeScript 是一种由微软开发的自由和开源的编程语言.它是 JavaScript 的一个超集,而且本质上向这个语言添加了可选的静态类 型和基于类的面向对象编程. TypeScript 扩 ...

  4. 【翻译】内部API的价值

    内部api的设计,主要是为了简化软件的开发,简化系统和操作过程.目前绝大多数用例是这样的. 内部api经常被忽略,因为它们是针对内部开发人员的.这种类型的api通常使用于特定公司及其部门的专用数据.尽 ...

  5. .NET 6 Preview 2 发布

    前言 在 2021 年 3 月 11 日, .NET 6 Preview 2 发布,这次的改进主要涉及到 MAUI.新的基础库和运行时.JIT 改进. .NET 6 正式版将会在 2021 年 11 ...

  6. Linux开发环境搭建——deepin系统的使用

    上大学的时候就在自己的笔记本上安装过深度操作系统(deepin),当时好像是15.x的版本.毕业后第一家公司是全Mac办公,因在学校期间有过完全Linux环境下的开发体验,上手Mac非常快.非常爽.前 ...

  7. DES加密--不安全加密

    package test; import java.security.InvalidKeyException; import java.security.Key; import java.securi ...

  8. Beego框架学习---layout的使用

    Beego框架学习---layout的使用 在管理系统中,管理菜单的界面是固定的,会变化的只是中间的部分.我就在想,是不是跟angular的"组件模块的router-outlet一样&quo ...

  9. Python之基础算法介绍

    一.算法介绍 1. 算法是什么 算法是指解题方案的准确而完整的描述,是一系列解决问题的清晰指令,算法代表着用系统的方法描述解决问题的策略机制.也就是说,能够对一定规范的输入,在有限时间内获得所要求的输 ...

  10. python打印9宫格25宫格81宫格.....

    """ 2 问题描述: 3 给定一个奇数(num),生成一个横竖斜加起来的和相等 4 问题解析: 5 这其实就是一个九宫格的问题 6 九宫格问题的解答技巧: 7 1要放在 ...