zoukankan      html  css  js  c++  java
  • 使用同步或异步的方式完成 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 核心编程

  • 相关阅读:
    floating IP 原理分析
    创建 floating IP
    Why Namespace?
    虚拟 ​router 原理分析- 每天5分钟玩转 OpenStack(101)
    链接脚本使用一例2---将二进制文件 如图片、MP3音乐、词典一类的东西作为目标文件中的一个段
    linux-2.6.26内核中ARM中断实现详解(转)
    有关Cache –(1) linux list之中的Prefetc
    Linux 内核中的 GCC 特性
    对entry-common.S和call.S的部分理解1
    kernel&uboot学习笔记
  • 原文地址:https://www.cnblogs.com/csnd/p/11800514.html
Copyright © 2011-2022 走看看