Kuidas torujuhtmeid Unixis rakendatakse

Kuidas torujuhtmeid Unixis rakendatakse
Selles artiklis kirjeldatakse torujuhtmete rakendamist Unixi tuumas. Olin mõnevõrra pettunud, et hiljuti ilmus artikkel pealkirjaga "Kuidas torujuhtmed Unixis töötavad?» selgus ei sisemise struktuuri kohta. Sain uudishimulikuks ja uurisin vastuse leidmiseks vanu allikaid.

Millest me räägime?

Torujuhtmed on "tõenäoliselt Unixi kõige olulisem leiutis" - Unixi aluseks oleva väikeste programmide kokkupanemise filosoofia ja tuttava käsurea loosungi määrav tunnus:

$ echo hello | wc -c
6

See funktsioon sõltub kerneli pakutavast süsteemikutsest pipe, mida on kirjeldatud dokumentatsiooni lehtedel toru (7) и toru (2):

Torujuhtmed pakuvad protsessidevaheliseks suhtluseks ühesuunalist kanalit. Konveieril on sisend (kirjutusots) ja väljund (lugemisots). Konveieri sisendisse kirjutatud andmeid saab lugeda väljundis.

Torujuhe luuakse helistades pipe(2), mis tagastab kaks failideskriptorit: üks viitab konveieri sisendile, teine ​​väljundile.

Ülaltoodud käsu jälgimise väljund demonstreerib konveieri loomist ja selle kaudu toimuvat andmevoogu ühest protsessist teise:

$ 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

Vanemprotsess kutsub pipe()manustatud failikirjelduste hankimiseks. Üks alamprotsess kirjutab ühele deskriptorile ja teine ​​protsess loeb samu andmeid teisest deskriptorist. Kest "nimetab ümber" deskriptorid 2 ja 3 koos dup4-ga, et need sobiksid stdin ja stdout.

Ilma torujuhtmeteta peaks kest failist andmete lugemiseks ühe protsessi väljundi faili kirjutama ja teise protsessi juhtima. Selle tulemusena raiskaksime rohkem ressursse ja kettaruumi. Kuid torujuhtmed on kasulikud enamaks kui lihtsalt ajutiste failide vältimiseks:

Kui protsess proovib lugeda tühjast konveierist, siis read(2) blokeeritakse, kuni andmed on saadaval. Kui protsess üritab kirjutada täiskonveierile, siis write(2) blokeerib seni, kuni konveierist on kirjutamise lõpuleviimiseks loetud piisavalt andmeid.

Nagu POSIX-i nõue, on ka see oluline omadus: konveierisse kirjutamine kuni PIPE_BUF baiti (vähemalt 512) peavad olema atomaarsed, et protsessid saaksid konveieri kaudu omavahel suhelda viisil, mida tavalised failid (mis selliseid garantiisid ei anna) ei suuda.

Tavafaili puhul saab protsess kogu oma väljundi sinna kirjutada ja teisele protsessile edasi anda. Või võivad protsessid töötada kõvas paralleelrežiimis, kasutades välist signaalimismehhanismi (nagu semafor), et teavitada üksteist kirjutamise või lugemise lõpetamisest. Konveierid päästavad meid kogu sellest sekeldusest.

Mida me otsime?

Selgitan sõrmedel, et teil oleks lihtsam ette kujutada, kuidas konveier töötab. Peate eraldama mällu puhvri ja teatud oleku. Andmete puhvrist lisamiseks ja eemaldamiseks vajate funktsioone. Funktsioonide väljakutsumiseks failideskriptoritel lugemis- ja kirjutamistoimingute ajal on teil vaja mõnda võimalust. Ja lukke on vaja ülalkirjeldatud erikäitumise rakendamiseks.

Oleme nüüd valmis uurima kerneli lähtekoodi ereda lambivalguse all, et kinnitada või ümber lükata meie ebamäärast vaimset mudelit. Kuid olge alati valmis ootamatusteks.

Kuhu me vaatame?

Ma ei tea, kus asub minu kuulsa raamatu koopia.Lõvide raamat« Unix 6 lähtekoodiga, kuid tänu Unixi pärandiühing saab internetist otsida lähtekood isegi vanemad Unixi versioonid.

TUHSi arhiivis ekslemine on nagu muuseumikülastus. Võime vaadata oma ühist ajalugu ja ma austan aastatepikkust pingutust kogu selle materjali osade kaupa vanadelt kassettidelt ja väljatrükkidelt taastada. Ja ma olen väga teadlik nendest fragmentidest, mis siiani puuduvad.

Olles rahuldanud oma uudishimu torujuhtmete iidse ajaloo vastu, saame võrdluseks vaadata tänapäevaseid südamikke.

Muide, pipe on tabelis süsteemikõne number 42 sysent[]. Kokkusattumus?

Traditsioonilised Unixi tuumad (1970–1974)

Ma ei leidnud jälgegi pipe(2) ega ka sisse PDP-7 Unix (jaanuar 1970), ega ka aastal esimene väljaanne Unix (november 1971), ega ka mittetäielikus lähtekoodis teine ​​väljaanne (juuni 1972).

TUHS väidab seda kolmas väljaanne Unix (veebruar 1973) oli esimene torujuhtmetega versioon:

Unixi kolmas väljaanne oli viimane versioon koos assembleris kirjutatud kerneliga, aga ka esimene torujuhtmetega versioon. 1973. aastal tehti tööd kolmanda väljaande täiustamiseks, kernel kirjutati ümber C-keeles ja nii sündis Unixi neljas väljaanne.

Üks lugeja leidis skannitud dokumendi, milles Doug McIlroy pakkus välja idee "ühendada programme nagu aiavoolik".

Kuidas torujuhtmeid Unixis rakendatakse
Brian Kernighani raamatusUnix: ajalugu ja memuaarid”, konveierite ilmumise ajalugu mainib ka seda dokumenti: "... see rippus minu Bell Labsi kontori seinal 30 aastat." Siin intervjuu McIlroygaja veel üks lugu alates McIlroy teos, mis on kirjutatud 2014. aastal:

Kui Unix ilmus, pani mu kirg korutiinide vastu mind paluma OS-i autoril Ken Thompsonil, et ta lubaks mõnesse protsessi kirjutatud andmetel minna mitte ainult seadmesse, vaid ka teise protsessi väljumisse. Ken pidas seda võimalikuks. Minimalistina soovis ta aga, et iga süsteemi funktsioon mängiks olulist rolli. Kas otse kirjutamine protsesside vahel on tõesti suur eelis vahefaili kirjutamise ees? Ja alles siis, kui tegin konkreetse ettepaneku tabava nimetusega "torujuhe" ja protsesside koosmõju süntaksi kirjeldusega, hüüdis Ken lõpuks: "Ma teen ära!".

Ja tegigi. Ühel saatuslikul õhtul muutis Ken tuuma ja kesta, parandas mitu standardprogrammi, et standardida sisendi vastuvõtmist (mis võib tulla torujuhtmest), ja muutis failinimesid. Järgmisel päeval kasutati torujuhtmeid rakendustes väga laialdaselt. Nädala lõpuks saatsid sekretärid nende abil tekstitöötlusprogrammidest dokumendid printerisse. Veidi hiljem asendas Ken algse API ja torujuhtmete kasutamise süntaksi puhtamate tavadega, mida on sellest ajast peale kasutatud.

Kahjuks on Unixi tuuma kolmanda väljaande lähtekood kadunud. Ja kuigi meil on tuuma lähtekood kirjutatud C-vormingus neljas trükk, mis ilmus novembris 1973, kuid see ilmus paar kuud enne ametlikku väljaandmist ega sisalda torujuhtmete rakendamist. Kahju, et selle legendaarse Unixi funktsiooni lähtekood kaob ehk igaveseks.

Meil on dokumentatsiooni tekst pipe(2) mõlemast versioonist, nii et võite alustada dokumentatsioonist otsimisega kolmas trükk (teatud sõnade puhul alla joonitud "käsitsi", ^H-literaalide jada, millele järgneb allkriips!). See proto-pipe(2) on kirjutatud assembleris ja tagastab ainult ühe failideskriptori, kuid pakub juba oodatud põhifunktsioone:

Süsteemikõne toru loob I/O mehhanismi, mida nimetatakse torujuhtmeks. Tagastatud failideskriptorit saab kasutada lugemis- ja kirjutamistoiminguteks. Kui konveierisse midagi kirjutatakse, puhverdab see kuni 504 baiti andmeid, misjärel kirjutamisprotsess peatatakse. Konveierilt lugemisel võetakse puhverdatud andmed.

Järgmiseks aastaks oli kernel C-s ümber kirjutatud ja toru(2) neljas väljaanne omandas prototüübiga oma kaasaegse välimuse"pipe(fildes)"

Süsteemikõne toru loob I/O mehhanismi, mida nimetatakse torujuhtmeks. Tagastatud failideskriptoreid saab kasutada lugemis- ja kirjutamistoimingutes. Kui konveierisse midagi kirjutatakse, kasutatakse r1-s tagastatud deskriptorit (resp. fildes[1]), mis puhverdatakse kuni 4096 baiti andmeid, misjärel kirjutamisprotsess peatatakse. Konveierist lugemisel võtab andmed r0-le tagastatud deskriptor (resp. fildes[0]).

Eeldatakse, et kui konveier on määratletud, toimub kaks (või enam) vastastikku toimivat protsessi (mis on loodud järgnevate kutsumiste abil kahvel) edastab andmed torustikust kõnede abil lugenud и kirjutama.

Shellil on süntaks torujuhtme kaudu ühendatud protsesside lineaarse massiivi määratlemiseks.

Lugemiskutsed tühjast torujuhtmest (mis ei sisalda puhverdatud andmeid), millel on ainult üks ots (kõik kirjutamisfailide kirjeldused on suletud), tagastavad "faili lõpu". Sarnases olukorras tehtud kõnesid ignoreeritakse.

Kõige varem säilinud torujuhtme rakendamine seotud Unixi viiendale väljaandele (juuni 1974), kuid see on peaaegu identne järgmises väljaandes ilmunuga. Lisatud on ainult kommentaarid, seega võib viienda väljaande vahele jätta.

Unixi kuues väljaanne (1975)

Unixi lähtekoodi lugemise alustamine kuues trükk (mai 1975). Suuresti tänu Lions seda on palju lihtsam leida kui varasemate versioonide allikaid:

Palju aastaid raamat Lions oli ainus Unixi kerneli dokument, mis oli saadaval väljaspool Bell Labsi. Kuigi kuuenda väljaande litsents lubas õpetajatel kasutada selle lähtekoodi, välistas seitsmenda väljaande litsents selle võimaluse, mistõttu raamatut levitati ebaseaduslikes masinakirjas koopiates.

Täna saab osta raamatu kordustrükki, mille kaanel on kujutatud õpilasi koopiamasina juures. Ja tänu Warren Toomeyle (kes alustas TUHS-i projekti) saate alla laadida Kuues väljaanne PDF-allikas. Ma tahan teile anda ülevaate sellest, kui palju vaeva faili loomiseks kulus:

Rohkem kui 15 aastat tagasi sisestasin ma sisestatud lähtekoodi koopia Lionssest mulle ei meeldinud minu koopia kvaliteet teadmata hulga teiste koopiate hulgast. TUHS-i veel ei eksisteerinud ja mul polnud juurdepääsu vanadele allikatele. Kuid 1988. aastal leidsin vana 9 rajaga lindi, millel oli varukoopia PDP11 arvutist. Raske oli teada, kas see töötas, kuid seal oli terve /usr/src/ puu, milles enamikele failidele oli märgitud 1979, mis isegi siis nägi välja vana. See oli seitsmes väljaanne või PWB tuletis, mõtlesin ma.

Võtsin leiu aluseks ja toimetasin allikad käsitsi kuuenda väljaande seisu. Osa koodist jäi samaks, osa tuli veidi redigeerida, muutes tänapäevase märgi += vananenud =+ vastu. Midagi lihtsalt kustutati ja midagi tuli täielikult ümber kirjutada, kuid mitte liiga palju.

Ja täna saame Internetis TUHS-is lugeda kuuenda väljaande lähtekoodi arhiiv, mille juures Dennis Ritchie käsi oli.

Muide, esmapilgul on C-koodi peamine omadus enne Kernighani ja Ritchie perioodi selle lühidus. See ei juhtu sageli, kui mul õnnestub ilma põhjaliku redigeerimiseta sisestada koodijuppe, et need mahuksid oma saidi suhteliselt kitsale kuvaalale.

Varakult /usr/sys/ken/pipe.c on selgitav kommentaar (ja jah, seda on veel /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

Puhvri suurus ei ole pärast neljandat väljaannet muutunud. Kuid siin näeme ilma avaliku dokumentatsioonita, et torujuhtmed kasutasid kunagi faile varumäluna!

Mis puutub LARG-failidesse, siis need vastavad inode-lipp SUUR, mida "suur adresseerimisalgoritm" kasutab töötlemiseks kaudsed plokid suuremate failisüsteemide toetamiseks. Kuna Ken ütles, et parem on neid mitte kasutada, on mul hea meel tema sõna sekka jääda.

Siin on tegelik süsteemikutse 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;
}

Kommentaar kirjeldab selgelt, mis siin toimub. Kuid koodist pole nii lihtne aru saada, osaliselt seetõttu, kuidas "struct kasutaja u» ja registrid R0 и R1 edastatakse süsteemikõne parameetrid ja tagastusväärtused.

Proovime koos ialloc() asetage kettale inode (inode), ja abiga falloc() - hoidke kaks faili. Kui kõik läheb hästi, seame lipud, mis tuvastavad need failid torujuhtme kahe otsana, suuname need samale inoodile (mille viitearv on 2) ja märgime inode muudetud ja kasutusel olevaks. Pöörake tähelepanu taotlustele iput() veateedel, et vähendada viidete arvu uues sisendis.

pipe() läbi R0 и R1 tagastab lugemiseks ja kirjutamiseks failideskriptori numbrid. falloc() tagastab kursori failistruktuurile, aga ka "tagastab" kaudu u.u_ar0[R0] ja faili deskriptor. See tähendab, et kood on salvestatud r failideskriptor lugemiseks ja määrab deskriptori otse kirjutamiseks u.u_ar0[R0] peale teist kõnet falloc().

Lipp FPIPE, mille seadsime konveieri loomisel, juhib funktsiooni käitumist rdwr() failis sys2.c, mis kutsub välja konkreetsed I/O-rutiinid:

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

Siis funktsioon readp() в pipe.c loeb andmeid torujuhtmest. Kuid parem on jälgida rakendamist alates writep(). Jällegi on kood muutunud keerulisemaks argumendi läbimise konventsiooni olemuse tõttu, kuid mõned üksikasjad võib välja jätta.

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

Tahame konveieri sisendisse kirjutada baite u.u_count. Kõigepealt peame inode lukustama (vt allpool plock/prele).

Seejärel kontrollime inoodi viidete arvu. Kuni torujuhtme mõlemad otsad jäävad avatuks, peaks loendur olema 2. Hoiame kinni ühest lülist (alates rp->f_inode), nii et kui loendur on väiksem kui 2, siis peaks see tähendama, et lugemisprotsess on torujuhtme otsa sulgenud. Teisisõnu, me üritame kirjutada suletud torujuhtmele, mis on viga. Esimene veakood EPIPE ja signaali SIGPIPE ilmus Unixi kuuendas väljaandes.

Kuid isegi kui konveier on avatud, võib see olla täis. Sel juhul vabastame luku ja läheme magama lootuses, et mõni teine ​​protsess loeb torustikust ja vabastab selles piisavalt ruumi. Ärgates pöördume tagasi algusesse, riputame luku uuesti üles ja alustame uut kirjutamistsüklit.

Kui torustikus on piisavalt vaba ruumi, siis kirjutame sinna andmed kasutades writei ()... Parameeter i_size1 inode'a (tühja konveieriga võib olla 0-ga) osutab selles juba sisalduvate andmete lõppu. Kui kirjutamiseks on piisavalt ruumi, saame torujuhtme täita i_size1 kuni PIPESIZ. Seejärel vabastame luku ja proovime äratada kõik protsessid, mis ootavad torujuhtmest lugemist. Läheme tagasi algusesse, et näha, kas õnnestus kirjutada nii palju baite, kui vaja. Kui ei, siis alustame uut salvestustsüklit.

Tavaliselt parameeter i_mode inode kasutatakse õiguste salvestamiseks r, w и x. Kuid torujuhtmete puhul anname signaali, et mõni protsess ootab bittide abil kirjutamist või lugemist IREAD и IWRITE vastavalt. Protsess seab lipu ja kutsub sleep(), ja eeldatakse, et tulevikus helistab mõni muu protsess wakeup().

Tõeline maagia toimub sees sleep() и wakeup(). Neid rakendatakse aastal slp.c, kuulsa "Teilt ei eeldata sellest aru saamist" kommentaari allikas. Õnneks ei pea me koodist aru saama, vaadake lihtsalt mõnda kommentaari:

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

Protsess, mis kutsub sleep() konkreetse kanali puhul võib selle hiljem äratada mõni muu protsess, mis helistab wakeup() sama kanali jaoks. writep() и readp() koordineerida oma tegevust selliste seotud kõnede kaudu. pane tähele seda pipe.c alati prioriteediks PPIPE kui kutsutakse sleep(), nii et kõik sleep() võib signaal katkestada.

Nüüd on meil kõik funktsiooni mõistmiseks 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);
}

Teil võib olla lihtsam lugeda seda funktsiooni alt üles. "Loe ja tagasta" haru kasutatakse tavaliselt siis, kui konveieril on andmeid. Sel juhul kasutame loe () lugeda nii palju andmeid, kui on saadaval, alustades praegusest f_offset lugeda ja seejärel värskendada vastava nihke väärtust.

Järgmistel lugemistel on konveier tühi, kui lugemisnihe on saavutatud i_size1 inoodi juures. Lähtestame positsiooni 0-le ja proovime äratada protsessi, mis soovib konveierile kirjutada. Teame, et kui konveier on täis, writep() magama jääma ip+1. Ja nüüd, kui torujuhe on tühi, saame selle üles äratada, et jätkata selle kirjutamistsüklit.

Kui midagi lugeda pole, siis readp() oskab lipu panna IREAD ja magama edasi ip+2. Me teame, mis ta äratab writep()kui ta kirjutab konveierile mingeid andmeid.

Kommentaarid loe() ja kirjuta() aitab teil mõista, et parameetrite edastamise asemel "u» saame neid käsitleda nagu tavalisi I/O-funktsioone, mis võtavad faili, positsiooni, mällu puhvri ja loevad lugemiseks või kirjutamiseks vajalike baitide arvu.

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

Mis puutub siis "konservatiivsesse" blokeerimisse readp() и writep() lukustage inoodid, kuni need on lõpetatud või tulemuse saavutanud (st helistage wakeup). plock() и prele() toimige lihtsalt: kasutage teistsugust kõnede komplekti sleep и wakeup võimaldab meil äratada kõik protsessid, mis vajavad just vabastatud lukku:

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

Alguses ei saanud ma aru, miks readp() ei põhjusta prele(ip) enne kõnet wakeup(ip+1). Esimene asi writep() kutsub oma tsüklis, see plock(ip), mille tulemuseks on ummikseisu, kui readp() pole oma plokki veel eemaldanud, seega peab kood kuidagi õigesti töötama. Kui vaatate wakeup(), saab selgeks, et see märgib ainult magamise protsessi täitmiseks valmis, nii et edaspidi sched() tõesti käivitas selle. Niisiis readp() põhjused wakeup(), avab lukud, komplekteerib IREAD ja kõned sleep(ip+2)- kõik see enne writep() taaskäivitab tsükli.

See lõpetab torujuhtmete kirjelduse kuuendas väljaandes. Lihtne kood, kaugeleulatuvad tagajärjed.

Seitsmes väljaanne Unix (jaanuar 1979) oli uus suur väljalase (neli aastat hiljem), mis tutvustas palju uusi rakendusi ja kerneli funktsioone. Samuti on see läbi teinud olulisi muudatusi seoses tüübivalamise, ühenduste ja konstruktsioonidele viidavate trükitud osutite kasutamisega. Kuid torujuhtmete kood praktiliselt ei muutunud. Võime selle väljaande vahele jätta.

Xv6, lihtne Unixi-laadne kernel

Tuuma loomiseks Xv6 mõjutatud Unixi kuuendast väljaandest, kuid kirjutatud kaasaegses C keeles, et töötada x86 protsessoritega. Kood on kergesti loetav ja arusaadav. Erinevalt TUHS-iga Unixi allikatest saate selle kompileerida, muuta ja käivitada millegi muuga peale PDP 11/70. Seetõttu kasutatakse seda tuuma laialdaselt ülikoolides operatsioonisüsteemide õppematerjalina. Allikad on Githubis.

Kood sisaldab selget ja läbimõeldud teostust toru.c, mida toetab mälus olev puhver, mitte ketta inode. Siin annan ainult "struktuurse torujuhtme" ja funktsiooni määratluse 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() määrab kogu ülejäänud rakenduse oleku, mis sisaldab funktsioone piperead(), pipewrite() и pipeclose(). Tegelik süsteemikõne sys_pipe on ümbris, mis on rakendatud sysfile.c. Soovitan lugeda kogu tema koodi. Keerukus on kuuenda väljaande lähtekoodi tasemel, kuid seda on palju lihtsam ja meeldivam lugeda.

Linux 0.01

Linuxi 0.01 lähtekoodi leiate. Õpetlik on uurida tema torujuhtmete rakendamist fs/pipe.c. Siin kasutatakse konveieri tähistamiseks inode, kuid konveier ise on kirjutatud kaasaegses C-keeles. Kui olete kuuenda väljaande koodi sisse häkkinud, pole teil siin probleeme. Funktsioon näeb välja selline 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;
}

Isegi ilma struktuuride definitsioone vaatamata saate aru saada, kuidas kasutatakse inoodi viidete arvu kontrollimaks, kas kirjutamistoimingu tulemuseks on SIGPIPE. Lisaks bait-bait-tööle on seda funktsiooni lihtne ülaltoodud ideedega võrrelda. Isegi loogika sleep_on/wake_up ei tundu nii võõras.

Kaasaegsed Linuxi tuumad, FreeBSD, NetBSD, OpenBSD

Käisin kiiresti üle mõned kaasaegsed tuumad. Ühelgi neist pole juba kettapõhist teostust (pole üllatav). Linuxil on oma rakendus. Ja kuigi kolm kaasaegset BSD tuuma sisaldavad rakendusi, mis põhinevad John Dysoni kirjutatud koodil, on need aastate jooksul üksteisest liiga erinevad.

Lugema fs/pipe.c (Linuxis) või sys/kern/sys_pipe.c (*BSD-l) nõuab see tõelist pühendumist. Selliste funktsioonide nagu vektor ja asünkroonne I/O jõudlus ja tugi on tänapäeval koodis olulised. Ja mälu eraldamise, lukkude ja tuuma konfiguratsiooni üksikasjad on kõik väga erinevad. Seda pole ülikoolidel operatsioonisüsteemide sissejuhatava kursuse jaoks vaja.

Igal juhul oli minu jaoks huvitav mõned vanad mustrid välja kaevata (näiteks genereerida SIGPIPE ja tagasi EPIPE suletud torujuhtmele kirjutamisel) kõigis neis nii erinevates kaasaegsetes tuumades. Tõenäoliselt ei näe ma kunagi PDP-11 arvutit otseülekandes, kuid paar aastat enne minu sündi kirjutatud koodist on veel palju õppida.

Divi Kapoori poolt 2011. aastal kirjutatud artikkel "Torude ja FIFO-de Linuxi tuuma juurutamineon ülevaade Linuxi torujuhtmete (seni) toimimisest. A hiljutine luba linuxis illustreerib interaktsiooni torujuhtme mudelit, mille võimalused ületavad ajutiste failide omad; ja näitab ka seda, kui kaugele on torujuhtmed jõudnud Unixi kuuenda väljaande tuuma "väga konservatiivsest lukustamisest".

Allikas: www.habr.com

Lisa kommentaar