Introduzione ai sistemi operativi
Ciao, Habr! Vorrei presentarvi una serie di articoli tradotti su una libreria che trovo interessante: OSTEP. In questo materiale si analizza in profondità il funzionamento dei sistemi operativi simili a Unix, in particolare la gestione dei processi, vari pianificatori, la memoria e altri componenti simili che costituiscono un moderno sistema operativo. Potete visualizzare l'originale di tutti i materiali qui . Si prega di notare che la traduzione non è stata eseguita da un professionista (è abbastanza libera), ma spero di aver mantenuto il significato generale.
Le esercitazioni su questo argomento possono essere trovate qui:
- originale:
- originale:
- la mia personale adattazione:
Altre parti:
E potete anche dare un'occhiata al mio canale su =)
Consideriamo l'astrazione più fondamentale che il sistema operativo fornisce agli utenti: il processo. La definizione di processo è piuttosto semplice: è un programma in esecuzione. Il programma di per sé è una cosa senza vita, che risiede su disco: un insieme di istruzioni e forse alcuni dati statici, in attesa di essere avviati. È proprio il sistema operativo che prende questi byte e li avvia, trasformando il programma in qualcosa di utile.
Spesso gli utenti desiderano eseguire più di un programma contemporaneamente; ad esempio, puoi avviare sul tuo laptop un browser, un gioco, un lettore multimediale, un editor di testo e così via. Di fatto, un sistema tipico può eseguire decine e centinaia di processi simultaneamente. Questo fatto rende il sistema più facile da usare, non dovendo mai preoccuparti se la CPU è libera: semplicemente avvii i programmi.
Da qui sorge il problema: come garantire l'illusione di molteplici CPU? Come può un sistema operativo creare l'illusione di un numero praticamente infinito di CPU, anche se si dispone di un solo CPU fisico?
Il sistema operativo crea questa illusione attraverso la virtualizzazione della CPU. Eseguendo un processo, poi fermandolo, avviando un altro processo e così via, il sistema operativo può mantenere l'illusione che esistano molteplici CPU virtuali, anche se in realtà si tratta di uno o più processori fisici. Questa tecnica è chiamata partizionamento temporale delle risorse CPU. Questa tecnica consente agli utenti di eseguire quanti più processi simultanei desiderano. Il costo di tale soluzione è rappresentato dalle prestazioni: se più processi condividono la CPU, ciascun processo sarà elaborato più lentamente.
Per implementare la virtualizzazione della CPU, e in particolare per farlo bene, il sistema operativo ha bisogno di supporto sia a livello basso che a livello alto. Il supporto a basso livello è chiamato meccanismi — sono metodi o protocolli a basso livello che implementano la parte necessaria della funzionalità. Un esempio di tale funzionalità è il contesto di commutazione, che consente al sistema operativo di fermare un programma e avviarne un altro sul processore. Questa suddivisione temporale è realizzata in tutti i sistemi operativi moderni.
Sopra questi meccanismi si trova una certa logica incorporata nel sistema operativo, sotto forma di "politiche". Politica — è un certo algoritmo di decisione del sistema operativo. Tali politiche, ad esempio, determinano quale programma dovrebbe essere eseguito (dalla lista dei comandi) per primo. Così, ad esempio, questo compito sarà risolto dalla politica chiamata politica di pianificazione (scheduling policy) e nella scelta della decisione si baserà su dati come: la cronologia di esecuzione (quale programma è stato eseguito più a lungo nell'ultimo minuto), quale carico sta generando il processo (quali tipi di programmi sono stati eseguiti), le metriche di prestazione (se il sistema è ottimizzato per l'interazione interattiva o per la larghezza di banda) e così via.
Astrazione: processo
L'astrazione di un programma in esecuzione gestita dal sistema operativo è ciò che chiamiamo processo. Come già detto, un processo è semplicemente un programma in esecuzione in un dato intervallo di tempo. È il programma tramite il quale possiamo ottenere informazioni aggregate da diverse risorse di sistema, a cui il programma accede o che il programma coinvolge durante la sua esecuzione.
Per comprendere gli elementi costitutivi di un processo, è necessario comprendere gli stati del sistema: cosa può leggere o modificare un programma durante l'esecuzione. In ogni momento è importante sapere quali elementi del sistema sono cruciali per l'esecuzione del programma.
Uno degli elementi ovvi dello stato del sistema che è incluso in un processo è la memoria. Le istruzioni si trovano in memoria. I dati che il programma legge o scrive si trovano anch'essi in memoria. Pertanto, la memoria che il processo può indirizzare (il cosiddetto spazio di indirizzamento) è parte del processo.
Anche i registri fanno parte dello stato del sistema. Molte istruzioni sono indirizzate a modificare il valore dei registri o a leggerne il valore, e così i registri diventano una parte fondamentale del funzionamento del processo.
Va notato che lo stato della macchina è anche costituito da alcuni registri speciali. Ad esempio, IP — instruction pointer — puntatore all'istruzione che il programma sta eseguendo in quel momento. Ci sono anche stack pointer e il relativo frame pointer, che vengono utilizzati per gestire: parametri delle funzioni, variabili locali e indirizzi di ritorno.
Infine, i programmi spesso accedono alla ROM (memoria permanente). Tali informazioni su “I/O” (input/output) devono includere un elenco dei file aperti dal processo in quel momento.
Processo API
Per migliorare la comprensione del funzionamento del processo, esamineremo alcuni esempi di chiamate di sistema che devono essere incluse in qualsiasi interfaccia di sistema operativo. Questi API sono disponibili in una forma o nell'altra su qualsiasi sistema operativo.
● Crea (creazione): Un sistema operativo deve includere un metodo per creare nuovi processi. Quando immetti un comando nel terminale o avvii un'applicazione facendo doppio clic sull'icona, viene inviato un messaggio al sistema operativo per creare un nuovo processo e avviare il programma specificato.
● Eliminazione: Poiché esiste un'interfaccia per la creazione di processi, il sistema operativo deve anche fornire la possibilità di terminare forzatamente un processo. La maggior parte dei programmi si avvia e si chiude naturalmente durante la propria esecuzione. In caso contrario, l'utente desidererebbe avere la possibilità di interromperli e quindi un'interfaccia per fermare il processo sarebbe utile.
● Attendere (attesa): A volte è utile attendere la conclusione di un processo, quindi vengono forniti alcuni interfacce che consentono di farlo.
● Controllo Misc (varie operazioni di controllo): Oltre a terminare e attendere un processo, ci sono anche altri vari metodi di controllo. Ad esempio, la maggior parte dei sistemi operativi offre la possibilità di mettere in pausa un processo (interrompere la sua esecuzione per un certo periodo) e successivamente riprenderlo (continuare l'esecuzione).
● Stato (stato): Esistono diverse interfacce per ottenere informazioni sullo stato del processo, come la durata del suo funzionamento o in quale stato si trovi attualmente.

Creazione del processo: dettagli
Una delle cose interessanti è come i programmi si trasformano in processi. In particolare, come il sistema operativo carica e avvia un programma. Come viene creato specificamente un processo.
In primo luogo, il sistema operativo deve caricare il codice del programma e i dati statici nella memoria (nello spazio degli indirizzi del processo). I programmi sono generalmente memorizzati su disco o su un'unità a stato solido in un formato eseguibile. Pertanto, il processo di caricamento del programma e dei dati statici in memoria richiede al sistema operativo la capacità di leggere questi byte dal disco e posizionarli da qualche parte in memoria.
Nei sistemi operativi precedenti, il processo di caricamento veniva eseguito in modo impaziente (eagerly), il che significa che il codice veniva caricato completamente nella memoria prima che il programma venisse avviato. I sistemi operativi moderni lo fanno in modo pigro (lazily), cioè caricando pezzi di codice o dati solo quando necessari per l'esecuzione del programma.
Dopo che il codice e i dati statici sono stati caricati nella memoria del sistema operativo, è necessario eseguire alcune altre operazioni prima di avviare il processo. Una certa quantità di memoria deve essere allocata per lo stack. I programmi utilizzano lo stack per variabili locali, parametri di funzione e indirizzi di ritorno.. Il sistema operativo alloca questa memoria e la restituisce al processo. Lo stack può anche essere allocato con alcuni argomenti, in particolare riempie i parametri della funzione main(), come l'array argc e argv.
Il sistema operativo può anche allocare una certa quantità di memoria per l'heap del programma. L'heap viene utilizzato dai programmi per i dati dinamicamente allocati richiesti esplicitamente.. I programmi richiedono questo spazio chiamando una funzione malloc() e lo puliscono esplicitamente, chiamando una funzione. free(). La heap è necessaria per strutture dati come: liste collegate, tabelle hash, alberi e altro. Inizialmente, viene allocata una piccola quantità di memoria per la heap, ma nel tempo, durante l'esecuzione del programma, la heap può richiedere ulteriore memoria tramite la chiamata API della libreria malloc(). Il sistema operativo è coinvolto nel processo di allocazione di ulteriore memoria per soddisfare queste richieste.
Il sistema operativo si occuperà anche di compiti di inizializzazione, in particolare quelli relativi all'input/output. Ad esempio, nei sistemi UNIX, ogni processo ha per impostazione predefinita 3 descrittori di file aperti per il flusso standard di input, output e errori. Questi descrittori permettono ai programmi di leggere l'input dal terminale e di visualizzare informazioni sullo schermo.
In questo modo, caricando il codice e i dati statici nella memoria, creando e inizializzando lo stack e svolgendo altre attività relative all'esecuzione di operazioni di input/output, il sistema operativo prepara il terreno per l'esecuzione del processo. In definitiva, rimane l'ultimo compito: avviare l'esecuzione del programma attraverso il suo punto di ingresso, chiamato funzione main(). Passando all'esecuzione della funzione main(), il sistema operativo trasferisce il controllo della CPU al processo appena creato, facendo così iniziare l'esecuzione del programma.
Stato del processo
Ora che abbiamo una certa comprensione di cosa sia un processo e come venga creato, elenchiamo gli stati in cui un processo può trovarsi. Nella sua forma più semplice, un processo può trovarsi in uno di questi stati:
● Esecuzione. Nello stato in esecuzione, il processo viene eseguito sulla CPU. Ciò significa che le istruzioni vengono eseguite.
● Pronto. Nello stato di attesa, il processo è pronto per avviarsi, ma per qualche motivo il sistema operativo non lo esegue nel momento specificato.
● Bloccato. In stato bloccato, il processo esegue alcune operazioni che non gli consentono di essere pronto per l'esecuzione fino a quando non si verifica un evento. Un esempio comune è quando un processo inizializza un'operazione IO, diventa bloccato e così un altro processo può utilizzare la CPU.

Possiamo immaginare questi stati come un grafo. Come possiamo vedere nell'immagine, lo stato di un processo può cambiare tra RUNNING e READY a discrezione del sistema operativo. Quando lo stato di un processo cambia da READY a RUNNING, ciò significa che il processo è stato programmato. In direzione opposta — è stato rimosso dalla pianificazione. Nel momento in cui un processo diventa BLOCKED, ad esempio, inizializzando un'operazione IO, il sistema operativo lo terrà in questo stato fino al verificarsi di un evento, come il completamento dell'IO. In quel momento, il passaggio allo stato READY e possibilmente immediatamente a RUNNING, se così decide il sistema operativo.
Diamo un'occhiata a un esempio di come due processi attraversano questi stati. Immaginiamo innanzitutto che entrambi i processi siano in esecuzione e utilizzino solo la CPU. In questo caso, i loro stati appariranno come segue.

Nel seguente esempio, il primo processo dopo un certo periodo di lavoro richiede IO e passa allo stato BLOCKED, dando l'opportunità di avviare il secondo processo (FIG 1.4). Il sistema operativo vede che il processo 0 non utilizza la CPU e avvia il processo 1. Durante l'esecuzione del processo 1, l'IO termina e lo stato del processo 0 cambia in READY. Infine, il processo 1 termina, e alla sua conclusione il processo 0 viene avviato, eseguito e completato.

Struttura dei dati
Il sistema operativo è a sua volta un programma e, come qualsiasi altro programma, ha alcune strutture dati chiave che monitorano diversi pezzi di informazione rilevanti. Per tenere traccia dello stato di ogni processo, il sistema operativo manterrà una certa process list per tutti i processi nello stato READY e alcune informazioni aggiuntive per monitorare i processi in esecuzione in quel momento. Inoltre, il sistema operativo deve monitorare anche i processi bloccati. Una volta completato l'IO, il sistema operativo deve risvegliare il processo necessario e portarlo nello stato pronto per l'esecuzione.
Ad esempio, il sistema operativo deve salvare lo stato dei registri del processore. Nel momento in cui il processo viene interrotto, lo stato dei registri viene salvato nello spazio indirizzi del processo, e quando il suo lavoro riprende, i valori dei registri vengono ripristinati, consentendo così la continuazione dell'esecuzione di questo processo.
Oltre agli stati ready, blocked, running, esistono anche altri stati. A volte, al momento della creazione, un processo può trovarsi nello stato INIT. Infine, un processo può essere riservato allo stato FINAL, quando è già completato, ma le informazioni su di esso non sono ancora state cancellate. In sistemi UNIX, tale stato è chiamato processo-zombie. Questo stato è utile nelle situazioni in cui il processo genitore desidera conoscere il codice di uscita del processo figlio; ad esempio, di solito 0 segnala una conclusione positiva e 1 un errore, tuttavia i programmatori possono definire codici di uscita aggiuntivi per segnalare vari problemi. Alla conclusione, il processo genitore effettua l'ultima chiamata di sistema, ad esempio wait(), per attendere la conclusione del processo figlio e segnalare al sistema operativo che è possibile liberare eventuali dati associati al processo completato.

Punti chiave della lezione:
● Processo — l'astrazione principale di un programma in esecuzione nel sistema operativo. In un dato momento, un processo può essere descritto dal suo stato: il contenuto della memoria nel suo spazio di indirizzi, il contenuto dei registri del processore, inclusi il puntatore delle istruzioni e il puntatore dello stack, oltre alle informazioni sull'I/O, come i file aperti che vengono letti o scritti.
● Processo API consiste nelle chiamate che i programmi possono fare riguardo ai processi. Di solito, queste sono chiamate per creare, eliminare o eseguire altre operazioni.
● Il processo si trova in uno dei molti stati, tra cui running, ready, blocked. Vari eventi, come la pianificazione, l'eccezione dalla pianificazione o l'attesa, possono tradurre lo stato del processo da uno all'altro.
● Elenco dei processi contiene informazioni su tutti i processi nel sistema. Ogni voce in esso è chiamata process control block, che è effettivamente una struttura che contiene tutte le informazioni necessarie su un processo specifico.
Fonte: habr.com
