Sistemi operativi: tre pezzi facili. Parte 3: API di processo (traduzione)

Introduzione ai sistemi operativi

Ehi Habr! Vorrei portare alla vostra attenzione una serie di articoli-traduzioni di una letteratura interessante secondo me - OSTEP. Questo materiale discute in modo abbastanza approfondito il lavoro dei sistemi operativi simili a Unix, vale a dire lavorare con processi, vari programmatori, memoria e altri componenti simili che compongono un sistema operativo moderno. Puoi vedere l'originale di tutti i materiali qui qui. Si prega di notare che la traduzione è stata fatta in modo non professionale (abbastanza liberamente), ma spero di aver mantenuto il significato generale.

Il lavoro di laboratorio su questo argomento può essere trovato qui:

Altre parti:

Puoi anche dare un'occhiata al mio canale su telegramma =)

Allarme! C'è un laboratorio per questa lezione! Aspetto github

API di processo

Consideriamo un esempio di creazione di un processo in un sistema UNIX. Avviene attraverso due chiamate di sistema forchetta() и exec ().

Chiama fork()

Sistemi operativi: tre pezzi facili. Parte 3: API di processo (traduzione)

Consideriamo un programma che effettua una chiamata fork(). Il risultato della sua esecuzione sarà il seguente.

Sistemi operativi: tre pezzi facili. Parte 3: API di processo (traduzione)

Prima di tutto entriamo nella funzione main() e stampiamo la stringa sullo schermo. La riga contiene l'identificatore del processo che nell'originale viene chiamato PID o identificatore di processo. Questo identificatore viene utilizzato in UNIX per fare riferimento a un processo. Il comando successivo chiamerà fork(). A questo punto viene creata una copia quasi esatta del processo. Per il sistema operativo, sembra che ci siano 2 copie dello stesso programma in esecuzione sul sistema, che a loro volta usciranno dalla funzione fork(). Il processo figlio appena creato (rispetto al processo genitore che lo ha creato) non verrà più eseguito, a partire dalla funzione main(). Va ricordato che un processo figlio non è una copia esatta del processo genitore; in particolare, ha il proprio spazio di indirizzi, i propri registri, il proprio puntatore alle istruzioni eseguibili e simili. Pertanto, il valore restituito al chiamante della funzione fork() sarà diverso. In particolare, il processo genitore riceverà in ritorno il valore PID del processo figlio, e il figlio riceverà un valore pari a 0. Utilizzando questi codici di ritorno, sarà quindi possibile separare i processi e forzare ciascuno di essi a svolgere il proprio lavoro . Tuttavia, l'esecuzione di questo programma non è strettamente definita. Dopo la divisione in 2 processi, il sistema operativo inizia a monitorarli e a pianificarne il lavoro. Se eseguito su un processore single-core, uno dei processi, in questo caso il genitore, continuerà a funzionare, e quindi il processo figlio riceverà il controllo. Al riavvio la situazione potrebbe essere diversa.

Chiama in attesa()

Sistemi operativi: tre pezzi facili. Parte 3: API di processo (traduzione)

Consideriamo il seguente programma. In questo programma, a causa della presenza di una chiamata aspettare() Il processo genitore attenderà sempre il completamento del processo figlio. In questo caso, otterremo sullo schermo un output di testo rigorosamente definito

Sistemi operativi: tre pezzi facili. Parte 3: API di processo (traduzione)

chiamata exec()

Sistemi operativi: tre pezzi facili. Parte 3: API di processo (traduzione)

Considera la sfida exec (). Questa chiamata di sistema è utile quando vogliamo eseguire un programma completamente diverso. Qui chiameremo execvp() per eseguire il programma wc che è un programma di conteggio parole. Cosa succede quando viene chiamato exec()? A questa chiamata viene passato il nome del file eseguibile e alcuni parametri come argomenti. Dopodiché vengono caricati il ​​codice e i dati statici di questo file eseguibile e il relativo segmento con il codice viene sovrascritto. Le restanti aree di memoria, come lo stack e l'heap, vengono reinizializzate. Dopodiché il sistema operativo esegue semplicemente il programma, passandogli una serie di argomenti. Quindi non abbiamo creato un nuovo processo, abbiamo semplicemente trasformato il programma attualmente in esecuzione in un altro programma in esecuzione. Dopo aver eseguito la chiamata exec() nel discendente, sembra che il programma originale non sia stato eseguito affatto.

Questa complicazione all'avvio è del tutto normale per una shell Unix e consente alla shell di eseguire il codice dopo la chiamata forchetta(), ma prima della chiamata exec (). Un esempio di tale codice potrebbe essere l'adattamento dell'ambiente shell alle esigenze del programma da avviare, prima di avviarlo.

Conchiglia - solo un programma utente. Ti mostra la riga dell'invito e aspetta che tu ci scriva qualcosa. Nella maggior parte dei casi, se scrivi lì il nome di un programma, la shell troverà la sua posizione, chiamerà il metodo fork(), quindi chiamerà qualche tipo di exec() per creare un nuovo processo e attenderne il completamento utilizzando un aspetta() chiamata. Quando il processo figlio esce, la shell ritornerà dalla chiamata wait() e stamperà nuovamente il prompt e attenderà l'immissione del comando successivo.

La suddivisione fork() e exec() consente alla shell di eseguire le seguenti operazioni, ad esempio:
wc file > nuovo_file.

In questo esempio, l'output del programma wc viene reindirizzato su un file. Il modo in cui la shell raggiunge questo obiettivo è abbastanza semplice: creando un processo figlio prima di chiamarlo exec (), la shell chiude l'output standard e apre il file nuovo file, quindi, tutti gli output dell'ulteriore programma in esecuzione wc verrà reindirizzato a un file anziché a una schermata.

Pipa Unix sono implementati in modo simile, con la differenza che utilizzano una chiamata pipe(). In questo caso, il flusso di output del processo sarà connesso a una coda di pipe situata nel kernel, alla quale sarà collegato il flusso di input di un altro processo.

Fonte: habr.com

Aggiungi un commento