본문 바로가기

3.구현/VC++

프로세스의 CPU 사용량 측정하기

CPU의 전체 사용량 혹은 특정 프로세스의 CPU 사용량을 측정하는 방법입니다. 만약 이글을 읽은 사람 중에서 .NET을 사용한다면 더 쉬운 방법이 있으니 다른 글[2]을 참조하길 바랍니다.

여기서 사용하는 방법은 레지스트를 통한 HKEY_PERFORMANCE_DATA 키에 값을 가져오는 방법입니다. 실제 regedit로 해서 HKEY_PERFORMANCE_DATA를 조회할 수는 없습니다. 조회할 수 있다면 좋을 텐데... 여하튼, 보이지 않는 레지스트리 키입니다. 그래서 더 어렵습니다. HKEY_PERFORMANCE_DATA에 있는 값 들을 통해서 성능에 관련된 정보를 획득할 수 있습니다. HKEY_PERFORMANCE_DATA를 통해서 값을 가져온다고 해도 원하는 값을 얻는데는 쉽지가 않습니다. 왜 MS에서는 이렇게 복잡하게 했을까요? 물론 무한 확장가능한 형태라고 볼 수 있지만, 사용하기 복잡하고 여러 단계를 걸처 접근하는 방법이라서 CPU 계산량도 무시할 수 없습니다.

이에 대한 자세한 내용은

어찌되었든 복잡한 HKEY_PERFORMANCE_DATA 레지스트리 세계로 가볼까요.

작성일: 2009.10.06 (http://ospace.tistory.com/), ospace114@엠팔.컴

특정 프로세스 사용량 얻기

여기서 정의되는 함수는 다음과 같습니다.

LPBYTE GetPerformanceData(LPTSTR src);
int GetCounterValue(const int& objId, const int& counterId, const char* instanceName,
                              PERF_DATA_BLOCK **dataBlock, LONGLONG &value);
double GetCpuUsage(const char* process);

정 의된 함수는 총 3개 입니다. 실제 사용하는 함수는 GetCpuUsage()입니다. 다른 함수는 통계 정보를 얻기 위해서 사용합니다. 간단하게 사용하는 법을 보겠습니다. 자세한 코드는 다음에서 보도록 하죠. 아래코드는 Idle 프로세스의 CPU 사용량을 표시하는 예제입니다. Idle는 사용되고 있지 않는 CPU 사용량입니다.

while(true) {
    const char proc[] = "Idle";
    printf("%s processor: %.2f%%\r", proc, GetCpuUsage(proc));
    Sleep(2000);
}

While문을 반복적으로 구동하면서 해당 프로세스의 CPU량을 표시하고 있습니다. 참고로 GetCpuUsage()에서 반환되는 CPU 사용량 단위는 Percentage(%)입니다.

GetCpuUsage() 함수

그럼 먼저 GetCpuUsage() 함수 부터 보자. 여기서 우리가 원하는 CPU 사용량을 얻을 수 있다. 첫번째 인자는 프로세서 명으로 앞의 예제에서 "Idle"이라 할 수 있다.

GetCouterValue() 에 의해서 HKEY_PERFORMANCE_DATA 키에 있는 원하는 카운터 객체(PERF_COUNTER_BLOCK)를 얻을 수 있고, 그곳에서 원하는 값을 얻을 수 있다. 이렇게 얻은 값을 통해서 CPU사용율을 계산할 수 있다.

double GetCpuUsage(const char* process)
{
    const int CPU_INDEX = 230; // Perf: Process
    const int CPU_COUNTER = 6;  

    static bool isCpuFirst = true;
    static LARGE_INTEGER oldCpuTime_100n = {0};
    static LONGLONG oldCpuVal = 0;

    LONGLONG newVal = 0;
    LARGE_INTEGER cpuTime_100n = {0};
    double cpuUsage = 0.0;

    PPERF_DATA_BLOCK cpuData = NULL;
    // Process 객체(CPU_INDEX)에서 해당 process 이름을 갖는 프로세스의 CPU 값을 얻는다.
    if( GetCounterValue(CPU_INDEX, CPU_COUNTER, process, &cpuData, newVal) < 0)
        return -1;
    // 획득한 성능 정보를 이용해서 CPU 사용율을 계산
    cpuTime_100n = cpuData->PerfTime100nSec;

    if( isCpuFirst ){
        isCpuFirst = false;
        oldCpuVal = newVal;
        oldCpuTime_100n = cpuTime_100n;
    } else {

        SYSTEM_INFO sysinfo;
        GetSystemInfo( &sysinfo );

        LONGLONG delta = newVal - oldCpuVal;
        double deltaTime = (double)cpuTime_100n.QuadPart - (double)oldCpuTime_100n.QuadPart;

        oldCpuVal = newVal;
        oldCpuTime_100n = cpuTime_100n;

        double a = (double)delta / deltaTime;

        cpuUsage = (a*100) / sysinfo.dwNumberOfProcessors; // 으히아싸님이 지적해주신 부분
    }

    if( NULL != cpuData ){
        free(cpuData);
        cpuData = NULL;
    }

    return cpuUsage;
}

여기까지 특별한 것이 없다. 코드 이해하기도 쉬울 것이다.

GetCounterValue() 함수

다 음은 GetCouterValue() 함수이다. 이 함수는 HKEY_PERFORMANCE_DATA 키에서 원하는 카운터 객체를 찾고 거기에서 값을 얻는 것이다. 사용되는 인자가 좀 많다. 첫번재 인자는 찾는 객체 ID이고 두 번째 인자는 찾고자하는 카운터 객체 ID이고 세 번째 인자는 찾고자하는 인스턴스 이름이다. 네 번째 인자는 성능 데이터 값(PERF_DATA_BLOCK)이 저장되고 다섯 번째는 카운트 블럭(PERF_COUNTER_BLOCK)의 값이 저장된다. 앞의 세개 인자는 입력 인자이다. 나머지 두개 인자는 출력 인자이다. 반환값은 에러가 발생시 -1이 반환되고 성공이면 0이 반환된다.

int GetCounterValue(const int& objId, const int& counterId, const char* instanceName, PERF_DATA_BLOCK **dataBlock, LONGLONG &value)
{
    TCHAR key[256];
    sprintf(key,TEXT("%d"), objId);

    PPERF_DATA_BLOCK   perfDataBlock = NULL;
    PPERF_OBJECT_TYPE perfObj = NULL;

    PPERF_COUNTER_DEFINITION perfCounterDef = NULL;
    PPERF_COUNTER_BLOCK perfCounterBlock = NULL;
    PPERF_INSTANCE_DEFINITION perfInstanceDef = NULL;

    perfDataBlock = (PPERF_DATA_BLOCK)GetPerformanceData(key); // process object

    if( NULL == perfDataBlock ) return -1;

    *dataBlock = perfDataBlock;

    perfObj = (PPERF_OBJECT_TYPE)((PBYTE)perfDataBlock + perfDataBlock->HeaderLength); // first object

    for(DWORD i = 0; i < perfDataBlock->NumObjectTypes; ++i) {
        if( perfObj->ObjectNameTitleIndex == objId) {
            perfCounterDef = (PPERF_COUNTER_DEFINITION)((PBYTE)perfObj + perfObj->HeaderLength); // first counter definition

            for( DWORD j=0; j < perfObj->NumCounters; ++j ){
                if(perfCounterDef->CounterNameTitleIndex == counterId) break;

                perfCounterDef = (PPERF_COUNTER_DEFINITION)((PBYTE)perfCounterDef + perfCounterDef->ByteLength); // next counter definition
            }

            if( perfObj->NumInstances == PERF_NO_INSTANCES ) {
                perfCounterBlock = (PPERF_COUNTER_BLOCK)((PBYTE)perfObj + perfObj->DefinitionLength);
            } else {
                perfInstanceDef = (PPERF_INSTANCE_DEFINITION)((PBYTE)perfObj + perfObj->DefinitionLength); // first instance

                _bstr_t bstrInst;
                _bstr_t bstrInInst = instanceName;
                for(int k=0; k < perfObj->NumInstances; ++k) {
                    bstrInst = (wchar_t*)((PBYTE)perfInstanceDef + perfInstanceDef->NameOffset);
                    if(!strcmp((LPCTSTR)bstrInst, (LPCSTR)bstrInInst)) {
                        perfCounterBlock = (PPERF_COUNTER_BLOCK)((LPBYTE)perfInstanceDef + perfInstanceDef->ByteLength);
                        break;
                    }

                    PPERF_COUNTER_BLOCK tmpblock = PPERF_COUNTER_BLOCK((PBYTE)perfInstanceDef + perfInstanceDef->ByteLength);
                    perfInstanceDef = (PPERF_INSTANCE_DEFINITION)((PBYTE)tmpblock + tmpblock->ByteLength);
                }
            }
        }
        perfObj = (PPERF_OBJECT_TYPE)((PBYTE)perfObj + perfObj->TotalByteLength); // next object
    }

    if( NULL != perfCounterBlock) {
        value = *((LONGLONG*) ((LPBYTE)perfCounterBlock + perfCounterDef->CounterOffset));
        return 0;
    } else {
        return -1;
    }
}

GetCounterValue() 함수의 코드는 조금 복잡하다. 이해 자체가 매우 어렵다. 여기서 관련된 구조체는 다음과 같다.

PERF_DATA_BLOCK

PERF_OBJECT_TYPE

PERF_COUNTER_DEFINITION

PERF_INSTANCE_DEFINITION

PERF_COUNTER_BLOCK

꽤 많은 객체가 엉켜있다. 각각 구조체가 무엇을 하는지 각 속한 항목은 어떤 것인지는 MSDN 도움말( Microsoft Visual Studio Documentation )에서 검색해보면 된다. 사실 MSDN 도움말로는 각 구조체의 관계를 파악하기는 힘들다. 조금이나마 관계를 파악하기 위해서 아래 그림을 참고하길 바란다.

상당히 큰 그림이다. 이걸 그리는데 조금 고생했다. 그냥 보기에는 조금 힘들다. 간단하게 설명하면 다음과 같다.

기 본적인 객체는 PERF_DATA_BLOCK에서 시작한다. 그 뒤로 여러 객체들이 따라오는데 각 객체는 PERF_OBJECT_TYPE를 헤더로 갖는다. 그리고 각 객체에는 여러 카운터 객체가 포함된다. 그리고 각 객체에 인스탄스 개수에 따라서 여러 인스탄스 객체가 포함되어 있다.

좀 더 상세하게 설명하면 PERF_DATA_BLOCK에서 다음 객체(PERF_OBJECT_TYPE)를 찾으려면 PERF_DATA_BLOCK의 HeaderLength만큼 이동하면 되고 여러 객체에서 다음 객체를 찾으려면 PERF_OBJECT_TYPE에서 TotalByteLength만큼 이동하면 된다. 그리고 각 객체(PERF_DATA_BLOCK)에서 카운터 정의 객체(PERF_COUNTER_DEFINITION)을 찾으려면 PERF_DATA_BLOCK에서 PERF_DATA_BLOCK의 HeaderLength만큼 이동하면 된다. 카운터 객체가 여러 개가 오는데 다음 카운터 객체(PERF_COUNTER_DEFINITION)를 찾으려면 PERF_COUNTER_DEFINITION에서 PERF_COUNTER_DEFINITION의 ByteLength 만큼 이동하면 된다. 그리고 다중 인스턴스라면 첫번째 인스턴스 객체(PERF_INSTANCE_DEFINITION)를 찾으려면 PERF_OBJECT_TYPE에서 PERF_OBJECT_TYPE의 DefinitionLength 만큼 이동하면 된다. 그리고 다음 인스턴스 객체를 찾으려면 PERF_INSTANCE_DEFINITION에서 PERF_INSTANCE_DEFINITION의 ByteLength 만큼 이동하면 PERF_COUNTER_BLOCK 객체를 얻고 여기에서 다시 PERF_COUNTER_BLOCK의 ByteLength 만큼 이동하면 PERF_INSTANCE_DEFINITION를 얻을 수 있다.

다중 객체가 아니면 간단하다. 첫 번째 인스탄스 객체(PERF_INSTANCE_DEFINITION) 위치는 앞에서 구한 값으로 갖으면 된다. 다음은 PERF_COUNTER_BLOCK에서 ByteLength를 만큼 이동하면 된다.

좀 설명이 길어졌다. 이렇게 설명해도 잘 이해하기 힘들다. 본인도 이렇게 그림을 그리고 직접 트레이싱하면서 이해할 수 있었다. 물론 실제 코드로 확인해봐야 한다.

이정도로 해서 다음 함수로 넘어가겠다.

GetPerformanceData() 함수

이 합수는 레지스트리에 있는 HKEY_PERFORMANCE_DATA 키에서 원하는 객체 값을 얻어 오는 것이다. 아래 예제에서는 원하는 객체를 문자열로 넘겨서 가져오게 하였다. 이 함수는 GetCounterValue() 함수에 비해서는 간단하다. 입력되는 인자는 원하는 인스탄스 객체 명을 받는다. 반환되는 값는 PERF_DATA_BLOCK의 포인터를 반환한다. 만약 에러가 발생하면 NULL값이 반환된다.

#ifndef DEFAULT_PERF_BUFFER
#define DEFAULT_PERF_BUFFER (2048*10)
#endif

#define PERF_BUFFER_INCREMENT (1024*10)

LPBYTE GetPerformanceData(LPTSTR src)
{
    DWORD bufSize = DEFAULT_PERF_BUFFER;

    LPBYTE buf = (LPBYTE)malloc(bufSize);
    LPBYTE tmpBuf = NULL;

    if( NULL == buf )
        return NULL;

    LONG lRes;

    while ( ERROR_MORE_DATA == (lRes = RegQueryValueEx( HKEY_PERFORMANCE_DATA, src, NULL, NULL, buf, &bufSize ))) {
        // Get a more buffer
        bufSize += PERF_BUFFER_INCREMENT;

        tmpBuf = (LPBYTE)realloc(buf, bufSize);

        if( NULL != tmpBuf) {
            buf = tmpBuf;
        } else {
            printf("Reallocation error\n");
            RegCloseKey(HKEY_PERFORMANCE_DATA);
            free(buf);
            return NULL;
        }
    }


    if( ERROR_SUCCESS != lRes ) {
        printf("RegQueryValueEx failed with 0x%x.\n", lRes);      
        free(buf);
    }

    RegCloseKey(HKEY_PERFORMANCE_DATA);

    return buf;
}

DEFAULT_PERF_BUFFER 는 초기 버퍼 크기로 설정 안되어 있으면 20KB으로 설정된다. 그리고 용량이 부족하면 PERF_BUFFER_INCREMENT 만큼 증가하면서 할당하고 PERF_DATA_BLOCK를 획득하여 반환하게 된다.

결론

이 것으로 마무리하려고 합니다. 이거 정리하는데 좀 시간이 걸리는 군요. ㅡ.ㅡ; 관련 자료를 찾는데 하루 이상 걸리고 이해하는데도 시간이 많이 걸리는 군요. 닷넷이라면 좋은 예제가 있고 코드도 간단해서 쉽게 사용할 수 있는데 그렇지 않으면 관련된 자료를 찾기가 힘들더군요. 그러다가 그냥 구글링하면 의외의 자료도 건지더군요.

코드는 따로 올리지는 않겠습니다. 위의 예제를 복사해서 붙여넣으면 됩니다. 코드 상에 에러는 거의 없을 거라고 봅니다. 혹시 잘 안된다면 본인에게 메일을 주면 최대한 빠른 시간(?)내에 답변을 드리도록 하겠습니다. ^^;

아직 이외의 방법은 찾지 못했습니다. 앞에서도 언급했지만 상당히 복잡한 코드입니다. 그만큼 에러라든가 수정하기 힘들다는 말이겠죠. 혹시 더 간단한 방법을 알고 있다면 저에게 알려주시면 고맙겠습니다. ^^ ospace.

참조

[1] Dudi Avramov, How to get CPU usage by performance counters (without PDH), http://www.codeproject.com/KB/system/cpuusageByDudiAvramov.aspx

[2] Zuoliu Ding, An Implementation of System Monitor, http://www.codeproject.com/KB/miscctrl/SystemMonitor.aspx

[3] Performance Counters, http://msdn.microsoft.com/en-us/library/aa373083(VS.85).aspx

[4] GetSystemInfo function, MSDN, http://msdn.microsoft.com/en-us/library/windows/desktop/ms724381(v=vs.85).aspx

반응형