第二部分,工作机制

第4章 进程

概述:进程是应用程序的一个运行实例,由内核对象和虚拟地址空间组成。Windows通过进程来隔离应用程序,每个进程拥有独立的地址空间和资源。理解进程的创建、终止以及进程间的差异是Windows系统编程的基础。

什么是进程

进程是应用程序的一个运行实例,它由两部分组成:一个是进程内核对象(操作系统用来管理进程的数据结构),另一个是虚拟地址空间(包含可执行代码、数据、DLL和动态分配的内存)。进程本身并不执行代码,它只是线程的容器,线程才是CPU调度的基本单位。每个进程至少有一个主线程,可以拥有多个线程。进程为线程提供资源上下文,包括虚拟地址空间、句柄表、环境变量和当前目录等。

//进程相关的基本API函数

//获取当前进程的伪句柄(始终为-1,即0xFFFFFFFF)
HANDLE GetCurrentProcess();

//获取当前进程的真实句柄(可以被子进程继承)
HANDLE GetCurrentProcessId();

//获取当前进程ID
DWORD GetCurrentProcessId();

//获取指定进程ID对应的进程句柄
HANDLE OpenProcess(
DWORD dwDesiredAccess, //访问权限
BOOL bInheritHandle, //是否可继承
DWORD dwProcessId //目标进程ID
);

//进程信息结构
typedef struct _PROCESS_INFORMATION {
HANDLE hProcess; //进程内核对象句柄
HANDLE hThread; //主线程内核对象句柄
DWORD dwProcessId; //进程ID
DWORD dwThreadId; //主线程ID
} PROCESS_INFORMATION, *LPPROCESS_INFORMATION;

//启动信息结构
typedef struct _STARTUPINFO {
DWORD cb; //结构体大小,必须初始化为sizeof(STARTUPINFO)
LPTSTR lpReserved;
LPTSTR lpDesktop; //桌面名称
LPTSTR lpTitle; //控制台窗口标题
DWORD dwX; //窗口位置X
DWORD dwY; //窗口位置Y
DWORD dwXSize; //窗口宽度
DWORD dwYSize; //窗口高度
DWORD dwXCountChars; //控制台宽度(字符数)
DWORD dwYCountChars; //控制台高度(字符数)
DWORD dwFillAttribute; //控制台文本颜色
DWORD dwFlags; //标志位
WORD wShowWindow; //窗口显示方式
WORD cbReserved2;
LPBYTE lpReserved2;
HANDLE hStdInput; //标准输入句柄
HANDLE hStdOutput; //标准输出句柄
HANDLE hStdError; //标准错误句柄
} STARTUPINFO, *LPSTARTUPINFO;

创建进程:CreateProcess函数

CreateProcess是Windows中创建新进程的核心函数。它执行时会依次完成以下操作:创建进程内核对象、创建虚拟地址空间、将可执行文件和DLL映射到地址空间、创建主线程内核对象、启动C/C++运行时启动代码,最终调用主线程的入口点函数。CreateProcess有10个参数,提供了精细的控制能力,包括安全性、继承性、创建标志、环境变量、工作目录和启动信息等。

//CreateProcess函数原型
BOOL CreateProcess(
LPCTSTR lpApplicationName, //可执行文件路径(通常为NULL)
LPTSTR lpCommandLine, //命令行字符串(可包含路径和参数)
LPSECURITY_ATTRIBUTES lpProcessAttributes, //进程安全属性
LPSECURITY_ATTRIBUTES lpThreadAttributes, //主线程安全属性
BOOL bInheritHandles, //是否继承父进程的可继承句柄
DWORD dwCreationFlags, //创建标志
LPVOID lpEnvironment, //环境变量块(NULL表示继承父进程)
LPCTSTR lpCurrentDirectory, //当前目录(NULL表示继承父进程)
LPSTARTUPINFO lpStartupInfo, //启动信息
LPPROCESS_INFORMATION lpProcessInformation //返回进程信息
);

//创建进程示例
BOOL CreateNewProcess(LPCTSTR pszAppName, LPCTSTR pszCmdLine) {
STARTUPINFO si = { sizeof(si) }; //必须初始化cb成员
PROCESS_INFORMATION pi;

//创建进程
BOOL bSuccess = CreateProcess(
pszAppName, //通常为NULL,让系统从命令行解析
pszCmdLine, //命令行,必须是可写的缓冲区(非const)
NULL, //默认进程安全属性
NULL, //默认线程安全属性
FALSE, //不继承句柄
0, //默认创建标志
NULL, //继承父进程环境
NULL, //继承父进程当前目录
&si, //启动信息
&pi //接收进程信息
);

if (bSuccess) {
//关闭句柄(不关闭会导致内核对象泄漏)
//注意:关闭句柄不会终止进程,只是减少使用计数
CloseHandle(pi.hThread);
CloseHandle(pi.hProcess);
return TRUE;
}

DWORD dwError = GetLastError();
return FALSE;
}

/*
重要提示:
1. lpCommandLine必须是可写的缓冲区,因为CreateProcessW可能会修改它
2. 如果lpApplicationName为NULL,系统按以下顺序搜索可执行文件:
- 主调进程.exe所在目录
- 主调进程的当前目录
- Windows系统目录(System32)
- Windows目录
- PATH环境变量中的目录
3. 如果指定了lpApplicationName,则不会搜索,只在当前目录查找
4. 文件名如果没有扩展名,默认添加.exe
*/

//使用示例:启动记事本打开文件
void RunNotepad() {
TCHAR szCmdLine[] = TEXT("notepad.exe C:\\test.txt");
CreateNewProcess(NULL, szCmdLine);
}

进程创建标志和优先级类

dwCreationFlags参数控制进程的创建方式和初始状态。可以组合使用多个标志(用OR运算),还可以指定优先级类。常用的创建标志包括:CREATE_SUSPENDED(挂起主线程,允许父进程修改子进程内存后再运行)、CREATE_NEW_CONSOLE(为新进程创建新控制台窗口)、DETACHED_PROCESS(阻止CUI进程访问父进程控制台)、CREATE_UNICODE_ENVIRONMENT(环境块使用Unicode)等。优先级类决定了进程内线程的基准调度优先级。

//常用的进程创建标志
#define DEBUG_PROCESS 0x00000001 //调试子进程及其所有子进程
#define DEBUG_ONLY_THIS_PROCESS 0x00000002 //只调试直接子进程
#define CREATE_SUSPENDED 0x00000004 //创建后挂起主线程
#define DETACHED_PROCESS 0x00000008 //脱离父进程控制台
#define CREATE_NEW_CONSOLE 0x00000010 //创建新控制台窗口
#define CREATE_NEW_PROCESS_GROUP 0x00000200 //新进程组(影响Ctrl+C处理)
#define CREATE_UNICODE_ENVIRONMENT 0x00000400 //环境块使用Unicode
#define CREATE_SEPARATE_WOW_VDM 0x00000800 //16位程序在单独VDM中运行
#define CREATE_SHARED_WOW_VDM 0x00001000 //16位程序在共享VDM中运行
#define CREATE_FORCEDOS 0x00002000 //强制运行MS-DOS程序
#define CREATE_DEFAULT_ERROR_MODE 0x04000000 //不继承父进程错误模式
#define CREATE_NO_WINDOW 0x08000000 //不创建控制台窗口(CUI程序)

//优先级类(与创建标志组合使用)
#define IDLE_PRIORITY_CLASS 0x00000040 //空闲(4)
#define BELOW_NORMAL_PRIORITY_CLASS 0x00004000 //低于正常(6)
#define NORMAL_PRIORITY_CLASS 0x00000020 //正常(8,默认)
#define ABOVE_NORMAL_PRIORITY_CLASS 0x00008000 //高于正常(10)
#define HIGH_PRIORITY_CLASS 0x00000080 //高(13)
#define REALTIME_PRIORITY_CLASS 0x00000100 //实时(24)

//创建挂起状态的进程示例
BOOL CreateSuspendedProcess(LPCTSTR pszCmdLine, LPPROCESS_INFORMATION ppi) {
STARTUPINFO si = { sizeof(si) };

BOOL bSuccess = CreateProcess(
NULL,
pszCmdLine,
NULL, NULL, FALSE,
CREATE_SUSPENDED | NORMAL_PRIORITY_CLASS, //挂起创建
NULL, NULL, &si, ppi
);

if (bSuccess) {
//此时可以修改子进程地址空间,如注入DLL、修改内存等
//完成后恢复线程执行
ResumeThread(ppi->hThread);
}

return bSuccess;
}

//创建独立控制台的进程
void CreateProcessWithNewConsole() {
STARTUPINFO si = { sizeof(si) };
PROCESS_INFORMATION pi;
TCHAR szCmdLine[] = TEXT("cmd.exe /k echo Hello");

CreateProcess(NULL, szCmdLine, NULL, NULL, FALSE,
CREATE_NEW_CONSOLE, //新控制台
NULL, NULL, &si, &pi);

CloseHandle(pi.hThread);
CloseHandle(pi.hProcess);
}

进程实例句柄和命令行参数

每个加载到进程地址空间的可执行文件或DLL都被赋予一个唯一的实例句柄(HINSTANCE或HMODULE)。对于可执行文件,实例句柄实际上就是该模块在虚拟地址空间中的基地址。WinMain函数的hInstance参数就是进程主执行文件的实例句柄。GetModuleHandle函数可以获取已加载模块的句柄。命令行参数通过WinMain的lpCmdLine或GetCommandLine函数获取,需要手动解析或使用CommandLineToArgvW转换为参数数组。

//实例句柄相关函数

//获取应用程序实例句柄(WinMain参数中的hInstance)
HINSTANCE GetModuleHandle(
LPCTSTR lpModuleName //模块名(NULL表示获取主执行文件句柄)
);

//获取命令行字符串
LPTSTR GetCommandLine();

//将命令行解析为参数数组(Unicode版本)
LPWSTR* CommandLineToArgvW(
LPCWSTR lpCmdLine, //命令行字符串
int* pNumArgs //接收参数个数
);

//使用示例
void ProcessCommandLine() {
//获取命令行字符串
LPTSTR lpCmdLine = GetCommandLine();

//解析参数
int nArgs;
LPWSTR* szArglist = CommandLineToArgvW(GetCommandLineW(), &nArgs);

if (szArglist != NULL) {
for (int i = 0; i < nArgs; i++) {
wprintf(L"参数 %d: %s\n", i, szArglist[i]);
}
//必须释放分配的内存
LocalFree(szArglist);
}
}

//获取模块基地址示例
void GetModuleInfo() {
//获取主执行文件基地址
HMODULE hMod = GetModuleHandle(NULL);
printf("主模块基地址: 0x%p\n", hMod);

//获取特定DLL基地址
HMODULE hKernel32 = GetModuleHandle(TEXT("kernel32.dll"));
printf("kernel32.dll基地址: 0x%p\n", hKernel32);

//获取模块文件名
TCHAR szPath[MAX_PATH];
GetModuleFileName(hMod, szPath, MAX_PATH);
printf("模块路径: %s\n", szPath);
}

//WinMain函数原型(GUI程序入口)
int WINAPI WinMain(
HINSTANCE hInstance, //当前应用程序实例句柄
HINSTANCE hPrevInstance, //16位Windows遗留,始终为NULL
LPSTR lpCmdLine, //命令行参数字符串(不含程序名)
int nCmdShow //窗口初始显示方式
);

//wWinMain(Unicode版本)
int WINAPI wWinMain(
HINSTANCE hInstance,
HINSTANCE hPrevInstance,
LPWSTR lpCmdLine,
int nCmdShow
);

终止进程

进程有四种终止方式:主线程入口函数返回(最优雅的方式)、进程中的线程调用ExitProcess(应避免)、另一个进程调用TerminateProcess(强制终止,应避免)、以及所有线程自然终止(几乎不会发生)。主线程返回时,C/C++运行时库会清理全局对象并调用ExitProcess。ExitProcess会终止整个进程,包括所有线程,并设置退出代码。TerminateProcess是异步的,被终止的进程无法执行清理操作,可能导致资源泄漏或数据损坏。

//进程终止相关函数

//终止当前进程(不会返回)
VOID ExitProcess(
UINT uExitCode //退出代码(0表示成功,非0表示错误)
);

//终止指定进程(异步,强制终止)
BOOL TerminateProcess(
HANDLE hProcess, //目标进程句柄(需要PROCESS_TERMINATE权限)
UINT uExitCode //退出代码
);

//获取进程退出代码
BOOL GetExitCodeProcess(
HANDLE hProcess, //进程句柄
LPDWORD lpExitCode //接收退出代码(STILL_ACTIVE表示仍在运行)
);

//示例:优雅地终止当前进程
void ExitGracefully(int nExitCode) {
//执行必要的清理工作
//保存配置、关闭文件、释放资源等

//调用ExitProcess(实际上主线程返回时会自动调用)
ExitProcess(nExitCode);
}

//示例:等待进程终止并获取退出代码
DWORD WaitForProcessExit(HANDLE hProcess, DWORD dwTimeout) {
//等待进程终止
DWORD dwResult = WaitForSingleObject(hProcess, dwTimeout);

if (dwResult == WAIT_OBJECT_0) {
DWORD dwExitCode;
if (GetExitCodeProcess(hProcess, &dwExitCode)) {
return dwExitCode;
}
}

return (DWORD)-1;
}

//示例:强制终止进程(谨慎使用)
BOOL ForceTerminateProcess(DWORD dwProcessId) {
//打开进程获取句柄
HANDLE hProcess = OpenProcess(PROCESS_TERMINATE, FALSE, dwProcessId);
if (hProcess == NULL) {
return FALSE;
}

//强制终止
BOOL bResult = TerminateProcess(hProcess, 1); //退出代码1表示异常终止

//关闭句柄
CloseHandle(hProcess);

return bResult;
}

/*
重要提示:
1. 主线程返回是终止进程的最佳方式,允许运行时库正确清理
2. ExitProcess会立即终止进程,不会执行局部对象的析构函数
3. TerminateProcess是最后的手段,被终止进程无法保存数据或释放资源
4. 进程终止时,系统会:关闭所有对象句柄、终止所有线程、变为信号状态
5. 进程退出代码可以通过GetExitCodeProcess查询,STILL_ACTIVE(259)表示仍在运行
*/

子进程与父进程的关系

通过CreateProcess创建的进程之间存在父子关系,但这种关系仅限于创建时刻。子进程会继承父进程的某些属性:环境变量、当前目录、控制台(除非指定DETACHED_PROCESS或CREATE_NEW_CONSOLE)、以及错误模式等。父进程可以通过PROCESS_INFORMATION结构获取子进程的句柄和ID,从而监控或控制子进程。但子进程没有内置机制获取父进程信息,Toolhelp函数可以查询进程列表和父子关系,但进程ID可能被重用,不能依赖父子关系进行长期通信。

//进程枚举和关系查询相关函数

//创建Toolhelp32快照
HANDLE CreateToolhelp32Snapshot(
DWORD dwFlags, //快照包含的内容(TH32CS_SNAPPROCESS等)
DWORD th32ProcessID //进程ID(0表示当前进程)
);

//进程条目结构
typedef struct tagPROCESSENTRY32 {
DWORD dwSize; //结构体大小
DWORD cntUsage; //使用计数(已废弃)
DWORD th32ProcessID; //进程ID
ULONG_PTR th32DefaultHeapID; //默认堆ID
DWORD th32ModuleID; //模块ID(已废弃)
DWORD cntThreads; //线程数
DWORD th32ParentProcessID; //父进程ID(创建时的父进程)
LONG pcPriClassBase; //优先级类基准
DWORD dwFlags; //标志
TCHAR szExeFile[MAX_PATH]; //可执行文件名
} PROCESSENTRY32, *LPPROCESSENTRY32;

//枚举进程
BOOL Process32First(HANDLE hSnapshot, LPPROCESSENTRY32 lppe);
BOOL Process32Next(HANDLE hSnapshot, LPPROCESSENTRY32 lppe);

//示例:枚举所有进程并显示父子关系
void EnumerateProcesses() {
HANDLE hSnapshot = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0);
if (hSnapshot == INVALID_HANDLE_VALUE) {
return;
}

PROCESSENTRY32 pe;
pe.dwSize = sizeof(pe);

if (Process32First(hSnapshot, &pe)) {
do {
printf("进程名: %s\n", pe.szExeFile);
printf(" PID: %u\n", pe.th32ProcessID);
printf(" 父PID: %u\n", pe.th32ParentProcessID);
printf(" 线程数: %u\n\n", pe.cntThreads);
} while (Process32Next(hSnapshot, &pe));
}

CloseHandle(hSnapshot);
}

//示例:获取指定进程的父进程ID(注意:PID可能被重用)
DWORD GetParentProcessId(DWORD dwProcessId) {
DWORD dwParentId = 0;
HANDLE hSnapshot = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0);

if (hSnapshot != INVALID_HANDLE_VALUE) {
PROCESSENTRY32 pe;
pe.dwSize = sizeof(pe);

if (Process32First(hSnapshot, &pe)) {
do {
if (pe.th32ProcessID == dwProcessId) {
dwParentId = pe.th32ParentProcessID;
break;
}
} while (Process32Next(hSnapshot, &pe));
}

CloseHandle(hSnapshot);
}

return dwParentId;
}

//示例:等待子进程初始化完成
void WaitForChildProcessReady(LPCTSTR pszCmdLine) {
STARTUPINFO si = { sizeof(si) };
PROCESS_INFORMATION pi;

if (CreateProcess(NULL, pszCmdLine, NULL, NULL, FALSE, 0,
NULL, NULL, &si, &pi)) {
//等待进程完成初始化并等待用户输入(适用于GUI程序)
WaitForInputIdle(pi.hProcess, INFINITE);

//现在子进程的主窗口已创建,可以与之交互
//例如:查找窗口、发送消息等

CloseHandle(pi.hThread);
CloseHandle(pi.hProcess);
}
}

总结:

进程是Windows系统中程序运行的基本隔离单元,由内核对象和虚拟地址空间组成。CreateProcess函数提供了创建新进程的完整功能,包括安全性控制、句柄继承、环境设置和启动状态控制。进程实例句柄标识了加载到地址空间中的可执行模块,命令行参数需要正确解析。终止进程时应优先让主线程正常返回,避免使用强制终止。父子进程关系仅在创建时存在,不应依赖其进行长期通信。理解进程的生命周期和管理机制是进行Windows系统编程和进程间通信的基础。