16/05/2021
Unix har i generationer indprentet en fundamental æstetik hos ingeniører for, hvordan man designer og strukturerer komplekse softwaresystemer. Kernen i denne filosofi er et lille sæt af kraftfulde ideer, kendt som abstraktioner. Når en programmør først har forstået disse grundlæggende koncepter, bliver det muligt at forstå resten af systemet og hvordan man effektivt kan løse opgaver. Denne artikel vil udforske de centrale abstraktioner, der gør Unix og dets efterfølgere som Linux så elegante og magtfulde, herunder den berømte idé om, at alt er en fil, hvordan processer styres, og hvordan programmer kan kommunikere med hinanden.

Alt er en fil: Unix' Forenende Princip
En af de mest revolutionerende og centrale ideer i Unix er princippet om, at alt er en fil. I denne kontekst er en fil simpelthen en strøm af bytes. Det er en kraftfuld forenkling, der gælder for almindelige dokumenter, mapper, hardwareenheder som printere og terminaler, og endda netværksforbindelser. Kernen (kernel) vedligeholder for hver proces en såkaldt filbeskrivelsestabel (file descriptor table). En filbeskrivelse er et simpelt heltal, der fungerer som et indeks i denne tabel, og som et program kan bruge til at læse fra eller skrive til en given filstrøm.
De to mest fundamentale systemkald, der arbejder med disse filbeskrivelser, er:
write(desc, buffer, len): Skriver op tillenbytes fra en hukommelsesbuffer til den strøm, der er identificeret veddesc.read(desc, buffer, max): Læser op tilmaxbytes fra strømmendescind i en hukommelsesbuffer.
Denne ensartede tilgang betyder, at et program ikke behøver at vide, om det skriver til en terminal, en fil på harddisken eller sender data over et netværk. Interfacet er det samme. Hver proces starter med tre standard filbeskrivelser, som er universelt anerkendte:
- 0 (stdin): Standard input. Herfra forventes processen at læse sine inputdata, som standard er det tastaturet.
- 1 (stdout): Standard output. Hertil skriver processen sit primære output, som standard er det terminalvinduet.
- 2 (stderr): Standard error. Hertil skrives fejlmeddelelser for at adskille dem fra det normale output.
Denne mekanisme er det, der gør omdirigering i en shell (kommandolinje) mulig. Kommandoen ls > fil.txt instruerer shell'en i at køre programmet ls, men i stedet for at lade dets standard output (stdout, filbeskrivelse 1) pege på terminalen, skal det pege på filen fil.txt. Programmet ls opfører sig præcis som altid; det er kernen, der har omdirigeret datastrømmen.
Pipes: Kædning af Programmer
Idéen om "alt er en fil" blev udvidet med introduktionen af en speciel type fil kaldet et pipe (rørledning). Et pipe er en envejs kommunikationskanal, der administreres af kernen, og som lader output fra én proces blive til input for en anden. I en shell repræsenteres dette af symbolet |.
Overvej kommandoen: ls /usr/bin | grep 'perl' | wc -l
ls /usr/bin: Lister indholdet af mappen/usr/binog sender det til sit stdout.|: Shell'en opretter et pipe. Outputtet fralsbliver ikke sendt til terminalen, men ind i skriveenden af dette pipe.grep 'perl': Dette program læser fra sit stdin, som nu er forbundet til læseenden af pipet. Det filtrerer inputtet og sender kun de linjer, der indeholder ordet 'perl', til sit eget stdout.|: Endnu et pipe oprettes. Outputtet fragrepledes ind i dette nye pipe.wc -l: Tæller antallet af linjer, det modtager fra sit stdin (som kommer fragrep), og skriver resultatet til sit stdout (terminalen).
Denne simple, men ekstremt kraftfulde, mekanisme gør det muligt at bygge komplekse værktøjer ved at kombinere simple, specialiserede programmer.
Processtyring: Den Virtuelle Computer
En proces i Unix kan bedst beskrives som en virtuel computer. Kernen giver hver proces illusionen af, at den har sin egen private CPU og sit eget store, sammenhængende hukommelsesrum. I virkeligheden deler processerne den fysiske hardware, og det er kernens opgave at administrere denne deling retfærdigt og sikkert.
Kernen vedligeholder en procestabel med information om hver aktiv proces, indekseret af et unikt proces-ID (PID). For hver proces gemmes oplysninger om:
- CPU-tilstand: Værdierne i CPU'ens registre, når processen sidst kørte.
- Hukommelsesstyring: En beskrivelse af processens virtuelle adresserum, som typisk er opdelt i segmenter som kode (text), initialiserede data, uinitialiserede data (BSS), heap (til dynamisk allokering) og stak (stack).
- Filbeskrivelsestabel: Den førnævnte tabel, der holder styr på åbne filer.
Systemkald til Processtyring
Fire systemkald er helt centrale for, hvordan en shell starter og styrer andre programmer.
| Systemkald | Beskrivelse |
|---|---|
pid = fork() | Opretter en ny proces (barneprocessen), der er en næsten identisk kopi af den kaldende proces (forælderprocessen). Den eneste forskel er returværdien: barnet får 0, mens forælderen får barnets PID. |
err = execve(...) | Erstatter den nuværende proces' hukommelse og kode med et nyt program. Filbeskrivelsestabellen bibeholdes. Hvis kaldet lykkes, returnerer det aldrig. |
pid = waitpid(...) | En forælderproces venter på, at en af dens barneprocesser afslutter eller ændrer tilstand. Dette er nødvendigt for at rydde op efter barneprocessen i kernens procestabel. |
exit(status) | Afslutter den kaldende proces og returnerer en statuskode, som forælderprocessen kan aflæse via waitpid(). |
Kombinationen af fork og exec er den klassiske Unix-måde at køre et nyt program på. En shell vil først kalde fork() for at skabe en kopi af sig selv. Derefter vil barneprocessen kalde execve() for at erstatte sig selv med det program, brugeren ønskede at køre. Imens kan forælderprocessen (shell'en) enten vente på, at barnet afslutter ved at kalde waitpid(), eller den kan fortsætte med at køre og lade barneprocessen køre i baggrunden (hvis brugeren brugte &).
Signaler og Zombieprocesser
Hvad sker der, hvis en forælderproces ikke kalder waitpid() efter en barneproces er afsluttet? Barneprocessen frigør sin hukommelse og de fleste ressourcer, men dens indgang i kernens procestabel forbliver, da den indeholder afslutningsstatus, som forælderen potentielt kan være interesseret i. En sådan afsluttet, men ikke opryddet, proces kaldes en zombieproces. Hvis et system akkumulerer for mange zombier, kan det løbe tør for plads i procestabellen.
For at håndtere dette asynkront bruger Unix et signal-system. Et signal er en notifikation sendt til en proces for at informere den om en begivenhed. Når en barneproces afslutter, sender kernen et SIGCHLD-signal til forælderprocessen. En velopdragen forælder, som en shell, vil have en signal-handler registreret for SIGCHLD. Denne handler er en funktion, der automatisk kaldes, når signalet modtages, og dens opgave er at kalde waitpid() for at rydde op efter den afdøde barneproces og dermed forhindre den i at blive en zombie.
Ofte Stillede Spørgsmål
Hvad betyder "alt er en fil" i praksis?
Det betyder, at programmører kan bruge de samme simple funktioner (read, write, open, close) til at interagere med en bred vifte af systemressourcer. Et program, der læser data fra standard input, kan få sine data fra en bruger, der taster på et keyboard, fra en fil på harddisken, eller fra outputtet af et andet program via et pipe, helt uden at programmet selv skal ændres. Denne ensartethed er ekstremt kraftfuld og fremmer genbrugelighed.
Hvad er den præcise forskel på `fork()` og `execve()`?
fork() skaber en ny proces, der er en klon af den gamle. Efter et fork()-kald har du to processer, der kører den samme kode fra det punkt, hvor fork() blev kaldt. execve() derimod transformerer den eksisterende proces. Den erstatter processens nuværende program med et helt nyt program. Den skaber ikke en ny proces, men genbruger den gamle. En shell bruger fork() til at skabe en ny proces-kontekst og derefter execve() i den nye proces for at køre den ønskede kommando.
Hvorfor er pipes så vigtige?
Pipes er limen, der gør det muligt at bygge komplekse arbejdsgange ud af små, simple værktøjer. Hvert værktøj er designet til at gøre én ting godt (f.eks. sortere tekst, tælle linjer, finde mønstre). I stedet for at skulle bygge et monolitisk program, der kan alt, kan en bruger dynamisk kombinere disse små værktøjer ved hjælp af pipes til at løse en specifik opgave. Dette er kernen i Unix-filosofien om "skriv programmer, der gør én ting og gør det godt".
Hvad er en "zombieproces", og er den farlig?
En zombieproces er en afsluttet proces, som forælderen endnu ikke har "anerkendt" ved at kalde waitpid(). Den bruger næsten ingen systemressourcer (ingen CPU, meget lidt hukommelse), men den optager en plads i kernens procestabel. En enkelt zombie er harmløs. Problemet opstår, hvis et fejlbehæftet program skaber mange barneprocesser og aldrig rydder op efter dem. Dette kan i sidste ende fylde procestabellen, så nye processer ikke kan startes. Korrekt programmering med signal-handlere eller eksplicit venten på børn forhindrer dette problem.
Hvis du vil læse andre artikler, der ligner Unix' Kerneabstraktioner: En Dybdegående Guide, kan du besøge kategorien Teknologi.
