windows - 用户空间中断计时器访问,例如通过 KeQueryInterruptTime(或类似的)

标签 windows winapi timer

是否有“Nt”或类似的(即非内核模式驱动程序)函数等同于 KeQueryInterruptTime 或类似的东西?好像没有NtQueryInterruptTime之类的东西,至少我没找到。

我想要的是某种相当准确和可靠的单调计时器(因此不是 QPC)它相当高效并且不会像溢出的 32 位计数器那样令人惊讶,并且没有不必要的“智能”、无时区或复杂结构。

理想情况下,我想要类似 timeGetTime 的 64 位值。它甚至不必是同一个计时器。
从 Vista 开始存在 GetTickCount64,这本身是可以接受的,但我不想仅仅因为这样一个愚蠢的原因而中断 XP 支持。

读取 0x7FFE0008 处的四字,如 here 所示...好吧,有效 ...它证明实际的内部计数器在 XP 下确实是 64 位的(它也是尽可能快的),但是嗯...让我们不要说话关于读取一些未知的、硬编码的内存位置是一种多么讨厌的黑客行为。

在调用一个人为的昏迷(将 64 位计数器缩小到 32 位)高级 API 函数和读取原始内存地址之间一定有什么东西?

最佳答案

下面是 GetTickCount() 的线程安全包装器示例,它将滴答计数值扩展到 64 位,并且等效于 GetTickCount64()。

为避免意外的计数器翻转,请确保每 49.7 天调用此函数几次。您甚至可以拥有一个专用线程,其唯一目的是调用此函数,然后在无限循环中休眠大约 20 天。

ULONGLONG MyGetTickCount64(void)
{
  static volatile LONGLONG Count = 0;
  LONGLONG curCount1, curCount2;
  LONGLONG tmp;

  curCount1 = InterlockedCompareExchange64(&Count, 0, 0);

  curCount2 = curCount1 & 0xFFFFFFFF00000000;
  curCount2 |= GetTickCount();

  if ((ULONG)curCount2 < (ULONG)curCount1)
  {
    curCount2 += 0x100000000;
  }

  tmp = InterlockedCompareExchange64(&Count, curCount2, curCount1);

  if (tmp == curCount1)
  {
    return curCount2;
  }
  else
  {
    return tmp;
  }
}

编辑:这是一个测试 MyGetTickCount64() 的完整应用程序。

// Compiled with Open Watcom C 1.9: wcl386.exe /we /wx /q gettick.c

#include <windows.h>
#include <stdio.h>
#include <stdarg.h>
#include <stdlib.h>

//
// The below code is an ugly implementation of InterlockedCompareExchange64()
// that is apparently missing in Open Watcom C 1.9.
// It must work with MSVC++ too, however.
//
UINT8 Cmpxchg8bData[] =
{
  0x55,             // push      ebp
  0x89, 0xE5,       // mov       ebp, esp
  0x57,             // push      edi
  0x51,             // push      ecx
  0x53,             // push      ebx
  0x8B, 0x7D, 0x10, // mov       edi, [ebp + 0x10]
  0x8B, 0x07,       // mov       eax, [edi]
  0x8B, 0x57, 0x04, // mov       edx, [edi + 0x4]
  0x8B, 0x7D, 0x0C, // mov       edi, [ebp + 0xc]
  0x8B, 0x1F,       // mov       ebx, [edi]
  0x8B, 0x4F, 0x04, // mov       ecx, [edi + 0x4]
  0x8B, 0x7D, 0x08, // mov       edi, [ebp + 0x8]
  0xF0,             // lock:
  0x0F, 0xC7, 0x0F, // cmpxchg8b [edi]
  0x5B,             // pop       ebx
  0x59,             // pop       ecx
  0x5F,             // pop       edi
  0x5D,             // pop       ebp
  0xC3              // ret
};

LONGLONG (__cdecl *Cmpxchg8b)(LONGLONG volatile* Dest, LONGLONG* Exch, LONGLONG* Comp) =
  (LONGLONG (__cdecl *)(LONGLONG volatile*, LONGLONG*, LONGLONG*))Cmpxchg8bData;

LONGLONG MyInterlockedCompareExchange64(LONGLONG volatile* Destination,
                                        LONGLONG Exchange,
                                        LONGLONG Comparand)
{
  return Cmpxchg8b(Destination, &Exchange, &Comparand);
}

#ifdef InterlockedCompareExchange64
#undef InterlockedCompareExchange64
#endif

#define InterlockedCompareExchange64(Destination, Exchange, Comparand) \
  MyInterlockedCompareExchange64(Destination, Exchange, Comparand)

//
// This stuff makes a thread-safe printf().
// We don't want characters output by one thread to be mixed
// with characters output by another. We want printf() to be
// "atomic".
// We use a critical section around vprintf() to achieve "atomicity".
//
static CRITICAL_SECTION PrintfCriticalSection;

int ts_printf(const char* Format, ...)
{
  int count;
  va_list ap;

  EnterCriticalSection(&PrintfCriticalSection);

  va_start(ap, Format);
  count = vprintf(Format, ap);
  va_end(ap);

  LeaveCriticalSection(&PrintfCriticalSection);

  return count;
}

#define TICK_COUNT_10MS_INCREMENT 0x800000

//
// This is the simulated tick counter.
// Its low 32 bits are going to be returned by
// our, simulated, GetTickCount().
//
// TICK_COUNT_10MS_INCREMENT is what the counter is
// incremented by every time. The value is so chosen
// that the counter quickly overflows in its
// low 32 bits.
//
static volatile LONGLONG SimulatedTickCount = 0;

//
// This is our simulated 32-bit GetTickCount()
// that returns a count that often overflows.
//
ULONG SimulatedGetTickCount(void)
{
  return (ULONG)SimulatedTickCount;
}

//
// This thread function will increment the simulated tick counter
// whose value's low 32 bits we'll be reading in SimulatedGetTickCount().
//
DWORD WINAPI SimulatedTickThread(LPVOID lpParameter)
{
  UNREFERENCED_PARAMETER(lpParameter);

  for (;;)
  {
    LONGLONG c;

    Sleep(10);

    // Get the counter value, add TICK_COUNT_10MS_INCREMENT to it and
    // store the result back.
    c = InterlockedCompareExchange64(&SimulatedTickCount, 0, 0);
    InterlockedCompareExchange64(&SimulatedTickCount, c + TICK_COUNT_10MS_INCREMENT, c) != c);
  }

  return 0;
}

volatile LONG CountOfObserved32bitOverflows = 0;
volatile LONG CountOfObservedUpdateRaces = 0;

//
// This prints statistics that includes the true 64-bit value of
// SimulatedTickCount that we can't get from SimulatedGetTickCount() as it
// returns only its lower 32 bits.
//
// The stats also include:
// - the number of times that MyGetTickCount64() observes an overflow of
//   SimulatedGetTickCount()
// - the number of times MyGetTickCount64() fails to update its internal
//   counter because of a concurrent update in another thread.
//
void PrintStats(void)
{
  LONGLONG true64bitCounter = InterlockedCompareExchange64(&SimulatedTickCount, 0, 0);

  ts_printf("  0x%08X`%08X <- true 64-bit count; ovfs: ~%d; races: %d\n",
            (ULONG)(true64bitCounter >> 32),
            (ULONG)true64bitCounter,
            CountOfObserved32bitOverflows,
            CountOfObservedUpdateRaces);
}

//
// This is our poor man's implementation of GetTickCount64()
// on top of GetTickCount().
//
// It's thread safe.
//
// When used with actual GetTickCount() instead of SimulatedGetTickCount()
// it must be called at least a few times in 49.7 days to ensure that
// it doesn't miss any overflows in GetTickCount()'s return value.
//
ULONGLONG MyGetTickCount64(void)
{
  static volatile LONGLONG Count = 0;
  LONGLONG curCount1, curCount2;
  LONGLONG tmp;

  curCount1 = InterlockedCompareExchange64(&Count, 0, 0);

  curCount2 = curCount1 & 0xFFFFFFFF00000000;
  curCount2 |= SimulatedGetTickCount();

  if ((ULONG)curCount2 < (ULONG)curCount1)
  {
    curCount2 += 0x100000000;

    InterlockedIncrement(&CountOfObserved32bitOverflows);
  }

  tmp = InterlockedCompareExchange64(&Count, curCount2, curCount1);

  if (tmp != curCount1)
  {
    curCount2 = tmp;

    InterlockedIncrement(&CountOfObservedUpdateRaces);
  }

  return curCount2;
}

//
// This is an error counter. If a thread that uses MyGetTickCount64() notices
// any problem with what MyGetTickCount64() returns, it bumps up this error
// counter and stops. If one of threads sees a non-zero value in this
// counter due to an error in another thread, it stops as well.
//
volatile LONG Error = 0;

//
// This is a thread function that will be using MyGetTickCount64(),
// validating its return value and printing some stats once in a while.
//
// This function is meant to execute concurrently in multiple threads
// to create race conditions inside of MyGetTickCount64() and test it.
//
DWORD WINAPI TickUserThread(LPVOID lpParameter)
{
  DWORD user = (DWORD)lpParameter; // thread number
  ULONGLONG ticks[4];

  ticks[3] = ticks[2] = ticks[1] = MyGetTickCount64();

  while (!Error)
  {
    ticks[0] = ticks[1];
    ticks[1] = MyGetTickCount64();

    // Every ~100 ms sleep a little (slightly lowers CPU load, to about 90%)
    if (ticks[1] > ticks[2] + TICK_COUNT_10MS_INCREMENT * 10L)
    {
      ticks[2] = ticks[1];
      Sleep(1 + rand() % 20);
    }

    // Every ~1000 ms print the last value from MyGetTickCount64().
    // Thread 1 also prints stats here.
    if (ticks[1] > ticks[3] + TICK_COUNT_10MS_INCREMENT * 100L)
    {
      ticks[3] = ticks[1];
      ts_printf("%u:0x%08X`%08X\n", user, (ULONG)(ticks[1] >> 32), (ULONG)ticks[1]);

      if (user == 1)
      {
        PrintStats();
      }
    }

    if (ticks[0] > ticks[1])
    {
      ts_printf("%u:Non-monotonic tick counts: 0x%016llX > 0x%016llX!\n",
                user,
                ticks[0],
                ticks[1]);
      PrintStats();
      InterlockedIncrement(&Error);
      return -1;
    }
    else if (ticks[0] + 0x100000000 <= ticks[1])
    {
      ts_printf("%u:Too big tick count jump: 0x%016llX -> 0x%016llX!\n",
                user,
                ticks[0],
                ticks[1]);
      PrintStats();
      InterlockedIncrement(&Error);
      return -1;
    }

    Sleep(0); // be nice, yield to other threads.
  }

  return 0;
}

//
// This prints stats upon Ctrl+C and terminates the program.
//
BOOL WINAPI ConsoleEventHandler(DWORD Event)
{
  if (Event == CTRL_C_EVENT)
  {
    PrintStats();
  }

  return FALSE;
}

int main(void)
{
  HANDLE simulatedTickThreadHandle;
  HANDLE tickUserThreadHandle;
  DWORD dummy;

  // This is for the missing InterlockedCompareExchange64() workaround.
  VirtualProtect(Cmpxchg8bData, sizeof(Cmpxchg8bData), PAGE_EXECUTE_READWRITE, &dummy);

  InitializeCriticalSection(&PrintfCriticalSection);

  if (!SetConsoleCtrlHandler(&ConsoleEventHandler, TRUE))
  {
    ts_printf("SetConsoleCtrlHandler(&ConsoleEventHandler) failed with error 0x%X\n", GetLastError());
    return -1;
  }

  // Start the tick simulator thread.

  simulatedTickThreadHandle = CreateThread(NULL, 0, &SimulatedTickThread, NULL, 0, NULL);

  if (simulatedTickThreadHandle == NULL)
  {
    ts_printf("CreateThread(&SimulatedTickThread) failed with error 0x%X\n", GetLastError());
    return -1;
  }

  // Start one thread that'll be using MyGetTickCount64().

  tickUserThreadHandle = CreateThread(NULL, 0, &TickUserThread, (LPVOID)2, 0, NULL);
  if (tickUserThreadHandle == NULL)
  {
    ts_printf("CreateThread(&TickUserThread) failed with error 0x%X\n", GetLastError());
    return -1;
  }

  // The other thread using MyGetTickCount64() will be the main thread.

  TickUserThread((LPVOID)1);

  //
  // The app terminates upon any error condition detected in TickUserThread()
  // in any of the threads or by Ctrl+C.
  //

  return 0;
}

作为测试,我已经在一台有 2 个 CPU 的空闲机器上运行这个测试应用程序 5 个多小时(空闲,以避免潜在的长时间饥饿,因此避免错过每 5 秒发生的计数器溢出) 并且仍然表现良好。

这是控制台的最新输出:

2:0x00000E1B`C8800000
1:0x00000E1B`FA800000
  0x00000E1B`FA800000 <- true 64-bit count; ovfs: ~3824; races: 110858

如您所见,MyGetTickCount64() 已观察到 3824 次 32 位溢出,并且无法使用其第二个 InterlockedCompareExchange64()< 更新 Count 的值 110858 次。因此,溢出确实发生了,最后一个数字意味着该变量实际上正在被两个线程同时更新。

您还可以看到,这两个线程在 TickUserThread() 中从 MyGetTickCount64() 接收到的 64 位滴答计数在顶部没有任何遗漏32 位,非常接近 SimulatedTickCount 中的实际 64 位滴答计数,其低 32 位由 SimulatedGetTickCount() 返回。由于线程调度和不频繁的统计打印,0x00000E1BC8800000 在视觉上落后于 0x00000E1BFA800000,它正好落后 100*TICK_COUNT_10MS_INCREMENT,即 1 秒。当然,在内部,差异要小得多。

现在,关于 InterlockedCompareExchange64() 的可用性...有点奇怪,它是 officially available since Windows Vista and Windows Server 2003 . Server 2003 实际上是从与 Windows XP 相同的代码库构建的。

但这里最重要的是,此函数是建立在 Pentium CMPXCHG8B 指令之上的,该指令自 1998 年或更早以来就已可用 (1) , (2) .我可以在我的 Windows XP (SP3) 二进制文件中看到这条指令。它在 ntkrnlpa.exe/ntoskrnl.exe(内核)和 ntdll.dll(导出内核的 NtXxxx() 函数的 DLL)中建立在)。查找 0xF0、0x0F、0xC7 的字节序列并反汇编该位置周围的代码以查看这些字节是否巧合。

您可以通过 CPUID 指令(CPUID 函数 0x00000001 和函数 0x80000001 的 EDX 位 8)检查该指令的可用性,如果指令不存在则拒绝运行而不是崩溃,但是这些天你不太可能找到不支持此指令的机器。如果这样做,它就不是适合 Windows XP 的好机器,也可能不适合您的应用程序。

关于windows - 用户空间中断计时器访问,例如通过 KeQueryInterruptTime(或类似的),我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/8211820/

相关文章:

c - Arduino 问题 : Using TimerOne and delayMicroseconds()

ios - 如何使用 Date() 函数和应用程序位于前台和后台时的时间戳来获取耗时?

windows - Windows批处理程序中读取文件名

windows - 如何在 Windows 中远程执行脚本?

windows - 为什么 Cordova Windows 8 应用程序导致 wwahost.exe 中出现未处理的 win32 异常?

winapi - 如何初始化 LPBOOL? Kotlin Native 中的 Win32 Api

c++ - 填充矩形类型的 PictureBox,C++ WINAPI

windows - 无法在具有自签名证书的 Windows 上使用 git 解析 "unable to get local issuer certificate"

c - 如何并行化 Windows 消息循环和 Hook 回调逻辑及其创建的 Ruby 线程?

java - 当项目可见 2-3 秒时,如何将 View 增加到 Recyclerview 项目?