Si janë implementuar tubat në Unix

Si janë implementuar tubat në Unix
Në këtë artikull përshkruhet implementimi i konvejereve në kernelin Unix. Unë isha disi i zhgënjyer që artikulli i fundit me titullin "Si funksionojnë konvejere në Unix?" përfundoi jo me strukturën e brendshme. Më bëri kurioz, dhe unë hulumtova në burime të vjetra për të gjetur një përgjigje.

ÇfarĂ« Ă«shtĂ« nĂ« bisedĂ«?

Konvejere — "ndoshta shpikja mĂ« e rĂ«ndĂ«sishme nĂ« Unix" — janĂ« njĂ« karakteristikĂ« pĂ«rcaktuese e filozofisĂ« Unix pĂ«r tĂ« bashkuar programe tĂ« vogla, si dhe njĂ« shkrim i njohur nĂ« terminal:

$ echo hello | wc -c
6

Kjo funksionalitet varet nga thirrja sistemike e ofruar nga bërthama pipe, që përshkruhet në faqet e dokumentacionit pipe(7) dhe pipe(2):

Konvejere ofrojnë një kanal në një drejtim për komunikimin ndërprocesor. Një konvejere ka një hyrje (write end) dhe një dalje (read end). Të dhënat e shkruara në hyrjen e konveyorëve mund të lexohen në dalje.

Konvejere krijohet me thirrjen pipe(2), e cila kthen dy identifikues dosjesh: një i referohet hyrjes së konveyerëve, tjetri daljeve.

Rezultatet e gjurmimit të komandës së mësipërme demonstrojnë krijimin e konveyerëve dhe rrjedhën e të dhënave përmes tyre nga një proces në tjetrin:

$ 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

Procesi prind thërret pipe(), për të marrë identifikuesit e lidhur të dosjeve. Një proces i birësuar shkruan në një identifikues dosjeje, ndërsa procesi tjetër lexon të njëjtat të dhëna nga një identifikues tjetër. Shell me ndihmën e dup2 "ri-identifikon" identifikuesit 3 dhe 4 për t'iu përgjigjur stdin dhe stdout.

Pa konvejere, shell do të duhet të shkruajë rezultatin e një procesi në një skedarin dhe t'ia kalojë atë një procesi tjetër për të lexuar të dhënat nga skedari. Si rezultat, do të përdornim më shumë burime dhe hapësirë në disk. Megjithatë, konvejere janë të mira jo vetëm për faktin se evitojnë përdorimin e skedarëve të përkohshëm:

Nëse një proces përpiqet të lexojë nga një konvejere të zbrazët, atëherë read(2) do të bllokohet deri sa të dhënat të bëhen të disponueshme. Nëse një proces përpiqet të shkruajë në një konvejere të mbushur, atëherë write(2) do të bllokohet deri sa të lexohen mjaftueshëm të dhëna nga tubi për të realizuar shkrimin.

Siç kërkon POSIX, kjo është një pronë e rëndësishme: shkrimi në tub deri në PIPE_BUF byte (të paktën 512) duhet të jetë atomik, në mënyrë që proceset të mund të bashkëpunojnë përmes tubos ashtu siç skedarët e zakonshëm (të cilët nuk ofrojnë këto garanci) nuk mund t'i bëjnë.

Kur përdoret një skedar i zakonshëm, një proces mund të shkruajë të gjitha të dhënat e tij aty dhe t'ia kalojë një procesi tjetër. Ose proceset mund të veprojnë në një mod të fortë paralelizimi, duke informuar njëri-tjetrin për përfundimin e shkrimit ose leximit përmes një mekanizmi të jashtëm sinjalizimi (si semafori). Tubët na çlirojnë nga të gjitha këto telashe.

ÇfarĂ« po kĂ«rkojmĂ«?

Do ta shpjegoj në mënyrë të thjeshtë, që të keni një ide se si mund të funksionojë tubi. Do t'ju nevojitet një memorie e alokuar për një tampon dhe një gjendje të caktuar. Do të nevojiten funksione për të shtuar dhe hequr të dhëna nga tampona. Do të nevojitet një mjet për të thirrur funksionet gjatë operacioneve të leximit dhe shkrimit në deshifrat e skedarëve. Dhe do të nevojiten bllokime për të realizuar sjelljen speciale të përshkruar më lart.

Tani jemi gati të shqyrtojmë nën dritën e fortë të llambës kodin burimor të bërthamës, për të konfirmuar ose hedhur poshtë modelin tonë të paqartë. Por gjithmonë duhet të jeni të gatshëm për befasi.

Ku po kërkojmë?

Nuk e di se ku ndodhet kopja ime e njohur e librit "Lions book" me kodin burimor të Unix 6, por falë Shoqëria e Trashëgimisë Unix mund të kërkoj për të në internet në kodin burimor të versioneve edhe më të vjetra të Unix.

Endeja në arkivat e TUHS është si të vizitosh një muze. Mund të shohim historinë tonë të përbashkët dhe ndiej respekt për përpjekjet shumëvjeçare për të rikuperuar të gjithë këto materiale bit për bit nga kasetat e vjetra dhe printimet. Dhe kam një ndjenjë të fortë për ato fragmente që ende mungojnë.

Pas plotësimit të kuriozitetit tim për historinë e lashtë të tubëve, për krahasim, mund të shohim bërthamat moderne.

Për më tepër, pipe është thirrja sistemike numër 42 në tabelën sysent[]. Rastësi?

BĂ«rthamat tradicionale tĂ« Unix (1970–1974)

Nuk kam gjetur asnjë gjurmë pipe(2) as në PDP-7 Unix (janar 1970), as në edicioni i parë i Unix (nëntor 1971), as në kodin burimor të paplotë edicioni i dytë (qershor 1972).

TUHS pretendon se edicioni i tretë i Unix (shkurti 1973) ishte versioni i parë me kanale:

Redaktimi i tretë i Unix ishte versioni i fundit me bërthamën e shkruar në assembler, por gjithashtu ishte versioni i parë me kanale. Gjatë vitit 1973, u punua për përmirësimin e redaktimit të tretë, bërthama u rishkrua në C, dhe kështu u shfaq redaktimi i katërt i Unix.

Një nga lexuesit gjeti një skan të dokumentit, në të cilin Doug McIlroy propozoi idenë e "lidhnies së programeve sipas parimit të tubit të kopshtit".

Si janë implementuar tubat në Unix
Në librin e Brian Kernighan "Unix: Një Histori dhe një Kujtim" gjithashtu përmendet ky dokument: "... ai ka qëndruar në murin e zyrës sime në Bell Labs për 30 vjet". Këtu është intervista me McIlroy, dhe një histori tjetër nga punimet e McIlroy, shkruar në 2014:

Kur u shfaq Unix, pasioni im për korutinat më bëri të kërkova nga autori i OS-së, Ken Thompson, të lejonte që të dhënat, të regjistruara në një proces, të shkonin jo vetëm në një pajisje, por edhe në daljen për një proces tjetër. Ken vendosi që kjo ishte e mundur. Megjithatë, si minimalist, ai donte që çdo funksion sistemik të luante një rol të konsiderueshëm. A ka vërtet ndonjë përfitim të madh nga regjistrimi i drejtpërdrejtë midis proceseve krahasuar me regjistrimin në një skedar ndërmjetës? Dhe vetëm kur unë paraqita një propozim konkret me një emër të goditur "kanal" dhe një përshkrim të sintaksës për ndërveprimin e proceseve, Ken më në fund thirri: "Do ta bëj atë!".

Dhe e bëri. Një natë fatkeqe Ken ndryshoi bërthamën dhe shell-in, rregulloi disa programe standarde, duke standardizuar procedurën e tyre për pranimin e të dhënave (të cilat mund të vijnë nga kanali), dhe gjithashtu ndryshoi emrat e skedarëve. Të nesërmen, kanalet filluan të përdoren shumë gjerësisht në aplikacione. Deri në fund të javës, sekretaret i dërgonin me to dokumentet në printer nga redaktorët e tekstit. Pak më vonë Ken zëvendësoi API origjinal dhe sintaksën për përdorimin e kanaleve me marrëveshje më të pastra, që nga atëherë janë përdorur.

FatkeqĂ«sisht, kodet burim tĂ« bĂ«rthamĂ«s sĂ« redaktimit tĂ« tretĂ« tĂ« Unix janĂ« humbur. Dhe megjithĂ«se kemi kodin burim tĂ« bĂ«rthamĂ«s, tĂ« shkruar nĂ« C, redaktimi i katĂ«rt, i dalĂ« nĂ« nĂ«ntor 1973, ai doli disa muaj pĂ«rpara lĂ«shimit zyrtar dhe nuk pĂ«rmban implementimin e kanaleve. ËshtĂ« pĂ«r tĂ« ardhur keq qĂ« kodi burim i funksionit legjendar tĂ« Unix Ă«shtĂ« humbur, ndoshta pĂ«rjetĂ«sisht.

Ne kemi dokumentacionin e tekstit për pipe(2) nga të dy edicionet, kështu që mund të filloni të kërkoni në dokumentacionin e tretë (në fjalë të caktuara, të theksuara "duke dorëzuar", një varg literal ^H, pas të cilit vjen nënlinja!). Ky prototippipe(2) është shkruar në assembler dhe kthen vetëm një descriptor skedari, por tashmë ofron funksionalitetin kryesor të pritur:

Thirrja sistemike pipe krijon një mekanizëm futje-daljeje të quajtur tub. Descriptor skedari i kthyer mund të përdoret për operacione lehtësimi dhe shkruanje. Kur diçka shkruhet në tub, atëherë buferizohet deri në 504 byte të dhënash, pas së cilës procesi i shkruajtur ndalet. Kur lexohet nga tubi, të dhënat e buferizuara merren.

NĂ« vitin e ardhshĂ«m bĂ«rthamja u rishkrua nĂ« C, ndĂ«rsa pipe(2) nĂ« рДЎаĐșта e katĂ«rt mori formĂ«n e tij moderne me prototipin "pipe(fildes)»:

Thirrja sistemike pipe krijon një mekanizëm futje-daljeje të quajtur tub. Descriptorët e skedarëve të kthyer mund të përdoren në operacione lehtësimi dhe shkruan. Kur diçka shkruhet në tub, përdoret descriptor-i i kthyer në r1 (përkatësisht fildes[1]), atëherë buferizohet deri në 4096 byte të dhënash, pas së cilës procesi i shkruani ndalet. Kur lexohet nga tubi, descriptor-i i kthyer në r0 (përkatësisht fildes[0]) merr të dhënat.

Supozimi është se pas përcaktimit të tubit, dy (ose më shumë) procese që bashkëpunojnë (të krijuara nga thirrje të mëpasshme fork) do të transferojnë të dhëna nga tubi duke përdorur thirrjet read dhe write.

Në shell ka një sintaksë për përcaktimin e një array linear procesesh, të lidhur përmes tubit.

Thirrje për lexim nga një tub të zbrazët (që nuk përmban të dhëna të buferizuara), që ka vetëm një fund (të gjitha descriptorët e skedarëve të shkruar janë mbyllur), kthejnë "fundin e skedarit". Thirrjet për shkruan në një situatë të ngjashme injorohen.

Implementimi më i hershëm i ruajtur i tubit belongs në redaktimin e pestë të Unix (qershor 1974), por është pothuajse identike me atë që doli në edicionin e ardhshëm. Vetëm janë shtuar komente, kështu që redaktimi i pestë mund të përjashtohet.

Redaktimi i gjashtë i Unix (1975)

Fillojmë të lexojmë kodin burimor të Unix të redaktimit të gjashtë (maj 1975). Në shumë mënyra falë Lions ta gjejmë shumë më të lehtë sesa kodet burimore të versione më të hershme:

Shumë vite libri Lions ka qenë dokumenti i vetëm mbi kernelin Unix, i disponueshëm jashtë mureve të Bell Labs. Megjithëse licenca e edicionit të gjashtë lejonte mësuesit të përdornin kodin e saj burimor, licenca e edicionit të shtatë e përjashtoi këtë mundësi, prandaj libri u shpërnda në formën e kopjave të paligjshme të shtypura.

Sot është e mundur të blini një edicion të riprodhuar të librit, ku në kopertinë paraqiten studentët pranë një aparati kopjush. Falë Warren Tumi (i cili filloi projektin TUHS) mund të shkarkoni skedarin PDF me kodin burimor të edicionit të gjashtë. Dua t'ju jap një pasqyrë se sa energji është shpenzuar për krijimin e skedarit:

Më shumë se 15 vjet më parë, unë e shkrova një kopje të kodit burimor të dhënë në Lions, sepse nuk më pëlqente cilësia e kopjes sime nga një numër i panjohur kopjave të tjera. TUHS ende nuk ishte krijuar, dhe nuk kisha akses në burimet e vjetra. Por në vitin 1988, gjeta një kasetë të vjetër me 9 rrugë, në të cilën ishte një kopje rezervë nga kompjuteri PDP11. Ishte e vështirë të kuptohej nëse funksiononte, por atje ishte një pemë e paprekur /usr/src/, në të cilën shumica e skedarëve ishin të etiketuar me vitin 1979, që atëherë dukeshin si antikitet. Kjo ishte edicioni i shtatë ose një degëzim i tij PWB, siç mendoja.

E mora këtë gjetje si bazë dhe redaktova manualisht burimet në gjendjen e edicionit të gjashtë. Disa pjesë të kodit mbetën të njëjta, disa duhej të redaktoheshin pak, duke ndryshuar token moderne += në atë të vjetër =+. Disa thjesht i hoqa, ndërsa disa duhej të shkruheshin plotësisht, por jo shumë.

Dhe sot ne mund të lexojmë në internet në TUHS kodin burimor të edicionit të gjashtë nga arkivi, të cilin e ndihmoi Dennis Ritchie.

Apropos, në pamje të parë, karakteristika kryesore e kodit C deri në periudhën e Kernighan dhe Ritchie është shkurtësia. Nuk është hera e parë që më ndodh të përfshij fragmente kodi pa redaktim të gjerë, në mënyrë që të përputhen me një fushë relativisht të ngushtë shfaqjeje në sajtin tim.

Në fillim /usr/sys/ken/pipe.c ka një koment shpjegues (dhe po, ka ende /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

Madhësia e buferit nuk ishte ndryshuar që nga edicioni i katërt. Por këtu pa ndonjë dokumentacion publik shohim se njëherë e një kohë, linjat e prodhimit përdornin skedarët si një depo rezervë!

Sa i përket skedarëve LARG, ata përputhen me flag-in inode LARG, i cili përdoret për "algoritmin e adresimit të madh" për përpunimin blloq të tërthorta me qëllim për të mbështetur sisteme më të mëdha të skedarëve. Pasi Ken tha se është më mirë t'i shmangim ato, do ta besoj me kënaqësi.

Këtu është një thirrje e vërtetë sistemore 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;
}

NĂ« koment Ă«shtĂ« e qartĂ« se çfarĂ« po ndodh kĂ«tu. Por tĂ« kuptosh kodin nuk Ă«shtĂ« aq e lehtĂ«, pjesĂ«risht pĂ«r shkakun se si me ‘struct user u’ dhe regjistrat R0 dhe R1 kalojnĂ« parametrat e thirrjeve sistemore dhe vlerat e kthimit.

Le të provojmë me ndihmën e ialloc() të vendosim në disk inode (indikatori i indeksit), dhe me ndihmën e falloc() të vendosim në memorje dy skedare. Nëse gjithçka shkon mirë, atëherë ne do të vendosim flagat për të identifikuar këto skedare si dy extrema të tubit, do t'i indikojmë në të njëjtin inode (numri i referencave do të shkojë në 2), dhe do ta shënojmë inode si të modifikuar dhe në përdorim. Kushtojini vëmendje thirrjeve të iput() në rrugët me gabime (error paths) për të ulur numrin e referencave në inode-n e ri.

pipe() duhet pĂ«rmes R0 dhe R1 tĂ« kthejĂ« numrat e descriptorĂ«ve tĂ« skedarĂ«ve pĂ«r lexim dhe shk Schreib. falloc() kthe njĂ« tregues nĂ« strukturĂ«n e skedarit, por gjithashtu ‘kthe’ pĂ«rmes u.u_ar0[R0] dhe descriptorĂ«t e skedarĂ«ve. DomethĂ«nĂ«, kodi ruan nĂ« r descriptorin e skedarit pĂ«r tĂ« lexuar dhe i jep descriptorin pĂ«r tĂ« shkruar direkt nga u.u_ar0[R0] pas thirrjes sĂ« dytĂ« falloc().

Flamuri FPIPE, i cili e drejton funksionin rdwr() në sys2.c, e cila thërret nënprogramet specifike për hyrje-dalje I/O:

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

Pastaj funksioni readp() në pipe.c lexon të dhënat nga tubi. Por është më mirë të ndjekësh realizimin duke filluar nga writep(). Përshkruaj, kodi u komplikuar për shkak të veçorive të marrëveshjes për kalimin e argumenteve, por disa detaje mund të injorohen.

writep(fp)
{
    regjistroj *rp, *ip, c;

    rp = fp;
    ip = rp->f_inode;
    c = u.u_count;

loop:
    /* Nëse gjithçka është bërë, kthehu. */

    plock(ip);
    nëse(c == 0) {
        prele(ip);
        u.u_count = 0;
        kthehu;
    }

    /*
     * Nëse nuk ka të dy anët e leximit dhe shkruajtur të aktivizuara,
     * kthe gabim dhe njofto gjithashtu.
     * /

    nëse(ip->i_count i_size1 == PIPSIZ) {
        ip->i_mode =| IWRITE;
        prele(ip);
        sleep(ip+1, PPIPE);
        shko në loops;
    }

    /* Shkruaj atë që është e mundur dhe kthehu. */

    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);
    nëse(ip->i_mode&IREAD) {
        ip->i_mode =& ~IREAD;
        wakeup(ip+2);
    }
    shko në loop;
}

Në hyrje të tubit duam të shkruajmë byte u.u_count. Së pari kërkojmë të bllokojmë indikatorin e indeksit (shih më poshtë plock/prele).

Pastaj kontrollojmë numëruesin e lidhjeve inode. Ndërsa të dy skajet e tubit mbeten të hapura, numëruesi duhet të jetë 2. Ne mbajmë një lidhje (nga rp->f_inode), kështu që nëse numëruesi është më pak se 2, kjo do të thotë që procesi lexues ka mbyllur skajin e tij të tubit. Me fjalë të tjera, po përpiqemi të shkruajmë në një tub të mbyllur dhe kjo është një gabim. Kodi i gabimit EPIPE dhe sinjali SIGPIPE u shfaqën në edicionin e gjashtë të Unix.

Por edhe nëse tubi është i hapur, ai mund të jetë i mbushur. Në këtë rast, ne heqim bllokimin dhe shkojmë për të fjetur në shpresë se një proces tjetër do të lexojë nga tubi dhe do të lirojë vend të mjaftueshëm. Pasi të zgjohemi, kthehemi në fillim, përsëri vendosim bllokimin dhe fillojmë një cikël të ri shkruese.

Nëse ka mjaft hapësirë të lirë në tub, atëherë ne shkruajmë të dhënat me writei(). Parametri i_size1 i inode-it (në një tub të zbrazët, mund të jetë e barabartë me 0) tregojnë përfundimin e të dhënave që tashmë ndodhen aty. Nëse ka mjaft vend për të shkruar, ne mund ta mbushim tubin deri në i_size1 në PIPESIZ. Pastaj heqim bllokimin dhe përpiqemi të zgjojmë çdo proces që pret mundësinë të lexojë nga tubi. Kthehemi në fillim për të parë nëse arritëm të shkruajmë aq byte sa na nevojiteshin. Nëse nuk arritëm, atëherë fillojmë një cikël të ri shkruese.

Zakonesh, parametri i_mode i inode-it përdoret për të ruajtur lejet r, w dhe x. Por në rastin e tubeve, ne sinjalizojmë për pritjen për shkak të ndonjë procesi shkruese ose leximi përmes biteve IREAD dhe IWRITE përkatësisht. Procesi vendos një flamur dhe thërret sleep(), dhe pritet që më vonë, ndonjë proces tjetër të thërrasë wakeup().

Mrekullia reale ndodh në sleep() dhe wakeup(). Ata janë implementuar në slp.c, burimi i komentit të famshëm "Nuk jeni të detyruar ta kuptoni këtë" (You are not expected to understand this). Fatmirësisht, ne nuk jemi të detyruar të kuptojmë kodin, thjesht do të shohim disa komente:

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

Procesi që thërret sleep() për një kanal të caktuar mund të zgjohet më vonë nga një proces tjetër që thërret wakeup() për të njëjtin kanal. writep() dhe readp() koordinon veprimet e tij përmes thirrjeve të tilla çift. Kini parasysh se pipe.c ka gjithmonë prioritet PPIPE në thirrje sleep(), prandaj të gjitha sleep() mund të ndërpriten nga sinjali.

Tani kemi gjithçka për të kuptuar funksionin readp():

readp(fp)
int *fp;
{
    register *rp, *ip;

    rp = fp;
    ip = rp->f_inode;

loop:
    /* Bllokim i ruajtur. */

    plock(ip);

    /*
     * Nëse kreu (leximi) ka arritur në
     * bishtin (shkrimi), rivendos të dy në 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);
            }
        }

        /*
         * Nëse nuk ka as lexues as
         * shkrues aktiv, kthehu pa
         * plotësuar leximin.
         * /

        prele(ip);
        if(ip->i_count i_mode =| IREAD;
        sleep(ip+2, PPIPE);
        goto loop;
    }

    /* Lexo dhe kthehu */

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

Mund të jetë më e lehtë të lexoni këtë funksion nga poshtë lart. Dega "lexo dhe kthehu" përdoret zakonisht kur në tub ka disa të dhëna. Në këtë rast ne përdorim readi() për të lexuar aq të dhëna sa janë në dispozicion duke filluar nga aktualja f_offset e leximit, dhe pastaj azhurnojmë vlerën e përgjegjshmërisë përkatëse.

Në leximin e ardhshëm, tubi do të jetë bosh nëse pozita e leximit arriti vlerën i_size1 te inode-it. Ne e rivendosim pozitën në 0 dhe përpiqemi të zgjojmë çdo proces që dëshiron të shkruajë në tub. E dimë që kur tubi të jetë plotë, writep() do të flejë në ip+1.Tani, kur tubi është bosh, mund ta zgjojmë që të rinisë ciklin e tij të shkrimit.

Nëse nuk ka për të lexuar, atëherë readp() mund të vendosë një flag IREAD dhe të flejë në ip+2.E dimë se do ta zgjojë writep(), kur të shkruajë në tub disa të dhëna.

Komentet për readi() dhe writei() ndihmojnë në kuptimin se në vend të kalimit të parametrave përmes "u", mund të trajtojmë ato si funksione të zakonshme të hyrjes-daljes, që marrin skedarin, pozitën, tamponin në memorie, dhe llogarisin numrin e bajtëve për të lexuar ose shkruar.

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

Sa i përket bllokimit "konservator", ai readp() dhe writep() bllokon inode-in derisa të përfundojë punën ose të marrë rezultatin (dmth të thërrasë wakeup). plock() dhe prele() funksionojnë thjesht: me një grup tjetër thirrjesh fjetje dhe wakeup na lejojnë të zgjojmë çdo proces që ka nevojë për bllokimin që sapo e zgjidhëm:

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

NĂ« fillim nuk mund ta kuptoja pse readp() nuk thĂ«rriste prele(ip) para se tĂ« thĂ«rriste wakeup(ip+1).Gjithçka qĂ« writep() thĂ«rriste nĂ« ciklin e tij Ă«shtĂ« plock(ip), e cila sjell deri nĂ« bllokim tĂ« plotĂ«, nĂ«se readp() nuk e ka hequr bllokimin e tij, kĂ«shtu qĂ« kodi duhet tĂ« funksionojĂ« saktĂ«sisht. NĂ«se shohim pĂ«r wakeup(), atĂ«herĂ« bĂ«het e qartĂ« se ai thjesht e shĂ«non procesin e fjetur si tĂ« gatshĂ«m pĂ«r t'u realizuar, nĂ« mĂ«nyrĂ« qĂ« nĂ« tĂ« ardhmen sched() tĂ« vĂ«rtetojĂ« se e ka nisur. Pra, readp() shkakton wakeup(), heq bllokimin, cakton IREAD dhe thĂ«rret sleep(ip+2)— gjithçka kjo para se writep() tĂ« rinisĂ« ciklin.

Këtu përfundon përshkrimi i kanaleve në versionin e gjashtë. Kodi i thjeshtë, pasoja të thella.

Versioni i shtatë i Unix (janar 1979) ishte një lëshim i ri kryesor (pas katër vjetësh), në të cilin u shfaqën shumë aplikacione dhe veçori të reja të bërthamës. Gjithashtu ndodhi një ndryshim i konsiderueshëm lidhur me përdorimin e konvertimit të tipeve, union-eve dhe treguesve të tipizuar në struktura. Megjithatë Kodi i kanaleve pati pothuajse asnjë ndryshim. Mund ta kalojmë këtë version.

Xv6, një bërthamë e thjeshtë si Unix

Krijimi i bërthamës Xv6 u ndikua nga versioni i gjashtë i Unix, megjithatë është shkruar në C të modernizuar, për ta ekzekutuar në procesorët x86. Kodi është i lehtë për t'u lexuar, është i qartë. Për më tepër, ndryshe nga burimet e Unix me TUHS, ju mund ta kompilonit, ta modifikoni dhe ta ekzekutonit në diçka tjetër përveç PDP 11/70. Prandaj, kjo bërthamë përdoret gjerësisht në universitete si material mësimor për sistemet operative. Burimet janë në Github.

Kodi përmban një realizim të qartë dhe të menduar pipe.c, e mbështetur nga një buffer në memorie në vend të inode në disk. Këtu unë paraqes vetëm definicionin e "kanalit strukturor" dhe funksionin 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() cakton gjendjen e gjithë realizimit të mbetur, që përfshin funksionet piperead(), pipewrite() dhe pipeclose(). Thirrja reale e sistemit sys_pipe është një mbështjellës, i realizuar në sysfile.c. Këshilloj të lexoni të gjithë kodin e tij. Kompleksiteti është në nivelin e burimeve të versionit të gjashtë, por është shumë më e lehtë dhe më këndshme për t'u lexuar.

Linux 0.01

Mund të gjeni burimin e kodit Linux 0.01. Do të jetë edukative të shqyrtoni realizimin e kanaleve në të fs/pipe.c. Këtu përpara se përfaqësimi i kanalit përdor inode, por vetë kanali është shkruar në C të modernizuar. Nëse keni kaluar përmes kodit të versionit të gjashtë, këtu nuk do të hasni vështirësi. Këtu duket funksioni 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) { 
        current->signal |= (1< 0) {
        while (PIPE_FULL(*inode)) {
            wake_up(&inode->i_wait);
            if (inode->i_count != 2) {
                current->signal |= (1<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;
}

Edhe pa e parë definimin e strukturave, mund të kuptohet se si numëruesi i referencave inode përdoret për të verifikuar nëse operacioni i shkruar SIGPIPE. Përveç punës me bajta, kjo funksion është lehtësisht e krahasueshme me idetë e përmendura më sipër. Edhe logjika sleep_on/wake_up nuk duket kaq e huaj.

Bërthamat moderne Linux, FreeBSD, NetBSD, OpenBSD

Shpejt kalova në disa nga bërthamat moderne. Asnjëra prej tyre nuk ka më implementimin e përdorimit të diskut (nuk është e çuditshme). Linux ka implementimin e vet. Dhe megjithëse tri bërthamat moderne BSD përmbajnë implementime të bazuara në kodin e shkruar nga John Dyson, gjatë viteve ato janë diferencuar shumë nga njëra-tjetra.

PĂ«r tĂ« lexuar fs/pipe.c (nĂ« Linux) ose sys/kern/sys_pipe.c (nĂ« *BSD), kĂ«rkohet njĂ« angazhim i vĂ«rtetĂ«. Sot nĂ« kod janĂ« tĂ« rĂ«ndĂ«sishme performanca dhe mbĂ«shtetja pĂ«r funksione si operacionet e input-output vektoriale dhe asinkrone. Detajet e alokimit tĂ« memories, bllokimeve dhe konfiguratĂ«s sĂ« bĂ«rthamĂ«s — tĂ« gjitha kĂ«to shumĂ« ndahen. Nuk Ă«shtĂ« diçka qĂ« i nevojitet universiteteve pĂ«r njĂ« kurs hyrĂ«s pĂ«r sistemet operative.

Në çdo rast, më interesonte të zbulonja disa modele të lashta (për shembull, gjenerim SIGPIPE dhe kthim EPIPE kur shkruhet në një tub të mbyllur) në të gjitha këto, bërthamat moderne që janë kaq të ndryshme. Ndoshta nuk do të shoh kurrë në jetë kompjuterin PDP-11, por ende ka shumë për të mësuar nga kodi që është shkruar disa vite para lindjes sime.

Artikulli i shkruar nga Divi Kapoor në vitin 2011 "The Linux Kernel Implementation of Pipes and FIFOs" paraqet një përmbledhje se si punojnë (ende) tubat në Linux. Po ashtu një komit të fundit në Linux ilustron modelin e tubit të ndërveprimit, i cili tejkalon mundësitë e skedarëve temporalë; dhe gjithashtu tregon se sa larg janë shkuar tubat nga "bllokimi shumë konservator" në bërthamën e Unix në edicionin e gjashtë.

Burimi: habr.com

Blini hosting tĂ« besueshĂ«m pĂ«r faqe interneti me mbrojtje nga DDoS, serverĂ« VPS VDS đŸ”„ Blini hosting tĂ« besueshĂ«m pĂ«r faqe interneti me mbrojtje nga DDoS, serverĂ« VPS VDS | ProHoster