Mise à jour, 11 octobre 2026, 16 h 30 : j’ai depuis flashé le BIOS 1.30. Il ne corrige pas le décalage. tscsync aligne toujours les cœurs, mais sur deux démarrages en mode sync le noyau a quand même lâché le TSC, par son watchdog des clocksources et non par le contrôle de warp : BIOS 1.30 : la synchro tient, le TSC tombe quand même. Avec tsc=nowatchdog en plus, le TSC a tenu, et cinq minutes de mesure contre l’horloge temps réel n’ont trouvé ni warp ni dérive : tsc=nowatchdog : cinq minutes, aucun warp. Un démarrage n’est pas une série de démarrages, donc je dis que ça tient, pas que c’est corrigé.

À chaque démarrage, le même avertissement

Ceci traînait dans le journal du noyau de mon ThinkPad E595 à chaque démarrage, sans exception :

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

Rien n’a planté. Rien n’est apparu sur le bureau. La machine a tranquillement fait tourner toute son horloge sur une puce de timer plus lente dès le démarrage, et elle aurait continué indéfiniment si je n’avais pas lu le journal pour tout autre chose.

Le Time Stamp Counter (TSC) est un compteur dans chaque cœur de CPU qui avance à cadence fixe. Le lire tient en une seule instruction. Linux s’en sert comme horloge principale, mais seulement si tous les cœurs sont d’accord sur le compte1. Si un cœur est en retard, le noyau ne peut pas s’y fier et se rabat sur le High Precision Event Timer (HPET), un équipement de timer séparé. L’auteur de tscsync a mesuré une lecture d’horloge à environ 1,2 µs sur le HPET contre quelques dizaines de nanosecondes sur le TSC, et le noyau et le bureau lisent l’horloge des dizaines de milliers de fois par seconde1. C’est de la latence et de la batterie, payées à chaque démarrage, pour rien.

Alors, de combien le cœur était-il en retard ? Plus de trois secondes. Sur un portable à 2,1 GHz, ça fait 6,6 milliards de cycles.

Ce que le firmware transmet

Le message nomme CPU 0 et CPU 2, mais le fautif n’est pas CPU 2. Mesuré par tscsync depuis le chargeur d’amorçage, puis par le noyau une fois lancé, le tableau est le même à chaque fois :

CœurDécalage par rapport à CPU 0Ce que ça veut dire
CPU 0, le processeur d’amorçage (BSP)0la référence, et l’intrus
CPU 1 à 7, les autres cœurs (AP)environ +6,2 à +6,8 milliards de cyclestous d’accord entre eux à 32 cycles près
Écart à 2 096 MHz2,9 à 3,3 secondesle processeur d’amorçage est en retard, à chaque démarrage

Sept cœurs sont d’accord. Un a trois secondes de retard. Le firmware démarre le processeur d’amorçage, fait son travail dessus, et passe la main au chargeur d’amorçage sans jamais remettre ce compteur-là en ligne avec les autres.

Le TSC du processeur d'amorçage avant et après tscsyncAvant : tel que le firmware le transmetAprès : tscsync en mode syncCPU 0 (BSP)CPU 1CPU 2CPU 3CPU 4CPU 5CPU 6CPU 7CPU 0 (BSP)CPU 1CPU 2CPU 3CPU 4CPU 5CPU 6CPU 7CPU 0 environ 6,8 milliards de cycles (3,25 s) en retardCPU 1 à 7 d'accord à 32 cycles prèsBSP avancé, tous les cœurs à 45 cycles prèsLinux garde la clocksource TSC
À gauche : ce que le firmware transmet. Les sept processeurs d’application sont d’accord à 32 cycles près ; le processeur d’amorçage a environ 6,8 milliards de cycles de retard sur eux. À droite : une fois que tscsync a avancé le processeur d’amorçage en UEFI, tous les cœurs sont à 45 cycles près et le contrôle de warp du noyau passe.

Pourquoi Linux ne peut-il pas simplement corriger ça ? Sur les CPU Intel, il existe un registre, IA32_TSC_ADJUST, qui permet au noyau d’ajuster le compteur d’un cœur, et le code de synchronisation du noyau s’en sert quand il est là2. Les CPU AMD Zen ne l’ont pas. Sans lui, rien dans Linux ne peut réparer le décalage, et la tentative d’écrire les compteurs directement depuis le noyau (tsc=directsync) a été rejetée en amont comme peu fiable3.

Le noyau fait donc la seule chose honnête qui reste. Il mesure, il constate que le temps recule entre deux cœurs, il affiche l’avertissement et il arrête d’utiliser le TSC2.

Quarante démarrages, quarante warps

Un démarrage, c’est une anecdote. Le journal de cette machine remonte au 30 mai 2026, alors je lui ai posé la question.

DémarragesWarp, cyclesWarp à 2 096 MHz
Du 30 mai au 11 octobre 202640 sur 406,16 à 9,88 milliards2,94 à 4,72 s
Médiane6,57 milliards3,14 s
Après une coupure complète d’environ 27 s16,60 milliards3,15 s
Avec tscsync en mode sync, BIOS 1.281aucun consignéTSC conservé
BIOS 1.30, tscsync désactivé16,13 milliards2,92 s
BIOS 1.30, tscsync en mode sync2aucun consignéTSC lâché par le watchdog, voir plus bas
BIOS 1.30, mode sync et tsc=nowatchdog1aucun consignéTSC conservé

À chaque démarrage. Démarrage à froid, redémarrage, après une coupure assez longue pour qu’on s’attende à ce que tout résidu ait disparu. La valeur bouge, ce qui colle avec ce qu’a trouvé l’auteur de tscsync sur un Lenovo Legion : là-bas, le warp suit presque parfaitement le temps que la machine passe dans le firmware avant que le chargeur d’amorçage démarre3. Un démarrage qui reste plus longtemps sur le logo en sort plus en retard.

La correction tentante, c’est tsc=reliable sur la ligne de commande du noyau, qui dit à Linux d’arrêter de vérifier. Ne le faites pas. Les cœurs ont vraiment trois secondes d’écart, et couper le contrôle n’y change rien ; ça laisse juste le noyau utiliser une horloge qui saute en arrière quand une tâche passe d’un cœur à l’autre. Le README de tscsync dit la même chose, et nomme les saccades audio et vidéo qui s’ensuivent1.

Puis-je demander pourquoi ça sort encore ?

Je ne demande pas qui. Je demande ce qui, dans le processus de Lenovo, a laissé sortir un BIOS qui laisse un cœur trois secondes en retard à chaque démarrage.

La réponse est dans les notes de conception de tscsync, et elle n’est pas flatteuse. Fisher-Price OS (Windows) écrit lui-même les TSC au démarrage3. Un portable validé uniquement là-dessus passe, parce que le système d’exploitation range discrètement derrière le firmware. Linux garde son contrôle de sécurité actif et rapporte ce qu’on lui a donné, et c’est exactement pour ça que le défaut apparaît là et nulle part ailleurs.

Le plus gênant, c’est que ça aurait déjà été corrigé. Des utilisateurs rapportent que Lenovo a corrigé la même classe de bug par mise à jour du BIOS sur les ThinkPad A485, T14s Gen 1 AMD et E4954. Le E495 est le petit frère 14 pouces de cette machine, et ils partagent un seul BIOS : le readme de Lenovo pour le paquet liste les deux5.

Mon E595 était en 1.28 et faisait un warp à chaque démarrage. Lenovo a publié depuis les versions 1.29 et 1.30, et aucune des deux listes de changements ne dit un mot du TSC ; la 1.30 corrige « system randomly freeze since BIOS V1.27 » et quelques problèmes de sécurité5. J’ai donc installé la 1.30 et mesuré à nouveau. Le premier démarrage dessus, tscsync désactivé, a consigné un warp de 6 126 446 316 cycles : 2,92 secondes, CPU#0 -> CPU#2, comme toujours. La 1.30 ne le corrige pas. Les deux modèles partagent un seul BIOS, donc si le rapport sur le E495 était juste, cette machine serait corrigée aussi. Le rapport ne cite aucune version de BIOS et ne renvoie à aucun journal4.

Lenovo va-t-il revenir sur un portable de 2019 pour un défaut que lui cache le système d’exploitation sur lequel il teste ? Non.

tscsync, mesuré d’abord

tscsync est un petit programme UEFI qui se lance depuis GRUB ou systemd-boot juste avant Linux. Il demande à chaque cœur son TSC via les propres services multiprocesseurs du firmware, calcule le décalage de chaque cœur par rapport au processeur d’amorçage, avance les compteurs en retard jusqu’à ce qu’ils soient d’accord, puis vérifie le résultat avec un test calqué sur le contrôle de warp du noyau1. Il n’écrit jamais dans la flash, la NVRAM ou les réglages du firmware ; les compteurs qu’il modifie se remettent à zéro à l’extinction1.

Ça fait beaucoup à confier à un chargeur d’amorçage, donc j’ai d’abord lu le code, au commit 230eaa9, avant de le compiler. L’installation s’est ensuite faite comme le projet le demande :

ÉtapeCommandeCe qui est revenu
Compilationmakebinaires GRUB et systemd-boot
Installation, lecture seulesudo scripts/install.shmode mesure, rien d’écrit dans les compteurs
Extinction, démarrage, coup d’œiltscsync-statusCPU 0 2 964 ms derrière les 7 AP, les AP d’accord à quelques microsecondes près
Activationsudo scripts/set-mode.sh syncprend effet au prochain démarrage

Le mode mesure, c’est la partie à copier dans tout outil de ce genre. Il vous montre le défaut sur votre propre matériel avant d’avoir le droit de changer quoi que ce soit, et une protection contre les blocages le désactive si un démarrage n’atteint jamais Linux1.

Le premier démarrage en mode sync, tout droit sorti de 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

Et le noyau, une seconde plus tard :

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

Pas de ligne de warp. Pas de « unstable ». /sys/devices/system/clocksource/clocksource0/current_clocksource indique tsc. 121 ms de temps de démarrage pour obtenir l’horloge que le matériel était censé avoir depuis le début. C’était sous BIOS 1.28. La 1.30 n’a pas été aussi propre.

Les 21 nanosecondes que Linux a laissé passer

Il y a une ligne dans ce rapport qui a l’air pire qu’elle ne l’est, et elle mérite d’être expliquée plutôt que cachée. La passe de vérification finale de tscsync a signalé un cœur :

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 cycles, c’est environ 21 nanosecondes. tscsync vise un accord à une dizaine de cycles près1, donc selon sa propre cible CPU 7 a raté, et il l’a dit. Puis Linux a démarré et n’est pas du tout retombé sur le HPET.

Les deux contrôles posent des questions différentes :

Verdict de décalage de tscsyncTest de warp du noyau
Questionà quelle distance sont les compteurs ?le temps recule-t-il parfois entre les cœurs ?
Méthodeestimation à partir d’un aller-retour chronométréles cœurs lisent à tour de rôle et comparent avec la dernière lecture2
Seuilenviron 10 cyclesaucune lecture inférieure à la précédente
CPU 7 sur ce démarrage45 cycles, BADaucun warp, TSC conservé

Le warps bsp/ap 0/0 sur cette même ligne, c’est tscsync qui exécute le test façon noyau. Il n’en a vu aucun. Le noyau non plus.

Le projet sait déjà que son verdict peut être plus strict que celui du noyau. Un IdeaPad de sa liste de matériel avait quatre cœurs signalés à 28 à 42 cycles que le noyau a acceptés, et depuis la 2.1 un aller-retour lent est rapporté comme UNSURE plutôt que BAD4. La règle du README pour ce cas est la bonne : c’est le contrôle du noyau qui tranche, et si la clocksource est tsc, il est passé1. Un aller-retour de 357 cycles n’est pas lent sur cette machine, donc c’est sorti en BAD. Un outil qui montre son raisonnement et laisse ensuite le dernier mot au noyau, c’est le bon ordre des choses.

BIOS 1.30 : la synchro tient, le TSC tombe quand même

Sous la 1.30, j’ai remis tscsync en mode sync et redémarré deux fois. Les deux fois, il a fait son travail. Il a avancé le processeur d’amorçage d’environ 6,17 milliards de cycles, tous les cœurs sont ressortis à quelques dizaines de cycles près, et le noyau n’a consigné aucun warp. Les deux fois, Linux a quand même fini sur le HPET quelques secondes plus tard :

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

C’est un autre contrôle. Le test de warp s’exécute à mesure que chaque cœur démarre et demande si le temps recule parfois entre les cœurs. Le watchdog des clocksources, lui, continue de tourner après le démarrage. Environ toutes les demi-secondes, il compare de combien le TSC a avancé et de combien le HPET a avancé, et s’ils divergent de plus d’une petite marge, il marque le TSC instable6. Une fois marqué, il le reste jusqu’au démarrage suivant.

Deux contrôles du noyau, et quelle option de démarrage saute lequelFirmwareCPU 0 ~3 s en retardtscsync, en UEFIavance CPU 0Linux démarreles cœurs démarrentContrôle de warpune fois, au démarrageWatchdogcontre HPET, 2 fois/sPar défautles deux actifs : TSC lâché en 1.30✓ vérifié✓ vérifiétsc=nowatchdogce démarrage : TSC conservé✓ vérifié✗ sautétsc=reliabledéconseillé : masque un vrai warp✗ sauté✗ sauté
Deux contrôles distincts peuvent faire lâcher le TSC. Le contrôle de warp s’exécute une fois, à mesure que chaque cœur démarre ; c’est tscsync qui fait passer cette machine. Le watchdog continue de comparer avec le HPET toutes les demi-secondes. tsc=nowatchdog ne saute que le second, alors que tsc=reliable saute les deux et masquerait un vrai warp.
Démarrage sous 1.30Contrôle de warpIntervalle HPETIntervalle TSCTSC par rapport au HPET
Premier démarrage syncpassé495 426 793 ns495 006 210 nsenviron 850 ppm trop lent
Second démarrage syncpassé495 136 113 ns496 026 671 nsenviron 1 800 ppm trop rapide

Alors, le TSC tourne-t-il à la mauvaise cadence ? Non. Machine démarrée, j’ai chronométré le TSC contre l’horloge brute du noyau basée sur le HPET pendant 30 secondes, par fenêtres de 5 secondes. Chaque fenêtre est sortie à 2 096,062 MHz, à environ 4 parties par million de la propre calibration du noyau. Un vrai défaut de cadence ne passe pas de lent à rapide entre deux démarrages pour ensuite disparaître. Quelque chose perturbe une fenêtre d’une demi-seconde tôt au démarrage, et la ligne remote CPU 5 read timed out juste avant désigne un cœur qui, brièvement, ne répond pas. Un firmware qui vole du temps aux CPU collerait. Je ne l’ai pas prouvé.

Deux démarrages, c’est toute la preuve disponible. Je ne peux pas encore dire si c’est la 1.30, le hasard, ou quelque chose que déclenche l’écriture de tscsync. Le seul démarrage sync sous 1.28 a gardé le TSC et les deux sous 1.30 l’ont perdu, et ça ne suffit pas pour accuser le BIOS.

Il existe une option du noyau plus ciblée que tsc=reliable pour ce cas. tsc=nowatchdog désactive le watchdog des clocksources pour le TSC et laisse tranquille le contrôle de warp au démarrage7. Si tscsync échouait un jour à synchroniser, le noyau attraperait quand même le warp et retomberait sur le HPET. C’était le test suivant. Les chiffres, et quelques suggestions de changements dans les rapports de tscsync lui-même, sont sur le gestionnaire de tickets du projet8.

tsc=nowatchdog : cinq minutes, aucun warp

J’ai ajouté tsc=nowatchdog et redémarré. tscsync a avancé le processeur d’amorçage de 6 207 104 421 cycles, le noyau n’a consigné ni warp ni écart de fréquence, et current_clocksource indiquait tsc. Premier démarrage sous 1.30 à le garder.

Ça prouve seulement que c’était le watchdog qui lâchait le TSC. Ça ne prouve pas qu’on peut tourner sur le TSC en toute sécurité, et avec le watchdog coupé, plus rien dans le noyau ne surveille. Alors je l’ai surveillé moi-même.

Ma vérification de cadence précédente ne servait à rien ici. Elle chronométrait le TSC contre CLOCK_MONOTONIC_RAW, ce qui allait tant que le noyau tournait sur le HPET. Sur le TSC, cette horloge est construite à partir du TSC, donc le test comparerait le compteur avec lui-même. Le HPET ne peut pas non plus être lu depuis l’espace utilisateur, parce que le noyau de Fedora est compilé sans CONFIG_HPET_MMAP. L’horloge temps réel (RTC), si. Elle tourne sur son propre quartz, et le noyau réveille un programme à chaque tick de son compteur de secondes9.

J’ai donc écrit une petite vérification, tsccheck. Elle s’épingle sur CPU 0 et lance quatre tests toutes les 15 secondes :

VérificationCommentÀ quoi ressemble un défaut
Cadencecycles TSC comptés entre les ticks de seconde de la RTCdes MHz qui dérivent dans un sens
Décalage3 000 allers-retours de CPU 0 vers chaque cœur, décalage pris sur le plus rapideun décalage qui grandit
Warpsà chaque aller-retour, un cœur a-t-il lu moins qu’une valeur déjà vuetout ce qui dépasse zéro
Clocksourcelecture de current_clocksourceautre chose que tsc

L’aller-retour est le cœur de l’affaire. CPU 0 lit son compteur et le transmet, l’autre cœur lit le sien et le renvoie, et CPU 0 relit. Si les cœurs sont d’accord, la lecture du milieu doit tomber entre les deux autres :

Un aller-retour entre CPU 0 et un autre cœurCPU 0CPU 1 à 7, un à la foistempst0t1t2point milieudécalagedécalage = t1 − (t0 + t2) / 2sur le plus rapide de 3 000 allers-retourswarp si t1 < t0 ou t2 < t1le temps a reculé : un seul est un défaut
Un aller-retour. La lecture de l’autre cœur, t1, est comparée au point milieu des deux lectures de CPU 0, et le plus rapide de 3 000 allers-retours donne le décalage. Toute lecture inférieure à une lecture déjà vue est un warp, quel que soit le décalage.

La boucle qui fait ça :

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

Le programme entier est du C simple pour Linux x86-64, sans rien de lié à tscsync ou à ce portable. Il a besoin de root pour /dev/rtc0, et le troisième argument est le chiffre de votre propre noyau, tiré de journalctl -k -b | grep 'Refined TSC' :

gcc -O2 -Wall -pthread -o tsccheck tsccheck.c
sudo ./tsccheck 15 20 2096.073
tsccheck.c, toute la vérification en 161 lignes tsccheck.c · 4 kB

SHA-256 02eba1966a0097196acd5dfdcad9c4b315cafc1854c60f34c5b56080e6c38408.

Vingt échantillons sur cinq minutes, juste après le démarrage :

MesureRésultat
Clocksourcetsc dans les 20 échantillons, aucun message du noyau
Warps0 sur environ 420 000 allers-retours
Pire décalage par rapport à CPU 085 cycles, environ 41 ns, aucune tendance
Cadence sur toute la durée2 096,027 MHz, entre 2 096,022 et 2 096,031 dès la première minute
Cadence par fenêtre de 15 secondes2 095,943 à 2 096,104 MHz
Cadence du TSC par fenêtre de 15 secondes, et sur toute la durée+20 ppm0 ppm-20 ppm-40 ppm-60 ppm0 s60 s120 s180 s240 s300 schiffre du noyau, 2 096,073 MHz15 s : fenêtre -11,3 ppm, toute la durée -11,3 ppm30 s : fenêtre -61,1 ppm, toute la durée -36,2 ppm45 s : fenêtre -23,5 ppm, toute la durée -32,0 ppm60 s : fenêtre +15,0 ppm, toute la durée -20,2 ppm75 s : fenêtre -21,6 ppm, toute la durée -20,5 ppm90 s : fenêtre -21,8 ppm, toute la durée -20,7 ppm105 s : fenêtre -19,6 ppm, toute la durée -20,6 ppm120 s : fenêtre -50,5 ppm, toute la durée -24,3 ppm135 s : fenêtre +5,8 ppm, toute la durée -20,9 ppm150 s : fenêtre -20,3 ppm, toute la durée -20,9 ppm165 s : fenêtre -23,6 ppm, toute la durée -21,1 ppm180 s : fenêtre -13,7 ppm, toute la durée -20,5 ppm195 s : fenêtre -29,5 ppm, toute la durée -21,2 ppm210 s : fenêtre -22,3 ppm, toute la durée -21,3 ppm225 s : fenêtre -20,0 ppm, toute la durée -21,2 ppm240 s : fenêtre -24,7 ppm, toute la durée -21,4 ppm255 s : fenêtre -19,1 ppm, toute la durée -21,3 ppm270 s : fenêtre -22,2 ppm, toute la durée -21,3 ppm285 s : fenêtre -62,0 ppm, toute la durée -23,5 ppm300 s : fenêtre +9,2 ppm, toute la durée -21,9 ppmune fenêtre de 15 s, 61 ppm trop lentcumul jusqu'icifenêtre de 15 secondescumul jusqu'ici
Cadence contre la RTC, en parties par million par rapport au chiffre du noyau. Les fenêtres isolées sautent par paires ; le chiffre sur toute la durée se stabilise à environ 21 ppm trop lent dès la première minute et y reste.

Les fenêtres ont l’air pires qu’elles ne le sont. Un tick RTC en retard rend une fenêtre longue et la suivante courte, donc les erreurs viennent par paires qui s’annulent : 61 parties par million (ppm) trop lent à 30 secondes, 15 ppm trop rapide à 60. Une dérive ne s’annule pas d’elle-même. Le chiffre sur toute la durée a à peine bougé.

Le chiffre sur toute la durée reste 22 ppm sous les 2 096,073 MHz du noyau pendant les cinq minutes. C’est l’écart entre deux quartz, et il n’a pas bougé. chrony, qui pilote l’horloge contre des serveurs de temps sur Internet, l’avait à 16 à 19 ppm trop lent10. Même sens, même ordre de grandeur.

Les décalages sautent entre les mêmes quelques valeurs, 10, 21 et 33 cycles de part et d’autre, sans qu’aucun cœur ne s’éloigne. C’est le bruit d’un aller-retour d’environ 550 cycles. La vérification de tscsync en qualifierait encore certains de BAD. Le noyau, non.

Le TSC a-t-il tenu ? Oui, sur toutes les mesures que j’avais. Cinq minutes après un seul démarrage, ce n’est toujours pas une série de démarrages. La suite : la même vérification après une longue durée de fonctionnement, puis de nouveau sur quelques redémarrages de plus.

La correction a sa place dans le firmware

tscsync ne devrait pas avoir besoin d’exister. Son propre README le dit : les fabricants de firmware sont les bienvenus pour reprendre l’approche et le code, « which is where this should be fixed »1. Lenovo a le code source. L’auteur de tscsync n’avait que ce que son portable voulait bien lui dire, et 80 démarrages de journaux1.

C’est ça qui reste en travers. La correction a demandé à un inconnu extérieur à l’entreprise de mesurer, diagnostiquer et contourner, pendant que la société qui a vendu le matériel l’a validé sur le seul système d’exploitation qui cache le défaut. Une machine que vous ne pouvez pas réparer parce que son firmware est fermé n’est pas vraiment à vous, elle vous est prêtée sous conditions. Un firmware ouvert avec des mises à jour signées aurait permis de corriger ça une fois, proprement, par n’importe qui d’assez motivé pour regarder.

En attendant, beau travail de l’auteur de tscsync. Le mode mesure d’abord, une protection contre les blocages, les chiffres au grand jour et un verdict honnête même quand il contredit le noyau. C’est comme ça qu’on devrait construire les outils bas niveau.

Tous les hôtes KVM avec le même firmware

Sur un portable, ça coûte un peu de latence et un peu de batterie. Sur un hyperviseur, chaque invité le paie aussi, et rien de ce qui suit n’est dû à Proxmox. Tout se passe dans le code KVM du noyau Linux, donc c’est pareil sur n’importe quel hôte construit sur KVM, quoi qu’il y ait par-dessus : Proxmox VE, oVirt, OpenStack, ou QEMU et libvirt tout simplement11.

Ce que fait KVM quand l’hôte perd son TSC

KVM ne donne une horloge stable à ses invités que si l’hôte lui-même tourne sur le TSC. Dans le code source du noyau, l’horloge maître, une référence de temps partagée par tous les vCPU d’une VM, n’est activée que si host_tsc_clocksource est vrai12. Retombez sur le HPET et elle se désactive, et plusieurs choses changent d’un coup :

Hôte sur TSCHôte sur HPET, TSC marqué instable
Horloge maître KVM12activeinactive
Un vCPU passe sur un autre cœur12rien à mettre à jourune mise à jour d’horloge forcée sur chaque vCPU de la VM, jusqu’à 10 par seconde
Le décalage TSC de l’invité12laissé tel quelréécrit à chaque déplacement, et mis en mode rattrapage
PVCLOCK_TSC_STABLE_BIT vers les invités12positionnénon positionné
clock_gettime() dans l’invité13lu en espace utilisateurun appel système, décidé une fois au démarrage de l’invité
Lectures d’horloge de l’invité entre vCPU14indépendanteschaque lecture comparée à une seule valeur globale partagée

La dernière ligne est celle qui passe mal à l’échelle. Sans le bit stable, le noyau invité ne peut pas compter sur l’accord entre l’horloge de chaque vCPU et celle des autres, donc chaque lecture est comparée à « last_value, a global accumulator » et y est inscrite par un compare-and-exchange atomique si elle est plus récente14. Chaque vCPU de l’invité écrit dans cette seule valeur. Plus il y a de vCPU qui lisent l’horloge, plus ils font la queue dessus.

Rien de tout ça n’apparaît comme une erreur. La clocksource de l’invité indique toujours kvm-clock, et rien dans l’invité ne vous dira pourquoi il a ralenti.

Comment la clocksource de l'hôte arrive jusqu'à un invité KVMHôte sur TSCClocksource de l'hôte : tscune seule instruction pour lireHorloge maître KVM activebit stable positionné pour les invitésL'invité lit en espace utilisateurvDSO, environ 28 nsHôte sur HPETClocksource de l'hôte : hpetun aller-retour vers l'équipement timerHorloge maître KVM inactivepas de bit stable, mise à jour à chaque déplacement de vCPUL'invité fait un appel systèmeune valeur partagée, environ 190 nsMême code noyau sur Proxmox VE, oVirt, OpenStack ou QEMU et libvirt seuls
La clocksource de l’hôte décide de ce que reçoit chaque invité. Sur le TSC, les invités lisent l’horloge en espace utilisateur. Sur le HPET, KVM abandonne son horloge maître, chaque déplacement de vCPU force des mises à jour d’horloge, et chaque lecture d’horloge de l’invité devient un appel système comparé à une valeur que partagent tous les vCPU.

Ce que ça coûte, mesuré

Je l’ai mesuré sur ce portable plutôt que d’en rester à la théorie. La clocksource de l’hôte peut être changée à chaud, ce qui désactive l’horloge maître de KVM exactement comme le repli au démarrage. Un invité jetable a démarré le propre noyau de l’hôte avec 4 vCPU et un minuscule initramfs dont le seul programme chronomètre clock_gettime(). Deux passes dans chaque configuration, l’hôte d’abord, puis l’invité :

Nanosecondes par lecture d'horloge, hôte sur TSC contre hôte sur HPETHôteéchelle 0 à 2 400 nsHôte, 1 thread, hôte sur TSC : 24 ns24 nsHôte, 1 thread, hôte sur HPET : 1 542 ns1 542 ns1 threadHôte, les 8 cœurs, hôte sur TSC : 29 ns29 nsHôte, les 8 cœurs, hôte sur HPET : 1 922 ns1 922 nsles 8 cœursInvité, 4 vCPUéchelle 0 à 300 nsInvité, 4 vCPU, 1 vCPU, hôte sur TSC : 28 ns28 nsInvité, 4 vCPU, 1 vCPU, hôte sur HPET : 187 ns187 ns1 vCPUInvité, 4 vCPU, les 4 vCPU, hôte sur TSC : 31 ns31 nsInvité, 4 vCPU, les 4 vCPU, hôte sur HPET : 244 ns244 nsles 4 vCPUclocksource de l'hôte tscclocksource de l'hôte hpet
Nanosecondes par appel à clock_gettime(), moyenne de deux passes dans chaque configuration. L’hôte devient 61 à 73 fois plus lent sur le HPET ; l’invité environ 7 fois avec un vCPU et 8 avec les quatre. Les panneaux ont des échelles différentes, parce que les barres HPET de l’hôte écraseraient celles de l’invité.

L’invité ne paie pas directement le prix du HPET, parce que kvm-clock lit toujours le TSC en dessous. Ce qu’il perd, c’est la lecture en espace utilisateur et l’indépendance entre vCPU, et ça suffit à le rendre sept fois plus lent. Avec les quatre vCPU qui lisent en même temps, le coût monte encore, et c’est cette valeur globale partagée à l’œuvre. Un invité à 32 ou 64 vCPU fait davantage la queue dessus. Je ne l’ai pas mesuré.

Mettons un chiffre dessus. Un invité qui lit l’horloge un million de fois par seconde sur un vCPU y passe 2,8 % de ce vCPU sur un hôte sain, et 19 % sur celui-ci. Les bases de données, la Java Virtual Machine (JVM) et tout ce qui horodate son travail lisent beaucoup l’horloge.

Cette mesure sous-estime d’ailleurs le problème. Changer de clocksource à la main désactive l’horloge maître mais ne marque pas le TSC instable, donc les réécritures de décalage du tableau ci-dessus n’ont jamais eu lieu. Le repli au démarrage sur ce E595 le marque instable, et un hôte dans cet état paie les deux.

clockbench.c, le benchmark, à lancer sur l'hôte ou comme init d'un invité clockbench.c · 2 kB

Vérifier un hôte, et les limites

J’ai expliqué pourquoi les horloges des invités sont fragiles au départ dans l’article sur ptp_kvm ; ici, c’est le même problème qui arrive par en dessous. Vérifiez l’hôte, pas l’invité :

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

tscsync se lance depuis GRUB ou systemd-boot1, ce qui couvre la plupart des hôtes KVM. Proxmox VE utilise GRUB, ou systemd-boot sur une installation UEFI avec ZFS en racine et Secure Boot désactivé15. Soyez quand même honnête sur les limites avant que ça approche un nœud de production.

LimiteQue faire
AMD Zen et Hygon seulement1les hôtes Intel avec TSC_ADJUST sont déjà corrigés par le noyau
Toutes les machines de sa liste de matériel sont des portables4considérez un serveur comme non testé et signalez-le
Le mode sync refuse le matériel hors de sa liste sauf si vous l’autorisez explicitement1lisez pourquoi avant de passer outre
Un premier démarrage sync peut se bloquermode mesure d’abord, console hors bande ouverte, un nœud à la fois
Une mise à jour du BIOS peut corriger le défautrepassez en mode mesure ensuite et vérifiez1
Une synchro propre peut quand même perdre le TSC à cause du watchdogvérifiez current_clocksource après chaque démarrage ; tsc=nowatchdog si le watchdog est en cause, puis mesurez vous-même

Un défaut de firmware qui coûte un peu de batterie à un portable coûte à un hyperviseur une part de chaque invité qu’il fait tourner, à chaque seconde où il est allumé, et les propriétaires des invités n’ont aucun moyen de le voir. C’est l’argument pour vérifier current_clocksource sur chaque hôte que vous exploitez, quel qu’en soit le fabricant.


  1. tscsync README — ce qu’il fait, le coût d’une lecture d’horloge sur le HPET, pourquoi tsc=reliable n’est pas une correction, le modèle de sécurité, et la règle UNSURE. ↩︎ ↩︎ ↩︎ ↩︎ ↩︎ ↩︎ ↩︎ ↩︎ ↩︎ ↩︎ ↩︎ ↩︎ ↩︎ ↩︎

  2. Linux kernel — arch/x86/kernel/tsc_sync.c — la gestion de TSC_ADJUST, le test de warp et le message « Measured %Ld cycles TSC warp between CPUs, turning off TSC clock. ». ↩︎ ↩︎ ↩︎

  3. tscsync — docs/DESIGN.md — AMD Zen n’a pas d’IA32_TSC_ADJUST, tsc=directsync rejeté en amont, Windows écrit lui-même les TSC au démarrage, et le warp qui suit le temps passé dans le firmware sur le Legion Pro 5. ↩︎ ↩︎ ↩︎

  4. tscsync — docs/HARDWARE.md — les machines testées, les cœurs de l’IdeaPad 5 acceptés à -28 à -42 cycles, et les corrections de BIOS signalées sur les A485, E495 et T14s Gen 1. ↩︎ ↩︎ ↩︎ ↩︎

  5. Lenovo — BIOS Update Utility readme, ThinkPad E495/E595, version 1.30 — les deux modèles dans un seul paquet, l’historique des versions jusqu’à la 1.30 (14 mai 2024), et sa liste de changements. ↩︎ ↩︎

  6. Linux kernel — kernel/time/clocksource.c — le watchdog des clocksources, sa comparaison d’intervalles et le message « Marking clocksource %s unstable due to frequency skew ». ↩︎

  7. Linux kernel — kernel-parameters — tsc=reliable désactive la vérification à l’exécution et les contrôles de stabilité au démarrage ; tsc=nowatchdog ne désactive que le watchdog des clocksources. ↩︎

  8. tscsync issue #1 — Lenovo ThinkPad E595 — les rapports 1.28 et 1.30, l’analyse du watchdog et les changements suggérés. ↩︎

  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 », lu 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 et les outils basés sur libvirt, qui gèrent tous KVM. ↩︎

  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 », et PVCLOCK_TSC_STABLE_BIT positionné seulement avec l’horloge maître. ↩︎ ↩︎ ↩︎ ↩︎ ↩︎

  13. Linux kernel — arch/x86/kernel/kvmclock.c — l’invité n’active l’horloge en espace utilisateur VDSO_CLOCKMODE_PVCLOCK que si PVCLOCK_TSC_STABLE_BIT est positionné. ↩︎

  14. Linux kernel — arch/x86/kernel/pvclock.c — sans PVCLOCK_TSC_STABLE_BIT, chaque lecture est comparée à « last_value, a global accumulator » avec un compare-and-exchange atomique. ↩︎ ↩︎

  15. Proxmox VE — Host Bootloader — GRUB par défaut, systemd-boot pour les installations UEFI avec ZFS en racine et Secure Boot désactivé. ↩︎