Kako se cjevovodi implementiraju u Unixu

Kako se cjevovodi implementiraju u Unixu
Ovaj članak opisuje implementaciju cjevovoda u Unix kernelu. Bio sam pomalo razočaran nedavnim člankom pod naslovom "Kako cjevovodi rade u Unixu?» ispostavilo se ne o unutrašnjoj strukturi. Postao sam znatiželjan i kopao sam po starim izvorima da pronađem odgovor.

O čemu se radi?

Cjevovodi su "vjerovatno najvažniji izum u Unixu" - definišuća karakteristika Unixove osnovne filozofije sastavljanja malih programa i poznati slogan komandne linije:

$ echo hello | wc -c
6

Ova funkcionalnost ovisi o sistemskom pozivu koji pruža kernel pipe, što je opisano na stranicama dokumentacije cijev(7) и cijev(2):

Cjevovodi pružaju jednosmjerni kanal za međuprocesnu komunikaciju. Cjevovod ima ulaz (kraj za upisivanje) i izlaz (kraj za čitanje). Podaci upisani na ulaz cjevovoda mogu se čitati na izlazu.

Cjevovod se kreira pozivanjem pipe(2), koji vraća dva deskriptora datoteke: jedan se odnosi na ulaz cjevovoda, drugi na izlaz.

Izlaz praćenja iz gornje naredbe pokazuje kreiranje cjevovoda i protok podataka kroz njega od jednog procesa do drugog:

$ 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()da dobijete priložene deskriptore fajla. Jedan podređeni proces upisuje u jedan deskriptor, a drugi proces čita iste podatke iz drugog deskriptora. Shell "preimenuje" deskriptore 2 i 3 sa dup4 kako bi odgovarali stdin i stdout.

Bez cjevovoda, ljuska bi morala zapisati izlaz jednog procesa u datoteku i prenijeti ga drugom procesu kako bi pročitala podatke iz datoteke. Kao rezultat toga, trošili bismo više resursa i prostora na disku. Međutim, cjevovodi su dobri za više od izbjegavanja privremenih datoteka:

Ako proces pokuša čitati iz praznog cjevovoda, onda read(2) blokirat će se dok podaci ne budu dostupni. Ako proces pokuša pisati u cijeli cjevovod, onda write(2) će blokirati sve dok se ne pročita dovoljno podataka iz cjevovoda da se završi upisivanje.

Kao i POSIX zahtjev, ovo je važno svojstvo: pisanje u cjevovod do PIPE_BUF bajtovi (najmanje 512) moraju biti atomski tako da procesi mogu komunicirati jedni s drugima kroz cjevovod na način na koji normalne datoteke (koje ne pružaju takve garancije) ne mogu.

Sa običnom datotekom, proces može zapisati sav svoj izlaz u njega i proslijediti ga drugom procesu. Ili procesi mogu raditi u tvrdom paralelnom modu, koristeći vanjski signalni mehanizam (poput semafora) da obavještavaju jedni druge o završetku pisanja ili čitanja. Transporteri nas spašavaju od svih ovih problema.

šta tražimo?

Objasniću na prste kako biste lakše zamislili kako transporter može raditi. Morat ćete dodijeliti bafer i neko stanje u memoriji. Trebat će vam funkcije za dodavanje i uklanjanje podataka iz bafera. Trebat će vam neka mogućnost za pozivanje funkcija tokom operacija čitanja i pisanja na deskriptorima datoteka. I brave su potrebne za implementaciju posebnog ponašanja opisanog gore.

Sada smo spremni ispitati izvorni kod kernela pod jakim svjetlom lampe kako bismo potvrdili ili opovrgli naš nejasan mentalni model. Ali uvijek budite spremni na neočekivano.

Gdje tražimo?

Ne znam gde leži moj primerak čuvene knjige.Knjiga o lavovima« sa Unix 6 izvornim kodom, ali zahvaljujući Unix Heritage Society može se pretraživati ​​na internetu izvorni kod čak i starije verzije Unixa.

Lutanje kroz TUHS arhive je kao posjeta muzeju. Možemo da pogledamo našu zajedničku istoriju i poštujem godine napora da se sav ovaj materijal, malo po malo, povrati sa starih kaseta i ispisa. I vrlo sam svjestan tih fragmenata koji još uvijek nedostaju.

Pošto smo zadovoljili našu radoznalost o drevnoj istoriji cevovoda, možemo da pogledamo moderna jezgra za poređenje.

Inače, pipe je sistemski poziv broj 42 u tabeli sysent[]. Slučajnost?

Tradicionalna Unix jezgra (1970–1974)

Nisam našao nikakav trag pipe(2) ni u PDP-7 Unix (januar 1970), ni u prvo izdanje Unixa (novembar 1971), niti u nekompletnom izvornom kodu drugo izdanje (juni 1972).

TUHS to tvrdi treće izdanje Unixa (februar 1973.) je bila prva verzija sa cjevovodima:

Treće izdanje Unixa bila je posljednja verzija sa kernelom napisanim na asembleru, ali i prva verzija sa cevovodima. Tokom 1973. godine radilo se na poboljšanju trećeg izdanja, kernel je prepisan u C i tako je rođeno četvrto izdanje Unixa.

Jedan čitatelj je pronašao skenirani dokument u kojem je Doug McIlroy predložio ideju "povezivanja programa poput baštenskog crijeva".

Kako se cjevovodi implementiraju u Unixu
U knjizi Briana KernighanaUnix: Istorija i memoari“, istorija pojavljivanja transportera također spominje ovaj dokument: “... visio je na zidu u mojoj kancelariji u Bell Labs-u 30 godina.” Evo intervju sa McIlroyemi još jedna priča iz Mekilrojevo delo, napisano 2014:

Kada se pojavio Unix, moja strast prema korutinama natjerala me je da zamolim autora OS-a, Kena Thompsona, da dozvoli da podaci upisani u neki proces odu ne samo na uređaj, već i na izlaz u drugi proces. Ken je odlučio da je to moguće. Međutim, kao minimalista, želio je da svaka karakteristika sistema igra značajnu ulogu. Da li je direktno pisanje između procesa zaista velika prednost u odnosu na pisanje u međufajl? I tek kada sam dao konkretan predlog sa privlačnim nazivom "cevovod" i opisom sintakse interakcije procesa, Ken je na kraju uzviknuo: "Učiniću to!".

I jeste. Jedne kobne večeri, Ken je promijenio kernel i shell, popravio nekoliko standardnih programa kako bi standardizirao način na koji prihvataju unos (koji bi mogao doći iz cjevovoda) i promijenio imena datoteka. Sljedećeg dana, cjevovodi su se vrlo široko koristili u aplikacijama. Do kraja sedmice, sekretarice su ih koristile za slanje dokumenata iz programa za obradu teksta u štampar. Nešto kasnije, Ken je zamijenio originalni API i sintaksu za umotavanje upotrebe cjevovoda čistijim konvencijama koje se od tada koriste.

Nažalost, izvorni kod za treće izdanje Unix kernela je izgubljen. I iako imamo izvorni kod kernela napisan u C četvrto izdanje, koji je objavljen u novembru 1973. godine, ali je izašao nekoliko mjeseci prije zvaničnog izdanja i ne sadrži implementaciju pipeline-a. Šteta što je izvorni kod za ovu legendarnu Unix funkciju izgubljen, možda zauvijek.

Imamo tekst dokumentacije za pipe(2) iz oba izdanja, tako da možete početi pretraživanjem dokumentacije treće izdanje (za određene riječi, podvučeno "ručno", niz od ^H literala nakon čega slijedi donja crta!). Ovaj proto-pipe(2) je napisan u asembleru i vraća samo jedan deskriptor datoteke, ali već pruža očekivanu osnovnu funkcionalnost:

Sistemski poziv cijev kreira I/O mehanizam koji se zove cjevovod. Vraćeni deskriptor datoteke može se koristiti za operacije čitanja i pisanja. Kada se nešto upiše u cevovod, on baferuje do 504 bajta podataka, nakon čega se proces pisanja suspenduje. Prilikom čitanja iz cjevovoda, preuzimaju se baferirani podaci.

Do sljedeće godine, kernel je prepisan u C, i pipe(2) četvrto izdanje sa prototipom dobio svoj moderan izgled"pipe(fildes)"

Sistemski poziv cijev kreira I/O mehanizam koji se zove cjevovod. Vraćeni deskriptori datoteke mogu se koristiti u operacijama čitanja i pisanja. Kada se nešto upiše u cevovod, koristi se deskriptor vraćen u r1 (odnosno fildes[1]), baferovan do 4096 bajtova podataka, nakon čega se proces pisanja suspenduje. Prilikom čitanja iz cjevovoda, deskriptor vraćen na r0 (odnosno fildes[0]) uzima podatke.

Pretpostavlja se da nakon što je cjevovod definiran, dva (ili više) procesa u interakciji (kreirani naknadnim pozivanjem viljuška) će proslijediti podatke iz cjevovoda koristeći pozive čitati и pisati.

Shell ima sintaksu za definiranje linearnog niza procesa povezanih putem cjevovoda.

Pozivi za čitanje iz praznog cjevovoda (koji ne sadrži baferirane podatke) koji ima samo jedan kraj (svi deskriptori datoteke za upisivanje zatvoreni) vraćaju "kraj datoteke". Pozivi za pisanje u sličnoj situaciji se zanemaruju.

Najranije očuvana implementacija cjevovoda se primjenjuje do petog izdanja Unixa (juni 1974.), ali je gotovo identičan onom koji se pojavio u sljedećem izdanju. Dodati samo komentari, tako da se peto izdanje može preskočiti.

Unix šesto izdanje (1975.)

Počinje čitati Unix izvorni kod šesto izdanje (maj 1975). U velikoj mjeri zahvaljujući Lions mnogo je lakše pronaći nego izvore ranijih verzija:

Dugi niz godina knjiga Lions bio je jedini dokument o Unix kernelu dostupan izvan Bell Labs-a. Iako je licenca šestog izdanja dozvoljavala nastavnicima da koriste njen izvorni kod, licenca sedmog izdanja je isključila ovu mogućnost, pa je knjiga distribuirana u ilegalnim kucanim primjercima.

Danas možete kupiti ponovljeni primjerak knjige na čijim koricama su prikazani učenici za fotokopir aparatom. A zahvaljujući Warrenu Toomeyu (koji je započeo TUHS projekat), možete preuzeti Šesto izdanje Izvorni PDF. Želim vam dati predstavu o tome koliko je truda uloženo u kreiranje datoteke:

Prije više od 15 godina ukucao sam kopiju priloženog izvornog koda Lionsjer mi se nije svidio kvalitet mog primjerka iz nepoznatog broja drugih primjeraka. TUHS još nije postojao, a ja nisam imao pristup starim izvorima. Ali 1988. godine sam pronašao staru traku sa 9 traka koja je imala rezervnu kopiju sa PDP11 kompjutera. Bilo je teško znati da li radi, ali postojalo je netaknuto /usr/src/ stablo u kojem je većina fajlova označena kao 1979, što je i tada izgledalo drevno. Bilo je to sedmo izdanje, ili PWB derivat, pomislio sam.

Uzeo sam nalaz kao osnovu i ručno uredio izvore do stanja šestog izdanja. Dio koda je ostao isti, dio je morao biti malo izmijenjen, mijenjajući moderni token += u zastarjeli =+. Nešto je jednostavno obrisano, a nešto je trebalo potpuno prepisati, ali ne previše.

A danas na TUHS-u možemo čitati na mreži izvorni kod šestog izdanja arhivu, kojoj je ruku imao Dennis Ritchie.

Inače, na prvi pogled, glavna karakteristika C-koda prije perioda Kernighana i Ritchieja je njegova kratkoća. Nije često da sam u mogućnosti da ubacim isječke koda bez opsežnog uređivanja kako bi se uklopio u relativno usko područje prikaza na mojoj web lokaciji.

Na početku /usr/sys/ken/pipe.c postoji komentar sa objašnjenjem (i da, ima ih 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 se nije promijenila od četvrtog izdanja. Ali ovdje vidimo, bez ikakve javne dokumentacije, da su cjevovodi nekada koristili datoteke kao rezervnu memoriju!

Što se tiče LARG fajlova, oni odgovaraju inode-zastava LARG, koji koristi "veliki algoritam adresiranja" za obradu indirektni blokovi za podršku većim sistemima datoteka. Pošto je Ken rekao da je bolje da ih ne koristim, rado ću mu vjerovati 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;
}

Komentar jasno opisuje šta se ovde dešava. Ali nije tako lako razumjeti kod, dijelom zbog toga kako "struct korisnik u» i registre R0 и R1 prosljeđuju se parametri sistemskog poziva i povratne vrijednosti.

Hajde da probamo sa ialloc() mesto na disku inode (inode), i uz pomoć falloc() - radnja dva file. Ako sve prođe kako treba, postavićemo zastavice da identifikujemo ove datoteke kao dva kraja cevovoda, usmerićemo ih na isti inode (čiji broj referenci postaje 2) i označiti inode kao izmenjen i u upotrebi. Obratite pažnju na zahtjeve za stavljam() u stazama grešaka za smanjenje broja referenci u novom inodu.

pipe() dospjelo kroz R0 и R1 vratiti brojeve deskriptora fajla za čitanje i pisanje. falloc() vraća pokazivač na strukturu datoteke, ali i "vraća" putem u.u_ar0[R0] i deskriptor datoteke. Odnosno, kod je pohranjen u r deskriptor datoteke za čitanje i dodjeljuje deskriptor za pisanje direktno iz u.u_ar0[R0] nakon drugog poziva falloc().

Zastava FPIPE, koji postavljamo prilikom kreiranja cjevovoda, kontrolira ponašanje funkcije rdwr() u sys2.c, koji poziva specifične I/O rutine:

/*
 * 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() в pipe.c čita podatke iz cjevovoda. Ali bolje je pratiti implementaciju počevši od writep(). Opet, kod je postao komplikovaniji zbog prirode konvencije o prenošenju argumenata, ali neki detalji se mogu izostaviti.

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

Želimo upisati bajtove na ulaz cjevovoda u.u_count. Prvo moramo zaključati inode (pogledajte dolje plock/prele).

Zatim provjeravamo broj referenci inoda. Dokle god su oba kraja cjevovoda otvorena, brojač bi trebao biti 2. Držimo se za jednu kariku (od rp->f_inode), pa ako je brojač manji od 2, to bi trebalo da znači da je proces čitanja zatvorio svoj kraj cevovoda. Drugim rečima, pokušavamo da pišemo u zatvoreni cevovod, što je greška. Prvi kod greške EPIPE i signal SIGPIPE pojavio se u šestom izdanju Unixa.

Ali čak i ako je pokretna traka otvorena, može biti puna. U tom slučaju otpuštamo zaključavanje i idemo na spavanje u nadi da će drugi proces čitati iz cjevovoda i osloboditi dovoljno prostora u njemu. Kada se probudimo, vraćamo se na početak, ponovo spuštamo bravu i započinjemo novi ciklus pisanja.

Ako ima dovoljno slobodnog prostora u cjevovodu, tada u njega upisujemo podatke pomoću piši(). Parametar i_size1 inode'a (sa praznim cevovodom može biti jednak 0) ukazuje na kraj podataka koje već sadrži. Ako ima dovoljno prostora za pisanje, možemo ispuniti cjevovod iz i_size1 do PIPESIZ. Zatim otpuštamo zaključavanje i pokušavamo probuditi bilo koji proces koji čeka na čitanje iz cjevovoda. Vraćamo se na početak da vidimo da li smo uspjeli zapisati onoliko bajtova koliko nam je bilo potrebno. Ako ne uspije, započinjemo novi ciklus snimanja.

Obično parametar i_mode inode se koristi za pohranjivanje dozvola r, w и x. Ali u slučaju cjevovoda signaliziramo da neki proces čeka na pisanje ili čitanje pomoću bitova IREAD и IWRITE respektivno. Proces postavlja zastavu i poziva sleep(), a očekuje se da će se u budućnosti pozvati neki drugi proces wakeup().

Prava magija se dešava unutra sleep() и wakeup(). Oni se implementiraju u slp.c, izvor poznatog komentara "Ne očekuje se da ovo shvatite". Srećom, ne moramo razumjeti kod, samo pogledajte 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 se kasnije probuditi drugim procesom, koji će pozvati wakeup() za isti kanal. writep() и readp() koordiniraju svoje akcije putem takvih uparenih poziva. Zapiši to pipe.c uvijek dati prioritet PPIPE kada je pozvan sleep(), pa sve sleep() može biti prekinut signalom.

Sada imamo sve da razumijemo funkciju 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);
}

Možda će vam biti lakše čitati ovu funkciju odozdo prema gore. Grana "čitaj i vraćaj" se obično koristi kada postoje neki podaci u cjevovodu. U ovom slučaju koristimo čitaj() pročitajte onoliko podataka koliko je dostupno počevši od trenutnog f_offset pročitajte, a zatim ažurirajte vrijednost odgovarajućeg pomaka.

Prilikom narednih čitanja, cjevovod će biti prazan ako je dostignut pomak čitanja i_size1 na inodu. Resetujemo poziciju na 0 i pokušavamo da probudimo bilo koji proces koji želi da upiše u cevovod. Znamo da kada je transporter pun, writep() zaspati dalje ip+1. A sada kada je cevovod prazan, možemo ga probuditi da nastavi sa ciklusom pisanja.

Ako nema šta da se čita, onda readp() može postaviti zastavu IREAD i zaspi dalje ip+2. Znamo šta će ga probuditi writep()kada upisuje neke podatke u cjevovod.

Komentari na čitaj() i piši() pomoći će vam da shvatite da umjesto prolaska parametara kroz "u» možemo ih tretirati kao regularne I/O funkcije koje uzimaju datoteku, poziciju, bafer u memoriji i broje broj bajtova za čitanje ili pisanje.

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

Što se tiče "konzervativnog" blokiranja, onda readp() и writep() zaključajte inode dok ne završe ili ne dobiju rezultat (tj. call wakeup). plock() и prele() raditi jednostavno: koristeći drugačiji skup poziva sleep и wakeup omogući nam da probudimo bilo koji proces koji treba zaključavanje koje smo upravo otpustili:

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

U početku nisam mogao razumjeti zašto readp() ne uzrokuje prele(ip) prije poziva wakeup(ip+1). Prva stvar writep() poziva u svojoj petlji, ovo plock(ip), što dovodi do zastoja if readp() još nije uklonio svoj blok, tako da kod mora nekako raditi ispravno. Ako pogledate wakeup(), postaje jasno da samo označava proces spavanja kao spreman za izvršenje, tako da u budućnosti sched() zaista ga lansirao. Dakle readp() uzroka wakeup(), otključava, postavlja IREAD i pozive sleep(ip+2)- sve ovo pre writep() ponovo pokreće ciklus.

Ovo završava opis cjevovoda u šestom izdanju. Jednostavan kod, dalekosežne implikacije.

Sedmo izdanje Unixa (januar 1979.) je bilo novo veliko izdanje (četiri godine kasnije) koje je uvelo mnoge nove aplikacije i karakteristike kernela. Takođe je pretrpeo značajne promene u vezi sa upotrebom tipova, spojeva i tipovanih pokazivača na strukture. kako god kod cjevovoda praktično se nije promenio. Možemo preskočiti ovo izdanje.

Xv6, jednostavno jezgro nalik Unixu

Za stvaranje jezgra Xv6 pod uticajem šestog izdanja Unixa, ali napisan u modernom C-u za rad na x86 procesorima. Kod je lak za čitanje i razumljiv. Također, za razliku od Unix izvora sa TUHS-om, možete ga kompajlirati, modificirati i pokrenuti na nečemu drugom osim PDP 11/70. Stoga se ovo jezgro široko koristi na univerzitetima kao nastavni materijal o operativnim sistemima. Izvori su na Githubu.

Kod sadrži jasnu i promišljenu implementaciju cijevi.c, podržan baferom u memoriji umjesto inodom na disku. Ovdje dajem samo definiciju "strukturnog cjevovoda" 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 ostatka implementacije, što uključuje funkcije piperead(), pipewrite() и pipeclose(). Stvarni sistemski poziv sys_pipe je omotač implementiran u sysfile.c. Preporučujem da pročitate sav njegov kod. Kompleksnost je na nivou izvornog koda šestog izdanja, ali je mnogo lakši i prijatniji za čitanje.

Linux 0.01

Možete pronaći izvorni kod za Linux 0.01. Biće poučno proučiti implementaciju cjevovoda u njegovoj fs/pipe.c. Ovdje se inode koristi za predstavljanje cevovoda, ali sam cevovod je napisan u modernom C. Ako ste provalili svoj put kroz šesto izdanje koda, ovde nećete imati problema. Ovako 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<<(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;
}

Čak i bez gledanja u definicije strukture, možete shvatiti kako se broj referenci inode koristi za provjeru da li operacija pisanja rezultira SIGPIPE. Osim rada bajt po bajt, ovu funkciju je lako usporediti s gore navedenim idejama. Čak i logiku sleep_on/wake_up ne izgleda tako strano.

Moderni Linux kerneli, FreeBSD, NetBSD, OpenBSD

Brzo sam prešao preko nekih modernih kernela. Nijedna od njih već nema implementaciju zasnovanu na disku (nije iznenađujuće). Linux ima svoju implementaciju. I iako tri moderna BSD kernela sadrže implementacije zasnovane na kodu koji je napisao John Dyson, tokom godina su postali previše različiti jedni od drugih.

Citati fs/pipe.c (na Linuxu) ili sys/kern/sys_pipe.c (na *BSD), potrebna je prava posvećenost. Performanse i podrška za karakteristike kao što su vektorski i asinhroni I/O su danas važni u kodu. I detalji o dodjeli memorije, zaključavanja i konfiguracije kernela uvelike variraju. Ovo nije ono što je potrebno univerzitetima za uvodni kurs o operativnim sistemima.

U svakom slučaju, bilo mi je zanimljivo otkriti nekoliko starih obrazaca (na primjer, generiranje SIGPIPE i povratak EPIPE prilikom pisanja u zatvoreni cevovod) u svim ovim, tako različitim, modernim jezgrima. Vjerovatno nikada neću vidjeti PDP-11 kompjuter uživo, ali još uvijek ima puno toga za naučiti iz koda koji je napisan nekoliko godina prije mog rođenja.

Napisao Divi Kapoor 2011. godine, članak "Implementacija cijevi i FIFO-a Linux kernelaje pregled kako Linux cjevovodi (do sada) rade. A nedavno urezivanje na linuxu ilustruje cevovodni model interakcije, čije sposobnosti premašuju one privremenih datoteka; i takođe pokazuje koliko su dalekovodi otišli od "veoma konzervativnog zaključavanja" u šestom izdanju Unix kernela.

izvor: www.habr.com

Dodajte komentar