Tutte le CPU moderne hanno la capacità di interrompere le istruzioni della macchina attualmente in esecuzione. Salvano abbastanza stato (di solito, ma non sempre, nello stack) per consentire di riprendere l' esecuzione in seguito, come se nulla fosse accaduto (l'istruzione interrotta verrà riavviata da zero, di solito). Quindi iniziano a eseguire un gestore di interrupt , che è solo più codice macchina, ma posizionato in una posizione speciale in modo che la CPU sappia dove si trova in anticipo. I gestori di interrupt fanno sempre parte del kernel del sistema operativo: il componente che gira con il massimo privilegio ed è responsabile della supervisione dell'esecuzione di tutti gli altri componenti. 1,2
Gli interrupt possono essere sincroni , nel senso che vengono attivati dalla CPU stessa come risposta diretta a qualcosa che l'istruzione attualmente in esecuzione ha fatto, o asincroni , il che significa che si verificano in un momento imprevedibile a causa di un evento esterno, come i dati che arrivano sulla rete porta. Alcune persone riservano il termine "interruzione" per interruzioni asincrone e chiamano invece interruzioni sincrone "trappole", "guasti" o "eccezioni", ma quelle parole hanno tutti altri significati, quindi continuerò con "interruzione sincrona".
Ora, i sistemi operativi più moderni hanno una nozione di processi . Nella sua forma più semplice, si tratta di un meccanismo in base al quale il computer può eseguire più di un programma contemporaneamente, ma è anche un aspetto chiave di come i sistemi operativi configurano la protezione della memoria , che è una caratteristica della maggior parte (ma, ahimè, ancora non tutte ) CPU moderne. Si accompagna alla memoria virtuale, che è la capacità di modificare la mappatura tra indirizzi di memoria e posizioni effettive nella RAM. La protezione della memoria consente al sistema operativo di assegnare a ciascun processo il proprio blocco privato di RAM, a cui solo lui può accedere. Inoltre, consente al sistema operativo (che agisce per conto di alcuni processi) di designare regioni della RAM come di sola lettura, eseguibili, condivise tra un gruppo di processi cooperanti, ecc. Ci sarà anche un pezzo di memoria accessibile solo dal kernel. 3
Finché ogni processo accede alla memoria solo nei modi in cui la CPU è configurata per consentire, la protezione della memoria è invisibile. Quando un processo infrange le regole, la CPU genererà un interrupt sincrono, chiedendo al kernel di sistemare le cose. Accade regolarmente che il processo non abbia veramente violato le regole, solo il kernel deve fare un po 'di lavoro prima che il processo possa continuare. Ad esempio, se una pagina della memoria di un processo deve essere "sfrattata" nel file di scambio per liberare spazio nella RAM per qualcos'altro, il kernel contrassegnerà quella pagina inaccessibile. La prossima volta che il processo tenta di utilizzarlo, la CPU genererà un interrupt di protezione della memoria; il kernel recupererà la pagina dallo swap, la rimetterà dove era, la renderà nuovamente accessibile e riprenderà l'esecuzione.
Ma supponiamo che il processo abbia davvero infranto le regole. Ha tentato di accedere a una pagina a cui non è mai stata mappata alcuna RAM, oppure ha tentato di eseguire una pagina contrassegnata come non contenente codice macchina o altro. La famiglia di sistemi operativi generalmente conosciuta come "Unix" usa tutti i segnali per affrontare questa situazione. 4 I segnali sono simili agli interrupt, ma sono generati dal kernel e messi in campo dai processi, piuttosto che essere generati dall'hardware e messi in campo dal kernel. I processi possono definire gestori di segnalinel proprio codice e dire al kernel dove si trovano. Quei gestori di segnale verranno quindi eseguiti, interrompendo il normale flusso di controllo, quando necessario. Tutti i segnali hanno un numero e due nomi, uno dei quali è un acronimo criptico e l'altro una frase leggermente meno enigmatica. Il segnale che viene generato quando un processo infrange le regole di protezione della memoria è (per convenzione) il numero 11, e i suoi nomi sono SIGSEGV
"Errore di segmentazione". 5,6
Una differenza importante tra segnali e interruzioni è che esiste un comportamento predefinito per ogni segnale. Se il sistema operativo non riesce a definire i gestori per tutti gli interrupt, questo è un bug nel sistema operativo e l'intero computer si arresta in modo anomalo quando la CPU tenta di richiamare un gestore mancante. Ma i processi non hanno l'obbligo di definire i gestori di segnali per tutti i segnali. Se il kernel genera un segnale per un processo e quel segnale è stato lasciato al suo comportamento predefinito, il kernel andrà avanti e farà qualunque cosa sia il default e non disturberà il processo. La maggior parte dei comportamenti predefiniti dei segnali sono "non fare nulla" o "terminare questo processo e forse produrre anche un dump principale". SIGSEGV
è uno di questi ultimi.
Quindi, per ricapitolare, abbiamo un processo che ha infranto le regole di protezione della memoria. La CPU ha sospeso il processo e ha generato un interrupt sincrono. Il kernel ha messo in campo quell'interruzione e ha generato un SIGSEGV
segnale per il processo. Supponiamo che il processo non abbia impostato un gestore di segnali per SIGSEGV
, quindi il kernel esegue il comportamento predefinito, che è quello di terminare il processo. Ciò ha gli stessi effetti della _exit
chiamata di sistema: i file aperti vengono chiusi, la memoria viene deallocata, ecc.
Fino a questo punto nulla ha stampato alcun messaggio che un essere umano può vedere e la shell (o, più in generale, il processo genitore del processo appena terminato) non è stata coinvolta affatto. SIGSEGV
va al processo che ha infranto le regole, non il suo genitore. Il passaggio successivo nella sequenza, tuttavia, è notificare al processo padre che il relativo figlio è stato terminato. Questo può avvenire in molti modi diversi, di cui il più semplice è quando il genitore è già in attesa per questa notifica, utilizzando uno dei wait
chiamate di sistema ( wait
, waitpid
, wait4
, ecc). In tal caso, il kernel farà semplicemente tornare quella chiamata di sistema e fornirà al processo genitore un numero di codice chiamato stato di uscita. 7 Lo stato di uscita informa il genitore del motivo per cui il processo figlio è stato terminato; in questo caso, imparerà che il bambino è stato interrotto a causa del comportamento predefinito di un SIGSEGV
segnale.
Il processo genitore può quindi segnalare l'evento a un essere umano stampando un messaggio; i programmi di shell lo fanno quasi sempre. Il tuo crsh
non include il codice per farlo, ma succede comunque, perché la routine della libreria C system
esegue una shell con tutte le funzionalità /bin/sh
, "under the hood". crsh
è il nonno in questo scenario; la notifica del processo genitore viene messa in campo da /bin/sh
, che stampa il suo solito messaggio. Quindi /bin/sh
si chiude da solo, poiché non ha altro da fare e l'implementazione della libreria C system
riceve tale notifica di uscita. Puoi vedere quella notifica di uscita nel tuo codice, controllando il valore di ritorno disystem
; ma non ti dirà che il processo del nipote è morto su un segfault, perché è stato consumato dal processo di shell intermedio.
Le note
Alcuni sistemi operativi non implementano i driver di dispositivo come parte del kernel; tuttavia, tutti i gestori di interrupt devono comunque far parte del kernel, così come il codice che configura la protezione della memoria, poiché l'hardware non consente a nulla se non al kernel di fare queste cose.
Potrebbe esserci un programma chiamato "hypervisor" o "gestore della macchina virtuale" che è ancora più privilegiato rispetto al kernel, ma ai fini di questa risposta può essere considerato parte dell'hardware .
Il kernel è un programma di , ma è non è un processo; è più simile a una biblioteca. Tutti i processi eseguono parti del codice del kernel, di volta in volta, oltre al proprio codice. Potrebbero esserci un certo numero di "thread del kernel" che eseguono solo il codice del kernel, ma qui non ci riguardano.
L'unico e unico sistema operativo che probabilmente dovrai affrontare più che non può essere considerato un'implementazione di Unix è, ovviamente, Windows. Non utilizza segnali in questa situazione. (In effetti, non ha segnali; su Windows l' <signal.h>
interfaccia è completamente falsata dalla libreria C.) Utilizza invece qualcosa chiamato " gestione delle eccezioni strutturata ".
Alcune violazioni di protezione della memoria generano SIGBUS
("Errore bus") anziché SIGSEGV
. La linea tra i due non è specificata e varia da sistema a sistema. Se hai scritto un programma che definisce un gestore per SIGSEGV
, probabilmente è una buona idea definire lo stesso gestore SIGBUS
.
"Errore di segmentazione" era il nome dell'interrupt generato per le violazioni della protezione della memoria da uno dei computer che eseguivano Unix originale , probabilmente il PDP-11 . " Segmentazione " è un tipo di protezione della memoria, ma al giorno d'oggi il termine " errore di segmentazione " si riferisce genericamente a qualsiasi tipo di violazione della protezione della memoria.
Tutti gli altri modi in cui il processo genitore potrebbe essere notificato di un figlio che è terminato, finire con il genitore che chiama wait
e riceve uno stato di uscita. È solo che qualcos'altro accade prima.
crsh
è un'ottima idea per questo tipo di sperimentazione. Grazie per averci fatto sapere a tutti noi e l'idea alla base.