Domanda

Sto lavorando alla progettazione del kernel e ho alcune domande relative al paging.

L'idea di base che ho finora è questa: ogni programma ha il suo (o almeno così pensa) 4G di memoria, meno una sezione da qualche parte che riservo per le funzioni del kernel che il programma può chiamare. Pertanto, il sistema operativo deve trovare un modo per caricare le pagine in memoria che il programma deve utilizzare durante il suo funzionamento.

Ora, supponendo che avessimo infinite quantità di memoria e tempo del processore, potrei caricare / allocare qualsiasi pagina da cui il programma ha scritto o letto come è accaduto usando errori di pagina per pagine che non esistevano (o sono state scambiate) così il sistema operativo potrebbe rapidamente allocarli o scambiarli. Nel mondo reale, tuttavia, ho bisogno di ottimizzare questo processo, in modo che non abbiamo un programma che consuma costantemente tutta la memoria che abbia mai toccato.

Quindi suppongo che la mia domanda sia: come fa generalmente un SO a fare ciò? Il mio pensiero iniziale è quello di creare una funzione che il programma chiama per impostare / liberare pagine, che può quindi gestire da solo, ma un programma generalmente lo fa o il compilatore presuppone che abbia un regno libero? Inoltre, in che modo il compilatore gestisce le situazioni in cui deve allocare un segmento di memoria abbastanza grande? Devo fornire una funzione che tenti di assegnarle X pagine in ordine?

Questa ovviamente non è una domanda specifica del linguaggio, ma sono parziale rispetto allo standard C e buono con C ++, quindi vorrei che qualsiasi esempio di codice fosse in quello o nell'assembly. (L'assemblaggio non dovrebbe essere necessario, intendo farlo funzionare con il maggior numero di codice C possibile e ottimizzare come ultimo passaggio.)

Un'altra cosa a cui dovrebbe anche essere più facile rispondere: come si gestiscono generalmente le funzioni del kernel che un programma deve chiamare? Va bene solo avere una determinata area di memoria (stavo pensando alla fine dello spazio virtuale) che contiene la maggior parte delle funzioni di base / memoria specifica del processo che il programma può chiamare? Il mio pensiero da lì sarebbe quello di fare in modo che le funzioni del kernel facciano qualcosa di molto elaborato e scambino le pagine (in modo che i programmi non possano vedere le funzioni sensibili del kernel nel loro spazio) quando i programmi devono fare qualcosa di grave, ma non sono davvero concentrandosi sulla sicurezza a questo punto.

Quindi credo di essere più preoccupato per le idee di design generali che per le specifiche. Vorrei rendere il kernel completamente compatibile con GCC (in qualche modo) e devo assicurarmi che fornisca tutto ciò di cui un normale programma avrebbe bisogno.

Grazie per qualsiasi consiglio.

È stato utile?

Soluzione

Un buon punto di partenza per tutte queste domande è guardare come Unix lo fa. Come dice una famosa citazione, "coloro che non capiscono UNIX sono condannati a reinventarlo, male."

Per prima cosa, su come chiamare le funzioni del kernel. Non è sufficiente avere semplicemente le funzioni da qualche parte che un programma può chiamare, poiché molto probabilmente il programma è in esecuzione in "modalità utente" (anello 3 su IA-32) e il kernel deve funzionare in "modalità kernel" (di solito squilla 0 su IA-32) per eseguire le sue operazioni privilegiate. Devi in ??qualche modo effettuare la transizione tra le due modalità, e questo è molto specifico per l'architettura.

Su IA-32, il modo tradizionale è usare un gate nell'IDT insieme a un interrupt software (Linux usa int 0x80). I processori più recenti hanno altri (più veloci) modi per farlo, e quali sono disponibili dipende dal fatto che la CPU provenga da AMD o Intel e dal modello di CPU specifico. Per soddisfare questa variazione, i kernel Linux recenti usano una pagina di codice mappata dal kernel nella parte superiore dello spazio degli indirizzi per ogni processo. Quindi, su Linux recente, per effettuare una chiamata di sistema, si chiama una funzione in questa pagina, che a sua volta farà tutto il necessario per passare alla modalità kernel (il kernel ha più di una copia di quella pagina e sceglie quale copia usare all'avvio a seconda delle funzionalità del processore).

Ora, la gestione della memoria. Questo è un argomento enorme ; potresti scriverne un grande libro e non esaurire l'argomento.

Assicurati di tenere presente che ci sono almeno due viste della memoria: la vista fisica (l'ordine reale delle pagine, visibile al sottosistema di memoria hardware e spesso alle periferiche esterne) e la vista logica (l'ordine delle pagine viste dai programmi in esecuzione sulla CPU). È abbastanza facile confondere entrambi. Assegnerai le pagine fisiche e le assegnerai agli indirizzi logici sul programma o nello spazio degli indirizzi del kernel. Una singola pagina fisica può avere diversi indirizzi logici e può essere mappata a diversi indirizzi logici in diversi processi.

La memoria del kernel (riservata al kernel) è solitamente mappata nella parte superiore dello spazio degli indirizzi di ogni processo. Tuttavia, è impostato in modo da poter essere utilizzato solo in modalità kernel. Non c'è bisogno di trucchi fantasiosi per nascondere quella parte della memoria; l'hardware fa tutto il lavoro di blocco dell'accesso (su IA-32, viene eseguito tramite flag di pagina o limiti di segmento).

I programmi allocano la memoria sul resto dello spazio degli indirizzi in diversi modi:

  • Parte della memoria è allocata dal programma di caricamento del kernel. Ciò include il codice del programma (o "testo"), i dati inizializzati dal programma ("dati"), i dati non inizializzati del programma ("bss", a riempimento zero), lo stack e diverse probabilità e finali. Quanto allocare, dove, quali dovrebbero essere i contenuti iniziali, quali flag di protezione usare e molte altre cose, vengono letti dalle intestazioni sul file eseguibile da caricare.
  • Tradizionalmente su Unix, c'è un'area di memoria che può crescere e ridursi (il suo limite superiore può essere modificato tramite la chiamata di sistema brk () ). Questo è tradizionalmente usato dall'heap (l'allocatore di memoria nella libreria C, di cui malloc () è una delle interfacce, è responsabile dell'heap).
  • Spesso puoi chiedere al kernel di mappare un file su un'area dello spazio degli indirizzi. Le letture e le scritture in quell'area vengono (tramite la magia di paging) indirizzate al file di backup. Questo di solito si chiama mmap () . Con un mmap anonimo, è possibile allocare nuove aree dello spazio degli indirizzi che non sono supportate da alcun file, ma che si comportano allo stesso modo. Il caricatore del programma del kernel utilizzerà spesso mmap per allocare parti del codice del programma (ad esempio, il codice del programma può essere supportato dall'eseguibile stesso).

L'accesso ad aree dello spazio degli indirizzi che non sono allocate in alcun modo (o riservate al kernel) è considerato un errore e su Unix causerà l'invio di un segnale al programma.

Il compilatore alloca la memoria staticamente (specificandola nelle intestazioni dei file eseguibili; il programma di caricamento del kernel allocherà la memoria durante il caricamento del programma) o dinamicamente (chiamando una funzione sulla libreria standard del linguaggio, che di solito poi chiama un funzione nella libreria standard del linguaggio C, che quindi chiama il kernel per allocare memoria e la suddivide se necessario).

Il modo migliore per apprendere le basi di tutto ciò è leggere uno dei numerosi libri sui sistemi operativi, in particolare quelli che usano una variante Unix come esempio. Andrà in modo molto più dettagliato di quanto potrei su una risposta su StackOverflow.

Altri suggerimenti

La risposta a questa domanda dipende fortemente dall'architettura. Presumo che tu stia parlando di x86. Con x86, un kernel generalmente fornisce una serie di chiamate di sistema , che sono punti di ingresso predeterminati nel kernel. Il codice utente può inserire il kernel solo in quei punti specifici, quindi il kernel ha un attento controllo su come interagisce con il codice utente.

In x86, ci sono due modi per implementare le chiamate di sistema: con gli interrupt e con le istruzioni sysenter / sysexit. Con gli interrupt, il kernel imposta una tabella dei descrittori di interrupt (IDT), che definisce i possibili punti di ingresso nel kernel. Il codice utente può quindi usare l'istruzione int per generare un interrupt soft da chiamare nel kernel. Gli interrupt possono anche essere generati dall'hardware (i cosiddetti hard interrupt); tali interrupt dovrebbero generalmente essere distinti dagli interrupt soft, ma non devono esserlo.

Le istruzioni sysenter e sysexit sono un modo più veloce di eseguire chiamate di sistema, poiché la gestione degli interrupt è lenta; Non ho familiarità con il loro utilizzo, quindi non posso commentare se sono una scelta migliore per la tua situazione.

Qualunque cosa tu usi, dovrai definire l'interfaccia di chiamata del sistema. Probabilmente vorrai passare gli argomenti delle chiamate di sistema nei registri e non nello stack, poiché la generazione di un interrupt ti farà passare gli stack allo stack del kernel. Ciò significa che quasi sicuramente dovrai scrivere alcuni stub del linguaggio assembly sia sull'estremità della modalità utente per effettuare la chiamata di sistema, sia di nuovo sull'estremità del kernel per raccogliere gli argomenti della chiamata di sistema e salvare i registri.

Dopo aver installato tutto ciò, puoi iniziare a pensare alla gestione degli errori di pagina. Gli errori di pagina sono effettivamente solo un altro tipo di interruzione: quando il codice utente tenta di accedere a un indirizzo virtuale per il quale non vi è alcuna voce nella tabella delle pagine, genererà l'interruzione 14 e otterrai anche l'indirizzo di errore come codice di errore. Il kernel può prendere queste informazioni e quindi decidere di leggere la pagina mancante dal disco, aggiungere il mapping della tabella delle pagine e tornare al codice utente.

Consiglio vivamente di dare un'occhiata ad alcuni dei materiali dei Sistemi operativi MIT classe. Dai un'occhiata alla sezione dei riferimenti, ha un sacco di roba buona.

scroll top