Kako su cjevovodi implementirani u Unixu

Kako su cjevovodi implementirani u Unixu
Ovaj članak opisuje implementaciju cjevovoda u Unix kernelu. Donekle me razočarao nedavni članak pod naslovom "Kako cjevovodi rade u Unixu?"ispalo je ne o unutarnjoj strukturi. Postao sam znatiželjan i kopao sam po starim izvorima kako bih pronašao odgovor.

O čemu pričamo?

Cjevovodi, “vjerojatno najvažniji izum u Unixu,” definirajuća su karakteristika temeljne filozofije Unixa povezivanja malih programa, kao i poznati znak na naredbenom retku:

$ 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 pisanja) i izlaz (kraj čitanja). Podaci upisani na ulaz cjevovoda mogu se očitati na izlazu.

Cjevovod se stvara pomoću poziva pipe(2), koji vraća dva deskriptora datoteke: jedan se odnosi na ulaz cjevovoda, drugi na izlaz.

Izlaz praćenja iz gornje naredbe pokazuje stvaranje 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 montirane deskriptore datoteka. Jedan podređeni proces piše u jedan handle, a drugi proces čita iste podatke iz drugog handle-a. Ljuska koristi dup2 za "preimenovanje" deskriptora 3 i 4 da odgovaraju stdin i stdout.

Bez cijevi, ljuska bi morala zapisati rezultat jednog procesa u datoteku i proslijediti ga drugom procesu da pročita podatke iz datoteke. Kao rezultat toga, trošili bismo više resursa i prostora na disku. Međutim, cjevovodi su dobri ne samo zato što vam omogućuju izbjegavanje upotrebe privremenih datoteka:

Ako proces pokušava čitati iz praznog cjevovoda tada read(2) blokirat će se dok podaci ne postanu dostupni. Ako proces pokuša pisati u puni cjevovod, tada write(2) blokirat će se dok se iz cjevovoda ne pročita dovoljno podataka za izvođenje pisanja.

Kao i POSIX zahtjev, ovo je važno svojstvo: pisanje u cjevovod do PIPE_BUF bajtovi (najmanje 512) moraju biti atomski kako bi procesi mogli međusobno komunicirati kroz cjevovod na način na koji obične datoteke (koje ne daju takva jamstva) ne mogu.

Kada koristite običnu datoteku, proces može zapisati sav svoj izlaz u nju i proslijediti ga drugom procesu. Ili procesi mogu raditi u vrlo paralelnom načinu, koristeći vanjski signalni mehanizam (poput semafora) da obavijeste jedni druge kada je pisanje ili čitanje završeno. Transportne trake spašavaju nas od svih ovih gnjavaža.

Što tražimo?

Objasnit ću to jednostavnim riječima kako biste lakše zamislili kako pokretna traka može funkcionirati. Morat ćete dodijeliti međuspremnik i neko stanje u memoriji. Trebat će vam funkcije za dodavanje i uklanjanje podataka iz međuspremnika. Trebat će vam neka sredstva za pozivanje funkcija tijekom operacija čitanja i pisanja na deskriptorima datoteka. Trebat će vam i brave za implementaciju gore opisanog posebnog ponašanja.

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 gledamo?

Ne znam gdje je moj primjerak poznate knjige"Knjiga lavova"s izvornim kodom Unixa 6, ali zahvaljujući Unix Heritage Society možete pretraživati ​​online na izvorni kod čak i starije verzije Unixa.

Lutanje arhivama TUHS-a je poput posjeta muzeju. Možemo pogledati našu zajedničku povijest i poštujem dugogodišnje napore da se sav ovaj materijal malo po malo povrati sa starih vrpci i otisaka. I jako sam svjestan tih fragmenata koji još uvijek nedostaju.

Nakon što smo zadovoljili svoju znatiželju o drevnoj povijesti transportera, možemo pogledati moderne kernele za usporedbu.

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

Tradicionalne Unix jezgre (1970. – 1974.)

Nisam našao nikakve tragove pipe(2) niti u PDP-7 Unix (siječanj 1970.), niti u prvo izdanje Unixa (studeni 1971.), niti u nepotpunom izvornom kodu drugo izdanje (lipanj 1972.).

TUHS navodi da treće izdanje Unixa (veljača 1973.) postala je prva verzija s pokretnim trakama:

Unix 1973rd Edition je bila posljednja verzija s kernelom napisanim u asemblerskom jeziku, ali i prva verzija s cjevovodima. Tijekom XNUMX. radilo se na poboljšanju trećeg izdanja, kernel je prepisan u C-u, pa se pojavilo četvrto izdanje Unixa.

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

Kako su cjevovodi implementirani u Unixu
U knjizi Briana KernighanaUnix: Povijest i memoari“, u povijesti nastanka pokretnih traka spominje se i ovaj dokument: “...visio je na zidu u mom uredu u Bell Labsu 30 godina.” Ovdje intervju s McIlroyem, i još jedna priča iz McIlroyev rad, napisan 2014:

Kada se Unix pojavio, moja fascinacija korutinama navela me da zamolim autora OS-a, Kena Thompsona, da dopusti da podaci koji su upisani u proces idu ne samo na uređaj, već i na izlaz u drugi proces. Ken je zaključio da je to moguće. Međutim, kao minimalist, želio je da svaka funkcija sustava igra značajnu ulogu. Je li izravno pisanje između procesa doista velika prednost u odnosu na pisanje u međudatoteku? Tek kad sam dao konkretan prijedlog upečatljivog naziva "cjevovod" i opisom sintakse za interakciju između procesa, Ken je konačno uzviknuo: "Učinit ću to!"

I učinio. Jedne kobne večeri Ken je promijenio kernel i ljusku, popravio nekoliko standardnih programa kako bi standardizirao način na koji prihvaćaju unos (koji je mogao doći iz cjevovoda), a također je promijenio i nazive datoteka. Sljedeći dan, cjevovodi su se počeli vrlo široko koristiti u aplikacijama. Do kraja tjedna tajnice su ih koristile za slanje dokumenata iz programa za obradu teksta na pisač. Nešto kasnije, Ken je zamijenio izvorni API i sintaksu za omatanje korištenja cjevovoda čistijim konvencijama, koje se od tada koriste.

Nažalost, izvorni kod trećeg izdanja Unix kernela je izgubljen. I iako imamo izvorni kod kernela napisan u C četvrto izdanje, objavljen u studenom 1973., ali je izašao nekoliko mjeseci prije službenog izdanja i ne sadrži implementacije u tijeku. Šteta je što je izvorni kod za ovu legendarnu Unix funkciju izgubljen, možda zauvijek.

Imamo tekstualnu dokumentaciju 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 literala ^H, nakon čega slijedi podvlaka!). Ovaj proto-pipe(2) napisan je u asemblerskom jeziku i vraća samo jedan deskriptor datoteke, ali već pruža očekivanu temeljnu funkcionalnost:

Poziv sustava cijev stvara ulazno/izlazni mehanizam koji se naziva cjevovod. Vraćeni deskriptor datoteke može se koristiti za operacije čitanja i pisanja. Kada se nešto zapiše u cjevovod, do 504 bajta podataka se sprema u međuspremnik, nakon čega se proces pisanja obustavlja. Prilikom čitanja iz cjevovoda, podaci u međuspremniku se oduzimaju.

Do sljedeće godine kernel je prepisan u C-u, i cijev(2) u četvrtom izdanju dobio svoj moderni izgled s prototipom "pipe(fildes)'

Poziv sustava cijev stvara ulazno/izlazni mehanizam koji se naziva cjevovod. Vraćeni deskriptori datoteka mogu se koristiti u operacijama čitanja i pisanja. Kada se nešto upiše u cjevovod, koristi se oznaka vraćena u r1 (odnosno fildes[1]), sprema se u međuspremnik na 4096 bajtova podataka, nakon čega se proces pisanja obustavlja. Prilikom čitanja iz cjevovoda, regulator vraćen na r0 (odnosno fildes[0]) preuzima podatke.

Pretpostavlja se da nakon što je cjevovod definiran, dva (ili više) komunikacijska procesa (stvorena naknadnim pozivima na viljuška) prenosit će podatke iz cjevovoda pomoću poziva čitati и pisati.

Ljuska ima sintaksu za definiranje linearnog niza procesa povezanih cjevovodom.

Pozivi za čitanje iz praznog cjevovoda (koji ne sadrži podatke u međuspremniku) koji ima samo jedan kraj (svi deskriptori datoteke za pisanje su zatvoreni) vraćaju "kraj datoteke". Pozivi na pisanje u sličnoj situaciji se ignoriraju.

Najranije sačuvana implementacija cjevovoda vrijedi do petog izdanja Unixa (lipanj 1974.), ali je gotovo identična onoj koja se pojavila u sljedećem izdanju. Komentari su upravo dodani, tako da možete preskočiti peto izdanje.

Šesto izdanje Unixa (1975.)

Počnimo čitati izvorni kod Unixa šesto izdanje (svibanj 1975.). Uglavnom zahvaljujući Znamenitosti puno ga je lakše pronaći nego izvore ranijih verzija:

Već dugi niz godina knjiga Znamenitosti bio je jedini dokument o Unix kernelu dostupan izvan Bell Labsa. Iako je licenca za šesto izdanje dopuštala nastavnicima korištenje izvornog koda, licenca za sedmo izdanje je isključivala tu mogućnost, pa je knjiga distribuirana u obliku ilegalnih tipkanih kopija.

Danas možete kupiti reprint knjige na čijoj naslovnici su prikazani studenti za fotokopirnim strojem. A zahvaljujući Warrenu Toomeyu (koji je pokrenuo projekt TUHS) možete preuzeti PDF datoteka s izvornim kodom za šesto izdanje. Želim vam dati ideju o tome koliko je truda uloženo u stvaranje datoteke:

Prije više od 15 godina upisao sam kopiju navedenog izvornog koda Znamenitosti, jer mi se nije svidjela kvaliteta moje kopije od nepoznatog broja drugih kopija. TUHS još nije postojao i nisam imao pristup starim izvorima. Ali 1988. pronašao sam staru vrpcu s 9 zapisa koja je sadržavala sigurnosnu kopiju s računala PDP11. Bilo je teško reći radi li, ali postojalo je netaknuto stablo /usr/src/ u kojem je većina datoteka bila označena godinom 1979., koja je čak i tada izgledala prastaro. Bilo je to sedmo izdanje ili njegov derivat PWB, kako sam vjerovao.

Nalaz sam uzeo kao osnovu i ručno uredio izvore do šestog izdanja. Dio koda ostao je isti, no neki su morali biti malo uređeni, mijenjajući moderni += token u zastarjeli =+. Neke stvari su jednostavno izbrisane, a neke su se morale potpuno prepisati, ali ne previše.

I danas možemo čitati online na TUHS-u izvorni kod šestog izdanja iz arhivu, u kojoj je prste imao Dennis Ritchie.

Inače, na prvi pogled, glavna značajka C-koda prije razdoblja Kernighana i Ritchieja je njegova kratkoća. Ne događa se često da mogu umetnuti dijelove koda bez opsežnog uređivanja kako bi odgovarao relativno uskom području prikaza na mojoj web stranici.

Rano /usr/sys/ken/pipe.c postoji objašnjenje (i da, ima još toga /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 međuspremnika nije se promijenila od četvrtog izdanja. Ali ovdje vidimo, bez ikakve javne dokumentacije, da su cjevovodi nekada koristili datoteke kao sigurnosnu pohranu!

Što se tiče VELIKIH datoteka, one odgovaraju inode zastavica VELIKA, koji koristi "veliki algoritam adresiranja" za obradu neizravni blokovi za podršku većim datotečnim sustavima. Budući da je Ken rekao da ih je bolje ne koristiti, rado ću mu vjerovati na riječ.

Ovo je pravi sistemski poziv 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 što se ovdje događa. Ali razumijevanje koda nije tako lako, dijelom zbog načina na koji "struct korisnik u» i registri R0 и R1 prosljeđuju se parametri sistemskog poziva i povratne vrijednosti.

Pokušajmo sa ialloc() staviti na disk inode (ručica indeksa), i uz pomoć Faloc() - mjesto dva u memoriji datoteka. Ako sve bude u redu, postavit ćemo zastavice za identifikaciju ovih datoteka kao dva kraja cjevovoda, usmjeriti ih na isti inode (čiji će broj referenci biti postavljen na 2) i označiti inode kao izmijenjen i u upotrebi. Obratite pažnju na zahtjeve za iput() u stazama pogrešaka za smanjenje broja referenci u novom inodu.

pipe() mora kroz R0 и R1 vratiti brojeve deskriptora datoteke za čitanje i pisanje. falloc() vraća pokazivač na strukturu datoteke, ali također "vraća" putem u.u_ar0[R0] i deskriptor datoteke. Odnosno, kod se sprema r deskriptor datoteke za čitanje i dodjeljuje deskriptor datoteke za izravno pisanje u.u_ar0[R0] nakon drugog poziva falloc().

zastava FPIPE, koji postavljamo prilikom stvaranja cjevovoda, kontrolira ponašanje funkcije rdwr() u sys2.cpozivanje specifičnih I/O rutina:

/*
 * 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 složeniji zbog konvencija prosljeđivanja argumenata, ali se neki detalji 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 u ulaz cjevovoda u.u_count. Prvo moramo zaključati inode (pogledajte dolje plock/prele).

Zatim provjeravamo brojač referenci inoda. Sve dok su oba kraja cjevovoda otvorena, brojač bi trebao biti jednak 2. Držimo jednu vezu (od rp->f_inode), pa ako je brojač manji od 2, to mora značiti da je proces čitanja zatvorio svoj kraj cjevovoda. Drugim riječima, pokušavamo pisati u zatvoreni cjevovod, a to je pogreška. Kod prve pogreš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 otključavamo bravu i idemo spavati u nadi da će neki drugi proces očitati iz cjevovoda i osloboditi dovoljno mjesta u njemu. Nakon buđenja vraćamo se na početak, ponovno vješamo bravu i započinjemo novi ciklus snimanja.

Ako u cjevovodu ima dovoljno slobodnog prostora, upisujemo podatke u njega pomoću piši(). Parametar i_size1 at inode (ako je cjevovod prazan može biti jednak 0) označava kraj podataka koje već sadrži. Ako ima dovoljno prostora za snimanje, možemo napuniti cjevovod iz i_size1 na PIPESIZ. Zatim oslobađamo bravu i pokušavamo probuditi bilo koji proces koji čeka na čitanje iz cjevovoda. Vraćamo se na početak da vidimo jesmo li uspjeli napisati onoliko bajtova koliko nam je potrebno. Ako ne uspije, započinjemo novi ciklus snimanja.

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

Prava magija se događa u sleep() и wakeup(). Implementiraju se u slp.c, izvor poznatog komentara "Ne očekuje se da ovo razumijete". 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 uzrokuje sleep() za određeni kanal, kasnije se može probuditi drugim procesom, što će uzrokovati wakeup() za isti kanal. writep() и readp() koordiniraju svoje radnje putem takvih uparenih poziva. imajte na umu da pipe.c uvijek daje prednost PPIPE kad se prozove sleep(), znaci to je to sleep() može biti prekinut signalom.

Sada imamo sve za razumijevanje funkcije 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" obično se koristi kada postoje neki podaci u cjevovodu. U ovom slučaju koristimo pročitaj() čitamo onoliko podataka koliko je dostupno počevši od trenutnog f_offset očitanje, a zatim ažurirajte vrijednost odgovarajućeg pomaka.

Pri sljedećim čitanjima, cjevovod će biti prazan ako je dosegnut pomak čitanja i_size1 na inodu. Ponovno postavljamo poziciju na 0 i pokušavamo probuditi svaki proces koji želi pisati u cjevovod. Znamo da kada je pokretna traka puna, writep() zaspat će na ip+1. I sada kada je cjevovod prazan, možemo ga probuditi da nastavi svoj ciklus pisanja.

Ako nemate što čitati, onda readp() može postaviti zastavu IREAD i zaspati dalje ip+2. Znamo što će ga probuditi writep(), kada zapisuje neke podatke u cjevovod.

Komentari na readi() i writei() će vam pomoći da shvatite da umjesto prosljeđivanja parametara putem "u"Možemo ih tretirati kao normalne I/O funkcije koje uzimaju datoteku, poziciju, međuspremnik 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, dakle readp() и writep() blokiraju inode dok ne završe svoj posao ili ne prime rezultat (tj. poziv wakeup). plock() и prele() raditi jednostavno: koristeći drugačiji skup poziva sleep и wakeup dopustite nam da probudimo svaki 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);
    }
}

Isprva nisam mogao shvatiti zašto readp() ne uzrokuje prele(ip) prije poziva wakeup(ip+1). Prva stvar je writep() uzrokuje u svom ciklusu, ovo plock(ip), što dovodi do zastoja ako readp() još nisam uklonio moju blokadu, tako da kod nekako mora raditi ispravno. Ako pogledate wakeup(), tada postaje jasno da samo označava proces spavanja kao spreman za izvršenje, tako da u budućnosti sched() stvarno pokrenuo. Tako readp() uzroci wakeup(), skida bravu, postavlja IREAD i poziva sleep(ip+2)- sve ovo prije writep() nastavlja ciklus.

Ovim je završen opis pokretnih traka u šestom izdanju. Jednostavan kod, dalekosežne posljedice.

Sedmo izdanje Unixa (siječanj 1979.) bilo je novo veliko izdanje (četiri godine kasnije) koje je uvelo mnoge nove aplikacije i značajke jezgre. Također je doživio značajne promjene u vezi s upotrebom lijevanja slova, spojeva i tipiziranih pokazivača na strukture. Međutim transportna traka kod praktički nepromijenjena. Možemo preskočiti ovo izdanje.

Xv6, jednostavan kernel sličan Unixu

Za stvaranje kernela Xv6 pod utjecajem šestog izdanja Unixa, ali je napisan u modernom C-u za rad na x86 procesorima. Kod je lako čitljiv i razumljiv. Osim toga, za razliku od Unix izvora s TUHS-om, možete ga kompajlirati, modificirati i pokrenuti na nečemu što nije PDP 11/70. Stoga se ova jezgra naširoko koristi na sveučilištima kao obrazovni materijal o operativnim sustavima. Izvori su na Githubu.

Kod sadrži jasnu i promišljenu implementaciju cijev.c, podržan međuspremnikom u memoriji umjesto inodom na disku. Ovdje dajem samo definiciju "strukturalnog 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 omot implementiran u sysfile.c. Preporučujem da pročitate cijeli njegov kod. Kompleksnost je na razini izvornog koda šestog izdanja, ali je mnogo lakša i ugodnija za čitanje.

Linux 0.01

Može se pronaći izvorni kod Linuxa 0.01. Bit će poučno proučiti implementaciju cjevovoda u njegovoj fs/pipe.c. Ovo koristi inode za predstavljanje cjevovoda, ali sam cjevovod je napisan u modernom C-u. Ako ste se probili kroz kod šestog izdanja, ovdje 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 definicija strukture, možete shvatiti kako se broj referenci inoda koristi za provjeru rezultira li operacija pisanja SIGPIPE. Osim rada bajt po bajt, ovu je funkciju lako usporediti s gore opisanim idejama. Čak i logika sleep_on/wake_up ne izgleda tako strano.

Moderni Linux kerneli, FreeBSD, NetBSD, OpenBSD

Brzo sam prošao kroz neke moderne kernele. Nitko od njih više nema implementaciju diska (ne iznenađuje). Linux ima vlastitu implementaciju. Iako tri moderne BSD kernele sadrže implementacije temeljene na kodu koji je napisao John Dyson, tijekom godina postale su previše različite jedna od druge.

Čitati fs/pipe.c (na Linuxu) ili sys/kern/sys_pipe.c (na *BSD), potrebna je prava predanost. Današnji kod govori o performansama i podršci za značajke kao što su vektorski i asinkroni I/O. A detalji dodjele memorije, zaključavanja i konfiguracije jezgre uvelike se razlikuju. Ovo nije ono što fakulteti trebaju za uvodni tečaj operativnih sustava.

U svakom slučaju, zanimalo me iskopavanje nekih starih obrazaca (poput generiranja SIGPIPE i vratiti EPIPE kada pišete u zatvoreni cjevovod) u svim tim različitim modernim kernelima. Vjerojatno nikad neću vidjeti PDP-11 računalo u stvarnom životu, ali još uvijek ima mnogo toga za naučiti iz koda koji je napisan godinama prije mog rođenja.

Članak koji je napisala Divi Kapoor 2011.:Implementacija cijevi i FIFO u jezgri Linuxa" daje pregled kako cjevovodi (još uvijek) rade u Linuxu. A nedavna predaja u Linuxu ilustrira cjevovodni model interakcije, čije mogućnosti nadilaze one privremenih datoteka; i također pokazuje koliko daleko su cjevovodi došli od "vrlo konzervativnog zaključavanja" šestog izdanja Unix kernela.

Izvor: www.habr.com

Dodajte komentar