Kako su realizirane cijevi u Unixu

Kako su realizirane cijevi u Unixu
Ovaj članak opisuje implementaciju cijevi u Unix jezgru. Bio sam pomalo razočaran što je nedavni članak pod nazivom "Kako cijevi funkcionišu u Unixu?" bio ne o unutarnjoj strukturi. Zaintrigirao me je i potražio sam odgovore u starim izvorima.

O čemu se radi?

Cijevi – "vjerojatno najvažniji izum u Unixu" – su karakteristična osobina osnovne Unix filozofije integracije malih programa, kao i poznati natpis u komandnoj liniji:

$ echo hello | wc -c
6

Ova funkcionalnost zavisi od sistemskog poziva koji pruža jezgro pipe, koji je opisan na stranicama dokumentacije pipe(7) i pipe(2):

Cijevi omogućuju jednosmjernu komunikaciju između procesa. Cijev ima ulaz (write end) i izlaz (read end). Podaci zapisani u ulaz cijevi mogu se čitati na izlazu.

Cijev se stvara pozivom pipe(2), koji vraća dva datotemska deskriptora: jedan se odnosi na ulaz cijevi, drugi na izlaz.

Rezultati praćenja gornje komande pokazuju stvaranje cijevi i tok podataka kroz nju iz jednog procesa u drugi:

$ 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

Roditeljski proces poziva pipe(), kako bi dobio povezane datotemske deskriptore. Jedan podređeni proces zapisuje u jedan deskriptor, a drugi proces čita iste podatke iz drugog deskriptora. Shell pomoću dup2 "preimenuje" deskriptore 3 i 4 kako bi odgovarali stdin i stdout.

Bez cijevi, shell bi morao zapisivati rezultate jednog procesa u datoteku i prenositi ih drugom procesu kako bi mogao pročitati podatke iz datoteke. U tom slučaju trošili bismo više resursa i prostora na disku. Međutim, cijevi su korisne ne samo zato što izbjegavaju korištenje privremenih datoteka:

Ako proces pokuša pročitati iz prazne cijevi, tada read(2) će blokirati dok podaci ne postanu dostupni. Ako proces pokuša zapisati u punu cijev, tada write(2) blokirat će dok se iz cjevovoda ne pročita dovoljno podataka za izvršenje zapisa.

Kao što zahtijeva POSIX, ovo je važna osobina: zapis u cjevovod do PIPE_BUF bajta (barem 512) mora biti atomski, kako bi procesi mogli međusobno komunicirati kroz cjevovod na način na koji obične datoteke (koje ne pružaju takve garancije) ne mogu.

Kada koristimo običnu datoteku, proces može zapisati sve svoje izlazne podatke i proslijediti ih drugom procesu. Ili procesi mogu raditi u režimu grubog paralelizma, koristeći vanjski signalni mehanizam (poput semafora) kako bi jedni drugima javili završetak zapisivanja ili čitanja. Cjevovodi nas oslobađaju svih tih gnjavaža.

Što tražimo?

Objasnit ću slikovito, kako biste lakše zamislili kako cjevovod može raditi. Trebat će vam da alocirate u memoriji bafer i neko stanje. Trebat će vam funkcije za dodavanje i uklanjanje podataka iz bafera. Trebat će vam neki mehanizam za pozivanje funkcija tijekom operacija čitanja i pisanja u datotečne deskriptore. I trebaju vam zaključavanja kako biste implementirali opisano posebno ponašanje.

Sada smo spremni ispitati izvorni kod jezgre pod jakim svjetlom kako bismo potvrdili ili opovrgli našu nejasnu mentalnu sliku. No, uvijek budite spremni na iznenađenja.

Gdje tražimo?

Ne znam gdje se nalazi moj primjerak poznate knjige „Lions book„ s izvorni kodom Unix 6, ali zahvaljujući The Unix Heritage Society može se online potražiti u izvornom kodu još starijih verzija Unixa.

Lutanje arhivima TUHS sliči posjetu muzeju. Možemo zaviriti u našu zajedničku povijest, a ja osjećam poštovanje prema dugogodišnjim naporima na obnovi svih ovih materijala, bit po bit, sa starih kaseta i isprintanih dokumenata. I oštro sam svjestan onih fragmenata koji još nedostaju.

Zadovoljavajući svoju znatiželju u dijelu drevne povijesti cjevovoda, za usporedbu možemo pogledati moderne jezgre.

Usput, pipe je sistemski poziv broj 42 u tablici sysent[]. Podudarnost?

Tradicionalne jezgre Unixa (1970–1974)

Nisam našao nikakve tragove pipe(2) ni u PDP-7 Unix (siječanj 1970.), ni u prvom izdanju Unixa (studeni 1971.), ni u nepotpunom izvoru drugog izdanja (lipanj 1972.).

TUHS tvrdi da treće izdanje Unixa (veljača 1973.) bila je prva verzija s cijevima:

Treća verzija Unixa bila je posljednja verzija s kernelom napisanom u asembleru, ali također prva verzija s cijevima. Tijekom 1973. godine radilo se na poboljšanju treće verzije, kernel je prepisan u jeziku C, i tako se pojavila četvrta verzija Unixa.

Jedan od čitatelja pronašao je sken dokumenta u kojem je Doug McIlroy predložio ideju 'spajanja programa prema principu vrtnog crijeva'.

Kako su realizirane cijevi u Unixu
U knjizi Briana Kernighana 'Unix: Povijest i memoari', u povijesti nastanka cijevi također se spominje ovaj dokument: '… visio je na zidu u mom uredu u Bell Labsu punih 30 godina'. Evo intervjua s McIlroyem, i još jedna priča iz rada McIlroya, napisana 2014.:

Kada je Unix nastao, moja fascinacija korutinama navela me da zatražim od autora OS-a, Kena Thompsona, da dopusti podacima zabilježenim u nekom procesu da idu ne samo na uređaj, već i na izlaz u drugi proces. Ken je odlučio da je to moguće. Međutim, kao minimalist, želio je da svaka funkcija sustava igra značajnu ulogu. Ima li doista izravno pisanje između procesa veliku prednost u usporedbi s pisanjem u privremenu datoteku? I tek kada sam iznio konkretan prijedlog s privlačnim nazivom 'cijev' i opisom sintakse međusobnog djelovanja procesa, Ken je konačno povikao: 'Učinit ću to!'.

I učinio je. Jedne sudbonosne večeri Ken je promijenio kernel i ljusku, ispravio nekoliko standardnih programa, standardizirajući njihove procedure prihvaćanja ulaznih podataka (koji mogu dolaziti iz cijevi), kao i promijenio imena datoteka. Sljedećeg dana cijevi su počele biti vrlo široko korištene u aplikacijama. Do kraja tjedna tajnice su pomoću njih slale dokumente iz tekstualnih uređivača na pisač. Pomalo kasnije Ken je zamijenio izvorni API i sintaksu za korištenje cijevi u ljusci čistoćim dogovorima koji se od tada primjenjuju.

Nažalost, izvorni kod kernela treće verzije Unixa je izgubljen. Iako imamo izvorni kod kernela četvrte verzije, koja je objavljena u studenom 1973., ona je izašla nekoliko mjeseci prije službenog izdanja i ne sadrži implementaciju cijevi. Šteta što je izvorni kod legendarne funkcije Unixa izgubljen, možda zauvijek.

Imamo tekst dokumentacije o pipe(2) oba izdanja, pa se može početi s pretraživanjem u dokumentaciji treće verzije (prema određenim riječima, naglašenim 'ručno', linija iz literala ^H, nakon koje slijedi donja crta!). Ovaj proto-pipe(2) napisan je u asembleru i vraća samo jedan deskriptor datoteke, ali već pruža očekivanu osnovnu funkcionalnost:

Sistem poziv pipe stvara mehanizam ulaza i izlaza koji se zove cijev. Vraćeni deskriptor datoteke može se koristiti za operacije čitanja i pisanja. Kada se nešto zapisuje u cijev, podaci se buferiraju do 504 bajta, nakon čega se proces pisanja obustavlja. Pri čitanju iz cijevi, buferirani podaci se uzimaju.

Do sljedeće godine jezgra je prepisana u C, a pipe(2) u četvrtoj verziji dobila je svoj moderni izgled s prototipom 'pipe(fildes)»:

Sistem poziv pipe stvara mehanizam ulaza i izlaza koji se zove cijev. Vraćeni deskriptor datoteka može se koristiti za operacije čitanja i pisanja. Kada se nešto zapisuje u cijev, koristi se deskriptor koji se vraća u r1 (odnosno fildes[1]), koji se buferira do 4096 bajta podataka, nakon čega se proces pisanja obustavlja. Pri čitanju iz cijevi, deskriptor koji se vraća u r0 (odnosno fildes[0]), uzima podatke.

Pretpostavlja se da će nakon definicije cijevi dva (ili više) međusobno povezana procesa (stvorenih kasnijim pozivima fork) razmjenjivati podatke iz cijevi putem poziva read i write.

U ljusci postoji sintaksa za definiranje linearne mreže procesa, povezanih putem cijevi.

Pozivi za čitanje iz prazne cijevi (koja ne sadrži buferirane podatke), koja ima samo jedan kraj (svi deskriptori za pisanje su zatvoreni), vraćaju 'kraj datoteke'. Pozivi za pisanje u sličnoj situaciji se ignoriraju.

Najranija sačuvana implementacija cijevi odnosi se na petu verziju Unixa (jun 1974.), ali je gotovo identična onoj koja se pojavila u sljedećem izdanju. Samo su dodani komentari, tako da se peta verzija može preskočiti.

Šesta verzija Unixa (1975)

Počinjemo čitati izvorni kod Unixa šeste verzije (maj 1975.). U velikoj mjeri zahvaljujući Lions mnoge je lakše pronaći nego izvore ranijih verzija:

Dugo godina je knjiga Lions bila jedini dokument o jezgru Unixa dostupan izvan Bell Labs. Iako je licenca šeste verzije dopuštala profesorima korištenje njenog izvornog koda, licenca sedme verzije isključila je tu mogućnost, pa se knjiga distribuirala u obliku ilegalnih mašinopisnih kopija.

Danas možete kupiti reprint knjige čija naslovnica prikazuje studente kod kopira. Zahvaljujući Warrenu Tumu (koji je pokrenuo projekt TUHS) možete preuzeti PDF datoteku s izvorni koda šeste verzije. Želim vam dati dojam koliko je truda uloženo u izradu datoteke:

Prije više od 15 godina otkucao sam kopiju izvornog koda prikazanog u Lions, jer se kvalitetom moje kopije s nepoznatog broja drugih kopija nisam bio zadovoljan. TUHS tada još nije postojala, a nisam imao pristup starim izvorima. No, 1988. godine pronašao sam staru traku s 9 tragova koja je sadržavala sigurnosnu kopiju s računala PDP11. Bilo je teško odrediti radi li se, no unutra je bilo neporuženo stablo /usr/src/, u kojem su većina datoteka bile označene godinom 1979, što je tada već izgledalo kao antikvitet. To je bila sedma verzija ili njezina izvedenica PWB, kako sam mislio.

Uzeo sam taj pronalazak kao osnovu i ručno uredio izvore do stanja šeste verzije. Dio koda ostao je isti, dio je trebalo malo doraditi, zamjenjujući moderni token += s dotrajalim =+. Neki su se jednostavno izbrisali, a neki su se morali potpuno prepisati, ali ne previše.

I danas možemo online čitati na TUHS izvorni kod šeste verzije iz archiva kojem je doprinio Dennis Ritchie.

Usput, na prvi pogled, glavna karakteristika C koda do doba Kernighana i Ritchieja je njegova konciznost. Ne događa se često da bez opsežnog uređivanja uspijevam umetnuti fragmente koda kako bi odgovarali relativno uskom području prikaza na mojoj web stranici.

Na početku /usr/sys/ken/pipe.c nalazi se objašnjeni komentar (i da, tu je još /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

Veličina bafera nije se mijenjala od četvrte verzije. Ali ovdje bez ikakve javne dokumentacije vidimo da su nekada trake koristile datoteke kao alternativno skladište!

Što se tiče LARG datoteka, one odgovaraju inode oznaci LARG, koji se koristi „algoritmom velikog adresiranja“ za obradu indirektnih blokova s ciljem podrške većim datotečnim sustavima. Budući da je Ken rekao da ih je bolje ne koristiti, rado ću mu povjerovati na riječ.

Evo pravog sistemskog poziva 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;
}

U komentaru je jasno opisano što se ovdje događa. No, razumjeti kod nije tako jednostavno, dijelom zbog načina na koji se putem „struct user u“ i registara R0 i R1 prosljeđuju parametri sistemskih poziva i vraćene vrijednosti.

Pokušajmo putem ialloc() smjestiti na disku inode (indeksni deskriptor), a putem falloc() — smjestiti u memoriju dva datoteke. Ako sve prođe dobro, postavit ćemo oznake za određivanje tih datoteka kao dva kraja cijevi, navesti ih u istom inodeu (čiji će broj referenci postati 2), i označiti inode kao izmijenjen i korišten. Obratite pažnju na pozive iput() u pogrešnim putanjama (error paths) za smanjenje broja referenci u novom inodeu.

pipe() treba preko R0 i R1 vratiti brojeve datotečnih destruktora za čitanje i pisanje. falloc() vraća pokazivač na datotečnu strukturu, ali također „vraća“ putem u.u_ar0[R0] i datotečni deskriptor. To jest, kod sprema u r datotečni deskriptor za čitanje i dodjeljuje deskriptor za pisanje izravno iz u.u_ar0[R0] nakon drugog poziva falloc().

Oznaka FPIPE, koju smo postavili prilikom stvaranja cijevi, upravlja ponašanjem funkcije rdwr() u sys2.c, koja poziva specifične podprogramske ulaze-izlaze 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);
    }
        /* … */
}

Zatim funkcija readp() u pipe.c čita podatke iz cijevi. No, najbolje je pratiti implementaciju započinjući s writep(). Ponovit ću, kod je postao složen zbog osobitosti dogovora o prosljeđivanju argumenata, ali neke pojedinosti se mogu izostaviti.

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

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

loop:
    /* Ako je sve gotovo, vratite se. */

    plock(ip);
    if(c == 0) {
        prele(ip);
        u.u_count = 0;
        return;
    }

    /*
     * Ako obje strane za čitanje i pisanje
     * cijevi nisu aktivne, vratiti grešku i signalizirati također.
     */

    if(ip->i_count < 2) {
        prele(ip);
        u.u_error = EPIPE;
        psignal(u.u_procp, SIGPIPE);
        return;
    }

    /*
     * Ako je cijev puna, čekajte da čitanja smanje
     * i obrežite je.
     */

    if(ip->i_size1 == PIPSIZ) {
        ip->i_mode =| IWRITE;
        prele(ip);
        sleep(ip+1, PPIPE);
        goto loop;
    }

    /* Zapišite što je moguće i vratite se na petlju. */

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

U ulazu cijevi želimo zapisati bajtove u.u_count. Prvo trebamo blokirati indeksni descriptor (vidi dolje plock/prele).

Zatim provjeravamo broj referenci inode-a. Dok su oba kraja cjevovoda otvorena, broj bi trebao biti 2. Držimo jedan referencu (od rp->f_inode) tako da ako broj bude manji od 2, to bi značilo da je proces za čitanje zatvorio svoj kraj cjevovoda. Drugim riječima, pokušavamo pisati u zatvoren cjevovod, što je greška. Prvi put se kod greške EPIPE i signal SIGPIPE pojavili su se u šestoj verziji Unixa.

Ali čak i ako je cjevovod otvoren, može biti pun. U tom slučaju otpuštamo blokadu i idemo na spavanje nadajući se da će drugi proces pročitati iz cjevovoda i osloboditi dovoljno mjesta. Kada se probudimo, vraćamo se na početak, ponovno stavljamo blokadu i započinjemo novi krug pisanja.

Ako u cjevovodu ima dovoljno slobodnog mjesta, zapisujemo podatke koristeći writei(). Parametar i_size1 inode-a (samo prazni cjevovod može biti 0) ukazuje na kraj podataka koji su već sadržani u njemu. Ako ima dovoljno prostora za pisanje, možemo napuniti cjevovod od i_size1 do PIPESIZ. Zatim uklanjamo blokadu i pokušavamo probuditi bilo koji proces koji čeka priliku da pročita iz cjevovoda. Vraćamo se na početak da vidimo jesmo li uspjeli zapisati onoliko bajtova koliko nam je bilo potrebno. Ako nismo uspjeli, započinjemo novi krug pisanja.

Obično se parametar i_mode inode-a koristi za pohranu dozvola r, w i x. No, u slučaju cjevovoda, signaliziramo čekanje nekim procesom za pisanje ili čitanje pomoću bitova IREAD i IWRITE odgovarajuće. Proces postavlja zastavicu i poziva sleep(), a očekuje se da će u budućnosti neki drugi proces pozvati wakeup().

Pravo čarobnjaštvo događa se u sleep() i wakeup(). Oni su implementirani u slp.c, izvoru poznate tvrdnje "Ne morate to razumjeti" (You are not expected to understand this). Srećom, ne moramo razumjeti kod, samo ćemo pogledati neke komentare:

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

Proces koji poziva sleep() za određeni kanal, može kasnije biti probuđen od strane drugog procesa koji poziva wakeup() za isti kanal. writep() i readp() koordiniraju svoje aktivnosti putem takvih parnih poziva. Primijetite da pipe.c uvijek daje prioritet PPIPE pri pozivu sleep(), tako da svi sleep() mogu biti prekinuti signalom.

Sada imamo sve da razjasnimo funkciju readp():

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

    rp = fp;
    ip = rp->f_inode;

loop:
    /* Vrlo konzervativno zaključavanje. */

    plock(ip);

    /*
     * Ako je glava (čitanje) uhvatila
     * rep (pisanje), resetirajmo oboje na 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);
            }
        }

        /*
         * Ako nisu aktivni i čitatelj i
         * pisac, vrati se bez
         * zadovoljavanja čitanja.
         * /

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

    /* Čitaj i vrati */

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

Možda će vam biti lakše čitati ovu funkciju odozdo prema gore. Grana "read and return" obično se koristi kada u procesu postoje neki podaci. U tom slučaju koristimo readi() čitamo koliko god podataka je dostupno počevši od trenutnog f_offset čitanja, a zatim ažuriramo vrednost odgovarajuće pomeranja.

Prilikom narednog čitanja proces će biti prazan ako je vrednost pomeranja čitanja dostigla vrednost i_size1 inode-a. Resetujemo poziciju na 0 i pokušavamo da probudimo bilo koji proces koji želi da piše u proces. Značimo da kada proces postane pun, writep() uspavaće se na ip+1. A sada kada je proces prazan, možemo ga probuditi da nastavi svoj ciklus pisanja.

Ako nema čitanja, može postaviti zastavicu readp() i uspavati se na IREAD ip+2 . Znamo da će ga probuditi kada ubaci podatke u proces.Komentari za writep()readi() i writei()

pomažu u razumevanju da umesto prenosa parametara putem " " možemo ih tretirati kao obične funkcije ulaza-izlaza koje uzimaju datoteku, poziciju, bafer u memoriji, i prebrojavaju broj bajtova za čitanje ili pisanje. Što se tiče "konzervativnog" zaključavanja,uzaključava inode dok ne završi rad ili ne dobije rezultat (tj. ne pozove

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

wakeup readp() i writep() plock() prele()). rade jednostavno: koristeći drugi set poziva i dozvoljavaju nam da budemo probudimo bilo koji proces kojem je potrebna blokada koju smo upravo oslobodili: Isprva nisam mogao da shvatim zašto sleep i prele() ne poziva

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

prele(ip) readp() pre poziva wakeup(ip+1) . Prva stvar koju poziva u svom ciklusu jeplock(ip) writep() , što dovodi do blokiranja ako još nije oslobodio svoju blokadu, tako da bi kod nekako trebao raditi ispravno. Ako pogledate na, который приводит к взаимоблокировке, если readp() ещё не снял свой блок, поэтому код каким-то образом должен работать правильно. Если посмотреть на wakeup(), onda postaje jasno da samo označava uspavani proces kao spreman za izvršenje, kako bi u budućnosti sched() doista ga pokrenula. Dakle readp() poziva wakeup(), uklanja blokadu, postavlja IREAD i poziva sleep(ip+2)— sve to pre writep() obnavlja ciklus.

Time se opis cijevi u šestoj ediciji završava. Jednostavan kod, dalekosežni učinci.

Sedma edicija Unix-a (januar 1979.) bila je novo glavno izdanje (nakon četiri godine), u kojem je uvedeno mnogo novih aplikacija i svojstava jezgre. Također, došlo je do značajnih promjena vezanih uz korištenje prilagodbe tipova, unija i tipiziranih pokazivača na strukture. Međutim kod cijevi praktički se nije promijenio. Možemo preskočiti ovo izdanje.

Xv6, jednostavno Unix-slična jezgra

Na izradu jezgre Xv6 utjecala je šesta edicija Unix-a, međutim pisana je na modernom C-u kako bi se mogla pokrenuti na x86 procesorima. Kod se lako čita, razumljiv je. Štoviše, za razliku od izvora Unix-a s TUHS, možete ga compilirati, modificirati i pokrenuti na nečemu osim PDP 11/70. Stoga se ta jezgra široko koristi na visokoškolskim ustanovama kao nastavna materija o operativnim sustavima. Izvori su dostupni na Github-u.

U kodu se nalazi razumljiva i promišljena implementacija pipe.c, potpomognuta baferom u memoriji umjesto inode-a na disku. Ovdje ću navesti samo definiciju "strukturne cijevi" i funkcije 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() postavlja stanje preostale implementacije, koja uključuje funkcije piperead(), pipewrite() i pipeclose(). Stvarni sistemski poziv sys_pipe je omotač, implementiran u sysfile.c. Preporučujem da pročitate cijeli njegov kod. Složenost na razini izvora šeste edicije, ali se puno lakše i ugodnije čita.

Linux 0.01

Izvorni kod Linux-a 0.01 može se pronaći. Bilo bi poučno proučiti implementaciju cijevi u njegovom fs/pipe.c. Ovdje se za predstavljanje cijevi koristi inode, ali sama cijev je napisana na modernom C-u. Ako ste prošli kroz kod šeste edicije, ovdje nećete imati poteškoća. Tako izgleda funkcija 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<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;
}

Čak i bez gledanja u definicije struktura, može se shvatiti kako se brojač referenci inode koristi za provjeru dovodi li operacija pisanja do SIGPIPE. Osim po bajtnoj obradi, ovu funkciju je lako usporediti s gore opisanim idejama. Čak i logika sleep_on/wake_up ne izgleda tako strano.

Moderna jezgra Linuxa, FreeBSD, NetBSD, OpenBSD

Brzo sam prošao kroz neka moderna jezgra. U nijednom od njih više nema implementacije koja koristi disk (nije ni čudo). Linux ima svoju vlastitu implementaciju. Iako tri moderna BSD jezgra sadrže implementacije temeljene na kodu koji je napisao John Dyson, tijekom godina su se previše razlikovala jedna od druge.

Da biste čitali fs/pipe.c (na Linuxu) ili sys/kern/sys_pipe.c (na *BSD), potrebna je prava predanost. Danas su u kodu važni izvedba i podrška za funkcije poput vektorskih i asinkronih operacija ulaza-izlaza. A detalji o dodjeli memorije, zaključavanjima i konfiguraciji jezgre — sve to se znatno razlikuje. To nije ono što bi obrazovne institucije trebale za uvodni tečaj o operativnim sustavima.

U svakom slučaju, bilo mi je zanimljivo istražiti nekoliko starih obrazaca (na primjer, generiranje SIGPIPE i vraćanje EPIPE pri pisanju u zatvoreni cijev) u svim ovim, tako različitim, modernim jezgrama. Vjerojatno nikad neću vidjeti računalo PDP-11 uživo, ali još uvijek ima mnogo za učiti iz koda koji je napisan nekoliko godina prije mog rođenja.

Članak "The Linux Kernel Implementation of Pipes and FIFOs" koji je napisao Divya Kapoor 2011. godine predstavlja pregled kako (još uvijek) cijevi funkcioniraju u Linuxu. A nedavna izmjena u Linuxu illustrates the pipeline interaction model, whose capabilities exceed those of temporary files; and shows how far pipelines have evolved from the 'very conservative locking' in the sixth edition of the Unix kernel.

Izvor: habr.com

Kupite pouzdan hosting za web stranice s DDoS zaštitom, VPS VDS servere 🔥 Kupite pouzdan hosting za web stranice s DDoS zaštitom, VPS VDS servere | ProHoster