Si zbatohen tubacionet në Unix

Si zbatohen tubacionet në Unix
Ky artikull përshkruan zbatimin e tubacioneve në kernelin Unix. Isha disi i zhgënjyer që një artikull i fundit me titull "Si funksionojnë tubacionet në Unix?" doli jo në lidhje me strukturën e brendshme. U bëra kurioz dhe gërmova në burime të vjetra për të gjetur përgjigjen.

Për çfarë po flasim?

Tubacionet, "ndoshta shpikja më e rëndësishme në Unix", janë një karakteristikë përcaktuese e filozofisë themelore të Unix-it të lidhjes së programeve të vogla së bashku, si dhe një shenjë e njohur në vijën e komandës:

$ echo hello | wc -c
6

Ky funksionalitet varet nga thirrja e sistemit të ofruar nga kerneli pipee cila përshkruhet në faqet e dokumentacionit tub (7) и tub (2):

Tubacionet ofrojnë një kanal të njëanshëm për komunikimin ndërprocesor. Tubacioni ka një hyrje (fundi i shkrimit) dhe një dalje (fundi i leximit). Të dhënat e shkruara në hyrjen e tubacionit mund të lexohen në dalje.

Tubacioni krijohet duke përdorur thirrjen pipe(2), i cili kthen dy përshkrues skedarësh: një që i referohet hyrjes së tubacionit, i dyti në dalje.

Dalja e gjurmës nga komanda e mësipërme tregon krijimin e tubacionit 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

Thirrjet e procesit prind pipe()për të marrë përshkruesit e skedarëve të montuar. Një proces fëmijësh shkruan në një dorezë dhe një proces tjetër lexon të njëjtat të dhëna nga një dorezë tjetër. Shell përdor dup2 për të "riemërtuar" përshkruesit 3 dhe 4 për të përputhur stdin dhe stdout.

Pa tuba, guaska do të duhej të shkruante daljen e një procesi në një skedar dhe ta kalonte atë në një proces tjetër për të lexuar të dhënat nga skedari. Si rezultat, ne do të humbnim më shumë burime dhe hapësirë ​​​​në disk. Sidoqoftë, tubacionet janë të mira jo vetëm sepse ju lejojnë të shmangni përdorimin e skedarëve të përkohshëm:

Nëse një proces po përpiqet të lexojë nga një tubacion bosh atëherë read(2) do të bllokojë derisa të dhënat të bëhen të disponueshme. Nëse një proces përpiqet të shkruajë në një tubacion të plotë, atëherë write(2) do të bllokojë derisa të lexohen të dhëna të mjaftueshme nga tubacioni për të kryer shkrimin.

Ashtu si kërkesa POSIX, kjo është një veti e rëndësishme: shkrimi në tubacion deri në PIPE_BUF bajt (të paktën 512) duhet të jenë atomike në mënyrë që proceset të mund të komunikojnë me njëri-tjetrin përmes tubacionit në një mënyrë që skedarët e rregullt (të cilët nuk ofrojnë garanci të tilla) nuk munden.

Kur përdorni një skedar të rregullt, një proces mund të shkruajë të gjithë daljen e tij në të dhe ta kalojë atë në një proces tjetër. Ose proceset mund të funksionojnë në një mënyrë shumë paralele, duke përdorur një mekanizëm sinjalizimi të jashtëm (si një semafor) për të njoftuar njëri-tjetrin kur një shkrim ose lexim ka përfunduar. Transportuesit na shpëtojnë nga gjithë kjo sherr.

Çfarë po kërkojmë?

Unë do ta shpjegoj me fjalë të thjeshta në mënyrë që të jetë më e lehtë për ju të imagjinoni se si mund të funksionojë një transportues. Do t'ju duhet të ndani një tampon dhe një gjendje në memorie. Do t'ju duhen funksione për të shtuar dhe hequr të dhënat nga buferi. Do t'ju duhen disa mjete për të thirrur funksione gjatë operacioneve të leximit dhe shkrimit në përshkruesit e skedarëve. Dhe do t'ju duhen bravë për të zbatuar sjelljen e veçantë të përshkruar më sipër.

Tani jemi gati të marrim në pyetje kodin burimor të kernelit nën dritën e ndezur të llambës për të konfirmuar ose hedhur poshtë modelin tonë të paqartë mendor. Por gjithmonë jini të përgatitur për të papriturat.

Ku po kërkojmë?

Nuk e di ku është kopja ime e librit të famshëm "Libri i luanëve"Me kodin burimor Unix 6, por falë Shoqëria e Trashëgimisë Unix mund të kërkoni në internet në Kodi i burimit edhe versionet më të vjetra të Unix.

Të endesh nëpër arkivat e TUHS është si të vizitosh një muze. Ne mund të shikojmë historinë tonë të përbashkët dhe kam respekt për përpjekjet shumëvjeçare për të rikuperuar të gjithë këtë material pak nga pak nga kasetat dhe printimet e vjetra. Dhe unë jam i vetëdijshëm për ato fragmente që ende mungojnë.

Pasi të kemi kënaqur kureshtjen tonë për historinë e lashtë të transportuesve, mund të shikojmë bërthamat moderne për krahasim.

Rastësisht, pipe është numri i thirrjes së sistemit 42 në tabelë sysent[]. Rastësi?

Kernelet tradicionale të Unix-it (1970–1974)

Nuk gjeta 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 jo të plotë edicioni i dyte (qershor 1972).

TUHS deklaron se edicioni i tretë i Unix (shkurt 1973) u bë versioni i parë me transportues:

Unix 1973rd Edition ishte versioni i fundit me një kernel të shkruar në gjuhën e asamblesë, por edhe versioni i parë me tubacione. Gjatë vitit XNUMX, u punua për të përmirësuar botimin e tretë, kerneli u rishkrua në C dhe kështu u shfaq botimi i katërt i Unix.

Një lexues gjeti një skanim të një dokumenti në të cilin Doug McIlroy propozoi idenë e "lidhjes së programeve si një zorrë kopshti".

Si zbatohen tubacionet në Unix
Në librin e Brian KernighanUnix: Një histori dhe një kujtim", në historinë e shfaqjes së transportuesve, përmendet edhe ky dokument: "... ai u var në mur në zyrën time në Bell Labs për 30 vjet." Këtu intervistë me McIlroy, dhe një histori tjetër nga Vepra e McIlroy, e shkruar në 2014:

Kur doli Unix, magjepsja ime me korutinat më shtyu t'i kërkoja autorit të OS, Ken Thompson, të lejonte që të dhënat e shkruara në një proces të shkonin jo vetëm në pajisje, por edhe të dilnin në një proces tjetër. Ken vendosi që ishte e mundur. Megjithatë, si një minimalist, ai donte që çdo funksion i sistemit të luante një rol të rëndësishëm. A është me të vërtetë një avantazh i madh shkrimi drejtpërdrejt ndërmjet proceseve ndaj shkrimit në një skedar të ndërmjetëm? Vetëm kur bëra një propozim specifik me emrin tërheqës "pipeline" dhe një përshkrim të sintaksës për ndërveprimin midis proceseve, Keni më në fund thirri: "Unë do ta bëj!"

Dhe bëri. Një mbrëmje fatale, Ken ndryshoi kernelin dhe guaskën, rregulloi disa programe standarde për të standardizuar mënyrën se si ata pranonin hyrjen (që mund të vinte nga një tubacion), dhe gjithashtu ndryshoi emrat e skedarëve. Të nesërmen, tubacionet filluan të përdoren shumë gjerësisht në aplikime. Në fund të javës, sekretarët po i përdornin për të dërguar dokumente nga përpunuesit e tekstit në printer. Pak më vonë, Ken zëvendësoi API-në dhe sintaksën origjinale për mbështjelljen e përdorimit të tubacioneve me konventa më të pastra, të cilat janë përdorur që atëherë.

Fatkeqësisht, kodi burimor për kernelin e edicionit të tretë Unix ka humbur. Dhe megjithëse ne kemi kodin burimor të kernelit të shkruar në C edicioni i katërt, lëshuar në nëntor 1973, por doli disa muaj përpara publikimit zyrtar dhe nuk përmban zbatime të tubacionit. Është turp që kodi burimor për këtë funksion legjendar Unix humbet, ndoshta përgjithmonë.

Ne kemi dokumentacion teksti për pipe(2) nga të dy versionet, kështu që mund të filloni duke kërkuar dokumentacionin botimi i tretë (për disa fjalë, të nënvizuara "me dorë", një varg fjalësh fjalë për fjalë ^H, e ndjekur nga një nënvizim!). Ky proto-pipe(2) është shkruar në gjuhën e asamblesë dhe kthen vetëm një përshkrues skedari, por tashmë ofron funksionalitetin bazë të pritur:

Thirrja e sistemit tub krijon një mekanizëm hyrje/dalje të quajtur tubacion. Përshkruesi i skedarit të kthyer mund të përdoret për operacionet e leximit dhe shkrimit. Kur diçka shkruhet në tubacion, deri në 504 bajt të dhëna fshihen, pas së cilës procesi i shkrimit pezullohet. Kur lexoni nga tubacioni, të dhënat e buferuara hiqen.

Në vitin e ardhshëm kerneli ishte rishkruar në C, dhe tub (2) në edicionin e katërt fitoi pamjen e saj moderne me prototipin "pipe(fildes)'

Thirrja e sistemit tub krijon një mekanizëm hyrje/dalje të quajtur tubacion. Përshkruesit e skedarëve të kthyer mund të përdoren në operacionet e leximit dhe shkrimit. Kur diçka shkruhet në tubacion, doreza e kthyer në r1 (përkatësisht fildes[1]) përdoret, e ruajtur në 4096 bajtë të dhëna, pas së cilës procesi i shkrimit pezullohet. Kur lexohet nga tubacioni, doreza e kthyer në r0 (përkatësisht fildes[0]) merr të dhënat.

Supozohet se pasi të përcaktohet një tubacion, dy (ose më shumë) procese komunikimi (të krijuara nga thirrjet e mëvonshme në pirun) do të transferojë të dhëna nga tubacioni duke përdorur thirrje lexoj и shkruaj.

Predha ka një sintaksë për përcaktimin e një grupi linear procesesh të lidhura nga një tubacion.

Thirrjet për të lexuar nga një tubacion bosh (që nuk përmban të dhëna të buferuara) që ka vetëm një fund (të gjithë përshkruesit e skedarëve të shkrimit janë të mbyllur) kthejnë "fundin e skedarit". Thirrjet për të shkruar në një situatë të ngjashme shpërfillen.

Më e hershme zbatimi i ruajtur i tubacionit vlen në edicionin e pestë të Unix (qershor 1974), por është pothuajse identik me atë që u shfaq në botimin tjetër. Komentet sapo janë shtuar, kështu që mund të kapërceni edicionin e pestë.

Edicioni i gjashtë i Unix (1975)

Le të fillojmë të lexojmë kodin burimor të Unix edicioni i gjashtë (maj 1975). Kryesisht falë Kuriozitete është shumë më e lehtë për t'u gjetur sesa burimet e versioneve të mëparshme:

Për shumë vite libri Kuriozitete ishte dokumenti i vetëm në kernelin Unix i disponueshëm jashtë Bell Labs. Ndonëse licenca e botimit të gjashtë i lejonte mësuesit të përdornin kodin e saj burimor, licenca e botimit të shtatë e përjashtoi këtë mundësi, kështu që libri u shpërnda në formën e kopjeve të paligjshme të shkruara me makinë shkrimi.

Sot mund të blini një ribotim të librit, kopertina e të cilit tregon studentët në një makinë kopjimi. Dhe falë Warren Toomey (i cili filloi projektin TUHS) ju mund të shkarkoni Skedari PDF me kodin burimor për edicionin e gjashtë. Unë dua t'ju jap një ide se sa përpjekje është bërë për krijimin e skedarit:

Më shumë se 15 vjet më parë, shtypa një kopje të kodit burimor të dhënë Kuriozitete, sepse nuk më pëlqeu cilësia e kopjes sime nga një numër i panjohur kopjesh të tjera. TUHS nuk ekzistonte ende dhe unë nuk kisha akses në burimet e vjetra. Por në vitin 1988, gjeta një kasetë të vjetër me 9 këngë që përmbante një kopje rezervë nga një kompjuter PDP11. Ishte e vështirë të thuash nëse po funksiononte, por kishte një pemë të paprekur /usr/src/ në të cilën shumica e skedarëve ishin etiketuar me vitin 1979, i cili edhe atëherë dukej i lashtë. Ishte botimi i shtatë ose derivati ​​i tij PWB, siç besoja.

Mora gjetjen si bazë dhe redaktova manualisht burimet në botimin e gjashtë. Një pjesë e kodit mbeti e njëjtë, por disa duhej të modifikoheshin pak, duke ndryshuar shenjën moderne += në =+ të vjetëruar. Disa gjëra thjesht u fshinë, dhe disa duhej të rishkruheshin plotësisht, por jo shumë.

Dhe sot ne mund të lexojmë në internet në TUHS kodin burimor të edicionit të gjashtë nga arkiv, në të cilin kishte dorë Dennis Ritchie.

Nga rruga, në shikim të parë, tipari kryesor i kodit C para periudhës së Kernighan dhe Ritchie është ai shkurtësia. Nuk ndodh shpesh që unë të fus pjesë të kodit pa redaktim të gjerë për të përshtatur një zonë relativisht të ngushtë të shfaqjes në faqen time.

Në fillim /usr/sys/ken/pipe.c ka një koment shpjegues (dhe po, ka më shumë /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 ka ndryshuar që nga edicioni i katërt. Por këtu shohim, pa asnjë dokumentacion publik, se tubacionet dikur përdorën skedarë si ruajtje rezervë!

Sa i përket skedarëve LARG, ato korrespondojnë me flamuri inode LARG, i cili përdoret nga "algoritmi i madh i adresimit" për të përpunuar blloqe indirekte për të mbështetur sisteme skedarësh më të mëdhenj. Meqenëse Ken tha se është më mirë të mos i përdorësh, unë do ta pranoj fjalën e tij me kënaqësi.

Këtu është thirrja e vërtetë e sistemit 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;
}

Komenti përshkruan qartë se çfarë po ndodh këtu. Por të kuptuarit e kodit nuk është aq e lehtë, pjesërisht për shkak të mënyrës "struct user u» dhe regjistron R0 и R1 kalohen parametrat e thirrjes së sistemit dhe vlerat e kthimit.

Le të provojmë me ialloc () vënë në disk inode (doreza e indeksit), dhe me ndihmën falloc () - vendosni dy në kujtesë dosje. Nëse gjithçka shkon mirë, ne do të vendosim flamuj për t'i identifikuar këta skedarë si dy skajet e tubacionit, do t'i drejtojmë në të njëjtën inode (numri i referencës së të cilit do të vendoset në 2) dhe do ta shënojmë inodën si të modifikuar dhe në përdorim. Kushtojini vëmendje kërkesave për iput () në shtigjet e gabimeve për të reduktuar numrin e referencës në inodën e re.

pipe() duhet të kalojë R0 и R1 ktheni numrat e përshkruesit të skedarëve për lexim dhe shkrim. falloc() kthen një tregues në strukturën e skedarit, por gjithashtu "kthehet" nëpërmjet u.u_ar0[R0] dhe një përshkrues skedari. Kjo do të thotë, kodi ruhet brenda r përshkrues skedari për lexim dhe cakton një përshkrues skedari për shkrim direkt nga u.u_ar0[R0] pas thirrjes së dytë falloc().

flamur FPIPE, të cilin e vendosim gjatë krijimit të tubacionit, kontrollon sjelljen e funksionit rdwr() në sys2.cduke thirrur rutina specifike 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() в pipe.c lexon të dhënat nga tubacioni. Por është më mirë të gjurmoni zbatimin duke filluar nga writep(). Përsëri, kodi është bërë më kompleks për shkak të konventave të kalimit të argumenteve, por disa detaje mund të anashkalohen.

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

Ne duam të shkruajmë bajt në hyrjen e tubacionit u.u_count. Së pari duhet të bllokojmë inodën (shih më poshtë plock/prele).

Pastaj kontrollojmë numëruesin e referencës inode. Për sa kohë që të dy skajet e tubacionit mbeten të hapura, numëruesi duhet të jetë i barabartë me 2. Ne mbajmë një lidhje (nga rp->f_inode), kështu që nëse numëruesi është më i vogël se 2, duhet të nënkuptojë se procesi i leximit ka mbyllur fundin e tij të tubacionit. Me fjalë të tjera, ne po përpiqemi të shkruajmë në një tubacion të mbyllur, dhe ky është një gabim. Kodi i gabimit për herë të parë EPIPE dhe sinjal SIGPIPE u shfaq në edicionin e gjashtë të Unix.

Por edhe nëse transportuesi është i hapur, ai mund të jetë plot. Në këtë rast, ne e lëshojmë bllokimin dhe shkojmë të flemë me shpresën se një proces tjetër do të lexojë nga tubacioni dhe do të lirojë hapësirë ​​të mjaftueshme në të. Pasi jemi zgjuar, kthehemi në fillim, mbyllim përsëri bllokimin dhe fillojmë një cikël të ri regjistrimi.

Nëse ka hapësirë ​​të mjaftueshme të lirë në tubacion, atëherë ne shkruajmë të dhëna në të duke përdorur shkrimi ()... Parametri i_size1 inode (nëse tubacioni është bosh mund të jetë i barabartë me 0) tregon fundin e të dhënave që tashmë përmban. Nëse ka hapësirë ​​të mjaftueshme regjistrimi, ne mund ta mbushim tubacionin nga i_size1 tek PIPESIZ. Më pas e lëshojmë bllokimin dhe përpiqemi të zgjojmë çdo proces që pret të lexojë nga tubacioni. Kthehemi në fillim për të parë nëse ishim në gjendje të shkruanim aq bajt sa duheshin. Nëse dështon, atëherë fillojmë një cikël të ri regjistrimi.

Zakonisht parametri i_mode inode përdoret për të ruajtur lejet r, w и x. Por në rastin e tubacioneve, ne sinjalizojmë se një proces është duke pritur për një shkrim ose lexim duke përdorur bit IREAD и IWRITE përkatësisht. Procesi vendos flamurin dhe telefonon sleep(), dhe pritet që në të ardhmen të shkaktojë ndonjë proces tjetër wakeup().

Magjia e vërtetë ndodh në sleep() и wakeup(). Ato zbatohen në slp.c, burimi i komentit të famshëm “Nuk pritet ta kuptoni këtë”. Për fat të mirë, ne nuk duhet ta kuptojmë kodin, thjesht shikoni 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ë shkakton sleep() për një kanal të caktuar, më vonë mund të zgjohet nga një proces tjetër, i cili do të shkaktojë wakeup() për të njëjtin kanal. writep() и readp() koordinojnë veprimet e tyre përmes thirrjeve të tilla të çiftëzuara. vini re se pipe.c gjithmonë jep përparësi PPIPE kur thirret sleep(), pra kaq sleep() mund të ndërpritet nga një sinjal.

Tani kemi gjithçka për të kuptuar funksionin 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);
}

Mund ta keni më të lehtë ta lexoni këtë funksion nga poshtë lart. Dega "lexo dhe kthe" zakonisht përdoret kur ka disa të dhëna në linjë. Në këtë rast, ne përdorim Readi () ne lexojmë aq të dhëna sa janë në dispozicion duke filluar nga ajo aktuale f_offset duke lexuar dhe më pas përditësoni vlerën e kompensimit përkatës.

Në leximet e mëvonshme, tubacioni do të jetë bosh nëse kompensimi i leximit ka arritur i_size1 në inode. Ne rivendosim pozicionin në 0 dhe përpiqemi të zgjojmë çdo proces që dëshiron të shkruajë në tubacion. Ne e dimë se kur transportuesi është plot, writep() do të bie në gjumë ip+1. Dhe tani që tubacioni është bosh, ne mund ta zgjojmë atë për të rifilluar ciklin e tij të shkrimit.

Nëse nuk keni asgjë për të lexuar, atëherë readp() mund të vendosë një flamur IREAD dhe bie në gjumë ip+2. Ne e dimë se çfarë do ta zgjojë atë writep(), kur shkruan disa të dhëna në tubacion.

Komentet për readi() dhe writei() do t'ju ndihmojë të kuptoni se në vend që të kaloni parametrat nëpërmjet "u"Ne mund t'i trajtojmë ato si funksione normale I/O që marrin një skedar, një pozicion, një bufer në memorie dhe numërojnë numrin e bajteve 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", atëherë readp() и writep() bllokoni inodin derisa të mbarojnë punën e tyre ose të marrin një rezultat (d.m.th., telefononi wakeup). plock() и prele() punoni thjesht: duke përdorur një grup të ndryshëm thirrjesh sleep и wakeup na lejoni të zgjojmë çdo proces që ka nevojë për bllokimin që sapo lëshuam:

/*
 * 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 shkakton prele(ip) para thirrjes wakeup(ip+1). Gjëja e parë është writep() shkakton në ciklin e saj, këtë plock(ip), e cila çon në bllokim nëse readp() nuk e kam hequr ende bllokun tim, kështu që kodi duhet të funksionojë si duhet. Nëse shikoni wakeup(), atëherë bëhet e qartë se vetëm shënon procesin e gjumit si gati për t'u ekzekutuar, në mënyrë që në të ardhmen sched() me të vërtetë e nisi atë. Kështu që readp() shkaqet wakeup(), heq bllokimin, vendos IREAD dhe telefonatat sleep(ip+2)- e gjithë kjo më parë writep() rifillon ciklin.

Kjo plotëson përshkrimin e transportuesve në edicionin e gjashtë. Kod i thjeshtë, pasoja të gjera.

Edicioni i shtatë i Unix (Janar 1979) ishte një version i ri i madh (katër vjet më vonë) që prezantoi shumë aplikacione të reja dhe veçori të kernelit. Ai gjithashtu pësoi ndryshime të rëndësishme në lidhje me përdorimin e derdhjes së tipit, bashkimeve dhe treguesve të shtypur në struktura. Megjithatë kodi transportues praktikisht i pandryshuar. Mund ta anashkalojmë këtë edicion.

Xv6, një kernel i thjeshtë si Unix

Për të krijuar kernelin Xv6 ndikuar nga edicioni i gjashtë i Unix-it, por është shkruar në C moderne për të ekzekutuar në procesorë x86. Kodi është i lehtë për t'u lexuar dhe i kuptueshëm. Plus, ndryshe nga burimet Unix me TUHS, mund ta përpiloni, modifikoni dhe ekzekutoni në diçka tjetër përveç një PDP 11/70. Prandaj, ky kernel përdoret gjerësisht në universitete si material arsimor mbi sistemet operative. Burimet janë në Github.

Kodi përmban një zbatim të qartë dhe të menduar tub.c, i mbështetur nga një buffer në memorie në vend të një inode në disk. Këtu jap vetëm përkufizimin e "tubacionit 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() përcakton gjendjen e pjesës tjetër të zbatimit, i cili përfshin funksionet piperead(), pipewrite() и pipeclose(). Thirrja aktuale e sistemit sys_pipe është një mbështjellës i implementuar në sysfile.c. Unë rekomandoj të lexoni të gjithë kodin e tij. Kompleksiteti është në nivelin e kodit burimor të botimit të gjashtë, por është shumë më e lehtë dhe më e këndshme për t'u lexuar.

Linux 0.01

Kodi burimor Linux 0.01 mund të gjendet. Do të jetë udhëzuese për të studiuar zbatimin e tubacioneve në të fs/pipe.c. Kjo përdor një inode për të përfaqësuar tubacionin, por vetë tubacioni është i shkruar në C moderne. Nëse e keni përdorur kodin e edicionit të 6-të, nuk do të keni probleme këtu. 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) { /* 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;
}

Pa shikuar as përkufizimet e strukturës, mund të kuptoni se si përdoret numërimi i referencës inode për të kontrolluar nëse një operacion shkrimi rezulton në SIGPIPE. Përveç punës byte-pas-byte, ky funksion është i lehtë për t'u krahasuar me idetë e përshkruara më sipër. Edhe logjika sleep_on/wake_up nuk duket aq i huaj.

Kernelet moderne të Linux, FreeBSD, NetBSD, OpenBSD

Unë shpejt kalova nëpër disa bërthama moderne. Asnjë prej tyre nuk ka më një implementim të diskut (nuk është për t'u habitur). Linux ka implementimin e vet. Edhe pse tre kernelet moderne BSD përmbajnë zbatime të bazuara në kodin e shkruar nga John Dyson, me kalimin e viteve ato janë bërë shumë të ndryshme nga njëri-tjetri.

Te lexosh fs/pipe.c (në Linux) ose sys/kern/sys_pipe.c (në *BSD), kërkon përkushtim të vërtetë. Kodi i sotëm ka të bëjë me performancën dhe mbështetjen për veçori të tilla si I/O vektoriale dhe asinkrone. Dhe detajet e alokimit të memories, kyçet dhe konfigurimi i kernelit ndryshojnë shumë. Kjo nuk është ajo që u nevojitet kolegjeve për një kurs hyrës të sistemeve operative.

Gjithsesi, isha i interesuar të gërmoja disa modele të vjetra (si gjenerimi SIGPIPE dhe kthehu EPIPE kur shkruani në një tubacion të mbyllur) në të gjitha këto kernele të ndryshme moderne. Ndoshta nuk do të shoh kurrë një kompjuter PDP-11 në jetën reale, por ka ende shumë për të mësuar nga kodi që është shkruar vite përpara se të lindja.

Një artikull i shkruar nga Divi Kapoor në 2011:Zbatimi i kernel Linux i tubave dhe FIFO-ve" ofron një përmbledhje të mënyrës se si funksionojnë tubacionet (ende) në Linux. A kryerja e fundit në Linux ilustron një model ndërveprimi të linjës, aftësitë e të cilit i kalojnë ato të skedarëve të përkohshëm; dhe gjithashtu tregon se sa larg kanë shkuar tubacionet nga "mbyllja shumë konservatore" e kernelit të edicionit të gjashtë të Unix.

Burimi: www.habr.com

Shto një koment