Dedicare una CPU ad un singolo processo tramite CPU isolation: come fare?
Normalmente, più task si alternano tra loro per sfruttare le risorse delle CPU presenti in un dato dispositivo. Allo stesso tempo, le CPU svolgono tutte le attività di housekeeping del sistema e perdono in continuazione microsecondi a causa dei jitter. La percezione dell’utente è che tutto questo venga svolto in modalità multitasking, ma in realtà numerosi task si intervallano, sottraendosi tempo e risorse a vicenda. La CPU isolation consente di dedicare una o più CPU a uno o più processi in via esclusiva, quando questi richiedono elevate prestazioni o necessitano di essere eseguiti in un tempo prefissato. La CPU isolation si configura cosi come una valida alternativa alla patch real time del kernel, una soluzione poco standard e con aggiornamenti sporadici.
CPU isolation e CPU affinity in ambiente Linux: come fare
In ambito Linux il kernel costituisce il corpo centrale del sistema operativo: è il kernel che si occupa della gestione dell’hardware e di quella dei servizi, tramite richieste elaborate in sincrono, e della gestione di alcuni processi asincroni, quali timer di sistema e interrupts.
Attraverso lo scheduler, il kernel attribuisce inoltre delle quote di tempo all’esecuzione dei vari task, assegnando le risorse disponibili: in questo modo, i diversi processi si alternano sulla base di intervalli di tempo stabiliti. Nel caso dei sistemi multi core lo scheduler gestisce le quote di tempo da assegnare considerando i vari core disponibili. Questo significa che non per forza un processo viene eseguito integralmente sullo stesso processore: tendenzialmente, il sistema cerca di far sì che sia così, ma non è un imperativo.
Normalmente la gestione da parte dello scheduler dei processi che accedono alle risorse hardware non ha ricadute evidenti sull’esperienza dell’utente. Allo stesso modo, l’utente non percepisce lo svolgimento dei processi di housekeeping. Tutti questi processi portano però a dei piccoli ritardi difficilmente percepibili nell’esecuzioni dei vari task, detti jitter.
I jitter sono latenze con una durata temporale che normalmente impedisce loro di essere percepite dall’utente, ma tale da rallentare l’esecuzione dei vari task da parte dell’hardware.
I jitter si verificano anche quando la macchina sta eseguendo un solo programma: questo perché in ogni caso sono sempre in corso i processi di housekeeping, ma anche perché ogni processore ha un timer interno, detto tick di sistema, che rappresenta un interrupt.
Il tick di sistema tipicamente ha una frequenza variabile tra i 100 e i 1.000 Hz e si verifica quindi in maniera costante tra le 100 e le 1.000 volte al secondo, interrompendo l’esecuzione dei task.
Il tick di sistema causa un’interruzione brevissima ma costante nell’esecuzione dei task, anche nel caso ideale in cui non ci siano più task ad intervallarsi per l’utilizzo delle risorse a disposizione.
Il tick di sistema fa sì che le operazioni svolte dalla CPU vengano quindi continuamente, sebbene brevemente, interrotte.
Nel caso di task con necessità spinte di performance, questo continuo tick di sistema, per quanto brevissimo, può causare delle problematiche. L’obiettivo della CPU isolation è quindi quello di creare le condizioni ideali per cui un servizio o un programma possano essere svolti senza nessun tipo di interruzione. È importante sottolineare però come questa necessità si abbia solo in casi specifici: la CPU isolation non rappresenta la prassi perché nella maggior parte dei casi le interruzioni presenti come standard non vengono percepite dall’utente e non rappresentano un problema.
Il tick di sistema sulla singola CPU ha iniziato ad essere percepito come un problema da risolvere solo in tempi recenti, a partire dal 2007.
In quell’anno il tema iniziò infatti ad essere affrontato non per questioni di performance, come si potrebbe pensare, ma di risparmio energetico. Il tick di sistema fondamentalmente funziona infatti come un orologio interno alla CPU, che serve a gestire le quote di tempo assegnate ai diversi task da svolgere. Nel 2007 si iniziò quindi a valutarne l’utilità nel momento in cui la CPU è in idle e non sta quindi svolgendo nessun compito. Eliminare il tick di sistema almeno in questi momenti voleva dire ridurre il consumo energetico della macchina, riducendo quindi anche problemi collaterali come i costi energetici e i rischi di surriscaldamento. Proprio nel 2007, quindi, all’interno di Linux venne introdotto un parametro di sistema chiamato CONFIG_NO_HZ_IDLE. Se questo parametro è presente ed è abilitato, le CPU interrompono il tick di sistema nel momento in cui entrano in modalità idle.
Nata per ragioni di risparmio energetico, la possibilità di eliminare il tick di sistema venne poi sfruttata anche a scopo computazionale.
La possibilità di eliminare il ritardo dato dal tick di sistema consente infatti anche di avere processi più rapidi, incrementando le performance della macchina. Per fare questo, non è più il tick a tenere il tempo, ma ogni volta che un processo entra o esce dalla CPU viene registrato il tempo di entrata (o di uscita): è su questi timestamp che il sistema tiene il tempo. Questo metodo è efficace ma richiede anche una lettura molto più raffinata rispetto al tick di sistema.
In questa modalità, i timestamp dei processi vengono quindi utilizzati per tenere il tempo, in sostituzione del tick di sistema.
In questo modo, il tick di sistema può essere eliminato da una singola CPU o da più CPU. Se il paramento NOHZ_FULL è abilitato all’interno del kernel è infatti possibile dire al kernel da quale CPU eliminare il tick di sistema.
La scelta può essere fatta aggiungendo al boot il parametro “nohz_full=” e specificando dopo l’uguale l’indice delle CPU sulle quali si vuole agire.
Con il paramento NOHZ_FULL rimane in ogni caso un tick di sistema “ridotto”, con una frequenza di 1 Hz, che si ripete quindi una sola volta al secondo, rispetto ai 100-1.000 Hz standard. In questo modo, le CPU da cui viene (quasi del tutto) eliminato il tick di sistema vengono di fatto isolate dal sistema stesso in quanto su di esse non girerà più nessun processo se non specificato.
Isolare completamente una CPU vuol dire togliervi il tick, ma anche ridirezionare gli interrupt che girano sul sistema e le attività di housekeeping: per questo non è possibile isolare tutte le CPU contemporaneamente. Ogni volta che viene fatta una CPU isolation il carico della manutenzione ordinaria del sistema si ripartisce sulle restanti CPU, fino ad arrivare all’estremo in cui tutto il carico di questo lavoro rimane su una sola CPU. Per queste ragioni, il sistema stesso non consente di effettuare la CPU isolation su tutte le CPU presenti, lasciandone sempre almeno una non isolata di default. Non è comunque detto che una sola CPU possa essere in grado di farsi carico di tutto il lavoro di housekeeping che viene normalmente eseguito. Per questa ragione, la CPU isolation non dovrebbe mai coinvolgere una fetta eccessivamente grande delle CPU presenti nel sistema.
Avendo compiuto la CPU isolation, togliendo anche il tick di sistema e deviando gli interrupt sulle altre CPU, sulla CPU isolata è possibile far girare un singolo processo in modalità esclusiva.
Ricapitolando, perché questo sia possibile è necessario che il kernel sia abilitato alla CPU isolation e che gli sia detto da quale CPU deve essere eliminato il tick di sistema e su quale (o quali) CPU deve essere eseguito l’isolamento. Questa doppia indicazione è necessaria perché è anche possibile isolare una CPU senza toglierle il tick di sistema, risolvendo però solo in parte il problema dei ritardi nell’esecuzione dei task. Infine, è necessario dire al programma su quale CPU girare.
Prendiamo ad esempio il caso in cui si abbiano 4 CPU, indicizzate da 0 a 3: è possibile decidere di eliminare il tick di sistema dalla CPU 3, effettuandovi poi anche la CPU isolation, per farvi girare il programma che richiede performance elevate a livello di tempo in modalità esclusiva. Fatti questi passaggi si deve poi specificare, all’interno dell’applicativo, su quale CPU questo dovrà girare in maniera esclusiva: in questo modo si dà un’affinity all’applicativo stesso. Non ha invece senso dare la stessa affinità a due applicativi diversi, che finirebbero altrimenti col sottrarsi risorse a vicenda.
In uno scenario ideale, se tutte queste operazioni sono svolte correttamente, sulla CPU isolata un programma dovrebbe essere eseguito senza nessun tipo di interruzione.
È importante sottolineare come la CPU isolation così descritta, attuabile in ambito Linux, può essere in ogni caso compiuta solo se il kernel è abilitato e lo consente.
La CPU isolation è l’unica strada che garantisce la certezza che il programma a cui viene data l’affinity girerà esclusivamente sulla CPU scelta. È infatti possibile cercare di dare un’affinità ad un programma anche su una CPU non isolata, ma in questo caso non si può avere la certezza che effettivamente il programma giri solamente sulla CPU scelta. Il sistema infatti proverà a farlo, ma non potrà garantirlo: in generale, tramite la soft affinity, spesso lo scheduler cerca di far girare i programmi sulla medesima CPU in via preferenziale, ma non sempre questo risulta possibile, salvo aver compiuto la CPU isolation.
Il consiglio dell’esperto
Prendiamo come esempio una macchina con Ubuntu con 8 core e mettiamo di voler isolare e rendere tickless la CPU 6.
I passaggi da seguire sono:
– premere “esc” ripetutamente durante il boot per entrare nel menù di grub
– selezionare “Opzioni avanzate per Ubuntu” e premere invio
– selezionare il kernel che si vuole avviare e premere “e”
– spostarsi sulla riga “linux” e aggiungere i parametri “nohz_full=6” e “isolcpus=6”. Ad esempio:
linux /boot/vmlinuz-5,14,0-1045-oem root=UUID=4685… ro quiet splash $vt_handoff nohz_full=6 isolcpus=6
– premere F10 per avviare
Per verificare se tutto è stato fatto correttamente si può aprire un terminale e digitare il comando “cat /proc/cmdline”.
L’output dovrebbe essere del tipo:
BOOT_IMAGE=/boot/vmlinuz-5.14.0-1045-oem root=UUID=4685… ro quiet splash vt.handoff=7 nohz_full=6 isolcpus=6
Per un’ulteriore conferma, digitare il comando “cat /sys/devices/system/cpu/isolated”.
L’output dovrebbe restituire l’indice delle CPU isolate.
Nel caso in cui si vogliano isolare più CPU adiacenti occorre utilizzare il segno meno tra la prima e l’ultima CPU.
Ad esempio “nohz_full=1-4 isolcpus=1-4” porterà ad isolare e rendere tickless le CPU 1, 2, 3 e 4.
Nel caso invece in cui si vogliano isolare più CPU non adiacenti occorre utilizzare il segno virgola tra gli indici delle CPU.
Ad esempio “nohz_full=1,4 isolcpus=1,4” porterà ad isolare le CPU 1 e 4.
La procedura mostrata sopra non è persistente. Nel caso in cui la macchina dovesse essere riavviata, il boot avverrà normalmente.