Hvordan rørledninger implementeres i Unix

Hvordan rørledninger implementeres i Unix
Denne artikkelen beskriver implementeringen av pipelines i Unix-kjernen. Jeg var litt skuffet over at en nylig artikkel med tittelen "Hvordan fungerer rørledninger i Unix?"viste seg no om den indre strukturen. Jeg ble nysgjerrig og gravde i gamle kilder for å finne svaret.

Hva snakker vi om?

Pipelines, "sannsynligvis den viktigste oppfinnelsen i Unix," er en definerende egenskap ved den underliggende Unix-filosofien om å koble sammen små programmer, samt et kjent tegn på kommandolinjen:

$ echo hello | wc -c
6

Denne funksjonaliteten avhenger av kjernen-levert systemkall pipe, som er beskrevet på dokumentasjonssidene pipe(7) и pipe(2):

Rørledninger gir en ensrettet kanal for kommunikasjon mellom prosesser. Rørledningen har en inngang (skriveende) og en utgang (lesende). Data skrevet til inngangen til rørledningen kan leses ved utgangen.

Rørledningen opprettes ved hjelp av samtalen pipe(2), som returnerer to filbeskrivelser: en som refererer til inngangen til rørledningen, den andre til utdata.

Sporingsutgangen fra kommandoen ovenfor viser opprettelsen av rørledningen og strømmen av data gjennom den fra en prosess til en annen:

$ strace -qf -e execve,pipe,dup2,read,write 
    sh -c 'echo hello | wc -c'

execve("/bin/sh", ["sh", "-c", "echo hello | wc -c"], …)
pipe([3, 4])                            = 0
[pid 2604795] dup2(4, 1)                = 1
[pid 2604795] write(1, "hellon", 6)    = 6
[pid 2604796] dup2(3, 0)                = 0
[pid 2604796] execve("/usr/bin/wc", ["wc", "-c"], …)
[pid 2604796] read(0, "hellon", 16384) = 6
[pid 2604796] write(1, "6n", 2)        = 2

Foreldreprosessen kaller pipe()for å få monterte filbeskrivelser. En underordnet prosess skriver til ett håndtak, og en annen prosess leser de samme dataene fra et annet håndtak. Skallet bruker dup2 for å "gi nytt navn" til beskrivelser 3 og 4 for å matche stdin og stdout.

Uten rør, ville skallet måtte skrive utdataene fra en prosess til en fil og sende den til en annen prosess for å lese dataene fra filen. Som et resultat ville vi kaste bort mer ressurser og diskplass. Imidlertid er rørledninger gode ikke bare fordi de lar deg unngå bruk av midlertidige filer:

Hvis en prosess prøver å lese fra en tom rørledning da read(2) vil blokkere til dataene blir tilgjengelige. Hvis en prosess prøver å skrive til en full pipeline, da write(2) vil blokkere til nok data er lest fra rørledningen til å utføre skrivingen.

I likhet med POSIX-kravet er dette en viktig egenskap: å skrive til rørledningen frem til PIPE_BUF byte (minst 512) må være atomiske slik at prosesser kan kommunisere med hverandre gjennom rørledningen på en måte som vanlige filer (som ikke gir slike garantier) ikke kan.

Når du bruker en vanlig fil, kan en prosess skrive all utdata til den og sende den videre til en annen prosess. Eller prosesser kan operere i en svært parallell modus, ved å bruke en ekstern signaleringsmekanisme (som en semafor) for å varsle hverandre når en skriving eller lesing er fullført. Transportbånd sparer oss fra alt dette bryet.

Hva leter vi etter?

Jeg vil forklare det på en enkel måte slik at det er lettere for deg å forestille deg hvordan en transportør kan fungere. Du må tildele en buffer og en tilstand i minnet. Du trenger funksjoner for å legge til og fjerne data fra bufferen. Du trenger noen midler for å kalle opp funksjoner under lese- og skriveoperasjoner på filbeskrivelser. Og du trenger låser for å implementere den spesielle oppførselen beskrevet ovenfor.

Nå er vi klare til å avhøre kjernekildekoden under sterkt lampelys for å bekrefte eller avkrefte vår vage mentale modell. Men vær alltid forberedt på det uventede.

Hvor ser vi?

Jeg vet ikke hvor mitt eksemplar av den berømte boken er "Lions bok"med Unix 6 kildekode, men takket være Unix Heritage Society du kan søke på nettet på kildekode enda eldre versjoner av Unix.

Å vandre gjennom TUHS-arkivene er som å besøke et museum. Vi kan se på vår felles historie, og jeg har respekt for mange års innsats for å gjenvinne alt dette materialet bit for bit fra gamle bånd og trykk. Og jeg er svært klar over de fragmentene som fortsatt mangler.

Etter å ha tilfredsstilt vår nysgjerrighet om transportørenes eldgamle historie, kan vi se på moderne kjerner for sammenligning.

Forresten, pipe er systemanrop nummer 42 i tabellen sysent[]. Tilfeldigheter?

Tradisjonelle Unix-kjerner (1970–1974)

Jeg fant ingen spor pipe(2) verken i PDP-7 Unix (januar 1970), heller ikke i første utgave av Unix (november 1971), og heller ikke i den ufullstendige kildekoden andre utgave (juni 1972).

Det opplyser TUHS tredje utgave av Unix (februar 1973) ble den første versjonen med transportbånd:

Unix 1973rd Edition var den siste versjonen med en kjerne skrevet i assemblerspråk, men også den første versjonen med pipelines. I løpet av XNUMX ble det arbeidet med å forbedre den tredje utgaven, kjernen ble skrevet om i C, og dermed dukket den fjerde utgaven av Unix opp.

En leser fant en skanning av et dokument der Doug McIlroy foreslo ideen om å "koble programmer som en hageslange."

Hvordan rørledninger implementeres i Unix
I Brian Kernighans bokUnix: A History and a Memoir", i historien om fremveksten av transportører, er dette dokumentet også nevnt: "... det hang på veggen på kontoret mitt på Bell Labs i 30 år." Her intervju med McIlroy, og en annen historie fra McIlroys verk, skrevet i 2014:

Da Unix kom ut, førte min fascinasjon for koroutiner til at jeg spurte OS-forfatteren, Ken Thompson, om å la data skrevet til en prosess gå ikke bare til enheten, men også til en annen prosess. Ken bestemte at det var mulig. Men som minimalist ønsket han at hver systemfunksjon skulle spille en betydelig rolle. Er det virkelig en stor fordel å skrive direkte mellom prosesser fremfor å skrive til en mellomfil? Det var først da jeg kom med et spesifikt forslag med det fengende navnet «pipeline» og en beskrivelse av syntaksen for interaksjon mellom prosesser at Ken til slutt utbrøt: «Jeg skal gjøre det!»

Og gjorde. En skjebnesvanger kveld endret Ken kjernen og skallet, fikset flere standardprogrammer for å standardisere hvordan de godtok input (som kunne komme fra en pipeline), og endret også filnavn. Dagen etter begynte rørledninger å bli brukt veldig mye i applikasjoner. Ved slutten av uken brukte sekretærer dem til å sende dokumenter fra tekstbehandlere til skriveren. Litt senere erstattet Ken den originale API-en og syntaksen for å pakke bruken av rørledninger med renere konvensjoner, som har blitt brukt siden.

Dessverre har kildekoden for den tredje utgaven av Unix-kjernen gått tapt. Og selv om vi har kjernekildekoden skrevet i C fjerde utgave, utgitt i november 1973, men den kom ut flere måneder før den offisielle utgivelsen og inneholder ikke pipeline-implementeringer. Det er synd at kildekoden for denne legendariske Unix-funksjonen går tapt, kanskje for alltid.

Vi har tekstdokumentasjon for pipe(2) fra begge utgivelsene, så du kan begynne med å søke i dokumentasjonen tredje utgave (for visse ord, understreket "manuelt", en streng med bokstavelige bokstaver ^H, etterfulgt av et understrek!). Denne proto-pipe(2) er skrevet på assemblerspråk og returnerer bare én filbeskrivelse, men gir allerede den forventede grunnleggende funksjonaliteten:

Systemanrop rør oppretter en input/output-mekanisme kalt en pipeline. Den returnerte filbeskrivelsen kan brukes til lese- og skriveoperasjoner. Når noe skrives til rørledningen, bufres opptil 504 byte med data, hvoretter skriveprosessen stanses. Ved lesing fra rørledningen blir de bufrede dataene tatt bort.

Året etter hadde kjernen blitt skrevet om i C, og pipe(2) i fjerde utgave fikk sitt moderne utseende med prototypen "pipe(fildes)'

Systemanrop rør oppretter en input/output-mekanisme kalt en pipeline. De returnerte filbeskrivelsene kan brukes i lese- og skriveoperasjoner. Når noe skrives til rørledningen, brukes håndtaket som returneres i r1 (resp. fildes[1]), bufret til 4096 byte med data, hvoretter skriveprosessen suspenderes. Ved lesing fra rørledningen tar håndtaket tilbake til r0 (resp. fildes[0]) dataene.

Det antas at når en rørledning er definert, to (eller flere) kommunikasjonsprosesser (skapt av påfølgende anrop til gaffel) vil overføre data fra rørledningen ved hjelp av samtaler lese и skrive.

Skallet har en syntaks for å definere en lineær rekke prosesser forbundet med en rørledning.

Anrop om å lese fra en tom pipeline (som ikke inneholder bufrede data) som bare har én ende (alle skrivefilbeskrivelser er lukket) returnerer "end of file". Oppfordringer til å skrive i en lignende situasjon ignoreres.

Tidligst bevart rørledningsimplementering gjelder til den femte utgaven av Unix (juni 1974), men den er nesten identisk med den som dukket opp i neste utgivelse. Kommentarer er nettopp lagt til, så du kan hoppe over den femte utgaven.

Sjette utgave av Unix (1975)

La oss begynne å lese Unix-kildekoden sjette utgave (mai 1975). Mye takket være Lions det er mye lettere å finne enn kildene til tidligere versjoner:

I mange år boken Lions var det eneste dokumentet på Unix-kjernen tilgjengelig utenfor Bell Labs. Selv om lisensen for sjette utgave tillot lærere å bruke kildekoden, utelukket lisensen for syvende utgave denne muligheten, så boken ble distribuert i form av ulovlige maskinskrevne kopier.

I dag kan du kjøpe et opptrykk av boken, hvis omslag viser elevene ved en kopimaskin. Og takket være Warren Toomey (som startet TUHS-prosjektet) kan du laste ned PDF-fil med kildekode for sjette utgave. Jeg vil gi deg en idé om hvor mye innsats som ble lagt ned på å lage filen:

For mer enn 15 år siden skrev jeg en kopi av kildekoden som ble gitt inn Lions, fordi jeg ikke likte kvaliteten på kopien min fra et ukjent antall andre kopier. TUHS eksisterte ikke ennå, og jeg hadde ikke tilgang til de gamle kildene. Men i 1988 fant jeg et gammelt 9-spors bånd som inneholdt en sikkerhetskopi fra en PDP11-datamaskin. Det var vanskelig å si om det fungerte, men det var et intakt /usr/src/-tre der de fleste filene var merket med årstallet 1979, som selv da så gammelt ut. Det var den syvende utgaven eller dens avledede PWB, som jeg trodde.

Jeg tok funnet som grunnlag og redigerte kildene manuelt til sjette utgave. Noe av koden forble den samme, men noen måtte redigeres litt, og endret det moderne +=-tokenet til det utdaterte =+. Noen ting ble rett og slett slettet, og noen måtte skrives helt om, men ikke for mye.

Og i dag kan vi lese online på TUHS kildekoden til den sjette utgaven fra arkiv, som Dennis Ritchie hadde en hånd med.

Forresten, ved første øyekast, er hovedtrekket til C-koden før perioden til Kernighan og Ritchie dens korthet. Det er ikke ofte jeg klarer å sette inn kodebiter uten omfattende redigering for å passe til et relativt smalt visningsområde på nettstedet mitt.

Tidlig /usr/sys/ken/pipe.c det er en forklarende kommentar (og ja, det er mer /usr/sys/dmr):

/*
 * Max allowable buffering per pipe.
 * This is also the max size of the
 * file created to implement the pipe.
 * If this size is bigger than 4096,
 * pipes will be implemented in LARG
 * files, which is probably not good.
 */
#define    PIPSIZ    4096

Bufferstørrelsen har ikke endret seg siden den fjerde utgaven. Men her ser vi, uten offentlig dokumentasjon, at rørledninger en gang brukte filer som backup-lagring!

Når det gjelder LARG filer, tilsvarer de inode flagg LARG, som brukes av den "store adresseringsalgoritmen" for å behandle indirekte blokker for å støtte større filsystemer. Siden Ken sa at det er bedre å ikke bruke dem, tar jeg gjerne hans ord for det.

Her er den virkelige systemanropet pipe:

/*
 * The sys-pipe entry.
 * Allocate an inode on the root device.
 * Allocate 2 file structures.
 * Put it all together with flags.
 */
pipe()
{
    register *ip, *rf, *wf;
    int r;

    ip = ialloc(rootdev);
    if(ip == NULL)
        return;
    rf = falloc();
    if(rf == NULL) {
        iput(ip);
        return;
    }
    r = u.u_ar0[R0];
    wf = falloc();
    if(wf == NULL) {
        rf->f_count = 0;
        u.u_ofile[r] = NULL;
        iput(ip);
        return;
    }
    u.u_ar0[R1] = u.u_ar0[R0]; /* wf's fd */
    u.u_ar0[R0] = r;           /* rf's fd */
    wf->f_flag = FWRITE|FPIPE;
    wf->f_inode = ip;
    rf->f_flag = FREAD|FPIPE;
    rf->f_inode = ip;
    ip->i_count = 2;
    ip->i_flag = IACC|IUPD;
    ip->i_mode = IALLOC;
}

Kommentaren beskriver tydelig hva som skjer her. Men å forstå koden er ikke så lett, delvis på grunn av måten "struct bruker u» og registrerer R0 и R1 Systemanropsparametere og returverdier sendes.

La oss prøve med ialloc() sette på disk inode (indekshåndtak), og med hjelp falloc() - plasser to i minnet fil. Hvis alt går bra, setter vi flagg for å identifisere disse filene som to ender av rørledningen, peker dem til den samme inoden (hvis referanseantallet vil bli satt til 2), og merker inoden som modifisert og i bruk. Vær oppmerksom på forespørsler til jeg setter() i feilbaner for å redusere referanseantallet i den nye inoden.

pipe() må gjennom R0 и R1 returnere filbeskrivelsesnumre for lesing og skriving. falloc() returnerer en peker til filstrukturen, men "returnerer" også via u.u_ar0[R0] og en filbeskrivelse. Det vil si at koden lagres i r filbeskrivelse for lesing og tildeler en filbeskrivelse for å skrive direkte fra u.u_ar0[R0] etter den andre samtalen falloc().

flagg FPIPE, som vi angir når vi lager rørledningen, kontrollerer funksjonen til funksjonen rdwr() i sys2.ckaller spesifikke I/O-rutiner:

/*
 * common code for read and write calls:
 * check permissions, set base, count, and offset,
 * and switch out to readi, writei, or pipe code.
 */
rdwr(mode)
{
    register *fp, m;

    m = mode;
    fp = getf(u.u_ar0[R0]);
        /* … */

    if(fp->f_flag&FPIPE) {
        if(m==FREAD)
            readp(fp); else
            writep(fp);
    }
        /* … */
}

Deretter funksjonen readp() в pipe.c leser data fra rørledningen. Men det er bedre å spore implementeringen fra writep(). Igjen har koden blitt mer kompleks på grunn av konvensjonene for å sende argumenter, men noen detaljer kan utelates.

writep(fp)
{
    register *rp, *ip, c;

    rp = fp;
    ip = rp->f_inode;
    c = u.u_count;

loop:
    /* If all done, return. */

    plock(ip);
    if(c == 0) {
        prele(ip);
        u.u_count = 0;
        return;
    }

    /*
     * If there are not both read and write sides of the
     * pipe active, return error and signal too.
     */

    if(ip->i_count < 2) {
        prele(ip);
        u.u_error = EPIPE;
        psignal(u.u_procp, SIGPIPE);
        return;
    }

    /*
     * If the pipe is full, wait for reads to deplete
     * and truncate it.
     */

    if(ip->i_size1 == PIPSIZ) {
        ip->i_mode =| IWRITE;
        prele(ip);
        sleep(ip+1, PPIPE);
        goto loop;
    }

    /* Write what is possible and loop back. */

    u.u_offset[0] = 0;
    u.u_offset[1] = ip->i_size1;
    u.u_count = min(c, PIPSIZ-u.u_offset[1]);
    c =- u.u_count;
    writei(ip);
    prele(ip);
    if(ip->i_mode&IREAD) {
        ip->i_mode =& ~IREAD;
        wakeup(ip+2);
    }
    goto loop;
}

Vi ønsker å skrive bytes til pipeline-inngangen u.u_count. Først må vi låse inoden (se nedenfor plock/prele).

Deretter sjekker vi inodereferansetelleren. Så lenge begge ender av rørledningen forblir åpne, skal telleren være lik 2. Vi holder en lenke (fra rp->f_inode), så hvis telleren er mindre enn 2, må det bety at leseprosessen har lukket enden av rørledningen. Med andre ord, vi prøver å skrive til en lukket rørledning, og dette er en feil. Første gangs feilkode EPIPE og signal SIGPIPE dukket opp i den sjette utgaven av Unix.

Men selv om transportøren er åpen, kan den være full. I dette tilfellet slipper vi låsen og går i dvale i håp om at en annen prosess vil lese fra rørledningen og frigjøre nok plass i den. Etter å ha våknet går vi tilbake til begynnelsen, legger på låsen igjen og starter en ny opptakssyklus.

Hvis det er nok ledig plass i rørledningen, skriver vi data til den ved hjelp av skrivei(). Parameter i_size1 inode (hvis rørledningen er tom, kan den være lik 0) indikerer slutten av dataene som den allerede inneholder. Hvis det er nok opptaksplass, kan vi fylle rørledningen fra i_size1 til PIPESIZ. Så slipper vi låsen og prøver å vekke enhver prosess som venter på å lese fra rørledningen. Vi går tilbake til begynnelsen for å se om vi var i stand til å skrive så mange byte som vi trengte. Hvis det mislykkes, starter vi en ny opptakssyklus.

Vanligvis parameteren i_mode inode brukes til å lagre tillatelser r, w и x. Men når det gjelder rørledninger, signaliserer vi at en eller annen prosess venter på å skrive eller lese ved hjelp av biter IREAD и IWRITE hhv. Prosessen setter flagget og kaller sleep(), og det forventes at en annen prosess i fremtiden vil føre til wakeup().

Den virkelige magien skjer i sleep() и wakeup(). De er implementert i slp.c, kilden til den berømte "Du forventes ikke å forstå dette"-kommentaren. Heldigvis trenger vi ikke å forstå koden, bare se på noen kommentarer:

/*
 * Give up the processor till a wakeup occurs
 * on chan, at which time the process
 * enters the scheduling queue at priority pri.
 * The most important effect of pri is that when
 * pri<0 a signal cannot disturb the sleep;
 * if pri>=0 signals will be processed.
 * Callers of this routine must be prepared for
 * premature return, and check that the reason for
 * sleeping has gone away.
 */
sleep(chan, pri) /* … */

/*
 * Wake up all processes sleeping on chan.
 */
wakeup(chan) /* … */

Prosessen som forårsaker sleep() for en bestemt kanal, kan senere vekkes av en annen prosess, som vil forårsake wakeup() for samme kanal. writep() и readp() koordinere sine handlinger gjennom slike sammenkoblede samtaler. noter det pipe.c alltid prioriterer PPIPE når du ringer sleep(), så det er det sleep() kan bli avbrutt av et signal.

Nå har vi alt for å forstå funksjonen readp():

readp(fp)
int *fp;
{
    register *rp, *ip;

    rp = fp;
    ip = rp->f_inode;

loop:
    /* Very conservative locking. */

    plock(ip);

    /*
     * If the head (read) has caught up with
     * the tail (write), reset both to 0.
     */

    if(rp->f_offset[1] == ip->i_size1) {
        if(rp->f_offset[1] != 0) {
            rp->f_offset[1] = 0;
            ip->i_size1 = 0;
            if(ip->i_mode&IWRITE) {
                ip->i_mode =& ~IWRITE;
                wakeup(ip+1);
            }
        }

        /*
         * If there are not both reader and
         * writer active, return without
         * satisfying read.
         */

        prele(ip);
        if(ip->i_count < 2)
            return;
        ip->i_mode =| IREAD;
        sleep(ip+2, PPIPE);
        goto loop;
    }

    /* Read and return */

    u.u_offset[0] = 0;
    u.u_offset[1] = rp->f_offset[1];
    readi(ip);
    rp->f_offset[1] = u.u_offset[1];
    prele(ip);
}

Du kan finne det lettere å lese denne funksjonen fra bunn til topp. "Les og returner"-grenen brukes vanligvis når det er noen data i rørledningen. I dette tilfellet bruker vi lese() vi leser så mye data som er tilgjengelig fra den gjeldende f_offset lesing, og oppdater deretter verdien til den tilsvarende forskyvningen.

Ved påfølgende avlesninger vil rørledningen være tom hvis avlesningsforskyvningen har nådd i_size1 på inode. Vi tilbakestiller posisjonen til 0 og prøver å vekke enhver prosess som ønsker å skrive til rørledningen. Vi vet at når transportøren er full, writep() vil sovne på ip+1. Og nå som rørledningen er tom, kan vi vekke den for å gjenoppta skrivesyklusen.

Hvis du ikke har noe å lese, da readp() kan sette et flagg IREAD og sovne videre ip+2. Vi vet hva som vil vekke ham writep(), når den skriver noen data til rørledningen.

Kommentarer til readi() og writei() vil hjelpe deg å forstå at i stedet for å sende parametere via "u"Vi kan behandle dem som vanlige I/O-funksjoner som tar en fil, en posisjon, en buffer i minnet og teller antall byte å lese eller skrive.

/*
 * Read the file corresponding to
 * the inode pointed at by the argument.
 * The actual read arguments are found
 * in the variables:
 *    u_base        core address for destination
 *    u_offset    byte offset in file
 *    u_count        number of bytes to read
 *    u_segflg    read to kernel/user
 */
readi(aip)
struct inode *aip;
/* … */

/*
 * Write the file corresponding to
 * the inode pointed at by the argument.
 * The actual write arguments are found
 * in the variables:
 *    u_base        core address for source
 *    u_offset    byte offset in file
 *    u_count        number of bytes to write
 *    u_segflg    write to kernel/user
 */
writei(aip)
struct inode *aip;
/* … */

Når det gjelder den "konservative" blokkeringen, altså readp() и writep() blokker inoden til de er ferdige med arbeidet eller mottar et resultat (det vil si ring wakeup). plock() и prele() arbeid enkelt: ved å bruke et annet sett med samtaler sleep и wakeup la oss vekke enhver prosess som trenger låsen vi nettopp har sluppet:

/*
 * Lock a pipe.
 * If its already locked, set the WANT bit and sleep.
 */
plock(ip)
int *ip;
{
    register *rp;

    rp = ip;
    while(rp->i_flag&ILOCK) {
        rp->i_flag =| IWANT;
        sleep(rp, PPIPE);
    }
    rp->i_flag =| ILOCK;
}

/*
 * Unlock a pipe.
 * If WANT bit is on, wakeup.
 * This routine is also used to unlock inodes in general.
 */
prele(ip)
int *ip;
{
    register *rp;

    rp = ip;
    rp->i_flag =& ~ILOCK;
    if(rp->i_flag&IWANT) {
        rp->i_flag =& ~IWANT;
        wakeup(rp);
    }
}

Først kunne jeg ikke forstå hvorfor readp() ikke forårsaker prele(ip) før samtalen wakeup(ip+1). Det første er writep() årsaker i sin syklus, dette plock(ip), som fører til vranglås hvis readp() har ikke fjernet blokkeringen min ennå, så på en eller annen måte må koden fungere riktig. Hvis du ser på wakeup(), så blir det klart at det bare markerer soveprosessen som klar til å utføres, slik at i fremtiden sched() virkelig lanserte den. Så readp() årsaker wakeup(), fjerner låsen, setter IREAD og samtaler sleep(ip+2)- alt dette før writep() gjenopptar syklusen.

Dette fullfører beskrivelsen av transportbånd i sjette utgave. Enkel kode, vidtrekkende konsekvenser.

Syvende utgave av Unix (januar 1979) var en ny stor utgivelse (fire år senere) som introduserte mange nye applikasjoner og kjernefunksjoner. Den gjennomgikk også betydelige endringer i forbindelse med bruk av typestøping, fagforeninger og maskinskrevne pekepinner til konstruksjoner. derimot transportør kode praktisk talt uendret. Vi kan hoppe over denne utgaven.

Xv6, en enkel Unix-lignende kjerne

For å lage kjernen Xv6 påvirket av den sjette utgaven av Unix, men den er skrevet i moderne C for å kjøre på x86-prosessorer. Koden er lett å lese og forståelig. I tillegg, i motsetning til Unix-kilder med TUHS, kan du kompilere den, endre den og kjøre den på noe annet enn en PDP 11/70. Derfor er denne kjernen mye brukt på universiteter som undervisningsmateriell på operativsystemer. Kilder er på Github.

Koden inneholder en tydelig og gjennomtenkt implementering pipe.c, støttet av en buffer i minnet i stedet for en inode på disken. Her gir jeg bare definisjonen av "strukturell rørledning" og funksjonen pipealloc():

#define PIPESIZE 512

struct pipe {
  struct spinlock lock;
  char data[PIPESIZE];
  uint nread;     // number of bytes read
  uint nwrite;    // number of bytes written
  int readopen;   // read fd is still open
  int writeopen;  // write fd is still open
};

int
pipealloc(struct file **f0, struct file **f1)
{
  struct pipe *p;

  p = 0;
  *f0 = *f1 = 0;
  if((*f0 = filealloc()) == 0 || (*f1 = filealloc()) == 0)
    goto bad;
  if((p = (struct pipe*)kalloc()) == 0)
    goto bad;
  p->readopen = 1;
  p->writeopen = 1;
  p->nwrite = 0;
  p->nread = 0;
  initlock(&p->lock, "pipe");
  (*f0)->type = FD_PIPE;
  (*f0)->readable = 1;
  (*f0)->writable = 0;
  (*f0)->pipe = p;
  (*f1)->type = FD_PIPE;
  (*f1)->readable = 0;
  (*f1)->writable = 1;
  (*f1)->pipe = p;
  return 0;

 bad:
  if(p)
    kfree((char*)p);
  if(*f0)
    fileclose(*f0);
  if(*f1)
    fileclose(*f1);
  return -1;
}

pipealloc() angir tilstanden til resten av implementeringen, som inkluderer funksjonene piperead(), pipewrite() и pipeclose(). Faktisk systemanrop sys_pipe er en innpakning implementert i sysfile.c. Jeg anbefaler å lese hele koden hans. Kompleksiteten er på nivå med kildekoden til den sjette utgaven, men den er mye enklere og morsommere å lese.

Linux 0.01

Linux 0.01 kildekode kan bli funnet. Det vil være lærerikt å studere gjennomføringen av rørledninger i hans fs/pipe.c. Denne bruker en inode for å representere rørledningen, men selve rørledningen er skrevet i moderne C. Hvis du har jobbet deg gjennom XNUMX. utgave kode, vil du ikke ha noen problemer her. Slik ser funksjonen ut write_pipe():

int write_pipe(struct m_inode * inode, char * buf, int count)
{
    char * b=buf;

    wake_up(&inode->i_wait);
    if (inode->i_count != 2) { /* no readers */
        current->signal |= (1<<(SIGPIPE-1));
        return -1;
    }
    while (count-->0) {
        while (PIPE_FULL(*inode)) {
            wake_up(&inode->i_wait);
            if (inode->i_count != 2) {
                current->signal |= (1<<(SIGPIPE-1));
                return b-buf;
            }
            sleep_on(&inode->i_wait);
        }
        ((char *)inode->i_size)[PIPE_HEAD(*inode)] =
            get_fs_byte(b++);
        INC_PIPE( PIPE_HEAD(*inode) );
        wake_up(&inode->i_wait);
    }
    wake_up(&inode->i_wait);
    return b-buf;
}

Uten engang å se på strukturdefinisjonene, kan du finne ut hvordan inodereferansetellingen brukes til å sjekke om en skriveoperasjon resulterer i SIGPIPE. I tillegg til å jobbe byte-for-byte, er denne funksjonen enkel å sammenligne med ideene beskrevet ovenfor. Til og med logikk sleep_on/wake_up ser ikke så fremmed ut.

Moderne Linux-kjerner, FreeBSD, NetBSD, OpenBSD

Jeg kjørte raskt gjennom noen moderne kjerner. Ingen av dem har en diskimplementering lenger (ikke overraskende). Linux har sin egen implementering. Selv om de tre moderne BSD-kjernene inneholder implementeringer basert på kode som ble skrevet av John Dyson, har de med årene blitt for forskjellige fra hverandre.

Å lese fs/pipe.c (på Linux) eller sys/kern/sys_pipe.c (på *BSD), det krever virkelig dedikasjon. Dagens kode handler om ytelse og støtte for funksjoner som vektor og asynkron I/O. Og detaljene for minneallokering, låser og kjernekonfigurasjon varierer veldig. Dette er ikke hva høyskoler trenger for et introduksjonskurs i operativsystemer.

Uansett, jeg var interessert i å grave opp noen gamle mønstre (som å generere SIGPIPE og returnere EPIPE når du skriver til en lukket pipeline) i alle disse forskjellige moderne kjernene. Jeg kommer nok aldri til å se en PDP-11 datamaskin i virkeligheten, men det er fortsatt mye å lære av kode som ble skrevet år før jeg ble født.

En artikkel skrevet av Divi Kapoor i 2011:Linux-kjerneimplementeringen av rør og FIFO-er" gir en oversikt over hvordan pipelines (fortsatt) fungerer i Linux. EN nylig commit i Linux illustrerer en pipeline-modell for interaksjon, hvis muligheter overgår de til midlertidige filer; og viser også hvor langt pipelines har kommet fra den "veldig konservative låsingen" av den sjette utgaven av Unix-kjernen.

Kilde: www.habr.com

Legg til en kommentar