Hvordan pipelines implementeres i Unix

Hvordan pipelines implementeres i Unix
Denne artikel beskriver implementeringen af ​​pipelines i Unix-kernen. Jeg var noget skuffet over, at en nylig artikel med titlen "Hvordan fungerer pipelines i Unix?" viste sig nej om den indre struktur. Jeg blev nysgerrig og gravede i gamle kilder for at finde svaret.

Hvad taler vi om?

Pipelines er "sandsynligvis den vigtigste opfindelse i Unix" - et definerende træk ved Unix's underliggende filosofi om at sammensætte små programmer og det velkendte kommandolinjeslogan:

$ echo hello | wc -c
6

Denne funktionalitet afhænger af det kerneleverede systemkald pipe, som er beskrevet på dokumentationssiderne rør (7) и rør (2):

Rørledninger giver en envejskanal til kommunikation mellem processer. Rørledningen har et input (skriveende) og et output (læseende). Data skrevet til input af pipelinen kan læses ved output.

Pipelinen oprettes ved opkald pipe(2), som returnerer to filbeskrivelser: den ene refererer til input af pipelinen, den anden til outputtet.

Sporingsoutputtet fra ovenstående kommando viser oprettelsen af ​​en pipeline og strømmen af ​​data gennem den fra en proces til en anden:

$ 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

Forældreprocessen kalder pipe()for at få vedhæftede filbeskrivelser. En underordnet proces skriver til en deskriptor, og en anden proces læser de samme data fra en anden deskriptor. Skallen "omdøber" beskrivelserne 2 og 3 med dup4 for at matche stdin og stdout.

Uden pipelines ville shellen skulle skrive output fra en proces til en fil og overføre den til en anden proces for at læse dataene fra filen. Som et resultat ville vi spilde flere ressourcer og diskplads. Imidlertid er pipelines gode til mere end blot at undgå midlertidige filer:

Hvis en proces forsøger at læse fra en tom pipeline, så read(2) vil blokere, indtil dataene er tilgængelige. Hvis en proces forsøger at skrive til en fuld pipeline, så write(2) vil blokere, indtil der er læst nok data fra pipelinen til at fuldføre skrivningen.

Ligesom POSIX-kravet er dette en vigtig egenskab: at skrive til pipeline op til PIPE_BUF bytes (mindst 512) skal være atomare, så processer kan kommunikere med hinanden gennem rørledningen på en måde, som normale filer (som ikke giver sådanne garantier) ikke kan.

Med en almindelig fil kan en proces skrive hele sit output til den og sende den videre til en anden proces. Eller processer kan fungere i en hård parallel tilstand ved at bruge en ekstern signaleringsmekanisme (som en semafor) til at informere hinanden om færdiggørelsen af ​​en skrivning eller læsning. Transportører sparer os for alt dette besvær.

Hvad leder vi efter?

Jeg vil forklare på mine fingre for at gøre det lettere for dig at forestille dig, hvordan en transportør kan fungere. Du bliver nødt til at allokere en buffer og en tilstand i hukommelsen. Du skal bruge funktioner til at tilføje og fjerne data fra bufferen. Du skal bruge en vis facilitet til at kalde funktioner under læse- og skriveoperationer på filbeskrivelser. Og låse er nødvendige for at implementere den særlige adfærd beskrevet ovenfor.

Vi er nu klar til at afhøre kernens kildekode under skarpt lampelys for at bekræfte eller modbevise vores vage mentale model. Men vær altid forberedt på det uventede.

Hvor søger vi?

Jeg ved ikke, hvor mit eksemplar af den berømte bog ligger.Løver bog« med Unix 6 kildekode, men takket være Unix Heritage Society kan søges på nettet kildekode endnu ældre versioner af Unix.

At vandre gennem TUHS-arkiverne er som at besøge et museum. Vi kan se på vores fælles historie, og jeg har respekt for de mange års indsats for at genvinde alt dette materiale lidt efter lidt fra gamle kassetter og udskrifter. Og jeg er meget opmærksom på de fragmenter, der stadig mangler.

Efter at have tilfredsstillet vores nysgerrighed om rørledningernes gamle historie, kan vi se på moderne kerner til sammenligning.

Af den måde, pipe er systemkald nummer 42 i tabellen sysent[]. Sammentræf?

Traditionelle Unix-kerner (1970-1974)

Jeg fandt intet spor pipe(2) hverken i PDP-7 Unix (januar 1970), heller ikke i første udgave af Unix (november 1971), ej heller i ufuldstændig kildekode anden version (juni 1972).

Det hævder TUHS tredje udgave af Unix (Februar 1973) var den første version med rørledninger:

Den tredje udgave af Unix var den sidste version med en kerne skrevet i assembler, men også den første version med pipelines. I løbet af 1973 arbejdede man på at forbedre tredje udgave, kernen blev omskrevet i C, og dermed var den fjerde udgave af Unix født.

En læser fandt en scanning af et dokument, hvor Doug McIlroy foreslog ideen om at "tilslutte programmer som en haveslange."

Hvordan pipelines implementeres i Unix
I Brian Kernighans bogUnix: A History and a Memoir”, historien om fremkomsten af ​​transportører nævner også dette dokument: “... det hang på væggen på mit kontor på Bell Labs i 30 år.” Her interview med McIlroyog en anden historie fra McIlroys værk, skrevet i 2014:

Da Unix dukkede op, fik min passion for coroutines mig til at bede OS-forfatteren, Ken Thompson, om at tillade, at data, der er skrevet til en proces, ikke kun går til enheden, men også til afslutningen til en anden proces. Ken troede, det var muligt. Men som minimalist ønskede han, at alle systemfunktioner skulle spille en væsentlig rolle. Er direkte skrivning mellem processer virkelig en stor fordel i forhold til at skrive til en mellemfil? Og først da jeg lavede et specifikt forslag med det iørefaldende navn "pipeline" og en beskrivelse af syntaksen for processers interaktion, udbrød Ken endelig: "Jeg vil gøre det!".

Og gjorde. En skæbnesvanger aften ændrede Ken kernen og skallen, rettede adskillige standardprogrammer for at standardisere, hvordan de accepterer input (som kan komme fra en pipeline), og ændrede filnavne. Den næste dag blev rørledninger meget udbredt i applikationer. I slutningen af ​​ugen brugte sekretærerne dem til at sende dokumenter fra tekstbehandlere til trykkeriet. Noget senere erstattede Ken den originale API og syntaks for at indpakke brugen af ​​pipelines med renere konventioner, der har været brugt lige siden.

Desværre er kildekoden til den tredje udgave af Unix-kernen gået tabt. Og selvom vi har kernekildekoden skrevet i C fjerde udgave, som blev udgivet i november 1973, men den udkom et par måneder før den officielle udgivelse og indeholder ikke implementeringen af ​​rørledninger. Det er en skam, at kildekoden til denne legendariske Unix-funktion går tabt, måske for altid.

Vi har dokumentationstekst til pipe(2) fra begge udgivelser, så du kan starte med at søge i dokumentationen tredje udgave (for visse ord, understreget "manuelt", en streng af ^H bogstaver efterfulgt af en understregning!). Denne proto-pipe(2) er skrevet i assembler og returnerer kun én filbeskrivelse, men giver allerede den forventede kernefunktionalitet:

Systemkald rør skaber en I/O-mekanisme kaldet en pipeline. Den returnerede filbeskrivelse kan bruges til læse- og skriveoperationer. Når noget skrives til pipelinen, bufferer det op til 504 bytes data, hvorefter skriveprocessen afbrydes. Ved læsning fra pipelinen tages de bufferlagrede data.

Året efter var kernen blevet omskrevet i C, og pipe(2) fjerde udgave fik sit moderne udseende med prototypen "pipe(fildes)»:

Systemkald rør skaber en I/O-mekanisme kaldet en pipeline. De returnerede filbeskrivelser kan bruges i læse- og skriveoperationer. Når noget er skrevet til pipelinen, bruges deskriptoren, der returneres i r1 (resp. fildes[1]), bufferet op til 4096 bytes data, hvorefter skriveprocessen suspenderes. Når man læser fra pipelinen, tager deskriptoren tilbage til r0 (resp. fildes[0]) dataene.

Det antages, at når en pipeline er blevet defineret, to (eller flere) interagerende processer (skabt af efterfølgende påkaldelser gaffel) vil videregive data fra pipelinen ved hjælp af opkald læse и skriver.

Skallen har en syntaks til at definere en lineær række af processer forbundet via en pipeline.

Kald til at læse fra en tom pipeline (som ikke indeholder bufferdata), der kun har den ene ende (alle skrivefilbeskrivelser lukket) returnerer "end of file". Skriveopkald i en lignende situation ignoreres.

Tidligst bevaret pipeline-implementering gælder til den femte udgave af Unix (juni 1974), men den er næsten identisk med den, der udkom i næste udgivelse. Kun tilføjede kommentarer, så den femte udgave kan springes over.

Unix sjette udgave (1975)

Begynder at læse Unix-kildekoden sjette udgave (maj 1975). I høj grad takket være Lions det er meget nemmere at finde end kilderne til tidligere versioner:

I mange år bogen Lions var det eneste dokument på Unix-kernen, der var tilgængeligt uden for Bell Labs. Selvom den sjette udgave licens tillod lærere at bruge dens kildekode, udelukkede den syvende udgave licens denne mulighed, så bogen blev distribueret i ulovlige maskinskrevne kopier.

I dag kan du købe et genoptrykt eksemplar af bogen, hvis omslag forestiller elever ved kopimaskinen. Og takket være Warren Toomey (som startede TUHS-projektet), kan du downloade Sjette udgave Kilde PDF. Jeg vil gerne give dig en idé om, hvor meget indsats der blev lagt i at oprette filen:

For over 15 år siden indtastede jeg en kopi af kildekoden Lionsfordi jeg ikke kunne lide kvaliteten af ​​mit eksemplar fra et ukendt antal andre kopier. TUHS eksisterede ikke endnu, og jeg havde ikke adgang til de gamle kilder. Men i 1988 fandt jeg et gammelt bånd med 9 spor, der havde en backup fra en PDP11 computer. Det var svært at vide, om det virkede, men der var et intakt /usr/src/-træ, hvori de fleste af filerne var markeret 1979, som allerede dengang så gammel ud. Det var den syvende udgave, eller en PWB-afledt, troede jeg.

Jeg tog fundet som grundlag og redigerede manuelt kilderne til tilstanden i den sjette udgave. En del af koden forblev den samme, en del skulle redigeres lidt, hvilket ændrede det moderne token += til det forældede =+. Noget blev simpelthen slettet, og noget skulle skrives helt om, men ikke for meget.

Og i dag kan vi online på TUHS læse kildekoden til sjette udgave af arkiv, som Dennis Ritchie havde en hånd med.

Forresten, ved første øjekast er hovedtræk ved C-koden før perioden med Kernighan og Ritchie dens korthed. Det er ikke ofte, at jeg er i stand til at indsætte kodestykker uden omfattende redigering for at passe til et relativt snævert visningsområde på mit websted.

Early /usr/sys/ken/pipe.c der er en forklarende kommentar (og ja, der er mere /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 ændret sig siden den fjerde udgave. Men her ser vi, uden offentlig dokumentation, at pipelines engang brugte filer som reservelager!

Hvad angår LARG filer, svarer de til inode-flag LARG, som bruges af den "store adresseringsalgoritme" til at behandle indirekte blokke for at understøtte større filsystemer. Da Ken sagde, at det er bedre ikke at bruge dem, så vil jeg gerne tage hans ord for det.

Her er det rigtige systemkald 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 tydeligt, hvad der sker her. Men det er ikke så let at forstå koden, blandt andet på grund af hvordan "struct bruger u» og registre R0 и R1 systemopkaldsparametre og returværdier videregives.

Lad os prøve med ialloc() placere på disken inode (inode), og med hjælp falloc() - gemme to fil. Hvis alt går vel, sætter vi flag for at identificere disse filer som de to ender af pipelinen, peger dem på den samme inode (hvis referenceantal bliver 2) og markerer inoden som ændret og i brug. Vær opmærksom på anmodninger om Jeg sætter() i fejlstier for at reducere referenceantallet i den nye inode.

pipe() forfalder igennem R0 и R1 returnere filbeskrivelsesnumre til læsning og skrivning. falloc() returnerer en pointer til en filstruktur, men "returnerer" også via u.u_ar0[R0] og en filbeskrivelse. Det vil sige, at koden er gemt i r filbeskrivelse til læsning og tildeler en deskriptor til skrivning direkte fra u.u_ar0[R0] efter andet opkald falloc().

flag FPIPE, som vi indstiller ved oprettelse af pipeline, styrer funktionsmåden rdwr() i sys2.c, som kalder specifikke 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);
    }
        /* … */
}

Derefter funktionen readp() в pipe.c læser data fra rørledningen. Men det er bedre at spore implementeringen fra writep(). Igen er koden blevet mere kompliceret på grund af arten af ​​argumentet, der passerer konventionen, men nogle detaljer kan udelades.

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 at skrive bytes til pipeline-inputtet u.u_count. Først skal vi låse inoden (se nedenfor plock/prele).

Derefter tjekker vi inodereferencetallet. Så længe begge ender af rørledningen forbliver åbne, skal tælleren være 2. Vi holder fast i ét led (fra rp->f_inode), så hvis tælleren er mindre end 2, skulle det betyde, at læseprocessen har lukket sin ende af pipelinen. Vi forsøger med andre ord at skrive til en lukket pipeline, hvilket er en fejl. Første fejlkode EPIPE og signal SIGPIPE dukkede op i den sjette udgave af Unix.

Men selvom transportøren er åben, kan den være fuld. I dette tilfælde frigiver vi låsen og går i dvale i håb om, at en anden proces vil læse fra rørledningen og frigøre nok plads i den. Når vi vågner, vender vi tilbage til begyndelsen, lukker låsen op igen og starter en ny skrivecyklus.

Hvis der er nok ledig plads i rørledningen, så skriver vi data til den ved hjælp af skriv(). Parameter i_size1 inode'a (med en tom pipeline kan være lig med 0) peger mod slutningen af ​​de data, som den allerede indeholder. Hvis der er plads nok til at skrive, kan vi fylde rørledningen fra i_size1 til PIPESIZ. Så slipper vi låsen og forsøger at vække enhver proces, der venter på at læse fra rørledningen. Vi går tilbage til begyndelsen for at se, om vi formåede at skrive så mange bytes, som vi havde brug for. Hvis ikke, så starter vi en ny optagelsescyklus.

Normalt parameter i_mode inode bruges til at gemme tilladelser r, w и x. Men i tilfælde af pipelines signalerer vi, at en eller anden proces venter på en skrivning eller læsning ved hjælp af bits IREAD и IWRITE henholdsvis. Processen sætter flaget og kalder sleep(), og det forventes, at en anden proces vil kalde i fremtiden wakeup().

Den virkelige magi sker i sleep() и wakeup(). De er implementeret i slp.c, kilden til den berømte "Du forventes ikke at forstå dette" kommentar. Heldigvis behøver vi ikke at forstå koden, bare se nogle 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) /* … */

Processen der kalder sleep() for en bestemt kanal, kan senere vækkes af en anden proces, som vil kalde wakeup() for samme kanal. writep() и readp() koordinere deres handlinger gennem sådanne parrede opkald. Noter det pipe.c altid prioritere PPIPE når der ringes op sleep(), så alle sleep() kan afbrydes af et signal.

Nu har vi alt for at forstå funktionen 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 vil måske finde det nemmere at læse denne funktion fra bund til top. "Læs og returner"-grenen bruges normalt, når der er nogle data i pipelinen. I dette tilfælde bruger vi Læs() læse så mange data, som er tilgængelige fra den nuværende f_offset læs, og opdater derefter værdien af ​​den tilsvarende offset.

Ved efterfølgende aflæsninger vil rørledningen være tom, hvis aflæsningsforskydningen er nået i_size1 ved inoden. Vi nulstiller positionen til 0 og forsøger at vække enhver proces, der ønsker at skrive til pipelinen. Vi ved, at når transportøren er fuld, writep() falde i søvn på ip+1. Og nu hvor pipelinen er tom, kan vi vække den for at genoptage skrivecyklussen.

Hvis der ikke er noget at læse, så readp() kan sætte et flag IREAD og falder i søvn ip+2. Vi ved, hvad der vil vække ham writep()når den skriver nogle data til pipelinen.

Kommentarer vedr læs() og skriv() vil hjælpe dig med at forstå, at i stedet for at sende parametre gennem "u»Vi kan behandle dem som almindelige I/O-funktioner, der tager en fil, en position, en buffer i hukommelsen og tæller antallet af bytes, der skal læses eller skrives.

/*
 * 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;
/* … */

Med hensyn til "konservativ" blokering, altså readp() и writep() lås inoder, indtil de er færdige eller opnår et resultat (dvs. ring wakeup). plock() и prele() arbejde enkelt: ved at bruge et andet sæt opkald sleep и wakeup tillad os at vække enhver proces, der har brug for den lås, vi lige har frigivet:

/*
 * 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årsager prele(ip) før opkaldet wakeup(ip+1). Den første ting writep() kalder i sin løkke, dette plock(ip), hvilket resulterer i et dødvande, hvis readp() har ikke fjernet sin blok endnu, så koden skal på en eller anden måde fungere korrekt. Hvis man ser på wakeup(), bliver det klart, at det kun markerer soveprocessen som klar til udførelse, således at i fremtiden sched() virkelig lanceret det. Så readp() årsager wakeup(), låser op, sætter IREAD og opkald sleep(ip+2)- alt dette før writep() genstarter cyklussen.

Dette fuldender beskrivelsen af ​​rørledninger i sjette udgave. Simpel kode, vidtrækkende implikationer.

Syvende udgave Unix (januar 1979) var en ny større udgivelse (fire år senere), der introducerede mange nye applikationer og kernefunktioner. Den har også gennemgået væsentlige ændringer i forbindelse med brug af typestøbning, fagforeninger og maskinskrevne pejlemærker til konstruktioner. Imidlertid rørledningskode ændrede sig praktisk talt ikke. Vi kan springe denne udgave over.

Xv6, en simpel Unix-lignende kerne

At skabe en kerne Xv6 påvirket af den sjette udgave af Unix, men skrevet i moderne C til at køre på x86-processorer. Koden er let at læse og forstå. I modsætning til Unix-kilder med TUHS kan du også kompilere det, ændre det og køre det på noget andet end PDP 11/70. Derfor er denne kerne meget brugt på universiteter som undervisningsmateriale om operativsystemer. Kilder er på Github.

Koden indeholder en klar og gennemtænkt implementering rør.c, understøttet af en buffer i hukommelsen i stedet for en inode på disken. Her giver jeg kun definitionen af ​​"strukturel rørledning" og funktionen 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() indstiller tilstanden for resten af ​​implementeringen, som inkluderer funktioner piperead(), pipewrite() и pipeclose(). Selve systemkaldet sys_pipe er en indpakning implementeret i sysfile.c. Jeg anbefaler at læse hele hans kode. Kompleksiteten er på niveau med kildekoden til den sjette udgave, men den er meget nemmere og mere behagelig at læse.

Linux 0.01

Du kan finde kildekoden til Linux 0.01. Det vil være lærerigt at studere implementeringen af ​​rørledninger i hans fs/pipe.c. Her bruges en inode til at repræsentere pipelinen, men selve pipelinen er skrevet i moderne C. Hvis du har hacket dig igennem den sjette udgave kode, får du ingen problemer her. Sådan ser funktionen ud 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;
}

Selv uden at se på strukturdefinitionerne, kan du finde ud af, hvordan inodereferencetællingen bruges til at kontrollere, om en skriveoperation resulterer i SIGPIPE. Ud over byte-for-byte-arbejde er denne funktion let at sammenligne med ovenstående ideer. Selv logik sleep_on/wake_up ser ikke så fremmed ud.

Moderne Linux-kerner, FreeBSD, NetBSD, OpenBSD

Jeg gik hurtigt over nogle moderne kerner. Ingen af ​​dem har allerede en disk-baseret implementering (ikke overraskende). Linux har sin egen implementering. Og selvom de tre moderne BSD-kerner indeholder implementeringer baseret på kode, der blev skrevet af John Dyson, er de i årenes løb blevet for forskellige fra hinanden.

At læse fs/pipe.c (på Linux) eller sys/kern/sys_pipe.c (på *BSD), det kræver virkelig dedikation. Ydeevne og understøttelse af funktioner såsom vektor og asynkron I/O er vigtig i kode i dag. Og detaljerne om hukommelsesallokering, låse og kernekonfiguration varierer alle meget. Det er ikke, hvad universiteterne har brug for til et introduktionskursus i operativsystemer.

Under alle omstændigheder var det interessant for mig at afdække et par gamle mønstre (for eksempel at generere SIGPIPE og vende tilbage EPIPE når du skriver til en lukket pipeline) i alle disse, så forskellige, moderne kerner. Jeg kommer nok aldrig til at se en PDP-11 computer live, men der er stadig meget at lære af den kode, der blev skrevet et par år før jeg blev født.

Skrevet af Divi Kapoor i 2011, artiklen "Linux Kernel-implementering af rør og FIFO'erer en oversigt over, hvordan Linux-pipelines (indtil videre) fungerer. EN seneste commit på linux illustrerer pipeline-modellen for interaktion, hvis muligheder overstiger midlertidige filers; og viser også, hvor langt pipelines er gået fra "meget konservativ låsning" i den sjette udgave af Unix-kernen.

Kilde: www.habr.com

Tilføj en kommentar