背景
Windows 中经常遇到需要遍历所有进程的诉求,Windows 中有多种方式可以遍历进程。
下面将对常规和不常规的方式做一个汇总。
效果
先看对比效果:
如下图是四个方式遍历一次当前所有进程所花费的时长。可以看到 native 的速度遥遥领先。
那接下来就看看,这几种方式是怎么做的。
方法一:EnumProcesses
EnumProcesses 是最常用到的,也是最直接的方式了。
示例
DWORD aProcesses[1024] = { 0 }, cbNeeded, cProcesses;
EnumProcesses(aProcesses, sizeof(aProcesses), &cbNeeded);
注意
- EnumProcesses 使用中,无法提前预测当前系统中存在多少个进程,所以通常采取一个大数组的方式来获取。
- 如果想获取总共多少个进程,可以通过
cbNeeded/sizeof(DWORD)
来获取。 - 如果
cbNeeded/sizeof(DWORD)
与数组大小一致,可能是因为缓冲区不够,建议将缓冲区增大后再次枚举 - 只包含进程ID,如果要获取其他信息,需要先通过
OpenProcess
获取进程句柄来获取 - 枚举速度中偏上
方法二:CreateToolhelp32Snapshot
还有一种常见的方式,就是通过 CreateToolhelp32Snapshot
拍快照,再通过 Process32Next
来枚举进程的方式。
示例
void EnumProcessBySnap(std::vector<DWORD>& pids) {
HANDLE hProcessSnap = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0);
if (hProcessSnap == INVALID_HANDLE_VALUE) {
std::cerr << "CreateToolhelp32Snapshot (of processes) failed: " << GetLastError() << std::endl;
return;
}
PROCESSENTRY32 pe32;
pe32.dwSize = sizeof(PROCESSENTRY32);
// Retrieve information about the first process
if (!Process32First(hProcessSnap, &pe32)) {
std::cerr << "Process32First failed: " << GetLastError() << std::endl; // show cause of failure
CloseHandle(hProcessSnap); // clean the snapshot object
return;
}
do
{
pids.push_back(pe32.th32ProcessID);
} while (Process32Next(hProcessSnap, &pe32));
}
注意
- 快照中除了包含进程ID,还包含线程、父进程和优先级信息,所以占用的内存会相对比较大。需要结合自己的诉求
- 枚举速度最慢
- 占用的内存最大
方法三:NtQuerySystemInformation
通过 NtQuerySystemInformation
+ SystemProcessInformation
可以获取到包含 SYSTEM_PROCESS_INFORMATION
结构的数组。
示例
void EnumProcessByQueryInformation(std::vector<DWORD>& pids) {
DWORD dwSize = 0xFFFFF;
LPTSTR pBuf = new TCHAR[dwSize];
PSYSTEM_PROCESS_INFORMATION pInfo = { 0 };
pInfo = (PSYSTEM_PROCESS_INFORMATION)pBuf;
auto status = NtQuerySystemInformation(SystemProcessInformation, pInfo, dwSize, &dwSize);
if (!NT_SUCCESS(status)) {
std::cerr << "NtQuerySystemInformation failed: " << status << std::endl;
return;
}
while (pInfo->NextEntryOffset)
{
pids.push_back(HandleToULong(pInfo->UniqueProcessId));
pInfo = (PSYSTEM_PROCESS_INFORMATION)(((PUCHAR)pInfo) + pInfo->NextEntryOffset);
}
}
注意
- 跟
EnumProcesses
一样,缓冲区需要采用大数组来存储,缓冲区不够的时候,会报错 - 包含的信息较多,具体看
SYSTEM_PROCESS_INFORMATION
结构 - 由于其调用链较短,在获取足够数据的情况下,速度较快
- 占用内存较多
方法四:NtGetNextProcess
此方式是一个导出但未文档化的接口,可以从 或者 phnt 中找到对应的接口。
示例
void EnumProcess(std::vector<DWORD>& pids) {
HANDLE hProcess = nullptr;
HANDLE nextProcess = NULL;
while (NT_SUCCESS(NtGetNextProcess(hProcess, PROCESS_QUERY_INFORMATION, 0, 0, &nextProcess))) {
auto pid = GetProcessId(nextProcess);
pids.push_back(pid);
hProcess = nextProcess;
}
}
注意
- 使用此方式,请确保与运行的系统是兼容的。可以看对应版本的 ntoskrnl.exe 有无导出此接口
- 其返回的进程句柄,可以用来获取你想要的信息
- 此方法直接读取内核对象的链表,所以跟用户态拿到的进程列表存在出入。0(System Idle Process)、4(System)、332(Secure system)、374(Registry)等进程无法获取,还有部分进程内核进程对象存在,但用户态进程不一定存在,需要额外进行处理。请谨慎使用。
- 获取速度最快(比其他方式快了近10倍)
总结
不同的方式可以适应不同的诉求,各有利弊,需要了解详细后再选择使用。
0