在Windows系统上快速确定进程ID(PID)是否存在的方法是什么?

8
我意识到“快”有点主观,所以我将通过一些上下文来解释。我正在开发一个名为psutil的Python模块,用于以跨平台的方式读取进程信息。其中一个函数是pid_exists(pid)函数,用于确定PID是否在当前进程列表中。
目前,我正在使用显而易见的方法,使用EnumProcesses()来获取进程列表,然后遍历该列表并查找PID。然而,一些简单的基准测试显示,在UNIX平台(Linux、OS X、FreeBSD)上,我们使用kill(pid, 0)与信号0来确定PID是否存在的pid_exists函数要比现在的方法快得多。其他测试显示,几乎所有时间都花费在了EnumProcesses上。

有没有比使用EnumProcesses更快的方法来确定PID是否存在?我尝试了OpenProcess()并检查打开不存在进程时的错误,但这比遍历EnumProcesses列表慢4倍以上,所以也不行。还有其他(更好的)建议吗?

注意:这是一个Python库,旨在避免第三方库依赖项,如pywin32扩展。我需要一个比我们当前代码更快的解决方案,并且不依赖于pywin32或其他在标准Python发行版中不存在的模块。

编辑:为了澄清 - 我们非常清楚读取进程信息存在竞争条件的问题。如果进程在数据收集过程中消失或遇到其他问题,我们会引发异常。pid_exists()函数不旨在替代适当的错误处理。

更新:显然我的早期基准测试存在缺陷 - 我编写了一些简单的C测试应用程序,EnumProcesses始终比OpenProcess(与GetProcessExitCode结合使用,以防PID有效但进程已停止)慢得多,实际上更而不是更慢。


4个回答

8

OpenProcess 可以告诉你而无需枚举所有进程。我不知道速度有多快。

编辑:请注意,即使您从 OpenProcess 获取了句柄,您还需要使用 GetExitCodeProcess 验证进程的状态。


结果表明,尽管我之前进行了测试,但这仍然是更好的选择。如果感兴趣,请查看我的答案了解详情。 - Jay

5

事实证明,我的基准测试显然存在某些缺陷,因为后来的测试表明,与使用EnumProcesses相比,OpenProcess和GetExitCodeProcess要快得多。我不确定发生了什么,但我进行了一些新的测试,并验证了这是更快的解决方案:

int pid_is_running(DWORD pid)
{
    HANDLE hProcess;
    DWORD exitCode;

    //Special case for PID 0 System Idle Process
    if (pid == 0) {
        return 1;
    }

    //skip testing bogus PIDs
    if (pid < 0) {
        return 0;
    }

    hProcess = handle_from_pid(pid);
    if (NULL == hProcess) {
        //invalid parameter means PID isn't in the system
        if (GetLastError() == ERROR_INVALID_PARAMETER) { 
            return 0;
        }

        //some other error with OpenProcess
        return -1;
    }

    if (GetExitCodeProcess(hProcess, &exitCode)) {
        CloseHandle(hProcess);
        return (exitCode == STILL_ACTIVE);
    }

    //error in GetExitCodeProcess()
    CloseHandle(hProcess);
    return -1;
}

请注意,您需要使用GetExitCodeProcess(),因为OpenProcess()会成功地打开已经死亡的进程,所以您不能假设一个有效的进程句柄意味着该进程正在运行。
此外,请注意,OpenProcess()对于任何有效PID相差不超过3的PID也会成功打开(请参见为什么即使我将进程ID加三,OpenProcess仍然成功?)。

谢谢你上次的留言,我之前一直在为一个完全不存在的PID返回true而苦恼。 - Joe Jordan
handle_from_pid()是什么? - orgads

3
我会这样编写Jay的最后一个函数。
int pid_is_running(DWORD pid){
    HANDLE hProcess;
    DWORD exitCode;
    //Special case for PID 0 System Idle Process
    if (pid == 0) {
        return 1;
    }
    //skip testing bogus PIDs
    if (pid < 0) {
        return 0;
    }
    hProcess = handle_from_pid(pid);
    if (NULL == hProcess) {
        //invalid parameter means PID isn't in the system
        if (GetLastError() == ERROR_INVALID_PARAMETER) {
             return 0;
        }
        //some other error with OpenProcess
        return -1;
    }
    DWORD dwRetval = WaitForSingleObject(hProcess, 0);
    CloseHandle(hProcess); // otherwise you'll be losing handles

    switch(dwRetval) {
    case WAIT_OBJECT_0;
        return 0;
    case WAIT_TIMEOUT;
        return 1;
    default:
        return -1;
    }
}

主要区别在于关闭进程句柄(当此函数的客户端运行时间较长时很重要)和进程终止检测策略。WaitForSingleObject提供了等待一段时间的机会(将0更改为函数参数值),直到进程结束。

在这种情况下,我们不想等待(其他函数调用将检测进程是否已关闭并引发异常到Python)。但是,你关于关闭进程句柄的想法是正确的...我们的“真实”代码确实会关闭句柄,但我忘记在我发布的示例中这样做了。 - Jay

3

在使用pid_exists函数时存在固有的竞争条件:当调用程序使用答案时,该进程可能已经消失,或者具有查询ID的新进程可能已经被创建。我敢说,任何使用此函数的应用程序都存在设计缺陷,因此优化此函数不值得努力。


是的,任何类似于ps的应用程序,包括我们的库,都存在固有的竞态条件。然而,这个函数仍然具有有效的用例。请注意,如果在数据收集过程中出现故障,因为该进程已经消失,我们也会引发异常。 - Jay

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接