Si janë realizuar tubacionet në Unix

Si janë realizuar tubacionet në Unix
Ky kjo artikull përshkruan implementimin e tubave në thelbin Unix. Kam qenë disi i zhgënjyer që artikulli i fundit me titull "Si funksionojnë tubat në Unix?" rezultoi nuk për strukturën e brendshme. Më erdhi kurioziteti, dhe unë hulumtova në burime të vjetra për të gjetur përgjigjen.

Për çfarë bëhet fjalë?

Tubat — "ndoshta shpikja mĂ« e rĂ«ndĂ«sishme nĂ« Unix" — janĂ« njĂ« karakteristikĂ« pĂ«rcaktuese qĂ« qĂ«ndron nĂ« thelb tĂ« filozofisĂ« Unix tĂ« bashkimit tĂ« programeve tĂ« vogla, pĂ«rveç shenjĂ«s sĂ« njohur nĂ« vijĂ«n e komandĂ«s:

$ echo hello | wc -c
6

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

Tubat ofrojnë një kanal njëdrejtimëshe për ndërveprimin midis proceseve. Një tub ka një hyrje (write end) dhe një dalje (read end). Të dhënat e shkruara në hyrjen e tubit mund të lexohen në daljen e tij.

Tubi krijohet me një thirrje pipe(2), e cila kthen dy deshifrues të skedarëve: një për hyrjen e tubit, tjetri për daljen.

Rezultatet e ndjekjes së komandës së lartë tregojnë krijimin e tubit dhe rrjedhën e të dhënave përmes tij 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ë deshifrues të skedarëve të lidhur. Një proces i vogël shkruan në një deshifruese, ndërsa procesi tjetër lexon të njëjtat të dhëna nga një deshifruese tjetër. Shell me dup2 "rinovon" deshifruesit 3 dhe 4, që të përputhen me stdin dhe stdout.

Pa tubat, shell do të duhej të shkruante rezultatin e një procesi në një skedar dhe ta kalonte atë në një proces tjetër për ta lexuar të dhënat nga skedari. Si rezultat, do të shpenzonim më shumë burime dhe hapësirë në disk. Sidoqoftë, tubat nuk janë të mira vetëm për të shmangur përdorimin e skedarëve të përkohshëm:

Nëse një proces përpiqet të lexojë nga një tub 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ë tub të mbushur, atëherë write(2) do të bllokohet deri sa nga tubi të kenë lexuar mjaft të dhëna për të realizuar shkrimin.

Siç është kërkesa e POSIX, kjo është një pronë e rëndësishme: shkrimi në tub deri në PIPE_BUF bytes (të paktën 512) duhet të jetë atomar, në mënyrë që proceset të mund të ndërveprojnë me njëra-tjetrën përmes tubit ashtu siç dosjet e zakonshme (që nuk ofrojnë këto garanci) nuk mund të bëjnë.

Kur përdor një skedar të zakonshëm, një proces mund të shkruajë të gjithë të dhënat e tij të daljes dhe t'ia kalojë një procesi tjetër. Ose proceset mund të veprojnë në mënyrë të ngushtë, duke përdorur një mekanizëm sinjalizimi të jashtëm (si një semafor) për të komunikuar njëri me tjetrin për përfundimin e shkrimit ose leximit. Tubat na çlirojnë nga të gjitha këto shqetësime.

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

Do ta shpjegoj me gishta, që të keni një ide më të qartë se si mund të funksionojë një tub. Do t'ju nevojitet të alokoni një bufer në memorie dhe një gjendje. Do t'ju duhen funksione për të shtuar dhe hequr të dhëna nga buferi. Do të nevojitet një mjet për të thirrur funksionet gjatë operacioneve të leximit dhe shkrimit në deshifruesit e skedarëve. Dhe do t'ju nevojiten bllokimet për të realizuar sjelljen speciale të përshkruar më lart.

Tani jemi gati të shqyrtojmë nën dritën e fortë të llampës kodin burimor të bërthamës, për të konfirmuar ose për të mohuar modelin tonë të paqartë mendor. Por gjithmonë shikoni për befasitë.

Ku po kërkojmë?

Nuk e di se ku ndodhet ekzemplari im i njohur i librit "Lions book" me kodin burimor të Unix 6, por falë Shoqëria e Trashëgimisë Unix mund të kërkoni në online në kodin burimor të versioneve edhe më të vjetra të Unix.

Shëtitjet në arkivat TUHS janë si një vizitë në muze. Mund të hedhim një vështrim në 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 ndiej thellësisht fragmentet që ende mungojnë.

Pasi kënaqa kuriozitetin tim mbi historinë e lashtë të tubave, për të krahasuar mund të shohim bërthamat bashkëkohore.

Me thënë të vërtetën, pipe është thirrja e sistemit numër 42 në tabelën sysent[]. Rastësi?

BĂ«rthamat tradicionale Unix (1970–1974)

Nuk gjetëm asnjë gjurmë pipe(2) as në PDP-7 Unix (janar 1970), as në redaktimin e parë të Unix (nëntor 1971), as në kodin burimor të paplotë redaksinë e dytë (qershor 1972).

TUHS pretendon se redaktimi i tretë i Unix (shkurt 1973) ishte versioni i parë me tubat:

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

Një nga lexuesit gjeti një skan dokumenti, në të cilin Dag McIlroy propozoi idenë e "lidhenjës së programeve në parimin e një hose kopshti".

Si janë realizuar tubacionet në Unix
Në librin e Brian Kernighan, "Unix: A History and a Memoir", historia e shfaqjes së kanaleve përmendet gjithashtu 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ë tjetër histori nga punimi i McIlroy, i shkruar në vitin 2014:

Kur u shfaq Unix, pasioni im për korutina më bëri të kërkoj nga autori i OS-së, Ken Thompson, të lejojë që të dhënat e shkruara në një proces të shkonin jo vetëm në pajisje, por edhe në daljen në një proces tjetër. Ken vendosi se kjo ishte e mundur. Megjithatë, si një minimalist, ai donte që çdo funksion sistemor të luante një rol të rëndësishëm. A ka vërtet një përparësi të madhe në shkrimin direkt midis proceseve krahasuar me shkrimin në një skedë ndërmjetëse? Dhe vetëm kur unë dorëzova një propozim të veçantë me një emër tërheqës "kanal" dhe përshkrimin e sintaksës së ndërveprimit mes proceseve, Ken, në fund, thirri: "Unë do ta bëj këtë!".

Dhe e bëri. Një mbrëmje fatlume, Ken ndryshoi bërthamën dhe shell-in, rregulloi disa programe standarde, duke standardizuar procedurën e marrjes së të dhënave (të cilat mund të vijnë nga kanali), si dhe ndëroi emrat e skedarëve. Të nesërmen, kanalet filluan të përdoren gjerësisht në aplikacione. Në përfundim të javës, sekretaret i dërguan dokumentet në printer nga editorët e teksteve me ndihmën e tyre. Pak më vonë, Ken zëvendësoi API-në origjinale dhe sintaksën për përdorimin e kanaleve me marrëveshje më të pastra, të cilat përdoren nga ajo kohë.

Fatkeqësisht, kodi burimor i bërthamës së editimit të tretë të Unix është humbur. Edhe pse ne kemi kodin burimor të shkruar në C të edicionit të katërt, i cili doli në nëntor 1973, ai doli disa muaj para lëshimit zyrtar dhe nuk përmban implementimin e kanaleve. Më vjen keq që kodi burimor i funksionit legjendar të Unix është humbur, ndoshta për gjithmonë.

Ne kemi tekstin e dokumentacionit për pipe(2) të dy lëshimeve, kështu që mund të fillojmë nga kërkimi në dokumentacionin e edicionit të tretë (me fjalët e caktuara, të theksuara "me dorë", një rresht nga literalet ^H, pas të cilit vjen një nënvizim!). Ky proto-pipe(2) është shkruar në assembler dhe kthehet vetëm një descriptor skedari, por tashmë ofron funksionalitetin e nevojshëm:

Thirrja e sistemit pipe krijon një mekanizëm hyrje-dalje që quhet kanal. Descriptor skedari i kthyer mund të përdoret për operacione leximi dhe shkrimi. Kur diçka shkruhet në kanal, ajo buferizohet deri në 504 byte të dhënash, pas së cilës procesi i shkrimit pezullohet. Kur lexoni nga kanali, të dhënat e buferizuara merren.

Në vitin tjetër, bërthama u rishkrua në C dhe pipe(2) në edicionin e katërt mori formën e saj moderne me prototipin "pipe(fildes)"»:

Thirrja e sistemit pipe krijon një mekanizëm hyrje-dalje që quhet kanal. Descriptorët e kthyer mund të përdoren në operacionet e leximit dhe shkrimit. Kur diçka shkruhet në kanal, përdoret descriptor-i i kthyer në r1 (përkatësisht fildes[1]), që buferizohet deri në 4096 byte të dhënash, pas së cilës procesi i shkrimit pezullohet. Kur lexoni nga kanali, descriptor-i i kthyer në r0 (përkatësisht fildes[0]), merr të dhënat.

Mendohet se pas përcaktimit të kanalit, dy (ose më shumë) procese që bashkëpunojnë (të krijuara me thirrje të mëvonshme fork) do të transferojnë të dhënat nga kanali përmes thirrjeve read dhe shkruaj.

Në shell, ka sintaksë për të përcaktuar një array linear të proceseve të lidhura përmes kanaleve.

Thirrjet për të lexuar nga një kanal të zbrazët (pa të dhëna të buferizuara), që ka vetëm një fund (të gjitha descriptorët e skedarit për të shkruar janë të mbyllur), kthejnë "fundin e skedarit". Thirrjet për të shkruar në një situatë të ngjashme kanë kaluar pa u marrë parasysh.

Implementimi më i hershëm i mbetur i kanaleve i përket në edicionin e pestë të Unix (qershor 1974), por është pothuajse identike me atë që shfaqet në lëshimin e ardhshëm. Vetëm janë shtuar komentet, kështu që edicioni i pestë mund të kalojë.

Edicioni i gjashtë i Unix (1975)

Fillojmë të lexojmë kodin burimor të Unix të edicionit të gjashtë (maj 1975). Në shumë aspekte falë Lions është shumë më e lehtë ta gjesh atë sesa burimet e versioneve më herët:

Shumë vite libri Lions ishte dokumenti i vetëm për bërthamën Unix, i disponueshëm jashtë mureve të Bell Labs. Megjithëse licenca e redaksisë së gjashtë lejonte mësuesit të përdornin kodin e saj burimor, licenca e redaksisë së shtatë e përjashtoi këtë mundësi, prandaj libri shpërndahej në formën e kopjeve të paligjshme të shkruara me dorë.

Sot mund të blini një kopje të riprintuar të librit, në kopertinën e të cilit janë studentë pranë një aparati kopjimi. Falë Warren Tumin (i cili nisi projektin TUHS), mund të shkarkoni skedarin PDF me kodin burimor të redaksisë së gjashtës. Dua t'ju jap një ide se sa punë është dashur për të krijuar këtë skedar:

Më shumë se 15 vjet më parë, unë shkruajta një kopje të kodit burimor të paraqitur në Lions, pasi nuk më pëlqente cilësia e kopjes time nga numri i panjohur i kopjeve të tjera. TUHS nuk ishte ende ekzistues, dhe nuk kisha qasje në burimet e vjetra. Por në vitin 1988, gjej një kasetë të vjetër me 9 pista, mbi të cilën ishte një kopje rezervë nga kompjuteri PDP11. Ishte e vështirë të kuptohej nëse ajo funksiononte, por kishte një pemë të paprekur /usr/src/, ku shumica e skedave ishin të etiketuar me vitin 1979, i cili atëherë dukej si një antiku. Kjo ishte redaksia e shtatë ose një derivat i saj PWB, siç mendoja.

E mora atë gjetje si bazë dhe e redaktova manualisht kodin burimor deri në gjendjen e redaksisë së gjashtës. Disa pjesë të kodit mbetën të njëjta, disa ishin duhej të redaktoheshin pak, duke ndryshuar token moderne += në të kaluarën =+. Disa thjesht i fshiva, dhe disa të tjera duhej t'i shkruaja plotësisht, por jo shumë.

Dhe sot mund të lexojmë në internet në TUHS kodin burimor të redaksisë së gjashtës nga arkivi, në të cilin kontribuoi Dennis Ritchie.

Dhe, për ndonjë qëllim, karakteristika kryesore e kodit C përpara periudhës së Kernighan dhe Ritchie është shkurtësia. Nuk më ndodh shpesh të fut fragmente kodi pa pasur nevojë për redaktim të gjerë, për ta bërë atë të përshtatet në një hapësirë të relativisht të ngushtë për shfaqjen në faqen time.

Në fillim /usr/sys/ken/pipe.c ka një koment shpjegues (dhe po, atje 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 tamponit nuk është ndërruar që nga redaksia e katërt. Por këtu pa ndonjë dokumentacion publik ne shohim që dikur kanalet përdornin skedarë si një depo rezervë!

Sa i përket skedarëve LARG, ata korrespondojnë me flamurin inode LARG, i cili përdoret nga "algoritmi i adresimit të madh" për të trajtuar blloqet indirekte në mënyrë që të mbështesin sisteme më të mëdha skedarësh. Pasi Ken tha se ishte më mirë t'i përdornim, unë me kënaqësi i besoj atij.

Ja një thirrje sistemike e vërtetë 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 qartë përshkruhet se çfarë po ndodh. Por të kuptosh kodin nuk është aq e lehtë, pjesërisht për shkak të mënyrës se si me "struct user u" dhe regjistrat R0 dhe R1 merren parametrat e thirrjeve sistemike dhe vlerat e kthimit.

Le të provojmë me ialloc() të vendosim në disk inode (indeksi i descriptorit), dhe me falloc() të vendosim në memorie dy e skedarit. Nëse gjithçka shkon mirë, ne do të vendosim flamuj për t'i identifikuar këta skedarë si dy skajet e një kanali, do t'i indikojmë ata në të njëjtin inode (gjë që do ta bëjë numrin e referencave të tij 2), dhe do ta shënojmë inode si të modifikuar dhe të përdorur. Vini re thirrjet në iput() në rrugët e gabimeve (error paths) për të ulur numrin e referencave në inode-in e ri.

pipe() duhet të R0 dhe R1 kthente numrat e descriptorëve të skedarëve për lexim dhe shkruaj. falloc() kthen një pointer në strukturën e skedarëve, por gjithashtu "kthen" përmes u.u_ar0[R0] dhe descriptorin e skedarit. Domethënë, kodi ruan në r descriptorin e skedarit për lexim dhe i cakton descriptorin për shkruar direkt nga u.u_ar0[R0] pas thirrjes së dytë falloc().

Flamuri FPIPE, i cili ne e vendosëm gjatë krijimit të kanalit, menaxhon sjelljen e funksionit rdwr() në sys2.c, e cila thërret në mënyrë specifike programet nën-hyrje-hyrje 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);
    }
        /* 
 */
}

Më pas funksioni readp() në pipe.c lexon të dhënat nga kanali. Por është më mirë të ndjekim zbatimin duke filluar me writep(). Pa dyshim, kodi është komplikuar për shkak të veçorive të marrëveshjes së kalimit të argumenteve, por disa detaje mund të theken.

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

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

loop:
    /* Nëse është gjithçka e kryer, kthehu. */

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

    /*
     * Nëse nuk ka as të dy skajet leximi dhe shkruaj të kanaleve aktive, kthe një gabim dhe sinjalizoj gjithashtu.
     */

    nëse(ip->i_count < 2) {
        prele(ip);
        u.u_error = EPIPE;
        psignal(u.u_procp, SIGPIPE);
        kthehu;
    }

    /*
     * Nëse kanali është i plotë, prisni që leximet të shuhen
     * dhe ta shkurtëzojmë atë.
     */

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

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

    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;
        zgjo(ip+2);
    }
    shko në loop;
}

Në hyrjen e kanali duam të shkruajmë bytes u.u_count. Së pari, kërkojmë të bllokojmë index deskriptorin (shih më lart 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ë i vogël se 2, kjo duhet të nënkuptojë se procesi lexues ka mbyllur skajin e tij të tubit. Me fjalë të tjera, ne 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ë redaktimin e gjashtë të Unix-it.

Por edhe nëse tubi është i hapur, ai mund të jetë i mbushur. Në këto raste, ne heqim bllokimin dhe shkojmë të flajmë në shpresë që një proces tjetër të lexojë nga tubi dhe të çlirohet hapësira e mjaftueshme. Duke u zgjuar, kthehemi në fillim, përsëri vendosim bllokimin dhe fillojmë një cikël të ri shkrimi.

Nëse në tub ka hapësirë të mjaftueshme të lirë, atëherë ne shkruajmë në të duke përdorur writei(). Parametri i_size1 nga inode-i (nëse tubi është i zbrazët, mund të jetë 0) tregon përfundimin e të dhënave që tashmë janë aty. Nëse ka mjaft hapësirë për të shkruar, mund ta mbushim tubin deri i_size1 deri në PIPESIZ. Pastaj heqim bllokimin dhe përpiqemi të zgjojmë çdo proces që pret mundësinë për të lexuar nga tubi. Kthehemi në fillim për të parë nëse arritëm të shkruajmë aq byte sa na duhej. Nëse nuk ia dolëm, fillojmë një cikël të ri shkrimi.

Zakonisht parametri i_mode nga inode-i përdoret për të ruajtur lejet r, w dhe x. Por në rastin e tubave, ne sinjalizojmë pritjen të ndodhur nga një proces që shkruan apo lexon përmes bllokjeve IREAD dhe IWRITE përkatësisht. Procesi vendos një flamur dhe thërret sleep(), dhe pritet që në të ardhmen ndonjë proces tjetër të thërrasë wakeup().

Magjia e vërtetë ndodh në sleep() dhe wakeup(). Ato janë të implementuara në slp.c, burimi i famshëm i komentit "Nuk pritet të kuptoni këtë" (You are not expected to understand this). Fatmirësisht, nuk është nevoja të kuptojmë kodin, le 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ë zgjidhet më vonë nga një proces tjetër që thërret wakeup() për të njëjtin kanal. writep() dhe readp() koordinon veprimet e tyre përmes thirrjeve të tilla të çiftuara. Kujdes, që pipe.c ndihmon për gjithmonë përparësi PPIPE kur thërrisni sleep(), kështu që të gjithë sleep() mund të preken 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 shumë konservator. */

    plock(ip);

    /*
     * Nëse kreu (lexo) ka arritur në
     * bisht (shkruaj), 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
         * shkruese aktive, 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);
}

Ndoshta do të jetë më e lehtë ta lexoni këtë funksion nga poshtë lart. Degëza "lexo dhe kthehu" përdoret zakonisht kur në tub ka disa të dhëna. Në këtë rast ne përmes readi() lexojmë sa më shumë të dhëna që janë të disponueshme duke filluar nga pozita aktuale f_offset e leximit, dhe më pas përditësojmë vlerën e përkatës së pozicionit.

Në leximin e ardhshëm tubi do të jetë i zbrazët, nëse pozita e leximit ka arritur vlerën i_size1 nga inode-i. Ne rivendosim pozitat në 0 dhe përpiqemi të zgjojmë çdo proces që dëshiron të shkruajë në tub. Ne e dimë se kur tubi do të jetë i plotë, writep() do të flijë në ip+1. Tani, kur tubi është i zbrazët, mund ta zgjojmë që të rifillojë ciklin e tij të shkrimit.

Nëse nuk ka çfarë të lexosh, atëherë readp() mund të vendosë një flamur IREAD dhe të flë në ip+2. Ne e dimë se do ta zgjojë writep(), kur të shkruajë ndonjë të dhënë në tub.

Komentet mbi readi() dhe writei() ndihmojnë të kuptohet se në vend të kalimit të parametrave përmes "u" ne mund të trajtojmë ato si funksione të zakonshme hyrëse-dalëse, të cilat marrin skedarin, pozitat, buferrat në memorje dhe llogarisin numrin e byte-ëve për të lexuar apo 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", readp() dhe writep() bllokojnë inode deri sa të përfundojnë punën ose të marrin një rezultat (dmth thërrasin wakeup). plock() dhe prele() funksionojnë thjesht: përmes një seti tjetër thirrjesh gjumë dhe wakeup na lejon të zgjojmë çdo proces që ka nevojë për një bllokim që sapo e hoqë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Ă«rret prele(ip) para thirrjes wakeup(ip+1). GjĂ«ja e parĂ« qĂ«thĂ«rret nĂ« ciklin e tij Ă«shtĂ« writep() plock(ip) , qĂ« shkakton bllokim ndĂ«rsjellĂ«, nĂ«senuk e ka hequr bllokimin e vet, prandaj kodi nĂ« njĂ«farĂ« mĂ«nyre duhet tĂ« funksionojĂ« siç duhet. NĂ«se shikoni nĂ« readp() , bĂ«het e qartĂ« se ai thjesht shĂ«non procesin qĂ« fle si tĂ« gatshĂ«m pĂ«r t'u ekzekutuar, nĂ« mĂ«nyrĂ« qĂ« nĂ« tĂ« ardhmen wakeup()sched() realizoi vĂ«rtet ta fillonte. Pra, vĂ«rtet e nisi atĂ«. KĂ«shtu qĂ« readp() thĂ«rrit wakeup(), he heq blokkimin, cakton IREAD dhe thĂ«rret sleep(ip+2)— e gjithĂ« kjo para se writep() tĂ« rifillojĂ« ciklin.

Këtu përfundojnë përshkrimet e tubacioneve në edicionin e gjashtë. Kodi i thjeshtë, pasoja të mëdha.

Edisioni i shtatë i Unix (janar 1979) ishte një lëshim i ri kryesor (katër vjet pas), në të cilin erdhën shumë aplikacione dhe vetitë e reja të bërthamës. Për më tepër, ndodhi një ndryshim i rëndësishëm lidhur me përdorimin e tipizimeve, union-eve dhe pointer-ave të tipizuar në struktura. Megjithatë Kodi i tubacioneve praktikisht nuk ka ndryshuar. Mund ta kalojmë këtë edicion.

Xv6, një bërthamë e thjeshtë e ngjashme me Unix

Krijimi i bĂ«rthamĂ«s Xv6 u ndikua nga edicioni i gjashtĂ« i Unix, megjithatĂ« ajo Ă«shtĂ« shkruar nĂ« C modern, pĂ«r t’u ekzekutuar nĂ« procesorĂ«t x86. Kodi Ă«shtĂ« i lehtĂ« pĂ«r t'u lexuar, i qartĂ«. PĂ«r mĂ« tepĂ«r, ndryshe nga burimet e Unix nga TUHS, ju mund ta kompiloni, modifikoni dhe ekzekutoni atĂ« 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ë implementim të qartë dhe të menduar mirë pipe.c, i mbështetur në një bufer në memorie në vend të inode-it në disk. Këtu po sjell vetëm definimin e "tubacioneve strukturale" dhe funksionet 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ë implementimit tjetër, i cili përfshin funksionet piperead(), pipewrite() dhe pipeclose(). Thirrja reale në sistem sys_pipe është një mbështjellës, i implementuar në sysfile.c. E rekomandoj të lexoni të gjithë kodin e tij. Kompleksiteti është në nivelin e burimit të edicionit të gjashtë, por është shumë më e lehtë dhe më këndshme për t'u lexuar.

Linux 0.01

Kodi burimor i Linux 0.01 mund të gjendet. Do të ishte një përvojë e dobishme të studiohej implementimi i tubacioneve në të fs/pipe.c. Këtu, për përfaqësimin e tubacioneve përdoret inode, por vetë tubacioni është shkruar në C modern. Nëse keni kaluar përmes kodit të edicionit të gjashtë, këtu nuk do të hasni vështirësi. Kështu 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) { /* nuk ka lexues */
        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;
}

Madje pa parë definicionet e strukturave, mund të kuptohet se si përdoret numri i referencës në inode për të verifikuar nëse operacioni i shkrimit çon në SIGPIPE. Përveç punës me byte, ky funksion është e lehtë të përputhet me idetë e përshkruara më sipër. Edhe logjika sleep_on/wake_up nuk duket kaq e huaj.

Bërthamat moderne Linux, FreeBSD, NetBSD, OpenBSD

Unë kam bërë një shpejtësi për disa bërthama moderne. Asnjëra prej tyre nuk përmban më implementimin që përdor diskun (nuk është surprizë). Në Linux ka një implementim të vetin. Dhe megjithatë, tre bërthamat moderne të BSD përmbajnë implementime të bazuara në kodin e shkruar nga John Dyson, por gjatë viteve ato kanë ndryshuar shumë nga njëra-tjetra.

PĂ«r tĂ« lexuar fs/pipe.c (nĂ« Linux) ose sys/kern/sys_pipe.c (nĂ« *BSD), kĂ«rkohet angazhim i vĂ«rtetĂ«. Sot, nĂ« kod rendimenti dhe mbĂ«shtetje pĂ«r funksione si operacione tĂ« input-it dhe output-it vektorial dhe asinkron janĂ« tĂ« rĂ«ndĂ«sishme. NdĂ«rsa detajet e alokimit tĂ« memorie, bllokimeve dhe konfigurimit tĂ« bĂ«rthamĂ«s—gjithçka ka shumĂ« dallime. Kjo s’ështĂ« ajo qĂ« duan universitetet pĂ«r njĂ« kurs hyrĂ«s pĂ«r sistemet operative.

NĂ« çdo rast, mĂ« ka interesuar tĂ« gjej disa modele tĂ« vjetra (pĂ«r shembull, gjenerimin SIGPIPE dhe kthimin EPIPE kur shkruhet nĂ« njĂ« tubacion tĂ« mbyllur) nĂ« tĂ« gjitha kĂ«to bĂ«rthama moderne, kaq ndryshe. Ndoshta kurrĂ« nuk do ta shoh njĂ« kompjuter PDP-11 nĂ« jetĂ«, por ka akoma shumĂ« pĂ«r t’u mĂ«suar nga kodi, i cili u shkrua disa vjet para lindjes time.

Artikulli i shkruar nga Divi Kapoor në vitin 2011 "Implementimi i Tubacioneve dhe FIFOs në Bërthamën Linux" përbën një përmbledhje se si funksionojnë (ende) tubacionet në Linux. Një ndihmës i fundit në Linux ilustron modelin e tubacioneve, i cili tejkalon mundësitë e skedarëve përkohësorë; dhe tregon se sa larg kanë shkuar tubacionet nga "bllokimi shumë konservator" në bërthamën e Unix-it të edicionit të gjashtë.

Burimi: habr.com

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