Hoe pipelines wurde ymplementearre yn Unix

Hoe pipelines wurde ymplementearre yn Unix
Dit artikel beskriuwt de ymplemintaasje fan pipelines yn 'e Unix-kernel. Ik wie wat teloarsteld dat in resint artikel mei de titel "Hoe wurkje pipelines yn Unix?" it draaide út op net oer de ynterne struktuer. Ik waard nijsgjirrich en groeven yn âlde boarnen om it antwurd te finen.

Wêr hawwe wy it oer?

Pipelines binne "wierskynlik de wichtichste útfining yn Unix" - in definiearjend skaaimerk fan Unix's ûnderlizzende filosofy fan it gearstallen fan lytse programma's, en de bekende kommando-rigel slogan:

$ echo hello | wc -c
6

Dizze funksjonaliteit hinget ôf fan 'e kernel-fersoarge systeemoprop pipe, dat wurdt beskreaun op de dokumintaasje siden pipe (7) и pipe (2):

Pipelines jouwe in ien-wei kanaal foar kommunikaasje tusken prosessen. De pipeline hat in ynfier (skriuwein) en in útfier (lêsein). Gegevens skreaun nei de ynfier fan 'e pipeline kinne wurde lêzen by de útfier.

De pipeline wurdt makke troch te roppen pipe(2), dy't twa triembeskriuwers werombringt: ien ferwiist nei de ynfier fan 'e pipeline, de twadde nei de útfier.

De spoarútfier fan it boppesteande kommando toant it oanmeitsjen fan in pipeline en de stream fan gegevens dertroch fan it iene proses nei it oare:

$ 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 âlder proses ropt pipe()om taheakke triembeskriuwings te krijen. Ien bernproses skriuwt nei ien descriptor en in oar proses lêst deselde gegevens fan in oare descriptor. De shell "omneamt" descriptors 2 en 3 mei dup4 om te passen stdin en stdout.

Sûnder pipelines soe de shell it resultaat fan ien proses nei in bestân skriuwe moatte en it nei in oar proses skriuwe om de gegevens út it bestân te lêzen. As gefolch soene wy ​​mear boarnen en skiifromte fergrieme. Pipelines binne lykwols goed foar mear dan allinich it foarkommen fan tydlike bestannen:

As in proses besiket te lêzen út in lege pipeline, dan read(2) sil blokkearje oant de gegevens beskikber binne. As in proses besiket te skriuwen nei in folsleine pipeline, dan write(2) sil blokkearje oant genôch gegevens binne lêzen fan 'e pipeline om it skriuwen te foltôgjen.

Lykas de POSIX-eask is dit in wichtige eigenskip: skriuwen nei de pipeline oant PIPE_BUF bytes (op syn minst 512) moatte atoom wêze, sadat prosessen mei elkoar kommunisearje kinne fia de pipeline op in manier dat normale bestannen (dy't sokke garânsjes net leverje) net kinne.

Mei in gewoane bestân kin in proses al syn útfier dernei skriuwe en it trochjaan oan in oar proses. Of prosessen kinne operearje yn in hurde parallelle modus, mei help fan in eksterne sinjaalmeganisme (lykas in semafoar) ​​te ynformearjen inoar oer it foltôgjen fan in skriuwe of lêzen. Transportbanden besparje ús fan al dit gedoe.

Wat sykje wy?

Ik sil op myn fingers útlizze om it makliker te meitsjen foar jo foar te stellen hoe't in transportband kin wurkje. Jo moatte in buffer en wat steat yn it ûnthâld tawize. Jo sille funksjes nedich hawwe om gegevens ta te foegjen en te ferwiderjen fan 'e buffer. Jo sille wat foarsjenning nedich wêze om funksjes op te roppen tidens lês- en skriuwoperaasjes op triembeskriuwers. En slûzen binne nedich om de hjirboppe beskreaune spesjale gedrach út te fieren.

Wy binne no ree om de boarnekoade fan 'e kernel te ûndersiikjen ûnder fel lampeljocht om ús vague mentale model te befêstigjen of te bewizen. Mar wês altyd taret op it ûnferwachte.

Wêr sykje wy?

Ik wit net wêr't myn eksimplaar fan it ferneamde boek leit.Lions boek« mei Unix 6 boarnekoade, mar tank oan De Unix Heritage Society kin online socht wurde boarnekoade sels âldere ferzjes fan Unix.

Swalkjen troch de TUHS-argiven is as in besite oan in museum. Wy kinne nei ús dielde skiednis sjen en ik haw respekt foar de jierrenlange ynspanning om al dit materiaal bytsje by bytsje werom te heljen fan âlde kassetten en printsjes. En ik bin my perfoarst bewust fan dy fragminten dy't noch ûntbrekke.

Nei't ús nijsgjirrigens tefreden is oer de âlde skiednis fan pipelines, kinne wy ​​​​sjogge nei moderne kearnen foar fergeliking.

By de manier, pipe is systeem call nûmer 42 yn 'e tabel sysent[]. Tafal?

Tradysjonele Unix-kernels (1970-1974)

Ik fûn gjin spoar pipe(2) ek yn PDP-7 Unix (jannewaris 1970), noch yn earste edysje Unix (novimber 1971), noch yn ûnfolsleine boarnekoade twadde edysje (juny 1972).

TUHS beweart dat tredde edysje Unix (febrewaris 1973) wie de earste ferzje mei pipelines:

De tredde edysje fan Unix wie de lêste ferzje mei in kernel skreaun yn assembler, mar ek de earste ferzje mei pipelines. Yn 1973 waard wurke oan it ferbetterjen fan de tredde edysje, de kearn waard op 'e nij skreaun yn C, en sa waard de fjirde edysje fan Unix berne.

Ien lêzer fûn in scan fan in dokumint wêryn Doug McIlroy it idee foarstelde om "programma's te ferbinen lykas in túnslang."

Hoe pipelines wurde ymplementearre yn Unix
Yn it boek fan Brian KernighanUnix: In histoarje en in memoires", de skiednis fan it uterlik fan transportbanden ek neamt dit dokumint: "... it hong oan 'e muorre yn myn kantoar by Bell Labs foar 30 jier." Hjir ynterview mei McIlroyen in oar ferhaal fan McIlroy's wurk, skreaun yn 2014:

Doe't Unix ferskynde, makke myn passy foar koroutines my de OS-auteur, Ken Thompson, te freegjen om gegevens dy't skreaun binne nei in proses te tastean net allinich nei it apparaat te gean, mar ek nei de útgong nei in oar proses. Ken tocht dat it mooglik wie. As minimalist woe hy lykwols dat elke systeemfunksje in wichtige rol spile. Is direkt skriuwen tusken prosessen echt in grut foardiel boppe skriuwen nei in tuskenbestân? En pas doe't ik in spesifyk foarstel makke mei de pakkende namme "pipeline" en in beskriuwing fan 'e syntaksis fan' e ynteraksje fan prosessen, rôp Ken úteinlik: "Ik sil it dwaan!".

En die. Ien needlottige jûn feroare Ken de kernel en shell, reparearre ferskate standertprogramma's om te standerdisearjen hoe't se ynfier akseptearje (dy't miskien komme fan in pipeline), en feroare bestânsnammen. De oare deis waarden pipelines in soad brûkt yn applikaasjes. Oan 'e ein fan 'e wike brûkten de sekretarissen se om dokuminten fan tekstferwurkers nei de printer te stjoeren. Wat letter ferfong Ken de orizjinele API en syntaksis foar it ynpakken fan it gebrûk fan pipelines mei skjinnere konvinsjes dy't sûnt dy tiid binne brûkt.

Spitigernôch is de boarnekoade foar de tredde edysje fan Unix-kernel ferlern gien. En hoewol wy de kernelboarnekoade hawwe skreaun yn C fjirde edysje, dat waard útbrocht yn novimber 1973, mar it kaam út in pear moannen foar de offisjele release en befettet net de ymplemintaasje fan pipelines. It is spitich dat de boarnekoade foar dizze legindaryske Unix-funksje ferlern is, miskien foar altyd.

Wy hawwe dokumintaasje tekst foar pipe(2) fan beide releases, sadat jo begjinne kinne troch de dokumintaasje te sykjen tredde edysje (foar bepaalde wurden, "hânmjittich" ûnderstreke, in string fan ^H letterliken folge troch in ûnderstreekje!). Dit proto-pipe(2) is skreaun yn assembler en jout mar ien bestânbeskriuwing werom, mar leveret al de ferwachte kearnfunksjonaliteit:

Systeem oprop piip makket in I / O meganisme neamd in pipeline. De weromjûne triembeskriuwing kin brûkt wurde foar lês- en skriuwoperaasjes. As wat wurdt skreaun nei de pipeline, buffert it oant 504 bytes oan gegevens, wêrnei't it skriuwproses wurdt ophâlden. By it lêzen fan 'e pipeline wurde de buffered gegevens nommen.

Tsjin it folgjende jier wie de kernel opnij skreaun yn C, en pipe (2) fjirde edysje krige syn moderne uterlik mei it prototype "pipe(fildes)»:

Systeem oprop piip makket in I / O meganisme neamd in pipeline. De weromjûne triembeskriuwers kinne brûkt wurde yn lês- en skriuwoperaasjes. As der wat wurdt skreaun nei de pipeline, wurdt de beskriuwing weromjûn yn r1 (resp. fildes[1]) brûkt, buffered oant 4096 bytes oan gegevens, wêrnei't it skriuwproses wurdt ophâlden. By it lêzen fan 'e pipeline, de descriptor werom nei r0 (resp. fildes [0]) nimt de gegevens.

Der wurdt fan útgien dat ienris in pipeline is definiearre, twa (of mear) ynteraktive prosessen (makke troch folgjende oproppen foarke) sil gegevens fan 'e pipeline trochjaan mei oproppen lêze и skriuwe.

De shell hat in syntaksis foar it definiearjen fan in lineêre array fan prosessen ferbûn fia in pipeline.

Oproppen om te lêzen fan in lege pipeline (gjin buffered gegevens befetsje) dy't mar ien ein hat (alle skriuwbeskriuwers sluten) jouwe "ein fan bestân" werom. Skriuwoproppen yn in ferlykbere situaasje wurde negearre.

Earste bewarre pipeline ymplemintaasje ferwiist nei de fyfde edysje fan Unix (juny 1974), mar it is hast identyk oan dejinge dy't ferskynde yn 'e folgjende release. Allinnich opmerkings tafoege, sadat de fyfde edysje oerslein wurde kin.

Unix Sixth Edition (1975)

Begjin te lêzen Unix-boarnekoade seisde edysje (mei 1975). Foar in grut part te tankjen oan Lions it is folle makliker te finen dan de boarnen fan eardere ferzjes:

In protte jierren it boek Lions wie it ienige dokumint oer de Unix-kernel beskikber bûten Bell Labs. Hoewol't de lisinsje foar de sechsde edysje de learkrêften tastien hie om syn boarnekoade te brûken, hat de lisinsje fan 'e sânde edysje dizze mooglikheid útsletten, sadat it boek ferspraat waard yn yllegale typskreaune kopyen.

Tsjintwurdich kinne jo in werprinte eksimplaar fan it boek keapje, wêrfan de omslach studinten by de kopieermasine ôfbyldet. En tank oan Warren Toomey (dy't it TUHS-projekt begon), kinne jo downloade Sechste edysje Boarne PDF. Ik wol jo in idee jaan fan hoefolle muoite gie yn it meitsjen fan it bestân:

Mear as 15 jier lyn typ ik in kopy fan 'e boarnekoade yn Lionsom't ik de kwaliteit fan myn eksimplaar fan in ûnbekend oantal oare eksimplaren net leuk fûn. TUHS bestie noch net, en ik hie gjin tagong ta de âlde boarnen. Mar yn 1988 fûn ik in âlde tape mei 9 tracks dy't hie in reservekopy fan in PDP11 kompjûter. It wie dreech om te witten oft it wurke, mar d'r wie in yntakt /usr/src/-beam wêryn't de measte bestannen 1979 waarden markearre, dy't sels doe âld like. It wie de sânde edysje, of in PWB-ôflieding, tocht ik.

Ik naam de fynst as basis en bewurke de boarnen mei de hân nei de steat fan de sechsde edysje. In diel fan 'e koade bleau itselde, in diel moast wat bewurke wurde, it feroarjen fan it moderne token += nei it ferâldere =+. Der waard gewoan wat wiske, en wat moast folslein opnij skreaun wurde, mar net te folle.

En hjoed kinne wy ​​lêze online by TUHS de boarne koade fan de sechsde edysje fan argyf, dêr't Dennis Ritchie in hân oan hie.

Trouwens, op it earste each, is it haadfunksje fan 'e C-koade foar de perioade fan Kernighan en Ritchie har koarteheid. It komt net faak foar dat ik stikjes koade kin ynfoegje sûnder wiidweidige bewurking om in relatyf smel werjeftegebiet op myn side te passen.

Oan it begjin /usr/sys/ken/pipe.c d'r is in ferklearjende opmerking (en ja, d'r is mear /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 buffergrutte is net feroare sûnt de fjirde edysje. Mar hjir sjogge wy, sûnder publike dokumintaasje, dat pipelines eartiids bestannen brûkten as fallback-opslach!

As foar LARG triemmen, se oerienkomme mei inode-flagge LARG, dat wurdt brûkt troch it "grutte adresalgoritme" om te ferwurkjen yndirekte blokken om gruttere triemsystemen te stypjen. Om't Ken sei dat it better is om se net te brûken, dan sil ik syn wurd graach nimme.

Hjir is de echte systeemoprop 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 beskriuwt dúdlik wat hjir bart. Mar it is net sa maklik om de koade te begripen, foar in part fanwegen hoe "struct brûker u» en registers R0 и R1 systeemopropparameters en weromwearden wurde trochjûn.

Litte wy besykje mei ialloc() plak op skiif ynode (inode), en mei help falloc() - winkel twa map. As alles goed giet, sille wy flaggen ynstelle om dizze bestannen te identifisearjen as de twa einen fan 'e pipeline, wize se op deselde inode (waans referinsjetelling 2 wurdt), en markearje de inode as feroare en yn gebrûk. Jou omtinken oan fersiken om ik set() yn flaterpaden om de referinsjetelling yn 'e nije inode te ferleegjen.

pipe() troch troch R0 и R1 return file descriptor nûmers foar lêzen en skriuwen. falloc() jout in oanwizer nei in triem struktuer, mar ek "werom" fia u.u_ar0[R0] en in triem descriptor. Dat is, de koade wurdt opslein yn r triem descriptor foar lêzen en jout in descriptor foar skriuwen direkt út u.u_ar0[R0] nei twadde oprop falloc().

Flag FPIPE, dy't wy ynstelle by it meitsjen fan de pipeline, kontrolearret it gedrach fan 'e funksje rdwr() in sys2.c, dy't spesifike I/O-routines neamt:

/*
 * 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 funksje readp() в pipe.c lêst gegevens út de pipeline. Mar it is better om te trace de ymplemintaasje begjinnend fan writep(). Wer, de koade is wurden mear yngewikkelder fanwege de aard fan it argumint trochjaan konvinsje, mar guon details kinne wurde weilitten.

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

Wy wolle bytes skriuwe nei de pipeline-ynfier u.u_count. Earst moatte wy de inode beskoattelje (sjoch hjirûnder plock/prele).

Dan kontrolearje wy de ynode referinsje count. Salang't beide úteinen fan 'e pipeline iepen bliuwe, moat de teller 2 wêze. Wy hâlde oan ien keppeling (fanôf rp->f_inode), dus as de teller minder is dan 2, dan moat dit betsjutte dat it lêsproses syn ein fan 'e pipeline hat sletten. Mei oare wurden, wy besykje te skriuwen nei in sletten pipeline, dat is in flater. Earste flaterkoade EPIPE en sinjaal SIGPIPE ferskynde yn 'e sechsde edysje fan Unix.

Mar sels as de transportband iepen is, kin it fol wêze. Yn dit gefal, wy loslitte it slot en gean te sliepen yn 'e hope dat in oar proses sil lêze út' e pipeline en frij genôch romte yn it. As wy wekker wurde, geane wy ​​werom nei it begjin, hingje it slot wer op en begjinne in nije skriuwsyklus.

As d'r genôch frije romte is yn 'e pipeline, dan skriuwe wy it mei help fan gegevens skriuwe()... Parameter i_size1 de inode'a (mei in lege pipeline kin gelyk wêze oan 0) wiist nei it ein fan 'e gegevens dy't it al befettet. As der genôch romte is om te skriuwen, kinne wy ​​de pipeline fan folje i_size1 до PIPESIZ. Dan litte wy it slot los en besykje elk proses wekker te meitsjen dat wachtet om te lêzen fan 'e pipeline. Wy geane werom nei it begjin om te sjen oft wy slagge om te skriuwen safolle bytes as wy nedich. Sa net, dan begjinne wy ​​in nije opname syklus.

Gewoanlik parameter i_mode inode wurdt brûkt om tagongsrjochten op te slaan r, w и x. Mar yn it gefal fan pipelines, wy sinjalearje dat guon proses wachtet op in skriuwe of lêzen mei help fan bits IREAD и IWRITE respektivelik. It proses stelt de flagge en ropt sleep(), en it wurdt ferwachte dat yn 'e takomst wat oar proses sil belje wakeup().

De echte magy bart yn sleep() и wakeup(). Se wurde útfierd yn slp.c, de boarne fan 'e ferneamde "Jo wurde net ferwachte dat jo dit begripe" kommentaar. Gelokkich hoege wy de koade net te begripen, sjoch gewoan nei wat 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) /* … */

It proses dat ropt sleep() foar in bepaald kanaal, kin letter wekker wurde troch in oar proses, dat sil neame wakeup() foar itselde kanaal. writep() и readp() koördinearje harren aksjes troch sokke keppele oproppen. notysje dat pipe.c altyd prioritearje PPIPE wannear neamd sleep(),so allegear sleep() kin wurde ûnderbrutsen troch in sinjaal.

No hawwe wy alles om de funksje te begripen 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);
}

Jo kinne it makliker fine om dizze funksje fan ûnder nei boppe te lêzen. De tûke "lêze en werom" wurdt normaal brûkt as d'r wat gegevens yn 'e pipeline binne. Yn dit gefal brûke wy lêze() lês safolle gegevens as beskikber is fanôf de hjoeddeiske f_offset lêzen, en dan fernije de wearde fan de byhearrende offset.

By folgjende lêzen sil de pipeline leech wêze as de lêsoffset berikt is i_size1 oan de inode. Wy sette de posysje werom nei 0 en besykje elk proses wekker te meitsjen dat nei de pipeline skriuwe wol. Wy witte dat as de transportband fol is, writep() yn sliep falle op ip+1. En no't de pipeline leech is, kinne wy ​​it wekker meitsje om syn skriuwsyklus te hervatten.

As der neat te lêzen is, dan readp() kin in flagge ynstelle IREAD en yn 'e sliep falle ip+2. Wy witte wat him wekker meitsje sil writep()as it wat gegevens skriuwt nei de pipeline.

Opmerkings op lês() en skriuw() sil jo helpe te begripen dat ynstee fan parameters troch te jaan troch "u» wy kinne se behannelje as gewoane I/O-funksjes dy't in bestân, in posysje, in buffer yn it ûnthâld nimme, en it oantal bytes telle om te lêzen of te skriuwen.

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

As foar "konservative" blokkearjen, dan readp() и writep() beskoattelje ynoden oant se einigje of in resultaat krije (bygelyks belje wakeup). plock() и prele() wurkje gewoan: mei help fan in oare set fan oproppen sleep и wakeup lit ús elk proses wekker meitsje dat it slot nedich is dat wy krekt hawwe frijlitten:

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

Ik koe earst net begripe wêrom readp() feroarsaket net prele(ip) foar de oprop wakeup(ip+1). It earste writep() ropt yn syn loop, dit plock(ip), wat resultearret yn in deadlock as readp() hat syn blok noch net fuortsmiten, dus de koade moat op ien of oare manier goed wurkje. As jo ​​sjogge nei wakeup(), wurdt dúdlik dat it allinich it sliepproses markearret as klear foar útfiering, sadat yn 'e takomst sched() echt lansearre it. Sa readp() oarsaken wakeup(), ûntsluten, sets IREAD en ropt sleep(ip+2)- dit alles earder writep() herstart de syklus.

Dit foltôget de beskriuwing fan pipelines yn 'e sechsde edysje. Ienfâldige koade, fiergeande gefolgen.

Sânde edysje Unix (Jannewaris 1979) wie in nije grutte release (fjouwer jier letter) dy't in protte nije applikaasjes en kernelfunksjes yntrodusearre. It hat ek ûndergien wichtige feroarings yn ferbân mei it brûken fan type casting, fakbûnen en typte pointers nei struktueren. lykwols pipelines koade praktysk net feroare. Wy kinne dizze edysje oerslaan.

Xv6, in ienfâldige Unix-like kernel

Om in kearn te meitsjen Xv6 beynfloede troch de sechsde edysje fan Unix, mar skreaun yn moderne C om te rinnen op x86-processors. De koade is maklik te lêzen en begryplik. Ek, yn tsjinstelling ta Unix-boarnen mei TUHS, kinne jo it kompilearje, wizigje en it útfiere op wat oars as PDP 11/70. Dêrom wurdt dizze kearn in protte brûkt yn universiteiten as lesmateriaal oer bestjoeringssystemen. Boarnen binne op Github.

De koade befettet in dúdlike en trochtochte ymplemintaasje pipe.c, stipe troch in buffer yn it ûnthâld ynstee fan in inode op skiif. Hjir jou ik allinnich de definysje fan "strukturele pipeline" en de funksje 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 steat fan al de rest fan de útfiering, dat omfiemet funksjes piperead(), pipewrite() и pipeclose(). De eigentlike systeem oprop sys_pipe is in wrapper ymplementearre yn sysfile.c. Ik advisearje it lêzen fan al syn koade. De kompleksiteit is op it nivo fan 'e boarnekoade fan' e sechsde edysje, mar it is folle makliker en nofliker om te lêzen.

Linux 0.01

Jo kinne de boarnekoade fine foar Linux 0.01. It sil wêze learsum te bestudearjen de útfiering fan pipelines yn syn fs/pipe.c. Hjir wurdt in ynode brûkt om de pipeline foar te stellen, mar de pipeline sels is skreaun yn moderne C. As jo ​​jo wei troch de koade fan 'e sechsde edysje hackt hawwe, sille jo hjir gjin problemen hawwe. Dit is hoe't de funksje derút sjocht 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;
}

Sels sûnder nei de struktuerdefinysjes te sjen, kinne jo útfine hoe't de ynode-referinsjetelling wurdt brûkt om te kontrolearjen as in skriuwoperaasje resulteart yn SIGPIPE. Neist byte-by-byte-wurk is dizze funksje maklik te fergelykjen mei de boppesteande ideeën. Sels logika sleep_on/wake_up sjocht der net sa frjemd út.

Moderne Linux Kernels, FreeBSD, NetBSD, OpenBSD

Ik gie gau oer wat moderne kernels. Gjin fan harren hat al in skiif-basearre ymplemintaasje (net ferrassend). Linux hat syn eigen ymplemintaasje. En hoewol de trije moderne BSD-kernels ymplemintaasjes befetsje basearre op koade dy't skreaun is troch John Dyson, binne se yn 'e rin fan' e jierren te ferskillend fan elkoar wurden.

Lêze fs/pipe.c (op Linux) of sys/kern/sys_pipe.c (op * BSD), it nimt echte tawijing. Prestaasje en stipe foar funksjes lykas vector en asynchrone I/O binne hjoed wichtich yn koade. En de details fan ûnthâld tawizing, slûzen, en kernel konfiguraasje fariearje allegear sterk. Dit is net wat universiteiten nedich hawwe foar in ynliedende kursus oer bestjoeringssystemen.

Yn alle gefallen wie it foar my nijsgjirrich om in pear âlde patroanen te ûntdekken (bygelyks generearjen SIGPIPE en werom EPIPE by it skriuwen nei in sletten pipeline) yn al dizze, sa ferskillende, moderne kernels. Ik sil wierskynlik noait in PDP-11-kompjûter live sjen, mar d'r is noch in protte te learen fan 'e koade dy't in pear jier skreaun is foardat ik berne waard.

Skreaun troch Divi Kapoor yn 2011, it artikel "De Linux Kernel-ymplemintaasje fan pipes en FIFO'sis in oersjoch fan hoe't Linux pipelines (oant no ta) wurkje. IN resinte commit op linux yllustrearret it pipelinemodel fan ynteraksje, wêrfan de mooglikheden dy fan tydlike bestannen oertreffe; en lit ek sjen hoe fier pipelines binne gien fan "hiel konservative beskoattelje" yn 'e seisde edysje fan Unix kernel.

Boarne: www.habr.com

Add a comment