Unix-en kanalizazioak nola inplementatzen diren

Unix-en kanalizazioak nola inplementatzen diren
Artikulu honek kanalizazioen inplementazioa deskribatzen du Unix nukleoan. Zertxobait etsita nengoen azken artikulu batek "Nola funtzionatzen dute kanalizazioak Unix-en?"agertu zen ez barne egiturari buruz. Jakin-mina piztu eta iturri zaharretan sartu nintzen erantzuna aurkitzeko.

Zertaz ari gara?

Pipelineak, "seguruenik Unix-en asmakizun garrantzitsuena", programa txikiak elkarrekin lotzeko Unix-en filosofiaren ezaugarri definitzaile bat dira, baita komando-lerroko seinale ezagun bat ere:

$ echo hello | wc -c
6

Funtzio hau nukleoak emandako sistema-deiaren araberakoa da pipe, dokumentazio orrietan azaltzen dena hodia (7) ΠΈ hodia (2):

Pipelinek norabide bakarreko kanala eskaintzen dute prozesuen arteko komunikaziorako. Kanalizazioak sarrera (idazketa amaiera) eta irteera (irakurketa amaiera) ditu. Hodiaren sarreran idatzitako datuak irteeran irakur daitezke.

Deia erabiliz kanalizazioa sortzen da pipe(2), bi fitxategi deskribatzaile itzultzen dituena: bata kanalizazioaren sarrerari erreferentzia egiten diona, bigarrena irteerari.

Goiko komandoaren arrastoaren irteerak kanalizazioaren sorrera eta prozesu batetik besterako datuen fluxua erakusten du:

$ 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

Guraso-prozesuak deiak egiten ditu pipe()muntatutako fitxategien deskribatzaileak lortzeko. Haur-prozesu batek helduleku batean idazten du, eta beste prozesu batek beste helduleku batetik datu berak irakurtzen ditu. Shell-ak dup2 erabiltzen du 3 eta 4 deskribatzaileak "berriendatzeko" stdin eta stdout bat etortzeko.

Tuturik gabe, shell-ak prozesu baten irteera fitxategi batean idatzi beharko luke eta beste prozesu batera pasatu beharko luke fitxategiko datuak irakurtzeko. Ondorioz, baliabide eta disko espazio gehiago xahutuko genituzke. Hala ere, kanalizazioak onak dira aldi baterako fitxategiak erabiltzea saihesteko aukera ematen dutelako ez ezik:

Prozesu bat kanalizazio huts batetik irakurtzen saiatzen ari bada read(2) blokeatu egingo da datuak eskuragarri egon arte. Prozesu bat kanalizazio oso batean idazten saiatzen bada, orduan write(2) blokeatuko du kanaletik idazketa egiteko behar adina datu irakurri arte.

POSIX eskakizuna bezala, propietate garrantzitsu bat da: kanalizaziora idaztea PIPE_BUF byteek (gutxienez 512) atomikoak izan behar dute, prozesuek kanalizazioaren bidez elkarren artean komunikatu ahal izateko, fitxategi arruntek (berme horiek ematen ez dituztenek) ezin duten moduan.

Fitxategi arrunt bat erabiltzean, prozesu batek bere irteera guztia idatzi eta beste prozesu batera pasa dezake. Edo prozesuek modu oso paraleloan funtziona dezakete, kanpoko seinaleztapen-mekanismo bat erabiliz (semaforo bat bezala) elkarri jakinarazteko idazketa edo irakurketa amaitutakoan. Garraiogailuek traba guzti honetatik salbatzen gaituzte.

Zer bilatzen ari gara?

Termino sinpleetan azalduko dut, errazagoa izan dadin irudikatzea garraiatzaile batek nola funtziona dezakeen. Buffer bat eta egoera batzuk esleitu beharko dituzu memorian. Bufferretik datuak gehitzeko eta kentzeko funtzioak beharko dituzu. Fitxategien deskribatzaileetan irakurtzeko eta idazteko eragiketetan funtzioei deitzeko baliabide batzuk beharko dituzu. Eta sarrailak beharko dituzu goian deskribatutako portaera berezia ezartzeko.

Orain prest gaude nukleoaren iturburu-kodea argi distiratsuaren azpian galdetzeko, gure eredu mental lausoa baieztatzeko edo ezeztatzeko. Baina beti prest egon ezustekoetarako.

Nora bilatzen ari gara?

Ez dakit non dagoen liburu ospetsuaren kopia”Lehoien liburua"Unix 6 iturburu-kodearekin, baina eskerrak Unix Heritage Society sarean bilatu dezakezu iturburu kodea are Unix-en bertsio zaharragoak.

TUHS artxiboetan barrena ibiltzea museo bat bisitatzea bezalakoa da. Partekatutako historiari erreparatu diezaiokegu, eta errespetua daukat material hori guztia zinta eta grabatu zaharretatik pixkanaka-pixkanaka berreskuratzeko urte askotako ahalegina. Eta oraindik ere falta diren zati horietaz ohartzen naiz.

Zinta garraiatzaileen antzinako historiari buruz dugun jakin-mina ase ondoren, nukleo modernoak aldera ditzakegu.

Bide batez, pipe taulako sistemaren 42 zenbakia da sysent[]. Kasualitatea?

Unix kernel tradizionalak (1970–1974)

Ez nuen arrastorik aurkitu pipe(2) ezta barruan ere PDP-7 Unix (1970eko urtarrila), ezta urtean Unix-en lehen edizioa (1971ko azaroa), ezta iturri-kode osatugabean ere bigarren edizioa (1972ko ekaina).

TUHSek dio Unix-en hirugarren edizioa (1973ko otsaila) garraiagailudun lehen bertsioa bihurtu zen:

Unix 1973. Edizioa muntaia-lengoaian idatzitako nukleoa zuen azken bertsioa izan zen, baina baita kanalizazioekin lehen bertsioa ere. XNUMXan zehar, hirugarren edizioa hobetzeko lanak egin ziren, nukleoa C-n berridatzi zen, eta horrela Unix-en laugarren edizioa agertu zen.

Irakurle batek dokumentu baten eskaneatu bat aurkitu zuen, non Doug McIlroyk "lorategiko mahuka bat bezalako programak konektatzeko" ideia proposatu zuen.

Unix-en kanalizazioak nola inplementatzen diren
Brian Kernighanen liburuanUnix: Historia eta memoria batβ€œ, garraiatzaileen agerpenaren historian, dokumentu hau ere aipatzen da: β€œ... Bell Labs-eko nire bulegoko horman zintzilikatu zen 30 urtez”. Hemen McIlroy-ri egindako elkarrizketa, eta beste istorio bat McIlroyren lana, 2014an idatzia:

Unix atera zenean, koroutinekiko lilurak eraman ninduen Ken Thompson sistema eragilearen egileari galdetzera prozesu batean idatzitako datuak gailura ez ezik, beste prozesu batera ateratzeko ere uzteko. Kenek erabaki zuen posible zela. Hala ere, minimalista den heinean, sistemaren funtzio guztiek paper garrantzitsua izatea nahi zuen. Prozesuen artean zuzenean idaztea abantaila handia al da benetan tarteko fitxategi batean idaztearen aldean? "Pipeline" izen erakargarriarekin eta prozesuen arteko elkarrekintzarako sintaxiaren deskribapenarekin proposamen zehatz bat egin nuenean bakarrik esan zuen Kenek azkenean: "Egin dut!"

Eta egin zuen. Arratsalde zoritxarreko batean, Ken-ek nukleoa eta shell-a aldatu zituen, hainbat programa estandar konpondu zituen sarrera nola onartzen zuten estandarizatzeko (hodibide batetik etor zitekeen), eta fitxategien izenak ere aldatu zituen. Hurrengo egunean, hodiak oso zabal erabiltzen hasi ziren aplikazioetan. Aste bukaerarako, idazkariek testu-prozesadoreetatik inprimagailura dokumentuak bidaltzeko erabiltzen zituzten. Pixka bat geroago, Ken-ek jatorrizko APIa eta sintaxia ordezkatu zituen kanalizazioen erabilera konbentzio garbiagoekin biltzeko, orduz geroztik erabili izan direnak.

Zoritxarrez, hirugarren edizioko Unix nukleoaren iturburu-kodea galdu egin da. Eta nukleoaren iturburu kodea C-n idatzita badugu ere laugarren edizioa, 1973ko azaroan kaleratu zen, baina kaleratze ofiziala baino hilabete batzuk lehenago atera zen eta ez du kanalizazio inplementaziorik. Pena da Unix funtzio mitiko honen iturburu-kodea galtzea, agian betiko.

Testu-dokumentazioa dugu pipe(2) bi argitalpenetatik, dokumentazioa bilatuz has zaitezke hirugarren edizioa (zenbait hitzetarako, "eskuz" azpimarratuta, ^H literalen kate bat, azpimarra batez jarraian!). proto-pipe(2) mihiztadura-lengoaian idatzita dago eta fitxategi deskribatzaile bakarra itzultzen du, baina dagoeneko espero den oinarrizko funtzionaltasuna eskaintzen du:

Sistema-deia kanalizazio pipeline izeneko sarrera/irteera mekanismo bat sortzen du. Itzulitako fitxategi deskribatzailea irakurtzeko eta idazteko eragiketak egiteko erabil daiteke. Hodibidean zerbait idazten denean, 504 byte-ko datuak bufferean gordetzen dira, ondoren idazketa-prozesua eten egiten da. Hoditik irakurtzean, buffer-eko datuak kentzen dira.

Hurrengo urtean nukleoa C-n berridatzita zegoen, eta pipa(2) laugarren edizioan bere itxura modernoa lortu zuen prototipoarekin "pipe(fildes)'

Sistema-deia kanalizazio pipeline izeneko sarrera/irteera mekanismo bat sortzen du. Itzulitako fitxategien deskribatzaileak irakurtzeko eta idazteko eragiketetan erabil daitezke. Pipelinean zerbait idazten denean, r1-n itzultzen den heldulekua (esp. fildes[1]) erabiltzen da, 4096 byte-ko datuetan gordeta, eta ondoren idazketa-prozesua eten egiten da. Tutorialetik irakurtzean, r0-ra itzuli den heldulekuak (resp. fildes[0]) hartzen ditu datuak.

Suposatzen da kanalizazio bat definitu ondoren, komunikazio-prozesu bi (edo gehiago) (ondorengo deien bidez sortuak). fork) kanaletik datuak transferituko ditu deiak erabiliz irakurri ΠΈ idatzi.

Shell-ak kanalizazio baten bidez konektatutako prozesuen matrize lineal bat definitzeko sintaxia du.

Mutur bakarra duen kanalizazio huts batetik (daturik ez duena) irakurtzeko deiek (idazketa-fitxategien deskribatzaile guztiak itxita daude) "fitxategiaren amaiera" itzultzen dute. Antzeko egoera batean idazteko deiak ez dira aintzat hartzen.

Lehenago kanalizazioaren ezarpena gordeta aipatzen du Unix-en bosgarren ediziora (1974ko ekaina), baina hurrengo argitalpenean agertu zenaren ia berdina da. Iruzkinak gehitu berri dira, bosgarren edizioa salta dezakezu.

Unix-en seigarren edizioa (1975)

Has gaitezen Unix iturburu-kodea irakurtzen seigarren edizioa (1975eko maiatza). Hein handi batean esker Lions askoz errazagoa da lehenagoko bertsioen iturriak baino aurkitzea:

Urte askotan liburua Lions Bell Labs-etik kanpo zegoen Unix kernel-eko dokumentu bakarra zen. Seigarren edizioko lizentziak irakasleei bere iturburu-kodea erabiltzea ahalbidetzen bazuen ere, zazpigarren edizioko lizentziak aukera hori baztertzen zuen, beraz, liburua makinaz idatzitako kopia ilegalen moduan banatu zen.

Gaur liburuaren berrargitalpena eros dezakezu, zeinaren azalean ikasleak kopia makina batean ageri dira. Eta Warren Toomey-ri esker (TUHS proiektua hasi zuena) deskarga dezakezu PDF fitxategia iturburu kodearekin seigarren ediziorako. Fitxategia sortzeko zenbat ahalegin egin den ideia bat eman nahi dizut:

Duela 15 urte baino gehiago, emandako iturburu-kodearen kopia bat idatzi nuen Lions, ez zitzaidalako gustatu nire kopiaren kalitatea beste kopia kopuru ezezagun batetik. TUHS oraindik ez zen existitzen eta ez nuen iturri zaharretarako sarbiderik. Baina 1988an, 9 pistako zinta zahar bat aurkitu nuen PDP11 ordenagailu baten babeskopia zuena. Zaila zen funtzionatzen zuen ala ez esatea, baina bazegoen /usr/src/ zuhaitz osorik eta bertan fitxategi gehienak 1979 urtearekin etiketatuta zeuden, orduan ere antzinako itxura zuena. Zazpigarren edizioa edo bere eratorria PWB zen, nik uste nuen bezala.

Aurkikuntza oinarritzat hartu nuen eta eskuz editatu nituen iturriak seigarren ediziorako. Kode batzuk berdin jarraitzen zuten, baina batzuk apur bat editatu behar izan ziren, += token modernoa =+ zaharkituta aldatuz. Gauza batzuk besterik gabe ezabatu ziren, eta beste batzuk guztiz berridatzi behar izan ziren, baina ez gehiegi.

Eta gaur sarean irakur dezakegu TUHS-en seigarren edizioaren iturburu kodea artxiboa, Dennis Ritchie-k eskua izan zuena.

Bide batez, lehen begiratuan, Kernighan eta Ritchieren garaiaren aurretik C-kodearen ezaugarri nagusia berea da. laburtasuna. Ez da sarritan kode zatiak txertatzeko gai naizela, editatu gabe, nire webguneko bistaratzeko eremu estu samarrera egokitzeko.

Goiz /usr/sys/ken/pipe.c azalpenezko iruzkin bat dago (eta bai, gehiago dago /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

Buffer-aren tamaina ez da aldatu laugarren ediziotik. Baina hemen ikusten dugu, inolako dokumentazio publikorik gabe, kanalizazioak behinola fitxategiak erabiltzen zituela babeskopiko biltegiratze gisa!

LARG fitxategiei dagokienez, horri dagozkio inodo bandera LARG, prozesatzeko "helbideratze-algoritmo handiak" erabiltzen duena zeharkako blokeak fitxategi-sistema handiagoak onartzeko. Kenek esan zuenez hobe dela ez erabiltzea, pozik hartuko dut hitza.

Hona hemen benetako sistema deia 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;
}

Iruzkinak argi deskribatzen du hemen gertatzen ari dena. Baina kodea ulertzea ez da hain erraza, neurri batean moduagatik "struct erabiltzailea uΒ» eta erregistroak R0 ΠΈ R1 sistema-deiaren parametroak eta itzultzeko balioak pasatzen dira.

Saia gaitezen ialloc() diskoan jarri inodoa (indizearen heldulekua), eta laguntzarekin falloc() - jarri bi oroimenean fitxategia. Dena ondo badoa, fitxategi hauek kanalizazioaren bi mutur gisa identifikatzeko banderak ezarriko ditugu, inodo berera zuzenduko ditugu (bere erreferentzia kopurua 2 izango da) eta inodoa aldatu eta erabiltzen ari dela markatuko dugu. Erreparatu eskaerei jartzen dut() errore-bideetan inodo berriko erreferentzia-zenbaketa murrizteko.

pipe() bidez egin behar da R0 ΠΈ R1 irakurtzeko eta idazteko fitxategi deskribatzaile-zenbakiak itzultzeko. falloc() Erakuslea itzultzen du fitxategi-egiturara, baina baita "itzultzen" bidez u.u_ar0[R0] eta fitxategi deskribatzailea. Hau da, kodea gordetzen du r irakurtzeko fitxategi deskribatzailea eta zuzenean idazteko fitxategi deskribatzailea esleitzen du u.u_ar0[R0] bigarren deialdiaren ostean falloc().

bandera FPIPE, kanalizazioa sortzean ezartzen duguna, funtzioaren portaera kontrolatzen du rdwr() sys2.cI/O errutina espezifikoak deitzea:

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

Ondoren funtzioa readp() Π² pipe.c kanaletik datuak irakurtzen ditu. Baina hobe da ezarpena bertatik abiatuta trazatzea writep(). Berriz ere, kodea konplexuagoa bihurtu da argumentuak pasatzearen konbentzioengatik, baina xehetasun batzuk ezaba daitezke.

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

Pipelinearen sarreran byteak idatzi nahi ditugu u.u_count. Lehenik eta behin inodoa blokeatu behar dugu (ikus behean plock/prele).

Ondoren, inodoaren erreferentzia-kontagailua egiaztatuko dugu. Hodiaren bi muturrak irekita jarraitzen duten bitartean, kontagailuak 2ren berdina izan behar du. Esteka bat edukitzen dugu (tik. rp->f_inode), beraz, kontagailua 2 baino txikiagoa bada, irakurketa prozesuak hodiaren amaiera itxi duela esan behar du. Beste era batera esanda, kanalizazio itxi batean idazten saiatzen ari gara, eta hau errore bat da. Lehen aldiz errore-kodea EPIPE eta seinale SIGPIPE Unix-en seigarren edizioan agertu zen.

Baina garraiatzailea irekita egon arren, beteta egon daiteke. Kasu honetan, blokeoa askatu eta lotara joango gara beste prozesu batek kanaletik irakurriko duen eta bertan nahikoa leku askatuko duenaren esperantzarekin. Esnatu ondoren, hasierara itzuliko gara, berriro blokeoa eskegi eta grabazio-ziklo berri bati ekingo diogu.

Kanpoan nahikoa leku libre badago, datuak idatziko ditugu erabiliz idatzi(). Parametroa i_size1 inodoak (kanalizazioa hutsik badago 0-ren berdina izan daiteke) dagoeneko dituen datuen amaiera adierazten du. Grabatzeko leku nahikoa badago, kanalizazioa bertatik bete dezakegu i_size1 to PIPESIZ. Ondoren, blokeoa askatzen dugu eta kanaletik irakurtzeko zain dagoen edozein prozesu esnatzen saiatzen gara. Hasierara itzuliko gara, behar adina byte idazteko gai garen ikusteko. Huts egiten badu, grabazio-ziklo berri bat hasiko dugu.

Normalean parametroa i_mode inodoa baimenak gordetzeko erabiltzen da r, w ΠΈ x. Baina kanalizazioen kasuan, prozesuren bat bitak erabiliz idazteko edo irakurtzeko zain dagoela adierazten dugu IREAD ΠΈ IWRITE hurrenez hurren. Prozesuak bandera eta deiak ezartzen ditu sleep(), eta etorkizunean beste prozesuren batek eragingo duela espero da wakeup().

Benetako magia bertan gertatzen da sleep() ΠΈ wakeup(). urtean ezartzen dira slp.c, "Ez duzu hau ulertzea espero" iruzkin famatuaren iturria. Zorionez, ez dugu kodea ulertu behar, ikusi iruzkin batzuk:

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

Sortzen duen prozesua sleep() kanal jakin baterako, geroago beste prozesu batek esnatu dezake, eta horrek eragingo du wakeup() kanal bererako. writep() ΠΈ readp() haien ekintzak koordinatzea parekatutako deien bidez. apuntatu hori pipe.c beti ematen du lehentasuna PPIPE deituta sleep(), beraz, hori da sleep() seinale batek eten dezake.

Orain dena dugu funtzioa ulertzeko 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);
}

Baliteke funtzio hau behetik goitik irakurtzea errazagoa izatea. "Irakurri eta itzuli" adarra erabili ohi da kanalean datu batzuk daudenean. Kasu honetan, erabiltzen dugu irakurri() egungotik hasita dauden adina datu irakurtzen ditugu f_offset irakurri, eta, ondoren, eguneratu dagokion desplazamenduaren balioa.

Ondorengo irakurketetan, kanalizazioa hutsik egongo da irakurritako desplazamendura iritsi bada i_size1 inodoan. Posizioa 0-ra berrezartzen dugu eta kanalizazioan idatzi nahi duen edozein prozesu esnatzen saiatzen gara. Badakigu garraiatzailea beteta dagoenean, writep() gainean lo hartuko du ip+1. Eta orain kanalizazioa hutsik dagoenez, esna dezakegu bere idazketa-zikloa berriro hasteko.

Irakurtzeko ezer ez baduzu, orduan readp() bandera bat ezarri dezake IREAD eta lo hartu ip+2. Badakigu zerk esnatuko duen writep(), kanalean datu batzuk idazten dituenean.

Iruzkinak irakurri() eta idatzi() Hori ulertzen lagunduko dizu parametroak pasa beharrean "u"Fitxategi bat, posizio bat, memorian buffer bat hartzen duten eta irakurtzeko edo idazteko byte kopurua zenbatzen duten I/O funtzio arrunt gisa trata ditzakegu.

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

Blokeo β€œkontserbadoreari” dagokionez, bada readp() ΠΈ writep() blokeatu inodoa lana amaitu arte edo emaitza bat jaso arte (hau da, deitu wakeup). plock() ΠΈ prele() besterik gabe lan egin: dei-multzo ezberdin bat erabiliz sleep ΠΈ wakeup askatu berri dugun blokeoa behar duen edozein prozesu esna dezagun:

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

Hasieran ezin nuen ulertu zergatik readp() ez du eragiten prele(ip) deialdiaren aurretik wakeup(ip+1). Lehenengo gauza da writep() bere zikloan eragiten du, hau plock(ip), blokeoa dakar bada readp() oraindik ez dut nire blokeoa kendu, beraz, nolabait, kodeak behar bezala funtzionatu behar du. Begiratuz gero wakeup(), orduan argi geratzen da lo egiteko prozesua exekutatzeko prest bezala markatzen duela soilik, etorkizunean sched() benetan jarri zuen martxan. Beraz readp() arrazoiak wakeup(), sarraila kentzen du, ezartzen IREAD eta deiak sleep(ip+2)- hau guztia aurretik writep() zikloari berrekingo dio.

Honek seigarren edizioko garraiatzaileen deskribapena osatzen du. Kode sinplea, ondorio zabalak.

Unix-en zazpigarren edizioa (1979ko urtarrila) bertsio nagusi berri bat izan zen (lau urte geroago), aplikazio eta nukleoaren ezaugarri berri asko sartu zituena. Aldaketa nabarmenak ere jasan zituen mota-galdaketa, batasunak eta egituren erakusle mekanizatuen erabilerarekin lotuta. Hala ere garraio-kodea ia aldatu gabe. Edizio hau salta dezakegu.

Xv6, Unix itxurako kernel sinplea

Nukleoa sortzeko Xv6 Unix-en seigarren edizioak eraginda, baina C modernoan idatzita dago x86 prozesadoreetan exekutatzeko. Kodea irakurterraza eta ulergarria da. Gainera, TUHS duten Unix iturriak ez bezala, konpilatu, aldatu eta exekutatu dezakezu PDP 11/70 ez den beste zerbaitetan. Horregatik, kernel hau unibertsitateetan oso erabilia da sistema eragileei buruzko hezkuntza-material gisa. Iturriak Github-en daude.

Kodeak inplementazio argi eta pentsakor bat dauka hodia.c, memorian buffer batek babestuta, diskoko inodo baten ordez. Hemen "egiturazko kanalizazioa" eta funtzioaren definizioa baino ez dut ematen 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() funtzioak barne hartzen dituen gainerako ezarpenen egoera ezartzen du piperead(), pipewrite() ΠΈ pipeclose(). Benetako sistema-deia sys_pipe urtean inplementatutako bilgarri bat da sysfile.c. Bere kode osoa irakurtzea gomendatzen dut. Konplexutasuna seigarren edizioko iturburu-kodearen mailan dago, baina askoz errazagoa eta atseginagoa da irakurtzen.

Linux 0.01

Linux 0.01 iturburu kodea aurki daiteke. Irakasgarria izango da bere hoditegien ezarpena aztertzea fs/pipe.c. Honek inodo bat erabiltzen du kanalizazioa irudikatzeko, baina kanalizazioa bera C modernoan idatzita dago. 6. edizioko kodean lan egin baduzu, ez duzu arazorik izango hemen. Hau da funtzioaren itxura 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;
}

Egituraren definizioak begiratu ere egin gabe, inodoaren erreferentzia-zenbaketa nola erabiltzen den jakin dezakezu idazketa-eragiketa batek sortzen duen ala ez egiaztatzeko. SIGPIPE. Bytez byte lan egiteaz gain, funtzio hau erraza da goiko deskribatutako ideiekin alderatzea. Baita logika ere sleep_on/wake_up ez dirudi hain arrotza.

Linux kernel modernoak, FreeBSD, NetBSD, OpenBSD

Azkar kernel moderno batzuk zeharkatu nituen. Horietako inork ez du jada disko inplementaziorik (ez da harritzekoa). Linuxek bere inplementazioa du. Hiru BSD nukleo modernoek John Dysonek idatzitako kodean oinarritutako inplementazioak badituzte ere, urteen poderioz elkarrengandik oso desberdindu dira.

Irakurri fs/pipe.c (Linux-en) edo sys/kern/sys_pipe.c (*BSD-n), benetako dedikazioa behar da. Gaurko kodea errendimenduari eta laguntzari buruzkoa da, hala nola, I/O bektoriala eta asinkronoa bezalako funtzioetarako. Eta memoriaren esleipenaren, blokeoen eta nukleoaren konfigurazioaren xehetasunak asko aldatzen dira. Hau ez da unibertsitateek sistema eragileen hastapeneko ikastaro baterako behar dutena.

Dena den, eredu zahar batzuk aurkitzea interesatzen zitzaidan (sortzea adibidez SIGPIPE eta itzuli EPIPE kanalizazio itxi batean idaztean) nukleo moderno ezberdin hauetan guztietan. Ziurrenik ez dut inoiz PDP-11 ordenagailurik ikusiko bizitza errealean, baina oraindik asko dago ikasteko ni jaio baino urte lehenago idatzitako kodetik.

Divi Kapoor-ek 2011n idatzitako artikulu bat:Pipeen eta FIFOen Linux Kernelaren ezarpena" Linux-en kanalizazioak (oraindik) nola funtzionatzen duen ikuspegi orokorra eskaintzen du. A azken konpromisoa Linux-en interakzio-eredu bat erakusten du, zeinaren gaitasunak aldi baterako fitxategienak gainditzen dituena; eta seigarren edizioko Unix kernelaren "blokeo oso kontserbadoretik" kanalizazioak noraino iritsi diren erakusten du.

Iturria: www.habr.com

Gehitu iruzkin berria