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

1  2  3  4  [5]  6  7  8  9  10  11  12  13  14  15  ...
NoWriterDateCnt.TitleFile(s)
13518정성태1/9/20242432닷넷: 2197. C# - ClientWebSocket의 Ping, Pong 처리
13517정성태1/8/20242260스크립트: 63. Python - 공개 패키지를 이용한 위성 이미지 생성 (pystac_client, odc.stac)
13516정성태1/7/20242378닷넷: 2196. IIS - AppPool의 "Disable Overlapped Recycle" 옵션의 부작용
13515정성태1/6/20242675닷넷: 2195. async 메서드 내에서 C# 7의 discard 구문 활용 사례 [1]
13514정성태1/5/20242351개발 환경 구성: 702. IIS - AppPool의 "Disable Overlapped Recycle" 옵션
13513정성태1/5/20242272닷넷: 2194. C# - WebActivatorEx / System.Web의 PreApplicationStartMethod 특성
13512정성태1/4/20242206개발 환경 구성: 701. IIS - w3wp.exe 프로세스의 ASP.NET 런타임을 항상 Warmup 모드로 유지하는 preload Enabled 설정
13511정성태1/4/20242223닷넷: 2193. C# - ASP.NET Web Application + OpenAPI(Swashbuckle) 스펙 제공
13510정성태1/3/20242143닷넷: 2192. C# - 특정 실행 파일이 있는지 확인하는 방법 (Linux)
13509정성태1/3/20242215오류 유형: 887. .NET Core 2 이하의 프로젝트에서 System.Runtime.CompilerServices.Unsafe doesn't support netcoreapp2.0.
13508정성태1/3/20242278오류 유형: 886. ORA-28000: the account is locked
13507정성태1/2/20242939닷넷: 2191. C# - IPGlobalProperties를 이용해 netstat처럼 사용 중인 Socket 목록 구하는 방법파일 다운로드1
13506정성태12/29/20232510닷넷: 2190. C# - 닷넷 코어/5+에서 달라지는 System.Text.Encoding 지원
13505정성태12/27/20233048닷넷: 2189. C# - WebSocket 클라이언트를 닷넷으로 구현하는 예제 (System.Net.WebSockets)파일 다운로드1
13504정성태12/27/20232635닷넷: 2188. C# - ASP.NET Core SignalR로 구현하는 채팅 서비스 예제파일 다운로드1
13503정성태12/27/20232518Linux: 67. WSL 환경 + mlocate(locate) 도구의 /mnt 디렉터리 검색 문제
13502정성태12/26/20232595닷넷: 2187. C# - 다른 프로세스의 환경변수 읽는 예제파일 다운로드1
13501정성태12/25/20232341개발 환경 구성: 700. WSL + uwsgi - IPv6로 바인딩하는 방법
13500정성태12/24/20232454디버깅 기술: 194. Windbg - x64 가상 주소를 물리 주소로 변환
13498정성태12/23/20233126닷넷: 2186. 한국투자증권 KIS Developers OpenAPI의 C# 래퍼 버전 - eFriendOpenAPI NuGet 패키지
13497정성태12/22/20232524오류 유형: 885. Visual Studiio - error : Could not connect to the remote system. Please verify your connection settings, and that your machine is on the network and reachable.
13496정성태12/21/20232563Linux: 66. 리눅스 - 실행 중인 프로세스 내부의 환경변수 설정을 구하는 방법 (gdb)
13495정성태12/20/20232468Linux: 65. clang++로 공유 라이브러리의 -static 옵션 빌드가 가능할까요?
13494정성태12/20/20232628Linux: 64. Linux 응용 프로그램의 (C++) so 의존성 줄이기(ReleaseMinDependency) - 두 번째 이야기
13493정성태12/19/20232812닷넷: 2185. C# - object를 QueryString으로 직렬화하는 방법
13492정성태12/19/20232465개발 환경 구성: 699. WSL에 nopCommerce 예제 구성
1  2  3  4  [5]  6  7  8  9  10  11  12  13  14  15  ...