Update, 11 oktober 2026, 16:30: ik heb inmiddels BIOS 1.30 geflasht. Die herstelt de afwijking niet. tscsync zet de kernen nog steeds gelijk, maar op twee sync-boots liet de kernel de TSC toch vallen, via zijn clocksource-watchdog in plaats van de warpcontrole: BIOS 1.30: de synchronisatie houdt, de TSC gaat toch. Met tsc=nowatchdog erbij hield de TSC stand, en vijf minuten meten tegen de real-time clock vond geen warps en geen wegdrijven: tsc=nowatchdog: vijf minuten, geen warps. Eén boot is geen reeks boots, dus ik noem het houdt stand, niet opgelost.

Elke boot dezelfde waarschuwing

Dit stond bij elke boot in het kernellog van mijn 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

Niets crashte. Er verscheen niets op het bureaublad. De machine draaide vanaf het moment van opstarten stilletjes haar hele klok op een tragere timerchip, en dat was eeuwig zo doorgegaan als ik het journal niet voor iets anders had zitten lezen.

De Time Stamp Counter (TSC) is een teller in elke CPU-kern die op een vast tempo tikt. Hem lezen is één instructie. Linux gebruikt hem als hoofdklok, maar alleen als elke kern het over de stand eens is1. Loopt één kern achter, dan kan de kernel hem niet vertrouwen en valt hij terug op de High Precision Event Timer (HPET), een apart timerapparaat. De auteur van tscsync mat een kloklezing op HPET van ongeveer 1,2 µs, tegen tientallen nanoseconden op de TSC, en de kernel en het bureaublad lezen de klok tienduizenden keren per seconde1. Dat is latentie en batterij, bij elke boot betaald, voor niks.

Hoe ver liep de kern dan achter? Ruim drie seconden. Op een laptop van 2,1 GHz is dat 6,6 miljard cycli.

Wat de firmware overdraagt

De melding noemt CPU 0 en CPU 2, maar de fout zit niet in CPU 2. Gemeten door tscsync vanuit de bootloader, en door de kernel zodra die start, is het beeld elke keer hetzelfde:

KernAfwijking ten opzichte van CPU 0Wat het betekent
CPU 0, de opstartprocessor (BSP)0de referentie, en de vreemde eend
CPU’s 1 tot en met 7, de andere kernen (AP’s)ongeveer +6,2 tot +6,8 miljard cyclionderling eens tot op 32 cycli
Gat bij 2.096 MHz2,9 tot 3,3 secondende opstartprocessor loopt achter, bij elke boot

Zeven kernen zijn het eens. Eén is drie seconden te laat. De firmware brengt de opstartprocessor op gang, doet haar werk erop en geeft het stokje door aan de bootloader zonder die ene teller ooit weer gelijk te zetten met de rest.

De TSC van de opstartprocessor vóór en na tscsyncVóór: zoals de firmware overdraagtNa: tscsync in sync-modusCPU 0 (BSP)CPU 1CPU 2CPU 3CPU 4CPU 5CPU 6CPU 7CPU 0 (BSP)CPU 1CPU 2CPU 3CPU 4CPU 5CPU 6CPU 7CPU 0 ongeveer 6,8 miljard cycli (3,25 s) achterCPU's 1 tot 7 eens tot op 32 cycliBSP vooruitgezet, elke kern binnen 45 cycliLinux houdt de TSC als clocksource
Links: wat de firmware overdraagt. De zeven applicatieprocessors zijn het eens tot op 32 cycli; de opstartprocessor loopt ongeveer 6,8 miljard cycli achter. Rechts: nadat tscsync de opstartprocessor in UEFI vooruit heeft gezet, zit elke kern binnen 45 cycli en slaagt de warpcontrole van de kernel.

Waarom kan Linux het niet gewoon herstellen? Op Intel-CPU’s is er een register, IA32_TSC_ADJUST, waarmee de kernel de teller van een kern kan bijstellen, en de synchronisatiecode van de kernel gebruikt het als het er is2. AMD Zen-CPU’s hebben het niet. Zonder dat register kan niets in Linux de afwijking herstellen, en de poging in de kernel om de tellers direct te schrijven (tsc=directsync) is upstream als onbetrouwbaar afgewezen3.

Dus doet de kernel het enige eerlijke dat overblijft. Hij meet, ziet de tijd tussen twee kernen achteruitgaan, drukt de waarschuwing af en stopt met de TSC2.

Veertig boots, veertig warps

Eén boot is een anekdote. Het journal op deze machine gaat terug tot 30 mei 2026, dus ik heb het gevraagd.

BootsWarp, cycliWarp bij 2.096 MHz
30 mei tot 11 oktober 202640 van 406,16 tot 9,88 miljard2,94 tot 4,72 s
Mediaan6,57 miljard3,14 s
Na volledig uitschakelen van ongeveer 27 s16,60 miljard3,15 s
Met tscsync in sync-modus, BIOS 1.281niets gelogdTSC behouden
BIOS 1.30, tscsync uit16,13 miljard2,92 s
BIOS 1.30, tscsync in sync-modus2niets gelogdTSC laten vallen door de watchdog, zie verderop
BIOS 1.30, sync-modus en tsc=nowatchdog1niets gelogdTSC behouden

Elke boot. Koude start, herstart, na een uitschakeling die lang genoeg was dat je zou verwachten dat alles wat bleef hangen weg is. De hoeveelheid schommelt, en dat past bij wat de auteur van tscsync zelf vond op een Lenovo Legion: daar schaalt de warp vrijwel perfect met hoe lang de machine in de firmware doorbrengt voordat de bootloader start3. Een boot die langer op het logo blijft hangen, loopt daarna verder achter.

De verleidelijke oplossing is tsc=reliable op de kernelopdrachtregel, wat Linux zegt te stoppen met controleren. Doe het niet. De kernen liggen echt drie seconden uit elkaar, en de controle uitzetten verandert daar niets aan; het laat de kernel alleen een klok gebruiken die achteruit springt als een taak naar een andere kern verhuist. De README van tscsync zegt hetzelfde, en noemt het haperende geluid en beeld dat volgt1.

Mag ik vragen waarom dit nog steeds de deur uitgaat?

Ik vraag niet wie. Ik vraag wat er in het proces van Lenovo een BIOS de deur uit liet gaan die bij elke boot een kern drie seconden achter laat lopen.

Het antwoord staat in de ontwerpnotities van tscsync, en het is niet vleiend. Fisher-Price OS (Windows) schrijft de TSC’s bij het opstarten zelf3. Een laptop die alleen daarop is gevalideerd, slaagt, omdat het besturingssysteem stilletjes achter de firmware opruimt. Linux laat zijn veiligheidscontrole aan en meldt wat het kreeg, en precies daarom duikt de fout daar op en nergens anders.

Het pijnlijke is dat dit naar verluidt al eens eerder is opgelost. Gebruikers melden dat Lenovo dezelfde soort fout met een BIOS-update heeft opgelost op de ThinkPad A485, de T14s Gen 1 AMD en de E4954. De E495 is het 14-inch zusje van deze machine, en ze delen één BIOS: de eigen readme van Lenovo bij het pakket noemt ze allebei5.

Mijn E595 zat op 1.28 en warpte bij elke boot. Lenovo heeft sindsdien 1.29 en 1.30 uitgebracht, en geen van beide wijzigingslijsten zegt een woord over de TSC; 1.30 verhelpt “system randomly freeze since BIOS V1.27” en een paar beveiligingsproblemen5. Dus heb ik 1.30 erop gezet en opnieuw gemeten. De eerste boot erop, met tscsync uitgeschakeld, logde een warp van 6.126.446.316 cycli: 2,92 seconden, CPU#0 -> CPU#2, zoals altijd. 1.30 lost het niet op. De twee modellen delen één BIOS, dus als de melding over de E495 klopte, was deze machine ook hersteld. De melding noemt geen BIOS-versie en linkt geen logs4.

Gaat Lenovo terug naar een laptop uit 2019 voor een fout die het besturingssysteem waarop ze testen voor ze verbergt? Nee.

tscsync, eerst gemeten

tscsync is een klein UEFI-programma dat vanuit GRUB of systemd-boot draait, vlak voor Linux. Het vraagt elke kern om zijn TSC via de eigen multiprocessordiensten van de firmware, rekent per kern de afwijking ten opzichte van de opstartprocessor uit, zet de achterlopende tellers vooruit tot ze het eens zijn, en controleert het resultaat met een test naar het voorbeeld van de eigen warpcontrole van de kernel1. Het schrijft nooit naar flash, NVRAM of firmware-instellingen; de tellers die het verandert, worden bij uitschakelen teruggezet1.

Dat is veel om aan een bootloader over te laten, dus ik heb de code eerst gelezen, op commit 230eaa9, voordat ik hem bouwde. De installatie ging daarna zoals het project vraagt:

StapOpdrachtWat er terugkwam
Bouwenmakebinaries voor GRUB en systemd-boot
Installeren, alleen lezensudo scripts/install.shmeetmodus, niets naar de tellers geschreven
Uitschakelen, opstarten, kijkentscsync-statusCPU 0 2.964 ms achter alle 7 AP’s, de AP’s onderling eens tot op microseconden
Aanzettensudo scripts/set-mode.sh syncwerkt vanaf de volgende boot

De meetmodus is het deel dat het waard is om in elk soortgelijk gereedschap over te nemen. Hij laat je de fout op je eigen hardware zien voordat hij iets mag veranderen, en een hangbeveiliging zet hem uit als een boot Linux nooit bereikt1.

De eerste boot in sync-modus, rechtstreeks uit 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

En de kernel, een seconde later:

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

Geen warpregel. Geen “unstable”. /sys/devices/system/clocksource/clocksource0/current_clocksource geeft tsc. 121 ms boottijd voor de klok die de hardware altijd al had moeten hebben. Dat was op BIOS 1.28. 1.30 is niet zo netjes geweest.

De 21 nanoseconden die Linux terzijde schoof

Er staat één regel in dat rapport die er erger uitziet dan hij is, en die is het waard om uit te leggen in plaats van te verstoppen. De laatste verificatieronde van tscsync markeerde één kern:

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 cycli is ongeveer 21 nanoseconden. tscsync mikt op overeenstemming binnen ruwweg 10 cycli1, dus volgens zijn eigen doel miste CPU 7, en dat zei het ook. Daarna startte Linux en viel het helemaal niet terug op HPET.

De twee controles stellen verschillende vragen:

Oordeel van tscsync over de afwijkingWarptest van de kernel
Vraaghoe ver liggen de tellers uit elkaar?gaat de tijd tussen kernen ooit achteruit?
Methodeschatting uit een getimed heen-en-terugkernen lezen om beurten, vergelijken met de vorige lezing2
Normongeveer 10 cycligeen lezing lager dan de vorige
CPU 7 bij deze boot45 cycli, BADgeen warp, TSC behouden

De warps bsp/ap 0/0 op diezelfde regel is tscsync dat de test in kernelstijl draait. Het zag er geen. De kernel ook niet.

Het project weet al dat zijn oordeel strenger kan zijn dan dat van de kernel. Een IdeaPad in de hardwarelijst had vier kernen gemarkeerd op 28 tot 42 cycli die de kernel accepteerde, en sinds 2.1 wordt een traag heen-en-terug als UNSURE gemeld in plaats van BAD4. De regel van de README voor dat geval is de juiste: de eigen controle van de kernel beslist, en als de clocksource tsc is, is hij geslaagd1. Een heen-en-terug van 357 cycli is op deze machine niet traag, dus kwam het er als BAD uit. Gereedschap dat zijn werk laat zien en de kernel dan het laatste woord geeft, is de goede volgorde.

BIOS 1.30: de synchronisatie houdt, de TSC gaat toch

Op 1.30 heb ik tscsync terug in sync-modus gezet en twee keer herstart. Beide keren deed het zijn werk. Het zette de opstartprocessor ongeveer 6,17 miljard cycli vooruit, elke kern kwam binnen een paar dozijn cycli uit, en de kernel logde geen warp. Beide keren belandde Linux een paar seconden later toch op 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

Dit is een andere controle. De warptest draait terwijl elke kern opkomt en vraagt of de tijd tussen kernen ooit achteruitgaat. De clocksource-watchdog blijft na het opstarten draaien. Ongeveer elke halve seconde vergelijkt hij hoe ver de TSC is opgeschoven met hoe ver HPET is opgeschoven, en als ze meer dan een kleine marge verschillen, markeert hij de TSC als instabiel6. Eenmaal gemarkeerd blijft dat zo tot de volgende boot.

Twee kernelcontroles, en welke bootoptie welke overslaatFirmwareCPU 0 ~3 s achtertscsync, in UEFIzet CPU 0 vooruitLinux startkernen komen opWarpcontroleéén keer, bij bootWatchdogelke 0,5 s tegen HPETStandaardbeide aan: TSC weg op 1.30✓ gecontroleerd✓ gecontroleerdtsc=nowatchdogdeze boot: TSC behouden✓ gecontroleerd✗ overgeslagentsc=reliableafgeraden: verbergt een echte warp✗ overgeslagen✗ overgeslagen
Twee aparte controles kunnen de TSC laten vallen. De warpcontrole draait één keer, terwijl elke kern opkomt; tscsync is wat deze machine erlangs helpt. De watchdog blijft elke halve seconde tegen HPET vergelijken. tsc=nowatchdog slaat alleen de tweede over, terwijl tsc=reliable ze allebei overslaat en een echte warp zou verbergen.
Boot op 1.30WarpcontroleHPET-intervalTSC-intervalTSC tegen HPET
Eerste sync-bootgeslaagd495.426.793 ns495.006.210 nsongeveer 850 ppm traag
Tweede sync-bootgeslaagd495.136.113 ns496.026.671 nsongeveer 1.800 ppm snel

Loopt de TSC dan op het verkeerde tempo? Nee. Met de machine eenmaal op heb ik de TSC 30 seconden lang in vensters van 5 seconden getimed tegen de op HPET gebaseerde ruwe klok van de kernel. Elk venster kwam uit op 2.096,062 MHz, ongeveer 4 miljoenste van de eigen kalibratie van de kernel. Een echte tempofout zwaait niet tussen twee boots van traag naar snel om daarna te verdwijnen. Iets verstoort vroeg in de boot één venster van een halve seconde, en de regel remote CPU 5 read timed out vlak ervoor wijst op een kern die even niet antwoordt. Firmware die tijd van de CPU’s steelt, zou passen. Bewezen heb ik het niet.

Twee boots is al het bewijs dat er is. Ik kan nog niet zeggen of het 1.30 is, toeval, of iets wat de schrijfactie van tscsync in gang zet. De ene sync-boot op 1.28 hield de TSC en de twee op 1.30 verloren hem, en dat is niet genoeg om de BIOS de schuld te geven.

Voor dit geval is er een smallere kerneloptie dan tsc=reliable. tsc=nowatchdog zet de clocksource-watchdog voor de TSC uit en laat de warpcontrole bij het opstarten met rust7. Als tscsync ooit niet zou synchroniseren, zou de kernel de warp nog steeds vangen en terugvallen op HPET. Dat was de volgende test. De cijfers, en een paar voorgestelde wijzigingen aan de eigen rapportage van tscsync, staan in de issuetracker van het project8.

tsc=nowatchdog: vijf minuten, geen warps

Ik voegde tsc=nowatchdog toe en herstartte. tscsync zette de opstartprocessor 6.207.104.421 cycli vooruit, de kernel logde geen warp en geen skew, en current_clocksource gaf tsc. De eerste boot op 1.30 die hem hield.

Dat bewijst alleen dat de watchdog de TSC liet vallen. Het bewijst niet dat de TSC veilig is om op te draaien, en met de watchdog uit kijkt niets in de kernel er nog naar. Dus heb ik zelf gekeken.

Mijn eerdere tempocontrole had hier geen zin. Die timede de TSC tegen CLOCK_MONOTONIC_RAW, en dat was prima zolang de kernel op HPET draaide. Op de TSC is die klok uit de TSC opgebouwd, dus de test zou de teller met zichzelf vergelijken. HPET is vanuit gebruikersruimte ook niet te lezen, omdat de kernel van Fedora zonder CONFIG_HPET_MMAP is gebouwd. De real-time clock (RTC) wel. Die loopt op een eigen kristal, en de kernel wekt een programma bij elke tik van zijn secondenteller9.

Dus schreef ik een kleine controle, tsccheck. Hij pint zichzelf vast op CPU 0 en draait elke 15 seconden vier tests:

ControleHoeHoe een fout eruitziet
TempoTSC-cycli geteld tussen secondentikken van de RTCde MHz die één kant op drijven
Afwijking3.000 keer heen-en-terug van CPU 0 naar elke kern, afwijking uit de snelsteeen afwijking die groeit
Warpsbij elk heen-en-terug: las een kern lager dan een waarde die al gezien wasalles boven nul
Clocksourcecurrent_clocksource lezenalles behalve tsc

Het heen-en-terug is de kern ervan. CPU 0 leest zijn teller en geeft hem door, de andere kern leest de zijne en geeft die terug, en CPU 0 leest opnieuw. Als de kernen het eens zijn, moet de middelste lezing tussen de andere twee vallen:

Eén heen-en-terug tussen CPU 0 en een andere kernCPU 0CPU 1 tot 7, één tegelijktijdt0t1t2middenafwijkingafwijking = t1 − (t0 + t2) / 2uit de snelste van 3.000 rondeswarp als t1 < t0 of t2 < t1tijd ging achteruit: elke is een fout
Eén heen-en-terug. De lezing van de andere kern, t1, wordt vergeleken met het midden van de twee lezingen van CPU 0, en de snelste van 3.000 rondes geeft de afwijking. Elke lezing lager dan een die al gezien was, is een warp, wat de afwijking ook is.

De lus die het doet:

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));
	}
}

Het hele programma is gewone C voor x86-64 Linux, met niets dat aan tscsync of aan deze laptop vastzit. Het heeft root nodig voor /dev/rtc0, en het derde argument is het getal van je eigen kernel uit journalctl -k -b | grep 'Refined TSC':

gcc -O2 -Wall -pthread -o tsccheck tsccheck.c
sudo ./tsccheck 15 20 2096.073
tsccheck.c, de hele controle in 161 regels tsccheck.c · 4 kB

SHA-256 02eba1966a0097196acd5dfdcad9c4b315cafc1854c60f34c5b56080e6c38408.

Twintig metingen over vijf minuten, direct na het opstarten:

MetingResultaat
Clocksourcetsc in alle 20 metingen, geen kernelmeldingen
Warps0 in ongeveer 420.000 keer heen-en-terug
Grootste afwijking van CPU 085 cycli, ongeveer 41 ns, geen trend
Tempo over de hele run2.096,027 MHz, vanaf de eerste minuut binnen 2.096,022 tot 2.096,031
Tempo per venster van 15 seconden2.095,943 tot 2.096,104 MHz
TSC-tempo per venster van 15 seconden, en over de hele run+20 ppm0 ppm-20 ppm-40 ppm-60 ppm0 s60 s120 s180 s240 s300 sgetal van de kernel, 2.096,073 MHz15 s: venster -11,3 ppm, hele run -11,3 ppm30 s: venster -61,1 ppm, hele run -36,2 ppm45 s: venster -23,5 ppm, hele run -32,0 ppm60 s: venster +15,0 ppm, hele run -20,2 ppm75 s: venster -21,6 ppm, hele run -20,5 ppm90 s: venster -21,8 ppm, hele run -20,7 ppm105 s: venster -19,6 ppm, hele run -20,6 ppm120 s: venster -50,5 ppm, hele run -24,3 ppm135 s: venster +5,8 ppm, hele run -20,9 ppm150 s: venster -20,3 ppm, hele run -20,9 ppm165 s: venster -23,6 ppm, hele run -21,1 ppm180 s: venster -13,7 ppm, hele run -20,5 ppm195 s: venster -29,5 ppm, hele run -21,2 ppm210 s: venster -22,3 ppm, hele run -21,3 ppm225 s: venster -20,0 ppm, hele run -21,2 ppm240 s: venster -24,7 ppm, hele run -21,4 ppm255 s: venster -19,1 ppm, hele run -21,3 ppm270 s: venster -22,2 ppm, hele run -21,3 ppm285 s: venster -62,0 ppm, hele run -23,5 ppm300 s: venster +9,2 ppm, hele run -21,9 ppméén venster van 15 s, 61 ppm traaghele run tot nu toevenster van 15 secondenhele run tot nu toe
Tempo tegen de RTC, in miljoenste delen ten opzichte van het getal van de kernel. De losse vensters springen in paren op en neer; het getal over de hele run vlakt binnen de eerste minuut af op ongeveer 21 ppm traag en blijft daar.

De vensters zien er erger uit dan ze zijn. Een late RTC-tik maakt één venster lang en het volgende kort, dus de fouten komen in paren die elkaar opheffen: 61 miljoenste (ppm) traag na 30 seconden, 15 ppm snel na 60. Wegdrijven heft zichzelf niet op. Het getal over de hele run bewoog nauwelijks.

Het getal over de hele run zit de volle vijf minuten 22 ppm onder de 2.096,073 MHz van de kernel. Dat is het verschil tussen twee kristallen, en het stond stil. chrony, dat de klok bijstuurt tegen tijdservers op internet, had hem 16 tot 19 ppm traag10. Dezelfde richting, dezelfde grootte.

De afwijkingen springen tussen dezelfde paar waarden heen en weer, 10, 21 en 33 cycli aan weerszijden, zonder dat een kern wegloopt. Dat is ruis van een heen-en-terug van ongeveer 550 cycli. De verificatie van tscsync zou er sommige nog steeds BAD noemen. De kernel niet.

Hield de TSC stand? Ja, op elke maat die ik had. Vijf minuten na één boot is nog steeds geen reeks boots. Hierna komt dezelfde controle na een lange uptime, en nog eens over een paar herstarts.

De oplossing hoort in de firmware

tscsync zou niet moeten hoeven bestaan. De eigen README zegt het: firmwareleveranciers mogen de aanpak en de code hebben, “which is where this should be fixed”1. Lenovo heeft de broncode. De auteur van tscsync had alleen wat de eigen laptop prijsgaf, en 80 boots aan logs1.

Dat is wat blijft hangen. Er was een vreemde van buiten het bedrijf nodig om de fout te meten, te diagnosticeren en te omzeilen, terwijl de firma die de hardware verkocht hem valideerde tegen het ene besturingssysteem dat de fout verbergt. Een machine die je niet kunt repareren omdat de firmware gesloten is, is niet echt van jou; je hebt hem in bruikleen, onder voorwaarden. Open firmware met ondertekende updates had dit één keer, goed, laten oplossen door iedereen die genoeg gaf om te kijken.

Intussen: goed werk van de auteur van tscsync. Eerst de meetmodus, een hangbeveiliging, de cijfers openbaar en een eerlijk oordeel, ook als het met de kernel van mening verschilt. Zo hoort gereedschap op laag niveau gebouwd te worden.

Elke KVM-host met dezelfde firmware

Op een laptop kost dit wat latentie en wat batterij. Op een hypervisor betaalt elke gast mee, en niets van wat volgt komt door Proxmox. Het zit allemaal in de KVM-code van de Linux-kernel, dus het is hetzelfde op elke host die op KVM is gebouwd, wat er ook bovenop zit: Proxmox VE, oVirt, OpenStack, of kale QEMU en libvirt11.

Wat KVM doet als de host zijn TSC verliest

KVM geeft zijn gasten alleen een stabiele klok als de host zelf op de TSC draait. In de kernelbroncode wordt de master clock, één gedeelde tijdreferentie voor elke vCPU in een VM, alleen aangezet als host_tsc_clocksource waar is12. Val terug op HPET en hij gaat uit, en er verandert meteen van alles:

Host op TSCHost op HPET, TSC als instabiel gemarkeerd
KVM-master clock12aanuit
Een vCPU verhuist naar een andere kern12niets bij te werkeneen klokupdate afgedwongen op elke vCPU in die VM, tot 10 per seconde
De TSC-offset van de gast12met rust gelatenbij elke verhuizing herschreven, en in inhaalmodus gezet
PVCLOCK_TSC_STABLE_BIT naar gasten12gezetniet gezet
clock_gettime() in de gast13gelezen in gebruikersruimteeen systeemaanroep, één keer besloten bij het opstarten van de gast
Kloklezingen in de gast over vCPU’s heen14onafhankelijkelke lezing gecontroleerd tegen één gedeelde globale waarde

De laatste rij is degene die slecht schaalt. Zonder de stable bit kan de gastkernel er niet op vertrouwen dat de klok van elke vCPU met de andere overeenkomt, dus elke lezing wordt vergeleken met “last_value, a global accumulator” en met een atomische compare-and-exchange ingeruild als hij nieuwer is14. Elke vCPU in de gast schrijft naar die ene waarde. Hoe meer vCPU’s de klok lezen, hoe meer ze erop in de rij staan.

Niets ervan verschijnt als fout. De clocksource van de gast geeft nog steeds kvm-clock, en niets in de gast vertelt je waarom hij trager werd.

Hoe de clocksource van de host een KVM-gast bereiktHost op TSCClocksource host: tsclezen in één instructieKVM-master clock aanstable bit gezet voor gastenGast leest in gebruikersruimtevDSO, ongeveer 28 nsHost op HPETClocksource host: hpeteen rit naar het timerapparaatKVM-master clock uitgeen stable bit, updates bij elke vCPU-verhuizingGast doet een systeemaanroepéén gedeelde waarde, ongeveer 190 nsDezelfde kernelcode op Proxmox VE, oVirt, OpenStack of kale QEMU en libvirt
De clocksource van de host bepaalt wat elke gast krijgt. Op de TSC lezen gasten de klok in gebruikersruimte. Op HPET laat KVM zijn master clock vallen, dwingt elke vCPU-verhuizing klokupdates af, en wordt elke kloklezing in de gast een systeemaanroep die wordt gecontroleerd tegen één waarde die elke vCPU deelt.

Wat het kost, gemeten

Ik heb het op deze laptop gemeten in plaats van het als theorie te laten. De clocksource van de host kan tijdens het draaien worden omgezet, en dat zet de master clock van KVM uit, net als de terugval bij het opstarten. Een wegwerpgast startte de eigen kernel van de host met 4 vCPU’s en een piepkleine initramfs waarvan het enige programma clock_gettime() timet. Twee runs per kant, eerst de host, dan de gast:

Nanoseconden per kloklezing, host op TSC tegen host op HPETHostschaal 0 tot 2.400 nsHost, 1 thread, host op TSC: 24 ns24 nsHost, 1 thread, host op HPET: 1.542 ns1.542 ns1 threadHost, alle 8 kernen, host op TSC: 29 ns29 nsHost, alle 8 kernen, host op HPET: 1.922 ns1.922 nsalle 8 kernenGast, 4 vCPU'sschaal 0 tot 300 nsGast, 4 vCPU's, 1 vCPU, host op TSC: 28 ns28 nsGast, 4 vCPU's, 1 vCPU, host op HPET: 187 ns187 ns1 vCPUGast, 4 vCPU's, alle 4 vCPU's, host op TSC: 31 ns31 nsGast, 4 vCPU's, alle 4 vCPU's, host op HPET: 244 ns244 nsalle 4 vCPU'shost-clocksource tschost-clocksource hpet
Nanoseconden per aanroep van clock_gettime(), gemiddelde van twee runs per kant. De host wordt op HPET 61 tot 73 keer trager; de gast ongeveer 7 keer met één vCPU en 8 keer met alle vier. De panelen hebben verschillende schalen, omdat de HPET-balken van de host die van de gast plat zouden drukken.

De gast betaalt de prijs van HPET niet direct, want kvm-clock leest daaronder nog steeds de TSC. Wat hij verliest, is de lezing in gebruikersruimte en de onafhankelijkheid tussen vCPU’s, en dat alleen al is zeven keer trager. Met alle vier de vCPU’s tegelijk aan het lezen gaan de kosten weer omhoog, en dat is die gedeelde globale waarde aan het werk. Een gast met 32 of 64 vCPU’s staat er harder voor in de rij. Dat heb ik niet gemeten.

Zet er een getal op. Een gast die op één vCPU een miljoen keer per seconde de klok leest, besteedt daar op een gezonde host 2,8% van die vCPU aan, en op deze 19%. Databases, de Java Virtual Machine (JVM) en alles wat zijn werk van een tijdstempel voorziet, lezen de klok veel.

Deze run onderschat het ook nog. De clocksource met de hand omzetten zet de master clock uit, maar markeert de TSC niet als instabiel, dus het herschrijven van de offset uit de tabel hierboven draaide nooit. De terugval bij het opstarten op deze E595 markeert hem wel als instabiel, en een host in die toestand betaalt allebei.

clockbench.c, de benchmark, te draaien op de host of als init van een gast clockbench.c · 2 kB

Een host controleren, en de grenzen

Waarom gastklokken überhaupt kwetsbaar zijn, heb ik behandeld in het ptp_kvm-artikel; dit is hetzelfde probleem dat van onderaf binnenkomt. Controleer de host, niet de gast:

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

tscsync draait vanuit GRUB of systemd-boot1, en dat dekt de meeste KVM-hosts. Proxmox VE gebruikt GRUB, of systemd-boot bij een UEFI-installatie met ZFS als root en Secure Boot uit15. Wees wel eerlijk over de grenzen voordat het in de buurt van een productienode komt.

GrensWat je eraan doet
Alleen AMD Zen en Hygon1Intel-hosts met TSC_ADJUST worden al door de kernel hersteld
Elke machine in de hardwarelijst is een laptop4behandel een server als ongetest en meld hem
Sync-modus weigert hardware buiten de lijst, tenzij je ervoor kiest1lees waarom voordat je het overschrijft
Een eerste sync-boot kan blijven hangeneerst meetmodus, out-of-band-console open, één node tegelijk
Een BIOS-update kan de fout verhelpenzet het daarna terug op meten en controleer1
Een schone synchronisatie kan de TSC nog steeds aan de watchdog verliezencontroleer current_clocksource na elke boot; tsc=nowatchdog als de watchdog de oorzaak is, en meet het dan zelf

Een firmwarefout die een laptop wat batterij kost, kost een hypervisor een stukje van elke gast die hij draait, elke seconde dat hij aanstaat, en de eigenaars van de gasten kunnen het op geen enkele manier zien. Daarom controleer je current_clocksource op elke host die je draait, wie hem ook gemaakt heeft.


  1. tscsync README — wat het doet, de kosten van een kloklezing op HPET, waarom tsc=reliable geen oplossing is, het veiligheidsmodel en de UNSURE-regel. ↩︎ ↩︎ ↩︎ ↩︎ ↩︎ ↩︎ ↩︎ ↩︎ ↩︎ ↩︎ ↩︎ ↩︎ ↩︎ ↩︎

  2. Linux kernel — arch/x86/kernel/tsc_sync.c — de afhandeling van TSC_ADJUST, de warptest en de melding “Measured %Ld cycles TSC warp between CPUs, turning off TSC clock.” ↩︎ ↩︎ ↩︎

  3. tscsync — docs/DESIGN.md — AMD Zen heeft geen IA32_TSC_ADJUST, tsc=directsync upstream afgewezen, Windows schrijft de TSC’s bij het opstarten zelf, en de warp die schaalt met de tijd in de firmware op de Legion Pro 5. ↩︎ ↩︎ ↩︎

  4. tscsync — docs/HARDWARE.md — geteste machines, de kernen van de IdeaPad 5 die op -28 tot -42 cycli werden geaccepteerd, en de gemelde BIOS-oplossingen op de A485, E495 en T14s Gen 1. ↩︎ ↩︎ ↩︎ ↩︎

  5. Lenovo — BIOS Update Utility readme, ThinkPad E495/E595, version 1.30 — beide modellen in één pakket, de versiegeschiedenis tot 1.30 (14 mei 2024) en de wijzigingslijst ervan. ↩︎ ↩︎

  6. Linux kernel — kernel/time/clocksource.c — de clocksource-watchdog, zijn vergelijking van intervallen en de melding “Marking clocksource %s unstable due to frequency skew”. ↩︎

  7. Linux kernel — kernel-parameters — tsc=reliable schakelt de verificatie tijdens het draaien en de stabiliteitscontroles bij het opstarten uit; tsc=nowatchdog schakelt alleen de clocksource-watchdog uit. ↩︎

  8. tscsync issue #1 — Lenovo ThinkPad E595 — de meldingen voor 1.28 en 1.30, de analyse van de watchdog en de voorgestelde wijzigingen. ↩︎

  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”, gelezen via /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 en de op libvirt gebaseerde tools, die allemaal KVM beheren. ↩︎

  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”, en PVCLOCK_TSC_STABLE_BIT alleen gezet met de master clock. ↩︎ ↩︎ ↩︎ ↩︎ ↩︎

  13. Linux kernel — arch/x86/kernel/kvmclock.c — de gast zet de klok in gebruikersruimte VDSO_CLOCKMODE_PVCLOCK alleen aan als PVCLOCK_TSC_STABLE_BIT gezet is. ↩︎

  14. Linux kernel — arch/x86/kernel/pvclock.c — zonder PVCLOCK_TSC_STABLE_BIT wordt elke lezing gecontroleerd tegen “last_value, a global accumulator” met een atomische compare-and-exchange. ↩︎ ↩︎

  15. Proxmox VE — Host Bootloader — standaard GRUB, systemd-boot voor UEFI-installaties met ZFS als root en Secure Boot uit. ↩︎