Systemtimer in Windows: große Veränderung

Das Verhalten von Windows Scheduler hat sich in Windows 10 2004 ohne Warnungen oder Dokumentationsänderungen erheblich geändert. Dies wird wahrscheinlich mehrere Anwendungen beschädigen. Dies ist nicht das erste Mal, dass dies passiert ist , aber diese Änderung ist schwerwiegender.



Kurz gesagt, Aufrufe von timeBeginPeriod von einem Prozess wirken sich jetzt weniger auf andere Prozesse aus als zuvor, obwohl der Effekt immer noch vorhanden ist.



Ich denke, das neue Verhalten ist im Wesentlichen eine Verbesserung, aber es ist seltsam und verdient es, dokumentiert zu werden. Ehrlich gesagt, ich warne Sie - ich habe nur die Ergebnisse meiner eigenen Experimente, daher kann ich nur die Ziele und einige Nebenwirkungen dieser Änderung erraten. Wenn einer meiner Befunde falsch ist, lassen Sie es mich bitte wissen.



Timer-Interrupts und ihr Grund zu sein



Zunächst ein kleiner Kontext zum Betriebssystemdesign. Es ist wünschenswert, dass das Programm einschlafen und später aufwachen kann. Tatsächlich sollte dies nicht sehr oft gemacht werden - Threads warten normalerweise auf Ereignisse, nicht auf Timer -, aber manchmal ist es notwendig. Windows verfügt also über eine Sleep- Funktion.  Übergeben Sie die gewünschte Sleep-Dauer in Millisekunden, um den Vorgang zu



aktivieren : Sleep (1);



Es lohnt sich zu überlegen, wie dies umgesetzt wird. Wenn Sleep (1) aufgerufen wird, geht der Prozessor im Idealfall in den Ruhezustand. Aber wie weckt das Betriebssystem den Thread, wenn der Prozessor im Ruhezustand ist? Die Antwort sind Hardware-Interrupts. Das Betriebssystem programmiert einen Chip - einen Hardware-Timer, der dann einen Interrupt auslöst, der den Prozessor aufweckt, und das Betriebssystem startet dann Ihren Thread.



Die Funktionen WaitForSingleObject und WaitForMultipleObjects haben ebenfalls Zeitüberschreitungswerte, und diese Zeitüberschreitungen werden mit demselben Mechanismus implementiert.



Wenn viele Threads auf Timer warten, kann das Betriebssystem für jeden Thread einen Hardware-Timer für eine einzelne Zeit programmieren. Dies führt jedoch normalerweise dazu, dass Threads zu einer zufälligen Zeit aufwachen und der Prozessor nicht normal schläft. Die Energieeffizienz der CPU hängt stark von ihrer Ruhezeit ab (die normale Zeit beträgt 8 ms ), und zufällige Nachläufe tragen nicht dazu bei. Wenn mehrere Threads ihre Timer-Erwartungen synchronisieren oder bündeln, wird das System energieeffizienter.



Es gibt viele Möglichkeiten, Wakes zu kombinieren, aber der Hauptmechanismus in Windows besteht darin, einen Timer, der mit einer konstanten Rate tickt, global zu unterbrechen. Wenn ein Thread Sleep (n) aufruft , plant das Betriebssystem, dass der Thread unmittelbar nach dem ersten Timer-Interrupt gestartet wird. Dies bedeutet, dass der Thread möglicherweise etwas später aufwacht, Windows jedoch kein Echtzeitbetriebssystem ist und keine bestimmte Aufweckzeit garantiert (zu diesem Zeitpunkt sind die Prozessorkerne möglicherweise ausgelastet). Daher ist es ganz normal, etwas später aufzuwachen.



Das Intervall zwischen Timer-Interrupts hängt von der Windows- und Hardwareversion ab, auf allen meinen Computern ist es jedoch standardmäßig 15,625 ms (1000 ms / 64). Dies bedeutet, dass wenn Sie Sleep (1) anrufenZu einem zufälligen Zeitpunkt wird der Prozess in der Zukunft zwischen 1,0 ms und 16,625 ms aktiviert, wenn der nächste globale Timer-Interrupt ausgelöst wird (oder einmal später, wenn es zu früh ist).



Kurz gesagt, die Art der Timer-Verzögerungen ist so, dass das Betriebssystem (sofern nicht aktiv auf den Prozessor gewartet wird und dieser bitte nicht verwendet wird ) Threads nur zu einem bestimmten Zeitpunkt mithilfe von Timer-Interrupts aktivieren kann und Windows reguläre Interrupts verwendet.



Einige Programme berücksichtigen nicht so viele Latenzen (WPF, SQL Server, Quartz, PowerDirector, Chrome, Go Runtime, viele Spiele usw.). Glücklicherweise können sie das Problem mit der timeBeginPeriod- Funktion behebenDadurch kann das Programm ein kleineres Intervall anfordern. Es gibt auch eine NtSetTimerResolution- Funktion , mit der das Intervall auf weniger als eine Millisekunde eingestellt werden kann. Sie wird jedoch selten verwendet und nie benötigt, sodass ich sie nicht noch einmal erwähnen werde.



Jahrzehntelanger Wahnsinn



Hier ist eine verrückte Sache: timeBeginPeriod kann von jedem Programm aufgerufen werden und ändert das Timer-Interrupt-Intervall, und der Timer-Interrupt ist eine globale Ressource.



Stellen wir uns vor, Prozess A befindet sich in einer Schleife mit einem Aufruf von Sleep (1) . Dies ist falsch, aber es ist und standardmäßig alle 15,625 ms oder 64 Mal pro Sekunde aktiviert. Dann kommt Prozess B herein und ruft timeBeginPeriod (2) auf . Dies führt dazu, dass der Timer häufiger ausgelöst wird und Prozess A plötzlich 500 Mal pro Sekunde statt 64 Mal pro Sekunde aufwacht. Das ist Wahnsinn! Aber so hat Windows immer funktioniert.



Zu diesem Zeitpunkt, wenn Prozess C erschien und timeBeginPeriod (4) aufrief, das würde nichts ändern - Prozess A würde 500 Mal pro Sekunde aufwachen. In einer solchen Situation legt nicht der letzte Aufruf die Regeln fest, sondern der Aufruf mit dem Mindestintervall.



Somit kann ein Aufruf von timeBeginPeriod von einem beliebigen laufenden Programm das globale Timer-Interrupt-Intervall festlegen. Wenn dieses Programm timeEndPeriod beendet oder aufruft , wird das neue Minimum wirksam. Wenn ein Programm timeBeginPeriod (1) aufruft , ist dies jetzt das systemweite Timer-Interrupt-Intervall. Wenn ein Programm timeBeginPeriod (1) und ein anderes timeBeginPeriod (4) aufruft , wird das Timer-Interrupt-Intervall von einer Millisekunde zu einem universellen Gesetz.



Dies ist wichtig, da eine hohe Timer-Interrupt-Rate - und die damit verbundene hohe Thread-Planungsrate - erhebliche CPU-Leistung verschwenden kann, wie hier erläutert .



Eine Anwendung, die eine zeitgesteuerte Planung benötigt, ist der Webbrowser. Der JavaScript-Standard verfügt über eine setTimeout- Funktion , die den Browser auffordert , nach einigen Millisekunden eine JavaScript-Funktion aufzurufen. Chromium verwendet Timer, um diese und andere Funktionen zu implementieren (im Grunde WaitForSingleObject mit Timeouts, nicht Sleep). Dies erfordert häufig eine erhöhte Timer-Interrupt-Rate. Um die Batterielebensdauer niedrig zu halten, wurde Chromium kürzlich überarbeitet, um die Timer-Interrupt-Rate bei Batteriestrom unter 125 Hz (Intervall 8 ms) zu halten .



timeGetTime



Die timeGetTime- Funktion (nicht zu verwechseln mit GetTickCount) gibt die aktuelle Zeit zurück, die durch einen Timer-Interrupt aktualisiert wurde. Prozessoren waren in der Vergangenheit nicht sehr gut darin, eine genaue Zeit zu halten (ihre Uhren werden absichtlich oszilliert, um nicht als FM-Sender zu dienen, und aus anderen Gründen), weshalb CPUs häufig auf separate Taktgeneratoren angewiesen sind, um eine genaue Zeit aufrechtzuerhalten. Das Lesen von diesen Chips ist teuer, weshalb Windows einen 64-Bit-Millisekundenzähler verwaltet, der mit einem Timer-Interrupt aktualisiert wird. Dieser Timer wird im gemeinsamen Speicher gespeichert, sodass jeder Prozess die aktuelle Uhrzeit von dort kostengünstig ablesen kann, ohne zur Uhr gehen zu müssen. timeGetTime ruft ReadInterruptTick auf , das im Wesentlichen nur diesen 64-Bit-Zähler liest. So einfach ist das!



Da der Zähler durch den Timer-Interrupt aktualisiert wird, können wir ihn aufspüren und die Timer-Interrupt-Frequenz ermitteln.



Neue undokumentierte Realität



Mit der Veröffentlichung von Windows 10 2004 (April 2020) haben sich einige dieser Mechanismen geringfügig geändert, jedoch auf sehr verwirrende Weise. Erstens gab es Meldungen, dass timeBeginPeriod nicht mehr funktionierte . Tatsächlich stellte sich heraus, dass alles viel komplizierter war.



Die ersten Experimente ergaben gemischte Ergebnisse. Wenn ich das Programm mit einem Aufruf an lief timebeginperiod (2) , ClockRes zeigte ein Timer - Intervall von 2.0ms , sondern ein separates Testprogramm mit einer Sleep (1) Schleife wachte pro Sekunde statt 500 mal etwa 64 mal wie in früheren Versionen von Windows.



Wissenschaftliches Experiment



Dann schrieb ich ein paar Programme, um das Systemverhalten zu untersuchen. Ein Programm ( change_interval.cpp ) befindet sich nur in einer Schleife und ruft timeBeginPeriod in Intervallen von 1 bis 15 ms auf. Sie hält jedes Intervall vier Sekunden lang und geht dann zum nächsten über und so weiter im Kreis. Fünfzehn Codezeilen. Einfach.



Ein anderes Programm ( Measure_interval.cpp ) führt mehrere Tests durch, um zu überprüfen, wie sich sein Verhalten ändert, wenn sich change_interval.cpp ändert. Das Programm überwacht drei Parameter.



  1. Sie fragt das Betriebssystem, wie die aktuelle Auflösung des globalen Timers NtQueryTimerResolution verwendet .

  2. timeGetTime, ,  — , .

  3. Sleep(1), . .


@FelixPetriconi führte Tests für mich unter Windows 10 1909 und Tests unter Windows 10 2004 durch. Hier sind die jitterfreien Ergebnisse:







Dies bedeutet, dass timeBeginPeriod weiterhin das globale Zeitintervall für alle Windows-Versionen festlegt. Aus den Ergebnissen von timeGetTime () können wir sagen, dass der Interrupt mit dieser Rate auf mindestens einem Prozessorkern ausgelöst wird und die Zeit aktualisiert wird. Beachten Sie auch, dass 2.0 in der ersten Zeile für 1909 auch 2.0 in Windows XP , dann 1.0 in Windows 7/8 war und dann wieder auf 2.0 zurückgekehrt zu sein scheint?



Das Planungsverhalten in Windows 10 2004 ändert sich jedoch dramatisch. Zuvor war die Verzögerung für den Ruhezustand (1)In jedem Prozess entspricht nur das Timer-Interrupt-Intervall, mit Ausnahme von timeBeginPeriod (1).







In Windows 10 2004 sieht die Beziehung zwischen timeBeginPeriod und der Schlaflatenz in einem anderen Prozess (der timeBeginPeriod nicht aufgerufen hat ) seltsam aus:







Die genaue Form der linken Seite des Diagramms ist jedoch unklar es geht definitiv in die entgegengesetzte Richtung zum vorherigen!



Warum?



Auswirkungen



Wie in der Diskussion zu Reddit und Hacker-News ausgeführt, ist es wahrscheinlich, dass die linke Hälfte des Diagramms ein Versuch ist, die "normale" Latenz angesichts der verfügbaren Genauigkeit des globalen Timer-Interrupts so genau wie möglich nachzuahmen. Das heißt, bei einem Interruptintervall von 6 Millisekunden tritt die Verzögerung um ungefähr 12 ms (zwei Zyklen) auf, und bei einem Interruptintervall von 7 Millisekunden wird sie um ungefähr 14 ms (zwei Zyklen) verzögert. Die Messung der tatsächlichen Verzögerungen zeigt jedoch, dass die Realität noch verwirrender ist. Wenn ein Timer-Interrupt auf 7 ms eingestellt ist, ist eine Sleep (1) -Latenz von 14 ms nicht einmal das häufigste Ergebnis:







Einige Leser geben dem System möglicherweise zufälliges Rauschen, aber wenn die Timer-Interrupt-Rate 9 ms oder mehr beträgt, ist das Rauschen Null, also nicht könnte eine Erklärung sein.Versuchen Sie, den aktualisierten Code selbst auszuführen . Die Timer-Interrupt-Intervalle von 4 ms bis 8 ms scheinen besonders umstritten zu sein. Intervallmessungen sollten wahrscheinlich mit QueryPerformanceCounter durchgeführt werden, da der aktuelle Code zufällig von Änderungen der Planungsregeln und Änderungen der Timergenauigkeit beeinflusst wird.



Das ist alles sehr seltsam und ich verstehe die Logik oder die Implementierung nicht. Das mag ein Fehler sein, aber ich bezweifle es. Ich denke, dahinter steckt eine komplexe Abwärtskompatibilitätslogik. Der effektivste Weg, um Kompatibilitätsprobleme zu vermeiden, besteht darin, die Änderungen vorzugsweise im Voraus zu dokumentieren. Hier werden die Änderungen ohne vorherige Ankündigung vorgenommen.



Dies wirkt sich nicht auf die meisten Programme aus. Wenn ein Prozess eine schnellere Timer-Unterbrechung wünscht, muss er timeBeginPeriod selbst aufrufen.... Die folgenden Probleme können jedoch auftreten:



  • Das Programm geht möglicherweise versehentlich davon aus, dass Sleep (1) und timeGetTime dieselbe Auflösung haben, was nicht mehr der Fall ist. Diese Annahme erscheint jedoch unwahrscheinlich.

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

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




Das Testprogramm change_interval.cpp funktioniert nur, wenn niemand eine höhere Timer-Interrupt-Rate anfordert. Da sowohl Chrome als auch Visual Studio die Gewohnheit haben, dies zu tun, musste ich den größten Teil meiner Experimente ohne Internetzugang und Codierung im Editor durchführen . Jemand schlug Emacs vor, aber es liegt außerhalb meiner Macht, mich auf diese Diskussion einzulassen.



All Articles