Hoe pyplyne in Unix geïmplementeer word

Hoe pyplyne in Unix geïmplementeer word
Hierdie artikel beskryf die implementering van pyplyne in die Unix-kern. Ek was ietwat teleurgesteld dat 'n onlangse artikel getiteld "Hoe werk pyplyne in Unix?" uitgedraai geen oor die interne struktuur. Ek het nuuskierig geraak en in ou bronne gegrawe om die antwoord te vind.

Waaroor praat ons?

Pyplyne is "waarskynlik die belangrikste uitvinding in Unix" - 'n bepalende kenmerk van Unix se onderliggende filosofie om klein programme saam te stel, en die bekende opdragreël slagspreuk:

$ echo hello | wc -c
6

Hierdie funksionaliteit hang af van die kern-verskafde stelseloproep pipe, wat op die dokumentasiebladsye beskryf word pyp (7) и pyp (2):

Pyplyne bied 'n eenrigtingkanaal vir interproseskommunikasie. Die pyplyn het 'n inset (skryf einde) en 'n uitset (lees einde). Data wat na die invoer van die pyplyn geskryf is, kan by die uitset gelees word.

Die pyplyn word geskep deur te bel pipe(2), wat twee lêerbeskrywers terugstuur: een verwys na die invoer van die pyplyn, die tweede na die uitvoer.

Die spooruitset van die bogenoemde opdrag toon die skepping van 'n pyplyn en die vloei van data daardeur van een proses na 'n ander:

$ 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

Die ouerproses roep pipe()om aangehegte lêerbeskrywings te kry. Een kinderproses skryf na een beskrywer en 'n ander proses lees dieselfde data van 'n ander beskrywer. Die dop "hernoem" beskrywings 2 en 3 met dup4 om by stdin en stdout te pas.

Sonder pyplyne sal die dop die uitvoer van een proses na 'n lêer moet skryf en dit na 'n ander proses moet pyp om die data van die lêer te lees. As gevolg hiervan sal ons meer hulpbronne en skyfspasie mors. Pyplyne is egter goed vir meer as om net tydelike lêers te vermy:

As 'n proses uit 'n leë pyplyn probeer lees, dan read(2) sal blokkeer totdat die data beskikbaar is. As 'n proses probeer om na 'n volledige pyplyn te skryf, dan write(2) sal blokkeer totdat genoeg data vanaf die pyplyn gelees is om die skryfwerk te voltooi.

Soos die POSIX-vereiste, is dit 'n belangrike eienskap: skryf na die pyplyn tot PIPE_BUF grepe (minstens 512) moet atoom wees sodat prosesse deur die pyplyn met mekaar kan kommunikeer op 'n manier wat normale lêers (wat nie sulke waarborge verskaf nie) nie kan nie.

Met 'n gewone lêer kan 'n proses al sy uitvoere daaraan skryf en dit aan 'n ander proses deurgee. Of prosesse kan in 'n harde parallelle modus werk, deur 'n eksterne seinmeganisme (soos 'n semafoor) te gebruik om mekaar in te lig oor die voltooiing van 'n skryf of lees. Transportbande red ons van al hierdie gesukkel.

Waarna soek ons?

Ek sal op my vingers verduidelik om dit vir jou makliker te maak om te dink hoe 'n vervoerband kan werk. Jy sal 'n buffer en 'n sekere toestand in die geheue moet toewys. Jy sal funksies nodig hê om data by die buffer by te voeg en te verwyder. Jy sal 'n mate van fasiliteit nodig hê om funksies te bel tydens lees- en skryfbewerkings op lêerbeskrywers. En slotte is nodig om die spesiale gedrag hierbo beskryf te implementeer.

Ons is nou gereed om die bronkode van die kern onder helder lamplig te ondersoek om ons vae verstandelike model te bevestig of te weerlê. Maar wees altyd voorbereid op die onverwagte.

Waar soek ons?

Ek weet nie waar my kopie van die bekende boek lê nie.Leeus boek« met Unix 6 bronkode, maar te danke aan Die Unix Heritage Society kan aanlyn gesoek word bronkode selfs ouer weergawes van Unix.

Om deur die TUHS-argiewe te dwaal is soos om 'n museum te besoek. Ons kan na ons gedeelde geskiedenis kyk en ek het respek vir die jare se poging om al hierdie materiaal bietjie vir bietjie van ou kassette en drukstukke te herwin. En ek is terdeë bewus van daardie fragmente wat nog vermis word.

Nadat ons ons nuuskierigheid oor die antieke geskiedenis van pypleidings bevredig het, kan ons na moderne kerns kyk vir vergelyking.

By the way, pipe is stelseloproepnommer 42 in die tabel sysent[]. Toeval?

Tradisionele Unix-pitte (1970–1974)

Ek het geen spoor gevind nie pipe(2) ook nie in nie PDP-7 Unix (Januarie 1970), ook nie in eerste uitgawe Unix (November 1971), ook nie in onvolledige bronkode nie tweede uitgawe (Junie 1972).

TUHS beweer dit derde uitgawe Unix (Februarie 1973) was die eerste weergawe met pyplyne:

Die derde uitgawe van Unix was die laaste weergawe met 'n kern geskryf in assembler, maar ook die eerste weergawe met pyplyne. Gedurende 1973 is daar gewerk om die derde uitgawe te verbeter, die kern is in C herskryf, en so is die vierde uitgawe van Unix gebore.

Een leser het 'n skandering van 'n dokument gevind waarin Doug McIlroy die idee voorgestel het om "programme soos 'n tuinslang te koppel."

Hoe pyplyne in Unix geïmplementeer word
In Brian Kernighan se boekUnix: 'n Geskiedenis en 'n Memoir”, die geskiedenis van die voorkoms van vervoerbande noem ook hierdie dokument: “... dit het vir 30 jaar aan die muur in my kantoor by Bell Labs gehang.” Hier onderhoud met McIlroyen nog 'n storie van McIlroy se werk, geskryf in 2014:

Toe Unix verskyn het, het my passie vir koroutines my die OS-outeur, Ken Thompson, laat vra om data wat na een of ander proses geskryf is, toe te laat om nie net na die toestel te gaan nie, maar ook na die uitgang na 'n ander proses. Ken het gedink dit is moontlik. As 'n minimalis wou hy egter hê dat elke stelselkenmerk 'n beduidende rol moes speel. Is direkte skryf tussen prosesse werklik 'n groot voordeel bo skryf na 'n tussenlêer? En eers toe ek 'n spesifieke voorstel met die pakkende naam "pyplyn" en 'n beskrywing van die sintaksis van die interaksie van prosesse maak, het Ken uiteindelik uitgeroep: "Ek sal dit doen!".

En het. Een noodlottige aand het Ken die kern en dop verander, verskeie standaardprogramme reggestel om te standaardiseer hoe hulle insette aanvaar (wat van 'n pyplyn kan kom), en lêername verander. Die volgende dag is pypleidings baie wyd in toepassings gebruik. Teen die einde van die week het die sekretaresses hulle gebruik om dokumente van woordverwerkers na die drukker te stuur. Ietwat later het Ken die oorspronklike API en sintaksis vir die gebruik van pyplyne vervang met skoner konvensies wat sedertdien gebruik is.

Ongelukkig is die bronkode vir die derde uitgawe Unix-kern verlore. En alhoewel ons die kernbronkode het wat in C geskryf is vierde uitgawe, wat in November 1973 vrygestel is, maar dit het 'n paar maande voor die amptelike vrystelling uitgekom en bevat nie die implementering van pypleidings nie. Dit is jammer dat die bronkode vir hierdie legendariese Unix-funksie verlore is, miskien vir altyd.

Ons het dokumentasie teks vir pipe(2) van beide vrystellings, sodat jy kan begin deur die dokumentasie te soek derde uitgawe (vir sekere woorde, "manueel onderstreep", 'n string ^H letterlike gevolge deur 'n onderstreep!). Hierdie proto-pipe(2) is in assembler geskryf en gee slegs een lêerbeskrywer terug, maar verskaf reeds die verwagte kernfunksies:

Stelseloproep pyp skep 'n I/O-meganisme wat 'n pyplyn genoem word. Die teruggestuurde lêerbeskrywing kan gebruik word vir lees- en skryfbewerkings. Wanneer iets na die pyplyn geskryf word, buffer dit tot 504 grepe data, waarna die skryfproses opgeskort word. Wanneer die pyplyn gelees word, word die gebufferde data geneem.

Teen die volgende jaar is die kern herskryf in C, en pyp(2) vierde uitgawe het sy moderne voorkoms verkry met die prototipe "pipe(fildes)'

Stelseloproep pyp skep 'n I/O-meganisme wat 'n pyplyn genoem word. Die teruggestuurde lêerbeskrywings kan in lees- en skryfbewerkings gebruik word. Wanneer iets na die pyplyn geskryf word, word die beskrywer wat in r1 (resp. fildes[1]) teruggestuur word, gebruik, gebuffer tot 4096 grepe data, waarna die skryfproses opgeskort word. Wanneer die pyplyn gelees word, neem die beskrywer terug na r0 (resp. fildes[0]) die data.

Daar word aanvaar dat sodra 'n pyplyn gedefinieer is, twee (of meer) interaksie prosesse (geskep deur daaropvolgende oproepe vurk) sal data vanaf die pyplyn deur oproepe deurgee lees и skryf.

Die dop het 'n sintaksis om 'n lineêre reeks prosesse wat via 'n pyplyn verbind is, te definieer.

Oproepe om te lees vanaf 'n leë pyplyn (wat geen gebufferde data bevat nie) wat net een kant het (alle skryflêerbeskrywings gesluit) gee "einde van lêer" terug. Skryfoproepe in 'n soortgelyke situasie word geïgnoreer.

Vroegste behoue ​​pypleiding implementering toepassing na die vyfde uitgawe van Unix (Junie 1974), maar dit is amper identies aan die een wat in die volgende uitgawe verskyn het. Slegs opmerkings bygevoeg, dus kan die vyfde uitgawe oorgeslaan word.

Unix sesde uitgawe (1975)

Begin om Unix-bronkode te lees sesde uitgawe (Mei 1975). Grootliks te danke aan Lions dit is baie makliker om te vind as die bronne van vroeëre weergawes:

Vir baie jare die boek Lions was die enigste dokument oor die Unix-kern wat buite Bell Labs beskikbaar was. Alhoewel die sesde uitgawe lisensie onderwysers toegelaat het om sy bronkode te gebruik, het die sewende uitgawe lisensie hierdie moontlikheid uitgesluit, sodat die boek in onwettige getikte kopieë versprei is.

Vandag kan jy ’n herdrukkopie van die boek koop, waarvan die omslag studente by die kopieerder uitbeeld. En te danke aan Warren Toomey (wat die TUHS-projek begin het), kan jy aflaai Sesde Uitgawe Bron PDF. Ek wil jou 'n idee gee van hoeveel moeite gedoen is om die lêer te skep:

Meer as 15 jaar gelede het ek 'n kopie van die bronkode wat in verskaf is, ingetik Lionsomdat ek nie van die kwaliteit van my kopie van 'n onbekende aantal ander kopieë gehou het nie. TUHS het nog nie bestaan ​​nie, en ek het nie toegang tot die ou bronne gehad nie. Maar in 1988 het ek 'n ou band met 9 snitte gekry wat 'n rugsteun van 'n PDP11-rekenaar gehad het. Dit was moeilik om te weet of dit werk, maar daar was 'n ongeskonde /usr/src/-boom waarin die meeste van die lêers 1979 gemerk is, wat selfs toe oud gelyk het. Dit was die sewende uitgawe, of 'n PWB-afgeleide, het ek gedink.

Ek het die vonds as basis geneem en die bronne handmatig geredigeer tot die toestand van die sesde uitgawe. 'n Deel van die kode het dieselfde gebly, 'n gedeelte moes effens geredigeer word, wat die moderne teken += verander het na die verouderde =+. Iets is eenvoudig uitgevee, en iets moes heeltemal oorgeskryf word, maar nie te veel nie.

En vandag kan ons aanlyn by TUHS die bronkode van die sesde uitgawe van lees argief, waaraan Dennis Ritchie 'n hand gehad het.

Terloops, met die eerste oogopslag is die hoofkenmerk van die C-kode voor die tydperk van Kernighan en Ritchie sy bondigheid. Dit is nie gereeld dat ek stukke kode kan invoeg sonder uitgebreide redigering om 'n relatief nou vertoonarea op my webwerf te pas nie.

Vroeë /usr/sys/ken/pipe.c daar is 'n verduidelikende opmerking (en ja, daar 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

Die buffergrootte het nie verander sedert die vierde uitgawe nie. Maar hier sien ons, sonder enige publieke dokumentasie, dat pypleidings eens lêers as terugvalberging gebruik het!

Wat LARG lêers betref, stem hulle ooreen met inode-vlag GROOT, wat deur die "groot adresseringsalgoritme" gebruik word om te verwerk indirekte blokke om groter lêerstelsels te ondersteun. Aangesien Ken gesê het dat dit beter is om dit nie te gebruik nie, sal ek graag sy woord daarvoor aanvaar.

Hier is die regte stelseloproep 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;
}

Die kommentaar beskryf duidelik wat hier gebeur. Maar dit is nie so maklik om die kode te verstaan ​​nie, deels as gevolg van hoe "struktureer gebruiker u» en registers R0 и R1 stelseloproepparameters en terugkeerwaardes word deurgegee.

Kom ons probeer met ialloc() plaas op skyf inode (inode), en met die hulp falloc() - stoor twee lêer. As alles goed gaan, sal ons vlae stel om hierdie lêers as die twee punte van die pyplyn te identifiseer, hulle na dieselfde inode (wie se verwysingtelling 2 word) wys en die inode as gewysig en in gebruik merk. Gee aandag aan versoeke om ek sit() in foutpaaie om die verwysingtelling in die nuwe inode te verlaag.

pipe() verskuldig deur R0 и R1 stuur lêerbeskrywernommers terug vir lees en skryf. falloc() gee 'n wyser terug na 'n lêerstruktuur, maar "stuur ook terug" via u.u_ar0[R0] en 'n lêerbeskrywing. Dit wil sê, die kode word gestoor in r lêerbeskrywer vir lees en ken 'n beskrywer toe vir skryf direk vanaf u.u_ar0[R0] na die tweede oproep falloc().

vlag FPIPE, wat ons stel wanneer ons die pyplyn skep, beheer die gedrag van die funksie rdwr() in sys2.c, wat spesifieke I/O-roetines noem:

/*
 * 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 die funksie readp() в pipe.c lees data uit die pyplyn. Maar dit is beter om die implementering op te spoor vanaf writep(). Weereens, die kode het meer ingewikkeld geword as gevolg van die aard van die argument wat konvensie deurstaan, maar sommige besonderhede kan weggelaat word.

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

Ons wil grepe na die pyplyninvoer skryf u.u_count. Eerstens moet ons die inode sluit (sien hieronder plock/prele).

Dan gaan ons die inode-verwysingtelling na. Solank beide punte van die pyplyn oop bly, moet die toonbank 2 wees. Ons hou vas aan een skakel (vanaf rp->f_inode), dus as die teller minder as 2 is, moet dit beteken dat die leesproses sy einde van die pyplyn gesluit het. Met ander woorde, ons probeer om na 'n geslote pyplyn te skryf, wat 'n fout is. Eerste foutkode EPIPE en sein SIGPIPE verskyn in die sesde uitgawe van Unix.

Maar selfs al is die vervoerband oop, kan dit vol wees. In hierdie geval maak ons ​​die slot los en gaan slaap in die hoop dat 'n ander proses uit die pyplyn sal lees en genoeg spasie daarin sal vrystel. Wanneer ons wakker word, keer ons terug na die begin, hang die slot weer op en begin 'n nuwe skryfsiklus.

As daar genoeg vrye spasie in die pyplyn is, dan skryf ons data aan dit met behulp van skryf ()... Parameter i_size1 die inode'a (met 'n leë pyplyn kan gelyk wees aan 0) wys na die einde van die data wat dit reeds bevat. As daar genoeg spasie is om te skryf, kan ons die pyplyn van vul i_size1 aan PIPESIZ. Dan los ons die slot en probeer om enige proses wat wag om uit die pyplyn te lees wakker te maak. Ons gaan terug na die begin om te sien of ons daarin geslaag het om soveel grepe te skryf as wat ons nodig gehad het. Indien nie, begin ons 'n nuwe opnamesiklus.

Gewoonlik parameter i_mode inode word gebruik om toestemmings te stoor r, w и x. Maar in die geval van pyplyne, beduie ons dat een of ander proses wag vir 'n skryf of lees met behulp van bisse IREAD и IWRITE onderskeidelik. Die proses stel die vlag en roep sleep(), en daar word verwag dat 'n ander proses in die toekoms sal oproep wakeup().

Die ware magie gebeur in sleep() и wakeup(). Hulle word geïmplementeer in slp.c, die bron van die bekende "Daar word nie van jou verwag om dit te verstaan ​​nie" opmerking. Gelukkig hoef ons nie die kode te verstaan ​​nie, kyk net na 'n paar opmerkings:

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

Die proses wat roep sleep() vir 'n spesifieke kanaal, kan later wakker gemaak word deur 'n ander proses, wat sal oproep wakeup() vir dieselfde kanaal. writep() и readp() koördineer hul optrede deur sulke gepaarde oproepe. Let daarop dat pipe.c altyd prioritiseer PPIPE wanneer geroep word sleep(), so almal sleep() kan deur 'n sein onderbreek word.

Nou het ons alles om die funksie te verstaan 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);
}

Jy sal dit dalk makliker vind om hierdie funksie van onder na bo te lees. Die "lees en terug"-tak word gewoonlik gebruik wanneer daar data in die pyplyn is. In hierdie geval gebruik ons lees () lees soveel data as wat beskikbaar is vanaf die huidige een f_offset lees, en werk dan die waarde van die ooreenstemmende offset op.

By daaropvolgende lesings sal die pyplyn leeg wees as die leesverskuiwing bereik het i_size1 by die inode. Ons stel die posisie terug na 0 en probeer enige proses wakker maak wat na die pyplyn wil skryf. Ons weet dat wanneer die vervoerband vol is, writep() aan die slaap raak ip+1. En noudat die pyplyn leeg is, kan ons dit wakker maak om sy skryfsiklus te hervat.

As daar niks is om te lees nie, dan readp() 'n vlag kan stel IREAD en aan die slaap raak ip+2. Ons weet wat hom sal wakker maak writep()wanneer dit sommige data na die pyplyn skryf.

Kommentaar op lees() en skryf() sal jou help om te verstaan ​​dat in plaas daarvan om parameters deur "u» ons kan hulle behandel soos gewone I/O-funksies wat 'n lêer, 'n posisie, 'n buffer in geheue neem, en die aantal grepe tel om te lees of te skryf.

/*
 * 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 "konserwatiewe" blokkering betref, dan readp() и writep() sluit inodes totdat hulle klaar is of 'n resultaat kry (d.w.s. bel wakeup). plock() и prele() werk eenvoudig: gebruik 'n ander stel oproepe sleep и wakeup laat ons toe om enige proses wakker te maak wat die slot nodig het wat ons pas vrygestel het:

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

Ek kon eers nie verstaan ​​hoekom nie readp() veroorsaak nie prele(ip) voor die oproep wakeup(ip+1). Die eerste ding writep() roep in sy lus, hierdie plock(ip), wat lei tot 'n dooiepunt as readp() het nog nie sy blok verwyder nie, so die kode moet op een of ander manier korrek werk. As jy kyk na wakeup(), word dit duidelik dat dit net die slaapproses as gereed vir uitvoering merk, sodat dit in die toekoms sched() het dit regtig van stapel gestuur. Dus readp() oorsake wakeup(), ontsluit, stel IREAD en oproepe sleep(ip+2)- dit alles voorheen writep() herbegin die siklus.

Dit voltooi die beskrywing van pypleidings in die sesde uitgawe. Eenvoudige kode, verreikende implikasies.

Sewende Uitgawe Unix (Januarie 1979) was 'n nuwe groot vrystelling (vier jaar later) wat baie nuwe toepassings en kernkenmerke bekendgestel het. Dit het ook aansienlike veranderinge ondergaan in verband met die gebruik van tipe gietwerk, vakbonde en getikte wysers na strukture. Egter pypleiding kode feitlik nie verander nie. Ons kan hierdie uitgawe oorslaan.

Xv6, 'n eenvoudige Unix-agtige kern

Om 'n kern te skep Xv6 beïnvloed deur die sesde uitgawe van Unix, maar geskryf in moderne C om op x86-verwerkers te werk. Die kode is maklik om te lees en verstaanbaar. Ook, anders as Unix-bronne met TUHS, kan jy dit saamstel, wysig en dit op iets anders as PDP 11/70 laat loop. Daarom word hierdie kern wyd in universiteite gebruik as onderrigmateriaal oor bedryfstelsels. Bronne is op Github.

Die kode bevat 'n duidelike en deurdagte implementering pyp.c, gerugsteun deur 'n buffer in die geheue in plaas van 'n inode op skyf. Hier gee ek slegs die definisie van "strukturele pyplyn" en die funksie 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() stel die toestand van al die res van die implementering, wat funksies insluit piperead(), pipewrite() и pipeclose(). Die werklike stelsel oproep sys_pipe is 'n omhulsel geïmplementeer in sysfile.c. Ek beveel aan om al sy kode te lees. Die kompleksiteit is op die vlak van die bronkode van die sesde uitgawe, maar dit is baie makliker en lekkerder om te lees.

Linux 0.01

U kan die bronkode vir Linux 0.01 vind. Dit sal leersaam wees om die implementering van pypleidings in sy te bestudeer fs/pipe.c. Hier word 'n inode gebruik om die pyplyn voor te stel, maar die pyplyn self is in moderne C geskryf. As jy jou pad deur die sesde uitgawe-kode gekap het, sal jy nie enige probleme hier hê nie. Dit is hoe die funksie lyk 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;
}

Selfs sonder om na die struktuurdefinisies te kyk, kan jy uitvind hoe die inode-verwysingtelling gebruik word om te kyk of 'n skryfbewerking tot gevolg het SIGPIPE. Benewens greep-vir-greep-werk, is hierdie funksie maklik om met bogenoemde idees te vergelyk. Selfs logika sleep_on/wake_up lyk nie so uitheems nie.

Moderne Linux-pitte, FreeBSD, NetBSD, OpenBSD

Ek het vinnig oor 'n paar moderne pitte gegaan. Nie een van hulle het reeds 'n skyfgebaseerde implementering nie (nie verbasend nie). Linux het sy eie implementering. En hoewel die drie moderne BSD-pitte implementerings bevat gebaseer op kode wat deur John Dyson geskryf is, het hulle oor die jare te verskillend van mekaar geword.

Om te lees fs/pipe.c (op Linux) of sys/kern/sys_pipe.c (op *BSD), dit verg werklike toewyding. Werkverrigting en ondersteuning vir kenmerke soos vektor en asinchroniese I/O is vandag belangrik in kode. En die besonderhede van geheuetoewysing, slotte en kernkonfigurasie verskil almal baie. Dit is nie wat universiteite nodig het vir 'n inleidende kursus oor bedryfstelsels nie.

Dit was in elk geval vir my interessant om 'n paar ou patrone op te grawe (byvoorbeeld generering SIGPIPE en keer terug EPIPE wanneer jy na 'n geslote pyplyn skryf) in al hierdie, so verskillende, moderne pitte. Ek sal seker nooit 'n PDP-11-rekenaar regstreeks sien nie, maar daar is nog baie om te leer uit die kode wat 'n paar jaar voor my geboorte geskryf is.

Geskryf deur Divi Kapoor in 2011, die artikel "Die Linux Kernel Implementering van Pype en EIEU'sis 'n oorsig van hoe Linux pyplyne (tot dusver) werk. A onlangse commit op Linux illustreer die pyplynmodel van interaksie, waarvan die vermoëns dié van tydelike lêers oorskry; en wys ook hoe ver pyplyne gegaan het van "baie konserwatiewe sluiting" in die sesde uitgawe Unix-kern.

Bron: will.com

Voeg 'n opmerking