Windows中的系统计时器:重大更改

Windows Scheduler行为在Windows 10 2004中已发生重大变化,没有任何警告或文档更改。这可能会破坏几个应用程序。不是第一次发生这种情况,但是这种变化更为严重。



简而言之,尽管效果仍然存在但从一个进程调用timeBeginPeriod现在对其他进程的影响比以前更少。



我认为新行为本质上是一种改进,但它很奇怪,值得记录。老实说,我警告您-我只有自己实验的结果,因此我只能猜测此更改的目标和一些副作用。如果我的发现有误,请通知我。



计时器中断及其出现的原因



首先,有关操作系统设计的一些背景知识。希望程序能够入睡并随后唤醒。实际上,这不应该经常执行-线程通常等待事件,而不是定时器-但有时是必须的。因此,Windows具有“睡眠”功能 -向其传递所需的睡眠持续时间(以毫秒为单位),它将唤醒该过程:



Sleep(1);



值得考虑如何实现它。理想情况下,当调用Sleep(1)时处理器将进入睡眠状态。但是,如果处理器处于睡眠状态,操作系统如何唤醒线程?答案是硬件中断。操作系统对芯片进行编程-硬件计时器,该计时器随后触发唤醒处理器的中断,然后操作系统启动线程。



WaitForSingleObject和WaitForMultipleObjects函数也具有超时值,并且使用相同的机制来实现这些超时。



如果有许多线程在等待计时器,则OS可以为每个线程在单独的时间编程一个硬件计时器,但这通常会导致以下事实:线程在随机时间唤醒,并且处理器无法正常睡眠。 CPU的电源效率在很大程度上取决于其睡眠时间(正常时间为8ms),并且随机唤醒对此没有帮助。如果多个线程同步或池化其计时器等待,则系统将更加节能。



有多种组合唤醒的方法,但是Windows中的主要机制是以恒定速率全局中断计时器滴答。当线程调用Sleep(n)时,操作系统将安排线程在第一个计时器中断后立即启动。这意味着线程可能会在稍后唤醒,但是Windows并不是实时操作系统,它根本不能保证特定的唤醒时间(此时处理器核心可能很忙),因此稍后唤醒是很正常的。



计时器中断之间的时间间隔取决于Windows和硬件的版本,但是在我的所有计算机上,其默认值为15.625ms(1000ms / 64)。这意味着如果您调用“睡眠”(1)在某个随机时间,该过程将在以后触发下一个全局定时器中断时(或者为时过早,则在以后的时间)触发,在将来的1.0ms到16.625ms之间唤醒。



简而言之,计时器延迟的性质是这样的(除非使用活动的等待处理器,并且请不要使用它),操作系统只能使用计时器中断在特定时间唤醒线程,而Windows使用常规中断。



某些程序无法适应如此广泛的延迟(WPF,SQL Server,Quartz,PowerDirector,Chrome,Go Runtime,许多游戏等)。幸运的是,他们可以使用timeBeginPeriod函数解决问题这允许程序请求较小的间隔。还有一个NtSetTimerResolution函数,该函数允许将间隔设置为小于一毫秒,但是它很少使用并且从不需要,所以我不再赘述。



几十年的疯狂



这是一个疯狂的事情:timeBeginPeriod可以由任何程序调用,并且它会更改计时器中断间隔,并且计时器中断是全局资源。



让我们想象一下,进程A处于调用Sleep(1)的循环中。这是错误的,但是是这样,默认情况下,它每15.625ms或每秒64次唤醒。然后进程B进入并调用timeBeginPeriod(2)。这导致计时器更频繁地触发,并且突然过程A每秒唤醒500次,而不是每秒64次。这太疯狂了!但这就是Windows一直工作的方式。



此时,如果进程C出现并调用timeBeginPeriod(4),这不会改变任何东西-进程A每秒将不断唤醒500次。在这种情况下,设置规则的不是最后一次调用,而是间隔最小的调用。



因此,从任何正在运行的程序中调用timeBeginPeriod可以设置全局计时器中断间隔。如果该程序退出或调用timeEndPeriod,则新的最小值将生效。如果一个程序调用timeBeginPeriod(1),则这是系统范围的计时器中断间隔。如果一个程序调用timeBeginPeriod(1),而另一个程序调用timeBeginPeriod(4),则定时器中断间隔1毫秒成为普遍规律。



这很重要,因为高计时器中断率及其相关的高线程调度率会浪费大量CPU资源,如此处所述



需要基于计时器的计划的一种应用是Web浏览器。 JavaScript标准具有setTimeout函数,该函数要求浏览器在几毫秒后调用JavaScript函数。 Chromium使用计时器来实现此功能和其他功能(基本上是带有超时而不是睡眠的WaitForSingleObject)。这通常需要增加计时器中断率。为了降低电池寿命,最近对Chromium进行了重新设计,以使其计时器中断频率保持低于125 Hz(8ms间隔)



timeGetTime



timeGetTime 函数(不要与GetTickCount混淆)返回当前时间,该时间由计时器中断更新。处理器历来都不擅长保持准确的时间(出于其他原因,它们的时钟故意波动以避免用作FM发射器),因此CPU经常依赖单独的时钟发生器来保持准确的时间。从这些芯片读取数据非常昂贵,这就是Windows维护带有计时器中断更新的64位毫秒计数器的原因。该计时器存储在共享内存中,因此任何进程都可以廉价地从那里读取当前时间,而不必花费时间。timeGetTime调用ReadInterruptTick,它实际上只是读取此64位计数器。就这么简单!



由于计数器是通过定时器中断更新的,因此我们可以对其进行跟踪并找到定时器中断的频率。



新的无证现实



随着Windows 10 2004(2020年4月)的发布,其中的某些机制已稍作更改,但以一种非常混乱的方式。首先,有消息说timeBeginPeriod不再起作用实际上,一切都变得更加复杂。



最初的实验给出了不同的结果。当我运行程序并调用timeBeginPeriod(2)时clockres显示了2.0ms的计时器间隔,但是带有Sleep(1)循环的单独测试程序每秒唤醒了约64次,而不是以前版本的Windows中的500次。



科学实验



然后,我编写了一些程序来研究系统行为。一个程序(change_interval.cpp)只是循环放置,以1到15ms的间隔调用timeBeginPeriod她将每个间隔保持四秒钟,然后继续前进到下一个,依此类推。十五行代码。简单。



另一个程序(measure_interval.cpp)运行多个测试,以检查change_interval.cpp更改时其行为如何更改。该程序监视三个参数。



  1. 她问操作系统使用NtQueryTimerResolution当前全局计时器的分辨率是多少

  2. timeGetTime, ,  — , .

  3. Sleep(1), . .


@FelixPetriconi在Windows 10 1909上为我运行了测试,在Windows 10 2004上运行了测试。这是无抖动的结果:







这意味着timeBeginPeriod仍在所有Windows版本上设置全局计时器间隔。从timeGetTime()结果,我们可以说中断是在至少一个处理器内核上以此速率触发的,并且时间已更新。还要注意,1909年第一行中的2.0在Windows XP中也是2.0,在Windows 7/8中也是1.0,然后似乎又回到了2.0?



但是,调度行为在Windows 10 2004中发生了巨大变化。以前,睡眠(1)的延迟在任何进程中,除了timeBeginPeriod(1)之外,其他时间都等于计时器中断间隔,它给出了如下图:







在Windows 10 2004中,另一个进程(未调用timeBeginPeriod)中timeBeginPeriod和睡眠等待时间之间的关系看起来很奇怪: 不清楚图的左侧的确切形状,但是它肯定与上一个方向相反! 为什么?















特效



正如在有关reddit和hacker-news的讨论中所指出的那样,考虑到全局计时器中断的可用精度,图表的左半部分可能试图尽可能地模仿“正常”延迟。即,以6毫秒的中断间隔,延迟发生约12 ms(两个周期),而以7毫秒的中断间隔,将延迟约14 ms(两个周期)。但是,测量实际的延迟表明,现实更加令人困惑。将计时器中断设置为7ms时,最常见的结果甚至不是14msSleep(1)延迟







某些读者可能将随机噪声归咎于系统,但是当计时器中断率为9ms或更高时,该噪声为零,因此不是可能是一个解释。尝试自己运行更新的代码。定时器中断间隔从4ms到8ms似乎特别有争议。间隔测量可能应该使用QueryPerformanceCounter进行,因为当前代码受调度规则的更改和计时器精度的更改随机影响。



这一切都非常奇怪,我不了解其逻辑或实现。这可能是一个错误,但我对此表示怀疑。我认为这背后有一个复杂的向后兼容逻辑。但是,避免兼容性问题的最有效方法是最好将更改记录在案,最好是事先记录下来,在此进行的修改恕不另行通知。



这不会影响大多数程序。如果进程需要更快的计时器中断,则它本身必须调用timeBeginPeriod... 但是,可能会出现以下问题:



  • 程序可能会意外地假设Sleep(1)timeGetTime具有相同的分辨率,而不再是这种情况。虽然,这样的假设似乎不太可能。

  • , .  — Windows System Timer Tool TimerResolution 1.2. «» , . , . , , .

  • , , . , . . , , timeBeginPeriod , , .




change_interval.cpp 测试程序在没人请求更高的计时器中断率的情况下起作用。由于Chrome和Visual Studio都有这样做的习惯,因此我不得不在没有Internet访问和在记事本中进行编码的情况下进行大部分实验有人建议使用Emacs,但参与讨论超出了我的能力范围。



All Articles