Introduzione ai sistemi operativi
Ciao, Habr! Voglio presentarvi una serie di articoli tradotti su una letteratura che considero interessante: OSTEP. In questo materiale viene esaminato in profondità il funzionamento dei sistemi operativi UNIX-like, in particolare la gestione dei processi, dei vari scheduler, della memoria e di altri componenti simili che costituiscono un moderno sistema operativo. Puoi consultare 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:
— originale:
— originale:
— la mia personale adattazione:
E potete anche dare un'occhiata al mio canale su =)
Funzionamento del programma
Cosa succede quando un programma è in esecuzione? Un programma avviato esegue una semplice operazione: esegue istruzioni. Ogni secondo milioni, e forse miliardi, di istruzioni vengono estratte dal processore dalla memoria RAM, e a sua volta le decodifica (ad esempio, riconosce a quale tipo appartengono queste istruzioni) ed esegue. Queste possono includere l'addizione di due numeri, l'accesso alla memoria, il controllo di una condizione, il passaggio a una funzione e così via. Dopo aver completato l'esecuzione di un'istruzione, il processore passa a un'altra. E così, istruzione dopo istruzione, vengono eseguite fino a quando il programma non termina.
Questo esempio è naturalmente semplificato: in realtà, per accelerare il funzionamento, l'hardware moderno consente di eseguire istruzioni fuori sequenza, calcolare risultati possibili, eseguire istruzioni simultaneamente e altre simili astuzie.
Modello di calcolo di Von Neumann
La forma semplificata del funzionamento che abbiamo descritto è simile al modello di calcolo di Von Neumann. Von Neumann è uno dei pionieri dei sistemi informatici e uno degli autori della teoria dei giochi. Durante l'esecuzione del programma si verificano molti altri eventi, funzionano molte altre logiche e processi esterni, il cui obiettivo principale è semplificare l'avvio, l'operatività e la manutenzione del sistema.
Esiste un insieme di software responsabile per la semplicità di avvio dei programmi (o addirittura per consentire l'esecuzione simultanea di più programmi). Questo consente ai programmi di condividere la stessa memoria e di interagire con vari dispositivi. Questo insieme di software (programmi) è fondamentalmente ciò che chiamiamo sistema operativo, la cui funzione include il monitoraggio affinché il sistema funzioni correttamente ed efficacemente, oltre a garantire la facilità di gestione di questo sistema.
Sistema operativo
Il sistema operativo, abbreviato in SO, è un insieme di programmi interconnessi, progettati per gestire le risorse del computer e organizzare l'interazione tra l'utente e il computer..
Il SO raggiunge la sua efficienza principalmente attraverso la tecnica più importante — la tecnica di virtualizzazione. Il SO interagisce con le risorse fisiche (processore, memoria, disco e simili) e le trasforma in una forma più generica, con maggiori capacità e più facile da utilizzare. Pertanto, per una comprensione generale, possiamo paragonare molto grossolanamente il sistema operativo a una macchina virtuale.
Per consentire agli utenti di dare comandi al sistema operativo, sfruttando così le capacità della macchina virtuale (come: avvio di un programma, allocazione di memoria, accesso a file e così via), il sistema operativo fornisce un'interfaccia chiamata API (application programming interface), che può essere chiamata. Un tipico sistema operativo offre la possibilità di effettuare cento chiamate di sistema.
Infine, poiché la virtualizzazione consente a più programmi di funzionare (in questo modo condividendo la CPU) e contemporaneamente accedere alle loro istruzioni e dati (condividendo così la memoria), oltre ad accedere ai dischi (condividendo così i dispositivi di input/output), il sistema operativo viene anche chiamato gestore delle risorse. Ogni processore, disco e memoria è una risorsa del sistema e pertanto uno dei compiti del sistema operativo è gestire queste risorse in modo efficiente, equo o, viceversa, a seconda dell'obiettivo per cui è stato sviluppato questo sistema operativo.
Virtualizzazione della CPU
Consideriamo il seguente programma:
(https://www.youtube.com/watch?v=zDwT5fUcki4&feature=youtu.be)

Non esegue azioni particolari, essenzialmente tutto ciò che fa è chiamare la funzione spin(), il cui compito è controllare ciclicamente il tempo e restituire, dopo che è trascorsa un secondo. In questo modo, ripete all'infinito la stringa che l'utente ha fornito come argomento.
Avviamo questo programma e forniamo come argomento il carattere “A”. Il risultato non è particolarmente interessante: il sistema semplicemente esegue il programma, che periodicamente stampa sullo schermo il carattere “A”.
Ora proviamo una variante in cui sono in esecuzione più istanze dello stesso programma, ma che stampano lettere diverse, per rendere il tutto più comprensibile. In questo caso, il risultato sarà leggermente diverso. Nonostante abbiamo un solo processore, il programma viene eseguito contemporaneamente. Come è possibile? La risposta è che il sistema operativo, con l'aiuto delle capacità hardware, crea l'illusione. L'illusione che nel sistema ci siano più processori virtuali, trasformando un processore fisico in un numero teoricamente infinito, consentendo così ai programmi di sembrare eseguirsi contemporaneamente. Questa illusione si chiama Virtualizzazione CPU.
Questa situazione solleva molte domande, ad esempio, se più programmi desiderano avviarsi contemporaneamente, quale di essi verrà eseguito? A questa domanda rispondono le “politiche” del sistema operativo. Le politiche vengono utilizzate in molti ambiti del sistema operativo e rispondono a domande simili, e sono anche i meccanismi di base che il sistema operativo implementa. Da qui il ruolo del sistema operativo come gestore delle risorse.
Virtualizzazione della memoria
Adesso analizziamo la memoria. Il modello fisico della memoria nei sistemi moderni si presenta come un array di byte. Per leggere dalla memoria è necessario specificare l'indirizzo della cella, per ottenere accesso ad essa. Per scrivere o aggiornare i dati è necessario specificare anche i dati e l'indirizzo della cella in cui devono essere scritti.
Le richieste alla memoria avvengono continuamente durante l'esecuzione del programma. Il programma memorizza nella memoria tutta la sua struttura dati e accede ad essa eseguendo varie istruzioni. Anche le istruzioni, nel frattempo, sono memorizzate nella memoria, quindi l'accesso avviene anche per ogni richiesta della successiva istruzione.
Chiamata a malloc()
Consideriamo il seguente programma, che alloca un'area di memoria utilizzando una chiamata malloc() (https://youtu.be/jnlKRnoT1m0):

Il programma esegue diverse operazioni. Innanzitutto, alloca una certa quantità di memoria (riga 7), quindi visualizza l'indirizzo della cella di memoria allocata (riga 9) e scrive zero nella prima posizione di memoria allocata. Successivamente, il programma entra in un ciclo in cui incrementa il valore memorizzato all'indirizzo nella variabile “p”. Inoltre, visualizza l'ID del proprio processo. L'ID del processo è unico per ogni processo in esecuzione.Se tuttavia avviamo più copie, ci imbatteremo in un risultato interessante: nel primo caso, se non facciamo nulla e semplicemente avviamo più copie, gli indirizzi saranno diversi. Ma questo non è in linea con la nostra teoria! Esatto, poiché nelle distribuzioni moderne è abilitata per impostazione predefinita la funzione di randomizzazione della memoria. Se la disattiviamo, otterremo il risultato atteso: gli indirizzi di memoria delle due applicazioni in esecuzione contemporanea saranno identici.

Di conseguenza, risulta che due programmi indipendenti operano con i propri spazi di indirizzi privati, che a loro volta vengono mappati dal sistema operativo nella memoria fisica.Pertanto, l'uso degli indirizzi di memoria all'interno di un programma non influenzerà in alcun modo gli altri, e ogni programma ha l'impressione di avere una propria porzione di memoria fisica, interamente dedicata al proprio utilizzo. La realtà, tuttavia, è che la memoria fisica è una risorsa condivisa, gestita dal sistema operativo.
Coerenza
Un altro dei temi importanti nell'ambito dei sistemi operativi è la coerenza. Questo termine viene utilizzato quando si parla di problemi nel sistema che possono sorgere quando si lavora su molteplici attività contemporaneamente all'interno di un programma. Problemi di coerenza possono anche sorgere all'interno dello stesso sistema operativo. Negli esempi precedenti sulla virtualizzazione della memoria e del processore, abbiamo compreso che il sistema operativo gestisce molte cose contemporaneamente: avvia il primo processo, poi il secondo e così via. Si è scoperto che tale comportamento può portare a determinati problemi. Ad esempio, i programmi multithread moderni incontrano tali difficoltà.
Consideriamo il seguente programma:

Il programma nella funzione principale crea due thread, usando la chiamata Pthread_create(). In questo esempio, il thread può essere visto come una funzione eseguita in uno spazio di memoria insieme ad altre funzioni, con il numero di funzioni attive evidentemente superiore a uno. In questo esempio, ogni thread si attiva ed esegue la funzione worker() che a sua volta semplicemente incrementa una variabile,.
Eseguiamo questo programma con l'argomento 1000. Come avete già potuto intuire, il risultato dovrebbe essere 2000, poiché ogni thread ha incrementato la variabile 1000 volte. Tuttavia, non è tutto così semplice. Proviamo a far partire il programma con un numero di ripetizioni di un ordine superiore.

Inserendo come input un numero, ad esempio, 100000, ci aspettiamo di ottenere come output il numero 200000. Tuttavia, eseguendo il numero 100000 più volte, non solo non vedremo la risposta corretta, ma otterremo anche risposte errate e diverse. La chiave è che per incrementare un numero sono necessarie tre operazioni: estrazione del numero dalla memoria, incremento e poi scrittura del numero di nuovo. Poiché tutte queste istruzioni non vengono eseguite atomicamente (tutte simultaneamente), possono verificarsi situazioni strane. Questo problema si chiama in programmazione race condition — condizione di gara. Quando forze ignote in momenti ignoti possono influenzare l'esecuzione di alcune delle vostre operazioni.
Fonte: habr.com
