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:
initprontorunningswapped (swapped pronto/swapped bloccato)sleep/bloccatozombieterminatoIl descrittore di un processo PCB (Process Control Block), è suddiviso in due strutture dati distinte:
Essistono diversi comandi di sistema che permettono la manipolazione dei processi:
fork: crea un processoexit: termina un processowait: sospende un processo in attesa della terminazione dei figliexec..: sostituzione di codice e datiI 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();
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:
PID del figlio, o un valore negativo in caso di fallimento0In 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 :
1509Figlio:0
exit e waitUn processo può terminare in due modi:
Ctrl+C)exit()/**
* Ă 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:
WIFEXITED(status): restituisce true se è terminato volontariamenteWEXITSTATUS(status): restituisce lo stato di terminazioneexec..()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)
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.
signalI 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:
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 |
Vediamo adesso alcune system calls per i segnali
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:
SIG_IGN: per ignorare il segnaleSIG_DFL: per ripristinare lâazione di defaultLa 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).
CosĂŹ definita:
int kill(pid_t pid, int sig)
Invia il segnale sig al processo pid, dove se:
pid > 0: il segnale viene inviato a pidpid == 0: il segnale viene inviato a tutti i processi nello stesso process group del chiamantepid == -1: il segnale viene inviato a tutti i processi nello stesso process group a cui il chiamante può inviare segnalipid < -1: il segnale viene inviato ai processi il cui process group è -pidLa funzione restituisce 0 in caso di successo.
Possiamo consultarla a:
man 2 kill
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
CosĂŹ definita:
unsigned int sleep(unsigned int seconds)
Mette nello stato sleeping il processo chiamante fino a che:
seconds secondiAllâ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
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
pipeI 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
Vi sono diversi comandi per gestire i processi direttamente dal terminale, ne vediamo qualcuno.
killIl 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.
psIl 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:
S - sleepT - bloccatoR - runningZ - zombiePer 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"
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:
PID: è lâID univoco del processoPPID: è lâID univoco del processo padrePGID: è lâID univoco del process group al quale appartiene il processoGli altri 4 identificatori che determinano i permessi del processo si dividono in real e effective:
RUID (Real User ID): è lâID dellâutente che ha mandato in esecuzione il processoRGID (Real Group ID): è lâID del gruppo primario dellâutente che ha mandato in esecuzione il processoEUID (Effective User ID)EGID (Effective Group ID)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,
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();
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.
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
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:
stdin, in caso di lettura ottiene EOFstdout viene rediretto su un file chiamato nohup.out creato nella locazione nel quale si trova il terminale quando esegue il comandodisown %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.
topIl 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:
h: helpH: permette di vedere i singoli threadd: intervallo di aggiornamento (delay)k: invio di un segnalen: numero di processi da visualizzarer: reniceu: utente da specificareq: quit