Hur pipelines implementeras i Unix

Hur pipelines implementeras i Unix
Den här artikeln beskriver implementeringen av pipelines i Unix-kärnan. Jag blev något besviken över att en artikel nyligen hade rubriken "Hur fungerar pipelines i Unix?" visade sig ingen om den inre strukturen. Jag blev nyfiken och grävde i gamla källor för att hitta svaret.

Vad pratar vi om?

Pipelines är "förmodligen den viktigaste uppfinningen i Unix" - ett avgörande inslag i Unix's underliggande filosofi att sätta ihop små program, och den välbekanta kommandoradssloganen:

$ echo hello | wc -c
6

Denna funktion beror på det systemanrop som tillhandahålls av kärnan pipe, som beskrivs på dokumentationssidorna rör (7) и rör (2):

Pipelines tillhandahåller en enkelriktad kanal för kommunikation mellan processer. Pipelinen har en ingång (skrivände) och en utgång (läsände). Data som skrivs till pipelinens ingång kan läsas vid utgången.

Pipelinen skapas genom att anropa pipe(2), som returnerar två filbeskrivningar: den ena hänvisar till pipelinens input, den andra till utgången.

Spårningsutgången från kommandot ovan visar skapandet av en pipeline och flödet av data genom den från en process till en annan:

$ 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

Föräldraprocessen anropar pipe()för att få bifogade filbeskrivningar. En underordnad process skriver till en deskriptor och en annan process läser samma data från en annan deskriptor. Skalet "döper om" beskrivningarna 2 och 3 med dup4 för att matcha stdin och stdout.

Utan pipelines skulle skalet behöva skriva utdata från en process till en fil och skicka den till en annan process för att läsa data från filen. Som ett resultat skulle vi slösa mer resurser och diskutrymme. Men pipelines är bra för mer än att bara undvika tillfälliga filer:

Om en process försöker läsa från en tom pipeline, då read(2) kommer att blockera tills uppgifterna är tillgängliga. Om en process försöker skriva till en fullständig pipeline, då write(2) kommer att blockera tills tillräckligt med data har lästs från pipeline för att slutföra skrivningen.

Liksom POSIX-kravet är detta en viktig egenskap: att skriva till pipeline upp till PIPE_BUF byte (minst 512) måste vara atomära så att processer kan kommunicera med varandra genom pipelinen på ett sätt som vanliga filer (som inte ger sådana garantier) inte kan.

Med en vanlig fil kan en process skriva all utdata till den och skicka den vidare till en annan process. Eller processer kan arbeta i ett hårt parallellt läge, med hjälp av en extern signaleringsmekanism (som en semafor) för att informera varandra om slutförandet av en skrivning eller läsning. Transportörer räddar oss från allt detta krångel.

Vad letar vi efter?

Jag kommer att förklara på mina fingrar för att göra det lättare för dig att föreställa dig hur en transportör kan fungera. Du kommer att behöva allokera en buffert och något tillstånd i minnet. Du behöver funktioner för att lägga till och ta bort data från bufferten. Du kommer att behöva någon möjlighet att anropa funktioner under läs- och skrivoperationer på filbeskrivningar. Och lås behövs för att implementera det speciella beteendet som beskrivs ovan.

Vi är nu redo att förhöra källkoden för kärnan under starkt lampljus för att bekräfta eller motbevisa vår vaga mentala modell. Men var alltid beredd på det oväntade.

Var letar vi?

Jag vet inte var mitt exemplar av den berömda boken ligger.Lions bok« med Unix 6 källkod, men tack vare Unix Heritage Society kan sökas på nätet källkod ännu äldre versioner av Unix.

Att vandra genom TUHS arkiv är som att besöka ett museum. Vi kan titta på vår delade historia och jag har respekt för åratal av ansträngningar att bit för bit återvinna allt detta material från gamla kassetter och utskrifter. Och jag är mycket medveten om de fragment som fortfarande saknas.

Efter att ha tillfredsställt vår nyfikenhet om rörledningarnas gamla historia kan vi titta på moderna kärnor för jämförelse.

Förresten, pipe är systemanrop 42 i tabellen sysent[]. Tillfällighet?

Traditionella Unix-kärnor (1970–1974)

Jag hittade inga spår pipe(2) varken i PDP-7 Unix (januari 1970), inte heller i första upplagan av Unix (november 1971), inte heller i ofullständig källkod andra upplagan (juni 1972).

TUHS hävdar det tredje upplagan av Unix (februari 1973) var den första versionen med pipelines:

Den tredje upplagan av Unix var den sista versionen med en kärna skriven i assembler, men också den första versionen med pipelines. Under 1973 pågick arbetet med att förbättra den tredje upplagan, kärnan skrevs om i C, och därmed föddes den fjärde upplagan av Unix.

En läsare hittade en skanning av ett dokument där Doug McIlroy föreslog idén om att "ansluta program som en trädgårdsslang."

Hur pipelines implementeras i Unix
I Brian Kernighans bokUnix: A History and a Memoir", historien om transportörernas utseende nämner också detta dokument: "... det hängde på väggen på mitt kontor på Bell Labs i 30 år." Här intervju med McIlroyoch en annan berättelse från McIlroys verk, skrivet 2014:

När Unix dök upp fick min passion för coroutines mig att fråga OS-författaren, Ken Thompson, att tillåta data som skrivits till någon process att gå inte bara till enheten utan också till utgången till en annan process. Ken trodde att det var möjligt. Men som minimalist ville han att varje systemfunktion skulle spela en betydande roll. Är direktskrivning mellan processer verkligen en stor fördel jämfört med att skriva till en mellanfil? Och först när jag gjorde ett specifikt förslag med det catchy namnet "pipeline" och en beskrivning av syntaxen för processers interaktion, utbrast Ken till slut: "I will do it!".

Och gjorde. En ödesdiger kväll ändrade Ken kärnan och skalet, fixade flera standardprogram för att standardisera hur de accepterar input (som kan komma från en pipeline) och ändrade filnamn. Nästa dag användes rörledningar mycket i applikationer. I slutet av veckan använde sekreterarna dem för att skicka dokument från ordbehandlare till tryckeriet. Något senare ersatte Ken det ursprungliga API:et och syntaxen för att omsluta användningen av pipelines med renare konventioner som har använts sedan dess.

Tyvärr har källkoden för den tredje upplagan av Unix-kärnan gått förlorad. Och även om vi har kärnans källkod skriven i C fjärde upplagan, som släpptes i november 1973, men den kom ut några månader före den officiella releasen och innehåller inte implementeringen av pipelines. Det är synd att källkoden för denna legendariska Unix-funktion går förlorad, kanske för alltid.

Vi har dokumentationstext för pipe(2) från båda utgåvorna, så du kan börja med att söka i dokumentationen tredje upplagan (för vissa ord, understruket "manuellt", en sträng av ^H bokstaver följt av ett understreck!). Detta proto-pipe(2) är skrivet i assembler och returnerar endast en filbeskrivning, men tillhandahåller redan den förväntade kärnfunktionaliteten:

Systemanrop Röret skapar en I/O-mekanism som kallas en pipeline. Den returnerade filbeskrivningen kan användas för läs- och skrivoperationer. När något skrivs till pipelinen buffrar det upp till 504 byte data, varefter skrivprocessen avbryts. När man läser från pipelinen tas buffrad data.

Året efter hade kärnan skrivits om i C, och pipe(2) fjärde upplagan fick sitt moderna utseende med prototypen "pipe(fildes)»:

Systemanrop Röret skapar en I/O-mekanism som kallas en pipeline. De returnerade filbeskrivningarna kan användas i läs- och skrivoperationer. När något skrivs till pipelinen används deskriptorn som returneras i r1 (resp. fildes[1]), buffrad upp till 4096 byte med data, varefter skrivprocessen avbryts. När man läser från pipeline tar deskriptorn tillbaka till r0 (resp. fildes[0]) data.

Det antas att när en pipeline väl har definierats, två (eller flera) interagerande processer (skapade av efterföljande anrop gaffel) kommer att skicka data från pipeline med samtal läsa и skriva.

Skalet har en syntax för att definiera en linjär array av processer kopplade via en pipeline.

Anrop att läsa från en tom pipeline (som inte innehåller någon buffrad data) som bara har en ände (alla skrivfilsbeskrivningar stängda) returnerar "slut på fil". Skrivsamtal i liknande situation ignoreras.

Tidigast bevarad pipelineimplementering applicerar till den femte upplagan av Unix (juni 1974), men den är nästan identisk med den som dök upp i nästa utgåva. Endast lagt till kommentarer, så den femte upplagan kan hoppas över.

Unix sjätte upplagan (1975)

Börjar läsa Unix-källkod sjätte upplagan (maj 1975). Till stor del tack vare Lions det är mycket lättare att hitta än källorna till tidigare versioner:

I många år boken Lions var det enda dokumentet på Unix-kärnan som var tillgängligt utanför Bell Labs. Även om den sjätte upplagan licensen tillät lärare att använda dess källkod, uteslöt den sjunde upplagan denna möjlighet, så boken distribuerades i olagliga maskinskrivna kopior.

Idag kan du köpa ett nytryckt exemplar av boken, vars omslag föreställer elever vid kopiatorn. Och tack vare Warren Toomey (som startade TUHS-projektet) kan du ladda ner Sjätte upplagan Källa PDF. Jag vill ge dig en uppfattning om hur mycket ansträngning som lagts ner på att skapa filen:

För över 15 år sedan skrev jag in en kopia av källkoden som tillhandahålls i Lionseftersom jag inte gillade kvaliteten på min kopia från ett okänt antal andra kopior. TUHS fanns inte ännu, och jag hade inte tillgång till de gamla källorna. Men 1988 hittade jag ett gammalt band med 9 spår som hade en backup från en PDP11-dator. Det var svårt att veta om det fungerade, men det fanns ett intakt /usr/src/-träd där de flesta filerna var märkta 1979, som redan då såg uråldriga ut. Det var den sjunde upplagan, eller ett PWB-derivat, trodde jag.

Jag tog fyndet som grund och redigerade manuellt källorna till tillståndet för den sjätte upplagan. En del av koden förblev densamma, en del måste redigeras något, vilket ändrade den moderna token += till den föråldrade =+. Något raderades helt enkelt och något måste skrivas om helt, men inte för mycket.

Och idag kan vi läsa online på TUHS källkoden för den sjätte upplagan av arkiv, som Dennis Ritchie hade en hand till.

Förresten, vid första anblicken är huvuddraget i C-koden före Kernighans och Ritchies period dess korthet. Det är inte ofta som jag kan infoga kodavsnitt utan omfattande redigering för att passa ett relativt smalt visningsområde på min webbplats.

Tidig /usr/sys/ken/pipe.c det finns en förklarande kommentar (och ja, det finns 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

Buffertstorleken har inte ändrats sedan den fjärde upplagan. Men här ser vi, utan någon offentlig dokumentation, att pipelines en gång använde filer som reservlagring!

När det gäller LARG filer motsvarar de inod-flagga LARG, som används av den "stora adresseringsalgoritmen" för att bearbeta indirekta block för att stödja större filsystem. Eftersom Ken sa att det är bättre att inte använda dem, tar jag gärna hans ord för det.

Här är det verkliga 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 tydligt vad som händer här. Men det är inte så lätt att förstå koden, delvis på grund av hur "strukturera användare u» och register R0 и R1 systemanropsparametrar och returvärden skickas.

Låt oss försöka med ialloc() placera på disken inod (inod), och med hjälp falloc() - lagra två fil. Om allt går bra kommer vi att sätta flaggor för att identifiera dessa filer som de två ändarna av pipelinen, peka dem mot samma inod (vars referensantal blir 2) och markera inoden som modifierad och i bruk. Var uppmärksam på förfrågningar till jag lägger() i felvägar för att minska referensantalet i den nya inoden.

pipe() förfaller genom R0 и R1 returnera filbeskrivningsnummer för läsning och skrivning. falloc() returnerar en pekare till en filstruktur, men "returerar" också via u.u_ar0[R0] och en filbeskrivning. Det vill säga koden lagras i r filbeskrivning för läsning och tilldelar en deskriptor för att skriva direkt från u.u_ar0[R0] efter andra samtalet falloc().

flag FPIPE, som vi ställer in när vi skapar pipelinen, kontrollerar funktionens beteende rdwr() i sys2.c, som anropar specifika 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);
    }
        /* … */
}

Sedan funktionen readp() в pipe.c läser data från pipeline. Men det är bättre att spåra implementeringen från writep(). Återigen har koden blivit mer komplicerad på grund av arten av argumentet som passerar konventionen, men vissa detaljer kan utelämnas.

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 vill skriva bytes till pipeline-ingången u.u_count. Först måste vi låsa inoden (se nedan plock/prele).

Sedan kontrollerar vi inodreferensantalet. Så länge som båda ändarna av rörledningen förblir öppna bör räknaren vara 2. Vi håller fast vid en länk (från rp->f_inode), så om räknaren är mindre än 2, bör detta betyda att läsningsprocessen har stängt slutet av pipelinen. Med andra ord, vi försöker skriva till en stängd pipeline, vilket är ett misstag. Första felkoden EPIPE och signalera SIGPIPE dök upp i den sjätte upplagan av Unix.

Men även om transportören är öppen kan den vara full. I det här fallet släpper vi låset och går och lägger oss i hopp om att en annan process ska läsa från rörledningen och frigöra tillräckligt med utrymme i den. När vi vaknar går vi tillbaka till början, lägger på låset igen och startar en ny skrivcykel.

Om det finns tillräckligt med ledigt utrymme i pipelinen, skriver vi data till den med hjälp av skriv(). Parameter i_size1 inode'a (med en tom pipeline kan vara lika med 0) pekar mot slutet av data som den redan innehåller. Om det finns tillräckligt med utrymme att skriva kan vi fylla pipelinen från i_size1 до PIPESIZ. Sedan släpper vi låset och försöker väcka alla processer som väntar på att läsas från pipelinen. Vi går tillbaka till början för att se om vi lyckades skriva så många byte som vi behövde. Om inte, startar vi en ny inspelningscykel.

Vanligtvis parameter i_mode inode används för att lagra behörigheter r, w и x. Men när det gäller pipelines signalerar vi att någon process väntar på skrivning eller läsning med hjälp av bitar IREAD и IWRITE respektive. Processen sätter flaggan och anropar sleep(), och det förväntas att någon annan process kommer att krävas i framtiden wakeup().

Den verkliga magin händer i sleep() и wakeup(). De implementeras i slp.c, källan till den berömda kommentaren "Du förväntas inte förstå det här". Som tur är behöver vi inte förstå koden, titta bara på några 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 som kallar sleep() för en viss kanal, kan senare väckas av en annan process, som kommer att anropa wakeup() för samma kanal. writep() и readp() samordna sina handlingar genom sådana parade samtal. anteckna det pipe.c alltid prioritera PPIPE när man ringer sleep(), så alla sleep() kan avbrytas av en signal.

Nu har vi allt för att förstå 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 kanske tycker att det är lättare att läsa den här funktionen nerifrån och upp. Grenen "läs och returnera" används vanligtvis när det finns en del data i pipelinen. I det här fallet använder vi läsa() läs så mycket data som är tillgängligt från den nuvarande f_offset läs och uppdatera sedan värdet för motsvarande offset.

Vid efterföljande avläsningar kommer pipelinen att vara tom om läsoffset har nåtts i_size1 vid inoden. Vi återställer positionen till 0 och försöker väcka alla processer som vill skriva till pipelinen. Vi vet att när transportören är full, writep() somna på ip+1. Och nu när pipelinen är tom kan vi väcka den för att återuppta skrivcykeln.

Om det inte finns något att läsa, då readp() kan sätta en flagga IREAD och somna vidare ip+2. Vi vet vad som kommer att väcka honom writep()när den skriver lite data till pipelinen.

Kommentarer på read() och writei() hjälper dig att förstå att istället för att skicka parametrar genom "u» vi kan behandla dem som vanliga I/O-funktioner som tar en fil, en position, en buffert i minnet och räknar antalet byte att läsa eller skriva.

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

Vad gäller "konservativ" blockering alltså readp() и writep() lås inoder tills de slutar eller får ett resultat (dvs. ring wakeup). plock() и prele() arbeta enkelt: med en annan uppsättning samtal sleep и wakeup låt oss väcka alla processer som behöver låset vi just släppte:

/*
 * 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 kunde jag inte förstå varför readp() inte orsakar prele(ip) innan samtalet wakeup(ip+1). Den första saken writep() anropar i sin loop, detta plock(ip), vilket resulterar i ett dödläge om readp() har inte tagit bort blocket ännu, så koden måste på något sätt fungera korrekt. Om man tittar på wakeup(), blir det tydligt att det bara markerar sömnprocessen som redo för utförande, så att i framtiden sched() har verkligen lanserat det. Så readp() orsaker wakeup(), låser upp, ställer in IREAD och samtal sleep(ip+2)- allt detta tidigare writep() startar om cykeln.

Detta kompletterar beskrivningen av pipelines i den sjätte upplagan. Enkel kod, långtgående konsekvenser.

Sjunde upplagan av Unix (januari 1979) var en ny stor utgåva (fyra år senare) som introducerade många nya applikationer och kärnfunktioner. Den har också genomgått betydande förändringar i samband med användning av typgjutning, fackföreningar och maskinskrivna pekare till strukturer. dock rörledningskod praktiskt taget inte förändrats. Vi kan hoppa över den här upplagan.

Xv6, en enkel Unix-liknande kärna

Att skapa en kärna Xv6 influerad av den sjätte upplagan av Unix, men skriven i modern C för att köras på x86-processorer. Koden är lätt att läsa och förståelig. Dessutom, till skillnad från Unix-källor med TUHS, kan du kompilera den, modifiera den och köra den på något annat än PDP 11/70. Därför används denna kärna flitigt på universiteten som ett undervisningsmaterial om operativsystem. Källor finns på Github.

Koden innehåller en tydlig och genomtänkt implementering pipe.c, uppbackad av en buffert i minnet istället för en inod på disken. Här ger jag bara definitionen av "strukturell pipeline" och 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() ställer in tillståndet för resten av implementeringen, som inkluderar funktioner piperead(), pipewrite() и pipeclose(). Själva systemanropet sys_pipe är ett omslag implementerat i sysfile.c. Jag rekommenderar att du läser hela hans kod. Komplexiteten är på nivån med källkoden för den sjätte upplagan, men den är mycket lättare och trevligare att läsa.

Linux 0.01

Du kan hitta källkoden för Linux 0.01. Det kommer att vara lärorikt att studera genomförandet av pipelines i hans fs/pipe.c. Här används en inod för att representera pipelinen, men själva pipelinen är skriven i modern C. Om du har hackat dig igenom den sjätte utgåvans kod kommer du inte att ha några problem här. Så här ser funktionen 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;
}

Även utan att titta på strukturdefinitionerna kan du ta reda på hur inodreferensräkningen används för att kontrollera om en skrivoperation resulterar i SIGPIPE. Förutom byte-byte-arbete är denna funktion lätt att jämföra med ovanstående idéer. Även logik sleep_on/wake_up ser inte så främmande ut.

Moderna Linux-kärnor, FreeBSD, NetBSD, OpenBSD

Jag gick snabbt över några moderna kärnor. Ingen av dem har redan en diskbaserad implementering (inte överraskande). Linux har sin egen implementering. Och även om de tre moderna BSD-kärnorna innehåller implementeringar baserade på kod som skrevs av John Dyson, har de under åren blivit alltför olika varandra.

Att läsa fs/pipe.c (på Linux) eller sys/kern/sys_pipe.c (på *BSD), det kräver verklig hängivenhet. Prestanda och stöd för funktioner som vektor och asynkron I/O är viktigt i kod idag. Och detaljerna för minnesallokering, lås och kärnkonfiguration varierar mycket. Detta är inte vad universitet behöver för en introduktionskurs om operativsystem.

Hur som helst var det intressant för mig att gräva fram några gamla mönster (till exempel generera SIGPIPE och återvänd EPIPE när du skriver till en sluten pipeline) i alla dessa, så olika, moderna kärnor. Jag kommer nog aldrig att se en PDP-11-dator live, men det finns fortfarande mycket att lära av koden som skrevs några år innan jag föddes.

Artikeln skrevs av Divi Kapoor 2011 "Linux Kernel-implementering av rör och FIFOär en översikt över hur Linux-pipelines (hittills) fungerar. A senaste commit på linux illustrerar pipelinemodellen för interaktion, vars kapacitet överstiger temporära filer; och visar också hur långt pipelines har gått från "mycket konservativ låsning" i den sjätte upplagan av Unix-kärnan.

Källa: will.com

Lägg en kommentar