Introduzione ai sistemi operativi
Ciao, Habr! Vorrei presentarvi una serie di articoli di traduzione su una letteratura che trovo interessante — OSTEP. In questo materiale vengono esaminati in profondità i funzionamenti dei sistemi operativi Unix-like, in particolare — il lavoro con i processi, diversi scheduler, memoria e altri componenti simili che costituiscono un moderno sistema operativo. Potete vedere l'originale di tutti i materiali qui . Si prega di considerare che la traduzione è stata eseguita non professionalmente (abbastanza liberamente), ma spero di aver mantenuto il significato generale.
I lavori di laboratorio su questo argomento possono essere trovati qui:
Altre parti:
E potete anche dare un'occhiata al mio canale su =)
Allerta! c'è un laboratorio per questa lezione! guarda
Process API
Esaminiamo un esempio di creazione di un processo in un sistema UNIX. Questo avviene attraverso due chiamate di sistema fork() e exec().
Chiamata fork()

Esaminiamo un programma che esegue la chiamata fork(). Il risultato della sua esecuzione sarà il seguente.

Innanzitutto, entriamo nella funzione main() ed eseguiamo la stampa di una stringa a schermo. La stringa contiene l'identificatore del processo che in originale si chiama PID o identificatore di processo. Questo identificatore è utilizzato in UNIX per riferirsi al processo. La successiva istruzione sarà l'esecuzione della chiamata fork(). In questo momento viene creata una copia praticamente identica del processo. Per il sistema operativo appare come se ci fossero due copie della stessa programma in esecuzione, che a loro volta usciranno dall'esecuzione della funzione fork(). Il processo figlio appena creato (rispetto al processo padre che lo ha creato) non verrà più eseguito, a partire dalla funzione main(). Va ricordato che il processo figlio non è una copia esatta del processo padre, in particolare ha il proprio spazio di indirizzamento, i propri registri, il proprio puntatore alle istruzioni eseguibili e così via. Pertanto, il valore restituito alla chiamata della funzione fork() sarà diverso. In particolare, il processo padre riceverà come restituzione il valore PID del processo figlio, mentre il figlio riceverà un valore uguale a 0. Con questi codici di ritorno sarà poi possibile separare i processi e far eseguire a ciascuno di loro il proprio compito. Tuttavia, l'esecuzione di questo programma non è rigorosamente definita. Dopo la separazione in due processi, il sistema operativo inizia a monitorarli e pianificare il loro lavoro. In caso di esecuzione su una CPU a un solo core, il lavoro verrà proseguito da uno dei processi, in questo caso — quello padre, e poi il controllo passerà al processo figlio. Al successivo avvio, la situazione potrebbe essere diversa.
Chiamata wait()

Esaminiamo il seguente programma. In questo programma, grazie alla presenza della chiamata wait() il processo padre attenderà sempre il completamento del lavoro del processo figlio. In questo caso riceveremo un output del testo completamente definito a schermo

Chiamata exec()

Esaminiamo la chiamata exec(). Questa chiamata di sistema è utile quando vogliamo avviare un programma completamente diverso. Qui chiameremo execvp() per avviare il programma wc, che è un programma per il conteggio delle parole. Cosa succede quando viene chiamato exec()? A questa chiamata vengono passati come argomenti il nome del file eseguibile e alcuni parametri. Successivamente, viene caricata la codifica e i dati statici da questo file eseguibile e viene sovrascritto il proprio segmento di codice. Altre aree della memoria, come lo stack e il heap, vengono reinizializzate. Dopodiché, il sistema operativo esegue semplicemente il programma, passando un insieme di argomenti. Così, non abbiamo creato un nuovo processo, abbiamo semplicemente trasformato il programma in esecuzione nel nuovo programma in esecuzione. Dopo l'esecuzione della chiamata exec(), nel figlio si ha l'impressione che il programma originale non sia stato avviato affatto.
Questo tipo di complicazione dell'avvio è del tutto normale per la shell Unix, e consente a questa shell di eseguire codice dopo la chiamata fork(), ma prima della chiamata exec(). Un esempio di tale codice potrebbe essere l'adattamento dell'ambiente della shell in base alle esigenze del programma avviato, prima del suo avvio effettivo.
Shell è solo un programma utente. Ti mostra una riga di comando e aspetta che tu ci scriva qualcosa. Nella maggior parte dei casi, se scrivi il nome di un programma, la shell ne troverà la posizione, chiamerà il metodo fork(), e poi per creare un nuovo processo invocherà uno dei tipi di exec() e attenderà la sua esecuzione tramite la chiamata wait(). Quando il processo figlio termina, la shell ritorna dalla chiamata wait() e visualizza di nuovo la riga di comando, aspettando il successivo comando di input.
La separazione di fork() & exec() consente alla shell di fare cose come:
wc file > new_file.
In questo esempio, l'output del programma wc è stato reindirizzato a un file. Il modo in cui la shell raggiunge questo è abbastanza semplice: quando crea il processo figlio prima della chiamata exec(), la shell chiude il flusso di output standard e apre il file new_file, in questo modo, tutta l'output del programma successivamente avviato wc verrà reindirizzato al file invece che allo schermo.
La pipe di Unix è implementata in modo simile, con la differenza che utilizza la chiamata pipe(). In questo caso, il flusso di output del processo sarà connesso a una coda pipe, situata nel kernel, alla quale sarà unito il flusso di input di un altro processo.
Fonte: habr.com
