vuoi
o PayPal
tutte le volte che vuoi
API POSIX
Le api posix sono una serie di librerie che contengono delle system call
standard che ogni sistema operativo deve implementare. Chi implementa i
sistemi operativi deve creare queste system call, ma sono queste system call si
va a mettere un layer di librerie in modo che posso rendere anche un
programma portabile. Quindi creando questo layer di librerie, che appunto sono
le librerie posix, posso utilizzare la funzione di questa libreria sia su un sistema
operativo Windows che su un sistema Mac. Le librerie, quindi, nascondono i
dettagli delle implementazioni delle system call, mentre le funzioni di
interfaccia offerte dalle librerie sono uguali dappertutto e proprio per questo
motivo non utile utilizzare la system call.
CREAZIONE DI UN PROCESSO
La creazione di un processo è una delle operazioni più importante di un sistema
operativo. Ogni sistema operativo possiede un’opportuna system call
(chiamata di sistema) di creazione di un processo. La tipica system call che
crea un processo è la fork(). Ogni processo è creato a partire da un altro
processo, il processo che ne crea un altro è detto processo padre, mentre il
processo creato è detto processo figlio. poiché ogni processo può a sua volta
creare altri processi, nel sistema si crea un albero di processi. Gli eventi che
creano un nuovo processo sono:
Start up del sistema operativo
Esecuzione di un comando o di un programma
Chiamata di un’opportuna system call in un programma
Un processo termina dopo l’esecuzione dell’ultima istruzione del suo codice,
oppure quando viene chiamata una system exit(). Sarà poi il sistema operativo
a provvedere a rimuovere le risorse che erano state allocate al processo
terminato. I dati in output del processo terminato possono essere inviati al
processo padre, se questo è in attesa per la terminazione del processo figlio
(wait()). In alcuni sistemi, un processo può ‘uccidere’ un altro processo
appartenente allo stesso utente con un’opportuna syscall kill o abort. Il sistema
stello può decidere anche di ‘uccidere’ un processo se:
Il processo sta usando troppo risorse
Il suo processo padre è morto. In questo caso si verifica una terminazione
a cascata
SCHEMA STANDARD DI UNA FUNZIONE FORK. FALLIMENTO DI UNA FORK
#include <unistd.h>
#include <stdio.h>
Int main(){
pid_t ret = fork(); // valore restituito di fork()
if (ret < 0){ // entry point (posizione successiva alla fork)
/* se il PID del figlio è minore di 0 è errore. Una fork fallisce quando non è
possibile creare un processo, questo accade quando non c’è più spazio in
memoria per creare nuovi processi */
} else if (ret == 0){
// codice figlio
} else{
// codice padre (PID>0)
}
// codice condiviso da padre e figlio
return 0;
}
Quando si esegue questo codice non c’è modo di sapere se verrà eseguito
prima il padre o il figlio.
Una fork fallisce quando non è possibile creare un processo, e tipicamente
questo accade quando non c’è più spazio in memoria per creare nuovi processi.
Esiste infatti una forma di attaccato chiamata fork bomb.
Schema fork bomb:
#include <unistd.h>
#include <stdio.h>
int main(){
while(1){
if(fork()<0){
printf(“Errore fork”);
}
return 0;
}
Ogni volta che io chiamo una fork, quello che accade è che viene creato un
processo.
PROCESSI ORFANI
Un processo orfano in un sistema operativo si verifica quando un processo
padre termina o viene terminato prima che il suo processo figlio rimane in
esecuzione anche se il genitore non esiste più per gestirlo. Quando un processo
padre termina volontariamente o a causa di un errore, tutti i suoi figli che sono
ancora in esecuzione diventano orfani. Ad esempio, se un’applicazione crea un
processo figlio per eseguire un’attività in background e poi termina
prematuramente, il figlio diventa orfano. Nei sistemi Unix/Linux i processi orfani
vengono automaticamente adottati dal processo con PID 1, ovvero init o il suo
equivalente moderno (come systemd). Questo garantisce che il sistema
continui a monitorare il processo orfano e che le risorse vengano rilasciate
correttamente una volta che il processo termina. Il processo init esegue il
‘reaping’ dei processi orfani chiamando la system call wait(), prevenendo
l’accumulo di processi zombie. Un processo orfano è ancora attivo e
funzionante, anche se il suo genitore non esiste più.
PROCESSI ZOMBIE
Un processo zombie in un sistema operativo è un processo che ha terminato la
sua esecuzione, ma la sua voce nella tabella dei processi rimane ancora
presente. Questo accade perché il processo padre non ha ancora letto lo stato
di uscita del figlio tramite una chiamata di sistema come wait().
Caratteristiche dei processi zombie:
Terminato ma non rimosso: il processo ha completato la sua esecuzione e
ha liberato le risorse (CPU, memoria), ma conserva un record nella
tabella dei processi.
Non attivo: i processi zombie non sono attivi e non consumano risorse di
sistema significative, ma occupano una voce nella tabella dei processi,
che è una risorsa limitata.
Transitorio o permanente: normalmente, i processi zombie sono
transitori, poiché il processo padre legge presto il loro stato. Tuttavia, se il
padre non esegue i wait(), i processi zombie possono accumularsi.
Cause dei processi zombie:
Mancata chiamata a wait(): quando un processo figlio termina, invia un
segnale al processo padre. Se il processo padre non gestisce questo
segnale o non chiama la funzione wait(), il processo figlio rimane
processo zombie.
Bug nel codice del processo padre: se il padre non è stato programmato
per gestire correttamente i figli terminati, si possono generare zombie.
Processo padre in stato di blocco: se il processo padre è bloccato o
occupato in un’altra attività e non recupera lo stato de figli, si creano
processi zombie temporanei.
Come gestisce il sistema operativo i processi zombie:
Rimozione tramite wait(): quando il processo padre chiama la funzione
wait(), il sistema operativo rimuove la voce del processo figlio dalla
tabella dei processi.
Adottati da init: se il processo padre termina prima del figlio, il processo
figlio viene adottato dal processo con PID 1, che esegue
automaticamente il reaping per eliminare i processi zombie.
Rischi dei processi zombie:
Eccessivo accumulo: se il sistema accumula molti processi zombie, la
tabella dei processi piò esaurirsi, impedendo la creazione di nuovi
processi.
Indicatori di problemi nel software: la presenza di zombie persistenti può
indicare errori nella gestione dei processi da parte dell’applicazione.
SYSTEM CALL EXEC IN C
La system call fork ci permette di creare un nuovo processo. Quando vado ad
eseguire un programma attraverso la shell, quello che accade è che la shell
crea un suo clone attraverso l’utilizzo della fork e poi tramite la system call
exec va a cambiare il codice del suo clone, mettendo dentro il codice che noi
vogliamo eseguire; quindi, mentre la system call fork ci permette di creare un
nuovo processo, la system call exec ci permette di sostituire il codice in
memoria che questo processo esegue. Se non fa altro che andare a fare una
sovrascrittura sul codice e sui dati statici del processo, inizializza anche i
registri e lo stack. Ciò che rimane uguale però una volta eseguita la system call
exec, è il PID, il padre PID e anche i file descriptor legati a quel processo.
Possiamo quindi capire che con fork ed exec possiamo andare a creare una
gerarchia dei processi.
Ci sono varie varianti che chiamano questa system call:
1. execl(“/bin/programma”, arg0, arg1, …, NULL);
2. execlp(“programma”, arg0, arg1, …, NULL);
3. execv(“bin/programma”, argv);
4. execvp(“programma”, argv);
Le prime due prendono una lista di argomenti terminata da NULL e gli altri due,
invece, prendono un array di puntatori a stringhe sempre terminato da NULL.
La presenza della p nella funzione che chiamiamo va ad indicare che viene
utilizzata la variabile globale puff che permette di non specificare il percorso
assoluto dell’eseguibile che voglio sfruttare.
LIMITED DIRECT EXECUTION
Il Limited Direct Execution è un modello per creare un processo su una CPU.
Come prima cosa interviene il sistema operativo. Il suo compito è quello di
creare una entry nella process list che si occupa di mantenere le varie
informazioni di tutti i processi del sistema, non solo il processo che è in
esecuzione ma anche tutti i processi che sono nella coda di wait e nella coda di
ready. Il sistema operativo deve allocare memoria necessaria per il processo e
caricare il programma in memoria, dopo di che si occupa di inizializzare i
registri, lo stack e settare il program counter sul main (cioè, andare a mettere
l’indirizzo della prima istruzione da eseguire del processo). A questo punto si
può passare all’esecuzione del programma. Il sistema operativo abbandona la
CPU, e questa manda in esecuzione il processo. Una volta terminato tutto il
codice da eseguire, ci sarà l’istruzione di return, che altro non fa che ripassare
il controllo al sistema operativo, che andrà a liberare la memoria e l’entry nella
process list. In questo modello abbiamo due problemi principali:
1. non abbiamo un intervento da parte del sistema operativo nel caso in cui
un processo semplice vuole interagire con dell’hardware; infatti, deve
esserci il sistema operativo che si mette di mezzo.
2. Non abbiamo il concetto di time sharing, ovvero in questo caso abbiamo
un processo che viene eseguito; quindi, utilizza la CPU continuamente fin
quando non termina la sua esecuzione. Ma se questo processo fosse
molto lungo potrei avere altri processi in attesa che la CPU venga
liberata.
RING O MODALITA’ DI ESECUZIONE
Per creare un processo in modo sicuro ci aiutano i ring. I due più importanti
sono:
1. User mode: viene utilizzata per eseguire il codice utente.
2. Kernel mode: viene utilizzata per eseguire il codice del sistema operativo
Struttura dei ring:
Ring 0: esegue il codice del sistema operativo (kernel).
Ring 1: esegue il codice del device drivers.
Ring 2: esegue il codice del device drivers.
Ring 3: esegue il codice delle applicazioni.
Inoltre, esiste un registro che tiene traccia del ring in cui mi trovo nel momento
dell’esecuzione di un programma. Possiamo quindi immaginare che questo ring
register possa contenere il valore