You need to enable JavaScript to run this app.
最新活动
大模型
产品
解决方案
定价
生态与合作
支持与服务
开发者
了解我们

Windows平台下C++裁判系统开发:进程运行、CPU时间与内存消耗测量的问题求助

问题分析与解决方案

你在Windows下用C++开发裁判系统时遇到的两个核心问题:一是首次尝试中进程提前终止导致计时/内存测量异常,二是第二次用GetQueuedCompletionStatus时无法正确设置时间限制,本质上都是进程生命周期管理顺序错误资源监控时机不对导致的。

先拆解你两次尝试的问题:

第一次尝试的核心错误

  • WaitForSingleObject用了INFINITE参数,完全失去了时间限制能力;而且循环判断STILL_ACTIVE是多余的——WaitForSingleObject只有在进程结束或等待失败时才会返回,不会返回STILL_ACTIVE
  • 启动std::async监控内存的时机过早:此时进程还处于挂起状态,getMaxMemoryUsage可能获取不到有效数据,甚至因为进程未运行导致逻辑异常。
  • 未使用Job Object:虽然被测代码没有创建子进程,但Job Object是Windows下监控/管控进程最可靠的方式,能避免很多边界问题。

第二次尝试的核心错误

  • 执行顺序完全颠倒:在ResumeThread后立刻调用feature.get(),会阻塞主线程直到内存监控任务完成,但此时被测进程可能还在运行,直接导致后续的GetQueuedCompletionStatus失去意义。
  • 过早关闭了进程句柄:CloseHandle(ProcessInformation.hProcess)在等待进程结束前执行,会导致后续无法获取进程状态或终止进程。
  • 未正确处理GetQueuedCompletionStatus的返回值:超时、错误情况都没有处理,导致无法判断是进程正常结束还是超时。

修正后的实现方案

下面是结合Job Object、异步内存监控、带超时等待的完整裁判函数,解决你的时间限制和内存测量问题:

辅助函数补充(完善关键逻辑)

#include <windows.h>
#include <iostream>
#include <future>
#include <chrono>
#include <atomic>

// 获取高精度当前毫秒数
long long getMillisecondsNow() {
    LARGE_INTEGER freq, curr;
    QueryPerformanceFrequency(&freq);
    QueryPerformanceCounter(&curr);
    return (curr.QuadPart * 1000) / freq.QuadPart;
}

// 安全终止进程并清理句柄
void killProcess(PROCESS_INFORMATION& pi) {
    TerminateProcess(pi.hProcess, 1);
    WaitForSingleObject(pi.hProcess, 1000);
    CloseHandle(pi.hThread);
    CloseHandle(pi.hProcess);
}

// 监控进程最大内存使用(带终止信号)
long long getMaxMemoryUsage(HANDLE hProcess, long long memoryLimit, std::atomic<bool>& stopMonitoring) {
    PROCESS_MEMORY_COUNTERS_EX pmc;
    long long maxMem = 0;
    while (!stopMonitoring.load()) {
        if (GetProcessMemoryInfo(hProcess, (PROCESS_MEMORY_COUNTERS*)&pmc, sizeof(pmc))) {
            long long currMem = pmc.WorkingSetSize;
            if (currMem > maxMem) {
                maxMem = currMem;
            }
            // 超过内存限制时提前终止监控
            if (maxMem > memoryLimit) {
                break;
            }
        }
        std::this_thread::sleep_for(std::chrono::milliseconds(10)); // 平衡精度与CPU占用
    }
    return maxMem / 1024; // 转换为KB单位
}

// 重定向输入输出(保留你的原有实现逻辑)
void IORedirection(STARTUPINFO& si, const std::wstring& inputPath, const std::wstring& outputPath) {
    si.dwFlags |= STARTF_USESTDHANDLES;
    si.hStdInput = CreateFile(inputPath.c_str(), GENERIC_READ, FILE_SHARE_READ, nullptr, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, nullptr);
    si.hStdOutput = CreateFile(outputPath.c_str(), GENERIC_WRITE, 0, nullptr, CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL, nullptr);
    si.hStdError = si.hStdOutput; // 错误输出同步到标准输出文件
}

修正后的裁判函数

void myJudgerFixed(std::wstring aName, std::wstring aInputFilePath, std::wstring aOutputFilePath) {
    const long long TIME_LIMIT = 1000; // 时间限制:1000毫秒
    const long long MEMORY_LIMIT = 200 * 1024 * 1024; // 内存限制:200MB(字节)

    PROCESS_INFORMATION pi = {0};
    STARTUPINFO si = {sizeof(STARTUPINFO)};
    IORedirection(si, aInputFilePath, aOutputFilePath);

    // 1. 创建Job Object与IO完成端口,用于可靠监控进程退出
    CHandle hJob(CreateJobObject(nullptr, nullptr));
    if (!hJob) {
        std::cerr << "ERROR: 无法创建Job对象" << std::endl;
        return;
    }

    CHandle hIOCP(CreateIoCompletionPort(INVALID_HANDLE_VALUE, nullptr, 0, 1));
    if (!hIOCP) {
        std::cerr << "ERROR: 无法创建IO完成端口" << std::endl;
        return;
    }

    JOBOBJECT_ASSOCIATE_COMPLETION_PORT portInfo;
    portInfo.CompletionKey = hJob;
    portInfo.CompletionPort = hIOCP;
    if (!SetInformationJobObject(hJob, JobObjectAssociateCompletionPortInformation, &portInfo, sizeof(portInfo))) {
        std::cerr << "ERROR: 无法关联Job与IO完成端口" << std::endl;
        return;
    }

    // 2. 创建挂起状态的子进程
    wchar_t* cmd = const_cast<wchar_t*>(aName.c_str());
    if (!CreateProcess(nullptr, cmd, nullptr, nullptr, TRUE, 
        CREATE_UNICODE_ENVIRONMENT | CREATE_SUSPENDED | CREATE_NO_WINDOW, 
        nullptr, nullptr, &si, &pi)) {
        std::cerr << "ERROR: 无法启动被测进程" << std::endl;
        return;
    }

    // 3. 将进程绑定到Job对象
    if (!AssignProcessToJobObject(hJob, pi.hProcess)) {
        std::cerr << "ERROR: 无法将进程分配到Job" << std::endl;
        killProcess(pi);
        return;
    }

    // 4. 启动异步内存监控任务(带终止信号)
    std::atomic<bool> stopMonitoring(false);
    auto memoryFuture = std::async(std::launch::async, getMaxMemoryUsage, pi.hProcess, MEMORY_LIMIT, std::ref(stopMonitoring));

    // 5. 记录开始时间并恢复进程运行
    long long startTime = getMillisecondsNow();
    ResumeThread(pi.hThread);

    // 6. 等待进程结束或触发超时
    DWORD completionCode = 0;
    ULONG_PTR completionKey = 0;
    LPOVERLAPPED overlapped = nullptr;
    BOOL waitResult = GetQueuedCompletionStatus(hIOCP, &completionCode, &completionKey, &overlapped, TIME_LIMIT);

    long long endTime = getMillisecondsNow();
    long long timeUsage = endTime - startTime;
    bool isTimeout = false;

    if (!waitResult) {
        DWORD err = GetLastError();
        if (err == WAIT_TIMEOUT) {
            std::cout << "结果:超时!" << std::endl;
            isTimeout = true;
            killProcess(pi); // 超时强制终止进程
        } else {
            std::cerr << "ERROR: 等待进程状态失败,错误码:" << err << std::endl;
            killProcess(pi);
        }
    } else {
        // 进程正常结束,获取退出码
        DWORD exitCode = 0;
        GetExitCodeProcess(pi.hProcess, &exitCode);
        std::cout << "结果:进程正常结束,退出码:" << exitCode << std::endl;
        CloseHandle(pi.hThread);
        CloseHandle(pi.hProcess);
    }

    // 7. 停止内存监控并获取结果
    stopMonitoring.store(true);
    long long memoryUsage = memoryFuture.get();

    // 8. 输出最终统计信息
    std::cout << "--------------------" << std::endl;
    std::cout << "时间消耗:" << timeUsage << " ms" << std::endl;
    std::cout << "内存消耗:" << memoryUsage << " KB" << std::endl;
    if (memoryUsage > MEMORY_LIMIT / 1024) {
        std::cout << "警告:内存超过限制!" << std::endl;
    }
}

关键修正点说明

  1. Job Object + IO完成端口:通过Job关联进程,能100%捕获进程的所有退出事件(正常结束、崩溃、被终止等),避免漏判。
  2. 可控的异步内存监控:用std::atomic<bool>作为终止信号,确保内存监控线程能及时停止,不会出现无限阻塞的情况。
  3. 带超时的等待逻辑GetQueuedCompletionStatus的最后一个参数直接设置为时间限制,超时后立刻终止进程并标记结果。
  4. 正确的资源释放顺序:进程句柄在确认进程结束后才关闭,保证能正确获取退出码和内存数据。
  5. 平衡的内存查询频率:每隔10ms查询一次进程工作集,既保证测量精度,又不会占用过多CPU资源。

内容的提问来源于stack exchange,提问作者r_comrad

火山引擎 最新活动