Microsoft MVP성태의 닷넷 이야기
글쓴 사람
정성태 (techsharer at outlook.com)
홈페이지
첨부 파일
(연관된 글이 1개 있습니다.)
(시리즈 글이 5개 있습니다.)
.NET Framework: 442. C# - 시스템의 CPU 사용량 및 프로세스(EXE)의 CPU 사용량 알아내는 방법
; https://www.sysnet.pe.kr/2/0/1684

Linux: 56. 리눅스 - /proc/pid/stat 정보를 이용해 프로세스의 CPU 사용량 구하는 방법
; https://www.sysnet.pe.kr/2/0/13215

Windows: 260. CPU 사용률을 나타내는 2가지 수치 - 사용량(Usage)과 활용률(Utilization)
; https://www.sysnet.pe.kr/2/0/13582

Windows: 261. CPU Utilization이 100% 넘는 경우를 성능 카운터로 확인하는 방법
; https://www.sysnet.pe.kr/2/0/13583

닷넷: 2233. C# - 프로세스 CPU 사용량을 나타내는 성능 카운터와 Win32 API
; https://www.sysnet.pe.kr/2/0/13589




C# - 시스템의 CPU 사용량 및 프로세스(EXE)의 CPU 사용량 알아내는 방법

CPU 사용률을 C#으로 알아내는 방법을 검색해 보면, 거의 WMI를 이용한 방법만을 소개하는 경우가 많습니다. 물론, WMI가 간편하긴 하지만 부하(overhead)를 고려한다면 다른 방법을 찾는 것이 좋습니다. 당연히 ^^ Win32 API가 훨씬 가볍겠지요.

Determine CPU usage of current process (C++ and C#)
; http://www.philosophicalgeek.com/2009/01/03/determine-cpu-usage-of-current-process-c-and-c/comment-page-1/

위의 소스코드와 같이 GetSystemTimes Win32 API를 이용해서,

[System.Runtime.InteropServices.DllImport("kernel32.dll", SetLastError = true)]
static extern bool GetSystemTimes(out FILETIME lpIdleTime,
            out FILETIME lpKernelTime, out FILETIME lpUserTime);

다음과 같이 호출해 주면 됩니다.

FILETIME sysIdle, sysKernel, sysUser;
GetSystemTimes(out sysIdle, out sysKernel, out sysUser);

이때 Kernel에는 Idle 시간이 포함되어 있기 때문에 각 영역별로는 이런 식으로 셈을 해줘야 합니다.

// GetSystemTimes에서 반환받은 FILETIME 구조체의 값을 ulong으로 변환
// ulong uKernel = 변환(sysKernel);
// ulong uUser = 변환(sysUser);
// ulong uIdle = 변환(sysIdle);

ulong sysTotal = uKernel + uUser;
long kernelTotal = (long)(uKernel - uIdle);

그런데, GetSystemTimes가 반환하는 시간은 CPU 사용률이 아닙니다. 대신 CPU가 해당 모드에서 소비된 100 나노초 단위의 시간이기 때문에 이것 자체로는 (시스템이 부팅된 이후) 현재까지의 CPU 사용률에 대한 평균을 구하는 의미밖에 없습니다. 즉, 우리가 알고 싶은 것은 작업관리자에서처럼 초마다 변화하는 CPU 사용률이기 때문에 여기서 한가지 더 작업을 해야 합니다.

따라서, 최초에 한번 GetSystemTimes로 시간을 구해 보존한 다음, 이후 시간이 흘러 한번 더 GetSystemTimes을 호출해 각각의 모드에 소비된 시간의 차를 구하는 방식을 사용합니다. 그래서 코드가 아래와 같이 바뀝니다.

using System.Runtime.InteropServices.ComTypes;

class CpuUsage
{
    [System.Runtime.InteropServices.DllImport("kernel32.dll", SetLastError = true)]
    static extern bool GetSystemTimes(out FILETIME lpIdleTime,
                out FILETIME lpKernelTime, out FILETIME lpUserTime);

    FILETIME _prevSysKernel;
    FILETIME _prevSysUser;
    FILETIME _prevSysIdle;

    public void GetUsage(out float processorCpuUsage)
    {
        processorCpuUsage = 0.0f;

        FILETIME sysIdle, sysKernel, sysUser;

        if (!GetSystemTimes(out sysIdle, out sysKernel, out sysUser))
        {
            return 0.0f;
        }

        if (_prevSysIdle.dwLowDateTime != 0 && _prevSysIdle.dwHighDateTime != 0)
        {
            ulong sysKernelDiff = SubtractTimes(sysKernel, _prevSysKernel);
            ulong sysUserDiff = SubtractTimes(sysUser, _prevSysUser);
            ulong sysIdleDiff = SubtractTimes(sysIdle, _prevSysIdle);

            ulong sysTotal = sysKernelDiff + sysUserDiff;
            long kernelTotal = (long)(sysKernelDiff - sysIdleDiff);

            processorCpuUsage = (float)((((ulong)kernelTotal + sysUserDiff) * 100.0) / sysTotal);
        }

        _prevSysKernel = sysKernel;
        _prevSysUser = sysUser;
        _prevSysIdle = sysIdle;
    }

    private UInt64 SubtractTimes(FILETIME a, FILETIME b)
    {
        ulong aInt = ((ulong)a.dwHighDateTime << 32) | (ulong)a.dwLowDateTime;
        ulong bInt = ((ulong)b.dwHighDateTime << 32) | (ulong)b.dwLowDateTime;

        return aInt - bInt;
    }
}

간혹 가다가 다른 소스코드를 보면 구해진 (100 나노초 단위의) FILETIME 시간을 10,000으로 나눠서 밀리초로 구한 다음 100분율로 변환하는 것을 볼 수 있는데요. 쓸모없는 과정입니다. CPU 사용률을 구하는 경우라면 굳이 밀리초로 변환할 필요가 없습니다.




그런데, 위의 코드를 실제로 실행해 보면 약간의 문제가 있는 것을 볼 수 있습니다. 문제의 발단은 SubtractTime 내의 코드에 있는데요.

(ulong)a.dwLowDateTime
(ulong)b.dwLowDateTime

System.Runtime.InteropServices.ComTypes.FILETIME 정의를 보면,

using System;
using System.Runtime.InteropServices;
    
[StructLayout(LayoutKind.Sequential)]
public struct FILETIME
{
    public int dwLowDateTime;
    public int dwHighDateTime;
}
보시다시피 dwLowDateTime 형식이 부호 있는 정수형(Int32)입니다. 만약 이 값이 음수(-)인 경우가 발생하면 ulong으로 형변환을 하는 경우 음수 비트가 확장되어 dwHighDateTime과의 OR 연산 결과값이 틀어져버립니다. 이런 부작용을 막고 싶다면 ulong이 아닌 uint로 형변환을 낮춰야 합니다.
private UInt64 SubtractTimes(FILETIME a, FILETIME b)
{
    ulong aInt = ((ulong)a.dwHighDateTime << 32) | (uint)a.dwLowDateTime;
    ulong bInt = ((ulong)b.dwHighDateTime << 32) | (uint)b.dwLowDateTime;

    return aInt - bInt;
}

또는, 부호없는 정수를 사용하는 FILETIME을 사용하도록 구조체를 아예 교체하는 것도 방법이 될 수 있습니다.

[System.Runtime.InteropServices.StructLayout(System.Runtime.InteropServices.LayoutKind.Sequential)]
public struct FILETIME
{
    public uint dwLowDateTime;
    public uint dwHighDateTime;
}




그런데, 실행하다 보면 여전히 얼토당토않는 수치가 나오는 경우를 볼 수 있습니다. 가만 보니, kernel 시간에서 idle 시간을 뺀 값이 음수(-)인 경우가 있습니다. 예를 들어, 다음과 같은 수치가 나오는 것입니다.

kernel 7984
idle 7985

참... 뭐라 할 말이 없군요. ^^ idle 시간이 kernel 시간에 포함되는 것인데 그것이 음수가 나오다니요? 생각해 볼 수 있는 가능성이라고는 GetSystemTimes 내부에서 CPU 시간에 대한 스냅샷을 뜰 때 kernel 시간을 구한 후 미묘하게 100 나노초 후에 idle 시간이 구해지는 경우가 아닌가 싶습니다.

어쨌든, 이런 수치가 나올 때가 있으므로 이에 대한 보정이 필요합니다.

long kernelTotal = (long)(sysKernelDiff - sysIdleDiff);

if (kernelTotal < 0)
{
    kernelTotal = 0;
}

여기까지 구했으면 완벽합니다. ^^




기왕 구하는 거 해당 프로세스(EXE)의 CPU 사용률도 구해보는 것도 좋겠지요? ^^ 방법은 동일하게 EXE에 소비된 CPU 시간(100나노초)을 한번 구해두고 이후로 그 차이 값을 구하면 해당 시간 동안 소비된 CPU 시간이 나옵니다. 그 후, 전체 CPU 시간으로 나누면 해당 프로세스에 할당된 CPU 소비량을 알 수 있게 됩니다.

다행히 프로세스의 CPU 시간은 Win32 API를 이용할 필요가 없고 Process 타입의 TotalProcessorTime 값을 사용하면 됩니다. 이것이 모두 반영된 소스코드를 아래에 실었으니 참고하세요. ^^

using System;
using System.Diagnostics;
using System.Runtime.InteropServices.ComTypes;

namespace CpuUsageCs
{
    class CpuUsage
    {
        [System.Runtime.InteropServices.DllImport("kernel32.dll", SetLastError = true)]
        static extern bool GetSystemTimes(out FILETIME lpIdleTime,
                    out FILETIME lpKernelTime, out FILETIME lpUserTime);

        FILETIME _prevSysKernel;
        FILETIME _prevSysUser;
        FILETIME _prevSysIdle;

        TimeSpan _prevProcTotal;

        public CpuUsage()
        {
            _prevProcTotal = TimeSpan.MinValue;
        }

        public float GetUsage(out float processorCpuUsage)
        {
            processorCpuUsage = 0.0f;

            float _processCpuUsage = 0.0f;
            FILETIME sysIdle, sysKernel, sysUser;

            Process process = Process.GetCurrentProcess();
            TimeSpan procTime = process.TotalProcessorTime;

            if (!GetSystemTimes(out sysIdle, out sysKernel, out sysUser))
            {
                return 0.0f;
            }

            if (_prevProcTotal != TimeSpan.MinValue)
            {
                ulong sysKernelDiff = SubtractTimes(sysKernel, _prevSysKernel);
                ulong sysUserDiff = SubtractTimes(sysUser, _prevSysUser);
                ulong sysIdleDiff = SubtractTimes(sysIdle, _prevSysIdle);

                ulong sysTotal = sysKernelDiff + sysUserDiff;
                long kernelTotal = (long)(sysKernelDiff - sysIdleDiff);

                if (kernelTotal < 0)
                {
                    kernelTotal = 0;
                }

                processorCpuUsage = (float)((((ulong)kernelTotal + sysUserDiff) * 100.0) / sysTotal);

                long procTotal = (procTime.Ticks - _prevProcTotal.Ticks);

                if (sysTotal > 0)
                {
                    _processCpuUsage = (short)((100.0 * procTotal) / sysTotal);
                }
            }

            _prevProcTotal = procTime;
            _prevSysKernel = sysKernel;
            _prevSysUser = sysUser;
            _prevSysIdle = sysIdle;

            return _processCpuUsage;
        }

        private UInt64 SubtractTimes(FILETIME a, FILETIME b)
        {
            ulong aInt = ((ulong)a.dwHighDateTime << 32) | (uint)a.dwLowDateTime;
            ulong bInt = ((ulong)b.dwHighDateTime << 32) | (uint)b.dwLowDateTime;

            return aInt - bInt;
        }
    }
}

사용법도 간단합니다.

while (true)
{
    float processorCpuUsage;
    float processCpuUsage = usage.GetUsage(out processorCpuUsage);
    Console.WriteLine("{0}% process usage, {1}% CPU", processCpuUsage, processorCpuUsage);
    Thread.Sleep(1000);
}

(첨부한 소스코드에서 테스트를 해볼 수 있습니다.)




이번 테스트를 하면서 발견한 사실이 하나 있다면 (적어도 제가 테스트한 윈도우 8.1의 경우) 작업 관리자의 CPU 사용률과 성능모니터링 도구(Perfmon.msc)의 CPU 사용률이 같지 않다는 것입니다.

cpuusage_cs_1.png

보시는 바와 같이 GetSystemTimes로 구한 CPU 사용률은 성능 모니터링 도구의 값과 거의 일치하는 반면 작업 관리자의 CPU 사용률과는 다릅니다. (처음에는 Hyper-V를 사용해서 그런 줄 알았는데, Hyper-V가 없는 경우에도 마찬가지 현상이 발생합니다.)

딱히 웹 검색을 해도 이에 대한 글이 발견되지는 않았습니다. 혹시 아시는 분은 ^^ 덧글 좀 부탁드립니다.




(2022-12-02: 업데이트) 작업 관리자와 성능 모니터링 도구의 수치는 각각 다음과 같이 정리할 수 있습니다.

성능 모니터링 도구를 띄웠을 때 보여주는 수치 == "Object == Processor Information"의 "% Processor Time (_Total)" 
또한 이 값은 "Object == Processor"의 "%Processor Time (_Total)"과도 동일한 값입니다.

반면, 작업 관리자가 보여주는 값 == "Object == Processor Information"의 "% Processor Utility (_Total)"과 일치합니다.

따라서 성능 카운터의 설명을 빌리면,

"Object == Processor Information"의 "% Processor Time (_Total)"

% Processor Time is the percentage of elapsed time that the processor spends to execute a non-Idle thread. It is calculated by measuring the percentage of time that the processor spends executing the idle thread and then subtracting that value from 100%. (Each processor has an idle thread to which time is accumulated when no other threads are ready to run). This counter is the primary indicator of processor activity, and displays the average percentage of busy time observed during the sample interval. It should be noted that the accounting calculation of whether the processor is idle is performed at an internal sampling interval of the system clock tick. On todays fast processors, % Processor Time can therefore underestimate the processor utilization as the processor may be spending a lot of time servicing threads between the system clock sampling interval. Workload based timer applications are one example of applications which are more likely to be measured inaccurately as timers are signaled just after the sample is taken.

"Object == Processor"의 "%Processor Time (_Total)"

% Processor Time is the percentage of elapsed time that the processor spends to execute a non-Idle thread. It is calculated by measuring the percentage of time that the processor spends executing the idle thread and then subtracting that value from 100%. (Each processor has an idle thread that consumes cycles when no other threads are ready to run). This counter is the primary indicator of processor activity, and displays the average percentage of busy time observed during the sample interval. It should be noted that the accounting calculation of whether the processor is idle is performed at an internal sampling interval of the system clock (10ms). On todays fast processors, % Processor Time can therefore underestimate the processor utilization as the processor may be spending a lot of time servicing threads between the system clock sampling interval. Workload based timer applications are one example of applications which are more likely to be measured inaccurately as timers are signaled just after the sample is taken.

"Object == Processor Information"의 "% Processor Utility (_Total)"

Processor Utility is the amount of work a processor is completing, as a percentage of the amount of work the processor could complete if it were running at its nominal performance and never idle. On some processors, Processor Utility may exceed 100%.

이렇게 정리가 되는데, 자세히 보면 "Object == Processor Information"의 "% Processor Time (_Total)", "Object == Processor"의 "%Processor Time (_Total)"의 경우에도 설명이 살짝 다릅니다. (혹시, 저 현란한 영문장을 해석해 멋지게 설명해 주실 분 계실까요? ^^ 단어 하나하나 뜯어보면 매우 평이한 문장인데... ^^; 도대체가 눈에 들어오지 않는군요.)




[이 글에 대해서 여러분들과 의견을 공유하고 싶습니다. 틀리거나 미흡한 부분 또는 의문 사항이 있으시면 언제든 댓글 남겨주십시오.]

[연관 글]






[최초 등록일: ]
[최종 수정일: 3/29/2024]

Creative Commons License
이 저작물은 크리에이티브 커먼즈 코리아 저작자표시-비영리-변경금지 2.0 대한민국 라이센스에 따라 이용하실 수 있습니다.
by SeongTae Jeong, mailto:techsharer at outlook.com

비밀번호

댓글 작성자
 



2014-06-11 09시44분
[spowner] 유용한 정보 감사드립니다
[guest]
2016-03-22 02시06분
[유수석] PerfMon과 TaskMan의 CPU 사용율이 다른 이유는 샘플링하는 시간이 서로 달라서라고 판단됩니당.
[guest]
2016-03-22 02시41분
넵. 물론 샘플링하는 시간이 다르긴 할 텐데, 제가 만들어 실행한 코드와 perfmon은 거의 비슷한 값을 유지하는 반면 작업 관리자는 너무 큰 차이를 보여서... 그랬습니다. 지금도 테스트해 보면, 작업 관리자는 항상 5% 이상의 수치를 보여주지만 perfmon은 1% ~ 2%를 자주 보여주고 평균도 3% ~ 4% 대에 머무르고 있습니다.
정성태
2016-03-22 07시04분
[유수석] 1초라면 CPU 입장에선 졸라 긴 시간이고 샘플링 타이밍이 서로 다르니 1-2% 차이는 발생할 수 있을 듯...
CPU에 부하를 좀 줘서 테스트하면 비슷한 시간이 나오지 않을까요?
집에가서 테스트 해봐야징... (물론 게임으로... 이번 신작 The Division 졸라 잼남)
[guest]
2016-03-22 12시11분
넵. 저도 한번 테스트해 보겠습니다. ^^ (그나저나 ^^ 저 역시 이번에 나온 Halo 5 해야 하는데, 시간 핑계만 대고 있습니다.)
정성태

... 16  17  [18]  19  20  21  22  23  24  25  26  27  28  29  30  ...
NoWriterDateCnt.TitleFile(s)
13201정성태12/20/20225803오류 유형: 835. PyCharm 사용 시 C 드라이브 용량 부족
13200정성태12/19/20224755오류 유형: 834. 이벤트 로그 - SSL Certificate Settings created by an admin process for endpoint
13199정성태12/19/20224940개발 환경 구성: 656. Internal Network 유형의 스위치로 공유한 Hyper-V의 VM과 호스트가 통신이 안 되는 경우
13198정성태12/18/20224861.NET Framework: 2080. C# - Microsoft.XmlSerializer.Generator 처리 없이 XmlSerializer 생성자를 예외 없이 사용하고 싶다면?파일 다운로드1
13197정성태12/17/20224650.NET Framework: 2079. .NET Core/5+ 환경에서 XmlSerializer 사용 시 System.IO.FileNotFoundException 예외 발생하는 경우파일 다운로드1
13196정성태12/16/20224846.NET Framework: 2078. .NET Core/5+를 위한 SGen(Microsoft.XmlSerializer.Generator) 사용법
13195정성태12/15/20225300개발 환경 구성: 655. docker - bridge 네트워크 모드에서 컨테이너 간 통신 시 --link 옵션 권장 이유
13194정성태12/14/20225430오류 유형: 833. warning C4747: Calling managed 'DllMain': Managed code may not be run under loader lock파일 다운로드1
13193정성태12/14/20225545오류 유형: 832. error C7681: two-phase name lookup is not supported for C++/CLI or C++/CX; use /Zc:twoPhase-
13192정성태12/13/20225595Linux: 55. 리눅스 - bash shell에서 실수 연산
13191정성태12/11/20226488.NET Framework: 2077. C# - 직접 만들어 보는 SynchronizationContext파일 다운로드1
13190정성태12/9/20226975.NET Framework: 2076. C# - SynchronizationContext 기본 사용법파일 다운로드1
13189정성태12/9/20227854오류 유형: 831. Visual Studio - Windows Forms 디자이너의 도구 상자에 컨트롤이 보이지 않는 문제
13188정성태12/9/20226438.NET Framework: 2075. C# - 직접 만들어 보는 TaskScheduler 실습 (SingleThreadTaskScheduler)파일 다운로드1
13187정성태12/8/20226377개발 환경 구성: 654. openssl - CA로부터 인증받은 새로운 인증서를 생성하는 방법 (2)
13186정성태12/6/20224924오류 유형: 831. The framework 'Microsoft.AspNetCore.App', version '...' was not found.
13185정성태12/6/20225896개발 환경 구성: 653. Windows 환경에서의 Hello World x64 어셈블리 예제 (NASM 버전)
13184정성태12/5/20225074개발 환경 구성: 652. ml64.exe와 link.exe x64 실행 환경 구성
13183정성태12/4/20225089오류 유형: 830. MASM + CRT 함수를 사용하는 경우 발생하는 컴파일 오류 정리
13182정성태12/4/20225809Windows: 217. Windows 환경에서의 Hello World x64 어셈블리 예제 (MASM 버전)
13181정성태12/3/20225214Linux: 54. 리눅스/WSL - hello world 어셈블리 코드 x86/x64 (nasm)
13180정성태12/2/20225287.NET Framework: 2074. C# - 스택 메모리에 대한 여유 공간 확인하는 방법파일 다운로드1
13179정성태12/2/20224621Windows: 216. Windows 11 - 22H2 업데이트 이후 Terminal 대신 cmd 창이 뜨는 경우
13178정성태12/1/20225198Windows: 215. Win32 API 금지된 함수 - IsBadXxxPtr 유의 함수들이 안전하지 않은 이유파일 다운로드1
13177정성태11/30/20225874오류 유형: 829. uwsgi 설치 시 fatal error: Python.h: No such file or directory
13176정성태11/29/20224744오류 유형: 828. gunicorn - ModuleNotFoundError: No module named 'flask'
... 16  17  [18]  19  20  21  22  23  24  25  26  27  28  29  30  ...