/ Windows / 140浏览

Windows 中遍历进程

背景

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倍)

总结

不同的方式可以适应不同的诉求,各有利弊,需要了解详细后再选择使用。

【译】ETW 堆跟踪 – 每个分配都被记录
【译】ETW 堆跟踪 – 每个分配都被记录
【译】Wait Analysis – 寻找空闲时间
【译】Wait Analysis – 寻找空闲时间
如何通过 ETW Provider 来记录应用日志
如何通过 ETW Provider 来记录应用日志
如何通过 C++ 实时监听 ETW 事件
如何通过 C++ 实时监听 ETW 事件
【译】调查并确定 Windows 运行速度变慢问题
【译】调查并确定 Windows 运行速度变慢问题
【译】丢失的 WPA 文档 —— 磁盘使用
【译】丢失的 WPA 文档 —— 磁盘使用

0

  1. This post has no comment yet

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注