PAYATHON 2026

Windows C++ 如何实现双进程互相守护并避免递归启动?

数据阿明

可以在启动伙伴进程时,通过命令行传递自己的 PID,让新进程连接到已经存在的守护伙伴。连接成功后,使用进程句柄等待对方退出,确认退出后再启动替代进程。

为避免递归启动,需要区分两种情况:首次启动时创建伙伴;带有伙伴信息启动时,连接现有进程。双方还需要各自的单实例限制,防止重复启动形成多组守护进程。

先明确双方的启动规则

可以约定以下命令行参数:

MainApp.exe
    首次启动,创建 Watchdog.exe,并传入自己的 PID。

Watchdog.exe --peer-pid <MainApp_PID>
    连接现有 MainApp.exe,等待它退出。

MainApp.exe --peer-pid <Watchdog_PID>
    由 Watchdog.exe 恢复启动,连接现有 Watchdog.exe。

例如,MainApp.exe 的 PID 是 1234,它应启动:

Watchdog.exe --peer-pid 1234

如果 Watchdog.exe 的 PID 是 5678,在 MainApp.exe 退出后,它应启动:

MainApp.exe --peer-pid 5678

新启动的 MainApp.exe 收到 --peer-pid 后,应连接 PID 为 5678 的 Watchdog.exe 并开始监控,不再执行首次启动时创建 Watchdog.exe 的逻辑。

也可以规定 Watchdog.exe 没有收到合法的 --peer-pid 时直接报告启动错误,避免它独立启动出另一组进程。

使用进程句柄等待退出

原来的 IsProcessAlive() 会把所有 OpenProcess() 失败都解释为进程不存在,但失败也可能是权限不足或参数错误造成的。如果随后立即启动新实例,就容易出现重复启动。

监控已知进程时,可以打开带有 SYNCHRONIZE 权限的句柄,通过 WaitForSingleObject() 等待进程退出。这样无需不断查询退出码,也不用依赖 STILL_ACTIVE 判断进程是否存活。

下面是打开现有伙伴的核心代码:

#include <windows.h>
#include <system_error>

HANDLE OpenPeer(DWORD peerPid) {
    HANDLE peer = OpenProcess(SYNCHRONIZE, FALSE, peerPid);
    if (!peer) {
        const DWORD error = GetLastError();
        throw std::system_error(
            static_cast<int>(error),
            std::system_category(),
            "OpenProcess");
    }
    return peer;
}

返回的句柄需要由调用方关闭。打开失败时,应记录错误并进入明确的恢复流程,不能直接假定需要再启动一个进程。

创建伙伴时直接保留进程句柄

CreateProcessW() 成功后,PROCESS_INFORMATION::hProcess 就是新进程的句柄。创建方可以直接保留它用于监控,无需再按 PID 查找。

下面的函数启动伙伴,同时把当前进程的 PID 传给它:

#include <windows.h>
#include <string>
#include <vector>
#include <system_error>

HANDLE StartPeer(const std::wstring& executablePath) {
    // executablePath 应为目标程序的完整路径。
    std::wstring commandLine =
        L"\"" + executablePath + L"\" --peer-pid " +
        std::to_wstring(GetCurrentProcessId());

    // CreateProcessW 可能修改命令行缓冲区。
    std::vector<wchar_t> buffer(
        commandLine.begin(), commandLine.end());
    buffer.push_back(L'\0');

    STARTUPINFOW startup{};
    startup.cb = sizeof(startup);
    PROCESS_INFORMATION process{};

    if (!CreateProcessW(
            executablePath.c_str(),
            buffer.data(),
            nullptr,
            nullptr,
            FALSE,
            0,
            nullptr,
            nullptr,
            &startup,
            &process)) {
        const DWORD error = GetLastError();
        throw std::system_error(
            static_cast<int>(error),
            std::system_category(),
            "CreateProcessW");
    }

    CloseHandle(process.hThread);
    return process.hProcess;
}

创建方持有新进程的句柄,被创建方通过 --peer-pid 打开创建方的句柄,双方由此建立监控关系。

确认退出后再重启

以下片段展示等待和替换伙伴的核心流程。它假定调用方已经完成单实例检查、参数解析和伙伴连接,且当前线程可以阻塞等待:

// peer 是有效进程句柄,由本段流程负责关闭。
// peerExecutablePath 是伙伴程序的完整路径。
for (;;) {
    DWORD result = WaitForSingleObject(peer, INFINITE);

    if (result != WAIT_OBJECT_0) {
        DWORD error = GetLastError();
        CloseHandle(peer);
        throw std::system_error(
            static_cast<int>(error),
            std::system_category(),
            "WaitForSingleObject");
    }

    // 旧伙伴已退出。
    CloseHandle(peer);
    peer = nullptr;

    // 新伙伴收到当前进程的 PID,连接回来。
    peer = StartPeer(peerExecutablePath);
}

WAIT_OBJECT_0 表示该句柄对应的进程已经退出,不区分正常退出、崩溃或外部终止。代码中的启动失败会抛出异常,需要由上层捕获并记录。

实际使用时还应加入重启间隔和次数限制。否则,伙伴启动后如果立即崩溃,循环重启仍会持续消耗资源。

补上单实例和启动确认

每个程序都应在入口处先执行单实例检查,例如分别持有一个命名 mutex:

Local\MyProduct.MainApp.Instance
Local\MyProduct.Watchdog.Instance

使用 CreateMutexW() 后,应检查创建结果和 ERROR_ALREADY_EXISTS,并在进程运行期间保留自己的 mutex 句柄。名称及命名空间应符合需要保护的范围;Local\ 对应当前会话,不能据此保证跨会话唯一。

启动逻辑可以整理为:

获取当前角色的单实例标记
    已有实例:结束当前这次启动

存在 --peer-pid:
    校验参数并连接现有伙伴
    连接失败:记录错误,进入受控恢复流程

不存在 --peer-pid:
    MainApp:首次创建 Watchdog
    Watchdog:报告缺少伙伴信息并退出

等待伙伴退出
    确认退出后,按重启策略创建替代进程

除了单实例检查,最好通过命名管道或事件做一次启动确认。CreateProcessW() 成功只代表进程已创建,新进程仍可能因为已有实例、参数错误或初始化失败而马上退出。守护方需要识别这些情况,避免把所有启动失败都变成无限重试。

PID 传递还有哪些边界条件?

  • PID 可能被复用。命令行传 PID 适合建立初始连接;成功取得句柄后,应保留句柄进行监控。如果目标在 OpenProcess() 前已经退出,单凭 PID 可能连接到后来复用该 PID 的进程。要求严格配对时,可使用受控的句柄继承,例如通过 STARTUPINFOEX 和 PROC_THREAD_ATTRIBUTE_HANDLE_LIST 仅传递所需进程句柄,并配合启动握手。
  • 正常退出需要协调。如果关闭 MainApp.exe 后也必须恢复,等待进程退出即可触发重启。如果还需要“退出整个应用”,双方必须通过 IPC 约定停止守护,否则退出的一方会被重新拉起。进程退出状态本身无法可靠表达用户意图。
  • 等待退出检测不到卡死。进程仍存活但失去响应时,进程句柄不会变为有信号状态;这类需求需要额外的心跳机制。
  • 双方同时退出时无法互相恢复。如果要求这种情况下也自动恢复,需要第三方监督,例如 Windows 服务或其他外部启动机制。

就当前故障而言,先改正 --peer-pid 的传递和启动分支,再使用持有的进程句柄等待退出,可以消除因每次启动都创建新伙伴而产生的递归链条。单实例检查、启动握手和重启限制则用于处理重复启动与异常恢复中的竞态。

备注:内容仅供参考。