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 해야 하는데, 시간 핑계만 대고 있습니다.)
정성태

... [31]  32  33  34  35  36  37  38  39  40  41  42  43  44  45  ...
NoWriterDateCnt.TitleFile(s)
12875정성태12/13/20216822스크립트: 35. python - time.sleep(...) 호출 시 hang이 걸리는 듯한 문제
12874정성태12/13/20216862오류 유형: 773. shell script 실행 시 "$'\r': command not found" 오류
12873정성태12/12/20217995오류 유형: 772. 리눅스 - PATH에 등록했는데도 "command not found"가 나온다면?
12872정성태12/12/20217838개발 환경 구성: 615. GoLang과 Python 빌드가 모두 가능한 docker 이미지 만들기
12871정성태12/12/20217869오류 유형: 771. docker: Error response from daemon: OCI runtime create failed
12870정성태12/9/20216422개발 환경 구성: 614. 파이썬 - PyPI 패키지 만들기 (4) package_data 옵션
12869정성태12/8/20218733개발 환경 구성: 613. git clone 실행 시 fingerprint 묻는 단계를 생략하는 방법
12868정성태12/7/20217349오류 유형: 770. twine 업로드 시 "HTTPError: 400 Bad Request ..." 오류 [1]
12867정성태12/7/20216997개발 환경 구성: 612. 파이썬 - PyPI 패키지 만들기 (3) entry_points 옵션
12866정성태12/7/202114388오류 유형: 769. "docker build ..." 시 "failed to solve with frontend dockerfile.v0: failed to read dockerfile ..." 오류
12865정성태12/6/20217135개발 환경 구성: 611. 파이썬 - PyPI 패키지 만들기 (2) long_description, cmdclass 옵션
12864정성태12/6/20215527Linux: 46. WSL 환경에서 find 명령을 사용해 파일을 찾는 방법
12863정성태12/4/20217465개발 환경 구성: 610. 파이썬 - PyPI 패키지 만들기
12862정성태12/3/20216163오류 유형: 768. Golang - 빌드 시 "cmd/go: unsupported GOOS/GOARCH pair linux /amd64" 오류
12861정성태12/3/20218422개발 환경 구성: 609. 파이썬 - "Windows embeddable package"로 개발 환경 구성하는 방법
12860정성태12/1/20216454오류 유형: 767. SQL Server - 127.0.0.1로 접속하는 경우 "Access is denied"가 발생한다면?
12859정성태12/1/202112693개발 환경 구성: 608. Hyper-V 가상 머신에 Console 모드로 로그인하는 방법
12858정성태11/30/20219949개발 환경 구성: 607. 로컬의 USB 장치를 원격 머신에 제공하는 방법 - usbip-win
12857정성태11/24/20217280개발 환경 구성: 606. WSL Ubuntu 20.04에서 파이썬을 위한 uwsgi 설치 방법
12856정성태11/23/20219208.NET Framework: 1121. C# - 동일한 IP:Port로 바인딩 가능한 서버 소켓 [2]
12855정성태11/13/20216410개발 환경 구성: 605. Azure App Service - Kudu SSH 환경에서 FTP를 이용한 파일 전송
12854정성태11/13/20218086개발 환경 구성: 604. Azure - 윈도우 VM에서 FTP 여는 방법
12853정성태11/10/20216350오류 유형: 766. Azure App Service - JBoss 호스팅 생성 시 "This region has quota of 0 PremiumV3 instances for your subscription. Try selecting different region or SKU."
12851정성태11/1/20217868스크립트: 34. 파이썬 - MySQLdb 기본 예제 코드
12850정성태10/27/20219027오류 유형: 765. 우분투에서 pip install mysqlclient 실행 시 "OSError: mysql_config not found" 오류
12849정성태10/17/20218139스크립트: 33. JavaScript와 C#의 시간 변환 [1]
... [31]  32  33  34  35  36  37  38  39  40  41  42  43  44  45  ...