Få debatter inom operativsystemsdesign har pågått lika länge som debatten mellan mikrokärnor och monolitiska kärnor.

På ytan verkar skillnaden enkel:

  • monolitiska kärnor behåller de flesta operativsystemstjänster i kärnan
  • mikrokärnor flyttar de flesta tjänster till användarrymden

I praktiken är kompromissen mer subtil.

Den verkliga frågan är inte om den ena strukturen alltid är snabbare, renare eller säkrare. Den verkliga frågan är var auktoritet, komplexitet, fel och prestandakostnader ska ligga.

Det här inlägget går tillbaka till den kompromissen, förklarar varför många äldre argument om mikrokärnor blev alltför förenklade och visar varför moderna system som EriX gör mikrokärnemodellen praktisk igen.


Debattens historiska form

Tidiga operativsystem byggdes under hårda hårdvarubegränsningar.

Minnet var begränsat. CPU:erna var långsammare. Kontextbyten var dyra. Cacher, TLB:er, multiprocessorsystem och snabba syscall-vägar var mycket mindre kapabla än de är i dag.

Under dessa begränsningar var monolitiska kärnor ett naturligt val.

Unix-liknande system placerade filsystem, drivrutiner, nätverksstackar, processhantering och många andra tjänster i ett enda privilegierat adressutrymme i kärnan. Den designen gjorde många operationer billiga:

  • ett filsystem kunde anropa blocklagret direkt
  • en nätverksstack kunde komma åt drivrutinens strukturer direkt
  • kärnans delsystem kunde dela data utan IPC

Resultatet var effektivt och pragmatiskt.

Det betydde också att stora mängder kod kördes med fulla kärnprivilegier.


Varför mikrokärnor uppstod

Mikrokärnor kom ur en annan observation:

Det mesta av operativsystemskoden behöver inte full maskinauktoritet.

Ett filsystem behöver inte ändra godtyckliga sidtabeller. En tangentbordsdriver behöver inte komma åt varje process. En nätverksstack behöver inte styra schemaläggaren.

Mikrokärnor behåller bara de mest grundläggande mekanismerna i kärnan, vanligen:

  • schemaläggning
  • hantering av adressutrymmen
  • interprocesskommunikation
  • hantering av kapabiliteter eller handtag
  • leverans av avbrott och undantag

Högre nivåers tjänster körs som vanliga processer i användarrymden.

Det ger systemet starkare isolering. En drivrutinskrasch behöver inte bli en kärnkrasch. Ett fel i ett filsystem behöver inte automatiskt bli godtycklig minneskorruption i kärnan. Auktoritet kan delas ut mer exakt.

Idén var övertygande, men tidiga implementationer hade ofta problem med prestanda och kompatibilitet.


Det första prestandaproblemet

Den klassiska kritiken mot mikrokärnor är att de är långsamma.

Den kritiken kom inte från ingenstans.

Vissa tidiga mikrokärnesystem lade traditionella operativsystemstjänster bakom många separata servrar i användarrymden och försökte sedan bevara välbekanta Unix-gränssnitt ovanpå. En enkel operation kunde bli en kedja av meddelanden:

  1. applikation till filserver
  2. filserver till minneshanterare
  3. minneshanterare till pager
  4. pager till blocktjänst
  5. blocktjänst till drivrutin

Varje steg kunde innebära ett kontextbyte, meddelandevalidering, ett schemaläggningsbeslut och ibland kopiering.

Om gränssnitten är för pratiga växer kostnaden.

Misstaget var att göra detta till en universell regel:

Mikrokärnor är långsamma.

En mer korrekt regel är:

Dåligt designade IPC-vägar och alltför pratiga tjänstegränser är långsamma.

Den skillnaden spelar roll.


Den monolitiska snabbvägen

Monolitiska kärnor kan vara extremt snabba eftersom de undviker många skyddsgränser.

Ett filsystem i kärnan kan anropa ett blocklager i kärnan med ett vanligt funktionsanrop. En drivrutin kan dela minne direkt med ett annat delsystem. Det behövs ingen serialisering av varje begäran till ett meddelandeformat.

Detta är en verklig fördel.

Men den är inte gratis.

Den monolitiska snabbvägen kommer ofta med:

  • mer privilegierad kod
  • mer delat muterbart tillstånd
  • mer komplex intern kärnlåsning
  • fler sätt för ett delsystem att korrumpera ett annat
  • en större betrodd beräkningsbas

Prestanda handlar inte bara om instruktionsräkning. Det handlar också om cachebeteende, låskontention, felisolering, återhämtning och kostnaden för att bevara korrekthet över tid.

En monolitisk kärna kan vinna ett rått mikrobenchmark och ändå göra isolering och granskningsbarhet svårare.


Prestandamyt: varje gräns är dödlig

En vanlig myt är att varje mikrokärnegräns är så dyr att designen inte kan konkurrera.

Den synen är föråldrad.

En gräns har en kostnad, men moderna system kan göra den kostnaden hanterbar:

  • snabba syscall- och returvägar
  • bättre schemaläggningsheuristik
  • datavägar med delat minne
  • sidmappning i stället för masskopiering
  • batchade begäranden
  • asynkron händelseleverans
  • noggrant designade IPC-ABI:er

Det viktiga designmålet är att hålla policy utanför kärnan utan att tvinga varje byte data genom kärnan.

Kärnan ska mediera auktoritet. Den behöver inte nödvändigtvis flytta all data.


Prestandamyt: IPC betyder att allt kopieras

IPC föreställs ofta som “kopiera hela denna buffert från process A till process B”.

Det är bara en möjlig design.

En mikrokärna kan skicka små kontrollmeddelanden samtidigt som den överför auktoritet till delat minne, frames, endpoints eller enhetsobjekt. Den dyra datavägen kan förbli mappad, medan kärnan bara validerar vem som får komma åt den.

Detta är centralt i kapabilitetsbaserad design.

I stället för att kopiera stora datastrukturer genom ett privilegierat delsystem kan en process få en kapabilitet som auktoriserar åtkomst till ett specifikt objekt med specifika rättigheter.

Kärnan är fortfarande ansvarig för att genomdriva överföringen. Den behöver inte förstå varje högnivåprotokoll som byggs ovanpå den överföringen.


Prestandamyt: drivrutiner i användarrymden är inte praktiska

Drivrutiner i användarrymden behandlas ofta som en forskningsidé.

Oron är begriplig. Hårdvaruåtkomst är känslig, avbrott är tidskänsliga och drivrutiner ligger ofta på heta vägar.

Men de flesta drivrutiner behöver inte full kärnauktoritet.

En drivrutin behöver vanligtvis åtkomst till:

  • ett specifikt intervall av I/O-portar
  • en specifik MMIO-region
  • en specifik avbrottslinje
  • ett specifikt DMA- eller buffertarrangemang

Detta är smalare former av auktoritet än “hela kärnan”.

Om kärnan kan delegera exakt dessa resurser kan en drivrutin köras utanför kärnan och ändå göra nyttigt arbete. Om den misslyckas har systemet en chans att stoppa, starta om eller ersätta den drivrutinen utan att behandla felet som minneskorruption i kärnan.

Kompromissen är verklig: drivrutiner i användarrymden behöver bra IPC, noggrann avbrottsleverans och explicit resursägande. Men modellen är inte i sig opraktisk.


Vad EriX placerar i kärnan

EriX är designat som en kapabilitetsmikrokärna.

EriX-kärnan är avsiktligt policyminimal. Dess arkitekturdokument definierar kärnan som ansvarig för att:

  • validera överlämningen från bootloader till kernel
  • hantera grundläggande kärnobjekt och kapabilitetssemantik
  • skapa root-uppgiften
  • exponera ingångspunkter för traps, syscalls och avbrott

Kärnan är uttryckligen inte ansvarig för:

  • systempolicy
  • processorkestreringspolicy
  • högnivåpolicy för minne
  • policy för tjänsters livscykel

Detta är mikrokärnelinjen i praktiken.

Kärnan börjar med maskinauktoritet, men den måste omvandla den auktoriteten till explicita kärnobjekt och kapabilitetsreferenser. Ingen ambient auktoritet ska läcka in i användarrymden.


Vad EriX flyttar ut ur kärnan

EriX placerar policybärande funktionalitet i tjänster i användarrymden.

Till exempel:

  • rootd är den första policybärande auktoriteten i användarrymden
  • procd äger processlivscykelhanteringen
  • deviced äger drivrutinspolicy och drivrutinsstartens orkestrering
  • vfsd äger det publika filsystemsnamnrymden
  • filsystemsleverantörer som ramfsd, e2fsd och fatd förblir privata backend-peers bakom vfsd

Detta är inte bara “flytta kod ut ur kärnan” som ett estetiskt val.

Varje tjänstegräns definierar en auktoritetsgräns.

rootd delar ut startkapabiliteter med minsta privilegium. procd skapar och startar processer genom staged child creation och install grants. deviced blir inte direkt kärnan; den ber procd hantera drivrutinsprocesser och skickar bara den drivrutinsauktoritet som krävs för varje roll.

Den strukturen är mer utförlig än en monolitisk kärnas anropsgraf, men den gör auktoritetsflödet synligt.


Smal auktoritet i stället för brett privilegium

En av de viktigaste implementationdetaljerna i EriX är övergången bort från ett brett root-endpoint som normal kontrolleryta vid körning.

Den nuvarande kärnan exponerar smala familjer av kernel-control-endpoints för specifika jobb:

  • tidskontroll
  • avbrottskontroll
  • hotplug-händelser
  • PCI-konfigurationsläsningar
  • konsol- och framebufferåtkomst
  • COM1-I/O
  • i8042-I/O
  • memory retyping
  • VSpace-mappning
  • lösning av pager-fel
  • processkontroll
  • ACPI-läsningar

Körningsdispatch nycklas av endpoint-objektet och dess typ, inte av ett privilegierat globalt slotnummer.

Det spelar roll eftersom en uppgift inte får auktoritet bara genom att känna till ett konventionellt slotvärde. Den måste faktiskt ha rätt kapabilitet i sitt eget lokala kapabilitetsutrymme.

Till exempel får drv-serial COM1-specifik I/O-auktoritet. drv-i8042 får i8042-specifik I/O-auktoritet. drv-acpi får ACPI-läsauktoritet. probed får PCI-konfigurationsläsauktoritet.

Det är en annan säkerhetsform än att placera alla dessa operationer bakom ett enda brett kärnhandtag.


Enhetsminne som ett explicit objekt

EriX behandlar också auktoritet över enhetsminne som explicit och typad.

Kärnan har ett separat CAP_TYPE_DEVICE_FRAME för validerat enhetsminne. I lagringsvägen kan en BAR-stödd MMIO-frame härledas för deviced, och deviced kan sedan installera bara den härledda enhetsframen i drivrutinens staged startup-bundle.

Poängen är inte att enhetsdrivrutiner blir enkla.

Poängen är att MMIO-auktoritet inte blandas ihop med vanliga RAM-frames och inte exponeras genom en generisk flyktväg för att “göra vad som helst med enhetsminne”.

Detta är precis den typ av detalj som gör moderna mikrokärnor praktiska: hårdvaruåtkomst delegeras som ett precist objekt med precisa rättigheter.


IPC som ABI, inte som olyckshändelse

I en monolitisk kärna är många interna gränssnitt vanliga funktionsanrop.

I en mikrokärna blir IPC en del av systemets ABI. Det gör den viktigare, inte mindre viktig.

EriX behandlar IPC som ett delat kontrakt:

  • meddelandehuvuden är versionerade
  • layouter är fasta
  • parsning använder kontrollerad aritmetik
  • felformade payloads misslyckas stängt
  • kapabilitetsöverföringar är explicita
  • runtime-meddelanden som bär överföringar kräver GRANT

Detta är motsatsen till att behandla IPC som en eftertanke.

Kostnaden för IPC styrs delvis av implementationen, men också av gränssnittsdesignen. Ett noggrant designat ABI undviker onödiga turer fram och tillbaka, håller meddelanden begränsade och separerar kontrollöverföring från dataförflyttning.


Varför mikrokärnor är praktiska igen

Mikrokärnor är mer praktiska i dag av flera skäl.

1. Hårdvaran har förändrats

Den relativa kostnaden för en skyddsgräns har förändrats.

Kontextbyten och syscalls är fortfarande inte gratis, men moderna CPU:er, minnessystem och avbrottsmekanismer gör den råa kostnaden mindre avgörande än när tidiga mikrokärneexperiment bedömdes.

Samtidigt är moderna system mer komplexa och mer exponerade. Kostnaden för en kärnkompromettering har ökat.

Isolering är mer värdefull nu.


2. Vi förstår IPC bättre

Lärdomen från tidigare system är inte “undvik IPC”.

Lärdomen är:

  • undvik onödig IPC
  • undvik alltför pratiga protokoll
  • undvik att kopiera stora data när auktoritetsöverföring räcker
  • designa tjänstegränser runt verkligt ägande

Mikrokärnor är praktiska när IPC behandlas som ett förstaklassigt designproblem.


3. Kapabiliteter gör gränser användbara

Att flytta kod till användarrymden är bara halva berättelsen.

Om varje användarrymdsserver fortfarande får brett implicit privilegium har systemet i stort sett återskapat en monolit med extra kontextbyten.

Kapabiliteter gör gränsen meningsfull.

I EriX representeras auktoritet av typade kapabiliteter med explicita rättigheter. Tjänster validerar kapabiliteterna de får. Startup-bundles beskriver deklarerad auktoritet. Kärn- och tjänstekod undviker att behandla kanoniska slotnummer som ambient behörighet.

Det gör nedbrytning till mer än modularitet. Det gör nedbrytning till en del av säkerhetsmodellen.


4. Språk och verktyg har förbättrats

Moderna implementationsspråk och verktyg ändrar också kompromissen.

Rust eliminerar inte operativsystemsbuggar, men det gör många minnessäkerhetsfel svårare att skriva av misstag. Det gör också unsafe-gränser synliga vid granskning.

För ett mikrokärnesystem är detta särskilt användbart. Kärnan kan förbli liten och granskningsbar, medan tjänster i användarrymden fortfarande kan skrivas med starkare säkerhetsgarantier än traditionella C-tunga systemkomponenter.

EriX kombinerar detta med ett clean-room-upplägg och inga tredjepartscrates, vilket gör systemet lättare att granska även om det ökar implementationsarbetet.


De återstående kostnaderna

Mikrokärnor har fortfarande verkliga kostnader.

De kräver:

  • mer explicit startlogik
  • noggrant versionerade IPC-kontrakt
  • robust tjänsteövervakning
  • mer tanke kring batching och dataförflyttning
  • tydligt ägande av varje kapabilitet
  • bra tracing och prestandamätning

De flyttar också en del komplexitet ut ur kärnan i stället för att ta bort den.

rootd, procd, deviced och filsystemstjänster behöver fortfarande noggrann design. De kan vara utanför kärnan, men de kan ändå vara betrodda komponenter för specifika delar av systemet.

Skillnaden är att deras auktoritet kan vara smalare än kärnauktoritet, och att deras fel kan isoleras mer avsiktligt.


Kompromissen igen

Den gamla inramningen var ofta:

  • monolitiska kärnor är snabba
  • mikrokärnor är rena men långsamma

Den inramningen är för enkel.

En bättre inramning är:

  • monolitiska kärnor optimerar för direkt samarbete inne i kärnan
  • mikrokärnor optimerar för explicit auktoritet och felisolering
  • båda designer kan vara snabba eller långsamma beroende på implementation
  • båda designer kan bli komplexa om gränserna väljs dåligt

För EriX följer mikrokärnevalet av systemets mål:

  • minimal betrodd beräkningsbas
  • explicit auktoritet genom kapabiliteter
  • strikt separation mellan kärna och användarrymd
  • granskningsbara tjänstegränser
  • deterministisk bootstrap och deterministiskt felbeteende

Dessa mål gör inte prestanda irrelevant.

De definierar var prestandaarbetet ska ske: snabb IPC, noggranna tjänstegränssnitt, datavägar med delat minne, smala endpoint-familjer och explicit kapabilitetsöverföring.


Framåt

Mikrokärnor är ingen genväg.

De kräver mer tidig designdisciplin än en enkel anropsgraf i kärnan. De tvingar systemet att tidigt definiera auktoritet, ägande och felbeteende.

Det är just därför de är intressanta.

EriX använder mikrokärnemodellen inte för att den är modern, utan för att den passar arkitekturen: en liten kärna, kapabilitetsmedierad auktoritet och policy implementerad av explicita tjänster i användarrymden.

Nästa inlägg kommer att undersöka idén som motiverar mycket av denna struktur: den betrodda beräkningsbasen.

Vi kommer att se vad TCB faktiskt omfattar, varför dess storlek påverkar attackytan och hur EriX försöker hålla betrodd kod liten genom att flytta policy till explicita, kapabilitetsbegränsade tjänster i användarrymden.