Kako so cevovodi implementirani v Unixu

Kako so cevovodi implementirani v Unixu
Ta članek opisuje izvedbo cevovodov v jedru Unix. Nekoliko me je razočaral nedavni članek z naslovom "Kako delujejo cevovodi v Unixu?" izkazalo se je ne o notranji strukturi. Postala sem radovedna in brskala sem po starih virih, da bi našla odgovor.

Za kaj se gre?

Cevovodi so "verjetno najpomembnejši izum v Unixu" - značilnost Unixove temeljne filozofije sestavljanja majhnih programov in znani slogan ukazne vrstice:

$ echo hello | wc -c
6

Ta funkcionalnost je odvisna od sistemskega klica, ki ga zagotavlja jedro pipe, ki je opisan na straneh dokumentacije cev (7) и cev (2):

Cevovodi zagotavljajo enosmerni kanal za komunikacijo med procesi. Cevovod ima vhod (konec pisanja) in izhod (konec branja). Podatke, zapisane na vhod cevovoda, je mogoče prebrati na izhodu.

Cevovod se ustvari s klicanjem pipe(2), ki vrne dva deskriptorja datoteke: eden se nanaša na vhod cevovoda, drugi na izhod.

Izhod sledenja iz zgornjega ukaza prikazuje ustvarjanje cevovoda in pretok podatkov skozi njega od enega procesa do drugega:

$ 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

Pokliče nadrejeni proces pipe()da dobite priložene deskriptorje datotek. En podrejeni proces piše v en deskriptor, drugi proces pa bere iste podatke iz drugega deskriptorja. Lupina "preimenuje" deskriptorja 2 in 3 z dup4, da se ujemata s stdin in stdout.

Brez cevovodov bi lupina morala zapisati izhod enega procesa v datoteko in ga posredovati drugemu procesu, da prebere podatke iz datoteke. Posledično bi zapravili več virov in prostora na disku. Vendar so cevovodi dobri za več kot le izogibanje začasnim datotekam:

Če proces poskuša brati iz praznega cevovoda, potem read(2) bo blokiran, dokler podatki niso na voljo. Če proces poskuša pisati v polni cevovod, potem write(2) bo blokiral, dokler se iz cevovoda ne prebere dovolj podatkov za dokončanje pisanja.

Tako kot zahteva POSIX je tudi to pomembna lastnost: pisanje v cevovod do PIPE_BUF bajti (vsaj 512) morajo biti atomski, tako da lahko procesi komunicirajo med seboj prek cevovoda na način, kot običajne datoteke (ki ne zagotavljajo takšnih jamstev) ne morejo.

Pri običajni datoteki lahko proces vanjo zapiše ves svoj izhod in ga posreduje drugemu procesu. Lahko pa procesi delujejo v trdem vzporednem načinu z uporabo zunanjega signalnega mehanizma (kot je semafor), da drug drugega obvestijo o zaključku pisanja ali branja. Tekoči trakovi nas rešijo vseh teh težav.

Kaj iščemo?

Razložil bom na prste, da si boste lažje predstavljali, kako lahko deluje tekoči trak. V pomnilniku boste morali dodeliti medpomnilnik in nekaj stanja. Potrebovali boste funkcije za dodajanje in odstranjevanje podatkov iz medpomnilnika. Potrebovali boste nekaj zmogljivosti za klicanje funkcij med operacijami branja in pisanja deskriptorjev datotek. In ključavnice so potrebne za izvajanje zgoraj opisanega posebnega vedenja.

Zdaj smo pripravljeni preiskati izvorno kodo jedra pod močno svetlobo luči, da potrdimo ali ovržemo naš nejasen mentalni model. Vedno pa bodite pripravljeni na nepričakovano.

Kam iščemo?

Ne vem, kje leži moj izvod slavne knjige.Knjiga o levih« z izvorno kodo Unix 6, vendar zahvaljujoč Unix Heritage Society je mogoče iskati na spletu izvorna koda tudi starejše različice Unixa.

Potepanje po arhivih TUHS je kot obisk muzeja. Ogledamo si lahko našo skupno zgodovino in spoštujem leta prizadevanj, da bi vse to gradivo po delih povrnili iz starih kaset in izpisov. In zelo se zavedam tistih drobcev, ki še vedno manjkajo.

Ko smo potešili svojo radovednost o starodavni zgodovini cevovodov, si lahko za primerjavo ogledamo sodobna jedra.

Mimogrede, pipe je številka sistemskega klica 42 v tabeli sysent[]. Naključje?

Tradicionalna jedra Unix (1970–1974)

Nisem našel nobene sledi pipe(2) niti noter PDP-7 Unix (januar 1970), niti v prva izdaja Unixa (november 1971), niti v nepopolni izvorni kodi druga izdaja (junij 1972).

TUHS trdi, da tretja izdaja Unixa (februar 1973) je bila prva različica s cevovodi:

Tretja izdaja Unixa je bila zadnja različica z jedrom, napisanim v asemblerju, a tudi prva različica s cevovodi. Leta 1973 je potekalo delo za izboljšanje tretje izdaje, jedro je bilo na novo napisano v C in tako se je rodila četrta izdaja Unixa.

En bralec je našel optično prebran dokument, v katerem je Doug McIlroy predlagal idejo o "povezovanju programov kot vrtna cev."

Kako so cevovodi implementirani v Unixu
V knjigi Briana KernighanaUnix: zgodovina in spomini«, zgodovina pojava transporterjev omenja tudi ta dokument: »... 30 let je visel na steni v moji pisarni v Bell Labs.« Tukaj intervju z McIlroyemin še ena zgodba iz McIlroyjevo delo, napisano leta 2014:

Ko se je pojavil Unix, sem zaradi strasti do korutin prosil avtorja operacijskega sistema, Kena Thompsona, naj dovoli, da podatki, zapisani v neki proces, gredo ne samo v napravo, ampak tudi do izhoda v drug proces. Ken je mislil, da je to mogoče. Vendar pa je kot minimalist želel, da ima vsaka funkcija sistema pomembno vlogo. Ali je neposredno pisanje med procesi res velika prednost pred pisanjem v vmesno datoteko? In šele, ko sem dal poseben predlog s privlačnim imenom "cevovod" in opisom sintakse interakcije procesov, je Ken končno vzkliknil: "To bom naredil!".

In naredil. Nekega usodnega večera je Ken spremenil jedro in lupino, popravil več standardnih programov, da bi standardiziral, kako sprejemajo vnos (ki bi lahko prišel iz cevovoda), in spremenil imena datotek. Naslednji dan so bili cevovodi zelo razširjeni v aplikacijah. Do konca tedna so tajnice z njimi pošiljale dokumente iz urejevalnikov besedil v tiskalnik. Nekoliko kasneje je Ken zamenjal prvotni API in sintakso za zavijanje uporabe cevovodov s čistejšimi konvencijami, ki so se uporabljale vse od takrat.

Na žalost je bila izvorna koda za tretjo izdajo jedra Unix izgubljena. In čeprav imamo izvorno kodo jedra, napisano v C četrta izdaja, ki je izšel novembra 1973, vendar je izšel nekaj mesecev pred uradnim izidom in ne vsebuje implementacije cevovodov. Škoda, da je izvorna koda za to legendarno funkcijo Unix izgubljena, morda za vedno.

Imamo besedilo dokumentacije za pipe(2) iz obeh izdaj, tako da lahko začnete z iskanjem po dokumentaciji tretja izdaja (za določene besede, podčrtane "ročno", niz literalov ^H, ki mu sledi podčrtaj!). Ta proto-pipe(2) je napisan v asemblerju in vrne samo en deskriptor datoteke, vendar že zagotavlja pričakovano osnovno funkcionalnost:

Sistemski klic cevi ustvari V/I mehanizem, imenovan cevovod. Vrnjeni deskriptor datoteke se lahko uporablja za operacije branja in pisanja. Ko se nekaj zapiše v cevovod, shrani v medpomnilnik do 504 bajtov podatkov, nato pa se postopek zapisovanja prekine. Pri branju iz cevovoda se vzamejo medpomnilniki.

Do naslednjega leta je bilo jedro prepisano v C in pipe(2) četrta izdaja je s prototipom dobil sodoben videz "pipe(fildes)»:

Sistemski klic cevi ustvari V/I mehanizem, imenovan cevovod. Vrnjene deskriptorje datotek je mogoče uporabiti v operacijah branja in pisanja. Ko se nekaj zapiše v cevovod, se uporabi deskriptor, vrnjen v r1 (oziroma fildes[1]), medpomnilnik do 4096 bajtov podatkov, po katerem se postopek pisanja prekine. Pri branju iz cevovoda deskriptor, vrnjen v r0 (oz. fildes[0]), prevzame podatke.

Predpostavlja se, da ko je cevovod definiran, sta dva (ali več) medsebojno delujočih procesov (ustvarjena z naknadnimi klici vilice) bo posredoval podatke iz cevovoda s pomočjo klicev preberite и pisati.

Lupina ima sintakso za definiranje linearnega niza procesov, povezanih prek cevovoda.

Klici za branje iz praznega cevovoda (ki ne vsebuje podatkov v medpomnilniku), ki ima samo en konec (vsi deskriptorji datotek za pisanje so zaprti), vrnejo "konec datoteke". Pisani klici v podobni situaciji so prezrti.

Najzgodnejši ohranjena cevovodna izvedba velja do pete izdaje Unixa (junij 1974), vendar je skoraj enak tistemu, ki se je pojavil v naslednji izdaji. Samo dodani komentarji, zato lahko peto izdajo preskočimo.

Unix šesta izdaja (1975)

Začetek branja izvorne kode Unix šesta izdaja (maj 1975). V veliki meri zahvaljujoč Lions je veliko lažje najti kot vire prejšnjih različic:

Že vrsto let knjiga Lions je bil edini dokument o jedru Unixa, ki je bil na voljo zunaj Bell Labs. Čeprav je licenca za šesto izdajo dovoljevala učiteljem uporabo njene izvorne kode, je licenca za sedmo izdajo to možnost izključevala, zato je bila knjiga razdeljena v nezakonitih tipkanih izvodih.

Danes lahko kupite ponatis knjige, katere naslovnica prikazuje študente za kopirnim strojem. In zahvaljujoč Warrenu Toomeyu (ki je začel projekt TUHS), lahko prenesete Šesta izdaja Izvor PDF. Želim vam predstaviti, koliko truda je bilo vloženega v ustvarjanje datoteke:

Pred več kot 15 leti sem vtipkal kopijo izvorne kode Lionsker mi ni bila všeč kakovost moje kopije od neznanega števila drugih kopij. TUHS še ni obstajal, jaz pa nisem imel dostopa do starih virov. Toda leta 1988 sem našel staro kaseto z 9 skladbami, ki je imela varnostno kopijo iz računalnika PDP11. Težko je bilo vedeti, ali je delovalo, vendar je bilo nedotaknjeno drevo /usr/src/, v katerem je bila večina datotek označena z letom 1979, ki je bilo že takrat videti starodavno. Mislil sem, da je to sedma izdaja ali izpeljanka PWB.

Najdbo sem vzel za osnovo in ročno uredil vire do stanja šeste izdaje. Del kode je ostal enak, del je bilo treba nekoliko urediti, tako da sodoben žeton += spremenili v zastarelega =+. Nekaj ​​je bilo preprosto izbrisano, nekaj pa je bilo treba v celoti prepisati, a ne preveč.

In danes lahko na spletu na TUHS preberemo izvorno kodo šeste izdaje arhiv, h kateremu je imel prste Dennis Ritchie.

Mimogrede, na prvi pogled je glavna značilnost kode C pred obdobjem Kernighana in Ritchieja njena kratkost. Ne zgodi se pogosto, da bi lahko vstavil delčke kode brez obsežnega urejanja, da bi ustrezal razmeroma ozkemu območju prikaza na mojem spletnem mestu.

Zgodnje /usr/sys/ken/pipe.c obstaja pojasnjevalni komentar (in ja, še več je /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

Velikost medpomnilnika se od četrte izdaje ni spremenila. Toda tukaj vidimo, brez kakršne koli javne dokumentacije, da so cevovodi nekoč uporabljali datoteke kot nadomestno shranjevanje!

Datoteke LARG ustrezajo inode-zastavica LARG, ki ga za obdelavo uporablja "algoritem velikega naslavljanja". posredni bloki za podporo večjim datotečnim sistemom. Ker je Ken rekel, da je bolje, da jih ne uporabljam, mu z veseljem verjamem na besedo.

Tukaj je pravi sistemski klic 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, kaj se tukaj dogaja. Vendar kode ni tako enostavno razumeti, delno zaradi tega, kako "struct uporabnik u» in registri R0 и R1 parametri sistemskega klica in vrnjene vrednosti so posredovani.

Poskusimo z ialloc() postavite na disk inode (inode), in s pomočjo Faloc() - shranite dva mapa. Če bo šlo vse v redu, bomo nastavili zastavice za identifikacijo teh datotek kot dveh koncev cevovoda, jih usmerili na isti inode (katerega število referenc postane 2) in označili inode kot spremenjen in v uporabi. Bodite pozorni na zahteve do položim() v poteh napak, da zmanjšate število sklicev v novem inodu.

pipe() zaradi R0 и R1 vrne številke deskriptorja datoteke za branje in pisanje. falloc() vrne kazalec na strukturo datoteke, vendar tudi "vrne" prek u.u_ar0[R0] in deskriptor datoteke. To pomeni, da je koda shranjena v r deskriptor datoteke za branje in dodeli deskriptor za neposredno pisanje u.u_ar0[R0] po drugem klicu falloc().

Zastava FPIPE, ki ga nastavimo pri ustvarjanju cevovoda, nadzoruje obnašanje funkcije rdwr() v sys2.c, ki kliče posebne V/I 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);
    }
        /* … */
}

Nato funkcija readp() в pipe.c bere podatke iz cevovoda. Vendar je bolje slediti izvajanju od writep(). Koda je ponovno postala bolj zapletena zaradi narave konvencije o posredovanju argumentov, vendar je mogoče nekatere podrobnosti izpustiti.

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

Bajte želimo zapisati v vhod cevovoda u.u_count. Najprej moramo zakleniti inode (glejte spodaj plock/prele).

Nato preverimo število referenc inode. Dokler sta oba konca cevovoda odprta, mora biti števec 2. Držimo se enega člena (od rp->f_inode), torej če je števec manjši od 2, bi to moralo pomeniti, da je proces branja zaprl svoj konec cevovoda. Z drugimi besedami, poskušamo pisati v zaprt cevovod, kar je napaka. Prva koda napake EPIPE in signal SIGPIPE pojavil v šesti izdaji Unixa.

A tudi če je tekoči trak odprt, je lahko poln. V tem primeru sprostimo ključavnico in gremo spat v upanju, da bo drug proces prebral iz cevovoda in sprostil dovolj prostora v njem. Ko se zbudimo, se vrnemo na začetek, ponovno obesimo ključavnico in začnemo nov cikel pisanja.

Če je v cevovodu dovolj prostega prostora, vanj zapišemo podatke z uporabo piši()... Parameter i_size1 inode'a (s praznim cevovodom je lahko enak 0) kaže na konec podatkov, ki jih že vsebuje. Če je dovolj prostora za pisanje, lahko napolnimo cevovod iz i_size1 za PIPESIZ. Nato sprostimo ključavnico in poskušamo prebuditi vse procese, ki čakajo na branje iz cevovoda. Vrnemo se na začetek, da vidimo, ali smo uspeli napisati toliko bajtov, kot smo jih potrebovali. Če ne, potem začnemo nov cikel snemanja.

Ponavadi parameter i_mode inode se uporablja za shranjevanje dovoljenj r, w и x. Toda v primeru cevovodov signaliziramo, da nek proces čaka na pisanje ali branje z uporabo bitov IREAD и IWRITE oz. Proces nastavi zastavico in pokliče sleep(), v prihodnosti pa se pričakuje, da bo poklical kakšen drug proces wakeup().

Prava čarovnija se zgodi v sleep() и wakeup(). Izvajajo se v slp.c, vir znamenitega komentarja "Od tebe se ne pričakuje, da boš razumel". Na srečo nam ni treba razumeti kode, samo poglejte nekaj komentarjev:

/*
 * 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, ki kliče sleep() za določen kanal, lahko pozneje prebudi drug proces, ki bo poklical wakeup() za isti kanal. writep() и readp() usklajujejo svoja dejanja prek takih seznanjenih klicev. Upoštevajte to pipe.c vedno daj prednost PPIPE ob klicu sleep(), torej vse sleep() lahko prekine signal.

Zdaj imamo vse za razumevanje 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);
}

Morda boste lažje prebrali to funkcijo od spodaj navzgor. Veja »branje in vrnitev« se običajno uporablja, ko je v cevovodu nekaj podatkov. V tem primeru uporabljamo preberi() preberi toliko podatkov, kolikor je na voljo, začenši s trenutnim f_offset preberite in nato posodobite vrednost ustreznega odmika.

Pri naslednjih branjih bo cevovod prazen, če je dosežen odmik branja i_size1 na inodu. Ponastavimo položaj na 0 in poskušamo prebuditi vsak proces, ki želi pisati v cevovod. Vemo, da ko je tekoči trak poln, writep() zaspati naprej ip+1. In zdaj, ko je cevovod prazen, ga lahko prebudimo, da nadaljuje svoj cikel pisanja.

Če ni kaj brati, potem readp() lahko nastavi zastavo IREAD in zaspi naprej ip+2. Vemo, kaj ga bo prebudilo writep()ko zapiše nekaj podatkov v cevovod.

Komentarji na read() in writei() vam bo pomagal razumeti, da namesto posredovanja parametrov prek "u» lahko jih obravnavamo kot običajne V/I funkcije, ki vzamejo datoteko, položaj, medpomnilnik v pomnilniku in štejejo število bajtov za branje ali 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;
/* … */

Glede "konzervativnega" blokiranja torej readp() и writep() zakleni inode, dokler ne končajo ali dobijo rezultat (tj. klic wakeup). plock() и prele() deluje preprosto: z uporabo drugega nabora klicev sleep и wakeup nam omogočajo, da prebudimo vsak proces, ki potrebuje ključavnico, ki smo jo pravkar sprostili:

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

Najprej nisem mogel razumeti, zakaj readp() ne povzroča prele(ip) pred klicem wakeup(ip+1). Prva stvar writep() klice v svoji zanki, to plock(ip), kar povzroči zastoj, če readp() še ni odstranil svoje blokade, zato mora koda nekako delovati pravilno. Če pogledate wakeup(), postane jasno, da le označi postopek spanja kot pripravljenega za izvedbo, tako da v prihodnje sched() res lansiral. torej readp() vzrokov wakeup(), odklepa, postavlja IREAD in klice sleep(ip+2)- vse to prej writep() ponovno zažene cikel.

S tem je opis cevovodov v šesti izdaji zaključen. Preprosta koda, daljnosežne posledice.

Sedma izdaja Unix (januar 1979) je bila nova velika izdaja (štiri leta kasneje), ki je predstavila številne nove aplikacije in funkcije jedra. Prav tako je bil podvržen pomembnim spremembam v povezavi z uporabo vlivanja tipov, zvez in tipkanih kazalcev na strukture. Vendar koda cevovodov praktično ni spremenila. To izdajo lahko preskočimo.

Xv6, preprosto Unixu podobno jedro

Za ustvarjanje jedra Xv6 pod vplivom šeste izdaje Unixa, vendar napisan v sodobnem C za delovanje na procesorjih x86. Koda je lahko berljiva in razumljiva. Poleg tega ga lahko za razliko od izvornih kod Unix s TUHS prevedete, spremenite in izvajate na nečem drugem kot na PDP 11/70. Zato se to jedro pogosto uporablja na univerzah kot učno gradivo o operacijskih sistemih. Viri so na Githubu.

Koda vsebuje jasno in premišljeno izvedbo cev.c, ki ga podpira medpomnilnik v pomnilniku namesto inode na disku. Tukaj podajam le definicijo "strukturnega cevovoda" in funkcijo 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() nastavi stanje vse preostale izvedbe, ki vključuje funkcije piperead(), pipewrite() и pipeclose(). Dejanski sistemski klic sys_pipe je ovoj, implementiran v sysfile.c. Priporočam branje celotne njegove kode. Kompleksnost je na ravni izvorne kode šeste izdaje, vendar je veliko lažja in prijetnejša za branje.

Linux 0.01

Najdete lahko izvorno kodo za Linux 0.01. Poučno bo preučiti izvedbo cevovodov v njegovem fs/pipe.c. Tu se za predstavitev cevovoda uporablja inode, sam cevovod pa je napisan v sodobnem C. Če ste se prebili skozi kodo šeste izdaje, tukaj ne boste imeli težav. 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<<(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;
}

Tudi brez pogleda na definicije strukture lahko ugotovite, kako se število referenc inode uporablja za preverjanje, ali operacija zapisovanja povzroči SIGPIPE. Poleg dela bajt za bajtom je to funkcijo enostavno primerjati z zgornjimi zamislimi. Tudi logika sleep_on/wake_up ne izgleda tako tujec.

Sodobna jedra Linuxa, FreeBSD, NetBSD, OpenBSD

Na hitro sem pregledal nekaj sodobnih jeder. Nobeden od njih še nima diskovne izvedbe (kar ni presenetljivo). Linux ima svojo izvedbo. In čeprav tri sodobna jedra BSD vsebujejo implementacije, ki temeljijo na kodi, ki jo je napisal John Dyson, so z leti postala med seboj preveč različna.

Brati fs/pipe.c (na Linuxu) oz sys/kern/sys_pipe.c (na *BSD), zahteva resnično predanost. Zmogljivost in podpora za funkcije, kot sta vektorski in asinhroni V/I, sta danes pomembni v kodi. Podrobnosti dodeljevanja pomnilnika, zaklepanja in konfiguracije jedra se zelo razlikujejo. To ni tisto, kar univerze potrebujejo za uvodni tečaj o operacijskih sistemih.

Vsekakor mi je bilo zanimivo odkriti nekaj starih vzorcev (npr. generiranje SIGPIPE in se vrniti EPIPE pri pisanju v zaprti cevovod) v vseh teh, tako različnih, sodobnih jedrih. Verjetno nikoli ne bom videl računalnika PDP-11 v živo, a še vedno se moram veliko naučiti iz kode, ki je bila napisana nekaj let pred mojim rojstvom.

Napisala Divi Kapoor leta 2011, članek "Implementacija cevi in ​​FIFO v jedru Linuxaje pregled delovanja (do sedaj) cevovodov Linuxa. A nedavna zaveza v linuxu ponazarja cevovodni model interakcije, katerega zmogljivosti presegajo zmogljivosti začasnih datotek; in tudi prikazuje, kako daleč so šli cevovodi od "zelo konzervativnega zaklepanja" v šesti izdaji jedra Unix.

Vir: www.habr.com

Dodaj komentar