Win32

前言

什么是WIN32 API

Win32 API是Windows平台上的一组应用程序接口,可以在C/C++等编程语言中使用,用于访问Windows操作系统的各种功能和资源,如文件、窗口、消息等。Win32 API提供了一系列的函数和数据结构,开发人员可以利用这些API实现Windows应用程序的各种功能,如窗口创建、消息处理、文件操作、进程管理等

API存放在哪里

Win32 API函数是通过DLL(动态链接库)实现的。这些DLL文件通常位于Windows系统目录的System32子目录下,例如C:\Windows\System32。每个DLL文件包含一个或多个函数的实现,这些函数可以被其他应用程序或者系统程序调用

通常来讲, C:\Windows\System32中存放的是64位DLL文件,而C:\Windows\SysWOW64中存放的是32位DLL文件

几个重要的DLL

1.kernel32.dll

kernel32.dll是Windows操作系统的核心动态链接库之一,它包含了大量的系统级函数,用于操作系统管理、内存管理、文件I/O、多线程、进程管理等方面。这些函数为开发人员提供了对底层系统资源的访问,使得开发者可以编写高效、可靠的应用程序。一些常见的函数包括CreateFile、ReadFile、WriteFile、GetModuleHandle、GetLastError等等

2.user32.dll

user32.dll是Windows操作系统的用户界面动态链接库,它包含了一系列用于窗口、消息、菜单、对话框、剪贴板等用户界面组件的函数。这些函数可以帮助开发人员实现各种视觉元素、控件、交互效果等等,提高了应用程序的交互性和易用性。一些常见的函数包括CreateWindowEx、SendMessage、SetWindowText、GetWindowRect等等

3.gdi32.dll

gdi32.dll是Windows操作系统的图形设备接口动态链接库,它包含了一系列用于绘图和图像处理的函数

什么是快照

在Win32系统编程中,快照是指一个静态的系统信息副本。在某些情况下,我们需要获取系统某个时刻的状态信息,如当前运行的进程和线程,这时候就可以使用快照来获取。通过获取快照,我们可以得到系统的静态信息,这样就可以对这些信息进行遍历和查询。同时,快照也可以在某些情况下提高系统资源的利用率,因为它不需要实时地获取系统信息。而且,快照在一定程度上也可以减少对系统的干扰,因为获取快照不需要打断正在执行的进程和线程

什么是回调函数

回调函数是一种函数指针,它作为参数传递给另一个函数,并在该函数执行过程中被调用。

例如,假设有一个函数doSomething(),该函数需要在某个事件发生时执行一些特定的操作。可以将一个回调函数作为参数传递给doSomething()函数,以便在事件发生时调用该回调函数

具体来说,假设回调函数的名称为callback(),那么可以定义一个函数指针类型,用于存储指向callback()函数的指针,如下所示:

typedef void (*CallbackFunction)(int arg);

然后,在doSomething()函数中,可以将一个指向callback()函数的指针作为参数传递进去,如下代码所示:

void doSomething(CallbackFunction callback, int arg) {
    // 在某个事件发生时调用回调函数
    if (/* 事件发生 */) {
        callback(arg);
    }
}

什么是同步读取和异步读取

同步读取是指调用线程会等待读取操作完成后才能继续执行,即读取操作是同步阻塞的。在同步读取中,读取操作完成后,数据被复制到缓冲区,并立即返回给调用线程。这种读取方式简单易用,但是它会阻塞调用线程,降低应用程序的并发性和响应性。

异步读取是指调用线程不会阻塞等待读取操作的完成,而是立即返回,并允许调用线程继续执行其他任务。在异步读取中,读取操作不会立即返回,而是在后台执行,并在完成后通知调用线程。这种读取方式可以提高应用程序的并发性和响应性,但是它需要更复杂的编程模型和更高的系统资源消耗

字符串编码

ASCII编码

ASCII编码是一种最早出现的字符编码方案,它是由美国标准化协会(ASA)于1963年制定的标准,用于在计算机系统中表示英语文本字符集。ASCII编码仅使用7位二进制数(共128个),用于表示英文字母、数字、标点符号以及一些控制字符,例如换行、回车、制表符等。这个编码方案中,每个字符都被分配一个唯一的编号,称为ASCII码值。

由于ASCII编码仅支持128个字符,因此它无法表示其他语言(如汉语、日语等)所需的字符。随着计算机技术和国际化的发展,ASCII编码已经逐渐被更强大的Unicode编码所取代,但ASCII编码仍然是计算机系统中最基本和最常用的字符编码方式之一,对于英语文本处理仍然有广泛的应用

下述表格为ASCII码对照表:

GB2312编码(ASCII码扩展)

GB2312是中华人民共和国发布的一种字符集标准,于1980年发布。它包含了简体中文中常用的6763个汉字以及包括拉丁字母、数字、标点符号等在内的682个字符,共计7445个字符。GB2312采用双字节编码,每个字符用两个字节来表示。该字符集广泛用于中文操作系统、应用程序和互联网应用中

GB2312编码的出现,使得计算机可以更加方便地处理和显示汉字,被广泛应用于中国大陆的计算机系统和应用软件中。但随着汉字数量和应用领域的不断扩大,GB2312编码逐渐无法满足需求,于是在其基础上发展出了GBK编码和GB18030编码,后者已经成为中国大陆计算机系统和应用软件中的主要字符集编码方案

GBK编码

GBK编码是汉字编码的一种,其全称是“汉字内码扩展规范”,由中国国家标准GB2312编码基础之上扩展而来。GBK编码是双字节编码,使用两个字节表示一个中文字符,因此一个GBK编码的字符占用两个字节的存储空间,但是对于英文字母、数字和符号等ASCII字符,仍然使用一个字节来表示。GBK编码支持简体中文和繁体中文,它的编码范围包括了GB2312编码的全部汉字和符号,以及收录了香港繁体中文、台湾繁体中文等汉字,共收录了21003个汉字和符号

UNICODE编码

Unicode编码是一种国际化的字符集标准,旨在为全世界范围内的所有书写系统提供一个统一的编码方案。它包含了世界各种语言所使用的所有字符,包括汉字、拉丁字母、希腊字母、西里尔字母、希伯来字母、阿拉伯字母等在内的超过14万个字符。

Unicode的编码范围是:0~0x10FFFF

要注意的是Unicode只是一个符号集, 它只规定了符号的二进制代码, 并没有规定这个二进制代码该如何存储。若要实现存储, UTF-8和UTF-16则是Unicode字符集的具体实现方式之一

UTF-16

UTF-16是Unicode编码中的一种字符编码方式,它使用16位(即2个字节)来表示每个字符。与UTF-8不同,UTF-16中的每个字符都使用相同数量的字节来表示,因此每个字符的长度都是固定的

UTF-16编码有两种存储方式:UTF-16 LE(Little Endian)和UTF-16 BE(Big Endian)。在UTF-16 LE编码中,低序字节存储在内存的低地址处,高序字节存储在内存的高地址处,这符合小端存储的规则;而在UTF-16 BE编码中,高序字节存储在内存的低地址处,低序字节存储在内存的高地址处,这符合大端存储的规则

要注意的是, 16位只是一个单位, 不代表一个字符只有16位, 具体要看此字符的unicode编码处于什么范围, 有可能此字符占2个字节, 也有可能占4个字节

UTF-8

UTF-8是一种Unicode字符集的变长字符编码方式,它可以用1-4个字节来表示Unicode字符集中的所有字符

UTF-8编码使用可变长度的编码方式,它的编码规则如下(与utf-16编码范围对比):

BOM

BOM(Byte Order Mark)是一个用于表示文本文件字节序的特殊标记,它通常出现在文本文件的开头处, 可用其来判断文本文件的存储格式

以下是不同编码对应的BOM:

细讲UTF-8编码规则

此处有两个文本文件(utf-16.txt和utf-8.txt),其内容是一样的,均为"测C", 分别使用utf-16 leutf-8存储

首先使用notepad++查看utf-16.txt的16进制内容, 前两个字节是BOM, 可以忽视掉。可以看到字符"测"的编码为"4b 6d", 由于文件采用的是小端存储, 所以其真正编码为"6d 4b"

也就说字符"测"在utf-16的编码范围中, 属于000800 ~ 00FFFF, 对应的utf-8的编码规则为1110xxxx 10xxxxxx 10xxxxxx

查看utf-8.txt的16进制内容, 前三个字节是BOM, 此处字符"测"的utf-8编码为"e6 b5 8b", 将其转换成二进制后即为"1110 0110 1011 0101 1000 1011"

根据utf-8的编码规则(1110xxxx 10xxxxxx 10xxxxxx), 我们取x的值, 最终的结果为0110 1101 0100 1011, 对应的16进制为"6d 4b", 即对应utf-16的编码

UTF-8和UTF-16的区别

1.编码方式

UTF-16采用24个字节来表示每个字符,而UTF-8则采用变长的编码方式,使用14个字节来表示不同的字符。UTF-8中的ASCII字符使用单字节编码,而其他字符则使用多字节编码

2.字节长度

UTF-16中的每个字符使用相同数量的字节来表示,因此每个字符的长度都是固定的;而UTF-8中不同的字符使用不同长度的字节来表示,因此字符的长度是可变的

3.存储方式

UTF-16有两种存储方式:UTF-16LE(Little Endian)和UTF-16BE(Big Endian),它们之间的区别在于字节的存储顺序。而UTF-8没有字节序的问题,因为它是以字节为单位进行编码的,不涉及多字节字符的存储顺序问题

一般来说,UTF-8编码更适合数据传输,而UTF-16编码更适合数据存储

常用数据类型

字符串类型

使用实例如下:

#include <stdio.h>
#include <Windows.h>

int main()
{	
	//char类型和PSTR类型
	char str1[] = "str1";
	PSTR Pstr1 = str1;
	printf("%s\n", Pstr1);
	
	//TCHAR类型和PTSTR类型
	TCHAR str2[] = TEXT("str2");
	PTSTR Pstr2 = str2;
	printf("%s\n", Pstr2);

	//wchar_t类型和PWSTR类型
	wchar_t str3[] = L"str3";
	PWSTR Pstr3 = str3;
	wprintf(L"%s", Pstr3); //宽字符需要使用wprintf函数输出,"L"是宽字符字符串前缀
}

整数类型

指针类型

句柄类型

进程

什么是进程

在计算机操作系统中,进程是正在运行中的程序的实例。进程是操作系统进行资源分配和管理的基本单位,包括内存、文件句柄、系统状态等。每个进程都有自己的独立内存空间和运行状态,因此它们不会互相干扰,也不会互相影响。多个进程可以在操作系统上同时运行,每个进程都在自己的空间里执行自己的代码

进程内存空间的划分

在Windows X86环境下, 进程的内存空间通常被划分为以下三个区域

进程的创建过程

首先有一点要清楚, 任何进程都是别的进程创建的, 而第一个进程是系统启动时由操作系统内核创建的, 它的进程ID为0, 称为系统空闲进程或系统进程

在Windows中,进程的创建可以使用CreateProcess函数,该函数会返回一个指向新进程的句柄

**1.为进程分配内存空间,加载EXE文件, 并将其映射到内存 **

2.创建进程内核对象EPROCESS

EPROCESS是Windows操作系统内核中的一种数据结构,它代表了一个进程对象。EPROCESS结构体包含了进程的许多信息,例如进程的PID、进程的线程列表、虚拟内存映射、访问权限、I/O访问权限等等

3.映射系统dll(ntdll.dll)至内存

4.创建线程内核对象ETHREAD

创建进程的时候系统会默认创建一个线程, 也就是说每个进程都至少有一个线程

5.系统启动线程

线程启动之前, 还需要映射运行可执行文件所需的其他dll文件, 随后线程才开始执行

进程涉及API

CreateProcess

CreateProcess函数用于创建一个新的进程并返回进程句柄, 其原型如下

BOOL CreateProcess(
  LPCTSTR lpApplicationName,  //需要运行的可执行文件名
  LPTSTR lpCommandLine,  //命令行参数字符串
  LPSECURITY_ATTRIBUTES lpProcessAttributes,  //进程的安全属性
  LPSECURITY_ATTRIBUTES lpThreadAttributes,  //线程的安全属性
  BOOL bInheritHandles,  //是否继承父进程的句柄
  DWORD dwCreationFlags,  //进程标志 
  LPVOID lpEnvironment,  //新进程的环境变量,如果为NULL,则将使用当前进程的环境变量
  LPCTSTR lpCurrentDirectory,  //新进程的当前目录
  LPSTARTUPINFO lpStartupInfo,  //指向STARTUPINFO结构的指针,包含了启动进程时的窗口状态和标志等信息
  LPPROCESS_INFORMATION lpProcessInformation  //指向PROCESS_INFORMATION结构的指针,用于返回新进程的进程句柄和主线程句柄
);

如下是PROCESS_INFORMATION结构的成员

typedef struct _PROCESS_INFORMATION {
    HANDLE hProcess;  //进程句柄	
    HANDLE hThread;  //线程句柄
    DWORD dwProcessId;  //进程ID
    DWORD dwThreadId;  //线程ID
}

如下是STARTUPINFO结构的成员

typedef struct _STARTUPINFOW {
    DWORD   cb;   //结构体大小
    LPWSTR  lpReserved;  //保留,置为NULL
    LPWSTR  lpDesktop;  //指定进程的窗口站和桌面名称,或者是一个空字符
    LPWSTR  lpTitle; //指定进程的窗口标题
    DWORD   dwX;  //指定主窗口左上角的初始位置,以屏幕坐标表示
    DWORD   dwY;  
    DWORD   dwXSize;  //指定主窗口的宽度和高度,以像素表示
    DWORD   dwYSize;  
    DWORD   dwXCountChars;   //指定主窗口的字符宽度和字符高度
    DWORD   dwYCountChars;
    DWORD   dwFillAttribute;  //指定用于填充主窗口的初始颜色和属性
    DWORD   dwFlags;  //指定控制进程如何创建的一组标志
    WORD    wShowWindow;  //指定主窗口最初如何显示
    WORD    cbReserved2;  //保留成员
    LPBYTE  lpReserved2;  //保留成员
    HANDLE  hStdInput;  //标准输入设备的句柄
    HANDLE  hStdOutput;  //标准输出设备的句柄
    HANDLE  hStdError;  //标准错误设备的句柄
}

如下代码是一个创建进程的简单实例, 用于启动计算器(calc.exe)

#include <stdio.h>
#include <Windows.h>

//创建子进程函数,传递两个参数,分别是应用程序名字和程序命令行参数,若子进程创建成功则返回子进程句柄
DWORD CreateChildProcess(TCHAR ApplicationName[],TCHAR CommandLine[]=NULL) {
	STARTUPINFO si;  //进程启动信息
	PROCESS_INFORMATION pi;  //进程信息
	ZeroMemory(&si, sizeof(si));  //将结构体si的所有成员都初始化为0
	ZeroMemory(&pi, sizeof(pi));  //将结构体pi的所有成员都初始化为0
	si.cb = sizeof(si);  //结构体大小

	if (!CreateProcess(
		ApplicationName,  //要执行的应用程序名称(包含路径)
		NULL,  //命令行参数
		NULL,  //进程句柄不可被继承
		NULL,  //线程句柄不可被继承
		FALSE,  //不继承句柄
		0,  //标志位为0
		NULL,  //使用父进程的环境变量
		NULL,  //使用父进程的工作目录
		&si,  //传递启动信息
		&pi)  //传递进程信息
		) {
		printf("CreateProcess failed (%d).\n", GetLastError());  //打印错误信息
		return 0;
	}
	
	return (DWORD)pi.hProcess;  //返回进程句柄
	//return (DWORD)pi.dwProcessId;  //返回进程ID

	//释放进程句柄和线程句柄
	CloseHandle(pi.hProcess);
	CloseHandle(pi.hThread);
}


int main()
{
	TCHAR ApplicationName[] = TEXT("E:\\calc.exe");
	DWORD ProcessHandle = CreateChildProcess(ApplicationName);
	return 0;
}

若需要以挂起的形式创建进程, 可将CreateProcess函数的第六个参数设置为CREARE_SUSPEND, 这样创建的进程一开始并不会自动启动线程, 而是需要自己手动执行ResumeThread函数恢复线程后才会启动

#include <Windows.h>
#include <stdio.h>

int main()
{
	STARTUPINFO si;  //进程启动信息
	PROCESS_INFORMATION pi;  //进程信息
	ZeroMemory(&si, sizeof(si));  //将结构体si的所有成员都初始化为0
	ZeroMemory(&pi, sizeof(pi));  //将结构体pi的所有成员都初始化为0
	si.cb = sizeof(si);  //结构体大小

	TCHAR ApplicationName[] = TEXT("E://test.exe");  //test.exe是一个只输出"进程执行"的文件

	if (!CreateProcess(
		ApplicationName,  //要执行的应用程序名称(包含路径)
		NULL,  //命令行参数
		NULL,  //进程句柄不可被继承
		NULL,  //线程句柄不可被继承
		FALSE,  //不继承句柄
		CREATE_SUSPENDED,  //以挂起的形式创建进程
		NULL,  //使用父进程的环境变量
		NULL,  //使用父进程的工作目录
		&si,  //传递启动信息
		&pi)  //传递进程信息
		) {
		printf("CreateProcess failed (%d).\n", GetLastError());  //打印错误信息
		return 0;
	}

	for (int i = 0; i < 5; i++){
		printf("#######\n");
		Sleep(1000);

	}

	ResumeThread(pi.hThread);


	//释放进程句柄和线程句柄
	CloseHandle(pi.hProcess);
	CloseHandle(pi.hThread);
}

如上代码所示, 线程会在for循环打印代码执行结束后才会启动线程, 执行结果如下

OpenProcess

OpenProcess函数用于打开一个已存在的进程对象,以便对该进程执行操作,例如向该进程发送信号或从该进程读取内存。此函数的调用者必须具有足够的权限来打开目标进程

如果函数执行成功,返回打开进程的句柄,否则返回NULL,并可通过调用GetLastError函数获取错误码

要注意的是, 使用CloseHandle函数释放句柄后, 就不能再使用OpenProcess函数来打开这个进程了, 因为CloseHandle函数会将句柄从进程的句柄表中移除,并且在所有引用计数都归零之后释放内存资源

OpenProcess函数的语法如下:

HANDLE OpenProcess(
  DWORD dwDesiredAccess,  // 指定进程的访问权限,可取值为PROCESS_ALL_ACCESS或其他指定的进程访问权限常量
  BOOL bInheritHandle,    // 指定句柄是否可被子进程继承,TRUE表示可继承,FALSE表示不可继承
  DWORD dwProcessId       // 指定要打开的进程的进程ID
);

以下是dwDesireAccess参数的可取值:

  • PROCESS_ALL_ACCESS:具有完全访问权限的进程访问权限。

  • PROCESS_CREATE_PROCESS:允许创建新进程。

  • PROCESS_CREATE_THREAD:允许在进程中创建新线程。

  • PROCESS_DUP_HANDLE:允许进程使用 DuplicateHandle 函数复制句柄。

  • PROCESS_QUERY_INFORMATION:允许查询进程信息,如进程ID、进程优先级等。

  • PROCESS_QUERY_LIMITED_INFORMATION:允许查询受限信息,如进程ID、进程优先级、进程占用内存等。

  • PROCESS_SET_INFORMATION:允许设置进程信息,如进程优先级、进程AffinityMask等。

  • PROCESS_SET_QUOTA:允许设置进程的工作集大小和默认的硬错误模式。

  • PROCESS_SUSPEND_RESUME:允许挂起和恢复进程。

  • PROCESS_TERMINATE:允许终止进程。

  • PROCESS_VM_OPERATION:允许进行虚拟内存操作,如 VirtualAlloc、VirtualProtect 等。

  • PROCESS_VM_READ:允许读取进程的虚拟内存。

  • PROCESS_VM_WRITE:允许写入进程的虚拟内存

TeminateProcess

TerminateProcess函数是Windows操作系统提供的函数之一,用于终止指定进程, 当调用TerminateProcess函数时,会向指定进程发送一个中断信号,强制其终止。

这个过程是非常暴力的,会直接终止进程的所有线程,不会给进程和线程任何清理资源的机会,因此使用该函数需要非常慎重

函数定义如下:

BOOL TerminateProcess(
  HANDLE hProcess,  //进程句柄,用于标识被终止的进程
  UINT   uExitCode  //进程的退出代码,表示进程退出的原因,可以随意填写
);

GetModuleFileName

GetModuleFileName函数用于获取指定模块的完整路径名。通常情况下,可以通过指定NULL作为参数hModule,来获取当前应用程序的完整路径名,该函数的声明如下:

DWORD GetModuleFileName(
  HMODULE hModule,  // 模块句柄,指定NULL表示获取当前应用程序的路径名
  LPTSTR lpFilename,  // 接收完整路径名的缓冲区
  DWORD nSize  // 缓冲区大小
);

GetCurrentDirectory

GetCurrentDirectory函数用于获取当前进程的工作目录。其函数原型为

DWORD GetCurrentDirectory(
  DWORD  nBufferLength,  // 缓冲区大小,单位为字节
  LPTSTR lpBuffer        // 存储路径的缓冲区
);

这个函数的路径是指当前进程的工作目录,而不是当前模块的目录。如果需要获取当前模块的目录,需要使用GetModuleFileName函数来获取模块文件的路径

#include <Windows.h>
#include <stdio.h>

int main()
{	
    //获取当前模块的完整路径
	char str1[256];
	GetModuleFileName(NULL, str1, 256);
	printf("当前应用程序路径名:%s\n", str1);

	//获取当前应用程序的工作目录
	char str2[256];
	GetCurrentDirectory(256, str2);
	printf("当前程序工作目录:%s", str2);

	return 0;
}

GetStartupInfo

GetStartupInfo函数用于检索当前进程的启动信息,它的主要功能是获取STARTUPINFO结构体,其中包含了进程的启动信息,如命令行参数、标准输入输出句柄、窗口显示方式等

调用GetStartupInfo函数需传递一个指向STARTUPINFO类型的指针

int main()
{
// 定义一个 STARTUPINFO 结构体变量 si
STARTUPINFO si;

// 使用 ZeroMemory 函数将 si 清零
ZeroMemory(&si, sizeof(si));

// 设置 si 的 cb 字段
si.cb = sizeof(si);

// 使用 GetStartupInfo 函数获取启动信息
GetStartupInfo(&si);

// 输出当前进程窗口状态
printf("Show window command: %d\n", si.dwFlags);

// 返回 0,表示程序执行成功
return 0;
}

GetCurrentProcessID

GetCurrentProcessID函数是Windows API中的一部分,它返回当前进程的进程ID(Process ID, 其返回值是一个无符号长整型

其语法格式如下:

GetProcessId(
    _In_ HANDLE Process
);

GetCurrentProcess

GetCurrentProcess函数是Windows API提供的一个函数,用于获取当前进程的句柄。该函数没有任何参数,调用后将返回一个类型为HANDLE的句柄,该句柄指向当前进程

GetCurrentProcess 获取当前进程的一个伪句柄GetCurrentProcess 总是返回-1(即0xFFFFFFFF),代表当前进程。这个句柄不在句柄表中,不是真正的句柄,所以叫伪句柄

EnumProcesses

EnumProcesses函数是Windows API中的一个函数,用于列举当前正在运行的进程的ID号,通常用于获取系统中所有进程的ID号列表。若函数执行成功则返回TRUE, 否则返回FLASE, 它的声明如下:

BOOL EnumProcesses(
  DWORD  *pProcessIds, // 接收进程ID的缓冲区
  DWORD  cb,           // 缓冲区大小(以字节为单位)
  DWORD  *pBytesReturned  // 实际写入缓冲区的字节数
);

如果包含了Windows.h头文件, 还提示"EnumProcesses"未定义标识符, 请检查是否正确链接了psapi.lib库文件。

可以通过在Visual Studio中转到“项目”菜单,然后选择“属性”来查看和配置链接器选项。在属性页面的左侧选择“链接器”,然后选择“输入”。在“附加依赖项”字段中添加“Psapi.lib”,然后包含头文件:include "psapi.h"

如下实例枚举当前系统运行的所有进程ID, 并打印至控制台

#include <Windows.h>
#include <stdio.h>
#include "psapi.h"

#define ARRAY_SIZE 1024

int main()
{
	DWORD aProcesses[ARRAY_SIZE], cbNeeded, cProcesses;
	if (!EnumProcesses(aProcesses, sizeof(aProcesses), &cbNeeded))
	{
		printf("EnumProcesses failed: %d\n", GetLastError());
		return 1;
	}

	// 计算枚举到的进程数
	cProcesses = cbNeeded / sizeof(DWORD);

	// 打印进程ID
	for (DWORD i = 0; i < cProcesses; i++)
	{
		if (aProcesses[i] != 0)
		{
			printf("Process ID: %u\n", aProcesses[i]);
		}
	}

	return 0;
}

CreateToolhelp32Snapshot

CreateToolhelp32Snapshot函数是Windows系统提供的一个快照函数,可以获取系统中当前正在运行的进程和线程的快照。该函数可以通过枚举系统中所有进程和线程来帮助实现进程和线程的监控和管理。在调用该函数时,需要指定快照类型,如进程快照、线程快照等。函数会返回一个句柄,该句柄可以作为参数传递给其他Tool Help函数,以获取有关系统中进程和线程的详细信息。在使用完成后,需要调用CloseHandle函数关闭句柄

如下实例使用CreateToolhelp32Snapshot函数枚举系统所有进程:

#include <windows.h>
#include <tlhelp32.h>
#include <tchar.h>
#include <iostream>

int main()
{
    HANDLE hProcessSnap;
    PROCESSENTRY32 pe32;

    // 获取系统进程快照
    hProcessSnap = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0);
    if (hProcessSnap == INVALID_HANDLE_VALUE) {
        std::cout << "CreateToolhelp32Snapshot failed: " << GetLastError() << std::endl;
        return 1;
    }

    // 设置pe32结构体的大小,否则Process32First/Next函数会失败
    pe32.dwSize = sizeof(PROCESSENTRY32);

    // 获取第一个进程的信息
    if (!Process32First(hProcessSnap, &pe32)) {
        std::cout << "Process32First failed: " << GetLastError() << std::endl;
        CloseHandle(hProcessSnap);
        return 1;
    }

    // 遍历进程列表,输出每个进程的PID和名称
    do {
        _tprintf(TEXT("PID=%d, Name=%s\n"), pe32.th32ProcessID, pe32.szExeFile);
    } while (Process32Next(hProcessSnap, &pe32));
	
    // 关闭进程快照句柄
    CloseHandle(hProcessSnap);

    return 0;
}

句柄表

什么是内核对象

在Windows操作系统中,内核对象是由内核负责管理的资源,如进程、线程、文件、互斥体、事件、信号量、共享内存、管道等。

每个内核对象都对应着一个内核对象结构体,内核对象结构体包含了该内核对象的属性、状态、引用计数等信息

什么是句柄表

句柄表(Handle Table)是Windows内核中的一个重要数据结构,用于存储内核对象的句柄(Handle),包括进程、线程、文件、事件、互斥体等。

每个进程都有一个句柄表,这个表存储了当前进程所拥有的所有内核对象的句柄,通过这些句柄可以访问相应的内核对象

如下图所示, 每个进程都在内核区域都会有一个EPROCESS对象(内核进程对象), 在这个进程里执行了四个函数, 分别是CreateProcessCreateThreadCreateEventCreateFile,执行这些函数的同时, 也会在内核区创建相应的对象

EPROCESS结构体有一个成员叫ObjectTable, 此成员指向句柄表, 句柄表存放了每个内核对象的句柄

多进程共享一个内核对象

在Windows操作系统中,不同的进程之间是相互独立的,它们各自拥有独立的虚拟地址空间和资源,进程之间不能直接访问彼此的内存空间。但有时候,不同的进程需要共享某些内核对象(如互斥体、事件等),以便它们能够协同工作。Windows内核提供了一些机制来实现多进程间共享内核对象的需求

在多进程共享一个内核对象时,可以使用内核对象的名字(例如互斥体名字、事件名字等)在不同的进程之间进行传递和共享。当一个进程创建一个具有名字的内核对象时,其他进程可以通过内核对象名字来打开这个对象,从而共享这个对象

句柄继承问题

在 Windows 操作系统中,每个进程都有一张句柄表,记录了该进程所打开的内核对象的句柄。当一个进程创建子进程时,子进程会默认继承父进程的句柄表。也就是说,子进程会复制父进程的句柄表,并与父进程共享同一份句柄表,子进程可以使用父进程打开的内核对象的句柄进行操作。

但并非所有类型的内核对象都可以被子进程继承,例如父进程打开的仅在父进程内有效的句柄,如 GDI 对象、用户对象等,子进程无法继承。另外,父进程可以在创建子进程时通过传递参数指定子进程继承哪些句柄。如果没有指定,则默认情况下,子进程将继承所有可继承的句柄。

如下图所示, 父进程调用了CreateProcess函数创建了一个子进程, 同时在内核区也会创建一个属于子进程的EPROCESS, 其实句柄表还有一列字段用于表示内核对象是否允许被继承, 1表示允许, 0表示不允许。CreateProcess函数有一个参数叫做bInheritHandles, 若此值为True, 则子进程会复制父进程的句柄表

什么是全局句柄表

全局句柄表是一个系统级别的内核对象,用于存储所有的内核对象句柄。在Windows操作系统中,每个进程都有一个独立的句柄表,用于存储它自己的内核对象句柄,而全局句柄表是操作系统内部用于管理所有进程的句柄的数据结构

当一个进程创建一个内核对象时,Windows操作系统会返回一个唯一的内核对象句柄。这个句柄会被存储到创建进程的句柄表中,同时也会被存储到全局句柄表中。其他进程可以通过特定的API函数获取这个句柄,并使用它来访问创建进程的内核对象

全局句柄表的作用在于提供了一种跨进程共享内核对象的方式,使得多个进程可以访问同一个内核对象,从而实现进程间的通信和协作。全局句柄表的管理和维护由操作系统负责,应用程序无法直接访问和修改它

线程

什么是线程

线程是附属在进程上的执行实体, 是代码的执行流程

一个进程可以包含多个线程, 但一个进程至少要包含一个线程

进程与线程的关系

可以将进程比作一个房子,它是一个容器,可以包含很多个线程(居住者)同时工作。线程可以在进程中进行交互和共享资源(房间、厨房等)。与居住在房子里的人一样,线程需要执行某些任务,并且它们可以使用相同的内存和资源来完成它们的工作

线程涉及API

CreateThread

CreateThread函数用于创建一个新线程,该线程在进程空间内独立运行。该函数返回新线程的句柄,以及线程的唯一标识符

在调用该函数后,需要通过 CloseHandle 函数关闭线程句柄,否则会导致资源泄漏

HANDLE CreateThread(
  LPSECURITY_ATTRIBUTES lpThreadAttributes,  //线程安全属性,可设置为 NULL
  SIZE_T dwStackSize,  //新线程的栈大小,若为 0 则使用默认大小
  LPTHREAD_START_ROUTINE lpStartAddress,  //线程函数的地址,即新线程所要执行的函数
  LPVOID lpParameter,  //传递给线程函数的参数
  DWORD dwCreationFlags,  //控制线程创建的标志,如是否立即启动线程等
  LPDWORD lpThreadId  //返回值,指向接收线程标识符的变量
);

线程函数

线程函数是线程执行的代码,它会在调用CreateThread函数创建线程后被调用, 其返回值为DWORD类型, 此值会传递给GetExitCodeThread函数。如果线程函数执行完毕后不返回任何值,则默认返回0

DWORD WINAPI ThreadProc(LPVOID lpParameter)
{
    return 0;
}

以下是一个创建线程并给先传递参数的实例,CreateThread 函数创建了一个新的线程并传递了一个 int 类型的参数 count。新线程的入口点是 ThreadProc 函数,该函数接受一个 LPVOID 类型的参数 lpParam,在这里将其转换为 int* 类型的指针,然后使用该参数进行迭代计数

#include <Windows.h>
#include <stdio.h>

DWORD WINAPI ThreadProc(LPVOID lpParam) {
    int* pCount = (int*)lpParam;
    for (int i = 0; i < *pCount; i++) {
        printf("Thread: %d\n", i + 1);
        Sleep(1000);
    }
    return 0;
}

int main() {
    int count = 5;
    HANDLE hThread;
    DWORD threadId;

    hThread = CreateThread(NULL, 0, ThreadProc, &count, 0, &threadId);

    if (hThread == NULL) {
        printf("Failed to create thread (%d)\n", GetLastError());
        return 1;
    }

    WaitForSingleObject(hThread, INFINITE);

    CloseHandle(hThread);

    return 0;
}

GetExitCodeThread

GetExitCodeThread函数用于获取指定线程的退出代码, 其语法格式如下所示:

BOOL GetExitCodeThread(
  HANDLE  hThread,  //要查询退出代码的线程句柄
  LPDWORD lpExitCode  //指向一个变量的指针,用于接收线程的退出代码
);

此函数的使用实例如下:

#include <Windows.h>
#include <stdio.h>

DWORD WINAPI ThreadProc(LPVOID lpParameter)
{	
	for (int i = 0; i < 5; i++)
	{
		Sleep(500);
		printf("%d\n",i);
	}
	//printf("Hello from new thread!\n");
	return 1;
}

int main()
{	
	DWORD i;  //用于接收线程的退出代码
	hThread = CreateThread(NULL, 0, ThreadProc, NULL, 0, NULL);
	WaitForSingleObject(hThread, INFINITE);
	GetExitCodeThread(hThread, &i);  //获取线程的退出代码
	printf("线程退出代码是:%d", i);
	return 0;
}

线程参数的生命周期

创建线程时需要注意向线程传递的参数的生命周期。一般情况下,线程创建后会立即运行,并且在运行过程中可能需要使用传递进来的参数。如果传递的参数的生命周期比线程短,当线程需要使用参数时,参数可能已经失效了,导致程序出错。因此,一般的做法是将参数拷贝一份给线程,在线程中使用拷贝的参数,确保参数的有效性

假设我们要创建一个线程来打印一个字符串,我们需要将该字符串作为参数传递给线程。但是,如果该字符串是在主线程中声明并初始化的局部变量,那么当主线程完成时,该字符串将被销毁,这可能会导致在线程中引用该字符串时出现问题

为了避免这种情况,可以通过以下方式解决:在主线程中使用动态内存分配函数(例如malloc)为字符串分配内存,将字符串的指针作为参数传递给线程,然后在线程完成后手动释放内存。这样,即使主线程完成并销毁了该字符串,线程仍然可以访问该字符串所在的内存空间。下面是一个示例代码

#include <Windows.h>
#include <stdio.h>

DWORD WINAPI PrintString(LPVOID lpParam)
{
    char* str = (char*)lpParam;
    printf("%s\n", str);
    return 0;
}

int main()
{
    char* str = (char*)malloc(sizeof(char) * 20);
    strcpy_s(str, 20, "Hello, World!");
	
    HANDLE hThread = CreateThread(NULL, 0, PrintString, str, 0, NULL);
    if (hThread == NULL)
    {
        printf("Failed to create thread, error code: %d\n", GetLastError());
        return 1;
    }
	
    WaitForSingleObject(hThread, INFINITE);
	
    free(str);
    CloseHandle(hThread);
	
    return 0;
}

线程控制函数

SuspendThread

SuspendThread函数用于暂停线程, 其语法格式如下

SuspendThread(
    _In_ HANDLE hThread  //线程句柄
);

ResumeThread

ResumeThread函数用于恢复线程, 其语法格式如下:

ResumeThread(
    _In_ HANDLE hThread  //线程句柄
    );

WaitForSingleObject

WaitForSingleObject函数是一个Windows API函数,它可以等待一个指定的内核对象变为可用。它的作用是使当前线程暂停执行,直到指定的内核对象变为有信号(signaled)状态,或者直到超时时间已过。简单来说就是等待指定线程执行结束后当前线程才能恢复执行

其语法格式如下所示:

DWORD WaitForSingleObject(
  HANDLE hHandle,   //句柄
  DWORD  dwMilliseconds  //超时时间
);

WaitForMutipleObjects

与WaitForSingleObjects函数不同的是, WaitForMutipleObjects函数可支持等待多个线程执行结束, 或者等待多个线程中其中一个执行结束

其语法格式如下:

DWORD WaitForMultipleObjects(
  DWORD        nCount,  //等待的句柄数量,即lphandles数组中句柄的个数
  const HANDLE *lpHandles,  //要等待的对象的句柄数组
  BOOL         bWaitAll,  //该值为TRUE时,只有在所有对象都变为可用之后才返回;当该值为FALSE时,只要有一个对象变为可用就返回
  DWORD        dwMilliseconds  //当该值为零时,函数不等待并立即返回。当该值为 INFINITE 时,函数无限期地等待直到句柄数组中有一个对象变为可用,或者等待失败
);

以下是WaitForMutipleObjects函数的使用实例

#include <Windows.h>
#include <stdio.h>

DWORD WINAPI ThreadProc(LPVOID lpParameter)
{	
	for (int i = 0; i < 5; i++)
	{
		Sleep(500);
		printf("%d\n",i);
	}
	return 1;
}

int main()
{	
	DWORD i;
	HANDLE arrThread[2];
	arrThread[0] = CreateThread(NULL, 0, ThreadProc, NULL, 0, NULL);
	arrThread[1] = CreateThread(NULL, 0, ThreadProc, NULL, 0, NULL);
	
	WaitForMultipleObjects(2,arrThread,TRUE,INFINITE);  //等待以上2个线程执行结束
	printf("两个线程执行完毕 \n");
	
	// 关闭线程句柄
	CloseHandle(arrThread[0]);
	CloseHandle(arrThread[1]);

	return 0;
}

线程上下文

什么是线程上下文

线程上下文(Thread Context)是指在一个线程中,当前执行代码的相关信息集合,包括寄存器状态、程序计数器、线程优先级、线程的上下文安全堆栈等等。

要注意的是,若要获取线程上下文信息,需要先将线程挂起。

CONTEXT结构

CONTEXT是Windows API中用于保存线程上下文的结构体,包含了处理器的寄存器、标志和其他与处理器相关的状态信息。

由于CONTEXT结构体成员太多了, 可以通过设置ContextFlags成员的值来获取指定范围的寄存器, 以下是常用的寄存器集的描述:

  • CONTEXT_INTEGER: 包含通用寄存器集(如EAX、EBX等)和指令指针EIP。

  • CONTEXT_CONTROL: 包含指令指针EIP、代码段寄存器CS、栈指针ESP和栈段寄存器SS。

  • CONTEXT_SEGMENTS: 包含数据段寄存器DS、源段寄存器SS、堆栈段寄存器SS和附加段寄存器ES、FS和GS。

  • CONTEXT_FLOATING_POINT: 包含浮点寄存器集。

  • CONTEXT_DEBUG_REGISTERS: 包含调试寄存器集。

可以通过GetThreadContextSetThreadContext函数来获取和设置线程上下文

GetThreadContext用于获取指定线程的上下文信息,调用成功后会将获取到的上下文信息存储在CONTEXT结构体中,其语法格式如下所示:

BOOL GetThreadContext(
    _In_ HANDLE hThread,  //线程句柄
    _Inout_ LPCONTEXT lpContext  //Context结构体指针
    );

SetThreadContext用于设置指定线程的上下文,即线程寄存器和指令指针等信息,其语法结构如下所示:

BOOL SetThreadContext(
    _In_ HANDLE hThread,  //线程句柄
    _In_ CONST CONTEXT* lpContext  //Context结构体指针
);

使用实例

#include <Windows.h>
#include <stdio.h>

DWORD WINAPI ThreadProc(LPVOID lpParameter)
{
	printf("Hello from new thread!\n");
	return 0;
}

int main()
{
	HANDLE hThread = CreateThread(
		NULL,           // 默认安全性描述符
		0,              // 默认堆栈大小
		ThreadProc,     // 线程函数
		NULL,           // 线程参数
		0,              // 立即启动线程
		NULL            // 不返回线程标识符
	);

	if (hThread == NULL)
	{
		printf("Failed to create thread (%d)\n", GetLastError());
		return 1;
	}
	SuspendThread(hThread);  //暂停
	CONTEXT context;  //定义线程上下文结构体
	context.ContextFlags = CONTEXT_INTEGER;  //设置线程上下文的寄存器值为CONTEXT_INTEGER
	GetThreadContext(hThread, &context);  //获取线程上下文
	printf("eax的值为%x", context.Eax);  //输出寄存器eax的值
	
	ResumeThread(hThread);  //恢复线程

	// 等待线程结束
	WaitForSingleObject(hThread, INFINITE);

	// 关闭线程句柄
	CloseHandle(hThread);

	return 0;
}

临界区

什么是临界区

当多个线程同时使用同一个资源(全局变量)时, 很可能会出现某些错误, 这时我们可以将这个资源变成临界资源, 然后通过临界区来使用这个临界资源

临界区(critical section)是操作系统用于同步线程之间访问共享资源的一种机制,是一段被保护的代码区域。同一时刻只允许一个线程进入临界区,其他线程需要等待当前线程退出临界区才能进入执行。

当线程进入临界区时,它会对临界资源进行加锁,这时其他线程无法访问该资源。只有当当前线程完成对临界资源的访问并释放锁后,其他线程才能进入临界区访问临界资源

什么是线程锁

线程锁实际上就是基于临界区实现的,通过在临界区代码块前加锁,在代码块结束后释放锁来保证临界区的互斥性

以下是实现线程锁的代码流程:

1.定义一个临界区

CRITICAL_SECTION cs;

2.初始化临界区

InitializeCriticalSection(&cs)

3.定义临界区范围

EnterCriticalSection(&cs);
	//使用临界资源
LeaveCriticalSection(&cs);

使用实例

以下代码是关于线程锁使用的实例, 线程1的代码执行完毕后线程2才能执行自己的代码

#include <Windows.h>
#include <stdio.h>
#include "psapi.h"

#define ARRAY_SIZE 1024

int Tickets = 10;  //定义一个全局变量,表示票数
CRITICAL_SECTION cs;  //定义临界区

DWORD WINAPI ThreadProc(LPVOID IpParameter) {
	EnterCriticalSection(&cs);  //进入临界区
	while (Tickets>0)
	{
		printf("还有%d张票,", Tickets);
		Tickets--;
		printf("卖出去一张,还剩%d张\n", Tickets);
	}
	LeaveCriticalSection(&cs);  //离开临界区
	return 0;

}

int main()
{	
	InitializeCriticalSection(&cs);  //初始化临界区
	HANDLE arrThread[2];
	arrThread[0] = CreateThread(NULL, 0, ThreadProc, NULL, 0, NULL);  //创建线程1
	arrThread[1] = CreateThread(NULL, 0, ThreadProc, NULL, 0, NULL);  //创建线程2
	WaitForMultipleObjects(2, arrThread, TRUE, INFINITE);
	CloseHandle(arrThread[0]);
	CloseHandle(arrThread[1]);
	return 0;
}

互斥体

什么是互斥体

如下图所示, 互斥体是一种用于控制线程同步的对象, 它能确保同一时刻只有一个线程进入临界区访问临界资源。

互斥体通过两种状态来控制对共享资源的访问, 分别是已锁定(0)和未锁定(1), 当一个线程从互斥体中获取锁(令牌), 其他线程就无法访问共享资源, 只有线程释放了锁(令牌), 其他线程才能继续竞争获取锁

在Windows系统中, 互斥体的实现是一个内核对象, 因此它可以跨进程使用

涉及API

CreateMutex

CreateMutex函数用于创建或者打开一个互斥体对象,其语法格式如下:

HANDLE CreateMutex(
  LPSECURITY_ATTRIBUTES lpMutexAttributes,  //一个指向SECURITY_ATTRIBUTES结构体的指针,用于指定新的互斥对象是否可以被继承
  BOOL                  bInitialOwner,  //指定互斥体对象的初始状态,若为Flase,则创建的互斥体是有信号的(其他进程的等待线程可以使用它);若为True则是无信号的(其他进程的等待线程必须等待互斥体被释放后才能使用它)
  LPCTSTR               lpName
);

ReleaseMutex

ReleaseMutex函数用来释放进程持有的互斥体对象,其语法格式如下:

BOOL ReleaseMutex(
  HANDLE hMutex  //互斥体句柄
);

使用实例

以下代码使用互斥体实现跨进程线程同步, 首先是进程A的代码, 此处为了方便测试, 我利用getchar()来阻塞代码执行(相当于断点), 这样互斥体就没有释放锁

//进程A
#include <iostream>
#include <windows.h>

int main()
{	
	//创建互斥体
	HANDLE hMutex = CreateMutex(NULL,FALSE,TEXT("mutex"));
	
	//获取锁
	WaitForSingleObject(hMutex, INFINITE);

	for (int i = 0; i < 5; i++)
	{
		printf("进程A的x线程\n");
	}

	getchar();
	
	//释放锁
	ReleaseMutex(hMutex);
	
}

下面是进程B的代码, 由于进程A的代码还没有释放锁, 因此进程B的代码无法执行

//进程B
#include <iostream>
#include <windows.h>

int main()
{
	//创建互斥体
	HANDLE hMutex = CreateMutex(NULL, FALSE, TEXT("mutex"));

	//获取锁
	WaitForSingleObject(hMutex, INFINITE);

	for (int i = 0; i < 5; i++)
	{
		printf("进程B的y线程\n");
	}

	//释放锁
	ReleaseMutex(hMutex);

}

互斥体和线程锁的区别

  • 互斥体可以用于跨进程的线程同步,线程锁只能用于同一进程内的线程同步

  • 互斥体可以设置等待超时, 而线程锁不行。当一个线程在执行过程中因为异常情况(例如程序崩溃)而突然终止时,如果它持有了某个共享资源的互斥体,其他线程在等待这个资源时可能会进入无限等待状态,因为该互斥体没有被释放。为了避免这种情况,互斥体通常会在创建时指定一个超时时间,一旦等待时间超过了这个时间,等待线程就会放弃等待并执行其他任务,从而避免了无限等待的情况发生

  • 互斥体的效率没有线程锁的高

事件

什么是事件

事件是一种同步对象,用于线程之间的通信和协调。事件对象有两种状态:有信号状态和无信号状态。当事件对象处于有信号状态时,等待该事件的线程可以被唤醒并继续执行。当事件对象处于无信号状态时,等待该事件的线程将被阻塞,直到事件被信号化为止

通常,一个线程使用 SetEvent 函数将事件对象信号化,而另一个或多个线程使用 WaitForSingleObjectWaitForMultipleObjects 函数等待该事件对象的信号状态

涉及api

CreateEvent

CreateEvent函数用于创建一个事件对象, 事件对象是内核对象的一种,可用于同步进程和线程,或者通知线程事件的发生

其语法如下所示:

HANDLE CreateEvent(
  LPSECURITY_ATTRIBUTES lpEventAttributes,  //安全描述符
  BOOL                  bManualReset,  //指定事件对象的重置类型,TRUE表手动重置,FALSE表自动重置
  BOOL                  bInitialState,  //事件创建出来是否是有信号的,True表示有信号,False表示无信号
  LPCTSTR               lpName   //指定事件的名称
);

关于第二个参数的描述:当事件对象处于非信号状态时,一个线程调用WaitForSingleObject等待该事件;如果事件已经处于信号状态,则WaitForSingleObject返回。在自动重置模式下,当WaitForSingleObject返回时,事件会自动返回到非信号状态。而在手动重置模式下,事件会一直保持在信号状态,直到由调用ResetEvent显式重置

SetEvent

SetEvent函数于设置事件对象为有信号状态, 若事件对象处于无信号状态, 则将其设置为有信号状态, 若事件对象处于有信号状态,则函数不起作用。此函数通常与CreateEvent函数一起使用来实现线程同步, 在线程同步中,此函数的作用就是把自己的线程挂起, 同时唤醒其他线程

其语法格式如下:

BOOL SetEvent(
  HANDLE hEvent  //要设置事件对象的句柄
);

使用实例

这段代码实现了一个生产者-消费者的解决方案,其中两个线程分别为生产者线程和消费者线程,通过共享的仓库(Storage变量)实现数据交互。

生产者线程不断地生产产品,存储到仓库中,并唤醒消费者线程,消费者线程不断地从仓库中消耗产品,并唤醒生产者线程

#include <iostream>
#include <windows.h>
int ProductMax = 10;  //生产数量
int Storage = 0;  //产品仓库,每次只能存储一个产品
HANDLE EventProduct, EventConsume;  //定义事件

//生产者线程
DWORD WINAPI ThreadProduct(LPVOID Parameter) {
	for (int i = 0; i < ProductMax; i++)
	{
		WaitForSingleObject(EventProduct,INFINITE);
		Storage = 1;  //仓库置1
		printf("生产者生产了1个产品\n");
		SetEvent(EventConsume);  //挂起生产者线程,唤醒消费者线程
	}
	return 0;	
}

//消费者线程
DWORD WINAPI ThreadConsume(LPVOID Parameter) {
	for (int i = 0; i < ProductMax; i++)
	{
		WaitForSingleObject(EventConsume, INFINITE);
		Storage = 0;  //仓库置0
		printf("消费者消耗了1个产品\n");
		SetEvent(EventProduct);  //挂起消费者线程,唤醒生产者线程
	}
	return 0;
}

int main()
{	
	//创建事件
	EventProduct = CreateEvent(NULL, FALSE, TRUE, NULL);
	EventConsume = CreateEvent(NULL, FALSE, FALSE, NULL);

	//创建线程
	HANDLE hThread[2];
	hThread[0] = CreateThread(NULL, 0, ThreadProduct, NULL, 0, NULL);
	hThread[1] = CreateThread(NULL, 0, ThreadConsume, NULL, 0, NULL);
	WaitForMultipleObjects(2, hThread, TRUE, INFINITE);
	
	//释放线程
	CloseHandle(hThread[0]);
	CloseHandle(hThread[1]);
}	

由下述执行结果可看出, 两个线程有序的完成了任务, 生产者每生产一个产品, 消费者就消耗一个产品。两个线程交替执行, 而不会出现某个线程同时执行多次

线程互斥和线程同步

  • 线程同步是指协调多个线程的执行顺序,以避免在并发执行时出现不一致的结果或冲突的情况。线程同步可以通过多种机制实现,如互斥锁、信号量、事件等。在线程同步中,通常会存在一些共享资源,多个线程需要协调访问这些共享资源,以避免访问的冲突

  • 线程互斥是一种线程同步机制,用于确保同一时间只有一个线程能够访问共享资源。线程互斥可以使用互斥体、临界区、信号量等机制来实现

Windows应用程序

什么是消息队列

当我们在使用鼠标或键盘时,操作系统会将这些动作转换为一个消息(message),并将其发送到相应的消息队列中。这个消息包含了一些信息,例如动作的类型、坐标、时间戳等等。

在Windows系统中,每个窗口都有一个消息队列,操作系统将接收到的消息按照先后顺序依次存储在该队列中,等待程序读取和处理

每个线程只有一个消息队列,消息队列是属于线程的,每个线程在创建窗口时才会分配一个消息队列,并不是每个线程都有消息队列。当消息被发送到一个窗口时,系统会将消息放入该窗口所属的线程的消息队列中,线程可以通过GetMessage函数从消息队列中获取消息并进行处理。

有一点要注意:一个窗口只能属于一个线程,但一个线程可以拥有多个窗口

什么是消息处理函数

消息处理函数用于处于各种事件消息的函数,也称为窗口过程函数。当一个窗口收到一个事件消息时,Windows操作系统会调用该窗口的消息处理函数来处理该消息

通常其语法形式如下:

LRESULT CALLBACK WndProc(
    HWND hWnd,  //窗口句柄
    UINT message, //消息类型
    
    //wParam和lParam是消息的参数
    WPARAM wParam,  
    LPARAM lParam)

当用户进行鼠标点击等操作时,操作系统会将消息发送到应用程序的消息队列中,然后应用程序的消息循环会处理这些消息,例如将其传递给某个特定窗口的消息处理函数

在处理窗口消息时,通常需要检查消息是由哪个窗口发送的,以便正确地响应消息。一个应用程序可以拥有多个窗口,并且每个窗口都有自己的消息处理函数

什么是消息循环

消息循环会不断地从系统队列中获取消息,然后将消息传递给相应的消息处理函数进行处理。在Windows操作系统中,消息循环通常是由函数GetMessage和DispatchMessage进行实现的

以下代码是Visual Studio提供的默认消息函数:

while (GetMessage(&msg, nullptr, 0, 0))
	{
		if (!TranslateAccelerator(msg.hwnd, hAccelTable, &msg))
		{
			TranslateMessage(&msg);  //翻译消息
			DispatchMessage(&msg);  //分发消息
		}
	}

常用API

WinMain函数

WinMain是Windows程序的入口函数, WinMain的返回值是一个整数,表示程序的退出状态码。在WinMain函数内部,我们可以创建窗口、初始化程序并执行消息循环等操作

它的语法格式如下:

int WINAPI WinMain(
HINSTANCE hInstance,  //当前模块的句柄	
HINSTANCE hPrevInstance,  //废弃,置NULL
LPSTR lpCmdLine, //命令行参数,包含应用程序启动时传递的所有命令行参数
int nCmdShow  //窗口显示方式,指定应用程序最初如何显示
);

CreateWindow

CreateWindow函数是Windows API中用于创建一个窗口的函数,返回一个HWND类型的窗口句柄,该句柄可以用于操作该窗口,如显示、隐藏、移动、调整大小等

每当使用CreateWindow创建窗口时,操作系统会在内核生成一个窗口对象,该窗口对象包含了窗口的状态信息(如窗口大小, 位置, 标题), 同时也包含了窗口的过程函数指针,用于处理窗口的消息

其语法格式如下:

HWND CreateWindow(
  LPCWSTR   lpClassName,  //指定窗口类名