Cosa accade esattamente in Linux quando lanciamo un comando?

Relazione fra Linux e POSIX

Credo che tutti coloro con una carriera nel mondo IT abbiano utilizzato, almeno una volta, il sistema operativo Linux.  Linux è uno di quei sistemi operativi che sono gergalmente definiti “UNIX-like”. Questi sistemi operativi sono definiti tali in quanto rispettano uno standard denominato “POSIX”, a cui è linkata la pagina di Wikipedia per approfondimento.

Per farla breve, però, tutti i sistemi operativi che rispettano lo standard POSIX avranno interfacce comuni per eseguire le medesime azioni. Questo si traduce, da parte dell’utente, in un’ereditata e implicita familiarità con tutti i sistemi operativi POSIX, anche se differenti.

Per rendere tutto questo concreto con un paio di esempi, la compliance con POSIX detta che il comando mkdir dovrà creare directory e deve accettare alcune opzioni, ciascuna con un comportamento ben definito; oppure che il comando ls dovrà listare file e cartelle, anch’esso con un comportamento e opzioni ben definiti.

L’implementazione di questi due comandi potrebbe differire, anche radicalmente, e solitamente questo è il caso, fra i vari sistemi operativi ma il comportamento dei due comandi sarà il medesimo.
Questi due esempi non scalfiscono neanche la superficie delle lunghe e dettagliate specifiche POSIX, ma dovrebbe darci il giusto intuito per comprendere che significa per un sistema operativo “essere POSIX-compliant”.

Solitamente, ma non è garantito, è possibile verificare che un dato comando aderisce allo standard POSIX consultando la sua man page.

Su un sistema macOS Ventura 13.4.1, l’output del comando man mkdir contiene la seguente dicitura.

linux
				
					STANDARDS
The mkdir utility is expected to be IEEE Std 1003.2 (“POSIX.2”) compatible.
				
			

Come si comporta un processo POSIX

Lo standard POSIX definisce anche ciò che accade al momento della creazione (“spawn”) di un processo. In particolare, cercheremo di capire cosa accade esattamente all’interno del nostro terminale quando eseguiamo un qualsiasi comando.

È opportuno cominciare con un’importante nozione: un processo rimane in vita purché esso stia facendo qualcosa. Questo qualcosa può essere sia un qualcosa di attivo, come l’eseguire una qualche computazione, che qualcosa di passivo, come aspettare un qualche input.

Partiamo dall’inizio. Per poter impartire comandi al nostro sistema operativo in maniera interattiva, avremo bisogno di un programma molto particolare il cui unico scopo è quello di interpretare ciò che scriviamo e interagire con il nostro sistema operativo. Tali programmi sono definiti “shell”. Infatti, subito dopo aver eseguito login con un utente presente nel sistema operativo, sarà avviata la login shell definita per quell’utente. Comunemente questa è bash ma potrebbe essere una qualsivoglia shell presente nel sistema.

La routine fork/exec

Ricordando quanto detto prima, ovvero che un processo rimane in vita purché esso stia facendo qualcosa, sarebbe opportuno chiedersi che succede esattamente alla nostra shell durante l’esecuzione di un comando.

La risposta è molto semplice. Ciascun comando eseguito all’interno della nostra shell sarà processo figlio (dall’inglese “child process”) della shell stessa. Questo è reso evidente dall’esecuzione del seguente comando, su un sistema Ubuntu 22.04.

				
					$ ps --forest

    PID TTY          TIME CMD
  46712 pts/0    00:00:00 bash
  46820 pts/0    00:00:00  \_ ps

				
			

Possiamo osservare, infatti, come sia presente il processo della mia shell, che nel mio caso è bash, e il processo del comando ps, marcato da ps stesso come figlio del processo di bash.

Non appena ps avrà terminato la propria esecuzione, il suo processo terminerà e si ritornerà al suo parent process: il process di bash iniziale. Non solo ritorneremo al processo di bash, ma ritorneremo all’esatto processo di bash che ha inizialmente creato il processo di ps, motivo per cui rivedremo la nostra shell così come l’abbiamo lasciata prima di eseguire il comando.

Per ottenere questo risultato, il processo della shell esegue una routine di azioni molto comune in Unix chiamata fork/exec, dalle ominime funzioni del linguaggio C: fork() ed exec*().

Con la prima, un processo creerà un processo figlio che è l’esatta copia di se stesso. Riprendendo l’output di ps di cui sopra, il risultato che otterremmo dopo che bash avrà chiamato solo fork() è il seguente.

				
					    PID TTY          TIME CMD
  46712 pts/0    00:00:00 bash
  46820 pts/0    00:00:00  \_ bash


				
			

In seguito, il processo di bash figlio, quello che nel nostro caso ha un PID (Process ID) di 46820, chiamerà una delle funzioni della famiglia exec*(), la quale gli permetterà di sostituire il proprio processo con quello di un comando specificato come argomenti della

funzione stessa. Ecco un esempio di invocazione della funzione execv():

				
					execv(“/usr/bin/ps”, [“--forest”, NULL]);
				
			

Sempre comparando l’output di ps di cui sopra, dopo la chiamata ad exec da parte del processo figlio di bash, questo sarà il risultato.

				
					    PID TTY          TIME CMD
  46712 pts/0    00:00:00 bash
  46820 pts/0    00:00:00  \_ ps

				
			

Questo è esattamente ciò che abbiamo osservato inizialmente in maniera empirica, subito dopo aver eseguito il comando ps.

È interessante notare come la nostra shell permetta anche di eseguire exec in maniera diretta, passando un comando come argomento.

				
					$ exec ps –forest
				
			

Con l’esecuzione del comando in alto, dovremmo osservare come il processo della nostra shell muore subito dopo la terminazione del processo di ps, la cui esecuzione sarà di difficile osservazione in quanto istantanea (in condizioni normali).

Il motivo per cui questo avviene è che stiamo dicendo alla nostra shell di eseguire l’azione di exec con il comando di ps direttamente su sé stessa, saltando la fase di fork. Il processo di bash con cui stiamo interagendo sarà sostituito direttamente da quello di ps, facendo sì che non ci sia più nessun parent process a cui ritornare non appena il processo di ps termina, come avvrebbe in una normale routine fork/exec.

Input/Output

Ci spostiamo ora nei meandri dell’input/output, cercando di capire come i processi interagiscono con gli stream di input e output.

Partiamo con alcune nozioni fondamentali. Ogni qualvolta un processo chiede al sistema operativo di aprire un file, tale processo riceve dal sistema operativo un numero che rappresenterà un “file descriptor” (spesso abbreviato come “fd”). Ogni qualvolta un processo necessita di eseguire un’operazione di read o write, dovrà specificare su quale file descriptor eseguire tale operazione. È utile inoltre notare come più processi possono aprire contemporaneamente lo stesso file, ottenendo diversi file descriptor che puntano allo stesso file.

In Unix, non appena un processo inizia il suo ciclo di vita, esso detiene almeno 3 file aperti con i seguenti file descriptor:

  • 0 (stdin)
  • 1 (stdout)
  • 2 (stderr)

In Linux, possiamo osservare quanto descritto sopra utilizzando la cartella /proc, che raccoglie informazioni sui processi raggruppati per PID.

				
					$ ls -l /proc/$$/fd

lrwx------ 1 user user 64 Jul  4 14:08 0 -> /dev/pts/0
lrwx------ 1 user user 64 Jul  4 14:08 1 -> /dev/pts/0
lrwx------ 1 user user 64 Jul  4 14:08 2 -> /dev/pts/0

				
			

$$ è una variabile speciale che la nostra shell è in grado di espandere col PID della shell stessa.

Possiamo osservare come all’interno della cartella “fd” per il PID della nostra shell ci siano 3 file, il cui nome è “0”, “1” e “2” rispettivamente. Possiamo inoltre notare come ciascuno di questi file sia effettivamente un symlink, che punta ad un reale file. È di facile intuizione, a questo punto, il fatto che il nome del symlink rappresenti il file descriptor e il target del symlink rappresenti il reale file a cui ciascun file descriptor punta.
Non entrerò nei dettagli di ciò che /dev/pts/0 rappresenta, in quanto significherebbe presentare un’ulteriore dissezione sui terminali, pseudo-terminali, tty, pseudo-tty ecc… nel mondo di Unix (magari in un altro articolo). È sufficiente sapere, per ora, che quel /dev/pts/0 collega i vari file descriptor al nostro terminale. Infatti, la nostra shell, di default, riceve input dal nostro terminale (fd 0 – stdin), stampa output sul nostro terminale (fd 1 – stdout) e stampa errori sul nostro terminale (fd 2 – stderr).

Proviamo adesso a redirezionare tutti gli stream per un altro processo di bash e osserviamo il contenuto della cartella fd.

				
					$ bash -c 'ls -l /proc/$$/fd' </dev/zero >mystdout.txt 2>mystderr.txt
				
			

Il comando in alto creerà un nuovo processo di bash, con stdin, stdout e stderr rispettivamente rediretti a /dev/zero, mystdout.txt e mystderr.txt. Questa istanza di bash eseguirà il comando ls -l /proc/$$/fd.

È importante eseguire questo comando con virgolette singole e non doppie, per far sì che $$ venga espanso nel contesto della shell figlia e non in quella padre.

Osserviamo quindi il nostro mystdout.txt.

				
					$ cat mystdout.txt

lr-x------ 1 user user 64 Jul  4 15:01 0 -> /dev/zero
l-wx------ 1 user user 64 Jul  4 15:01 1 -> /home/user/mystdout.txt
l-wx------ 1 user user 64 Jul  4 15:01 2 -> /home/user/mystderr.txt

				
			

Possiamo osservare come, per l’istanza bash figlia, file descriptor 0, 1 e 2 (rispettivamente stdin, stdout e stderr) non puntino più a /dev/pts/0, ma puntano invece ai file da noi specificati.

Conclusione

C’è ancora tanto altro intorno al mondo dei processi in Unix. Non vedo l’ora di parlare di variabili d’ambiente, pipes, OS threads vs green threads e tanto altro! Tuttavia il contenuto di questo articolo dovrebbe fungere da solida base per poter comprendere gli aspetti più tecnici dell’utilizzo dei processi nella quotidianità!

Condividi l’articolo!!


Scopri i nostri corsi!

Formazione Cloud-Native in presenza o da remoto
Picture of Gianluca Recchia

Gianluca Recchia

DCA | CKA | AWS SAP | AAI Instructor
Trainer e consulente con forte background in programmazione e scripting. Ho formato numerosi studenti su argomenti riguardo il mondo Devops, dell’automation, delle cloud infrastructure e della system administration. Advocate per tecnologie open-source e open-standards, spendo la mia vita in una continua quest volta al rendere il mio workflow più efficiente e produttivo possibile.

Find me on Linkedin