Kuinka liukuhihnat toteutetaan Unixissa

Kuinka liukuhihnat toteutetaan Unixissa
Tässä artikkelissa kuvataan liukuputkien käyttöönottoa Unix-ytimessä. Olin hieman pettynyt äskettäin julkaistuun artikkeliin "Miten putket toimivat Unixissa?" osoittautui ei sisäisestä rakenteesta. Innostuin ja kaivauduin vanhoihin lähteisiin löytääkseni vastauksen.

Mistä me puhumme?

Putkilinjat ovat "luultavasti tärkein keksintö Unixissa" - Unixin perustavanlaatuisen pienten ohjelmien kokoamisfilosofian ja tutun komentorivin iskulauseen määrittelevä piirre:

$ echo hello | wc -c
6

Tämä toiminto riippuu ytimen toimittamasta järjestelmäkutsusta pipe, joka on kuvattu dokumentaatiosivuilla putki (7) и putki (2):

Putkilinjat tarjoavat yksisuuntaisen kanavan prosessien väliseen viestintään. Liukuhihnalla on tulo (kirjoituspää) ja lähtö (lukupää). Liukuhihnan tuloon kirjoitetut tiedot voidaan lukea lähdöstä.

Liukulinja luodaan soittamalla pipe(2), joka palauttaa kaksi tiedostokuvaajaa: yksi viittaa liukuhihnan tuloon ja toinen tulosteeseen.

Yllä olevan komennon jäljitystulos näyttää liukuhihnan luomisen ja tietovirran sen läpi prosessista toiseen:

$ 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

Vanhempi prosessi kutsuu pipe()saadaksesi liitetiedostojen kuvaukset. Yksi lapsiprosessi kirjoittaa yhdelle kuvaajalle ja toinen prosessi lukee saman datan toisesta kuvaajasta. Kuori "nimeää uudelleen" kuvailevat 2 ja 3 dup4:lla vastaamaan stdin ja stdout.

Ilman liukuhihnaa komentotulkin pitäisi kirjoittaa yhden prosessin tulos tiedostoon ja siirtää se toiseen prosessiin lukeakseen tiedot tiedostosta. Tämän seurauksena tuhlaamme enemmän resursseja ja levytilaa. Liukuputket ovat kuitenkin hyviä muuhunkin kuin vain väliaikaisten tiedostojen välttämiseen:

Jos prosessi yrittää lukea tyhjästä liukuhihnasta, niin read(2) estetään, kunnes tiedot ovat saatavilla. Jos prosessi yrittää kirjoittaa koko liukuhihnalle, niin write(2) estetään, kunnes liukuhihnasta on luettu tarpeeksi dataa kirjoittamisen suorittamiseksi loppuun.

Kuten POSIX-vaatimus, tämä on tärkeä ominaisuus: kirjoittaminen liukuhihnaan asti PIPE_BUF tavujen (vähintään 512) on oltava atomisia, jotta prosessit voivat kommunikoida toistensa kanssa liukuhihnan kautta tavalla, jota tavalliset tiedostot (jotka eivät tarjoa tällaisia ​​takuita) eivät voi.

Tavallisella tiedostolla prosessi voi kirjoittaa kaiken tuotoksensa siihen ja välittää sen toiselle prosessille. Tai prosessit voivat toimia kovassa rinnakkaistilassa käyttämällä ulkoista signalointimekanismia (kuten semaforia) ilmoittamaan toisilleen kirjoittamisen tai lukemisen valmistumisesta. Kuljettimet säästävät meidät kaikelta tältä vaivalta.

Mitä me etsimme?

Selitän sormillani, jotta sinun olisi helpompi kuvitella, kuinka kuljetin voi toimia. Sinun on varattava puskuri ja jokin tila muistista. Tarvitset toimintoja tietojen lisäämiseen ja poistamiseen puskurista. Tarvitset jonkin toiminnon kutsuaksesi toimintoja tiedostokuvaajien luku- ja kirjoitustoimintojen aikana. Ja lukot tarvitaan toteuttamaan yllä kuvattu erityiskäyttäytyminen.

Olemme nyt valmiita tutkimaan ytimen lähdekoodia kirkkaassa lampunvalossa vahvistaaksemme tai kumotaksemme epämääräisen mentaalimallimme. Mutta ole aina valmis odottamattomiin.

Mistä me etsimme?

En tiedä missä kopioni kuuluisasta kirjasta on.Lionit kirja« Unix 6 lähdekoodilla, mutta kiitos Unix Heritage Society voi etsiä netistä lähdekoodi jopa vanhempia Unix-versioita.

TUHS:n arkistossa vaeltaminen on kuin museossa käyntiä. Voimme katsoa yhteistä historiaamme ja kunnioitan vuosien ponnisteluja saada kaikki tämä materiaali pala kerrallaan talteen vanhoista kasetteista ja tulosteista. Ja olen erittäin tietoinen niistä fragmenteista, jotka edelleen puuttuvat.

Kun olemme tyydyttäneet uteliaisuutemme putkilinjojen muinaisesta historiasta, voimme tarkastella nykyaikaisia ​​ytimiä vertailun vuoksi.

muuten, pipe on järjestelmäkutsunumero 42 taulukossa sysent[]. Yhteensattuma?

Perinteiset Unix-ytimet (1970–1974)

En löytänyt jälkeäkään pipe(2) ei sisälläkään PDP-7 Unix (tammikuu 1970), eikä vuonna ensimmäinen painos Unix (marraskuu 1971), eikä epätäydellinen lähdekoodi toinen painos (kesäkuu 1972).

TUHS väittää näin kolmas painos Unix (helmikuu 1973) oli ensimmäinen versio, jossa oli putkisto:

Unixin kolmas painos oli viimeinen versio, jossa oli assemblerillä kirjoitettu ydin, mutta myös ensimmäinen liukuhihnalla varustettu versio. Vuoden 1973 aikana työskenneltiin kolmannen painoksen parantamiseksi, ydin kirjoitettiin uudelleen C-kielellä ja näin syntyi Unixin neljäs painos.

Yksi lukija löysi skannauksen asiakirjasta, jossa Doug McIlroy ehdotti ajatusta "ohjelmien yhdistämisestä puutarhaletkun tapaan".

Kuinka liukuhihnat toteutetaan Unixissa
Brian Kernighanin kirjassaUnix: Historia ja muistelmat", kuljettimien ilmestymisen historia mainitsee myös tämän asiakirjan: "...se roikkui seinällä toimistossani Bell Labsissa 30 vuotta." Tässä haastattelu McIlroyn kanssaja toinen tarina sieltä McIlroyn vuonna 2014 kirjoitettu teos:

Kun Unix ilmestyi, intohimoni korutiineihin sai minut pyytämään käyttöjärjestelmän kirjoittajaa Ken Thompsonia sallimaan johonkin prosessiin kirjoitetun datan siirtyä laitteeseen, mutta myös poistumiseen toisesta prosessista. Ken piti sitä mahdollisena. Minimalistina hän kuitenkin halusi jokaisen järjestelmän ominaisuuden olevan tärkeässä roolissa. Onko suora kirjoittaminen prosessien välillä todella suuri etu verrattuna välitiedostoon kirjoittamiseen? Ja vasta kun tein erityisen ehdotuksen tarttuvalla nimellä "pipeline" ja kuvauksella prosessien vuorovaikutuksen syntaksista, Ken lopulta huudahti: "Minä teen sen!".

Ja teki. Eräänä kohtalokkaana iltana Ken vaihtoi ytimen ja komentotulkin, korjasi useita vakioohjelmia standardoidakseen syötteiden hyväksymisen (joka saattaa tulla putkistosta) ja muutti tiedostonimiä. Seuraavana päivänä putkia käytettiin erittäin laajasti sovelluksissa. Viikon loppuun mennessä sihteerit lähettivät niillä asiakirjoja tekstinkäsittelyohjelmista tulostimeen. Jonkin verran myöhemmin Ken korvasi alkuperäisen API:n ja syntaksin putkien käytön käärimiseksi puhtaammilla käytännöillä, joita on käytetty siitä lähtien.

Valitettavasti kolmannen painoksen Unix-ytimen lähdekoodi on kadonnut. Ja vaikka meillä on ytimen lähdekoodi kirjoitettu C-kielellä neljäs painos, joka julkaistiin marraskuussa 1973, mutta se ilmestyi muutama kuukausi ennen virallista julkaisua eikä sisällä putkien toteutusta. On sääli, että tämän legendaarisen Unix-ominaisuuden lähdekoodi on kadonnut, ehkä ikuisesti.

Meillä on dokumentaatioteksti pipe(2) molemmista julkaisuista, joten voit aloittaa hakemalla dokumentaatiosta kolmas painos (tietyille sanoille alleviivattu "manuaalisesti", ^H-literaalien merkkijono, jota seuraa alaviiva!). Tämä proto-pipe(2) on kirjoitettu assemblerissä ja palauttaa vain yhden tiedostokuvaajan, mutta tarjoaa jo odotetun ydintoiminnon:

Järjestelmäpuhelu putki luo I/O-mekanismin, jota kutsutaan liukuhihnaksi. Palautettua tiedostokuvaajaa voidaan käyttää luku- ja kirjoitustoimintoihin. Kun liukuhihnaan kirjoitetaan jotain, se puskuroi jopa 504 tavua dataa, minkä jälkeen kirjoitusprosessi keskeytyy. Liukuhihnasta luettaessa puskuroitu data otetaan.

Seuraavaan vuoteen mennessä ydin oli kirjoitettu uudelleen C-kielellä ja pipe(2) neljäs painos sai modernin ilmeensä prototyypin avulla "pipe(fildes)»:

Järjestelmäpuhelu putki luo I/O-mekanismin, jota kutsutaan liukuhihnaksi. Palautettuja tiedostokuvauksia voidaan käyttää luku- ja kirjoitusoperaatioissa. Kun liukuhihnaan kirjoitetaan jotain, käytetään r1:ssä palautettua kuvaajaa (vastaa fildes[1]), puskuroituna 4096 tavuun dataa, minkä jälkeen kirjoitusprosessi keskeytyy. Liukuhihnasta luettaessa arvoon r0 palautettu kuvaaja (vastaavasti fildes[0]) ottaa tiedot.

Oletetaan, että kun liukuhihna on määritelty, kaksi (tai useampia) vuorovaikutuksessa olevaa prosessia (jotka luovat myöhemmillä kutsuilla haarukka) välittää dataa putkesta puheluiden avulla luettu и kirjoittaa.

Kuoressa on syntaksi, jolla määritellään lineaarinen joukko prosesseja, jotka on yhdistetty liukuhihnan kautta.

Kutsut lukemaan tyhjästä liukuhihnasta (jossa ei ole puskuroitua dataa), jolla on vain yksi pää (kaikki kirjoitustiedoston kuvaajat suljettuina), palauttavat "tiedoston lopun". Kirjoituspuhelut vastaavassa tilanteessa ohitetaan.

Varhaisin säilytetty putkilinjan toteutus sovelletaan Unixin viidenteen painokseen (kesäkuu 1974), mutta se on melkein identtinen seuraavassa julkaisussa ilmestyneen kanssa. Lisätty vain kommentteja, joten viides painos voidaan ohittaa.

Unix Sixth Edition (1975)

Unix-lähdekoodin lukeminen alkaa kuudes painos (toukokuu 1975). Suurelta osin kiitos Lions se on paljon helpompi löytää kuin aikaisempien versioiden lähteet:

Kirja monta vuotta Lions oli ainoa Unix-ytimen asiakirja, joka oli saatavilla Bell Labsin ulkopuolella. Vaikka kuudennen painoksen lisenssi antoi opettajille mahdollisuuden käyttää sen lähdekoodia, seitsemännen painoksen lisenssi sulki tämän mahdollisuuden pois, joten kirjaa jaettiin laittomina koneella kirjoitettuina kopioina.

Tänään voit ostaa kirjan uusintapainoksen, jonka kannessa on kopiokoneen opiskelijat. Ja kiitos Warren Toomeyn (joka aloitti TUHS-projektin), voit ladata Kuudes painos Lähde PDF. Haluan antaa sinulle käsityksen siitä, kuinka paljon vaivaa tiedoston luomiseen käytettiin:

Yli 15 vuotta sitten kirjoitin kopion mukana tulevasta lähdekoodista Lionskoska en pitänyt kopioni laadusta tuntemattomasta määrästä muita kopioita. TUHS:ää ei ollut vielä olemassa, enkä päässyt vanhoihin lähteisiin. Mutta vuonna 1988 löysin vanhan nauhan, jossa oli 9 raitaa ja jolla oli varmuuskopio PDP11-tietokoneesta. Oli vaikea tietää, toimiko se, mutta siellä oli ehjä /usr/src/-puu, jossa suurin osa tiedostoista oli merkitty 1979, joka näytti silloinkin vanhalta. Se oli seitsemäs painos tai PWB-johdannainen, ajattelin.

Otin löydön pohjaksi ja muokkasin lähteet manuaalisesti kuudennen painoksen tilaan. Osa koodista pysyi ennallaan, osaa jouduttiin hieman muokkaamaan, muuttamalla moderni merkki += vanhentuneeksi =+. Jotain yksinkertaisesti poistettiin, ja jotain piti kirjoittaa kokonaan uudelleen, mutta ei liikaa.

Ja tänään voimme lukea verkossa TUHS:sta kuudennen painoksen lähdekoodia arkisto, jota Dennis Ritchie käsitti.

Muuten, ensi silmäyksellä C-koodin pääominaisuus ennen Kernighanin ja Ritchien jaksoa on sen lyhyys. Harvoin pystyn lisäämään koodinpätkiä ilman laajaa muokkausta, jotta ne mahtuvat sivustoni suhteellisen kapealle näyttöalueelle.

Varhainen /usr/sys/ken/pipe.c on selittävä kommentti (ja kyllä, on enemmän /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

Puskurin koko ei ole muuttunut neljännen painoksen jälkeen. Mutta tässä näemme, ilman mitään julkista dokumentaatiota, että liukuhihnat käyttivät aikoinaan tiedostoja varamuistina!

Mitä tulee LARG-tiedostoihin, ne vastaavat inode-lippu LARG, jota "suuri osoitusalgoritmi" käyttää käsittelyyn epäsuorat lohkot tukemaan suurempia tiedostojärjestelmiä. Koska Ken sanoi, että on parempi olla käyttämättä niitä, olen iloinen voidessani hyväksyä hänen sanansa.

Tässä on todellinen järjestelmäkutsu 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;
}

Kommentti kuvaa selvästi mitä täällä tapahtuu. Mutta koodin ymmärtäminen ei ole niin helppoa, osittain siksi, että "struct käyttäjä u» ja rekisterit R0 и R1 järjestelmäkutsuparametrit ja palautusarvot välitetään.

Kokeillaan kanssa ialloc() aseta levylle inode (inode), ja avustuksella falloc() - säilytä kaksi tiedosto. Jos kaikki menee hyvin, asetamme liput tunnistamaan nämä tiedostot liukuhihnan kahdeksi päksi, osoitamme ne samaan inodiin (jonka viitemääräksi tulee 2) ja merkitsemme inodin muokatuksi ja käytössä. Kiinnitä huomiota pyyntöihin panen() virhepoluissa viitemäärän pienentämiseksi uudessa inodessa.

pipe() kautta R0 и R1 palauttaa tiedostokuvausnumerot lukemista ja kirjoittamista varten. falloc() palauttaa osoittimen tiedostorakenteeseen, mutta myös "palauttaa" kautta u.u_ar0[R0] ja tiedostokuvaaja. Eli koodi on tallennettu r tiedostokuvaaja lukemista varten ja määrittää kuvauksen suoraan kirjoittamista varten u.u_ar0[R0] toisen puhelun jälkeen falloc().

lippu FPIPE, jonka asetimme liukuhihnaa luotaessa, ohjaa funktion toimintaa rdwr() sys2.c:ssä, joka kutsuu tiettyjä I/O-rutiineja:

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

Sitten toiminto readp() в pipe.c lukee dataa putkistosta. Mutta on parempi seurata toteutusta alkaen writep(). Koodi on jälleen monimutkaistunut argumentin läpivientikäytännön luonteen vuoksi, mutta joitain yksityiskohtia voidaan jättää pois.

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

Haluamme kirjoittaa tavuja liukuhihnan tuloon u.u_count. Ensin meidän on lukittava inode (katso alla plock/prele).

Sitten tarkistamme inodien viitemäärän. Niin kauan kuin putken molemmat päät ovat auki, laskurin tulee olla 2. Pidämme kiinni yhdestä linkistä (alkaen rp->f_inode), joten jos laskuri on pienempi kuin 2, tämän pitäisi tarkoittaa, että lukuprosessi on sulkenut liukuhihnan pään. Toisin sanoen yritämme kirjoittaa suljettuun liukuhihnaan, mikä on virhe. Ensimmäinen virhekoodi EPIPE ja signaali SIGPIPE ilmestyi Unixin kuudennessa painoksessa.

Mutta vaikka kuljetin olisi auki, se voi olla täynnä. Tässä tapauksessa vapautamme lukon ja menemme nukkumaan siinä toivossa, että toinen prosessi lukee putkistosta ja vapauttaa siihen tarpeeksi tilaa. Kun heräämme, palaamme alkuun, suljemme lukon uudelleen ja aloitamme uuden kirjoitussyklin.

Jos liukuhihnassa on tarpeeksi vapaata tilaa, kirjoitamme siihen dataa käyttämällä kirjoittaa()... Parametri i_size1 inode'a (tyhjällä liukuhihnalla voi olla 0) osoittaa sen jo sisältämien tietojen loppuun. Jos kirjoitustilaa on tarpeeksi, voimme täyttää putkilinjan i_size1 до PIPESIZ. Sitten vapautamme lukon ja yritämme herättää kaikki prosessit, jotka odottavat lukemistaan ​​liukuhihnasta. Palaamme alkuun nähdäksemme, onnistuimmeko kirjoittamaan niin monta tavua kuin tarvitsimme. Jos ei, aloitamme uuden tallennusjakson.

Yleensä parametri i_mode inodea käytetään käyttöoikeuksien tallentamiseen r, w и x. Mutta liukuputkien tapauksessa annamme signaalin, että jokin prosessi odottaa kirjoitusta tai lukua bittien avulla IREAD и IWRITE vastaavasti. Prosessi asettaa lipun ja kutsuu sleep(), ja on odotettavissa, että tulevaisuudessa jokin muu prosessi vaatii wakeup().

Todellinen taika tapahtuu sisällä sleep() и wakeup(). Ne toteutetaan vuonna slp.c, kuuluisan "Sinun ei odoteta ymmärtävän tätä" -kommentin lähde. Onneksi meidän ei tarvitse ymmärtää koodia, katso vain joitain kommentteja:

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

Prosessi, joka kutsuu sleep() tietylle kanavalle, voidaan myöhemmin herättää toisella prosessilla, joka kutsuu wakeup() samalle kanavalle. writep() и readp() koordinoi toimintaansa tällaisten puhelujen avulla. ota huomioon, että pipe.c aina priorisoida PPIPE kun soitetaan sleep(), siis kaikki sleep() voidaan katkaista signaalilla.

Nyt meillä on kaikki toiminnon ymmärtämiseksi 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);
}

Saatat olla helpompi lukea tämä toiminto alhaalta ylös. "Lue ja palauta" -haaraa käytetään yleensä, kun dataa on käsittelyssä. Tässä tapauksessa käytämme lukea() lue niin paljon tietoa kuin on saatavilla nykyisestä alkaen f_offset lue ja päivitä sitten vastaavan siirtymän arvo.

Seuraavissa lukemissa liukuhihna on tyhjä, jos lukupoikkeama on saavutettu i_size1 inodissa. Nollaamme sijainnin 0:ksi ja yritämme herättää kaikki prosessit, jotka haluavat kirjoittaa liukuhihnaan. Tiedämme, että kun kuljetin on täynnä, writep() nukahtaa päälle ip+1. Ja nyt, kun liukuhihna on tyhjä, voimme herättää sen jatkaaksemme kirjoitusjaksoaan.

Jos ei ole mitään luettavaa, niin readp() voi asettaa lipun IREAD ja nukahtaa ip+2. Tiedämme, mikä hänet herättää writep()kun se kirjoittaa tietoja liukuhihnaan.

Kommentteja lue() ja kirjoita() auttaa sinua ymmärtämään, että parametrien välittämisen sijaan "u» voimme käsitellä niitä kuten tavallisia I/O-toimintoja, jotka ottavat tiedoston, sijainnin, puskurin muistissa ja laskevat luettavien tai kirjoitettavien tavujen määrän.

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

Mitä tulee "konservatiiviseen" estoon readp() и writep() lukitse inodit, kunnes ne ovat valmiit tai saavat tuloksen (eli soita wakeup). plock() и prele() toimii yksinkertaisesti: käyttämällä erilaista puhelusarjaa sleep и wakeup anna meidän herättää kaikki prosessit, jotka tarvitsevat juuri vapauttamamme lukon:

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

Aluksi en ymmärtänyt miksi readp() ei aiheuta prele(ip) ennen puhelua wakeup(ip+1). Ensimmäinen asia writep() kutsuu silmukassaan, tämä plock(ip), mikä johtaa lukkiutumiseen, jos readp() ei ole vielä poistanut estoaan, joten koodin on toimittava jotenkin oikein. Jos katsot wakeup(), käy selväksi, että se merkitsee vain nukkumisprosessin valmiiksi suoritettaviksi, jotta tulevaisuudessa sched() todella käynnisti sen. Niin readp() syyt wakeup(), avaa, asettaa IREAD ja puhelut sleep(ip+2)- kaikki tämä ennen writep() käynnistää syklin uudelleen.

Tämä täydentää putkilinjojen kuvauksen kuudennessa painoksessa. Yksinkertainen koodi, kauaskantoiset vaikutukset.

Seitsemäs painos Unix (tammikuu 1979) oli uusi suuri julkaisu (neljä vuotta myöhemmin), joka esitteli monia uusia sovelluksia ja ydinominaisuuksia. Se on myös kokenut merkittäviä muutoksia tyyppivalujen, liitosten ja tyyppiosoittimien käytön yhteydessä rakenteisiin. kuitenkin putkien koodi käytännössä ei muuttunut. Voimme ohittaa tämän painoksen.

Xv6, yksinkertainen Unix-tyyppinen ydin

Ytimen luomiseksi Xv6 vaikutteita Unixin kuudennesta painoksesta, mutta kirjoitettu modernilla C-kielellä toimimaan x86-prosessoreilla. Koodi on helppolukuinen ja ymmärrettävä. Lisäksi, toisin kuin Unix-lähteet, joissa on TUHS, voit kääntää sen, muokata sitä ja käyttää sitä jollain muulla kuin PDP 11/70:llä. Siksi tätä ydintä käytetään laajasti yliopistoissa käyttöjärjestelmien opetusmateriaalina. Lähteet ovat Githubissa.

Koodi sisältää selkeän ja harkitun toteutuksen putki.c, jota tukee puskuri muistissa levyn inodin sijaan. Tässä annan vain määritelmän "rakenteelliselle putkilinjalle" ja funktiolle 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() määrittää kaiken muun toteutuksen tilan, joka sisältää toimintoja piperead(), pipewrite() и pipeclose(). Varsinainen järjestelmäkutsu sys_pipe on kääre, joka on toteutettu sysfile.c. Suosittelen lukemaan hänen koko koodinsa. Monimutkaisuus on kuudennen painoksen lähdekoodin tasolla, mutta sitä on paljon helpompi ja miellyttävämpi lukea.

Linux 0.01

Löydät lähdekoodin Linux 0.01:lle. On opettavaista tutkia putkistojen toteutusta hänen fs/pipe.c. Tässä inodea käytetään edustamaan liukuhihnaa, mutta itse liukuhihna on kirjoitettu nykyaikaisella C-kielellä. Jos olet murtautunut kuudennen painoksen koodin läpi, sinulla ei ole ongelmia tässä. Tältä toiminto näyttää 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;
}

Jopa katsomatta struct-määrityksiä, voit selvittää, kuinka inode-viittausmäärää käytetään tarkistamaan, johtaako kirjoitustoiminto SIGPIPE. Tavu-byte-työn lisäksi tätä toimintoa on helppo verrata yllä oleviin ideoihin. Jopa logiikka sleep_on/wake_up ei näytä niin vieraalta.

Nykyaikaiset Linux-ytimet, FreeBSD, NetBSD, OpenBSD

Kävin nopeasti läpi joitakin moderneja ytimiä. Yhdelläkään niistä ei ole jo levypohjaista toteutusta (ei yllättävää). Linuxilla on oma toteutus. Ja vaikka kolme modernia BSD-ydintä sisältävät toteutuksia, jotka perustuvat John Dysonin kirjoittamaan koodiin, ne ovat vuosien mittaan tulleet liian erilaisiksi toisistaan.

Lukea fs/pipe.c (Linuxissa) tai sys/kern/sys_pipe.c (*BSD:llä), se vaatii todellista omistautumista. Suorituskyky ja tuki ominaisuuksille, kuten vektori ja asynkroninen I/O, ovat tärkeitä koodissa nykyään. Ja muistin varaamisen, lukkojen ja ytimen kokoonpanon yksityiskohdat vaihtelevat suuresti. Tätä yliopistot eivät tarvitse käyttöjärjestelmien johdantokurssille.

Minusta oli joka tapauksessa mielenkiintoista kaivaa esiin muutamia vanhoja kuvioita (esim SIGPIPE ja palata EPIPE kirjoitettaessa suljettuun liukuhihnaan) kaikissa näissä niin erilaisissa nykyaikaisissa ytimissä. En luultavasti koskaan näe PDP-11-tietokonetta livenä, mutta muutama vuosi ennen syntymääni kirjoitetusta koodista on vielä paljon opittavaa.

Kirjoitti Divi Kapoor vuonna 2011, artikkeli "Putkien ja FIFO:iden Linux-ytimen toteutuson yleiskatsaus Linuxin putkien toiminnasta (toistaiseksi). A äskettäinen sitoutuminen linuxiin kuvaa vuorovaikutuksen putkimallia, jonka ominaisuudet ylittävät väliaikaisten tiedostojen ominaisuudet; ja näyttää myös kuinka pitkälle liukuhihnat ovat edenneet "erittäin konservatiivisesta lukituksesta" kuudennen painoksen Unix-ytimessä.

Lähde: will.com

Lisää kommentti