Update, 11. Oktober 2026, 16:30: Ich habe inzwischen BIOS 1.30 geflasht. Es behebt den Versatz nicht. tscsync bringt die Kerne immer noch in eine Linie, aber an zwei Sync-Boots hat der Kernel den TSC trotzdem fallen lassen, diesmal über seinen Clocksource-Watchdog statt über die Warp-Prüfung: BIOS 1.30: Der Sync hält, der TSC geht trotzdem. Mit zusätzlich tsc=nowatchdog hielt der TSC, und fünf Minuten Messung gegen die Echtzeituhr fanden keine Warps und keine Drift: tsc=nowatchdog: Fünf Minuten, keine Warps. Ein Boot ist keine Reihe von Boots, deshalb nenne ich das hält, nicht behoben.

Jeder Boot, dieselbe Warnung

Das stand bei jedem einzelnen Boot im Kernel-Log meines ThinkPad E595:

TSC synchronization [CPU#0 -> CPU#2]:
Measured 6601281288 cycles TSC warp between CPUs, turning off TSC clock.
tsc: Marking TSC unstable due to check_tsc_sync_source failed
clocksource: Switched to clocksource hpet

Nichts ist abgestürzt. Auf dem Desktop ist nichts aufgegangen. Die Maschine hat ihre ganze Uhr ab dem Booten still auf einem langsameren Timer-Chip laufen lassen, und das hätte sie ewig so weitergemacht, wenn ich das Journal nicht wegen etwas anderem gelesen hätte.

Der Time Stamp Counter (TSC) ist ein Zähler in jedem CPU-Kern, der mit fester Rate tickt. Ihn zu lesen ist ein einziger Befehl. Linux nutzt ihn als Hauptuhr, aber nur, wenn alle Kerne beim Zählerstand übereinstimmen1. Hängt ein Kern hinterher, kann der Kernel ihm nicht trauen und fällt auf den High Precision Event Timer (HPET) zurück, ein eigenes Timer-Gerät. Der Autor von tscsync hat ein Uhrlesen auf HPET mit etwa 1,2 µs gemessen, gegen einige zehn Nanosekunden auf dem TSC, und Kernel und Desktop lesen die Uhr zehntausende Male pro Sekunde1. Das ist Latenz und Akku, bei jedem Boot bezahlt, für nichts.

Wie weit hing der Kern also hinterher? Über drei Sekunden. Auf einem Laptop mit 2,1 GHz sind das 6,6 Milliarden Takte.

Was die Firmware übergibt

Die Meldung nennt CPU 0 und CPU 2, aber der Fehler liegt nicht bei CPU 2. Von tscsync aus dem Bootloader gemessen und vom Kernel, sobald er startet, ist das Bild jedes Mal dasselbe:

KernVersatz zu CPU 0Was das bedeutet
CPU 0, der Boot-Prozessor (BSP)0die Referenz, und der Ausreißer
CPUs 1 bis 7, die anderen Kerne (APs)etwa +6,2 bis +6,8 Milliarden Takteuntereinander auf 32 Takte einig
Abstand bei 2.096 MHz2,9 bis 3,3 Sekundender Boot-Prozessor hängt hinterher, bei jedem Boot

Sieben Kerne sind sich einig. Einer ist drei Sekunden zu spät. Die Firmware fährt den Boot-Prozessor hoch, erledigt ihre Arbeit auf ihm und übergibt an den Bootloader, ohne diesen einen Zähler je wieder mit dem Rest in eine Linie zu bringen.

Der TSC des Boot-Prozessors vor und nach tscsyncVorher: wie die Firmware übergibtNachher: tscsync im Sync-ModusCPU 0 (BSP)CPU 1CPU 2CPU 3CPU 4CPU 5CPU 6CPU 7CPU 0 (BSP)CPU 1CPU 2CPU 3CPU 4CPU 5CPU 6CPU 7CPU 0 etwa 6,8 Mrd. Takte (3,25 s) zurückCPUs 1 bis 7 auf 32 Takte einigBSP vorgeschoben, jeder Kern innerhalb von 45 TaktenLinux behält die TSC-Clocksource
Links: was die Firmware übergibt. Die sieben Anwendungsprozessoren sind sich auf 32 Takte einig; der Boot-Prozessor liegt etwa 6,8 Milliarden Takte hinter ihnen. Rechts: Nachdem tscsync den Boot-Prozessor in UEFI vorgeschoben hat, liegt jeder Kern innerhalb von 45 Takten, und die Warp-Prüfung des Kernels besteht.

Warum kann Linux das nicht einfach reparieren? Auf Intel-CPUs gibt es ein Register, IA32_TSC_ADJUST, mit dem der Kernel den Zähler eines Kerns nachstellen kann, und der Sync-Code des Kernels nutzt es, wenn es da ist2. AMD-Zen-CPUs haben es nicht. Ohne es kann nichts in Linux den Versatz reparieren, und der Versuch, die Zähler im Kernel direkt zu schreiben (tsc=directsync), wurde upstream als unzuverlässig abgelehnt3.

Also tut der Kernel das einzig Ehrliche, das ihm bleibt. Er misst, findet zwischen zwei Kernen rückwärts laufende Zeit, gibt die Warnung aus und hört auf, den TSC zu nutzen2.

Vierzig Boots, vierzig Warps

Ein Boot ist eine Anekdote. Das Journal auf dieser Maschine reicht bis zum 30. Mai 2026 zurück, also habe ich es gefragt.

BootsWarp, TakteWarp bei 2.096 MHz
30. Mai bis 11. Oktober 202640 von 406,16 bis 9,88 Milliarden2,94 bis 4,72 s
Median6,57 Milliarden3,14 s
Nach komplettem Ausschalten für etwa 27 s16,60 Milliarden3,15 s
Mit tscsync im Sync-Modus, BIOS 1.281keiner protokolliertTSC behalten
BIOS 1.30, tscsync aus16,13 Milliarden2,92 s
BIOS 1.30, tscsync im Sync-Modus2keiner protokolliertTSC vom Watchdog fallen gelassen, siehe unten
BIOS 1.30, Sync-Modus und tsc=nowatchdog1keiner protokolliertTSC behalten

Jeder Boot. Kaltstart, Neustart, nach einem Ausschalten, das lang genug war, dass man erwarten würde, alles Abgestandene sei weg. Der Betrag schwankt, und das passt zu dem, was der Autor von tscsync selbst an einem Lenovo Legion gefunden hat: Dort skaliert der Warp fast perfekt damit, wie lange die Maschine in der Firmware verbringt, bevor der Bootloader startet3. Ein Boot, der länger auf dem Logo steht, kommt weiter hinten heraus.

Der verlockende Fix ist tsc=reliable auf der Kernel-Kommandozeile, was Linux sagt, es solle aufhören zu prüfen. Lass es. Die Kerne liegen wirklich drei Sekunden auseinander, und die Prüfung abzuschalten ändert daran nichts; sie lässt den Kernel nur eine Uhr benutzen, die rückwärts springt, wenn ein Task zwischen Kernen wandert. Das README von tscsync sagt dasselbe und nennt das Stottern bei Audio und Video, das darauf folgt1.

Darf ich fragen, warum das immer noch ausgeliefert wird?

Ich frage nicht, wer. Ich frage, was in Lenovos Ablauf ein BIOS hat rausgehen lassen, das bei jedem Boot einen Kern drei Sekunden zurücklässt.

Die Antwort steht in den Design-Notizen von tscsync, und sie ist nicht schmeichelhaft. Fisher-Price OS (Windows) schreibt die TSCs beim Booten selbst3. Ein Laptop, der nur darauf validiert wird, besteht, weil das Betriebssystem still hinter der Firmware aufräumt. Linux lässt seine Sicherheitsprüfung an und meldet, was es bekommen hat, und genau deshalb zeigt sich der Fehler dort und nirgends sonst.

Das Unangenehme ist, dass das angeblich schon einmal behoben wurde. Nutzer berichten, Lenovo habe dieselbe Klasse von Fehler per BIOS-Update beim ThinkPad A485, beim T14s Gen 1 AMD und beim E495 behoben4. Das E495 ist das 14-Zoll-Geschwister dieser Maschine, und beide teilen sich ein BIOS: Lenovos eigenes Readme zum Paket führt beide auf5.

Mein E595 lief mit 1.28 und hatte bei jedem Boot einen Warp. Lenovo hat seitdem 1.29 und 1.30 veröffentlicht, und keine der beiden Änderungslisten sagt ein Wort zum TSC; 1.30 behebt „system randomly freeze since BIOS V1.27“ und einige Sicherheitsprobleme5. Also habe ich 1.30 aufgespielt und neu gemessen. Der erste Boot darauf, mit abgeschaltetem tscsync, protokollierte einen Warp von 6.126.446.316 Takten: 2,92 Sekunden, CPU#0 -> CPU#2, wie immer. 1.30 behebt es nicht. Die beiden Modelle teilen sich ein BIOS, wenn der Bericht zum E495 also stimmte, wäre diese Maschine auch repariert. Der Bericht nennt keine BIOS-Version und verlinkt keine Logs4.

Wird Lenovo für einen Fehler, den das Betriebssystem, auf dem sie testen, vor ihnen versteckt, zu einem Laptop von 2019 zurückgehen? Nein.

tscsync, zuerst gemessen

tscsync ist ein kleines UEFI-Programm, das aus GRUB oder systemd-boot kurz vor Linux läuft. Es fragt jeden Kern über die Multiprozessor-Dienste der Firmware selbst nach seinem TSC, rechnet den Versatz jedes Kerns zum Boot-Prozessor aus, schiebt die nachhängenden Zähler vor, bis sie übereinstimmen, und prüft das Ergebnis dann mit einem Test nach dem Vorbild der Warp-Prüfung des Kernels1. Es schreibt nie Flash, NVRAM oder Firmware-Einstellungen; die Zähler, die es ändert, werden beim Ausschalten zurückgesetzt1.

Das ist eine Menge, die man einem Bootloader überlässt, deshalb habe ich zuerst den Code gelesen, bei Commit 230eaa9, bevor ich es gebaut habe. Die Installation lief dann so, wie das Projekt es verlangt:

SchrittBefehlWas zurückkam
BauenmakeBinärdateien für GRUB und systemd-boot
Installieren, nur lesendsudo scripts/install.shMessmodus, nichts in die Zähler geschrieben
Ausschalten, booten, nachsehentscsync-statusCPU 0 2.964 ms hinter allen 7 APs, die APs auf Mikrosekunden einig
Einschaltensudo scripts/set-mode.sh syncwirkt ab dem nächsten Boot

Der Messmodus ist der Teil, der in jedes Werkzeug dieser Art übernommen gehört. Er zeigt dir den Fehler auf deiner eigenen Hardware, bevor er irgendetwas ändern darf, und ein Hänger-Schutz schaltet ihn ab, wenn ein Boot Linux nie erreicht1.

Der erste Boot im Sync-Modus, direkt aus tscsync-status:

survey (read-only):
  cpu  1 apic   1: offset 6815086011  rtt 126  BAD
  ...
  cpu  7 apic   7: offset 6815086010  rtt 378  BAD
  0 ok, 7 not ok
moving BSP forward by ~6815086010 cycles to match cpu 7:
  bsp vs cpu  7 apic   0: before 6815086001  after 3  rtt 357  iter 3  warps bsp/ap 0/0  OK
AP pass (corrects any AP still off):
  7 ok, 0 not ok
total 121 ms

Und der Kernel, eine Sekunde später:

clocksource: Switched to clocksource tsc-early
tsc: Refined TSC clocksource calibration: 2096.066 MHz
clocksource: Switched to clocksource tsc

Keine Warp-Zeile. Kein „unstable“. /sys/devices/system/clocksource/clocksource0/current_clocksource liest tsc. 121 ms Bootzeit für die Uhr, die die Hardware von Anfang an haben sollte. Das war auf BIOS 1.28. 1.30 war nicht so ordentlich.

Die 21 Nanosekunden, die Linux überstimmt hat

Eine Zeile in diesem Bericht sieht schlimmer aus, als sie ist, und es lohnt sich, sie zu erklären, statt sie zu verstecken. Der abschließende Prüfdurchlauf von tscsync hat einen Kern markiert:

cpu  7 apic   7: offset 45  rtt 357  warps bsp/ap 0/0  BAD
6 ok, 1 not ok
RESULT: some APs out of sync (Linux will fall back to HPET)

45 Takte sind etwa 21 Nanosekunden. tscsync zielt auf Übereinstimmung innerhalb von etwa 10 Takten1, nach seinem eigenen Ziel hat CPU 7 es also verfehlt, und das hat es auch gesagt. Dann bootete Linux und fiel überhaupt nicht auf HPET zurück.

Die beiden Prüfungen stellen verschiedene Fragen:

Versatz-Urteil von tscsyncWarp-Test des Kernels
Fragewie weit liegen die Zähler auseinander?läuft die Zeit zwischen Kernen je rückwärts?
MethodeSchätzung aus einem zeitgemessenen Hin und ZurückKerne lesen abwechselnd, Vergleich mit dem letzten Wert2
Bestehensgrenzeetwa 10 Taktekein Wert niedriger als der davor
CPU 7 bei diesem Boot45 Takte, BADkein Warp, TSC behalten

Das warps bsp/ap 0/0 in derselben Zeile ist tscsync, das den Test im Stil des Kernels laufen lässt. Es hat keinen gesehen. Der Kernel auch nicht.

Das Projekt weiß bereits, dass sein Urteil strenger sein kann als das des Kernels. Ein IdeaPad in seiner Hardwareliste hatte vier Kerne mit 28 bis 42 Takten markiert, die der Kernel annahm, und seit 2.1 wird ein langsames Hin und Zurück als UNSURE statt BAD gemeldet4. Die Regel des README für diesen Fall ist die richtige: Die eigene Prüfung des Kernels entscheidet, und wenn die Clocksource tsc ist, hat sie bestanden1. Ein Hin und Zurück von 357 Takten ist auf dieser Maschine nicht langsam, also kam stattdessen BAD heraus. Ein Werkzeug, das seinen Rechenweg zeigt und dann dem Kernel das letzte Wort lässt, ist richtig herum gebaut.

BIOS 1.30: Der Sync hält, der TSC geht trotzdem

Auf 1.30 habe ich tscsync wieder in den Sync-Modus gesetzt und zweimal neu gestartet. Beide Male hat es seine Arbeit getan. Es schob den Boot-Prozessor um etwa 6,17 Milliarden Takte vor, jeder Kern kam innerhalb weniger Dutzend Takte heraus, und der Kernel protokollierte keinen Warp. Beide Male landete Linux ein paar Sekunden später trotzdem auf HPET:

tsc: Detected 2095.890 MHz processor
tsc: Refined TSC clocksource calibration: 2096.053 MHz
clocksource: Watchdog remote CPU 5 read timed out
clocksource: Marking clocksource tsc unstable due to frequency skew
clocksource: Watchdog                    hpet interval:        495136113ns
clocksource: Clocksource                  tsc interval:        496026671ns
tsc: Marking TSC unstable due to clocksource watchdog

Das ist eine andere Prüfung. Der Warp-Test läuft, während jeder Kern hochkommt, und fragt, ob die Zeit zwischen Kernen je rückwärts läuft. Der Clocksource-Watchdog läuft nach dem Booten weiter. Etwa jede halbe Sekunde vergleicht er, wie weit sich der TSC bewegt hat, mit dem, wie weit sich HPET bewegt hat, und wenn sie um mehr als einen kleinen Spielraum abweichen, markiert er den TSC als instabil6. Einmal markiert, bleibt er markiert bis zum nächsten Boot.

Zwei Kernel-Prüfungen, und welche Boot-Option welche überspringtFirmwareCPU 0 ~3 s zurücktscsync, in UEFIschiebt CPU 0 vorLinux startetKerne kommen hochWarp-Prüfungeinmal, beim BootenWatchdogalle 0,5 s gegen HPETStandardbeide an: TSC fiel auf 1.30✓ geprüft✓ geprüfttsc=nowatchdogdieser Boot: TSC behalten✓ geprüft✗ übersprungentsc=reliablenicht ratsam: verdeckt echten Warp✗ übersprungen✗ übersprungen
Zwei getrennte Prüfungen können den TSC fallen lassen. Die Warp-Prüfung läuft einmal, während jeder Kern hochkommt; tscsync bringt diese Maschine an ihr vorbei. Der Watchdog vergleicht jede halbe Sekunde weiter mit HPET. tsc=nowatchdog überspringt nur die zweite, tsc=reliable überspringt beide und würde einen echten Warp verdecken.
Boot auf 1.30Warp-PrüfungHPET-IntervallTSC-IntervallTSC gegen HPET
Erster Sync-Bootbestanden495.426.793 ns495.006.210 nsetwa 850 ppm zu langsam
Zweiter Sync-Bootbestanden495.136.113 ns496.026.671 nsetwa 1.800 ppm zu schnell

Läuft der TSC also mit der falschen Rate? Nein. Bei laufender Maschine habe ich den TSC 30 Sekunden lang in 5-Sekunden-Fenstern gegen die HPET-basierte Rohuhr des Kernels gemessen. Jedes Fenster kam auf 2.096,062 MHz, etwa 4 Millionstel neben der eigenen Kalibrierung des Kernels. Ein echter Ratenfehler schwankt nicht zwischen zwei Boots von zu langsam zu zu schnell und verschwindet dann. Irgendwas stört früh im Boot ein Halbsekundenfenster, und die Zeile remote CPU 5 read timed out direkt davor deutet auf einen Kern, der kurz nicht antwortet. Firmware, die den CPUs Zeit stiehlt, würde passen. Bewiesen habe ich es nicht.

Zwei Boots sind alles, was es an Belegen gibt. Ich kann noch nicht sagen, ob es 1.30 ist, Zufall oder etwas, das der Schreibvorgang von tscsync auslöst. Der eine Sync-Boot auf 1.28 behielt den TSC, die zwei auf 1.30 verloren ihn, und das reicht nicht, um dem BIOS die Schuld zu geben.

Für diesen Fall gibt es eine engere Kernel-Option als tsc=reliable. tsc=nowatchdog schaltet den Clocksource-Watchdog für den TSC ab und lässt die Warp-Prüfung beim Booten in Ruhe7. Sollte tscsync je mit dem Abgleich scheitern, würde der Kernel den Warp immer noch erwischen und auf HPET zurückfallen. Das war der nächste Test. Die Zahlen und ein paar vorgeschlagene Änderungen an der Berichterstattung von tscsync selbst liegen im Issue-Tracker des Projekts8.

tsc=nowatchdog: Fünf Minuten, keine Warps

Ich habe tsc=nowatchdog hinzugefügt und neu gestartet. tscsync schob den Boot-Prozessor um 6.207.104.421 Takte vor, der Kernel protokollierte keinen Warp und keinen Skew, und current_clocksource las tsc. Der erste Boot auf 1.30, der ihn behielt.

Das beweist nur, dass der Watchdog den TSC fallen gelassen hatte. Es beweist nicht, dass man sicher auf dem TSC laufen kann, und mit abgeschaltetem Watchdog passt im Kernel niemand mehr auf. Also habe ich selbst aufgepasst.

Meine frühere Ratenprüfung half hier nicht. Sie maß den TSC gegen CLOCK_MONOTONIC_RAW, was in Ordnung war, solange der Kernel auf HPET lief. Auf dem TSC wird diese Uhr aus dem TSC gebildet, der Test würde den Zähler also mit sich selbst vergleichen. HPET lässt sich aus dem User Space auch nicht lesen, weil der Kernel von Fedora ohne CONFIG_HPET_MMAP gebaut ist. Die Echtzeituhr (RTC) schon. Sie läuft von ihrem eigenen Quarz, und der Kernel weckt ein Programm bei jedem Tick ihres Sekundenzählers9.

Also habe ich eine kleine Prüfung geschrieben, tsccheck. Sie pinnt sich auf CPU 0 und lässt alle 15 Sekunden vier Tests laufen:

PrüfungWieWie ein Fehler aussieht
RateTSC-Takte zwischen Sekunden-Ticks der RTC gezähltdie MHz driften in eine Richtung
Versatz3.000 Hin und Zurück von CPU 0 zu jedem Kern, Versatz aus dem schnellstenein Versatz, der wächst
Warpsbei jedem Hin und Zurück: Hat ein Kern einen niedrigeren Wert gelesen als einen schon gesehenen?alles über null
Clocksourcecurrent_clocksource lesenalles außer tsc

Das Hin und Zurück ist der Kern des Ganzen. CPU 0 liest ihren Zähler und reicht ihn hinüber, der andere Kern liest seinen eigenen und reicht ihn zurück, und CPU 0 liest noch einmal. Wenn die Kerne übereinstimmen, muss der mittlere Wert zwischen den beiden anderen landen:

Ein Hin und Zurück zwischen CPU 0 und einem anderen KernCPU 0CPU 1 bis 7, eine nach der anderenZeitt0t1t2MitteVersatzVersatz = t1 − (t0 + t2) / 2aus dem schnellsten von 3.000Warp, wenn t1 < t0 oder t2 < t1Zeit lief rückwärts: jeder ist ein Fehler
Ein Hin und Zurück. Der Wert des anderen Kerns, t1, wird mit der Mitte der beiden Werte von CPU 0 verglichen, und das schnellste von 3.000 Hin und Zurück liefert den Versatz. Jeder Wert, der niedriger ist als ein schon gesehener, ist ein Warp, wie groß der Versatz auch ist.

Die Schleife, die das macht:

for (int i = 0; i < ITERS; i++) {
	uint64_t t0 = rdtsc_o();                 /* CPU 0 */
	atomic_store(&pp.t0, t0);
	atomic_store(&pp.seq, 2 * i + 1);        /* the other core reads t1 now */
	while (atomic_load(&pp.seq) != 2 * i + 2)
		;
	uint64_t t2 = rdtsc_o();                 /* CPU 0 again */
	uint64_t t1 = atomic_load(&pp.t1);
	if (t1 < t0 || t2 < t1)
		r.warps++;                           /* time went backwards */
	if (i > 100 && t2 - t0 < r.rtt) {
		r.rtt = t2 - t0;
		r.offset = (int64_t)(t1 - (t0 + (t2 - t0) / 2));
	}
}

Das ganze Programm ist schlichtes C für x86-64-Linux, ohne irgendetwas, das an tscsync oder diesen Laptop gebunden ist. Es braucht Root für /dev/rtc0, und das dritte Argument ist der Wert deines eigenen Kernels aus journalctl -k -b | grep 'Refined TSC':

gcc -O2 -Wall -pthread -o tsccheck tsccheck.c
sudo ./tsccheck 15 20 2096.073
tsccheck.c, die ganze Prüfung in 161 Zeilen tsccheck.c · 4 kB

SHA-256 02eba1966a0097196acd5dfdcad9c4b315cafc1854c60f34c5b56080e6c38408.

Zwanzig Messungen über fünf Minuten, direkt nach dem Booten:

MessgrößeErgebnis
Clocksourcetsc in allen 20 Messungen, keine Kernel-Meldungen
Warps0 in etwa 420.000 Hin und Zurück
Größter Versatz zu CPU 085 Takte, etwa 41 ns, kein Trend
Rate über den ganzen Lauf2.096,027 MHz, ab der ersten Minute innerhalb von 2.096,022 bis 2.096,031
Rate je 15-Sekunden-Fenster2.095,943 bis 2.096,104 MHz
TSC-Rate je 15-Sekunden-Fenster und über den ganzen Lauf+20 ppm0 ppm-20 ppm-40 ppm-60 ppm0 s60 s120 s180 s240 s300 sWert des Kernels, 2.096,073 MHz15 s: Fenster -11,3 ppm, ganzer Lauf -11,3 ppm30 s: Fenster -61,1 ppm, ganzer Lauf -36,2 ppm45 s: Fenster -23,5 ppm, ganzer Lauf -32,0 ppm60 s: Fenster +15,0 ppm, ganzer Lauf -20,2 ppm75 s: Fenster -21,6 ppm, ganzer Lauf -20,5 ppm90 s: Fenster -21,8 ppm, ganzer Lauf -20,7 ppm105 s: Fenster -19,6 ppm, ganzer Lauf -20,6 ppm120 s: Fenster -50,5 ppm, ganzer Lauf -24,3 ppm135 s: Fenster +5,8 ppm, ganzer Lauf -20,9 ppm150 s: Fenster -20,3 ppm, ganzer Lauf -20,9 ppm165 s: Fenster -23,6 ppm, ganzer Lauf -21,1 ppm180 s: Fenster -13,7 ppm, ganzer Lauf -20,5 ppm195 s: Fenster -29,5 ppm, ganzer Lauf -21,2 ppm210 s: Fenster -22,3 ppm, ganzer Lauf -21,3 ppm225 s: Fenster -20,0 ppm, ganzer Lauf -21,2 ppm240 s: Fenster -24,7 ppm, ganzer Lauf -21,4 ppm255 s: Fenster -19,1 ppm, ganzer Lauf -21,3 ppm270 s: Fenster -22,2 ppm, ganzer Lauf -21,3 ppm285 s: Fenster -62,0 ppm, ganzer Lauf -23,5 ppm300 s: Fenster +9,2 ppm, ganzer Lauf -21,9 ppmein 15-s-Fenster, 61 ppm zu langsamganzer Lauf bisher15-Sekunden-Fensterganzer Lauf bisher
Rate gegen die RTC, in Millionsteln (ppm) vom Wert des Kernels. Die einzelnen Fenster springen paarweise herum; der Wert über den ganzen Lauf pendelt sich in der ersten Minute bei etwa 21 ppm zu langsam ein und bleibt dort.

Die Fenster sehen schlimmer aus, als sie sind. Ein später RTC-Tick macht ein Fenster lang und das nächste kurz, die Fehler kommen also in Paaren, die sich aufheben: 61 Millionstel (ppm) zu langsam bei 30 Sekunden, 15 ppm zu schnell bei 60. Drift hebt sich nicht selbst auf. Der Wert über den ganzen Lauf hat sich kaum bewegt.

Der Wert über den ganzen Lauf liegt die vollen fünf Minuten 22 ppm unter den 2.096,073 MHz des Kernels. Das ist der Abstand zwischen zwei Quarzen, und er blieb stehen. chrony, das die Uhr gegen Zeitserver im Internet nachführt, hatte sie bei 16 bis 19 ppm zu langsam10. Dieselbe Richtung, dieselbe Größe.

Die Versätze springen zwischen denselben paar Werten, 10, 21 und 33 Takte in beide Richtungen, ohne dass ein Kern davonläuft. Das ist Rauschen aus einem Hin und Zurück von etwa 550 Takten. Die Prüfung von tscsync würde einige davon immer noch BAD nennen. Der Kernel nicht.

Hat der TSC gehalten? Ja, bei jeder Messgröße, die ich hatte. Fünf Minuten nach einem Boot sind trotzdem keine Reihe von Boots. Als Nächstes kommt dieselbe Prüfung nach langer Laufzeit und noch einmal über ein paar weitere Neustarts.

Der Fix gehört in die Firmware

tscsync sollte nicht existieren müssen. Sein eigenes README sagt das: Firmware-Hersteller dürfen den Ansatz und den Code gern übernehmen, „which is where this should be fixed“1. Lenovo hat den Quellcode. Der Autor von tscsync hatte nur das, was sein Laptop ihm verraten wollte, und 80 Boots an Logs1.

Das ist der Teil, der hängen bleibt. Für den Fix musste ein Fremder von außerhalb der Firma messen, diagnostizieren und drumherum arbeiten, während die Firma, die die Hardware verkauft hat, sie gegen das eine Betriebssystem validiert hat, das den Fehler versteckt. Eine Maschine, die du nicht reparieren kannst, weil ihre Firmware geschlossen ist, gehört dir nicht wirklich, sie ist eine Leihgabe mit Auflagen. Offene Firmware mit signierten Updates hätte das einmal richtig beheben lassen, von jedem, dem es wichtig genug war, hinzusehen.

Bis dahin: gute Arbeit vom Autor von tscsync. Zuerst der Messmodus, ein Hänger-Schutz, die Zahlen offen auf dem Tisch und ein ehrliches Urteil, auch wenn es dem Kernel widerspricht. So sollten Low-Level-Werkzeuge gebaut werden.

Jeder KVM-Host mit derselben Firmware

Auf einem Laptop kostet das etwas Latenz und etwas Akku. Auf einem Hypervisor zahlt jeder Gast mit, und nichts von dem, was folgt, geht auf Proxmox zurück. Es lebt alles im KVM-Code des Linux-Kernels, ist also auf jedem Host auf KVM-Basis gleich, was auch immer obendrauf sitzt: Proxmox VE, oVirt, OpenStack oder schlichtes QEMU mit libvirt11.

Was KVM tut, wenn der Host seinen TSC verliert

KVM gibt seinen Gästen nur dann eine stabile Uhr, wenn der Host selbst auf dem TSC läuft. Im Kernel-Quellcode wird die Master Clock, eine gemeinsame Zeitreferenz für jede vCPU einer VM, nur eingeschaltet, wenn host_tsc_clocksource wahr ist12. Fällt der Host auf HPET zurück, schaltet sie ab, und mehreres ändert sich auf einmal:

Host auf TSCHost auf HPET, TSC als instabil markiert
KVM-Master-Clock12anaus
Eine vCPU wandert auf einen anderen Kern12nichts zu aktualisierenein Uhr-Update für jede vCPU dieser VM erzwungen, bis zu 10 pro Sekunde
Der TSC-Versatz des Gasts12bleibt in Ruhebei jedem Wandern neu geschrieben und in den Aufholmodus gesetzt
PVCLOCK_TSC_STABLE_BIT für Gäste12gesetztnicht gesetzt
clock_gettime() im Gast13im User Space gelesenein Systemaufruf, einmal beim Booten des Gasts entschieden
Uhrlesen im Gast über vCPUs hinweg14unabhängigjedes Lesen gegen einen gemeinsamen globalen Wert geprüft

Die letzte Zeile ist die, die schlecht skaliert. Ohne das Stable-Bit kann der Gast-Kernel nicht darauf vertrauen, dass die Uhr jeder vCPU mit den anderen übereinstimmt, also wird jedes Lesen mit „last_value, a global accumulator“ verglichen und per atomarem Compare-and-Exchange eingetauscht, wenn es neuer ist14. Jede vCPU im Gast schreibt auf diesen einen Wert. Je mehr vCPUs die Uhr lesen, desto mehr stehen sie daran an.

Nichts davon zeigt sich als Fehler. Die Clocksource des Gasts liest weiter kvm-clock, und nichts im Gast sagt dir, warum er langsamer geworden ist.

Wie die Clocksource des Hosts einen KVM-Gast erreichtHost auf TSCHost-Clocksource: tscein Befehl zum LesenKVM-Master-Clock anStable-Bit für Gäste gesetztGast liest im User SpacevDSO, etwa 28 nsHost auf HPETHost-Clocksource: hpetein Weg zum Timer-GerätKVM-Master-Clock auskein Stable-Bit, Updates bei jedem vCPU-WechselGast macht einen Systemaufrufein gemeinsamer Wert, etwa 190 nsDerselbe Kernel-Code auf Proxmox VE, oVirt, OpenStack oder schlichtem QEMU mit libvirt
Die Clocksource des Hosts entscheidet, was jeder Gast bekommt. Auf dem TSC lesen Gäste die Uhr im User Space. Auf HPET lässt KVM seine Master Clock fallen, jedes Wandern einer vCPU erzwingt Uhr-Updates, und jedes Uhrlesen im Gast wird zu einem Systemaufruf, geprüft gegen einen Wert, den sich alle vCPUs teilen.

Was es kostet, gemessen

Ich habe es auf diesem Laptop gemessen, statt es als Theorie stehen zu lassen. Die Clocksource des Hosts lässt sich im laufenden Betrieb umschalten, was die Master Clock von KVM genauso abschaltet wie der Rückfall beim Booten. Ein Wegwerf-Gast bootete den eigenen Kernel des Hosts mit 4 vCPUs und einer winzigen Initramfs, deren einziges Programm clock_gettime() stoppt. Zwei Läufe je Variante, erst Host, dann Gast:

Nanosekunden je Uhrlesen, Host auf TSC gegen Host auf HPETHostSkala 0 bis 2.400 nsHost, 1 Thread, Host auf TSC: 24 ns24 nsHost, 1 Thread, Host auf HPET: 1.542 ns1.542 ns1 ThreadHost, alle 8 Kerne, Host auf TSC: 29 ns29 nsHost, alle 8 Kerne, Host auf HPET: 1.922 ns1.922 nsalle 8 KerneGast, 4 vCPUsSkala 0 bis 300 nsGast, 4 vCPUs, 1 vCPU, Host auf TSC: 28 ns28 nsGast, 4 vCPUs, 1 vCPU, Host auf HPET: 187 ns187 ns1 vCPUGast, 4 vCPUs, alle 4 vCPUs, Host auf TSC: 31 ns31 nsGast, 4 vCPUs, alle 4 vCPUs, Host auf HPET: 244 ns244 nsalle 4 vCPUsHost-Clocksource tscHost-Clocksource hpet
Nanosekunden je Aufruf von clock_gettime(), Mittel aus zwei Läufen je Variante. Der Host wird auf HPET 61- bis 73-mal langsamer; der Gast etwa 7-mal mit einer vCPU und 8-mal mit allen vier. Die Felder haben verschiedene Skalen, weil die HPET-Balken des Hosts die des Gasts plattdrücken würden.

Der Gast zahlt den Preis von HPET nicht direkt, weil kvm-clock darunter immer noch den TSC liest. Was er verliert, ist das Lesen im User Space und die Unabhängigkeit zwischen den vCPUs, und das allein ist siebenmal langsamer. Lesen alle vier vCPUs gleichzeitig, steigen die Kosten noch einmal, und das ist dieser gemeinsame globale Wert bei der Arbeit. Ein Gast mit 32 oder 64 vCPUs steht stärker daran an. Gemessen habe ich das nicht.

Mach eine Zahl daraus. Ein Gast, der die Uhr eine Million Mal pro Sekunde auf einer vCPU liest, verbringt auf einem gesunden Host 2,8 % dieser vCPU damit und auf diesem 19 %. Datenbanken, die Java Virtual Machine (JVM) und alles andere, was seine Arbeit mit Zeitstempeln versieht, lesen die Uhr viel.

Dieser Lauf unterschätzt es außerdem. Die Clocksource von Hand umzuschalten schaltet die Master Clock ab, markiert den TSC aber nicht als instabil, die Neuschreibungen des Versatzes aus der Tabelle oben liefen also nie. Der Rückfall beim Booten auf diesem E595 markiert ihn als instabil, und ein Host in diesem Zustand zahlt beides.

clockbench.c, der Benchmark, auf dem Host oder als Init eines Gasts clockbench.c · 2 kB

Einen Host prüfen, und die Grenzen

Warum Gast-Uhren überhaupt fragil sind, habe ich im Beitrag zu ptp_kvm behandelt; das hier ist dasselbe Problem, das von unten kommt. Prüf den Host, nicht den Gast:

journalctl -k -b | grep -iE 'TSC warp|TSC unstable'
cat /sys/devices/system/clocksource/clocksource0/current_clocksource

tscsync läuft aus GRUB oder systemd-boot1, was die meisten KVM-Hosts abdeckt. Proxmox VE nutzt GRUB, oder systemd-boot bei einer UEFI-Installation mit ZFS als Root und abgeschaltetem Secure Boot15. Sei aber ehrlich bei den Grenzen, bevor es in die Nähe eines Produktionsknotens kommt.

GrenzeWas du dagegen tust
Nur AMD Zen und Hygon1Intel-Hosts mit TSC_ADJUST repariert der Kernel schon
Jede Maschine in seiner Hardwareliste ist ein Laptop4behandle einen Server als ungetestet und melde ihn
Der Sync-Modus verweigert Hardware außerhalb seiner Liste, außer du stimmst zu1lies nach, warum, bevor du das übergehst
Ein erster Sync-Boot kann hängenzuerst der Messmodus, Out-of-Band-Konsole offen, ein Knoten nach dem anderen
Ein BIOS-Update behebt den Fehler vielleichtdanach zurück auf Messmodus stellen und prüfen1
Ein sauberer Sync kann den TSC trotzdem an den Watchdog verlierennach jedem Boot current_clocksource prüfen; tsc=nowatchdog, wenn der Watchdog die Ursache ist, und dann selbst messen

Ein Firmware-Fehler, der einen Laptop etwas Akku kostet, kostet einen Hypervisor ein Stück jedes Gasts, den er betreibt, jede Sekunde, die er läuft, und die Besitzer der Gäste haben keine Möglichkeit, das zu sehen. Das ist das Argument dafür, current_clocksource auf jedem Host zu prüfen, den du betreibst, egal wer ihn gebaut hat.


  1. tscsync README — was es tut, die Kosten eines Uhrlesens auf HPET, warum tsc=reliable kein Fix ist, das Sicherheitsmodell und die Regel zu UNSURE. ↩︎ ↩︎ ↩︎ ↩︎ ↩︎ ↩︎ ↩︎ ↩︎ ↩︎ ↩︎ ↩︎ ↩︎ ↩︎ ↩︎

  2. Linux kernel — arch/x86/kernel/tsc_sync.c — die Behandlung von TSC_ADJUST, der Warp-Test und die Meldung „Measured %Ld cycles TSC warp between CPUs, turning off TSC clock.“ ↩︎ ↩︎ ↩︎

  3. tscsync — docs/DESIGN.md — AMD Zen hat kein IA32_TSC_ADJUST, tsc=directsync upstream abgelehnt, Windows schreibt die TSCs beim Booten selbst, und der Warp skaliert auf dem Legion Pro 5 mit der Zeit in der Firmware. ↩︎ ↩︎ ↩︎

  4. tscsync — docs/HARDWARE.md — getestete Maschinen, die IdeaPad-5-Kerne, die bei -28 bis -42 Takten angenommen wurden, und die gemeldeten BIOS-Fixes beim A485, E495 und T14s Gen 1. ↩︎ ↩︎ ↩︎ ↩︎

  5. Lenovo — BIOS Update Utility readme, ThinkPad E495/E595, version 1.30 — beide Modelle in einem Paket, die Versionsgeschichte bis 1.30 (14. Mai 2024) und deren Änderungsliste. ↩︎ ↩︎

  6. Linux kernel — kernel/time/clocksource.c — der Clocksource-Watchdog, sein Intervallvergleich und die Meldung „Marking clocksource %s unstable due to frequency skew“. ↩︎

  7. Linux kernel — kernel-parameters — tsc=reliable schaltet die Prüfung zur Laufzeit und die Stabilitätsprüfungen beim Booten ab; tsc=nowatchdog schaltet nur den Clocksource-Watchdog ab. ↩︎

  8. tscsync issue #1 — Lenovo ThinkPad E595 — die Berichte zu 1.28 und 1.30, die Analyse des Watchdogs und die vorgeschlagenen Änderungen. ↩︎

  9. Linux kernel — Real Time Clock (RTC) Drivers for Linux — „The clock can also be set to generate an interrupt upon every clock update, thus generating a 1Hz signal“, gelesen über /dev/rtc. ↩︎

  10. chrony — chronyc manual — tracking: „The ‘frequency’ is the rate by which the system’s clock would be wrong if chronyd was not correcting it.“ ↩︎

  11. KVM — Management Tools — Proxmox VE, oVirt, OpenStack und die Werkzeuge auf Basis von libvirt, die alle KVM verwalten. ↩︎

  12. Linux kernel — arch/x86/kvm/x86.c — pvclock_update_vm_gtod_copy(): „If the host uses TSC clock, then passthrough TSC as stable to the guest“, und PVCLOCK_TSC_STABLE_BIT nur mit der Master Clock gesetzt. ↩︎ ↩︎ ↩︎ ↩︎ ↩︎

  13. Linux kernel — arch/x86/kernel/kvmclock.c — der Gast schaltet die User-Space-Uhr VDSO_CLOCKMODE_PVCLOCK nur ein, wenn PVCLOCK_TSC_STABLE_BIT gesetzt ist. ↩︎

  14. Linux kernel — arch/x86/kernel/pvclock.c — ohne PVCLOCK_TSC_STABLE_BIT wird jedes Lesen gegen „last_value, a global accumulator“ mit einem atomaren Compare-and-Exchange geprüft. ↩︎ ↩︎

  15. Proxmox VE — Host Bootloader — standardmäßig GRUB, systemd-boot bei UEFI-Installationen mit ZFS-Root und abgeschaltetem Secure Boot. ↩︎