1. Indice

2. Processi in UNIX

UNIX è una famiglia di sistemi operativi multiprogrammati basati su processi.

Il processo UNIX mantiene spazi di indirizzamento separati:

La politica di assegnamento della CPU ai processi adottata da UNIX è basata sulla divisione di tempo, facendo attraversare loro vari stati:

Il descrittore di un processo PCB (Process Control Block), è suddiviso in due strutture dati distinte:

3. System Call per i Processi

Essistono diversi comandi di sistema che permettono la manipolazione dei processi:

I processi sono identificati da quello che si chiama PID (Process ID). Esistono alcune funzioni di sistema che permettono di ottenere informazioni su di esso:

// Restituisce il PID del processo
pid_t getpid();

// Restituisce il PID del processo padre
pid_t getppid();

3.1. Fork

Ogni processo è in grado di creare dinamicamente processi tramite la chiamata di sistema fork.

Il processo creato, detto figlio, ha uno spazio di dati separato, ma condivide con il processo che lo ha creato, detto padre, il codice. I processi figli possono a loro volta avere nuovi processi figli.

La funzione è cosÏ definita:

/**
* La funzione non richiede parametri
* Restituisce un risultato intero DIVERSO a padre e figlio
*/
pid_t fork(void)

Poiché il processo figlio condivide il codice con il padre, ne eredita una copia delle aree dati globali (stack, heap, User Structure, …). Ciò significa che alla creazione il figlio ha il proprio %RIP che punta alla al’istruzione successiva alla CALL fork, e, di conseguenza, ha nel registro %RAX il valore di ritorno della funzione.

La funzione è però progettata affinchÊ restituisca un risultato intero diversso a padre e figlio:

In questo modo, nel codice del padre, è possibile inserire dopo la chiamata alla fork una if-statement che permette di discriminare il comportamento del padre e del figlio:

pid_t pid;
pid = fork();

printf("%d\n", pid);

Padre : 1509 Figlio: 0

3.2. Terminazione Processi - exit e wait

Un processo può terminare in due modi:

/**
* È una chiamata senza ritorno che permette di terminare
* volontariamente un processo.
* @param status permette di comunicare al padre lo stato di terminazione
*/
void exit(int status)

Il processo padre può ottenere lo stato di terminazione del figlio mediante la system call wait()

/**
* Il padre si mette in attesa della terminazione del figlio
* @param status puntatore a intero che contiene lo stato di terminazione del figlio
* @return il PID del figlio che è terminato, valore negativo se non ha processi figli
*/
pid_t wait(int* status)

La sospensione del padre accade solo se tutti i figli sono ancora in esecuzione. Nel caso in cui almeno un figlio è terminato, la funzione ritorna immediatamente le informazioni di terminazione. Questo è possibile grazie all’esistenza dello stato zombie. I processi figli che terminano infatti entrano nello stato di zombie, proprio per permettere al padre di ottenere le informazioni sulla terminazione di quest’ultimo.

La variabile status contiene diverse informazioni su come il figlio e terminato, oltre allo stato di terminazione eventualmente fornito dal figlio stesso. Se il byte meno significativo di *status fosse 0, allora la terminazione è stata volontaria e il byte piÚ significativo contiene lo stato di terminazione.

Per gestire status in modo astratto, la libreria <sys/wait.h> fornisce alcune MACRO:

3.3. Sostituzione di codice - exec..()

Un processo può sostituire il programma che sta eseguendo (codice e dati) eseguendo una syscall della “famiglia” exec() (excecl(), execle(), execclp(), execv(), execve(), execvp()).

In particolare vediamo il comando execl:

/**
* @param path rappresenta i percorso assoluto del comando che si vuole eseguire (es. "bin/ls")
* @param arg0 rappresenta il nome del programma da eseguire (es. "ls")
* @param arg1_argN rappresentano gli eventuali argomeni del comando
* @return solo in caso di fallimento
*/
int execl(char* path, char* arg0, ..., char* argN, NULL)

4. Interazione tra Processi

I processi UNIX aderiscono al modello ad ambiente locale, dove non c’è condivisione di variabili e ogni processo ha uno spazio di indirizzamento provato.

L’unica forma di interazione tra processi è la cooperazione, che può avvenire tramite:

Le interazioni avvengono su astrazioni realizzate dal kernel interagendo mediante system calls.

4.1. 4.1 Sincronizzazione - signal

I segnali sono il meccanismo messo a disposizione dai sistemi UNIX/Linux per la sincronizzazione dei processi. Permettono la notifica di eventi asincroni da parte di un processo ad altri, e possono essere utilizzati anche dal SO per notificare il verificarsi di eccezioni a un processo utente.

I segnali sono rappresentati dalle interrupt software.

La ricezione di un segnale ha tre possibili effetti sul processo:

  1. Viene eseguita una funzione handler definita dal programmatore
  2. Viene eseguita un azione prefefinita dal sistema operativo attraverso un default handler
  3. Il segnale viene ignorato

Nei primi due casi il processo si comporta in modo asincrono rispetto al segnale, interrompendo il processo in esecuzione per eseguire l’handler. Se, alla terminazione dell’handler, il processo non era precedentemente terminato, allora questo riprende dall’istruzione successiva all’ultima eseguita prima dell’interruzione.

Versioni diverse di UNIX possono avere segnali diversi. La lista di questi segnali si trova nel file di sistema signal.h. La pagina di manuale sui segnali si trova in:

man 7 signal

Ciascun segnale è identificato da un intero e un nome simbolico:

Nome segnale Numero Segnale Descrizione
SIGHUP 1 Hang up, indica che il terminale è stato chiuso
SIGINT 2 Interruzione del processo (Ctrl+C)
SIGQUIT 3 Interruzione del processo e core dump (Ctrl+\)
SIGKILL 9 Interruzione immediata. Non è ignorabile e il processo che lo riceve non può eseguire opzioni di chiusura
SIGTERM 15 Terminazione del programma
SIGUSR1 10 Definito dall’utente. Di default termina il processo
SIGUSR2 12 Definito dall’utente. Di default termina il processo
SIGSEGV 11 Errore di segmentazione
SIGALRM 14 Indica terminazione del timer
SIGCHLD 17 Processo figlio terminato, fermato o risvegliato. Di default è ignorato
SIGSTOP 19 Ferma temporaneamente l’esecuzione del processo. Non è ignorabile
SIGTSTP 20 Sospende l’esecuzione del processo (Ctrl+Z)
SIGCONT 18 Il processo può continuare qual’ora fosse stato fermato da SIGSTOP o SIGTSTP

4.1.1. System calls per i segnali

Vediamo adesso alcune system calls per i segnali

4.1.1.1. Signal

CosĂŹ definita:

typedef void (*sighander_t)(int);

sighandler_t signal(int sig, sighandler_t handler)

Permette di definire la funzione handler che dovrĂ  gestire il segnale sig. La funzione handler dovrĂ  prevedere un parametro intero, che al momento della ricezione del segnale ne conterrĂ  il codice.

L’handler può valere delle macro:

La funzione restituisce un puntatore al precedente handler del segnale o SIG_ERR in caso di errore.

Possiamo consultarla a:

man 2 signal

In caso di fork() il figlio eredita dal padre le informazioni relative alla gestione dei segnali. È importante sottolineare che eventuali signal eseguite dal figlio non hanno effetto sul padre.

Le syscall exec.. non mantengono le associazioni segnale-handler (tranne per i segnali ignorati che continuano ad esserlo).

4.1.1.2. Kill

CosĂŹ definita:

int kill(pid_t pid, int sig)

Invia il segnale sig al processo pid, dove se:

La funzione restituisce 0 in caso di successo.

Possiamo consultarla a:

man 2 kill

4.1.1.3. Pause

CosĂŹ definita:

int pause(void)

Mette nello stato sleeping il processo fino alla ricezione di un segnale.

Se il gestore non terminasse l’esecuzione del processo, restituisce -1.

Possiamo consultarla a:

man pause

4.1.1.4. Sleep

CosĂŹ definita:

unsigned int sleep(unsigned int seconds)

Mette nello stato sleeping il processo chiamante fino a che:

All’accadere di uno dei due casi il processo viene risvegliato.

Ritorna 0 se è passato il tempo previsto, altrimenti il tempo rimanente allo scadere del timer nell’istante di arrivo del segnale.

Possiamo consultarla a:

man 3 sleep

4.1.1.5. Alarm

CosĂŹ definita:

unsigned int alarm(unsigned int seconds)

Provoca la ricezione di un segnale SIGALRM dopo seconds secondi.

Di default SIGALRM termina il processo, cancellando un eventuale allarme invocato precedentemente.

Se seconds == 0 viene eliminato un eventuale allarme precedente.

La funzione ritorna 0 se non c’era un allarme programmato, altrimenti il numero di secondi mancanti all’ultimo allarme programmato

Possiamo consultarla a:

man alarm

4.2. Comunicazione - pipe

I processi possono comunicare anche sfruttando il meccanismo delle pipe.

Le pipe implementano un sistema di comunicazione indiretta, senza naming esplicito.

Realizza il concetto di mailbox nella quale si possono accodare messaaggi in modo FIFO

La pipe è un canale monodirezionale con due estremi:

L’astrazione della pipe è realizzata in modo omogeneo rispetto alla gestione dei file. A ciascun estremo è associato un file descriptor, risolvendo i problemi di sincronizzazione con primitive read e write.

I figli ereditano gli stessi file descriptor del padre, e li possono utilizzare per comunicare con i fratelli e con il padre. È possibile implementare la comunicazone di processi che non si trovano nella stessa gerarchia attraverso i socket.

Possiamo consultare i pipe a:

man pipe

5. Gestione dei processi da terminale

Vi sono diversi comandi per gestire i processi direttamente dal terminale, ne vediamo qualcuno.

5.1. Invio segnali - kill

Il comando kill è un comando che permette l’invio di segnali a processi da terminale:

kill [options] pid [pid2...]

Il segnale di default inviato è SIGTERM, ma è possibile cambiarlo tramite opzione:

kill -SEGNALE pid		# invia il sengale SEGNALE

Per visualizzare la lista dei segnali disponibili è possibile eseguire:

kill -l

Un utente normale può inviare segnali sono ai processi di cui è proprietario. Un utente root invece può inviare segnali a tutti i processi.

5.2. Visualizzazione Processi - ps

Il comando ps permette di visualizzare i processi in esecuzione al momento della chiamata.

ps [options...]

Alcune opzioni principali disponibili in Linux sono nella seguente tabella:

Opzione Descrizione
-u utente Visualizza i processi dell’utente specificato
u Formato output utile all’analisi dell’utilizzo delle risorse
a Processi di tutti gli utenti
x Visualizza anche i processi che non sono stati generati da terminali
o Mostra solo i campi specificati piĂš avanti
-O Mostra i campi specificati oltre ad altri di default

Gli stati principali dei processi sono:

Per vedere tutti i possibili stati:

man ps

ps aux 						# visualizza tutti i possibili processi
ps aux | grep "filtro"		# tra tutti i possibili processi, mostra solo le righe che contengono "filtro"

6. Gerarchia dei processi

I sistemi UNIX/Linux prevedono un init system, ovvero un processo mandato in esecuzione dal kernel durante il boot. Questo è il primo processo ad andare in esecuzione (ha infatti PID = 1), ed è il padre di tutti gli altri processi.

In alcuni sistemi Linux come Debian/Ubuntu il gestore dei processi utilizzato è systemd.

Per visualizzare l’albero dei processi si utilizza il comando:

pstree
systemd─┬─ModemManager───3*[{ModemManager}]
		├─NetworkManager───3*[{NetworkManager}]
		├─accounts-daemon───3*[{accounts-daemon}]
		├─at-spi-bus-laun─┬─dbus-daemon
		│                 └─4*[{at-spi-bus-laun}]
		├─at-spi2-registr───3*[{at-spi2-registr}]
		├─avahi-daemon───avahi-daemon
		├─colord───3*[{colord}]
		├─cron
		├─cups-browsed───3*[{cups-browsed}]
		├─cupsd
		├─dbus-daemon
		├─gdm3─┬─gdm-session-wor─┬─gdm-wayland-ses─┬─dbus-run-sessio─┬─dbus-daemon
		│      │                 │                 │                 └─gnome-session-b─┬─gnome-shell─┬─Xwayland
	   ...    ...     			...			   	  ...								  ...			...

Un proccesso ha 7 identificatori.

Tre identificatori sono relativi all’identificazione:

Gli altri 4 identificatori che determinano i permessi del processo si dividono in real e effective:

EUID e EGID possono differire da RUID e RGID solo se il comando eseguito ha il suo bit SUID o SGID attivo. Sono spesso utilizzati per definire i privilegi di accesso alle risorse e di invocazione di system call nel processo.

Infatti un processo utente (non root) può inviare segnali ad un altro processo solo se il suo EUID/RUID coincide con il RUID del processo destinatario,

6.1. Funzioni per gli Identificatori

Di seguito possiamo trovare alcune funzioni get per recuperare gli identificatori:

/* Recupero il mio PID */
pid_t getpid();

/* Recupero il PPID */
pid_t getppid();

/* Recupero il PGID*/
pid_t getpgrp();

/* Recupero il RUID */
uid_t getuid();

/* Recupero il RGID */
uid_t getgid();

/* Recupero il EUID */
uid_t geteuid();

/* Recupero il EGID */
uid_t getegid();

6.2. Gruppi di processi

I processi sono organizzati in gruppi. Quando un nuovo processo viene mandato in esecuzione da terminale gli viene associato un nuovo process group. Gli eventuali figli di questo processo, compresi quelli generati dalla syscall exec, erediteranno il process group.

I gruppi permettono di mandare segnali ad una gerarchia di processi, e sono alla base del job-control offerto dalla shell.

7. PrioritĂ  dei processi

Lo scheduler Linux assegna la CPU ai processi tenendo conto di un livello di prioritĂ  assegnato a ciascun processo. La prioritĂ  dipende principalmente dalla classe di scheduling del processo, e si divide tra real-time e normale.

La priorità dei processi normali può essere in parte controllata mediante il concetto di niceness e la relativa system call nice. Ad ogni processo infatti è associato un valore di niceness nell’intervallo [-20, 19], dove un valore più alto porta ad avere minore priorità di esecuzione.

In questo modo un processo eseguito in background, e quindi non interattivo, può lasciare piÚ tempo di elaborazione agli altri processi.

Tramite il comando nice:

# Manda in esecuzione in background il processo del comando
#  bzip file &
# dandogli un valore di niceness specificato
nice -n valore_nice bzip2 file &

# Modifico la niceness di un processo giĂ  in esecuzione
renice valore_nice PID

8. Gestione dei processi da terminale

Con job-control si intende la possibilitĂ  di sospendere e riattivare gruppi di processi, detti jobs, offerta dalla shell mediante opportuni comandi.

Abbiamo giĂ  detto che la shell associa un JOB_ID distinto ad ogni comando eseguito (alle pipeline di comandi viene associato un solo job). Questi job sono salvati in una tabella specifica, visualizzabile tramite il comando jobs.

Un job in esecuzione in foreground ha il controllo dello standard input/output/error, di fatto è come se “prendesse il controllo del terminale” restituendolo alla shell solo alla sua terminazione.

Per eseguire job in background si utilizza il carattere & alla fine del comando:

comando &

In questo modo il processo non ha più accesso allo standard input, ma permettiamo all’utente di utilizzare la shell mentre il job viene eseguito in parallelo.

Nel caso avessimo giĂ  avviato un processo in foreground, per fermarlo possiamo inviare il segnale SIGTSTP attraverso la combinazione Ctrl+Z.

Per intervenire sui job che sono stati fermati in questo modo si utilizza jobs per ottenerle l’identificatore JOB_ID, e successivamente:

# Per farlo ripartire in foreground
fg JOB_ID

# Per farlo ripartire in background
bg JOB_ID

Anche sui job è possibile utilizzare il comando kill:

# Invio il segnale SIGTERM al job specificato
kill %JOB_ID

# Invio il segnale SIG al job specificato
kill -n SIG %JOB_ID

PoichĂŠ i job ereditano il process group del terminale che li inizializza, se questo viene chiuso, ricevono il segnale SIGHUP e, di default, anche loro vengonno terminati.

Per fare in modo che il segnale SIGHUP non porti alla terminazione di un job è possibile utilizzare due strumenti:

nohup comando

In questo modo il job eseguito è immune a SIGHUP. Tuttavia comporta due conseguenze per il job:

disown %JOB_ID

Può essere utilizzato per rendere immune a SUGHUP un job già in esecuzione.

Il job viene rimosso dalla tabella dei job. Di conseguenza la shell non invierĂ  piĂš il segnale SIGHUP quando viene chiusa.

Il comando non influsice direttamente sulle modifiche relative alla lettura sullo stdin e/o scrittura sullo stdout, ed è opportuno modificarle per evitare errori durante l’esecuzione.

8.1. Monitor di sistema - top

Il comando top permette di visualizzare i processi e di effettuare operazioni su di essi in modo interattivo. Vengono visualizzate anche informazioni complessive sul sistema (carico CPU, utilizzo della memoria, …)

I processi sono ordinati in ordine di utilizzo decrescente della CPU. Dall’interfaccia che si genera dopo aver chiamato il comando è possibile inviare segnali ai processi e cambiarne il valore di niceness.

Di seguito possiamo vedere una serie di comandi interattivi utilizzabili dall’interfaccia: