A csővezetékek Unixban való megvalósítása

A csővezetékek Unixban való megvalósítása
Ez a cikk a folyamatok megvalósítását írja le a Unix kernelben. Kissé csalódott voltam, hogy egy nemrégiben megjelent cikk a következő címmel jelent meg:Hogyan működnek a csővezetékek Unixban?"kiderült nincs a belső szerkezetről. Kíváncsi lettem, és régi forrásokba ástam a választ.

Miről beszélünk?

A csővezetékek, „valószínűleg a legfontosabb találmány a Unixban”, meghatározó jellemzői a kis programok összekapcsolásának alapjául szolgáló Unix filozófiának, valamint ismerős jel a parancssorban:

$ echo hello | wc -c
6

Ez a funkció a kernel által biztosított rendszerhívástól függ pipe, amely leírása a dokumentációs oldalakon található cső (7) и cső (2):

A csővezetékek egyirányú csatornát biztosítanak a folyamatok közötti kommunikációhoz. A csővezetéknek van bemenete (írási vége) és kimenete (olvasási vége). A csővezeték bemenetére írt adatok a kimeneten olvashatók.

A folyamat a hívás segítségével jön létre pipe(2), amely két fájlleírót ad vissza: az egyik a folyamat bemenetére, a másik a kimenetre vonatkozik.

A fenti parancs nyomkövetési kimenete mutatja a folyamat létrehozását és az azon keresztüli adatáramlást egyik folyamatból a másikba:

$ 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

A szülő folyamat hív pipe()csatolt fájlleírók beszerzéséhez. Egy utódfolyamat az egyik kezelőhöz ír, egy másik folyamat pedig egy másik kezelőből olvassa be ugyanazokat az adatokat. A shell a dup2-t használja a 3. és 4. leíró átnevezéséhez, hogy megfeleljen az stdin és az stdout.

Csövek nélkül a parancsértelmezőnek az egyik folyamat eredményét egy fájlba kellene írnia, és át kellene adnia egy másik folyamatnak, hogy kiolvassa az adatokat a fájlból. Ennek eredményeként több erőforrást és lemezterületet pazarolnánk el. A csővezetékek azonban nem csak azért jók, mert lehetővé teszik az ideiglenes fájlok használatának elkerülését:

Ha egy folyamat egy üres csővezetékből próbál olvasni, akkor read(2) blokkolja, amíg az adatok elérhetővé nem válnak. Ha egy folyamat egy teljes folyamatra próbál írni, akkor write(2) blokkolja, amíg elegendő adatot nem olvasott ki a folyamatból az írás végrehajtásához.

A POSIX-követelményhez hasonlóan ez is egy fontos tulajdonság: írás a folyamatba ig PIPE_BUF bájtoknak (legalább 512) atominak kell lenniük, hogy a folyamatok úgy kommunikálhassanak egymással a csővezetéken keresztül, ahogy a normál fájlok (amelyek nem nyújtanak ilyen garanciákat) nem.

Normál fájl használatakor egy folyamat az összes kimenetét ráírhatja, és továbbadhatja egy másik folyamatnak. Vagy a folyamatok rendkívül párhuzamos üzemmódban is működhetnek, külső jelzőmechanizmus (például szemafor) segítségével értesítik egymást, ha az írás vagy olvasás befejeződött. A szállítószalagok megmentenek minket ettől a sok gondtól.

Mit keresünk?

Egyszerűen elmagyarázom, hogy könnyebben el tudja képzelni, hogyan működik egy szállítószalag. Le kell foglalnia egy puffert és néhány állapotot a memóriában. Funkciókra lesz szüksége adatok hozzáadásához és eltávolításához a pufferből. Szüksége lesz bizonyos eszközökre a függvények meghívásához a fájlleírók olvasási és írási műveletei során. A fent leírt speciális viselkedés megvalósításához pedig zárakra lesz szükség.

Most készen állunk arra, hogy erős lámpafény mellett lekérdezzük a kernel forráskódját, hogy megerősítsük vagy megcáfoljuk homályos mentális modellünket. De mindig készülj fel a váratlanra.

Hol keresünk?

Nem tudom, hol van a híres könyvem példánya "Oroszlán könyv"Unix 6 forráskóddal, de köszönhetően A Unix Örökség Társaság online kereshetsz a címen forráskód még régebbi Unix verziók.

A TUHS archívumában való barangolás olyan, mint egy múzeum látogatása. Megtekinthetjük közös történelmünket, és tisztelettel fejezem ki azt a sok éves erőfeszítést, hogy ezt az anyagot apránként visszanyerjük a régi szalagokról és nyomatokról. És nagyon tisztában vagyok azokkal a töredékekkel, amelyek még mindig hiányoznak.

Miután kielégítettük a szállítószalagok ókori története iránti kíváncsiságunkat, összehasonlításképpen a modern kerneleket tekinthetjük meg.

By the way, pipe a 42-es rendszerhívás a táblázatban sysent[]. Véletlen egybeesés?

Hagyományos Unix kernelek (1970-1974)

Nem találtam nyomokat pipe(2) sem benne PDP-7 Unix (1970. január), sem ben Unix első kiadása (1971. november), sem a hiányos forráskódban második kiadás (1972. június).

A TUHS ezt állítja Unix harmadik kiadása (1973. február) lett az első szállítószalagos változat:

A Unix 1973rd Edition volt az utolsó assembly nyelven írt kernellel rendelkező verzió, de egyben az első csővezetékes verzió is. XNUMX folyamán dolgoztak a harmadik kiadás fejlesztésén, a kernelt átírták C nyelven, így megjelent a Unix negyedik kiadása.

Az egyik olvasó talált egy beszkennelt dokumentumot, amelyben Doug McIlroy a „programok kerti tömlőként való összekapcsolásának” ötletét javasolta.

A csővezetékek Unixban való megvalósítása
Brian Kernighan könyvébenUnix: Történelem és emlékirat", a szállítószalagok megjelenésének történetében ez a dokumentum is szerepel: "... 30 évig lógott a falon a Bell Labs-i irodámban." Itt interjú McIlroy-jal, és egy másik történet innen McIlroy 2014-ben írt munkája:

Amikor megjelent a Unix, a korutinok iránti rajongásom arra késztetett, hogy megkérjem az operációs rendszer szerzőjét, Ken Thompsont, engedje meg, hogy egy folyamatba írt adatok ne csak az eszközre kerüljenek, hanem egy másik folyamathoz is. Ken úgy döntött, hogy lehetséges. Minimalistaként azonban azt akarta, hogy minden rendszerfunkció jelentős szerepet kapjon. Valóban nagy előny a folyamatok közötti közvetlen írás a köztes fájlba való írással szemben? Ken végül csak akkor kiáltott fel, amikor konkrét javaslatot tettem a fülbemászó „pipeline” névvel és a folyamatok közötti interakció szintaxisának leírásával: „Megcsinálom!”

És meg is tette. Egy végzetes estén Ken megváltoztatta a rendszermagot és a shellt, több szabványos programot javított, hogy szabványosítsa a bemenetek fogadását (amelyek csővezetékről származhatnak), és megváltoztatta a fájlneveket is. A következő napon a csővezetékeket nagyon széles körben kezdték használni az alkalmazásokban. A hét végére a titkárok a szövegszerkesztőkből dokumentumokat küldtek a nyomtatóba. Kicsit később Ken lecserélte az eredeti API-t és szintaxist a csővezetékek használatának tisztább konvencióira, amelyeket azóta is használnak.

Sajnos a harmadik kiadású Unix kernel forráskódja elveszett. És bár a kernel forráskódja C-ben van írva negyedik kiadás, 1973 novemberében jelent meg, de több hónappal a hivatalos megjelenés előtt jelent meg, és nem tartalmaz pipeline implementációkat. Kár, hogy ennek a legendás Unix-funkciónak a forráskódja elveszett, talán örökre.

Szöveges dokumentációval rendelkezünk pipe(2) mindkét kiadásból, így a dokumentációban való kereséssel kezdheti harmadik kiadás (bizonyos szavaknál „manuálisan” aláhúzva, literálokból álló ^H karakterlánc, amelyet aláhúzás követ!). Ez a proto-pipe(2) assembly nyelven íródott, és csak egy fájlleírót ad vissza, de már biztosítja az elvárt alapfunkciókat:

Rendszerhívás cső létrehoz egy bemeneti/kimeneti mechanizmust, amelyet pipeline-nek neveznek. A visszaadott fájlleíró használható olvasási és írási műveletekhez. Amikor valamit a folyamatba írunk, legfeljebb 504 bájtnyi adat pufferelődik, majd az írási folyamat felfüggesztésre kerül. A csővezetékből történő olvasáskor a pufferelt adatok eltávolításra kerülnek.

A következő évben a kernelt átírták C nyelven, és pipe(2) a negyedik kiadásban a prototípussal nyerte el modern megjelenését"pipe(fildes)"

Rendszerhívás cső létrehoz egy bemeneti/kimeneti mechanizmust, amelyet pipeline-nek neveznek. A visszaadott fájlleírók olvasási és írási műveletekben használhatók. Amikor valamit írunk a folyamatba, akkor az r1-ben visszaadott kezelő (ill. fildes[1]) kerül felhasználásra, 4096 bájtnyi adatra pufferelve, majd az írási folyamat felfüggesztésre kerül. A csővezetékből történő olvasáskor az r0-ra visszaadott kezelő (illetve fildes[0]) veszi az adatokat.

Feltételezzük, hogy a folyamat definiálása után két (vagy több) kommunikációs folyamat (amelyeket a következő hívások hoznak létre villa) hívások segítségével továbbítja az adatokat a csővezetékről olvas и ír.

A héjnak van egy szintaxisa folyamatok lineáris tömbjének definiálására, amelyeket egy csővezeték köt össze.

Egy üres (pufferelt adatokat nem tartalmazó) csővezetékből történő olvasási hívások, amelyeknek csak az egyik vége van (az írási fájlleírók zárva vannak), a „fájl vége” értéket adják vissza. A hasonló helyzetben történő írásra vonatkozó felhívásokat figyelmen kívül hagyjuk.

Legkorábban konzervált csővezeték megvalósítás utal a Unix ötödik kiadásához (1974. június), de szinte teljesen megegyezik azzal, amely a következő kiadásban jelent meg. Most érkeztek megjegyzések, így kihagyhatod az ötödik kiadást.

A Unix hatodik kiadása (1975)

Kezdjük el olvasni a Unix forráskódot hatodik kiadás (1975. május). Nagyrészt köszönhetően Nevezetességek sokkal könnyebben megtalálható, mint a korábbi verziók forrásai:

Sok éven át a könyv Nevezetességek volt az egyetlen dokumentum a Unix kernelről, amely a Bell Labson kívül elérhető. Bár a hatodik kiadású licenc lehetővé tette a tanárok számára a forráskód használatát, a hetedik kiadású licenc kizárta ezt a lehetőséget, így a könyvet illegális géppel írt példányok formájában terjesztették.

Ma már megvásárolható egy utánnyomás a könyvből, melynek borítóján a diákok másológépnél láthatók. És hála Warren Toomeynek (aki elindította a TUHS projektet) letöltheti PDF fájl forráskóddal a hatodik kiadáshoz. Szeretnék képet adni arról, hogy mennyi erőfeszítésbe került a fájl létrehozása:

Több mint 15 évvel ezelőtt begépeltem a megadott forráskód másolatát Nevezetességek, mert ismeretlen számú más példánytól nem tetszett a másolatom minősége. A TUHS még nem létezett, és nem fértem hozzá a régi forrásokhoz. De 1988-ban találtam egy régi 9 sávos szalagot, amely egy PDP11 számítógépről készült biztonsági másolatot tartalmazott. Nehéz volt megállapítani, hogy működik-e, de volt egy sértetlen /usr/src/ fa, amelyben a legtöbb fájl 1979-es évszámmal volt ellátva, ami még akkor is ősinek tűnt. Ez volt a hetedik kiadás vagy annak származéka, a PWB, ahogy hittem.

A leletet vettem alapul, és manuálisan szerkesztettem a forrásokat a hatodik kiadásig. A kód egy része ugyanaz maradt, de néhányat kissé szerkeszteni kellett, a modern += tokent az elavult =+-ra cserélve. Néhány dolgot egyszerűen töröltek, néhányat pedig teljesen át kellett írni, de nem túl sokat.

Ma pedig online olvashatjuk a TUHS-en a hatodik kiadás forráskódját archívum, amelyhez Dennis Ritchie keze volt.

Mellesleg, első pillantásra a Kernighan és Ritchie időszaka előtti C-kód fő jellemzője az tömörség. Nem gyakran fordul elő, hogy részletes szerkesztés nélkül tudok kódrészleteket beszúrni, hogy elférjen a webhelyem viszonylag szűk megjelenítési területe.

Korai /usr/sys/ken/pipe.c van egy magyarázó megjegyzés (és igen, van még /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

A puffer mérete nem változott a negyedik kiadás óta. De itt látjuk, minden nyilvános dokumentáció nélkül, hogy a csővezetékek egykor fájlokat használtak biztonsági mentési tárolóként!

Ami a LARG fájlokat illeti, ezek megfelelnek a inode flag NAGY, amelyet a "nagy címzési algoritmus" használ a feldolgozásra közvetett blokkok nagyobb fájlrendszerek támogatására. Mivel Ken azt mondta, jobb, ha nem használja őket, boldogan fogadom a szavát.

Itt az igazi rendszerhívás 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;
}

A komment egyértelműen leírja, mi folyik itt. De a kód megértése nem olyan egyszerű, részben az út miatt."struct felhasználó u» és nyilvántartások R0 и R1 rendszerhívási paraméterek és visszatérési értékek kerülnek átadásra.

Próbáljuk meg ialloc() lemezre tenni inode (index fogantyú), és a segítségével falloc() - helyezzen el kettőt a memóriában fájlt. Ha minden jól megy, zászlókat állítunk be, amelyek a folyamat két végeként azonosítják ezeket a fájlokat, ugyanarra az inode-ra irányítjuk őket (amelynek hivatkozási száma 2 lesz), és az inode-ot módosítottként és használatban lévőként jelöljük meg. Ügyeljen a kérésekre Teszek() hibaútvonalakban, hogy csökkentse a referenciaszámot az új inoodban.

pipe() keresztül kell mennie R0 и R1 visszaadja a fájlleíró számokat olvasáshoz és íráshoz. falloc() mutatót ad vissza a fájlszerkezetre, de "visszaadja" a via u.u_ar0[R0] és egy fájlleíró. Vagyis a kód elmentődik r fájlleíró az olvasáshoz, és hozzárendel egy fájlleírót a közvetlen íráshoz u.u_ar0[R0] a második hívás után falloc().

zászló FPIPE, amelyet a folyamat létrehozásakor állítunk be, szabályozza a függvény viselkedését rdwr() a sys2.c-benmeghatározott I/O rutinok hívása:

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

Aztán a függvény readp() в pipe.c adatokat olvas be a csővezetékből. De jobb, ha nyomon követi a megvalósítást, kezdve writep(). A kód ismét összetettebbé vált az argumentumok átadásának konvenciói miatt, de néhány részlet elhagyható.

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

Bájtokat akarunk írni a csővezeték bemenetére u.u_count. Először zárolnunk kell az inódot (lásd alább plock/prele).

Ezután ellenőrizzük az inode referenciaszámlálót. Amíg a csővezeték mindkét vége nyitva marad, a számlálónak egyenlőnek kell lennie 2-vel. Egy linket tartunk (a rp->f_inode), tehát ha a számláló 2-nél kisebb, az azt jelenti, hogy az olvasási folyamat lezárta a csővezeték végét. Más szóval, egy zárt csővezetékre próbálunk írni, és ez hiba. Első hibakód EPIPE és jelezze SIGPIPE a Unix hatodik kiadásában jelent meg.

De még akkor is, ha a szállítószalag nyitva van, tele lehet. Ebben az esetben feloldjuk a zárat és aludni megyünk abban a reményben, hogy egy másik folyamat kiolvas a csővezetékből, és elegendő helyet szabadít fel benne. Miután felébredtünk, visszatérünk az elejére, ismét leteszzük a zárat és új felvételi ciklusba kezdünk.

Ha van elég szabad hely a folyamatban, akkor a segítségével adatokat írunk rá writei()... Paraméter i_size1 az inode-nál (ha a folyamat üres, akkor egyenlő lehet 0-val) jelzi a már benne lévő adatok végét. Ha van elegendő felvételi hely, akkor a csővezetéket meg tudjuk tölteni i_size1 a PIPESIZ. Ezután feloldjuk a zárat, és megpróbálunk felébreszteni minden olyan folyamatot, amely arra vár, hogy olvassa a folyamatot. Visszamegyünk az elejére, hogy megnézzük, képesek vagyunk-e annyi bájtot írni, amennyire szükségünk van. Ha nem sikerül, akkor új felvételi ciklust kezdünk.

Általában a paraméter i_mode Az inode az engedélyek tárolására szolgál r, w и x. A csővezetékek esetében azonban bitek segítségével jelezzük, hogy valamilyen folyamat írásra vagy olvasásra vár IREAD и IWRITE illetőleg. A folyamat beállítja a zászlót és hív sleep(), és várhatóan a jövőben valamilyen más folyamat fog okozni wakeup().

Az igazi varázslat benne történik sleep() и wakeup(). ben valósulnak meg slp.c, a híres „Nem várható el, hogy ezt megértsd” megjegyzés forrása. Szerencsére nem kell értenünk a kódot, csak nézzünk meg néhány megjegyzést:

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

A folyamat, ami okozza sleep() egy adott csatorna esetében később egy másik folyamat felébresztheti, ami azt okozza wakeup() ugyanarra a csatornára. writep() и readp() összehangolni tevékenységeiket ilyen páros hívásokon keresztül. vegye figyelembe, hogy pipe.c mindig elsőbbséget ad PPIPE amikor hívják sleep(), Szóval ennyi sleep() jelzés megszakíthatja.

Most már minden megvan a funkció megértéséhez 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);
}

Lehet, hogy könnyebben elolvashatja ezt a funkciót alulról felfelé. Az "olvasás és visszaküldés" ágat általában akkor használják, ha van néhány adat a folyamatban. Ebben az esetben használjuk olvas() annyi adatot olvasunk ki, amennyi elérhető az aktuálistól kezdve f_offset leolvasását, majd frissítse a megfelelő eltolás értékét.

A következő olvasásoknál a folyamat üres lesz, ha az olvasási eltolás elérte i_size1 inode-nál. Visszaállítjuk a pozíciót 0-ra, és megpróbálunk felébreszteni minden olyan folyamatot, amely írni akar a folyamatba. Tudjuk, hogy amikor a szállítószalag megtelik, writep() elalszik tovább ip+1. És most, hogy a folyamat üres, felébreszthetjük az írási ciklus folytatásához.

Ha nincs mit olvasni, akkor readp() zászlót állíthat fel IREAD és aludj tovább ip+2. Tudjuk, mi ébreszti fel writep(), amikor néhány adatot ír a folyamatba.

Megjegyzések a readi() és writei() segít megérteni, hogy ahelyett, hogy a paramétereket a "u"Kezelhetjük őket normál I/O függvényekként, amelyek egy fájlt, egy pozíciót, egy puffert vesznek fel a memóriában, és megszámolják az olvasandó vagy írandó bájtok számát.

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

Ami tehát a „konzervatív” blokkolást illeti readp() и writep() blokkolja az inodot, amíg be nem fejezik a munkájukat vagy eredményt nem kapnak (vagyis hívják wakeup). plock() и prele() egyszerűen működik: más híváskészlettel sleep и wakeup lehetővé teszi számunkra, hogy felébresszünk minden olyan folyamatot, amelyhez az imént feloldott zárra van szükség:

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

Először nem értettem, miért readp() nem okoz prele(ip) a hívás előtt wakeup(ip+1). Az első dolog az writep() ciklusában okozza ezt plock(ip), ami holtponthoz vezet, ha readp() még nem távolítottam el a blokkolásomat, így valahogy a kódnak megfelelően kell működnie. Ha megnézed wakeup(), akkor világossá válik, hogy csak az alvási folyamatot jelöli meg végrehajtásra késznek, így a jövőben sched() tényleg elindította. Így readp() okai wakeup(), eltávolítja a zárat, beállítja IREAD és hív sleep(ip+2)- mindezt korábban writep() folytatja a ciklust.

Ezzel befejeződik a szállítószalagok leírása a hatodik kiadásban. Egyszerű kód, messzemenő következmények.

A Unix hetedik kiadása (1979. január) egy új nagy kiadás volt (négy évvel később), amely számos új alkalmazást és kernelfunkciót vezetett be. Jelentős változásokon ment keresztül a típusöntvények, az uniók és a szerkezetekre mutató tipizált mutatók használata kapcsán is. azonban szállítószalag kódja gyakorlatilag változatlan. Ezt a kiadást kihagyhatjuk.

Xv6, egy egyszerű Unix-szerű kernel

A kernel létrehozásához Xv6 a Unix hatodik kiadása befolyásolta, de modern C-ben írták, hogy x86-os processzorokon fusson. A kód könnyen olvasható és érthető. Ráadásul a TUHS-t tartalmazó Unix-forrásokkal ellentétben lefordíthatja, módosíthatja, és nem PDP 11/70-en futtathatja. Ezért ezt a kernelt széles körben használják az egyetemeken operációs rendszerek oktatási anyagaként. Források a Githubon vannak.

A kód világos és átgondolt megvalósítást tartalmaz cső.c, amelyet egy puffer a memóriában támogat a lemez inode helyett. Itt csak a "szerkezeti csővezeték" és a funkció definícióját adom meg 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() beállítja a többi megvalósítás állapotát, amely magában foglalja a funkciókat is piperead(), pipewrite() и pipeclose(). Valós rendszerhívás sys_pipe -ban implementált burkoló sysfile.c. Javaslom elolvasni a teljes kódját. A bonyolultság a hatodik kiadás forráskódjának szintjén van, de sokkal könnyebben és élvezetesebben olvasható.

Linux 0.01

Linux 0.01 forráskód található. Tanulságos lesz tanulmányozni a csővezetékek megvalósítását az övében fs/pipe.c. Ez egy inode-ot használ a folyamat ábrázolására, de maga a folyamat a modern C-ben van írva. Ha végigdolgoztad a 6. kiadás kódját, itt nem lesz gond. Így néz ki a függvény 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;
}

A struktúra definíciók megtekintése nélkül is kitalálhatja, hogy az inode hivatkozási szám hogyan használható annak ellenőrzésére, hogy egy írási művelet eredményeként SIGPIPE. Amellett, hogy bájtonként működik, ez a funkció könnyen összehasonlítható a fent leírt ötletekkel. Még a logika is sleep_on/wake_up nem tűnik olyan idegennek.

Modern Linux kernelek, FreeBSD, NetBSD, OpenBSD

Gyorsan átfutottam néhány modern kernelen. Egyiknek sincs már lemezmegvalósítása (nem meglepő). A Linuxnak megvan a maga megvalósítása. Bár a három modern BSD kernel tartalmaz olyan implementációkat, amelyek John Dyson által írt kódon alapulnak, az évek során túlságosan különböztek egymástól.

Olvasni fs/pipe.c (Linuxon) ill sys/kern/sys_pipe.c (*BSD-n), valódi odaadást igényel. A mai kód a teljesítményről és az olyan funkciók támogatásáról szól, mint a vektoros és az aszinkron I/O. A memóriafoglalás, a zárolások és a kernelkonfiguráció részletei pedig nagyon eltérőek. A főiskoláknak nem erre van szükségük egy bevezető operációs rendszer-tanfolyamhoz.

Mindenesetre érdekelt néhány régi minta előásása (például generálás SIGPIPE és vissza EPIPE amikor zárt csővezetékre írunk) mindezekben a különböző modern kernelekben. Valószínűleg soha nem fogok látni PDP-11 számítógépet a valóságban, de még mindig sokat kell tanulnom abból a kódból, amelyet évekkel a születésem előtt írtak.

Divi Kapoor 2011-ben írt cikke:A csövek és FIFO-k Linux kernel megvalósítása" áttekintést nyújt arról, hogyan működnek (még mindig) a folyamatok Linux alatt. A legutóbbi véglegesítés Linux alatt szemlélteti az interakció folyamatmodelljét, amelynek képességei meghaladják az ideiglenes fájlok képességeit; és azt is megmutatja, milyen messzire jutottak el a csővezetékek a hatodik kiadású Unix kernel "nagyon konzervatív zárolásától".

Forrás: will.com

Hozzászólás