Hoe pijplijnen worden geïmplementeerd in Unix

Hoe pijplijnen worden geïmplementeerd in Unix
Dit artikel beschrijft de implementatie van pijplijnen in de Unix-kernel. Ik was enigszins teleurgesteld dat een recent artikel met de titel "Hoe werken pijplijnen in Unix?» bleek geen over de interne structuur. Ik werd nieuwsgierig en dook in oude bronnen om het antwoord te vinden.

Waar hebben we het over?

Pipelines zijn "waarschijnlijk de belangrijkste uitvinding in Unix" - een bepalend kenmerk van Unix' onderliggende filosofie van het samenstellen van kleine programma's, en de bekende opdrachtregelslogan:

$ echo hello | wc -c
6

Deze functionaliteit is afhankelijk van de door de kernel geleverde systeemaanroep pipe, die wordt beschreven op de documentatiepagina's pijp(7) и pijp(2):

Pijpleidingen bieden een eenrichtingskanaal voor communicatie tussen processen. De pijplijn heeft een invoer (schrijfeinde) en een uitvoer (leeseinde). Gegevens die naar de ingang van de pijplijn zijn geschreven, kunnen aan de uitgang worden gelezen.

De pijplijn wordt gemaakt door aan te roepen pipe(2), die twee bestandsdescriptors retourneert: de ene verwijst naar de invoer van de pijplijn, de tweede naar de uitvoer.

De traceeruitvoer van de bovenstaande opdracht toont het maken van een pijplijn en de stroom van gegevens er doorheen van het ene proces naar het andere:

$ 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

De bovenliggende procesaanroepen pipe()om bijgevoegde bestandsbeschrijvingen te krijgen. Eén kindproces schrijft naar één descriptor en een ander proces leest dezelfde gegevens van een andere descriptor. De shell "hernoemt" descriptors 2 en 3 met dup4 om overeen te komen met stdin en stdout.

Zonder pijplijnen zou de shell de uitvoer van het ene proces naar een bestand moeten schrijven en naar een ander proces moeten leiden om de gegevens uit het bestand te lezen. Als gevolg hiervan zouden we meer bronnen en schijfruimte verspillen. Pijplijnen zijn echter goed voor meer dan alleen het vermijden van tijdelijke bestanden:

Als een proces probeert te lezen uit een lege pijplijn, dan read(2) zal blokkeren totdat de gegevens beschikbaar zijn. Als een proces naar een volledige pijplijn probeert te schrijven, dan write(2) zal blokkeren totdat er voldoende gegevens uit de pijplijn zijn gelezen om het schrijven te voltooien.

Net als de POSIX-vereiste is dit een belangrijke eigenschap: schrijven naar de pijplijn tot PIPE_BUF bytes (minstens 512) moeten atomair zijn, zodat processen via de pijplijn met elkaar kunnen communiceren op een manier die normale bestanden (die dergelijke garanties niet bieden) niet kunnen.

Met een regulier bestand kan een proces al zijn uitvoer ernaartoe schrijven en doorgeven aan een ander proces. Of processen kunnen in een harde parallelle modus werken, gebruikmakend van een extern signaleringsmechanisme (zoals een semafoor) om elkaar te informeren over de voltooiing van een schrijf- of leesbewerking. Transportbanden besparen ons al dit gedoe.

Wat zoeken we?

Ik zal het op mijn vingers uitleggen om het je gemakkelijker te maken je voor te stellen hoe een lopende band kan werken. U moet een buffer en een bepaalde status in het geheugen toewijzen. U hebt functies nodig om gegevens toe te voegen aan en te verwijderen uit de buffer. U hebt enige mogelijkheid nodig om functies aan te roepen tijdens lees- en schrijfbewerkingen op bestandsdescriptors. En sloten zijn nodig om het hierboven beschreven speciale gedrag te implementeren.

We zijn nu klaar om de broncode van de kernel onder fel lamplicht te ondervragen om ons vage mentale model te bevestigen of te weerleggen. Maar wees altijd voorbereid op het onverwachte.

Waar zoeken we?

Ik weet niet waar mijn exemplaar van het beroemde boek ligt.Leeuwen boek« met Unix 6-broncode, maar dankzij De Unix Erfgoed Maatschappij online kan worden gezocht broncode zelfs oudere versies van Unix.

Dwalen door de TUHS-archieven is als een bezoek aan een museum. We kunnen naar onze gedeelde geschiedenis kijken en ik heb respect voor de jarenlange inspanning om al dit materiaal beetje bij beetje terug te halen uit oude cassettes en afdrukken. En ik ben me terdege bewust van die fragmenten die nog ontbreken.

Nu we onze nieuwsgierigheid naar de oude geschiedenis van pijpleidingen hebben bevredigd, kunnen we ter vergelijking naar moderne kernen kijken.

Overigens pipe is systeemoproepnummer 42 in de tabel sysent[]. Toeval?

Traditionele Unix-kernels (1970-1974)

Ik heb geen spoor gevonden pipe(2) noch in PDP-7 Unix (januari 1970), noch in eerste editie Unix (november 1971), noch in onvolledige broncode tweede druk (juni 1972).

TUHS beweert dat derde editie Unix (februari 1973) was de eerste versie met pijpleidingen:

De derde editie van Unix was de laatste versie met een assembler-kernel, maar de eerste versie met pijplijnen. In 1973 werd gewerkt aan het verbeteren van de derde editie, de kernel werd herschreven in C, en zo werd de vierde editie van Unix geboren.

Een lezer vond een scan van een document waarin Doug McIlroy het idee opperde om "programma's als een tuinslang met elkaar te verbinden".

Hoe pijplijnen worden geïmplementeerd in Unix
In het boek van Brian KernighanUnix: een geschiedenis en een memoires”, de geschiedenis van het verschijnen van transportbanden vermeldt ook dit document: “... het hing 30 jaar aan de muur in mijn kantoor bij Bell Labs.” Hier interview met McIlroyen nog een verhaal van McIlroy's werk, geschreven in 2014:

Toen Unix verscheen, zorgde mijn passie voor coroutines ervoor dat ik de auteur van het besturingssysteem, Ken Thompson, vroeg om gegevens die naar een bepaald proces waren geschreven, niet alleen naar het apparaat te laten gaan, maar ook naar de uitgang naar een ander proces. Ken besloot dat het mogelijk was. Als minimalist wilde hij echter dat elk systeemkenmerk een belangrijke rol zou spelen. Is direct schrijven tussen processen echt een groot voordeel ten opzichte van schrijven naar een tussenliggend bestand? En pas toen ik een specifiek voorstel deed met een pakkende naam "pijplijn" en een beschrijving van de syntaxis van de interactie van processen, riep Ken uiteindelijk uit: "Ik zal het doen!".

En deed. Op een noodlottige avond veranderde Ken de kernel en shell, repareerde verschillende standaardprogramma's om te standaardiseren hoe ze invoer accepteren (die mogelijk afkomstig is van een pijplijn), en veranderde bestandsnamen. De volgende dag werden pijpleidingen zeer veel gebruikt in toepassingen. Tegen het einde van de week gebruikten de secretaresses ze om documenten van tekstverwerkers naar de printer te sturen. Iets later verving Ken de originele API en syntaxis voor het verpakken van het gebruik van pijplijnen door schonere conventies die sindsdien zijn gebruikt.

Helaas is de broncode voor de Unix-kernel van de derde editie verloren gegaan. En hoewel we de kernelbroncode hebben geschreven in C vierde druk, dat werd uitgebracht in november 1973, maar het kwam een ​​paar maanden voor de officiële release uit en bevat niet de implementatie van pijpleidingen. Het is jammer dat de broncode voor deze legendarische Unix-functie verloren is gegaan, misschien wel voor altijd.

We hebben documentatietekst voor pipe(2) van beide releases, dus u kunt beginnen met het doorzoeken van de documentatie derde editie (voor bepaalde woorden, "handmatig" onderstreept, een reeks ^H-letterwoorden gevolgd door een onderstrepingsteken!). Dit prototypepipe(2) is geschreven in assembler en retourneert slechts één bestandsdescriptor, maar biedt al de verwachte kernfunctionaliteit:

Systeemoproep pijp creëert een I/O-mechanisme dat een pijplijn wordt genoemd. De geretourneerde bestandsdescriptor kan worden gebruikt voor lees- en schrijfbewerkingen. Wanneer er iets naar de pijplijn wordt geschreven, buffert het maximaal 504 bytes aan gegevens, waarna het schrijfproces wordt opgeschort. Bij het lezen van de pijplijn worden de gebufferde gegevens genomen.

Het volgende jaar was de kernel herschreven in C, en pijp (2) vierde editie kreeg zijn moderne uitstraling met het prototype "pipe(fildes)'

Systeemoproep pijp creëert een I/O-mechanisme dat een pijplijn wordt genoemd. De geretourneerde bestandsdescriptors kunnen worden gebruikt bij lees- en schrijfbewerkingen. Wanneer er iets naar de pijplijn wordt geschreven, wordt de in r1 geretourneerde descriptor (resp. fildes[1]) gebruikt, gebufferd tot 4096 bytes aan gegevens, waarna het schrijfproces wordt opgeschort. Bij het lezen van de pijplijn neemt de descriptor terug naar r0 (resp. fildes[0]) de gegevens.

Aangenomen wordt dat zodra een pijplijn is gedefinieerd, twee (of meer) op elkaar inwerkende processen (gecreëerd door opeenvolgende aanroepen vork) zal gegevens van de pijplijn doorgeven met behulp van oproepen dit artikel lezen и schrijven.

De shell heeft een syntaxis voor het definiëren van een lineaire reeks processen die via een pijplijn zijn verbonden.

Oproepen om te lezen van een lege pijplijn (die geen gebufferde gegevens bevat) die maar één uiteinde heeft (alle schrijfbestandsdescriptors gesloten) retourneren "einde van bestand". Schrijfoproepen in een vergelijkbare situatie worden genegeerd.

vroegste bewaarde pijplijnimplementatie van toepassing naar de vijfde editie van Unix (juni 1974), maar het is bijna identiek aan degene die in de volgende release verscheen. Alleen reacties toegevoegd, dus de vijfde druk kan worden overgeslagen.

Unix zesde editie (1975)

Unix-broncode beginnen te lezen zesde editie (mei 1975). Grotendeels dankzij Lions het is veel gemakkelijker te vinden dan de bronnen van eerdere versies:

Al jaren het boek Lions was het enige document over de Unix-kernel dat buiten Bell Labs beschikbaar was. Hoewel de licentie van de zesde editie leraren toestond de broncode te gebruiken, sloot de licentie van de zevende editie deze mogelijkheid uit, dus werd het boek verspreid in illegale getypte exemplaren.

Vandaag kun je een herdruk van het boek kopen, waarvan de omslag studenten bij het kopieerapparaat afbeeldt. En dankzij Warren Toomey (die het TUHS-project startte) kun je downloaden Zesde editie bron-pdf. Ik wil je een idee geven van hoeveel moeite er is gestoken in het maken van het bestand:

Meer dan 15 jaar geleden typte ik een kopie van de broncode in Lionsomdat ik de kwaliteit van mijn exemplaar van een onbekend aantal andere exemplaren niet leuk vond. TUHS bestond nog niet en ik had geen toegang tot de oude bronnen. Maar in 1988 vond ik een oude band met 9 nummers die een back-up had van een PDP11-computer. Het was moeilijk om te weten of het werkte, maar er was een intacte /usr/src/ tree waarin de meeste bestanden gemarkeerd waren met 1979, wat er toen al oud uitzag. Het was de zevende editie, of een PWB-afgeleide, dacht ik.

Ik nam de vondst als basis en bewerkte de bronnen handmatig tot de staat van de zesde editie. Een deel van de code bleef hetzelfde, een deel moest enigszins worden aangepast, waarbij het moderne token += werd gewijzigd in het verouderde =+. Er werd gewoon iets verwijderd en er moest iets volledig worden herschreven, maar niet te veel.

En vandaag kunnen we online bij TUHS de broncode lezen van de zesde editie van archief, waaraan Dennis Ritchie een hand had.

Trouwens, op het eerste gezicht is het belangrijkste kenmerk van de C-code vóór de periode van Kernighan en Ritchie de beknoptheid. Het komt niet vaak voor dat ik codefragmenten kan invoegen zonder uitgebreide bewerkingen om in een relatief smal weergavegebied op mijn site te passen.

Vroeg /usr/sys/ken/pipe.c er is een verklarende opmerking (en ja, er is meer /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

De buffergrootte is sinds de vierde editie niet gewijzigd. Maar hier zien we, zonder enige openbare documentatie, dat pijplijnen ooit bestanden gebruikten als fallback-opslag!

Wat betreft LARG-bestanden, ze komen overeen met inode-vlag LARG, die wordt gebruikt door het "grote adresseringsalgoritme" om te verwerken indirecte blokken om grotere bestandssystemen te ondersteunen. Aangezien Ken zei dat het beter is om ze niet te gebruiken, zal ik hem graag op zijn woord geloven.

Hier is de echte systeemaanroep 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;
}

De opmerking beschrijft duidelijk wat hier gebeurt. Maar het is niet zo eenvoudig om de code te begrijpen, mede door hoe "struct gebruiker u» en registreert R0 и R1 systeemaanroepparameters en retourwaarden worden doorgegeven.

Laten we het proberen met alias() plaats op schijf inode (inode), en met de hulp valk() - bewaar er twee het dossier. Als alles goed gaat, zullen we vlaggen instellen om deze bestanden te identificeren als de twee uiteinden van de pijplijn, ze naar dezelfde inode verwijzen (waarvan het aantal referenties 2 wordt) en de inode markeren als gewijzigd en in gebruik. Let op verzoeken aan ik zet() in foutpaden om het aantal referenties in de nieuwe ino te verlagen.

pipe() verschuldigd door R0 и R1 geef bestandsdescriptornummers terug voor lezen en schrijven. falloc() geeft een pointer terug naar een bestandsstructuur, maar "retourneert" ook via u.u_ar0[R0] en een bestandsdescriptor. Dat wil zeggen, de code is opgeslagen in r bestandsdescriptor voor lezen en wijst een descriptor toe om rechtstreeks vanuit te schrijven u.u_ar0[R0] na tweede oproep falloc().

vlag FPIPE, die we instellen bij het maken van de pijplijn, bepaalt het gedrag van de functie rdwr() in sys2.c, die specifieke I / O-routines oproept:

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

Dan de functie readp() в pipe.c leest gegevens uit de pijplijn. Maar het is beter om de implementatie te traceren vanaf writep(). Nogmaals, de code is gecompliceerder geworden vanwege de aard van de conventie voor het doorgeven van argumenten, maar sommige details kunnen worden weggelaten.

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;
}

We willen bytes naar de pijplijninvoer schrijven u.u_count. Eerst moeten we de inode vergrendelen (zie hieronder plock/prele).

Vervolgens controleren we het aantal inode-referenties. Zolang beide uiteinden van de pijplijn open blijven, zou de teller op 2 moeten staan. We houden vast aan één schakel (van rp->f_inode), dus als de teller minder dan 2 is, zou dit moeten betekenen dat het leesproces het einde van de pijplijn heeft afgesloten. Met andere woorden, we proberen naar een gesloten pijplijn te schrijven, wat een vergissing is. Eerste foutcode EPIPE en signaal SIGPIPE verscheen in de zesde editie van Unix.

Maar zelfs als de transportband open is, kan deze vol zijn. In dit geval laten we het slot los en gaan we slapen in de hoop dat een ander proces uit de pijplijn leest en er voldoende ruimte in vrijmaakt. Als we wakker worden, keren we terug naar het begin, hangen het slot weer op en beginnen een nieuwe schrijfcyclus.

Als er voldoende vrije ruimte in de pijplijn is, schrijven we er gegevens naar met behulp van schrijf(). Parameter i_size1 de inode'a (met een lege pijplijn kan gelijk zijn aan 0) wijst naar het einde van de gegevens die het al bevat. Als er voldoende ruimte is om te schrijven, kunnen we de pijplijn vullen i_size1 naar PIPESIZ. Vervolgens geven we de vergrendeling vrij en proberen we elk proces wakker te maken dat wacht om uit de pijplijn te lezen. We gaan terug naar het begin om te zien of we erin geslaagd zijn om zoveel bytes te schrijven als we nodig hadden. Zo niet, dan starten we een nieuwe opnamecyclus.

Meestal parameters i_mode inode wordt gebruikt om permissies op te slaan r, w и x. Maar in het geval van pijplijnen geven we aan dat een proces wacht op schrijven of lezen met behulp van bits IREAD и IWRITE respectievelijk. Het proces stelt de vlag in en roept sleep(), en de verwachting is dat in de toekomst een ander proces zal aanroepen wakeup().

De echte magie gebeurt binnen sleep() и wakeup(). Ze zijn geïmplementeerd in slp.c, de bron van de beroemde opmerking "U wordt niet verwacht dit te begrijpen". Gelukkig hoeven we de code niet te begrijpen, kijk maar eens naar enkele opmerkingen:

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

Het proces dat roept sleep() voor een bepaald kanaal, kan later worden gewekt door een ander proces, dat zal aanroepen wakeup() voor hetzelfde kanaal. writep() и readp() coördineren hun acties door middel van dergelijke gepaarde oproepen. Let daar op pipe.c altijd prioriteit geven PPIPE wanneer gebeld sleep(), dus allemaal sleep() kan worden onderbroken door een signaal.

Nu hebben we alles om de functie te begrijpen 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);
}

Misschien vindt u het gemakkelijker om deze functie van beneden naar boven te lezen. De tak "lezen en retourneren" wordt meestal gebruikt als er wat gegevens in de pijplijn zitten. In dit geval gebruiken we lezen() lees zoveel gegevens als beschikbaar vanaf de huidige f_offset lezen en vervolgens de waarde van de overeenkomstige offset bijwerken.

Bij volgende uitlezingen is de pijplijn leeg als de leesoffset is bereikt i_size1 bij de inode. We resetten de positie naar 0 en proberen elk proces wakker te maken dat naar de pijplijn wil schrijven. We weten dat wanneer de transportband vol is, writep() in slaap vallen ip+1. En nu de pijplijn leeg is, kunnen we hem wakker maken om de schrijfcyclus te hervatten.

Als er niets te lezen valt, dan readp() kan een vlag plaatsen IREAD en in slaap vallen ip+2. We weten waardoor hij wakker zal worden writep()wanneer het wat gegevens naar de pijplijn schrijft.

Opmerkingen over lezen() en schrijven() zal u helpen begrijpen dat in plaats van parameters door "u» we kunnen ze behandelen als gewone I/O-functies die een bestand, een positie, een buffer in het geheugen nemen en het aantal bytes tellen dat moet worden gelezen of geschreven.

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

Wat betreft "conservatieve" blokkering, dan readp() и writep() vergrendel inodes totdat ze klaar zijn of een resultaat krijgen (d.w.z. call wakeup). plock() и prele() werk eenvoudig: gebruik een andere set oproepen sleep и wakeup laat ons elk proces wakker maken dat het slot nodig heeft dat we zojuist hebben vrijgegeven:

/*
 * 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);
    }
}

Eerst begreep ik niet waarom readp() Veroorzaakt geen prele(ip) voor de oproep wakeup(ip+1). Het eerste ding writep() roept in zijn lus, dit plock(ip), wat resulteert in een impasse als readp() heeft zijn blokkering nog niet verwijderd, dus de code moet op de een of andere manier correct werken. Als je kijkt naar wakeup(), wordt het duidelijk dat het alleen het slaapproces markeert als klaar voor uitvoering, dus dat in de toekomst sched() echt gelanceerd. Dus readp() oorzaken wakeup(), ontgrendelt, zet IREAD en roept sleep(ip+2)- dit alles eerder writep() start de cyclus opnieuw.

Hiermee is de beschrijving van pijpleidingen in de zesde editie voltooid. Eenvoudige code, verstrekkende gevolgen.

Zevende editie Unix (januari 1979) was een nieuwe grote release (vier jaar later) die veel nieuwe applicaties en kernelfuncties introduceerde. Het heeft ook aanzienlijke veranderingen ondergaan in verband met het gebruik van type-casting, vakbonden en getypte verwijzingen naar structuren. Echter pijplijn code praktisch niet veranderd. Deze editie kunnen we overslaan.

Xv6, een eenvoudige Unix-achtige kernel

Om een ​​kern te creëren Xv6 beïnvloed door de zesde editie van Unix, maar geschreven in moderne C om op x86-processors te draaien. De code is gemakkelijk te lezen en begrijpelijk. Ook kun je, in tegenstelling tot Unix-bronnen met TUHS, het compileren, wijzigen en uitvoeren op iets anders dan PDP 11/70. Daarom wordt deze kern veel gebruikt op universiteiten als lesmateriaal over besturingssystemen. Bronnen zijn op Github.

De code bevat een heldere en doordachte implementatie pijp.c, ondersteund door een buffer in het geheugen in plaats van een inode op schijf. Hier geef ik alleen de definitie van "structurele pijplijn" en de functie 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() stelt de status in van de rest van de implementatie, inclusief functies piperead(), pipewrite() и pipeclose(). De eigenlijke systeemoproep sys_pipe is een wrapper geïmplementeerd in sysbestand.c. Ik raad aan om al zijn code te lezen. De complexiteit zit op het niveau van de broncode van de zesde editie, maar is veel makkelijker en prettiger om te lezen.

Linux 0.01

U kunt de broncode voor Linux 0.01 vinden. Het zal leerzaam zijn om de implementatie van pijpleidingen in zijn te bestuderen fs/pipe.c. Hier wordt een inode gebruikt om de pijplijn weer te geven, maar de pijplijn zelf is geschreven in moderne C. Als je je een weg door de code van de zesde editie hebt gehackt, zul je hier geen problemen hebben. Zo ziet de functie eruit 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;
}

Zelfs zonder naar de struct-definities te kijken, kunt u erachter komen hoe het aantal inode-referenties wordt gebruikt om te controleren of een schrijfbewerking resulteert in SIGPIPE. Naast byte-voor-byte werk is deze functie goed te vergelijken met bovenstaande ideeën. Zelfs logica sleep_on/wake_up ziet er niet zo vreemd uit.

Moderne Linux-kernels, FreeBSD, NetBSD, OpenBSD

Ik heb snel enkele moderne kernels doorgenomen. Geen van hen heeft al een schijfgebaseerde implementatie (niet verwonderlijk). Linux heeft zijn eigen implementatie. En hoewel de drie moderne BSD-kernels implementaties bevatten die zijn gebaseerd op code die is geschreven door John Dyson, zijn ze in de loop der jaren te veel van elkaar gaan verschillen.

Lezen fs/pipe.c (op Linux) of sys/kern/sys_pipe.c (op *BSD), er is echte toewijding voor nodig. Prestaties en ondersteuning voor functies zoals vector en asynchrone I/O zijn tegenwoordig belangrijk in code. En de details van geheugentoewijzing, vergrendelingen en kernelconfiguratie variëren allemaal enorm. Dat hebben universiteiten niet nodig voor een introductiecursus besturingssystemen.

Het was voor mij in ieder geval interessant om een ​​paar oude patronen aan het licht te brengen (bijvoorbeeld het genereren van SIGPIPE En terugkomen EPIPE bij het schrijven naar een gesloten pijplijn) in al deze, zo verschillende, moderne kernels. Ik zal waarschijnlijk nooit een PDP-11-computer live zien, maar er valt nog veel te leren van de code die een paar jaar voordat ik werd geboren werd geschreven.

Geschreven door Divi Kapoor in 2011, het artikel "De Linux Kernel Implementatie van Pipes en FIFO'sis een overzicht van hoe Linux-pijplijnen (tot nu toe) werken. A recente commit op linux illustreert het pijplijnmodel van interactie, waarvan de mogelijkheden die van tijdelijke bestanden overtreffen; en laat ook zien hoe ver pijplijnen zijn gegaan van "zeer conservatieve vergrendeling" in de zesde editie Unix-kernel.

Bron: www.habr.com

Voeg een reactie