进程

  1. 进程是操作系统结构的基础;是一个正在执行的程序;计算机中正在运行的程序实例;可以分配给处理器执行的一个实体
  2. 一个进程包含一个运行中的程序所用到的所有资源:一个虚拟的内存空间。内存空间中有很多模块,被分为两大部分:用户层和内核层。普通程序只能访问用户层空间(000000007FFFFFFF),大部分对象(结构体变量)都是放在内核空间(80000000FFFFFFFF),故不能直接访问到,只能通过句柄。所有的进程内核层是共享的,不同的进程用户层空间不一样。在进程的虚拟内存中一般会加载一个 exe 和很多的 dll,这些都称之为模块,进程本身是一个综合了各种资源的东西,不能执行代码,能执行代码的是归属于进程的线程
  3. 进程是惰性的,它仅是一个包含有线程的容器,其本身没有执行代码的能力,因此每个进程在创建之初都会创建一个主线程用于执行代码,若此主线程结束,系统就会销毁这个进程内核对象
  4. 进程是程序在一个数据集合上运行的过程(一个程序可能同时属于多个进程),是操作系统进行资源分配和调度的一个独立单位,进程可分为系统进程和用户进程
  5. 在 Windows 下,进程又被细化为线程,即一个进程下有多个能独立运行的更小的单位。程序是静止的,而进程是动态的

进程的创建

1
2
3
4
5
6
7
8
9
10
BOOL WINAPI CreateProcess(_In_opt_ LPCSTR lpApplicationName,//可执行文件名称
_Inout_opt_ LPSTR lpCommandLine,//命令行
_In_opt_ LPSECURITY_ATTRIBUTES lpProcessAttributes,//进程安全属性
_In_opt_ LPSECURITY_ATTRIBUTES lpThreadAttributes,//线程安全属性
_In_ BOOL bInheritHandles,//句柄继承
_In_ DWORD dwCreationFlags,//创建方式标志
_In_opt_ LPVOID lpEnvironment,//环境字符串块
_In_opt_ LPCSTR lpCurrentDirectory,//新进程的当前目录
_In_ LPSTARTUPINFOA lpStartupInfo,//进程配置结构体
_Out_ LPPROCESS_INFORMATION lpProcessInformation);

进程创建实例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
bool CreateChildProcess(LPTSTR lpPath, BOOL bWait) {
STARTUPINFO si = { 0 };
PROCESS_INFORMATION pi = { 0 };
si.cb = sizeof(si);
//创建子进程并判断是否成功
if (!CreateProcess(lpPath, 0, 0, 0, FALSE, 0, 0, 0, &si, &pi))
return false;
//是否需要等待进程执行结束
if (bWait)
WaitForSingleObject(pi.hProcess, INFINITE);
//关闭进程句柄和线程句柄
CloseHandle(pi.hProcess);
CloseHandle(pi.hThread);
return true;
}

线程

  1. 一个线程就是操作系统的一个内核对象,在 Windows 操作系统内核中没有进程的概念,只有线程的概念,进程只不过是在逻辑上对一组线程及其相关的资源进行的一种封装

  2. 线程是进程中某个单一顺序的控制流,也被称为轻量进程,指运行中的程序的调度单位,有自己的一块堆栈和执行环境

  3. 一个线程可以再次创建多个线程,由其创建的线程仍然可以分别创建多个线程

  4. 作为一个独立的执行单元来讲,线程占用的系统资源远不及进程,但其本质却未大打折扣

  5. CPU 执行代码主要依靠一套寄存器:

    通用寄存器:eax,ebx,ecx,edx,esi,esp,ebp

    指令寄存器:eip 存储下一条要执行的指令

    段寄存器:cs,ss,ds,es,fd,gs

  6. 所有线程都是操作系统统一管理的,每一个线程都有自己的优先级,根据优先级决定先后调用顺序,线程发生切换,实际就是切换线程的执行环境

  7. 退出线程的方式:

    1. 通过返回方式正常退出(最理想)
    2. 通过 ExitThread 强制结束本线程
    3. 通过 TernamiteThread 来结束指定线程
    4. 通过结束进程来结束所有线程

线程的创建

1
2
3
4
5
6
HANDLE WINAPI CreateThread(_In_opt_ LPSECURITY_ATTRIBUTES lpThreadAttributes,
_In_ SIZE_T dwStackSize,//指定线程可以拥有多少个栈空间
_In_ LPTHREAD_START_ROUTINE lpStartAddress, //线程函数起始地址
_In_opt_ __drv_aliasesMem LPVOID lpParameter, //线程函数参数
_In_ DWORD dwCreationFlags, //线程创建标志
_Out_opt_ LPDWORD lpThreadId /*新建线程的ID*/);

线程实例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
//线程函数
DWORD WINAPI ThreadProc(LPVOID lpParam) {
MessageBox(NULL, (LPCTSTR)lpParam, _T("CreateThread"), MB_OK);
return 0;
}
bool CreateChildThread() {
DWORD dwThreadId = 0;
HANDLE hThread = CreateThread(NULL,//默认安全属性
0,//默认堆栈大小
ThreadProc,//线程函数
_T("CreateThread"),//参数
0,//默认创建标志
&dwThreadId);//返回TID
if (hThread == 0) {
//ExitProcess
return false;
}
return true;
}

内核对象

  1. Windows 系统中对象被保护起来,需要通过他们的句柄,调用相应的 API 函数去操作这些对象,所有的 API 函数都属于不同的动态链接库(DLL 文件)。根据对象的不同使用情况,可以分为 user对象,GDI对象和内核对象,其中user对象和窗口、控件等有关,GDI对象和绘图有关,内核对象和系统全局性的一些功能有关

    常见的内核对象:

    进程 线程 访问令牌 文件 文件映射
    I/O 完成端口 邮槽 管道 互斥体 信号量
    事件 计时器 线程池
  2. 所有内核对象都遵循统一的使用模式:

    1. 创建对象
    2. 打开对象,得到句柄(可与第一步合并,表示创建时就打开)
    3. 通过 API 访问对象
    4. 关闭句柄
    5. 句柄完全关闭,对象自动销毁
  3. 所有内核对象都属于操作系统内核,可在不同的进程间访问到,俗称:内核对象是跨进程的,通过引用计数和句柄表管理

  4. 每一个内核对象结构体都有一个字段叫做引用计数,当有一个进程创建或打开此内核对象,则引用计数自增1,进程终止,或关闭句柄,引用计数自减1,当引用计数为0,内核对象自动销毁

  5. 每个内核对象创建时都有一个安全属性,这个安全属性标识了怎么去使用这个对象,比如可读可写,权限等,一般创建一个内核对象时都需要指定一个安全属性 SECURITY_ATTRIBUTES,若传 NULL 的话,就会指定一个默认的属性

  6. 内核对象的句柄和进程有关,对于同一个对象,在不同的进程中,其句柄值不同,这点和 GDI对象不同,GDI对象的句柄值是全局有效的。由此可见,不同类型的对象,其管理方式也是不同的

  7. 每个进程对象中都有一个句柄表,用于记录本进程所打开的所有内核对象,可以简单的将句柄表理解为一个一维的结构体数组,句柄值可看做句柄表中的索引,故而内核对象的句柄值仅仅对本进程有效。句柄表中的每一项描述了使用此句柄访问内核对象的权限,以及此句柄是否可以被子进程继承

  8. 获取内核对象句柄:

    1. 自己创建
    2. 自己打开(此对象是已创建好的)
    3. 从父进程继承过来
    4. 别的进程复制过来,DuplicateHandle() 函数

基本操作

  1. 进程的基本操作:

    创建:CreateProcess 终止:ExitProcess 打开已存在的进程:OpenProcess

    强制结束:TerminateProcess 进程间的通讯:COPY_DATA 消息,邮槽

  2. 线程的基本操作:

    创建:CreateThread 打开:OpenThread 挂起:SuspendThread 恢复:ResumeThread

    终止:ExitThread 强制结束:TerminateThread

遍历

方法很多,这里使用创建快照的方式:CreateToolHelp32Snapshot

  • 进程快照:得到系统上所有的进程
  • 模块快照:以进程为单位

需要知道:

  1. 进程是操作系统管理的,遍历时能够遍历出系统所有进程的信息(进程名,路径,进程 ID),通常都是知道进程名再去找 ID,ID 在每一次程序运行时都不一样,若要操作进程,就要使用 OpenProcess 函数得到其句柄,OpenProcess 函数的作用就是根据进程 ID 得到句柄的

  2. 模块是属于某个进程的,故遍历模块时需指定遍历哪一个进程的模块,能够遍历出的模块信息为:模块名,模块的起始地址(加载基址)

    遍历模块的用处:

    1. 可以知道一个程序都加载了哪些 DLL,监测 DLL 注入
    2. 分析 DLL 中的 PE 文件信息,可以为分析一个程序提供依据
  3. 线程虽属于一个进程,但其是操作系统统一管理的,故需遍历操作系统中的所有线程,然后自己过滤得到某个进程的线程,线程遍历得到的信息有:线程 ID,所属进程的 ID

    遍历线程的用处:得到进程中每个线程的信息,操作这些线程,比如挂起,终止等

进程的遍历,使用 Release

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
#include<windows.h>
#include<stdio.h>
#include<Tlhelp32.h>
int main()
{
HANDLE hProcessSnap;//进程快照句柄
HANDLE hProcess;//进程句柄
PROCESSENTRY32 stcPe32 = { 0 };//进程快照信息
stcPe32.dwSize = sizeof(PROCESSENTRY32);
//创建一个进程相关的快照句柄
hProcessSnap = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0);
if (hProcessSnap == INVALID_HANDLE_VALUE)
return false;
//通过进程快照句柄获取第一个进程信息
if (!Process32First(hProcessSnap, &stcPe32)) {
CloseHandle(hProcessSnap);
return false;
}
//循环遍历进程信息
do {
//获取优先级信息
hProcess = OpenProcess(PROCESS_QUERY_INFORMATION, FALSE,
stcPe32.th32ProcessID);
if (hProcess) {
GetPriorityClass(hProcess);//获取进程优先级
CloseHandle(hProcess);//关闭句柄
}
//获取进程的其他相关信息
printf("进程ID:%d ",stcPe32.th32ProcessID);
printf("线程数:%d ", stcPe32.cntThreads);
printf("父进程ID:%d", stcPe32.th32ParentProcessID);
printf("进程路径:%s", stcPe32.szExeFile);
printf("\n");
} while (Process32Next(hProcessSnap, &stcPe32));
//关闭句柄退出函数
CloseHandle(hProcessSnap);
return 0;
}

模块的遍历

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
HANDLE hModuleSnap = INVALID_HANDLE_VALUE;
MODULEENTRY32 me32 = { sizeof(MODULEENTRY32) };
//创建一个模块相关的句柄
DWORD dwPId = 0;
hModuleSnap = CreateToolhelp32Snapshot(TH32CS_SNAPMODULE,//指定模块类型
dwPId);//指定进程
if (hModuleSnap == INVALID_HANDLE_VALUE)
return false;
//通过模块快照句柄获取第一个模块信息
if (!Module32First(hModuleSnap, &me32)) {
CloseHandle(hModuleSnap);
return false;
}
//循环获取模块信息
do {
printf("模块句柄%d ", me32.hModule);
printf("加载基址%d ", me32.modBaseAddr);
printf("模块名%s ", me32.szExePath);
printf("\n");
} while (Module32Next(hModuleSnap, &me32));
//关闭句柄并退出函数
CloseHandle(hModuleSnap);

线程的遍历

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
#include<windows.h>
#include<stdio.h>
#include<Tlhelp32.h>
VOID ListProcessThreads(DWORD dwPID) {
HANDLE hThreadSnap = INVALID_HANDLE_VALUE;
THREADENTRY32 te32;
//创建快照
hThreadSnap = CreateToolhelp32Snapshot(TH32CS_SNAPTHREAD, 0);
if (hThreadSnap == INVALID_HANDLE_VALUE)
return;
//设置输入参数,结构体的大小
te32.dwSize = sizeof(THREADENTRY32);
//开始获取信息
if (!Thread32First(hThreadSnap, &te32)) {
CloseHandle(hThreadSnap);
return;
}
do {
if (te32.th32OwnerProcessID == dwPID) {//dwPID是指定进程的ID
//显示相关信息
printf("\n THREAD ID = 0x%08X", te32.th32ThreadID);
printf("\t base priority = %d", te32.tpBasePri);
printf("\t delta priority = %d", te32.tpDeltaPri);
}
} while (Thread32Next(hThreadSnap, &te32));
CloseHandle(hThreadSnap);
}
int main()
{
ListProcessThreads(8364);//传入进程ID
return 0;
}

文件的遍历

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
#include<atlstr.h>
#include<windows.h>
void listDir(const CString& path, int deep) {
if (deep == 0)
return;
WIN32_FIND_DATA wfd = { 0 };
HANDLE hFind = 0;
//查找第一个文件,注意路径需加上通配符
hFind = FindFirstFile(path + L"\\*", &wfd);
if (hFind == INVALID_HANDLE_VALUE)
return;
do {
//过滤掉当前目录和上层目录
if(wcscmp(wfd.cFileName, L".") == 0 ||
wcscmp(wfd.cFileName, L"..") == 0)
continue;
wprintf(L"%s\\%s\n", (LPCWSTR)path, wfd.cFileName);
//判断当前遍历到的数据是否是目录
//可以通过文件属性标志位来判断
if (wfd.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY) {
// 如果是目录, 就递归扫描这个目录
// 拼接成一个文件路径(遍历得到的文件名,只是一个没有路径的文件名)
// 传入进来的参数就是它所在的文件夹
// 此时将文件夹和目录名拼接在一起就能得到一个绝对路径了
CString absPath = path + L"\\" + wfd.cFileName;
//递归调用
listDir(absPath, deep - 1);
}
} while (FindNextFile(hFind, &wfd));
FindClose(hFind);
}
int main()
{
listDir(L"C:\\", 2);
return 0;
}

用树型控件实现文件目录的浏览

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
void CDlgFile::ShowFile(CString str_Dir, HTREEITEM tree_Root)
{
CFileFind FileFind;//通过CFileFind类获取文件路径或名称时,要至少调用一次FindNextFileW()函数
//临时变量,用以记录返回的树根节点
HTREEITEM tree_Temp;
//判断输入目录是否是'\',若不存在则补充
if (str_Dir.Right(1) != "\\")
str_Dir += "\\";
str_Dir += "*.*";
BOOL res = FileFind.FindFile(str_Dir);
while (res)
{
tree_Temp = tree_Root;
res = FileFind.FindNextFileW();
if (FileFind.IsDirectory() && !FileFind.IsDots())//目录是文件夹
{
CString strPath = FileFind.GetFilePath(); //得到路径,做为递归调用的开始
CString strTitle = FileFind.GetFileName();//得到目录名,做为树控的结点
tree_Temp = m_MyTree.InsertItem(strTitle, 0, 0, tree_Root);
ShowFile(strPath, tree_Temp);
}
else if (!FileFind.IsDirectory() && !FileFind.IsDots())//如果是文件
{
CString strPath = FileFind.GetFilePath(); //得到路径,做为递归调用的开始
CString strTitle = FileFind.GetFileName();//得到文件名,做为树控的结点
m_MyTree.InsertItem(strTitle, 0, 0, tree_Temp);
}
}
FileFind.Close();
}

进程间的通讯

WM_COPYDATA

WM_COPYDATA 是一个特殊的、专门用于传递数据的消息,此消息可以携带一个大体积的消息参数,不同于其他只能携带两个固定参数的消息。发送 WM_COPYDATA 消息时,wParam 应保存发送此消息的窗口句柄,lParam 应指向一个名为 COPYDATASTRUCT 的结构体

1
2
3
4
5
typedef struct tagCOPYDATASTRUCT {
ULONG_PTR dwData;//任意一个32位的值
DWORD cbData;//发送数据的大小
PVOID lpData;//待发送数据块指针
}COPYDATASTRUCT, *PCOPYDATASTRUCT;

需注意:WM_COPYDATA 的数据会被发送到目标进程的栈空间保存,因此单次发送的数据量不宜过大

发送方:为一个窗口名为“WM_COPYDATA接收方”的窗口发送消息

1
2
3
4
5
6
7
8
9
10
#include<windows.h>
int main()
{
COPYDATASTRUCT cds = { 0 };
cds.dwData = 0x123456;
cds.lpData = (LPVOID)L"abc";//内容
cds.cbData = strlen((char*)cds.lpData) + 10;//可以指定数据大小为11字节
HWND hWnd = FindWindow(NULL, L"WM_COPYDATA接收方");//接收方窗口名
SendMessage(hWnd, WM_COPYDATA, 0, (LPARAM)&cds);
}

接收方:首先创建一个对话框窗口,名为“WM_COPYDATA接收方”

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
#include <windows.h>
#include "resource.h"
#include <atlstr.h>
INT_PTR CALLBACK DlgProc(HWND hWnd, UINT uMsg, WPARAM w, LPARAM l)
{
switch (uMsg)
{
case WM_CLOSE:
EndDialog(hWnd,0);
break;
case WM_COPYDATA:
{
COPYDATASTRUCT* pCds = (COPYDATASTRUCT*)l;
CString buff;
buff.Format(L"%x 大小:%d 数据:%s",
pCds->dwData,
pCds->cbData,
pCds->lpData);
MessageBox(hWnd, buff, L"提示", 0);
}
break;
default:
break;
}
return false;
}
int WinMain( _In_ HINSTANCE hInstance, _In_opt_ HINSTANCE hPrevInstance,
_In_ LPSTR lpCmdLine, _In_ int nShowCmd )
{
DialogBox(hInstance, (TCHAR*)IDD_DIALOG1, NULL, DlgProc);
}

邮槽

  1. 邮槽是 Windows 系统中最简单的一种进程间通信方式,一个进程可以创建一个邮槽,其他进程可以通过打开此邮槽与创建邮槽的进程通讯
  2. 邮槽的通讯是单向的,消息被写入邮槽后以队列的方式保存
  3. 邮槽除可在本机内进行进程间通讯外,还可在主机间通讯
    • 创建邮槽对象:CreateMailslot()
    • 打开邮槽对象:CreateFile()
    • 读写邮槽:ReadFile/WriteFile
    • 获取邮槽信息:GetMailslotInfo()

服务端代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
#include<windows.h>
#include<stdio.h>
int main()
{
//创建一个邮槽服务端
HANDLE hMailslot;
//'\\\\.\\'表示当前主机,若想连接到其他主机,可将 . 换成其他主机名
//mailslot邮槽关键字
hMailslot = CreateMailslot(
L"\\\\.\\mailslot\\邮槽",
0, -1, 0);
//建立死循环来等待其他进程将信息投递到邮槽
DWORD msgSize = 0;//消息的字节数
DWORD nextMsgSize = 0;//下一条消息的字节数
DWORD msgCount = 0;//邮槽里共有几条消息
DWORD readTimeout = 0;//读取消息的超时时间
BOOL ret = 0;
while (1) {
//等待邮槽信息
ret = GetMailslotInfo(hMailslot, &msgSize, &nextMsgSize,
&msgCount, &readTimeout);
if(!ret)
continue;
if (msgCount == 0) {
Sleep(100);
continue;
}
//知道邮槽里的信息占多少个字节后,就可以申请堆空间来将邮槽内的信息读取出来
char* pBuff = new char[nextMsgSize + 1];
memset(pBuff, 0, nextMsgSize + 1);
ReadFile(hMailslot, pBuff, nextMsgSize, &msgSize, 0);
printf("服务端 > %s\n", pBuff);
delete[] pBuff;
}
}

客户端代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
#include <windows.h>
#include <stdio.h>
int main()
{
// 1. 打开一个邮槽对象
HANDLE hMailslot = INVALID_HANDLE_VALUE;
hMailslot =
CreateFile(L"\\\\.\\mailslot\\邮槽",
GENERIC_WRITE,/*以写的方式打开*/
FILE_SHARE_WRITE,/*共享写入的权限*/
NULL,/*安全描述符*/
OPEN_EXISTING,/*文件打开时必须存在*/
FILE_ATTRIBUTE_NORMAL,/*文件属性: 普通*/
NULL);
if (hMailslot == INVALID_HANDLE_VALUE) {
printf("打开失败: %d\n", GetLastError());
return 0;
}
char buff[100];
DWORD dwWrite = 0;
while ( 1 )
{
printf("请输入要发送的内容: ");
scanf_s("%s", buff, sizeof(buff));
WriteFile(hMailslot, buff, strlen(buff) + 1, &dwWrite, 0);
}
}

线程同步

  1. 若编写多线程程序,那么多个线程是并发执行,可以认为它们是在同时执行代码。但线程之间并非完全没有关系,很多时候会有以下两种关系:

    1. 线程 A 的继续执行,要以线程 B 完成某一操作之后为前提,这种需求称为同步
    2. 多个线程在争抢同一个资源,如全局变量,文件,数据结构,对象等,这种需求称为同步互斥
  2. 解决互斥问题

    1. 原子操作:适合去解决共享资源是全局变量的互斥问题,作用就是对于一个变量的基本算术运算保证是原子性的

      函数 作用 备注
      InterlockedIncrement 自增 InterlockedIncrement(&g_count)
      InterlockedDecrement 自减 InterlockedDecrement(&g_count);
      InterlockedExchangeAdd 加法/减法 InterlockedExchangeAdd(&g_count, 256L);
      InterlockedExchange 赋值 InterlockedExchange(&g_count, 256L);
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      24
      long g_n = 0;
      DWORD WINAPI ThreadPro1(LPVOID lparam) {
      for (int i = 0; i < 100000; i++)
      //g_n++;
      InterlockedIncrement(&g_n);
      return 0;
      }
      DWORD WINAPI ThreadPro2(LPVOID lparam) {
      for (int i = 0; i < 100000; i++)
      //g_n--;
      InterlockedDecrement(&g_n);
      return 0;
      }
      int main()
      {
      HANDLE hthread1, hthread2;
      hthread1 = CreateThread(NULL, NULL, ThreadPro1, NULL, NULL, NULL);
      hthread2 = CreateThread(NULL, NULL, ThreadPro2, NULL, NULL, NULL);
      WaitForSingleObject(hthread1, -1);
      WaitForSingleObject(hthread2, -1);
      printf("%d\n", g_n);
      getchar();
      return 0;
      }
  1. 原子操作仅仅能解决某一个变量问题,只能使得一个整型数据做简单算数运算时是原子的,但大部分时候其实是希望保护一段代码,使得这一段代码是原子操作,而非是某一个变量的操作,使用临界区恰能解决这个问题

    被保护的代码(代码访问了共享资源)放置在

函數 作用
InitializeCriticalSection 初始化
DeleteCriticalSection 销毁
EnterCriticalSection 进入临界区
LeaveCriticalSection 离开临界区
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
#include<stdio.h>
#include<windows.h>
//临界区
CRITICAL_SECTION g_crit;
long g_n = 0;
DWORD WINAPI ThreadPro1(LPVOID lparam) {
for (int i = 0; i < 100000; i++) {
EnterCriticalSection(&g_crit);
g_n++;
LeaveCriticalSection(&g_crit);
}
return 0;
}
DWORD WINAPI ThreadPro2(LPVOID lparam) {
for (int i = 0; i < 100000; i++) {
EnterCriticalSection(&g_crit);//进入临界区
g_n--;
LeaveCriticalSection(&g_crit);//离开临界区
}
return 0;
}
int main()
{
//初始化临界区
InitializeCriticalSection(&g_crit);
HANDLE hthread1, hthread2;
hthread1 = CreateThread(NULL, NULL, ThreadPro1, NULL, NULL, NULL);
hthread2 = CreateThread(NULL, NULL, ThreadPro2, NULL, NULL, NULL);
WaitForSingleObject(hthread1, -1);
WaitForSingleObject(hthread2, -1);
printf("%d\n", g_n);
DeleteCriticalSection(&g_crit);//销毁临界区
getchar();
return 0;
}

异步 IO

  1. 将同步 IO 模式改为异步 IO 的模式

    打开文件时加上重叠 IO 的标志:FILE_FLAG_OVERLAPPED

    普通的文件读取:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    int main()
    {
    //打开文件
    HANDLE hFile = CreateFile(
    L"异步IO.cpp",
    GENERIC_READ,//读写方式,只读
    FILE_SHARE_READ,//共享方式,共享读
    NULL,//安全描述符
    OPEN_EXISTING,//创建标志,存在时才打开
    FILE_ATTRIBUTE_NORMAL,//文件属性和标志,正常属性
    NULL
    );
    //获取一个文件的大小
    DWORD dwSize = GetFileSize(hFile, NULL);
    //读取文件
    char *p = new char[dwSize];
    DWORD dwRealSize = 0;
    ReadFile(hFile, p, dwSize, &dwRealSize, NULL);
    //关闭句柄
    CloseHandle(hFile);
    }

    异步 IO 方式:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    //打开文件
    HANDLE hFile = INVALID_HANDLE_VALUE;
    hFile = CreateFile(
    L"异步IO.cpp",
    GENERIC_READ,//读写方式,只读
    FILE_SHARE_READ,//共享方式,共享读
    NULL,//安全描述符
    OPEN_EXISTING,//创建标志,存在时才打开
    //文件属性和标志,正常属性|重叠IO标志
    FILE_ATTRIBUTE_NORMAL | FILE_FLAG_OVERLAPPED,
    NULL);
    //获取一个文件的大小
    DWORD dwSize = GetFileSize(hFile, NULL);
    //读取文件
    char *p = new char[dwSize];
    DWORD dwRealSize = 0;
    //异步IO函数返回时IO没有结束,所以原来的一些参数无效,如读取或写入多少个字节,文件指针也没用,因为同一时刻可能会有多个IO在进行
    OVERLAPPED al = {0};
    ReadFile(hFile, p, dwSize, NULL, &al);//调用ReadFile函数后,IO并未结束,还可以继续做其他事
    //.......
    WaitForSingleObject(hFile, -1);
    //关闭句柄
    CloseHandle(hFile);

    以重叠方式打开的句柄拥有以下特性:

    1. 句柄变成了可等待的状态

      • 有信号:当有一个 IO 任务被完成,就变成有信号
      • 无信号:默认无信号
    2. 句柄不能使用文件读写位置

      • 不能使用 SetFilePointer 来设置文件的读写位置
      • 调用 ReadFileWriteFile 时,也不会使用文件读写位置来定义读写的文件内容
      • 只能使用 OVERLAPPED 的结构体来指定文件的读写位置
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    typedef struct _OVERLAPPED {
    ULONG_PTR Internal;// [输出]保存IO任务的错误码
    ULONG_PTR InternalHigh;//[输出]保存IO任务的完成的字节数
    union {
    struct {
    DWORD Offset;//[输入]用于指定文件读写位置的低32位
    DWORD OffsetHigh;//[输入]用于指定文件读写位置的高32位
    } DUMMYSTRUCTNAME;
    PVOID Pointer;
    } DUMMYUNIONNAME;
    HANDLE hEvent;//[输入]用于提供任务完成通知事件对象
    } OVERLAPPED, *LPOVERLAPPED;
  1. 投递 IO 任务
    1. 通过 ReadFile 来投递一个读取的 IO 任务
    2. 通过 WriteFile 来投递一个写入的 IO 任务

获取 IO 完成通知

  1. 根据文件句柄的信号来判断

    缺点:若一个文件同时存在多个 IO 任务,只要其中一个任务完成了,文件句柄就变成有信号状态,无法判断是哪个 IO 被完成了

  2. 使用事件对象信号状态来判断

    1. 每个 IO 任务都有一个重叠 IO 结构体来配置信息
    2. 每个 IO 任务都会配有一个事件对象,哪个事件对象被设置成有信号了,就说明哪个 IO 任务完成了
    3. 要等待事件对象的信号时,需要创建线程等待

    缺点:若 IO 任务很多,就说明要等待的事件也很多,就需要创建大量线程等待,造成效率低下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
#include<windows.h>
#include<stdio.h>
struct MyOverlapped : public OVERLAPPED {
MyOverlapped() {
//将OVERLAPPED部分填充为0
memset(this, 0, sizeof(OVERLAPPED));
pBuff = nullptr;
}
char* pBuff;
~MyOverlapped() {
if (pBuff)
delete pBuff;
}
};
DWORD WINAPI ThreadProc(VOID* pArg) {//创建进程
MyOverlapped* pOv = (MyOverlapped*)pArg;
WaitForSingleObject(pOv->hEvent, -1);
printf("读取偏移:[%d]的IO任务完成,实际读取到的字节数:%d"
"读取到的内容是:%s",
pOv->Offset, pOv->InternalHigh, pOv->pBuff);
return 0;
}
int main()
{
HANDLE hFile = INVALID_HANDLE_VALUE;
hFile = CreateFile(
L"p3.cpp",
GENERIC_READ, //读写方式,只读
FILE_SHARE_READ,//共享方式,共享读
NULL, //安全描述符
OPEN_EXISTING, //创建标志,存在时才打开
//文件属性和标志,正常属性|重叠IO标志
FILE_ATTRIBUTE_NORMAL | FILE_FLAG_OVERLAPPED,
NULL);
MyOverlapped* ov = new MyOverlapped;
ov->Offset = 0;//表示从第20个字节开始读
ov->pBuff = new char[200];//缓冲区大小200字节
memset(ov->pBuff, 0, 200);
DWORD read = 0;
ov->hEvent = CreateEvent(NULL,//安全描述符
FALSE,//是否需要手动重置信号
FALSE,//初始是否有信号
NULL//事件对象名(一般为了跨进程使用)
);
ReadFile(hFile, ov->pBuff, 100, &read, ov);

MyOverlapped* ov2 = new MyOverlapped;
ov2->Offset = 100;
ov2->hEvent = CreateEvent(NULL, FALSE, FALSE, NULL);
ov2->pBuff = new char[200];
ReadFile(hFile, ov2->pBuff, 100, &read, ov2);

CreateThread(0, 0, &ThreadProc, ov, 0, 0);
CreateThread(0, 0, &ThreadProc, ov2, 0, 0);
while (1) {
Sleep(100);
}
}
  1. 使用扩展版的 API:

    • ReadFileEXWriteFileEx
    • 扩展版的函数能接收一个完成函数的回调函数
    • 当 IO 任务被系统底层处理完毕后,这个回调函数就会被系统插入到线程的 APC 队列中
    • 当线程被挂起(睡眠)时,挂起前,根据可警醒状态是否为真,来决定调用 APC 队列中的函数

    缺点:若在程序中没调用 Sleep 或其他等待函数,或调用了但没设置可警醒状态,那么即使 IO 任务完成了,回调函数也不会被调用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
#include <iostream>
#include <windows.h>
#include <stdio.h>
struct MyOverlapped : public OVERLAPPED {
MyOverlapped()
{
// 将OVERLAPPED部分填充为0
memset(this, 0, sizeof(OVERLAPPED));
pBuff = NULL;
}
char *pBuff;
~MyOverlapped() {
if (pBuff) {
delete pBuff;
}
}
};
VOID WINAPI ioProc(
_In_ DWORD dwErrorCode,/*错误码,*/
_In_ DWORD dwNumberOfBytesTransfered,/*实际完成的字节数*/
_Inout_ LPOVERLAPPED lpOverlapped/*重叠结构体,传入到ReadFile那个*/
)
{
MyOverlapped* pOv = (MyOverlapped*)lpOverlapped;
printf("读取偏移:[%d]的IO任务完成, 实际读取到的字节数:%d"
"读取到的内容是:%s",
pOv->Offset,
pOv->InternalHigh,
pOv->pBuff);
}

int main()
{
HANDLE hFile = INVALID_HANDLE_VALUE;
hFile = CreateFile(
L"p2.cpp",
GENERIC_READ,/*读写方式:只读*/
FILE_SHARE_READ,/*共享方式: 共享读*/
NULL,/*安全描述符*/
OPEN_EXISTING,/*创建标志:存在时才打开*/
FILE_ATTRIBUTE_NORMAL | FILE_FLAG_OVERLAPPED,/*文件属性和标志:正常属性|重叠IO标志 */
NULL);
MyOverlapped* ov = new MyOverlapped;
ov->Offset = 0;
ov->pBuff = new char[100];
memset(ov->pBuff, 0, 100);
ov->hEvent = NULL;
ReadFileEx(hFile, ov->pBuff, 50, ov, ioProc);
while (1)
{
SleepEx(30, TRUE);
}
}
  1. 通过完成端口来等待 IO 任务
    • 创建异步 IO 方式的文件对象
    • 创建一个完成端口对象
    • 将完成端口和文件对象进行绑定
    • 创建指定个数线程,在线程监视完成端口和完成列表的状态,通过 API GetQueuedCompletionStatus 来获取完成列表的状态
    • 发起异步 IO 任务
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
#include <windows.h>
#include <stdio.h>
struct MyOverlapped : public OVERLAPPED {
MyOverlapped()
{
// 将OVERLAPPED部分填充为0
memset(this, 0, sizeof(OVERLAPPED));
pBuff = NULL;
}
char *pBuff;
~MyOverlapped() {
if (pBuff) {
delete pBuff;
}
}
};
DWORD WINAPI threadProc(LPVOID pArg)
{
HANDLE hIoComp = (HANDLE)pArg;
DWORD dwBytes = 0;
ULONG_PTR completionKey = 0;
MyOverlapped* pOv = NULL;
BOOL ret = 0;
while (1)
{
ret = GetQueuedCompletionStatus(
hIoComp,
&dwBytes,/*实际完成的字节数*/
&completionKey,/*和文件设备关联在一起的完成键*/
(OVERLAPPED**)&pOv,/*通过ReadFile投递IO任务时传递重叠结构*/
-1/*超时时间*/);
if (ret == FALSE) {
continue;
}
printf("读取偏移:[%d]的IO任务完成, 实际读取到的字节数:%d"
"读取到的内容是:%s",
pOv->Offset,
pOv->InternalHigh,
pOv->pBuff);
delete pOv;
}
return 0;
}
int main()
{
// 1. 创建异步IO方式的文件对象
HANDLE hFile = INVALID_HANDLE_VALUE;
hFile = CreateFile(
L"p2.cpp",
GENERIC_READ,/*读写方式:只读*/
FILE_SHARE_READ,/*共享方式: 共享读*/
NULL,/*安全描述符*/
OPEN_EXISTING,/*创建标志:存在时才打开*/
FILE_ATTRIBUTE_NORMAL | FILE_FLAG_OVERLAPPED,/*文件属性和标志:正常属性|重叠IO标志 */
NULL);
// 2. 创建一个完成端口对象
SYSTEM_INFO si = { 0 };
GetSystemInfo(&si);
HANDLE hIoComp = CreateIoCompletionPort(
INVALID_HANDLE_VALUE,
NULL,
NULL,
si.dwNumberOfProcessors/*处理器个数*/);
// 3. 将完成端口和文件对象进行绑定
CreateIoCompletionPort(
hFile,/*要和完成端口进行绑定的文件句柄*/
hIoComp,/*绑定到哪个完成端口上*/
0,/*完成键, 和文件句柄关联的值*/
0);
// 4. 创建指定个数线程, 在线程监视完成端口 完成列表的状态
for (int i = 0; i < si.dwNumberOfProcessors; ++i) {
CreateThread(0, 0, &threadProc, hIoComp, 0, 0);
}
DWORD read = 0;
// 5. 发起异步IO任务.
for (int i = 0; i < 100; ++i) {
MyOverlapped* pOv = new MyOverlapped;
pOv->pBuff = new char[50];
memset(pOv->pBuff, 0, 50);
pOv->Offset = i * 40;
ReadFile(hFile, pOv->pBuff, 40, &read, pOv);
}
while (1)
{
Sleep(1000);
}
}

事件对象

  1. 内核对象

  2. 通过 CreateEvent 来创建

    1. 可配置手动使用或自动使用
    2. 可配置初始化是否有信号
    3. 通过 SetEvent 将事件设置为有信号
    4. 通过 ResetEvent 重置事件对象的信号(设置为无信号)
  3. 通过 WaitForSingleObject 来等待信号

    WaitForSingleObject 还会进一步修改等待事件对象的信号,从无信号开始阻塞等待,直到事件对象有信号后就从阻塞状态返回,返回前会顺手将事件对象设置为无信号