Qual è la differenza tra lo spazio utente e lo spazio del kernel?


73

Lo spazio del kernel viene utilizzato quando il kernel viene eseguito per conto del programma utente, ad esempio Chiamata di sistema? O è lo spazio degli indirizzi per tutti i thread del kernel (ad esempio lo scheduler)?

Se è il primo, significa che il normale programma utente non può avere più di 3 GB di memoria (se la divisione è 3 GB + 1 GB)? Inoltre, in quel caso come può il kernel utilizzare la memoria elevata, perché a quale indirizzo di memoria virtuale verranno mappate le pagine dalla memoria alta, poiché 1GB di spazio del kernel verrà mappato logicamente?

Risposte:


93

Lo spazio del kernel viene utilizzato quando il kernel viene eseguito per conto del programma utente, ad esempio Chiamata di sistema? O è lo spazio degli indirizzi per tutti i thread del kernel (ad esempio lo scheduler)?

Si e si.

Prima di andare oltre, dovremmo dichiararlo sulla memoria.

Memory get è diviso in due aree distinte:

  • Lo spazio utente , che è un insieme di posizioni in cui vengono eseguiti i normali processi utente (ovvero qualsiasi cosa diversa dal kernel). Il ruolo del kernel è quello di gestire le applicazioni in esecuzione in questo spazio facendo confusione tra loro e con la macchina.
  • Lo spazio del kernel , che è la posizione in cui è archiviato il codice del kernel, e viene eseguito in.

I processi in esecuzione nello spazio utente hanno accesso solo a una parte limitata della memoria, mentre il kernel ha accesso a tutta la memoria. Anche i processi in esecuzione nello spazio utente non hanno accesso allo spazio del kernel. I processi dello spazio utente possono accedere solo a una piccola parte del kernel tramite un'interfaccia esposta dal kernel - il sistema chiama . Se un processo esegue una chiamata di sistema, un interrupt software viene inviato al kernel, che quindi invia il gestore di interrupt appropriato e continua il suo lavoro al termine del gestore.

Il codice dello spazio del kernel ha la proprietà per essere eseguito in "modalità kernel", che (nel tuo tipico desktop -x86- computer) è quello che chiami codice che viene eseguito sotto l'anello 0 . Tipicamente nell'architettura x86, ci sono 4 anelli di protezione . Ring 0 (modalità kernel), Ring 1 (può essere utilizzato da hypervisor o driver di macchine virtuali), Ring 2 (può essere utilizzato da driver, non ne sono così sicuro). Ring 3 è ciò che le applicazioni tipiche vengono eseguite in. È l'anello meno privilegiato e le applicazioni in esecuzione su di esso hanno accesso a un sottoinsieme delle istruzioni del processore. L'anello 0 (spazio del kernel) è l'anello più privilegiato e ha accesso a tutte le istruzioni della macchina. Ad esempio, un'applicazione "semplice" (come un browser) non può utilizzare le istruzioni di assemblaggio x86lgdtper caricare la tabella dei descrittori globali o hltper arrestare un processore.

Se è il primo, significa che il normale programma utente non può avere più di 3 GB di memoria (se la divisione è 3 GB + 1 GB)? Inoltre, in quel caso come può il kernel utilizzare la memoria elevata, perché a quale indirizzo di memoria virtuale verranno mappate le pagine dalla memoria alta, poiché 1GB di spazio del kernel verrà mappato logicamente?

Per una risposta a questo, si prega di fare riferimento all'eccellente risposta di WAG qui


4
Non esitate a dirmi se ho fatto un errore da qualche parte. Sono nuovo nella programmazione del kernel e ho scaricato qui quello che ho imparato finora, insieme ad alcune altre informazioni che ho trovato sul web. Ciò significa che potrebbero esserci delle carenze nella mia comprensione dei concetti che possono essere dimostrate nel testo.
NlightNFotis

Grazie! Penso che ora lo capisco meglio. Solo per essere sicuro di averlo capito correttamente, ho un'altra domanda. Sempre considerando che i primi 3 GB sono utilizzati per spazio utente e 128 MB di spazio del kernel sono utilizzati per memoria alta, i rimanenti 896 MB (memoria bassa) sono mappati staticamente al momento dell'avvio?
Poojan,

1
@NlightNFotis Dico che quasi 15 persone credono che qualunque cosa tu abbia detto, sia corretta (o
almeno

Pensavo che l'anello x86 -1fosse per hypervisor? en.wikipedia.org/wiki/Protection_ring
Dori

1
Notare la differenza tra memoria virtuale e memoria fisica. Gran parte di ciò che chiedi riguarda la memoria virtuale. Questo è mappato alla memoria fisica, questo diventa complicato quando la memoria fisica si avvicina a 3 GB e viene utilizzato PAE. Quindi diventa di nuovo semplice quando viene utilizzato un kernel a 64 bit, in questo caso gli indirizzi negativi sono riservati per il kernel e quelli positivi per lo spazio utente. I processi a 32 bit possono ora utilizzare 4 GB di spazio virtuale. I processi a 64 bit possono usare molto di più, in genere 48 bit (in questo momento su x86-64).
ctrl-alt-delor,

16

Gli anelli CPU sono la distinzione più chiara

In modalità protetta x86, la CPU è sempre in uno dei 4 squilli. Il kernel di Linux utilizza solo 0 e 3:

  • 0 per il kernel
  • 3 per gli utenti

Questa è la definizione più dura e veloce di kernel vs userland.

Perché Linux non usa gli anelli 1 e 2: https://stackoverflow.com/questions/6710040/cpu-privilege-rings-why-rings-1-and-2-arent-used

Come viene determinato l'anello corrente?

L'anello corrente viene selezionato da una combinazione di:

  • tabella descrittiva globale: una tabella in memoria di voci GDT e ogni voce ha un campo Privlche codifica l'anello.

    L'istruzione LGDT imposta l'indirizzo sulla tabella dei descrittori corrente.

    Vedi anche: http://wiki.osdev.org/Global_Descriptor_Table

  • il segmento registra CS, DS, ecc., che indicano l'indice di una voce nel GDT.

    Ad esempio, CS = 0significa che la prima voce del GDT è attualmente attiva per il codice di esecuzione.

Cosa può fare ogni anello?

Il chip della CPU è fisicamente costruito in modo che:

  • l'anello 0 può fare qualsiasi cosa

  • l'anello 3 non può eseguire diverse istruzioni e scrivere in più registri, in particolare:

    • non può cambiare il proprio squillo! Altrimenti, potrebbe impostarsi su ring 0 e gli anelli sarebbero inutili.

      In altre parole, non è possibile modificare il descrittore di segmento corrente , che determina l'anello corrente.

    • impossibile modificare le tabelle delle pagine: https://stackoverflow.com/questions/18431261/how-does-x86-paging-work

      In altre parole, non è possibile modificare il registro CR3 e il paging stesso impedisce la modifica delle tabelle delle pagine.

      Ciò impedisce a un processo di vedere la memoria di altri processi per motivi di sicurezza / facilità di programmazione.

    • impossibile registrare i gestori di interrupt. Questi sono configurati scrivendo nelle posizioni di memoria, cosa che è impedita anche dal paging.

      I gestori funzionano nell'anello 0 e rompono il modello di sicurezza.

      In altre parole, non è possibile utilizzare le istruzioni LGDT e LIDT.

    • non può eseguire istruzioni IO come ine out, quindi avere accessi hardware arbitrari.

      Altrimenti, ad esempio, le autorizzazioni sui file sarebbero inutili se un programma potesse leggere direttamente dal disco.

      Più precisamente grazie a Michael Petch : in realtà è possibile che il sistema operativo consenta istruzioni IO sull'anello 3, che in realtà è controllato dal segmento di stato Task .

      Ciò che non è possibile è che l'anello 3 si autorizzi a farlo se non lo avesse in primo luogo.

      Linux non lo consente sempre. Vedi anche: https://stackoverflow.com/questions/2711044/why-doesnt-linux-use-the-hardware-context-switch-via-the-tss

In che modo programmi e sistemi operativi passano da un anello all'altro?

  • quando la CPU è accesa, inizia a eseguire il programma iniziale nell'anello 0 (bene, ma è una buona approssimazione). Puoi pensare che questo programma iniziale sia il kernel (ma normalmente è un bootloader che chiama il kernel ancora nell'anello 0).

  • quando un processo userland vuole che il kernel faccia qualcosa per esso come scrivere su un file, usa un'istruzione che genera un interruzione come int 0x80osyscall per segnalare il kernel. x86-64 ciao syscall Linux esempio del mondo:

    .data
    hello_world:
        .ascii "hello world\n"
        hello_world_len = . - hello_world
    .text
    .global _start
    _start:
        /* write */
        mov $1, %rax
        mov $1, %rdi
        mov $hello_world, %rsi
        mov $hello_world_len, %rdx
        syscall
    
        /* exit */
        mov $60, %rax
        mov $0, %rdi
        syscall
    

    compilare ed eseguire:

    as -o hello_world.o hello_world.S
    ld -o hello_world.out hello_world.o
    ./hello_world.out
    

    GitHub a monte .

    Quando ciò accade, la CPU chiama un gestore di callback di interrupt che il kernel ha registrato all'avvio. Ecco un esempio concreto di baremetal che registra un gestore e lo utilizza .

    Questo gestore viene eseguito nell'anello 0, che decide se il kernel consentirà questa azione, eseguirà l'azione e riavvierà il programma userland nell'anello 3. x86_64

  • quando execviene utilizzata la chiamata di sistema (o quando verrà avviato/init il kernel ), il kernel prepara i registri e la memoria del nuovo processo userland, quindi salta al punto di ingresso e commuta la CPU sul ring 3

  • Se il programma cerca di fare qualcosa di cattivo come scrivere su un registro proibito o un indirizzo di memoria (a causa del paging), la CPU chiama anche un gestore di callback del kernel nell'anello 0.

    Ma poiché la zona dell'utente era cattiva, questa volta il kernel potrebbe terminare il processo o dare un avviso con un segnale.

  • Quando il kernel si avvia, imposta un clock hardware con una certa frequenza fissa, che genera periodicamente interruzioni.

    Questo orologio hardware genera interrupt che eseguono l'anello 0 e gli consente di pianificare i processi di utenteland da riattivare.

    In questo modo, la pianificazione può avvenire anche se i processi non effettuano chiamate di sistema.

Qual è il punto di avere più anelli?

Ci sono due principali vantaggi nel separare kernel e userland:

  • è più facile creare programmi poiché sei più sicuro che uno non interferisca con l'altro. Ad esempio, un processo di area utente non deve preoccuparsi di sovrascrivere la memoria di un altro programma a causa del paging, né di mettere l'hardware in uno stato non valido per un altro processo.
  • è più sicuro. Ad esempio, le autorizzazioni per i file e la separazione della memoria potrebbero impedire a un'app di hacking di leggere i dati bancari. Ciò suppone, ovviamente, che ti fidi del kernel.

Come giocarci?

Ho creato un setup bare metal che dovrebbe essere un buon modo per manipolare direttamente gli anelli: https://github.com/cirosantilli/x86-bare-metal-examples

Purtroppo non ho avuto la pazienza di fare un esempio di userland, ma sono arrivato al punto di impostazione della paginazione, quindi userland dovrebbe essere fattibile. Mi piacerebbe vedere una richiesta pull.

In alternativa, i moduli del kernel Linux vengono eseguiti nell'anello 0, quindi è possibile utilizzarli per provare operazioni privilegiate, ad esempio leggere i registri di controllo: https://stackoverflow.com/questions/7415515/how-to-access-the-control-registers -cr0-CR2-cr3-da-un-programma-ottenere-segmenta / 7.419.306 7.419.306 #

Ecco una comoda configurazione QEMU + Buildroot per provarlo senza uccidere il tuo host.

L'aspetto negativo dei moduli del kernel è che altri kthread sono in esecuzione e potrebbero interferire con i tuoi esperimenti. Ma in teoria puoi assumere tutti i gestori di interrupt con il tuo modulo kernel e possedere il sistema, in realtà sarebbe un progetto interessante.

Anelli negativi

Sebbene gli anelli negativi non siano effettivamente citati nel manuale di Intel, in realtà ci sono modalità CPU che hanno ulteriori capacità rispetto allo stesso anello 0, e quindi si adattano bene al nome di "anello negativo".

Un esempio è la modalità hypervisor utilizzata nella virtualizzazione.

Per ulteriori dettagli consultare: https://security.stackexchange.com/questions/129098/what-is-protection-ring-1

BRACCIO

In ARM, invece, gli anelli sono chiamati Livelli di eccezione, ma le idee principali rimangono le stesse.

Esistono 4 livelli di eccezione in ARMv8, comunemente usati come:

  • EL0: userland

  • EL1: kernel ("supervisore" nella terminologia ARM).

    Inserito con l' svcistruzione (SuperVisor Call), precedentemente nota come swi prima dell'assemblaggio unificato , che è l'istruzione utilizzata per effettuare chiamate di sistema Linux. Ciao esempio mondiale ARMv8:

    .text
    .global _start
    _start:
        /* write */
        mov x0, 1
        ldr x1, =msg
        ldr x2, =len
        mov x8, 64
        svc 0
    
        /* exit */
        mov x0, 0
        mov x8, 93
        svc 0
    msg:
        .ascii "hello syscall v8\n"
    len = . - msg
    

    GitHub a monte .

    Provalo con QEMU su Ubuntu 16.04:

    sudo apt-get install qemu-user gcc-arm-linux-gnueabihf
    arm-linux-gnueabihf-as -o hello.o hello.S
    arm-linux-gnueabihf-ld -o hello hello.o
    qemu-arm hello
    

    Ecco un esempio concreto di baremetal che registra un gestore SVC ed esegue una chiamata SVC .

  • EL2: hypervisor , ad esempio Xen .

    Inserito con l' hvcistruzione (HyperVisor Call).

    Un hypervisor è per un sistema operativo, ciò che un sistema operativo è per l'utente.

    Ad esempio, Xen consente di eseguire più sistemi operativi come Linux o Windows sullo stesso sistema allo stesso tempo e isola i sistemi operativi tra loro per sicurezza e facilità di debug, proprio come Linux fa per i programmi utente.

    Gli hypervisor sono una parte fondamentale dell'infrastruttura cloud odierna: consentono l'esecuzione di più server su un singolo hardware, mantenendo l'utilizzo dell'hardware sempre vicino al 100% e risparmiando un sacco di soldi.

    AWS, ad esempio, ha utilizzato Xen fino al 2017, quando il suo passaggio a KVM ha fatto notizia .

  • EL3: ancora un altro livello. Esempio TODO.

    Inserito con l' smcistruzione (Secure Mode Call)

Il modello di riferimento dell'architettura ARMv8 DDI 0487C.a - Capitolo D1 - Il modello del programmatore a livello di sistema AArch64 - La figura D1-1 illustra in modo meraviglioso:

inserisci qui la descrizione dell'immagine

Nota come ARM, forse a causa del vantaggio del senno di poi, ha una convenzione di denominazione migliore per i livelli di privilegio rispetto a x86, senza la necessità di livelli negativi: 0 è il più basso e 3 il più alto. I livelli più alti tendono ad essere creati più spesso di quelli inferiori.

L'attuale EL può essere interrogato con l' MRSistruzione: https://stackoverflow.com/questions/31787617/what-is-the-current-execution-mode-exception-level-etc

ARM non richiede la presenza di tutti i livelli di eccezione per consentire implementazioni che non richiedono la funzionalità per salvare l'area del chip. ARMv8 "Livelli di eccezione" dice:

Un'implementazione potrebbe non includere tutti i livelli di eccezione. Tutte le implementazioni devono includere EL0 ed EL1. EL2 ed EL3 sono opzionali.

Ad esempio, QEMU utilizza EL1 per impostazione predefinita, ma EL2 ed EL3 possono essere abilitati con le opzioni della riga di comando: https://stackoverflow.com/questions/42824706/qemu-system-aarch64-entering-el1-when-emulating-a53-power-up

Frammenti di codice testati su Ubuntu 18.10.


3

Se è il primo, significa che il normale programma utente non può avere più di 3 GB di memoria (se la divisione è 3 GB + 1 GB)?

Sì, questo è il caso di un normale sistema Linux. C'erano un insieme di patch "4G / 4G" che fluttuavano intorno a un punto che rendevano gli spazi degli indirizzi utente e kernel completamente indipendenti (a un costo prestazionale perché rendeva più difficile l'accesso alla memoria utente da parte del kernel) ma non credo furono sempre fusi a monte e l'interesse scemò con l'ascesa di x86-64

Inoltre, in quel caso come può il kernel utilizzare la memoria elevata, perché a quale indirizzo di memoria virtuale verranno mappate le pagine dalla memoria alta, poiché 1GB di spazio del kernel verrà mappato logicamente?

Il modo in cui Linux funzionava (e funziona ancora su sistemi in cui la memoria è piccola rispetto allo spazio degli indirizzi) era che l'intera memoria fisica era mappata permanentemente nella parte del kernel dello spazio degli indirizzi. Ciò ha permesso al kernel di accedere a tutta la memoria fisica senza rimappare, ma chiaramente non si adatta a macchine a 32 bit con molta memoria fisica.

Così è nato il concetto di memoria bassa e alta. la memoria "bassa" viene mappata in modo permanente nello spazio degli indirizzi dei kernel. memoria "alta" non lo è.

Quando il processore esegue una chiamata di sistema, è in esecuzione in modalità kernel ma nel contesto del processo corrente. Quindi può accedere direttamente sia allo spazio degli indirizzi del kernel sia allo spazio degli indirizzi utente del processo corrente (supponendo che non si stiano utilizzando le summenzionate patch 4G / 4G). Ciò significa che non è un problema per l'allocazione della memoria "alta" a un processo utente.

L'uso della memoria "alta" per scopi del kernel è più un problema. Per accedere a memoria elevata che non è mappata al processo corrente, deve essere mappata temporaneamente nello spazio degli indirizzi del kernel. Ciò significa un codice aggiuntivo e una penalità di prestazione.

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.