Jak jsou potrubí implementována v Unixu

Jak jsou potrubí implementována v Unixu
Tento článek popisuje implementaci potrubí v jádře Unixu. Poněkud mě zklamalo, že nedávný článek s názvem "Jak fungují pipeline v Unixu?" ukázalo se ne o vnitřní struktuře. Byl jsem zvědavý a hledal jsem ve starých zdrojích, abych našel odpověď.

O čem to je?

Pipelines jsou „pravděpodobně nejdůležitějším vynálezem v Unixu“ – definujícím rysem základní filozofie Unixu sdružovat malé programy a známý slogan příkazového řádku:

$ echo hello | wc -c
6

Tato funkce závisí na systémovém volání poskytovaném jádrem pipe, který je popsán na stránkách dokumentace trubka (7) и trubka (2):

Pipelines poskytují jednosměrný kanál pro meziprocesovou komunikaci. Potrubí má vstup (zápis) a výstup (zápis). Data zapsaná na vstup potrubí lze číst na výstupu.

Potrubí se vytváří voláním pipe(2), který vrací dva deskriptory souborů: jeden odkazuje na vstup kanálu, druhý na výstup.

Výstup trasování z výše uvedeného příkazu ukazuje vytvoření potrubí a tok dat skrz něj z jednoho 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

Nadřazený proces volá pipe()k získání přiložených deskriptorů souborů. Jeden podřízený proces zapisuje do jednoho deskriptoru a jiný proces čte stejná data z jiného deskriptoru. Shell "přejmenuje" deskriptory 2 a 3 pomocí dup4, aby odpovídaly stdin a stdout.

Bez potrubí by shell musel zapsat výstup jednoho procesu do souboru a přenést ho do jiného procesu, aby mohl číst data ze souboru. V důsledku toho bychom plýtvali více prostředky a místem na disku. Potrubí je však dobré pro více než jen vyhýbání se dočasným souborům:

Pokud se proces pokusí číst z prázdného potrubí, pak read(2) zablokuje, dokud nebudou data dostupná. Pokud se proces pokusí zapisovat do úplného kanálu, pak write(2) se zablokuje, dokud nebude z kanálu načteno dostatek dat k dokončení zápisu.

Stejně jako požadavek POSIX je to důležitá vlastnost: zápis do kanálu až do PIPE_BUF bajtů (alespoň 512) musí být atomické, aby procesy mohly mezi sebou komunikovat prostřednictvím potrubí způsobem, který normální soubory (které neposkytují takové záruky) nemohou.

S běžným souborem může proces zapsat veškerý svůj výstup do něj a předat jej jinému procesu. Nebo procesy mohou fungovat v tvrdém paralelním režimu, využívajícím externí signalizační mechanismus (jako semafor), aby se navzájem informovaly o dokončení zápisu nebo čtení. Dopravníky nás ušetří všech těchto potíží.

co hledáme?

Vysvětlím na prstech, abyste si snadněji představili, jak může dopravník fungovat. Budete muset alokovat vyrovnávací paměť a nějaký stav v paměti. Budete potřebovat funkce pro přidání a odstranění dat z vyrovnávací paměti. Budete potřebovat nějaké zařízení pro volání funkcí během operací čtení a zápisu na deskriptory souborů. A zámky jsou potřeba k implementaci speciálního chování popsaného výše.

Nyní jsme připraveni prozkoumat zdrojový kód jádra pod jasným světlem lampy, abychom potvrdili nebo vyvrátili náš vágní mentální model. Buďte ale vždy připraveni na neočekávané.

kam se podíváme?

Nevím, kde leží moje kopie slavné knihy.Kniha lvi« se zdrojovým kódem Unix 6, ale díky Unix Heritage Society lze hledat online zdrojový kód i starší verze Unixu.

Putování archivy TUHS je jako návštěva muzea. Můžeme se podívat na naši společnou historii a já si vážím let snahy získat zpět všechen tento materiál kousek po kousku ze starých kazet a výtisků. A velmi si uvědomuji ty fragmenty, které stále chybí.

Po uspokojení naší zvědavosti na starou historii potrubí se můžeme pro srovnání podívat na moderní jádra.

Mimochodem, pipe je číslo systémového volání 42 v tabulce sysent[]. Náhoda?

Tradiční unixová jádra (1970–1974)

Nenašel jsem žádnou stopu pipe(2) ani dovnitř PDP-7 Unix (leden 1970), ani v první vydání Unix (listopad 1971), ani v neúplném zdrojovém kódu druhé vydání (červen 1972).

Tvrdí to TUHS třetí vydání Unix (únor 1973) byla první verze s potrubím:

Třetí vydání Unixu bylo poslední verzí s jádrem napsaným v assembleru, ale také první verzí s pipelines. Během roku 1973 probíhaly práce na vylepšení třetího vydání, jádro bylo přepsáno v C, a tak se zrodilo čtvrté vydání Unixu.

Jeden čtenář našel sken dokumentu, ve kterém Doug McIlroy navrhl myšlenku „propojení programů jako zahradní hadice“.

Jak jsou potrubí implementována v Unixu
V knize Briana KernighanaUnix: Historie a paměti“, historie vzhledu dopravníků také zmiňuje tento dokument: „... visel na zdi v mé kanceláři v Bell Labs po dobu 30 let.“ Tady rozhovor s McIlroyema další příběh z McIlroyovo dílo, napsané v roce 2014:

Když se objevil Unix, moje vášeň pro korutiny mě přiměla požádat autora OS, Kena Thompsona, aby umožnil datům zapsaným do nějakého procesu jít nejen do zařízení, ale také na výstup do jiného procesu. Ken si myslel, že je to možné. Jako minimalista však chtěl, aby každá funkce systému hrála významnou roli. Je přímý zápis mezi procesy skutečně velkou výhodou oproti zápisu do mezisouboru? A teprve když jsem udělal konkrétní návrh s chytlavým názvem „pipeline“ a popisem syntaxe interakce procesů, Ken nakonec zvolal: „Udělám to!“.

A udělal. Jednoho osudného večera Ken změnil jádro a shell, opravil několik standardních programů, aby standardizoval způsob, jakým přijímají vstup (který může pocházet z potrubí), a změnil názvy souborů. Následujícího dne byly potrubí velmi široce používány v aplikacích. Do konce týdne je sekretářky využívaly k odesílání dokumentů z textových procesorů do tiskárny. O něco později Ken nahradil původní API a syntaxi pro obalování použití potrubí čistšími konvencemi, které se od té doby používají.

Zdrojový kód třetího vydání unixového jádra byl bohužel ztracen. A přestože máme zdrojový kód jádra napsaný v C čtvrté vydání, který byl vydán v listopadu 1973, ale vyšel pár měsíců před oficiálním vydáním a neobsahuje implementaci potrubí. Je škoda, že zdrojový kód této legendární unixové funkce je ztracen, snad navždy.

Máme dokumentační text pro pipe(2) z obou verzí, takže můžete začít prohledáváním dokumentace třetí edice (u určitých slov podtrženo "ručně", řetězec literálů ^H následovaný podtržítkem!). Tento proto-pipe(2) je napsán v assembleru a vrací pouze jeden deskriptor souboru, ale již poskytuje očekávané základní funkce:

Systémové volání trubka vytváří I/O mechanismus zvaný pipeline. Vrácený deskriptor souboru lze použít pro operace čtení a zápisu. Když je něco zapsáno do kanálu, uloží se do vyrovnávací paměti až 504 bajtů dat, načež je proces zápisu pozastaven. Při čtení z potrubí se převezmou data ve vyrovnávací paměti.

V následujícím roce bylo jádro přepsáno v C a dýmka(2) čtvrté vydání získal svůj moderní vzhled s prototypem "pipe(fildes)»:

Systémové volání trubka vytváří I/O mechanismus zvaný pipeline. Vrácené deskriptory souborů lze použít při operacích čtení a zápisu. Když je něco zapsáno do potrubí, použije se deskriptor vrácený v r1 (resp. fildes[1]), uložený do vyrovnávací paměti až 4096 bajtů dat, načež je proces zápisu pozastaven. Při čtení z potrubí deskriptor vrácený na r0 (resp. fildes[0]) bere data.

Předpokládá se, že jakmile je potrubí definováno, dva (nebo více) interagující procesy (vytvořené následnými vyvoláními vidlice) bude předávat data z kanálu pomocí volání číst и zapsat.

Shell má syntaxi pro definování lineárního pole procesů spojených pomocí potrubí.

Volání ke čtení z prázdného kanálu (neobsahujícího žádná data ve vyrovnávací paměti), který má pouze jeden konec (všechny popisovače souborů pro zápis jsou uzavřeny), vracejí „konec souboru“. Psací hovory v podobné situaci jsou ignorovány.

Nejdříve zachovalá realizace potrubí týká k pátému vydání Unixu (červen 1974), ale je téměř identický s tím, který se objevil v dalším vydání. Pouze přidány komentáře, takže páté vydání lze přeskočit.

Unix šesté vydání (1975)

Začínám číst zdrojový kód Unixu šesté vydání (květen 1975). Z velké části díky Lions je mnohem snazší najít než zdroje dřívějších verzí:

Po mnoho let kniha Lions byl jediný dokument o unixovém jádře dostupný mimo Bell Labs. Ačkoli licence šestého vydání umožňovala učitelům používat jeho zdrojový kód, licence sedmého vydání tuto možnost vylučovala, takže kniha byla distribuována v nelegálních strojopisných kopiích.

Dnes si můžete koupit dotisk knihy, na jejíž obálce jsou studenti u kopírky. A díky Warrenu Toomeymu (který rozjel projekt TUHS) můžete stahovat Šesté vydání Zdroj PDF. Chci vám poskytnout představu o tom, kolik úsilí bylo vynaloženo na vytvoření souboru:

Před více než 15 lety jsem zadal kopii poskytnutého zdrojového kódu Lionsprotože se mi nelíbila kvalita mé kopie z neznámého počtu jiných kopií. TUHS ještě neexistoval a já jsem neměl přístup ke starým zdrojům. Ale v roce 1988 jsem našel starou pásku s 9 stopami, která měla zálohu z počítače PDP11. Bylo těžké vědět, jestli to funguje, ale existoval neporušený strom /usr/src/, ve kterém byla většina souborů označena rokem 1979, který i tehdy vypadal starověký. Bylo to sedmé vydání, nebo derivát PWB, pomyslel jsem si.

Nález jsem vzal za základ a ručně upravil prameny do stavu šestého vydání. Část kódu zůstala stejná, část musela být mírně upravena, přičemž se moderní token += změnil na zastaralý =+. Něco se prostě smazalo a něco se muselo úplně přepsat, ale ne příliš.

A dnes si můžeme online na TUHS přečíst zdrojový kód šestého vydání archiv, na kterém se podílel Dennis Ritchie.

Mimochodem, na první pohled je hlavním rysem C-kódu před obdobím Kernighana a Ritchieho jeho stručnost. Nestává se často, abych mohl vkládat úryvky kódu bez rozsáhlých úprav, aby se mi vešly na poměrně úzkou zobrazovací plochu na mém webu.

Brzy /usr/sys/ken/pipe.c je tam vysvětlující komentář (a ano, je toho víc /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 vyrovnávací paměti se od čtvrtého vydání nezměnila. Zde však bez jakékoli veřejné dokumentace vidíme, že kanály kdysi používaly soubory jako záložní úložiště!

Pokud jde o VELKÉ soubory, odpovídají inode-flag LARG, který používá ke zpracování „algoritmus velkého adresování“. nepřímé bloky pro podporu větších souborových systémů. Protože Ken řekl, že je lepší je nepoužívat, tak ho s radostí beru za slovo.

Zde je skutečné systémové volání 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ář jasně popisuje, co se zde děje. Ale není tak snadné porozumět kódu, částečně proto, jak "struct uživatele u» a registrů R0 и R1 jsou předány parametry systémového volání a návratové hodnoty.

Zkusme to s ialloc() umístit na disk inode (inode)a s pomocí Falloc() - uložit dvě soubor. Pokud vše půjde dobře, nastavíme příznaky, které tyto soubory identifikují jako dva konce potrubí, nasměrujeme je na stejný inode (jehož referenční počet bude 2) a označíme inode jako upravený a používaný. Věnujte pozornost žádostem o vstup() v chybových cestách ke snížení počtu odkazů v novém inodu.

pipe() splatný přes R0 и R1 vrátit čísla deskriptorů souborů pro čtení a zápis. falloc() vrací ukazatel na strukturu souboru, ale také "vrací" přes u.u_ar0[R0] a deskriptor souboru. To znamená, že kód je uložen v r deskriptor souboru pro čtení a přiřadí deskriptor pro zápis přímo z u.u_ar0[R0] po druhém hovoru falloc().

Vlajka FPIPE, který nastavíme při vytváření pipeline, řídí chování funkce rdwr() v sys2.c, který volá konkrétní I/O rutiny:

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

Pak funkce readp() в pipe.c čte data z potrubí. Ale je lepší sledovat implementaci počínaje writep(). Kód se opět stal komplikovanějším kvůli povaze konvence předávání argumentů, ale některé detaily lze vynechat.

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 zapisovat bajty na vstup potrubí u.u_count. Nejprve musíme zamknout inode (viz níže plock/prele).

Poté zkontrolujeme počet referencí inodů. Dokud zůstanou oba konce potrubí otevřené, počítadlo by mělo být 2. Držíme se jednoho článku (od rp->f_inode), takže pokud je počítadlo menší než 2, pak by to mělo znamenat, že proces čtení uzavřel svůj konec potrubí. Jinými slovy, snažíme se psát do uzavřeného potrubí, což je chyba. První chybový kód EPIPE a signál SIGPIPE se objevil v šestém vydání Unixu.

Ale i když je dopravník otevřený, může být plný. V tomto případě uvolníme zámek a jdeme spát v naději, že další proces bude číst z potrubí a uvolní v něm dostatek místa. Když se probudíme, vrátíme se na začátek, opět zavěsíme zámek a spustíme nový cyklus zápisu.

Pokud je v potrubí dostatek volného místa, zapíšeme do něj data pomocí napsat(). Parametr i_size1 inode'a (s prázdným potrubím se může rovnat 0) ukazuje na konec dat, která již obsahuje. Pokud je dostatek místa pro psaní, můžeme zaplnit potrubí z i_size1 na PIPESIZ. Poté zámek uvolníme a pokusíme se probudit jakýkoli proces, který čeká na čtení z potrubí. Vrátíme se na začátek, abychom zjistili, zda se nám podařilo zapsat tolik bajtů, kolik jsme potřebovali. Pokud selže, zahájíme nový nahrávací cyklus.

Obvykle parametr i_mode inode se používá k ukládání oprávnění r, w и x. Ale v případě potrubí signalizujeme, že nějaký proces čeká na zápis nebo čtení pomocí bitů IREAD и IWRITE respektive. Proces nastaví příznak a zavolá sleep()a očekává se, že v budoucnu zavolá nějaký jiný proces wakeup().

Skutečná magie se odehrává v sleep() и wakeup(). Jsou implementovány v slp.c, zdroj slavného komentáře „Neočekává se, že to pochopíte“. Naštěstí nemusíme rozumět kódu, stačí se podívat na některé komentáře:

/*
 * 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, který volá sleep() pro určitý kanál, může být později probuzen jiným procesem, který zavolá wakeup() pro stejný kanál. writep() и readp() koordinovat své akce prostřednictvím takto spárovaných hovorů. Všimněte si, že pipe.c vždy upřednostňovat PPIPE při zavolání sleep(), takže všechny sleep() může být přerušeno signálem.

Nyní máme vše pro pochopení funkce 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žná bude snazší číst tuto funkci zdola nahoru. Větev „přečíst a vrátit“ se obvykle používá, když jsou v potrubí nějaká data. V tomto případě používáme číst() přečíst tolik dat, kolik je k dispozici, počínaje aktuálním f_offset přečtěte a poté aktualizujte hodnotu odpovídajícího posunu.

Při následujících čteních bude potrubí prázdné, pokud bylo dosaženo offsetu čtení i_size1 u inodu. Pozici resetujeme na 0 a pokusíme se probudit jakýkoli proces, který chce zapisovat do pipeline. Víme, že když je dopravník plný, writep() usnout na ip+1. A teď, když je kanál prázdný, můžeme ho probudit a obnovit jeho zapisovací cyklus.

Pokud není co číst, tak readp() umí nastavit vlajku IREAD a usnout dál ip+2. Víme, co ho probudí writep()když zapisuje nějaká data do potrubí.

Komentáře k read() a writei() vám pomůže pochopit, že místo předávání parametrů přes "u» můžeme s nimi zacházet jako s běžnými I/O funkcemi, které berou soubor, pozici, vyrovnávací paměť v paměti a počítají počet bajtů ke čtení nebo zápisu.

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

Pokud jde o "konzervativní" blokování, pak readp() и writep() uzamknout inody, dokud neskončí nebo nezískají výsledek (tj wakeup). plock() и prele() pracovat jednoduše: pomocí jiné sady hovorů sleep и wakeup umožní nám probudit jakýkoli proces, který potřebuje zámek, který jsme právě uvolnili:

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

Nejdřív jsem nechápal proč readp() nezpůsobuje prele(ip) před hovorem wakeup(ip+1). První věc writep() volá ve své smyčce, toto plock(ip), což má za následek zablokování if readp() ještě neodstranil svůj blok, takže kód musí nějak správně fungovat. Pokud se podíváte na wakeup(), je zřejmé, že pouze označí proces spánku jako připravený k provedení, takže v budoucnu sched() opravdu spustil. Tak readp() příčin wakeup(), odemkne, nastaví IREAD a volání sleep(ip+2)- to všechno předtím writep() znovu spustí cyklus.

Tím je popis potrubí v šestém vydání dokončen. Jednoduchý kód, dalekosáhlé důsledky.

Sedmé vydání Unix (leden 1979) byla nová hlavní verze (o čtyři roky později), která zavedla mnoho nových aplikací a vlastností jádra. Významnými změnami prošel také v souvislosti s používáním typového odlitku, spojů a typových ukazatelů na struktury. nicméně kód potrubí prakticky nezměnil. Toto vydání můžeme přeskočit.

Xv6, jednoduché jádro podobné Unixu

K vytvoření jádra Xv6 ovlivněno šestou edicí Unixu, ale napsané v moderním C pro běh na procesorech x86. Kód je snadno čitelný a srozumitelný. Na rozdíl od unixových zdrojů s TUHS jej také můžete zkompilovat, upravit a spustit na něčem jiném než na PDP 11/70. Proto je toto jádro hojně využíváno na univerzitách jako výukový materiál o operačních systémech. Prameny jsou na Github.

Kód obsahuje jasnou a promyšlenou implementaci potrubí.c, zálohovaný vyrovnávací pamětí v paměti namísto inodu na disku. Zde uvádím pouze definici "konstrukčního potrubí" a funkci 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 všech zbývajících implementací, které zahrnují funkce piperead(), pipewrite() и pipeclose(). Skutečné systémové volání sys_pipe je obal implementovaný v sysfile.c. Doporučuji přečíst celý jeho kód. Složitost je na úrovni zdrojového kódu šestého vydání, ale čte se mnohem snadněji a příjemněji.

Linux 0.01

Můžete najít zdrojový kód pro Linux 0.01. Bude poučné studovat realizaci potrubí v jeho fs/pipe.c. Zde je k reprezentaci potrubí použit inode, ale potrubí samotné je napsáno v moderním C. Pokud jste se hackli přes kód šesté edice, nebudete zde mít žádné potíže. Takto funkce vypadá 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;
}

I bez prohlížení definic struktur můžete zjistit, jak se počet referencí inodů používá ke kontrole, zda operace zápisu vede k SIGPIPE. Kromě práce byte po byte je tato funkce snadno srovnatelná s výše uvedenými nápady. Dokonce i logika sleep_on/wake_up nevypadá tak mimozemsky.

Moderní linuxová jádra, FreeBSD, NetBSD, OpenBSD

Rychle jsem prošel některá moderní jádra. Žádný z nich již nemá diskovou implementaci (nepřekvapivé). Linux má svou vlastní implementaci. A přestože tři moderní jádra BSD obsahují implementace založené na kódu, který napsal John Dyson, v průběhu let se od sebe příliš odlišují.

Číst fs/pipe.c (na Linuxu) popř sys/kern/sys_pipe.c (na *BSD), vyžaduje to skutečné odhodlání. Výkon a podpora funkcí, jako jsou vektorové a asynchronní I/O, jsou dnes v kódu důležité. A detaily alokace paměti, zámků a konfigurace jádra se velmi liší. To není to, co univerzity potřebují pro úvodní kurz operačních systémů.

V každém případě pro mě bylo zajímavé odhalit několik starých vzorů (například generování SIGPIPE a zpět EPIPE při zápisu do uzavřeného potrubí) ve všech těchto, tak odlišných, moderních jádrech. Počítač PDP-11 naživo asi nikdy neuvidím, ale z kódu, který byl napsán pár let před mým narozením, je stále co učit.

Napsal Divi Kapoor v roce 2011, článek "Implementace potrubí a FIFO v jádře Linuxuje přehled toho, jak (zatím) fungují linuxové pipeline. A nedávný commit na linuxu ilustruje model potrubí interakce, jehož schopnosti přesahují možnosti dočasných souborů; a také ukazuje, jak daleko se potrubí dostalo od "velmi konzervativního zamykání" v šestém vydání unixového jádra.

Zdroj: www.habr.com

Přidat komentář