Microsoft MVP성태의 닷넷 이야기
글쓴 사람
정성태 (techsharer at outlook.com)
홈페이지
첨부 파일
(연관된 글이 3개 있습니다.)
(시리즈 글이 5개 있습니다.)
Windows: 120. 윈도우 운영체제의 시간 함수 (1) - GetTickCount와 timeGetTime의 차이점
; https://www.sysnet.pe.kr/2/0/11063

Windows: 121. 윈도우 운영체제의 시간 함수 (2) - Sleep 함수의 동작 방식
; https://www.sysnet.pe.kr/2/0/11065

Windows: 122. 윈도우 운영체제의 시간 함수 (3) - QueryInterruptTimePrecise, QueryInterruptTime 함수
; https://www.sysnet.pe.kr/2/0/11066

Windows: 123. 윈도우 운영체제의 시간 함수 (4) - RTC, TSC, PM Clock, HPET Timer
; https://www.sysnet.pe.kr/2/0/11067

Windows: 124. 윈도우 운영체제의 시간 함수 (5) - TSC(Time Stamp Counter)와 QueryPerformanceCounter
; https://www.sysnet.pe.kr/2/0/11068




윈도우 운영체제의 시간 함수 (3) - QueryInterruptTimePrecise, QueryInterruptTime 함수

지난 글을 통해, GetTickCount와 timeGetTime의 동작 방식을 살펴봤는데요. GetTickCount와 timeGetTime의 문제는 결국 운영체제가 메모리에 인터럽트가 발생할 때마다 그 횟수를 저장해 둔 변수의 값을 읽어온다는 것입니다. 그런데, 왜 그런 식으로 동작해야 할까요? 그냥 타이머 장치의 시간 값을 직접 구하는 Win32 API를 제공해주면 되는 것 아닐까요?

물론 이런 API가 있지만 아쉽게도 Windows 10부터 제공합니다.

QueryInterruptTimePrecise function
; https://learn.microsoft.com/en-us/windows/win32/api/realtimeapiset/nf-realtimeapiset-queryinterrupttimeprecise

lpInterruptTimePrecise [out]
A pointer to a ULONGLONG in which to receive the interrupt-time count in system time units of 100 nanoseconds. Divide by ten million, or 1e7, to get seconds (there are 1e9 nanoseconds in a second, so there are 1e7 100-nanoseconds in a second).


100 나노초 단위라고 하니, 만약 이 함수의 반환 값이 1,000이라고 했을 때 밀리 초로 환산하려면 10,000으로 나누어 0.1ms를 계산할 수 있습니다.

주기적인 타이머 인터럽트에 영향을 안 받는지... 실제로 다음의 예제로 테스트할 수 있습니다.

#include "stdafx.h"
#include <Windows.h>
// #include <realtimeapiset.h>

#pragma comment(lib, "winmm.lib")

typedef VOID (WINAPI *FuncQueryInterruptTimePrecise)(_Out_ PULONGLONG lpInterruptTimePrecise);

int main()
{
    int count = 0;

    HMODULE hModule = ::LoadLibrary(L"KernelBase.dll");

    FuncQueryInterruptTimePrecise func_QueryInterruptTimePrecise = 
        (FuncQueryInterruptTimePrecise)::GetProcAddress(hModule, "QueryInterruptTimePrecise");

    if (func_QueryInterruptTimePrecise == nullptr)
    {
        printf("Not a Windows 10 PC\n");
        return 0;
    }

    __int64 currentTime;
    __int64 gap[1000];

    count = -1;
    int maxCount = 100;
    __int64 diff = 0;

    printf("QueryInterruptTimePrecise\n");
    while (count ++ < maxCount)
    {
        func_QueryInterruptTimePrecise((PULONGLONG)&currentTime);
        gap[count] = currentTime;
        printf("%I64d\n", currentTime);
    }

    currentTime = gap[0];
    diff = 0;
    for (int i = 1; i < maxCount; i++)
    {
        diff = gap[i] - currentTime;
        printf("%I64d, %0.4f\n", diff, diff / 10000.0);
        currentTime = gap[i];
    }

    return 0;
}

while 반복문에서 QueryInterruptTimePrecise 함수로 시간 값을 보관한 그 간격을 출력한 결과는 다음과 같습니다.

2294, 0.2294
2612, 0.2612
2514, 0.2514
2473, 0.2473
2463, 0.2463
2499, 0.2499
2461, 0.2461
2445, 0.2445
3450, 0.3450
2648, 0.2648
2508, 0.2508
2436, 0.2436
2421, 0.2421
2424, 0.2424
2421, 0.2421
2449, 0.2449
2418, 0.2418
2415, 0.2415
2397, 0.2397
...[생략]...

timeBeginPeriod + timeGetTime의 조합도 1ms 단위의 변화만 감지할 수 있었던 것에 비하면, 타이머 장치에 직접 접근하는 덕분에 정밀도는 훨씬 높아졌습니다.

그런데, 왜 이 좋은 것을 그동안 제공하지 않았던 것일까요? 제 생각이지만, 윈도우 PC 환경에서 1ms 미만의 정밀도를 요구하는 작업이 그다지 크게 중요하다고 생각지는 않았던 것이 아닌가 싶습니다. 그 외에 또 하나 이유라면, 사실 QueryInterruptTimePrecise는 타이머 디바이스로부터 직접 값을 읽어오기 때문에 호출 시간이 timeGetTime에 비해 더 느리다는 단점이 있습니다. 즉, 시간 정밀도를 높이려고 호출한 API 자체가 시간이 더 걸려 버리는 상황이 발생하는 것입니다.




이와 유사한 함수의 이름으로 QueryInterruptTime이 있는데 역시 Windows 10부터 제공됩니다. 이 함수는 timeGetTime과 동작 방식은 유사하나 대신 값이 64비트 변수에 담겨있고 100ns 단위의 시간 값을 제공합니다.

실제로 기본 타이머 설정인 15.625ms로 테스트를 해보면,

#include "stdafx.h"
#include <Windows.h>
// #include <realtimeapiset.h>

#pragma comment(lib, "winmm.lib")

typedef VOID (WINAPI *FuncQueryInterruptTime)(_Out_ PULONGLONG lpInterruptTime);

int main()
{
    int count = 0;

    HMODULE hModule = ::LoadLibrary(L"KernelBase.dll");

    FuncQueryInterruptTime func_QueryInterruptTime =
        (FuncQueryInterruptTime)::GetProcAddress(hModule, "QueryInterruptTime");

    if (func_QueryInterruptTime == nullptr)
    {
        printf("Not a Windows 10 PC\n");
        return 0;
    }

    __int64 currentTime;
    __int64 gap[1000];

    count = -1;
    int maxCount = 100;
    __int64 diff = 0;

    printf("QueryInterruptTime\n");
    while (count++ < mxunt)
    {
        func_QueryInterruptTime((PULONGLONG)&curentTime);
        gap[count] = currentTime;
        printf("%I64d\n", currentTime);
    }

    currentTime = gap[0];
    diff = 0;
    for (int i = 1; i < maxCount; i++)
    {
        diff = gap[i] - currentTime;
        printf("%I64d, %0.4f\n", diff, diff / 10000.0);
        currentTime = gap[i];
    }

    return 0;
}

출력된 시간 간격은 GetTickCount 때의 결과와 유사하게 약 15.625ms 간격으로 변화가 발생합니다.

0, 0.0000
...[생략: 수십 번 반복]...
156319, 15.6319
...[생략]...

물론, 위의 소스 코드에서 timeBeginPeriod(1) 코드를 한 번 호출해 주면 QueryInterruptTime은 1ms 단위로 변합니다.

(첨부 파일은 이 글의 소스 코드를 포함합니다.)




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

[연관 글]






[최초 등록일: ]
[최종 수정일: 11/14/2023]

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

비밀번호

댓글 작성자
 




... 46  47  48  49  50  51  52  53  54  55  56  57  58  59  [60]  ...
NoWriterDateCnt.TitleFile(s)
12125정성태1/24/20208585VS.NET IDE: 141. IDE0019 - Use pattern matching
12124정성태1/23/202010407VS.NET IDE: 140. IDE1006 - Naming rule violation: These words must begin with upper case characters: ...
12123정성태1/23/202011882웹: 39. Google Analytics - gtag 함수를 이용해 페이지 URL 수정 및 별도의 이벤트 생성 방법 [2]
12122정성태1/20/20208872.NET Framework: 879. C/C++의 UNREFERENCED_PARAMETER 매크로를 C#에서 우회하는 방법(IDE0060 - Remove unused parameter '...')파일 다운로드1
12121정성태1/20/20209421VS.NET IDE: 139. Visual Studio - Error List: "Could not find schema information for the ..."파일 다운로드1
12120정성태1/19/202010859.NET Framework: 878. C# DLL에서 Win32 C/C++처럼 dllexport 함수를 제공하는 방법 - 네 번째 이야기(IL 코드로 직접 구현)파일 다운로드1
12119정성태1/17/202010900디버깅 기술: 160. Windbg 확장 DLL 만들기 (3) - C#으로 만드는 방법
12118정성태1/17/202011513개발 환경 구성: 466. C# DLL에서 Win32 C/C++처럼 dllexport 함수를 제공하는 방법 - 세 번째 이야기 [1]
12117정성태1/15/202010539디버깅 기술: 159. C# - 디버깅 중인 프로세스를 강제로 다른 디버거에서 연결하는 방법파일 다운로드1
12116정성태1/15/202011032디버깅 기술: 158. Visual Studio로 디버깅 시 sos.dll 확장 명령어를 (비롯한 windbg의 다양한 기능을) 수행하는 방법
12115정성태1/14/202010793디버깅 기술: 157. C# - PEB.ProcessHeap을 이용해 디버깅 중인지 확인하는 방법파일 다운로드1
12114정성태1/13/202012644디버깅 기술: 156. C# - PDB 파일로부터 심벌(Symbol) 및 타입(Type) 정보 열거 [1]파일 다운로드3
12113정성태1/12/202013262오류 유형: 590. Visual C++ 빌드 오류 - fatal error LNK1104: cannot open file 'atls.lib' [1]
12112정성태1/12/20209865오류 유형: 589. PowerShell - 원격 Invoke-Command 실행 시 "WinRM cannot complete the operation" 오류 발생
12111정성태1/12/202013092디버깅 기술: 155. C# - KernelMemoryIO 드라이버를 이용해 실행 프로그램을 숨기는 방법(DKOM: Direct Kernel Object Modification) [16]파일 다운로드1
12110정성태1/11/202011682디버깅 기술: 154. Patch Guard로 인해 블루 스크린(BSOD)가 발생하는 사례 [5]파일 다운로드1
12109정성태1/10/20209616오류 유형: 588. Driver 프로젝트 빌드 오류 - Inf2Cat error -2: "Inf2Cat, signability test failed."
12108정성태1/10/20209658오류 유형: 587. Kernel Driver 시작 시 127(The specified procedure could not be found.) 오류 메시지 발생
12107정성태1/10/202010641.NET Framework: 877. C# - 프로세스의 모든 핸들을 열람 - 두 번째 이야기
12106정성태1/8/202012001VC++: 136. C++ - OSR Driver Loader와 같은 Legacy 커널 드라이버 설치 프로그램 제작 [1]
12105정성태1/8/202010674디버깅 기술: 153. C# - PEB를 조작해 로드된 DLL을 숨기는 방법
12104정성태1/7/202011386DDK: 9. 커널 메모리를 읽고 쓰는 NT Legacy driver와 C# 클라이언트 프로그램 [4]
12103정성태1/7/202014061DDK: 8. Visual Studio 2019 + WDK Legacy Driver 제작- Hello World 예제 [1]파일 다운로드2
12102정성태1/6/202011703디버깅 기술: 152. User 권한(Ring 3)의 프로그램에서 _ETHREAD 주소(및 커널 메모리를 읽을 수 있다면 _EPROCESS 주소) 구하는 방법
12101정성태1/5/202011072.NET Framework: 876. C# - PEB(Process Environment Block)를 통해 로드된 모듈 목록 열람
12100정성태1/3/20209115.NET Framework: 875. .NET 3.5 이하에서 IntPtr.Add 사용
... 46  47  48  49  50  51  52  53  54  55  56  57  58  59  [60]  ...