Microkerneluri vs kerneluri monolitice: compromisuri revizitate
Puține dezbateri de proiectare a sistemelor de operare au durat la fel de mult ca dezbaterea dintre microkerneluri și kerneluri monolitice.
La suprafață, diferența pare simplă:
- kernelurile monolitice păstrează majoritatea serviciilor sistemului de operare în interiorul kernelului
- microkernelurile mută majoritatea serviciilor în spațiul utilizator
În practică, compromisul este mai subtil.
Întrebarea reală nu este dacă o structură este universal mai rapidă, mai curată sau mai sigură. Întrebarea reală este unde ar trebui să trăiască autoritatea, complexitatea, eșecul și costurile de performanță.
Această postare revizitează acel compromis, explică de ce multe argumente vechi despre microkerneluri au fost simplificate excesiv și arată de ce sisteme moderne precum EriX fac din nou practic modelul microkernel.
Forma istorică a dezbaterii⌗
Primele sisteme de operare au fost construite sub constrângeri hardware severe.
Memoria era limitată. CPU-urile erau mai lente. Schimbările de context erau costisitoare. Cache-urile, TLB-urile, sistemele multiprocesor și căile rapide de syscall erau mult mai puțin capabile decât sunt astăzi.
În aceste condiții, kernelurile monolitice erau o alegere naturală.
Sistemele de tip Unix plasau sisteme de fișiere, drivere de dispozitive, stive de rețea, gestionarea proceselor și multe alte servicii într-un singur spațiu de adrese privilegiat al kernelului. Această proiectare făcea multe operații ieftine:
- un sistem de fișiere putea apela direct stratul de blocuri
- o stivă de rețea putea accesa direct structurile driverului
- subsistemele kernelului puteau partaja date fără IPC
Rezultatul era eficient și pragmatic.
Însemna, de asemenea, că o cantitate mare de cod rula cu privilegii complete de kernel.
De ce au apărut microkernelurile⌗
Microkernelurile au pornit de la o observație diferită:
Cea mai mare parte a codului unui sistem de operare nu are nevoie de autoritate completă asupra mașinii.
Un sistem de fișiere nu trebuie să modifice tabele de pagini arbitrare. Un driver de tastatură nu trebuie să acceseze fiecare proces. O stivă de rețea nu trebuie să controleze planificatorul.
Microkernelurile păstrează în kernel doar mecanismele cele mai fundamentale, de obicei:
- planificare
- gestionarea spațiilor de adrese
- comunicare între procese
- gestionarea capabilităților sau a handle-urilor
- livrarea întreruperilor și excepțiilor
Serviciile de nivel mai înalt rulează ca procese obișnuite în spațiul utilizator.
Acest lucru oferă sistemului o izolare mai puternică. Un crash al unui driver nu trebuie să fie un crash al kernelului. Un bug într-un sistem de fișiere nu devine automat corupere arbitrară a memoriei kernelului. Autoritatea poate fi distribuită mai precis.
Ideea era convingătoare, dar implementările timpurii au avut adesea probleme de performanță și compatibilitate.
Prima problemă de performanță⌗
Critica clasică la adresa microkernelurilor este că sunt lente.
Această critică nu a apărut din senin.
Unele sisteme microkernel timpurii plasau servicii tradiționale ale sistemului de operare în spatele multor servere separate în spațiul utilizator, apoi încercau să păstreze deasupra interfețe Unix familiare. O operație simplă putea deveni un lanț de mesaje:
- aplicație către serverul de fișiere
- serverul de fișiere către managerul de memorie
- managerul de memorie către pager
- pagerul către serviciul de blocuri
- serviciul de blocuri către driver
Fiecare pas putea implica o schimbare de context, validarea mesajului, o decizie de planificare și uneori copiere.
Dacă interfețele sunt prea conversaționale, costul se acumulează.
Greșeala a fost transformarea acestui lucru într-o regulă universală:
Microkernelurile sunt lente.
O regulă mai precisă este:
Căile IPC prost proiectate și granițele de serviciu prea conversaționale sunt lente.
Această distincție contează.
Calea rapidă monolitică⌗
Kernelurile monolitice pot fi extrem de rapide deoarece evită multe granițe de protecție.
Un sistem de fișiere din kernel poate apela un strat de blocuri din kernel cu un apel de funcție normal. Un driver poate partaja memorie direct cu alt subsistem. Nu este nevoie să serializeze fiecare cerere într-un format de mesaj.
Acesta este un avantaj real.
Dar nu este gratuit.
Calea rapidă monolitică vine adesea cu:
- mai mult cod privilegiat
- mai multă stare mutabilă partajată
- mai multă complexitate a blocărilor interne din kernel
- mai multe moduri în care un subsistem îl poate corupe pe altul
- o bază de calcul de încredere mai mare
Performanța nu înseamnă doar numărarea instrucțiunilor. Înseamnă și comportamentul cache-ului, contencția pe blocări, izolarea eșecurilor, recuperarea și costul menținerii corectitudinii în timp.
Un kernel monolitic poate câștiga un microbenchmark brut și totuși poate face izolarea și auditabilitatea mai dificile.
Mit de performanță: fiecare graniță este fatală⌗
Un mit comun este că fiecare graniță de microkernel este atât de costisitoare încât proiectarea nu poate concura.
Această viziune este depășită.
O graniță are un cost, dar sistemele moderne pot face acel cost gestionabil:
- căi rapide de syscall și retur
- euristici de planificare mai bune
- căi de date cu memorie partajată
- mapare de pagini în locul copierii masive
- cereri grupate
- livrare asincronă a evenimentelor
- ABI-uri IPC proiectate cu atenție
Scopul important de proiectare este păstrarea politicii în afara kernelului fără a forța fiecare octet de date să treacă prin kernel.
Kernelul trebuie să medieze autoritatea. Nu trebuie neapărat să mute toate datele.
Mit de performanță: IPC înseamnă copierea tuturor datelor⌗
IPC este adesea imaginat ca “copiază tot acest buffer din procesul A în procesul B”.
Acesta este doar un design posibil.
Un microkernel poate transmite mesaje mici de control în timp ce transferă autoritate asupra memoriei partajate, frame-urilor, endpointurilor sau obiectelor de dispozitiv. Calea de date costisitoare poate rămâne mapată, în timp ce kernelul doar validează cine are voie să o acceseze.
Acest lucru este central pentru proiectarea bazată pe capabilități.
În loc să copieze structuri mari de date printr-un subsistem privilegiat, un proces poate primi o capabilitate care autorizează accesul la un obiect specific cu drepturi specifice.
Kernelul rămâne responsabil pentru aplicarea transferului. Nu trebuie să înțeleagă fiecare protocol de nivel înalt construit peste acel transfer.
Mit de performanță: driverele în spațiul utilizator nu sunt practice⌗
Driverele în spațiul utilizator sunt tratate adesea ca o idee de cercetare.
Îngrijorarea este de înțeles. Accesul la hardware este sensibil, întreruperile sunt sensibile la timp, iar driverele se află adesea pe căi fierbinți.
Dar majoritatea driverelor nu au nevoie de autoritate completă de kernel.
Un driver are de obicei nevoie de acces la:
- un interval specific de porturi I/O
- o regiune MMIO specifică
- o linie de întrerupere specifică
- un aranjament specific de DMA sau buffere
Acestea sunt forme de autoritate mai înguste decât “întregul kernel”.
Dacă kernelul poate delega exact aceste resurse, un driver poate rula în afara kernelului și totuși poate face muncă utilă. Dacă eșuează, sistemul are șansa să oprească, să repornească sau să înlocuiască acel driver fără a trata eșecul ca pe o corupere a memoriei kernelului.
Compromisul este real: driverele în spațiul utilizator au nevoie de IPC bun, livrare atentă a întreruperilor și proprietate explicită asupra resurselor. Dar modelul nu este inerent nepractic.
Ce pune EriX în kernel⌗
EriX este proiectat ca un microkernel bazat pe capabilități.
Kernelul EriX este intenționat minim în politică. Documentele sale de arhitectură definesc kernelul ca responsabil pentru:
- validarea transferului de la bootloader la kernel
- gestionarea obiectelor de bază ale kernelului și semantica capabilităților
- crearea taskului root
- expunerea punctelor de intrare pentru trap, syscall și întreruperi
Kernelul nu este explicit responsabil pentru:
- politica sistemului
- politica de orchestrare a proceselor
- politica de memorie de nivel înalt
- politica ciclului de viață al serviciilor
Aceasta este linia microkernelului în practică.
Kernelul pornește cu autoritate asupra mașinii, dar trebuie să convertească acea autoritate în obiecte explicite de kernel și referințe de capabilitate. Nicio autoritate ambientală nu trebuie să se scurgă în spațiul utilizator.
Ce mută EriX în afara kernelului⌗
EriX plasează funcționalitatea care poartă politică în servicii din spațiul utilizator.
De exemplu:
rootdeste prima autoritate din spațiul utilizator care poartă politicăprocddeține gestionarea ciclului de viață al proceselordeviceddeține politica driverelor și orchestrarea pornirii lorvfsddeține spațiul de nume public al sistemului de fișiere- furnizori de sisteme de fișiere precum
ramfsd,e2fsdșifatdrămân perechi backend private în spatele luivfsd
Aceasta nu este doar “mutarea codului în afara kernelului” ca alegere estetică.
Fiecare graniță de serviciu definește o graniță de autoritate.
rootd distribuie capabilități de pornire cu privilegiu minim. procd creează
și pornește procese prin crearea de copii în etape și granturi de instalare.
deviced nu devine direct kernelul; îi cere lui procd să gestioneze procesele
de driver și transmite doar autoritatea de driver necesară pentru fiecare rol.
Această structură este mai verbosă decât un graf de apeluri de kernel monolitic, dar face vizibil fluxul de autoritate.
Autoritate îngustă în locul privilegiului larg⌗
Unul dintre cele mai importante detalii de implementare din EriX este îndepărtarea de un endpoint root larg ca suprafață normală de control la runtime.
Kernelul actual expune familii înguste de endpointuri de control kernel pentru sarcini specifice:
- controlul timpului
- controlul întreruperilor
- evenimente hotplug
- citiri de configurație PCI
- acces la consolă și framebuffer
- I/O COM1
- I/O i8042
- retyping de memorie
- mapare VSpace
- rezolvarea faulturilor pagerului
- controlul proceselor
- citiri ACPI
Dispatch-ul la runtime este determinat de obiectul endpoint și de tipul lui, nu de un număr global privilegiat de slot.
Acest lucru contează deoarece un task nu obține autoritate doar cunoscând o valoare convențională de slot. Trebuie să dețină efectiv capabilitatea corectă în propriul spațiu local de capabilități.
De exemplu, drv-serial primește autoritate I/O specifică COM1. drv-i8042
primește autoritate I/O specifică i8042. drv-acpi primește autoritate de
citire ACPI. probed primește autoritate de citire a configurației PCI.
Aceasta este o formă de securitate diferită de plasarea tuturor acestor operații în spatele unui singur handle larg de kernel.
Memoria de dispozitiv ca obiect explicit⌗
EriX tratează și autoritatea asupra memoriei de dispozitiv ca explicită și tipată.
Kernelul are un CAP_TYPE_DEVICE_FRAME distinct pentru memoria de dispozitiv
validată. În calea de stocare, un frame MMIO bazat pe BAR poate fi derivat
pentru deviced, iar deviced poate instala apoi doar acel frame de dispozitiv
derivat în pachetul de pornire în etape al driverului.
Ideea nu este că driverele de dispozitiv devin simple.
Ideea este că autoritatea MMIO nu este confundată cu frame-uri RAM obișnuite și nu este expusă printr-o scăpare generică de tip “fă orice cu memoria de dispozitiv”.
Acesta este exact tipul de detaliu care face microkernelurile moderne viabile: accesul la hardware este delegat ca un obiect precis, cu drepturi precise.
IPC ca ABI, nu ca accident⌗
Într-un kernel monolitic, multe interfețe interne sunt apeluri de funcție obișnuite.
Într-un microkernel, IPC devine parte din ABI-ul sistemului. Asta îl face mai important, nu mai puțin important.
EriX tratează IPC ca pe un contract comun:
- anteturile mesajelor sunt versionate
- layouturile sunt fixe
- parsarea folosește aritmetică verificată
- payloadurile malformate eșuează închis
- transferurile de capabilități sunt explicite
- mesajele runtime care poartă transferuri necesită
GRANT
Aceasta este opusul tratării IPC ca o idee adăugată ulterior.
Costul IPC este controlat parțial prin implementare, dar și prin proiectarea interfețelor. Un ABI proiectat atent evită drumuri dus-întors inutile, menține mesajele limitate și separă transferul de control de mutarea datelor.
De ce microkernelurile sunt viabile din nou⌗
Microkernelurile sunt mai viabile astăzi din mai multe motive.
1. Hardware-ul s-a schimbat⌗
Costul relativ al unei granițe de protecție s-a schimbat.
Schimbările de context și syscallurile încă nu sunt gratuite, dar CPU-urile moderne, sistemele de memorie și mecanismele de întrerupere fac costul brut mai puțin decisiv decât era când au fost judecate primele experimente microkernel.
În același timp, sistemele moderne sunt mai complexe și mai expuse. Costul compromiterii kernelului a crescut.
Izolarea este acum mai valoroasă.
2. Înțelegem IPC mai bine⌗
Lecția sistemelor anterioare nu este “evită IPC”.
Lecția este:
- evită IPC inutil
- evită protocoalele prea conversaționale
- evită copierea datelor mari când transferul de autoritate este suficient
- proiectează granițe de serviciu în jurul proprietății reale
Microkernelurile sunt viabile când IPC este tratat ca o problemă de proiectare de prim rang.
3. Capabilitățile fac granițele utile⌗
Mutarea codului în spațiul utilizator este doar jumătate din poveste.
Dacă fiecare server din spațiul utilizator primește încă privilegiu implicit larg, sistemul a recreat în mare un monolit cu schimbări de context în plus.
Capabilitățile dau sens graniței.
În EriX, autoritatea este reprezentată de capabilități tipate cu drepturi explicite. Serviciile validează capabilitățile pe care le primesc. Pachetele de pornire descriu autoritatea declarată. Codul kernelului și al serviciilor evită să trateze numerele canonice de slot ca permisiune ambientală.
Asta face ca descompunerea să fie mai mult decât modularitate. O face parte din modelul de securitate.
4. Limbajele și uneltele s-au îmbunătățit⌗
Limbajele moderne de implementare și uneltele moderne schimbă și ele compromisul.
Rust nu elimină bugurile de sistem de operare, dar face mai dificilă scrierea accidentală a multor erori de siguranță a memoriei. De asemenea, face vizibile granițele unsafe în timpul revizuirii.
Pentru un sistem microkernel, acest lucru este deosebit de util. Kernelul poate rămâne mic și auditabil, în timp ce serviciile din spațiul utilizator pot fi scrise cu garanții de siguranță mai puternice decât componentele de sistem tradiționale dominate de C.
EriX combină acest lucru cu o abordare clean-room și fără crate-uri terțe, ceea ce menține sistemul mai ușor de auditat chiar dacă mărește efortul de implementare.
Costurile rămase⌗
Microkernelurile au încă costuri reale.
Ele necesită:
- logică de pornire mai explicită
- contracte IPC versionate cu grijă
- supraveghere robustă a serviciilor
- mai multă atenție la batching și mutarea datelor
- proprietate clară asupra fiecărei capabilități
- tracing bun și măsurarea performanței
Ele mută, de asemenea, o parte din complexitate în afara kernelului, în loc să o șteargă.
rootd, procd, deviced și serviciile de sistem de fișiere încă au nevoie de
proiectare atentă. Ele pot fi în afara kernelului, dar pot fi totuși componente
de încredere pentru anumite părți ale sistemului.
Diferența este că autoritatea lor poate fi mai îngustă decât autoritatea kernelului, iar eșecurile lor pot fi conținute mai deliberat.
Compromisul revizitat⌗
Încadrarea veche era adesea:
- kernelurile monolitice sunt rapide
- microkernelurile sunt curate, dar lente
Această încadrare este prea simplă.
O încadrare mai bună este:
- kernelurile monolitice optimizează cooperarea directă în kernel
- microkernelurile optimizează autoritatea explicită și izolarea eșecurilor
- oricare dintre proiectări poate fi rapidă sau lentă în funcție de implementare
- oricare dintre proiectări poate deveni complexă dacă granițele sunt alese prost
Pentru EriX, alegerea microkernelului decurge din obiectivele sistemului:
- bază de calcul de încredere minimă
- autoritate explicită prin capabilități
- separare strictă între kernel și spațiul utilizator
- granițe de serviciu auditabile
- bootstrap și comportament de eșec deterministe
Aceste obiective nu fac performanța irelevantă.
Ele definesc unde trebuie să aibă loc munca de performanță: IPC rapid, interfețe de serviciu atente, căi de date cu memorie partajată, familii înguste de endpointuri și transfer explicit de capabilități.
Privind înainte⌗
Microkernelurile nu sunt o scurtătură.
Ele cer mai multă disciplină de proiectare inițială decât un simplu graf de apeluri în kernel. Forțează sistemul să definească devreme autoritatea, proprietatea și comportamentul la eșec.
Tocmai de aceea sunt interesante.
EriX folosește modelul microkernel nu pentru că este la modă, ci pentru că se potrivește arhitecturii: un kernel mic, autoritate mediată de capabilități și politică implementată prin servicii explicite în spațiul utilizator.
Următoarea postare va examina ideea care motivează mare parte din această structură: baza de calcul de încredere.
Vom vedea ce include de fapt TCB, de ce dimensiunea ei afectează suprafața de atac și cum EriX încearcă să păstreze codul de încredere mic mutând politica în servicii explicite din spațiul utilizator, constrânse prin capabilități.