Ako sú implementované potrubia v Unixe

Ako sú implementované potrubia v Unixe
Tento článok popisuje implementáciu potrubí v jadre Unixu. Bol som trochu sklamaný, že nedávny článok s názvom „Ako fungujú potrubia v Unixe?"ukázalo sa nie o vnútornej štruktúre. Začal som byť zvedavý a hrabal som v starých zdrojoch, aby som našiel odpoveď.

o čom to hovoríme?

Pipelines, „pravdepodobne najdôležitejší vynález v Unixe“, sú definujúcou charakteristikou základnej Unixovej filozofie spájania malých programov, ako aj známym znakom na príkazovom riadku:

$ echo hello | wc -c
6

Táto funkcia závisí od systémového volania poskytovaného jadrom pipe, ktorý je popísaný na stránkach dokumentácie potrubie (7) и potrubie (2):

Potrubia poskytujú jednosmerný kanál pre medziprocesovú komunikáciu. Potrubie má vstup (koniec zápisu) a výstup (koniec čítania). Dáta zapísané na vstup potrubia je možné čítať na výstupe.

Potrubie sa vytvorí pomocou volania pipe(2), ktorý vracia dva deskriptory súborov: jeden odkazuje na vstup potrubia, druhý na výstup.

Výstup sledovania z vyššie uvedeného príkazu ukazuje vytvorenie potrubia a tok údajov cez neho z jedného procesu do druhého:

$ 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

Rodičovský proces volá pipe()na získanie pripojených deskriptorov súborov. Jeden podriadený proces zapisuje do jedného handle a iný proces číta tie isté údaje z iného handle. Shell používa dup2 na "premenovanie" deskriptorov 3 a 4, aby sa zhodovali so stdin a stdout.

Bez rúr by shell musel zapísať výsledok jedného procesu do súboru a odovzdať ho inému procesu, aby zo súboru prečítal údaje. V dôsledku toho by sme plytvali viac zdrojmi a miestom na disku. Potrubia sú však dobré nielen preto, že vám umožňujú vyhnúť sa používaniu dočasných súborov:

Ak sa proces pokúša čítať z prázdneho potrubia, potom read(2) zablokuje, kým nebudú dostupné údaje. Ak sa proces pokúsi zapísať do úplného kanála, potom write(2) sa zablokuje, kým sa z kanála neprečíta dostatok údajov na vykonanie zápisu.

Podobne ako požiadavka POSIX je to dôležitá vlastnosť: zapisovanie do potrubia až do PIPE_BUF bajty (najmenej 512) musia byť atómové, aby procesy mohli medzi sebou komunikovať prostredníctvom potrubia spôsobom, ktorý bežné súbory (ktoré neposkytujú takéto záruky) nedokážu.

Pri použití bežného súboru môže proces doň zapísať celý svoj výstup a odovzdať ho inému procesu. Alebo procesy môžu fungovať vo vysoko paralelnom režime pomocou externého signalizačného mechanizmu (ako je semafor), aby sa navzájom informovali o dokončení zápisu alebo čítania. Dopravníky nás ušetria od všetkých týchto problémov.

čo hľadáme?

Vysvetlím to jednoducho, aby ste si ľahšie predstavili, ako môže dopravník fungovať. Budete musieť prideliť vyrovnávaciu pamäť a nejaký stav v pamäti. Na pridávanie a odstraňovanie údajov z vyrovnávacej pamäte budete potrebovať funkcie. Budete potrebovať nejaké prostriedky na volanie funkcií počas operácií čítania a zápisu na deskriptoroch súborov. A na implementáciu vyššie opísaného špeciálneho správania budete potrebovať zámky.

Teraz sme pripravení skúmať zdrojový kód jadra pod jasným svetlom lampy, aby sme potvrdili alebo vyvrátili náš vágny mentálny model. Buďte však vždy pripravení na neočakávané.

kde to hľadáme?

Neviem, kde je moja kópia slávnej knihy"Kniha Levy"so zdrojovým kódom Unix 6, ale vďaka Unix Heritage Society môžete vyhľadávať online na zdrojový kód aj staršie verzie Unixu.

Putovanie po archívoch TUHS je ako návšteva múzea. Môžeme sa pozrieť na našu spoločnú históriu a rešpektujem mnohoročné úsilie o získanie všetkého tohto materiálu kúsok po kúsku zo starých pások a výtlačkov. A ja si veľmi dobre uvedomujem tie fragmenty, ktoré stále chýbajú.

Po uspokojení našej zvedavosti o dávnej histórii dopravníkov sa môžeme na porovnanie pozrieť na moderné jadrá.

Mimochodom, pipe je číslo systému 42 v tabuľke sysent[]. Náhoda?

Tradičné unixové jadrá (1970–1974)

Nenašla som žiadne stopy pipe(2) ani v PDP-7 Unix (január 1970), ani v prvé vydanie Unixu (november 1971), ani v neúplnom zdrojovom kóde druhé vydanie (jún 1972).

TUHS to uvádza tretie vydanie Unixu (február 1973) sa stala prvou verziou s dopravníkmi:

Unix 1973rd Edition bola posledná verzia s jadrom napísaným v assembleri, ale aj prvá verzia s pipeline. Počas roku XNUMX sa pracovalo na zlepšení tretieho vydania, jadro bolo prepísané do C, a tak sa objavilo štvrté vydanie Unixu.

Jeden čitateľ našiel sken dokumentu, v ktorom Doug McIlroy navrhol myšlienku „prepojiť programy ako záhradná hadica“.

Ako sú implementované potrubia v Unixe
V knihe Briana KernighanaUnix: História a spomienky“, v histórii vzniku dopravníkov sa spomína aj tento dokument: „... visel na stene v mojej kancelárii v Bell Labs 30 rokov.“ Tu rozhovor s McIlroyoma ďalší príbeh z McIlroyova práca, napísaná v roku 2014:

Keď vyšiel Unix, moja fascinácia korutínmi ma priviedla k tomu, že som požiadal autora operačného systému Kena Thompsona, aby umožnil dátam zapísaným do procesu ísť nielen do zariadenia, ale aj na výstup do iného procesu. Ken sa rozhodol, že je to možné. Ako minimalista však chcel, aby každá funkcia systému zohrávala významnú úlohu. Je zápis priamo medzi procesmi skutočne veľkou výhodou oproti zápisu do medzisúboru? Až keď som urobil konkrétny návrh s chytľavým názvom „pipeline“ a popisom syntaxe interakcie medzi procesmi, Ken nakoniec zvolal: „Urobím to!“

A urobil. Jedného osudného večera Ken zmenil jadro a shell, opravil niekoľko štandardných programov, aby štandardizoval spôsob, akým prijímali vstup (ktorý mohol pochádzať z potrubia), a tiež zmenil názvy súborov. Nasledujúci deň sa potrubia začali používať v aplikáciách veľmi široko. Do konca týždňa ich sekretárky používali na odosielanie dokumentov z textových procesorov do tlačiarne. O niečo neskôr Ken nahradil pôvodné API a syntax pre obalenie použitia potrubí čistejšími konvenciami, ktoré sa odvtedy používajú.

Bohužiaľ, zdrojový kód pre tretie vydanie unixového jadra sa stratil. A hoci máme zdrojový kód jadra napísaný v C štvrté vydanie, vydaný v novembri 1973, ale vyšiel niekoľko mesiacov pred oficiálnym vydaním a neobsahuje implementácie pipeline. Je škoda, že zdrojový kód tejto legendárnej unixovej funkcie je stratený, možno navždy.

Máme k dispozícii textovú dokumentáciu pipe(2) z oboch vydaní, takže môžete začať prehľadávaním dokumentácie tretia edícia (pre určité slová podčiarknuté „manuálne“, reťazec literálov ^H, za ktorým nasleduje podčiarknutie!). Tento proto-pipe(2) je napísaný v jazyku symbolických adries a vracia iba jeden deskriptor súboru, ale už poskytuje očakávanú základnú funkčnosť:

Systémové volanie rúrka vytvára vstupno/výstupný mechanizmus nazývaný pipeline. Vrátený deskriptor súboru možno použiť na operácie čítania a zápisu. Keď sa niečo zapíše do potrubia, uloží sa do vyrovnávacej pamäte až 504 bajtov údajov, po ktorých sa proces zápisu pozastaví. Pri čítaní z pipeline sa dáta vo vyrovnávacej pamäti odoberú.

V nasledujúcom roku bolo jadro prepísané v C a fajka(2) vo štvrtom vydaní prototyp získal svoj moderný vzhľad “pipe(fildes)»:

Systémové volanie rúrka vytvára vstupno/výstupný mechanizmus nazývaný pipeline. Vrátené deskriptory súborov možno použiť v operáciách čítania a zápisu. Keď sa niečo zapíše do potrubia, použije sa handle vrátený v r1 (resp. fildes[1]), uložený do vyrovnávacej pamäte na 4096 bajtov dát, po čom sa proces zápisu pozastaví. Pri čítaní z pipeline, handle vrátený na r0 (resp. fildes[0]) preberá dáta.

Predpokladá sa, že akonáhle je kanál definovaný, dva (alebo viaceré) komunikačné procesy (vytvorené následnými volaniami na vidlica) bude prenášať údaje z potrubia pomocou hovorov čítať и písať.

Shell má syntax na definovanie lineárneho poľa procesov spojených potrubím.

Volania na čítanie z prázdneho kanála (neobsahujúceho žiadne údaje vo vyrovnávacej pamäti), ktorý má iba jeden koniec (všetky popisovače zapisovacieho súboru sú uzavreté), vrátia „koniec súboru“. Výzvy na písanie v podobnej situácii sa ignorujú.

Najskoršie zachovaná realizácia plynovodu platí do piateho vydania Unixu (jún 1974), ale je takmer identický s tým, ktorý sa objavil v ďalšom vydaní. Komentáre boli práve pridané, takže piate vydanie môžete preskočiť.

Šiesta edícia Unixu (1975)

Začnime čítať zdrojový kód Unixu šieste vydanie (máj 1975). Z veľkej časti vďaka Levy je oveľa jednoduchšie nájsť ako zdroje predchádzajúcich verzií:

Dlhé roky kniha Levy bol jediný dokument o jadre Unixu dostupný mimo Bell Labs. Hoci licencia šiesteho vydania umožňovala učiteľom používať jej zdrojový kód, licencia siedmeho vydania túto možnosť vylučovala, takže kniha bola distribuovaná vo forme nelegálnych kópií písaných strojom.

Dnes si môžete kúpiť dotlač knihy, ktorej obal zobrazuje študentov pri kopírke. A vďaka Warrenovi Toomeymu (ktorý spustil projekt TUHS) si môžete stiahnuť PDF súbor so zdrojovým kódom pre šieste vydanie. Chcem vám poskytnúť predstavu o tom, koľko úsilia bolo vynaložené na vytvorenie súboru:

Pred viac ako 15 rokmi som napísal kópiu uvedeného zdrojového kódu Levy, pretože sa mi nepáčila kvalita mojej kópie z neznámeho počtu iných kópií. TUHS ešte neexistoval a k starým zdrojom som nemal prístup. Ale v roku 1988 som našiel starú 9-stopovú pásku, ktorá obsahovala zálohu z počítača PDP11. Bolo ťažké povedať, či to fungovalo, ale existoval neporušený strom /usr/src/, v ktorom bola väčšina súborov označená rokom 1979, ktorý aj vtedy vyzeral starodávne. Bolo to siedme vydanie alebo jeho derivát PWB, ako som veril.

Nález som vzal za základ a ručne upravil pramene do šiesteho vydania. Časť kódu zostala rovnaká, ale niektoré museli byť mierne upravené, pričom sa moderný token += zmenil na zastaraný =+. Niektoré veci boli jednoducho vymazané a niektoré museli byť úplne prepísané, ale nie príliš.

A dnes si môžeme prečítať online na TUHS zdrojový kód šiesteho vydania z archívu, na ktorom mal ruku Dennis Ritchie.

Mimochodom, na prvý pohľad je hlavnou črtou C-kódu pred obdobím Kernighana a Ritchieho jeho stručnosť. Nestáva sa mi často, že by som dokázal vložiť kúsky kódu bez rozsiahlych úprav tak, aby sa mi zmestila na relatívne úzku oblasť zobrazenia na mojej stránke.

Čoskoro /usr/sys/ken/pipe.c je tam vysvetľujúci komentár (a áno, je toho viac /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

Veľkosť vyrovnávacej pamäte sa od štvrtého vydania nezmenila. Ale tu vidíme, bez akejkoľvek verejnej dokumentácie, že potrubia kedysi používali súbory ako zálohovacie úložisko!

Pokiaľ ide o VEĽKÉ súbory, zodpovedajú inode flag LARG, ktorý používa na spracovanie „algoritmus veľkého adresovania“. nepriame bloky na podporu väčších súborových systémov. Keďže Ken povedal, že je lepšie ich nepoužívať, s radosťou ho vezmem za slovo.

Tu je skutočné systémové volanie 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;
}

Komentár jasne popisuje, čo sa tu deje. Pochopenie kódu však nie je také ľahké, čiastočne kvôli spôsobu "struct user u» a registrov R0 и R1 parametre systémového volania a návratové hodnoty sú odovzdané.

Skúsme s ialloc() dať na disk inode (úchytka indexu)a s pomocou Falloc() - umiestniť dva do pamäte súbor. Ak všetko pôjde dobre, nastavíme príznaky na identifikáciu týchto súborov ako dvoch koncov kanála, nasmerujeme ich na rovnaký inode (ktorého počet referencií bude nastavený na 2) a označíme inode ako upravený a používaný. Venujte pozornosť žiadostiam vstup() v chybových cestách, aby sa znížil počet odkazov v novom inode.

pipe() musí prejsť R0 и R1 vrátiť čísla deskriptorov súborov na čítanie a zápis. falloc() vráti ukazovateľ na štruktúru súboru, ale aj "vráti" cez u.u_ar0[R0] a deskriptor súboru. To znamená, že kód sa uloží r deskriptor súboru na čítanie a priradí deskriptor súboru na zápis priamo z u.u_ar0[R0] po druhom hovore falloc().

vlajka FPIPE, ktorý nastavíme pri vytváraní pipeline, riadi správanie funkcie rdwr() v sys2.cvolanie špecifických I/O rutín:

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

Potom funkcia readp() в pipe.c číta údaje z potrubia. Ale je lepšie sledovať implementáciu od začiatku writep(). Kód sa opäť stal zložitejším kvôli konvenciám odovzdávania argumentov, ale niektoré detaily možno vynechať.

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

Chceme zapísať bajty na vstup potrubia u.u_count. Najprv musíme uzamknúť inode (pozri nižšie plock/prele).

Potom skontrolujeme referenčné počítadlo inódov. Pokiaľ zostanú oba konce potrubia otvorené, počítadlo by sa malo rovnať 2. Držíme jeden článok (od rp->f_inode), takže ak je počítadlo menšie ako 2, musí to znamenať, že proces čítania uzavrel svoj koniec potrubia. Inými slovami, pokúšame sa zapisovať do uzavretého potrubia a toto je chyba. Prvýkrát kód chyby EPIPE a signál SIGPIPE sa objavil v šiestom vydaní Unixu.

Ale aj keď je dopravník otvorený, môže byť plný. V tomto prípade uvoľníme zámok a ideme spať v nádeji, že ďalší proces bude čítať z potrubia a uvoľní v ňom dostatok miesta. Po prebudení sa vrátime na začiatok, opäť zavesíme zámok a spustíme nový nahrávací cyklus.

Ak je v potrubí dostatok voľného miesta, zapíšeme doň dáta pomocou písať (). Parameter i_size1 at inode (ak je potrubie prázdne, môže sa rovnať 0) označuje koniec údajov, ktoré už obsahuje. Ak je dostatok miesta na nahrávanie, môžeme naplniť potrubie z i_size1 na PIPESIZ. Potom uvoľníme zámok a pokúsime sa prebudiť akýkoľvek proces, ktorý čaká na čítanie z potrubia. Vrátime sa na začiatok, aby sme zistili, či sme dokázali zapísať toľko bajtov, koľko sme potrebovali. Ak zlyhá, začneme nový cyklus nahrávania.

Zvyčajne parameter i_mode inode sa používa na ukladanie povolení r, w и x. Ale v prípade potrubí signalizujeme, že nejaký proces čaká na zápis alebo čítanie pomocou bitov IREAD и IWRITE resp. Proces nastaví príznak a zavolá sleep()a očakáva sa, že nejaký iný proces v budúcnosti spôsobí wakeup().

Skutočná mágia sa odohráva v sleep() и wakeup(). Sú implementované v slp.c, zdroj slávneho komentára „Neočakáva sa, že to pochopíte“. Našťastie nemusíme rozumieť kódu, stačí sa pozrieť na niektoré komentáre:

/*
 * 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, ktorý spôsobuje sleep() pre konkrétny kanál, môže byť neskôr prebudený iným procesom, ktorý spôsobí wakeup() pre ten istý kanál. writep() и readp() koordinovať svoje akcie prostredníctvom takýchto spárovaných hovorov. poznač si to pipe.c vždy dáva prednosť PPIPE pri zavolaní sleep(), takže to je všetko sleep() môže byť prerušené signálom.

Teraz máme všetko na pochopenie funkcie 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žno bude pre vás jednoduchšie prečítať túto funkciu zdola nahor. Vetva "čítať a vrátiť" sa zvyčajne používa, keď sú v potrubí nejaké údaje. V tomto prípade používame readi() čítame toľko údajov, koľko je k dispozícii od aktuálneho f_offset čítanie a potom aktualizujte hodnotu zodpovedajúceho posunu.

Pri ďalších čítaniach bude potrubie prázdne, ak sa dosiahne posun čítania i_size1 pri inode. Resetujeme pozíciu na 0 a pokúsime sa prebudiť akýkoľvek proces, ktorý chce zapisovať do potrubia. Vieme, že keď je dopravník plný, writep() zaspí na ip+1. A teraz, keď je kanál prázdny, môžeme ho prebudiť a obnoviť cyklus zápisu.

Ak nemáte čo čítať, tak readp() môže nastaviť vlajku IREAD a zaspať ďalej ip+2. Vieme, čo ho zobudí writep(), keď zapíše nejaké údaje do potrubia.

Komentáre pre readi() a writei() vám pomôže pochopiť, že namiesto odovzdávania parametrov cez "u„Môžeme s nimi zaobchádzať ako s normálnymi I/O funkciami, ktoré berú súbor, pozíciu, vyrovnávaciu pamäť v pamäti a počítajú počet bajtov na čítanie alebo zápis.

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

Čo sa týka „konzervatívneho“ blokovania readp() и writep() zablokujte inode, kým nedokončia svoju prácu alebo nedostanú výsledok (t. j. zavolajte wakeup). plock() и prele() pracovať jednoducho: pomocou inej skupiny hovorov sleep и wakeup umožňujú nám prebudiť akýkoľvek proces, ktorý potrebuje zámok, ktorý sme práve uvoľnili:

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

Najprv som nevedel pochopiť prečo readp() nespôsobuje prele(ip) pred hovorom wakeup(ip+1). Prvá vec je writep() spôsobuje vo svojom cykle toto plock(ip), čo vedie k zablokovaniu, ak readp() zatiaľ som neodstránil môj blok, takže kód musí nejako fungovať správne. Ak sa pozriete na wakeup(), potom je jasné, že označí iba proces spánku ako pripravený na vykonanie, takže v budúcnosti sched() naozaj to spustilo. Takže readp() príčiny wakeup(), odstráni zámok, nastaví IREAD a hovory sleep(ip+2)- toto všetko predtým writep() obnoví cyklus.

Týmto je popis dopravníkov v šiestom vydaní dokončený. Jednoduchý kód, ďalekosiahle dôsledky.

Siedme vydanie Unixu (január 1979) bolo nové hlavné vydanie (o štyri roky neskôr), ktoré zaviedlo mnoho nových aplikácií a funkcií jadra. Výraznými zmenami prešiel aj v súvislosti s používaním typového odliatku, spojov a typizovaných ukazovateľov na štruktúry. Avšak kód dopravníka prakticky nezmenené. Toto vydanie môžeme preskočiť.

Xv6, jednoduché jadro podobné Unixu

Na vytvorenie jadra Xv6 ovplyvnený šiestym vydaním Unixu, ale je napísaný v modernom jazyku C, aby fungoval na procesoroch x86. Kód je ľahko čitateľný a zrozumiteľný. Navyše, na rozdiel od zdrojových kódov Unix s TUHS ho môžete skompilovať, upraviť a spustiť na niečom inom ako na PDP 11/70. Preto je toto jadro široko používané na univerzitách ako vzdelávací materiál o operačných systémoch. Zdroje sú na Github.

Kód obsahuje jasnú a premyslenú implementáciu potrubie.c, zálohovaný vyrovnávacou pamäťou v pamäti namiesto inodu na disku. Tu uvádzam iba definíciu "štrukturálneho potrubia" a funkcie 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() nastavuje stav zvyšku implementácie, ktorá zahŕňa funkcie piperead(), pipewrite() и pipeclose(). Aktuálne systémové volanie sys_pipe je obal implementovaný v sysfile.c. Odporúčam prečítať si celý jeho kód. Zložitosť je na úrovni zdrojového kódu šiesteho vydania, no číta sa oveľa jednoduchšie a príjemnejšie.

Linux 0.01

Zdrojový kód Linuxu 0.01 nájdete. Bude poučné študovať realizáciu potrubí v jeho fs/pipe.c. Toto používa inode na reprezentáciu potrubia, ale samotný kanál je napísaný v modernom jazyku C. Ak ste sa prepracovali cez kód 6. vydania, nebudete tu mať žiadne problémy. Takto vyzerá funkcia 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;
}

Bez toho, aby ste sa pozreli na definície štruktúry, môžete zistiť, ako sa počet referencií inodov používa na kontrolu, či operácia zápisu vedie k SIGPIPE. Okrem práce bajt po byte je táto funkcia ľahko porovnateľná s myšlienkami opísanými vyššie. Dokonca aj logika sleep_on/wake_up nevyzerá tak mimozemsky.

Moderné linuxové jadrá, FreeBSD, NetBSD, OpenBSD

Rýchlo som prebehol niektoré moderné jadrá. Žiadny z nich už nemá implementáciu disku (nie je prekvapujúce). Linux má svoju vlastnú implementáciu. Hoci tri moderné jadrá BSD obsahujú implementácie založené na kóde, ktorý napísal John Dyson, v priebehu rokov sa od seba príliš odlišovali.

Čítať fs/pipe.c (v systéme Linux) resp sys/kern/sys_pipe.c (na *BSD), vyžaduje si to skutočné odhodlanie. Dnešný kód je o výkone a podpore funkcií, ako sú vektorové a asynchrónne I/O. A podrobnosti o alokácii pamäte, zámkoch a konfigurácii jadra sa veľmi líšia. To nie je to, čo vysoké školy potrebujú na úvodný kurz operačných systémov.

Každopádne, zaujímalo ma vykopať nejaké staré vzory (napríklad generovanie SIGPIPE a vrátiť sa EPIPE pri zápise do uzavretého potrubia) vo všetkých týchto rôznych moderných jadrách. Počítač PDP-11 v reálnom živote asi nikdy neuvidím, no stále sa mám čo učiť z kódu, ktorý bol napísaný roky pred mojím narodením.

Článok, ktorý napísal Divi Kapoor v roku 2011:Implementácia potrubí a FIFO v Linuxovom jadre“ poskytuje prehľad o tom, ako (ešte stále) fungujú pipeline v Linuxe. A nedávny záväzok v systéme Linux ilustruje model interakcie potrubia, ktorého schopnosti presahujú schopnosti dočasných súborov; a tiež ukazuje, ako ďaleko sa potrubia dostali od „veľmi konzervatívneho zamykania“ jadra šiestej edície unixového jadra.

Zdroj: hab.com

Pridať komentár