Kiel duktoj estas efektivigitaj en Unikso

Kiel duktoj estas efektivigitaj en Unikso
Ĉi tiu artikolo priskribas la efektivigon de duktoj en la Unikso-simila kerno. Mi estis iom seniluziigita, ke lastatempa artikolo titolita "Kiel funkcias duktoj en Unikso?"rezultis ne pri la interna strukturo. Mi scivolemis kaj fosis en malnovajn fontojn por trovi la respondon.

Pri kio ni parolas?

Duktoj, "verŝajne la plej grava invento en Unikso", estas difina karakterizaĵo de la subesta Unix-filozofio ligi malgrandajn programojn kune, same kiel konata signo sur la komandlinio:

$ echo hello | wc -c
6

Ĉi tiu funkcio dependas de la sistemvoko provizita de kerno pipe, kiu estas priskribita sur la dokumentaj paĝoj pipo (7) и pipo (2):

Duktoj disponigas unudirektan kanalon por interproceza komunikado. La dukto havas enigaĵon (skribi finon) kaj eligaĵon (legi finaĵon). Datenoj skribitaj al la enigo de la dukto povas esti legitaj ĉe la eligo.

La dukto estas kreita per la voko pipe(2), kiu resendas du dosierpriskribilojn: unu rilatante al la enigo de la dukto, la dua al la eligo.

La spura eligo de la supra komando montras la kreadon de la dukto kaj la fluon de datumoj tra ĝi de unu procezo al alia:

$ 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

La gepatra procezo vokas pipe()por akiri muntitajn dosierpriskribilojn. Unu infana procezo skribas al unu tenilo, kaj alia procezo legas la samajn datumojn de alia tenilo. La ŝelo uzas dup2 por "alinomi" priskribilojn 3 kaj 4 por kongrui kun stdin kaj stdout.

Sen pipoj, la ŝelo devus skribi la rezulton de unu procezo al dosiero kaj transdoni ĝin al alia procezo por legi la datumojn de la dosiero. Kiel rezulto, ni malŝparus pli da rimedoj kaj diskospaco. Tamen, duktoj estas bonaj ne nur ĉar ili permesas vin eviti la uzon de provizoraj dosieroj:

Se procezo provas legi de malplena dukto tiam read(2) blokos ĝis la datumoj estos disponeblaj. Se procezo provas skribi al plena dukto, tiam write(2) blokos ĝis sufiĉaj datumoj estos legitaj de la dukto por plenumi la skribon.

Kiel la POSIX-postulo, ĉi tio estas grava eco: skribi al la dukto ĝis PIPE_BUF bajtoj (almenaŭ 512) devas esti atomaj tiel ke procezoj povas komuniki unu kun la alia tra la dukto en maniero kiel ke regulaj dosieroj (kiuj ne disponigas tiajn garantiojn) ne povas.

Kiam oni uzas regulan dosieron, procezo povas skribi sian tutan produktaĵon al ĝi kaj transdoni ĝin al alia procezo. Aŭ procezoj povas funkcii en tre paralela reĝimo, uzante eksteran signalan mekanismon (kiel semaforo) por sciigi unu la alian kiam skribo aŭ legado finiĝis. Transportiloj savas nin de ĉi tiu tuta ĝeno.

Kion ni serĉas?

Mi klarigos ĝin per simplaj terminoj, por ke estu pli facile por vi imagi kiel transportilo povas funkcii. Vi devos asigni bufron kaj iom da stato en memoro. Vi bezonos funkciojn por aldoni kaj forigi datumojn el la bufro. Vi bezonos iujn rimedojn por voki funkciojn dum legado kaj skriba operacioj sur dosierpriskribiloj. Kaj vi bezonos serurojn por efektivigi la specialan konduton priskribitan supre.

Nun ni pretas pridemandi la kernan fontkodon sub hela lamplumo por konfirmi aŭ kontraŭpruvi nian neklaran mensan modelon. Sed ĉiam estu preta por la neatendita.

Kien ni serĉas?

Mi ne scias kie estas mia kopio de la fama libro "Libro de Leonoj"kun Unix 6 fontkodo, sed danke al La Unix Heredaĵo-Socio vi povas serĉi interrete ĉe fontkodo eĉ pli malnovaj versioj de Unikso.

Vagi tra la TUHS-arkivoj estas kiel viziti muzeon. Ni povas rigardi nian komunan historion, kaj mi respektas la multjaran penadon por retrovi ĉi tiun tutan materialon iom post iom el malnovaj bendoj kaj presaĵoj. Kaj mi akre konscias pri tiuj fragmentoj, kiuj ankoraŭ mankas.

Kontentigis nian scivolemon pri la antikva historio de transportiloj, ni povas rigardi modernajn kernojn por komparo.

Por iu, pipe estas sistemvoko numero 42 en la tabelo sysent[]. Koincido?

Tradiciaj Uniksaj kernoj (1970-1974)

Mi ne trovis spurojn pipe(2) nek en PDP-7 Unikso (januaro 1970), nek en unua eldono de Unikso (novembro 1971), nek en la nekompleta fontkodo dua eldono (junio 1972).

TUHS deklaras tion tria eldono de Unikso (februaro 1973) iĝis la unua versio per transportiloj:

Unikso 1973a Eldono estis la lasta versio kun kerno skribita en asembla lingvo, sed ankaŭ la unua versio kun duktoj. Dum XNUMX oni laboris por plibonigi la trian eldonon, la kerno estis reverkita en C, kaj tiel aperis la kvara eldono de Unikso.

Unu leganto trovis skanadon de dokumento, en kiu Doug McIlroy proponis la ideon "konekti programojn kiel ĝardenhoson".

Kiel duktoj estas efektivigitaj en Unikso
En la libro de Brian KernighanUnikso: Historio kaj Memoraĵo", en la historio de la apero de transportiloj, ĉi tiu dokumento ankaŭ estas menciita: "... ĝi pendis sur la muro en mia oficejo ĉe Bell Labs dum 30 jaroj." Jen intervjuo kun McIlroy, kaj alia rakonto de La laboro de McIlroy, skribita en 2014:

Kiam Unikso aperis, mia fascino pri korutinoj igis min peti la aŭtoron de la OS, Ken Thompson, permesi ke datumoj skribitaj al procezo iri ne nur al la aparato, sed ankaŭ al eligo al alia procezo. Ken decidis ke ĝi eblas. Tamen, kiel minimumisto, li volis ke ĉiu sistema funkcio ludu gravan rolon. Ĉu skribi rekte inter procezoj vere estas granda avantaĝo super skribi al meza dosiero? Nur kiam mi faris specifan proponon kun la alloga nomo "dukto" kaj priskribo de la sintakso por interago inter procezoj, Ken finfine ekkriis: "Mi faros tion!"

Kaj faris. Iun fatalan vesperon, Ken ŝanĝis la kernon kaj ŝelon, riparis plurajn normajn programojn por normigi kiel ili akceptis enigaĵon (kiu povis veni de dukto), kaj ankaŭ ŝanĝis dosiernomojn. La sekvan tagon, duktoj komencis esti uzataj tre vaste en aplikoj. Je la fino de la semajno sekretarioj uzis ilin por sendi dokumentojn de tekstprilaboriloj al la presilo. Iom poste, Ken anstataŭigis la originan API kaj sintakson por envolvi la uzon de duktoj kun pli puraj konvencioj, kiuj estis uzataj ekde tiam.

Bedaŭrinde, la fontkodo por la tria eldono Unikso-kerno estas perdita. Kaj kvankam ni havas la kernan fontkodon skribitan en C kvara eldono, liberigita en novembro 1973, sed ĝi aperis plurajn monatojn antaŭ la oficiala liberigo kaj ne enhavas dukto efektivigojn. Estas domaĝe, ke la fontkodo por ĉi tiu legenda Unikso-funkcio estas perdita, eble por ĉiam.

Ni havas tekstan dokumentadon por pipe(2) de ambaŭ eldonoj, do vi povas komenci serĉante la dokumentaron tria eldono (por certaj vortoj, substrekitaj “mane”, ĉeno de literaloj ^H, sekvata de substreko!). Ĉi tiu proto-pipe(2) estas skribita en asembla lingvo kaj liveras nur unu dosierpriskribilon, sed jam provizas la atendatan bazan funkcion:

Sistemvoko pipo kreas enig/eligmekanismon nomitan dukto. La resendita dosierpriskribilo povas esti uzata por legado kaj skriba operacioj. Kiam io estas skribita al la dukto, ĝis 504 bajtoj da datumoj estas bufrigitaj, post kio la skribprocezo estas suspendita. Dum legado de la dukto, la bufritaj datumoj estas forprenitaj.

Antaŭ la sekva jaro la kerno estis reverkita en C, kaj pipo(2) en la kvara eldono akiris sian modernan aspekton kun la prototipo "pipe(fildes)»:

Sistemvoko pipo kreas enig/eligmekanismon nomitan dukto. La resenditaj dosierpriskribiloj povas esti uzataj en legado kaj skriba operacioj. Kiam io estas skribita al la dukto, la tenilo resendita en r1 (resp. fildes[1]) estas uzata, bufrigita al 4096 bajtoj da datenoj, post kio la skribprocezo estas suspendita. Legante el la dukto, la tenilo resendita al r0 (resp. fildes[0]) prenas la datumojn.

Estas supozite ke post kiam dukto estas difinita, du (aŭ pli) komunikaj procezoj (kreitaj per postaj vokoj al forkon) transdonos datumojn de la dukto per vokoj legi и skribi.

La ŝelo havas sintakson por difinado de lineara aro de procezoj ligitaj per dukto.

Alvokoj por legi de malplena dukto (enhavanta neniujn bufrajn datenojn) kiu havas nur unu finon (ĉiuj skribaj dosierpriskribiloj estas fermitaj) resendas "finon de dosiero". Alvokoj por skribi en simila situacio estas ignorataj.

Plej frue konservita dukto efektivigo rilatas al la kvina eldono de Unikso (junio 1974), sed ĝi estas preskaŭ identa al tiu kiu aperis en la venonta eldono. Komentoj ĵus aldoniĝis, do vi povas preterlasi la kvinan eldonon.

Sesa eldono de Unikso-similaj sistemoj (1975)

Ni komencu legi fontkodon de Unikso sesa eldono (majo 1975). Plejparte danke al leonoj ĝi estas multe pli facile trovebla ol la fontoj de pli fruaj versioj:

Dum multaj jaroj la libro leonoj estis la nura dokumento pri la Unikso-similaj sistemoj havebla ekster Bell Labs. Kvankam la sesa eldona permesilo permesis al instruistoj uzi ĝian fontkodon, la sepa eldona permesilo ekskludis tiun eblecon, do la libro estis disdonita en la formo de kontraŭleĝaj maŝinskribitaj kopioj.

Hodiaŭ vi povas aĉeti represaĵon de la libro, kies kovrilo montras studentojn ĉe kopimaŝino. Kaj danke al Warren Toomey (kiu komencis la projekton TUHS) vi povas elŝuti PDF-dosiero kun fontkodo por la sesa eldono. Mi volas doni al vi ideon pri kiom da penado estis por krei la dosieron:

Antaŭ pli ol 15 jaroj, mi tajpis kopion de la fontkodo donita leonoj, ĉar mi ne ŝatis la kvaliton de mia kopio de nekonata nombro da aliaj ekzempleroj. TUHS ankoraŭ ne ekzistis kaj mi ne havis aliron al la malnovaj fontoj. Sed en 1988, mi trovis malnovan 9-trakan bendon kiu enhavis sekurkopion de komputilo PDP11. Estis malfacile diri ĉu ĝi funkcias, sed estis nerompita /usr/src/ arbo en kiu la plej multaj el la dosieroj estis etikeditaj kun la jaro 1979, kiu eĉ tiam aspektis antikva. Ĝi estis la sepa eldono aŭ ĝia derivaĵo PWB, kiel mi kredis.

Mi prenis la trovaĵon kiel bazon kaj mane redaktis la fontojn al la sesa eldono. Kelkaj el la kodo restis la samaj, sed kelkaj devis esti iomete redaktitaj, ŝanĝante la modernan += ĵetonon al la malmoderna =+. Kelkaj aferoj estis simple forigitaj, kaj iuj devis esti tute reverkitaj, sed ne tro multe.

Kaj hodiaŭ ni povas legi rete ĉe TUHS la fontkodon de la sesa eldono de arkivo, al kiu Dennis Ritchie havis manon.

Cetere, unuavide, la ĉefa trajto de la C-kodo antaŭ la periodo de Kernighan kaj Ritchie estas ĝia mallongeco. Ne ofte mi kapablas enmeti pecojn de kodo sen ampleksa redaktado por konveni relative mallarĝan montran areon en mia retejo.

Komence /usr/sys/ken/pipe.c estas klariga komento (kaj jes, estas pli /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

La bufrograndeco ne ŝanĝiĝis ekde la kvara eldono. Sed ĉi tie ni vidas, sen ia publika dokumentado, ke duktoj iam uzis dosierojn kiel rezerva stokado!

Koncerne LARG-dosierojn, ili respondas al inodo flago LARG, kiu estas uzata de la "granda adresalgoritmo" por procesi nerektaj blokoj por subteni pli grandajn dosiersistemojn. Ĉar Ken diris, ke estas pli bone ne uzi ilin, mi volonte akceptos lian vorton.

Jen la vera sistemvoko 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;
}

La komento klare priskribas kio okazas ĉi tie. Sed kompreni la kodon ne estas tiel facila, parte pro la maniero "struct uzanto u» kaj registroj R0 и R1 sistemaj alvokaj parametroj kaj revenaj valoroj estas pasigitaj.

Ni provu kun ialloc() metu sur diskon inodo (indeksa tenilo), kaj kun la helpo falloc() - metu du en memoron dosiero. Se ĉio iras bone, ni starigos flagojn por identigi ĉi tiujn dosierojn kiel du finojn de la dukto, montros ilin al la sama inodo (kies referenca nombro estos agordita al 2), kaj markos la inodon kiel modifita kaj uzata. Atentu petojn al iput() en erarvojoj por redukti la referenckalkulon en la nova inodo.

pipe() devas tra R0 и R1 redonu dosierojn priskribaj nombroj por legado kaj skribo. falloc() resendas montrilon al la dosierstrukturo, sed ankaŭ "revenas" per u.u_ar0[R0] kaj dosierpriskribilo. Tio estas, la kodo ŝparas r dosierpriskribilo por legi kaj asignas dosierpriskribilon por skribi rekte de u.u_ar0[R0] post la dua voko falloc().

Flago FPIPE, kiun ni starigas dum kreado de la dukto, kontrolas la konduton de la funkcio rdwr () en sys2.cvokante specifajn I/O-rutinojn:

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

Tiam la funkcio readp() в pipe.c legas datumojn de la dukto. Sed estas pli bone spuri la efektivigon ekde writep(). Denove, la kodo fariĝis pli kompleksa pro la konvencioj de preterpasaj argumentoj, sed iuj detaloj povas esti preterlasitaj.

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

Ni volas skribi bajtojn al la dukto-enigo u.u_count. Unue ni devas ŝlosi la inodon (vidu sube plock/prele).

Tiam ni kontrolas la inodan referenco-nombrilon. Dum ambaŭ finoj de la dukto restas malfermitaj, la vendotablo devas esti egala al 2. Ni tenas unu ligilon (de rp->f_inode), do se la nombrilo estas malpli ol 2, ĝi devas signifi, ke la legado fermis sian finon de la dukto. Alivorte, ni provas skribi al fermita dukto, kaj ĉi tio estas eraro. Unuafoja erarkodo EPIPE kaj signalo SIGPIPE aperis en la sesa eldono de Unikso.

Sed eĉ se la transportilo estas malfermita, ĝi povas esti plena. En ĉi tiu kazo, ni liberigas la seruron kaj endormiĝas kun la espero, ke alia procezo legos el la dukto kaj liberigos sufiĉe da spaco en ĝi. Vekiĝinte, ni revenas al la komenco, denove pendigas la seruron kaj komencas novan registradon.

Se estas sufiĉe libera spaco en la dukto, tiam ni skribas datumojn al ĝi uzante skribii()... Parametro i_size1 ĉe inodo (se la dukto estas malplena ĝi povas esti egala al 0) indikas la finon de la datumoj kiujn ĝi jam enhavas. Se estas sufiĉe da registra spaco, ni povas plenigi la dukton de i_size1 por PIPESIZ. Tiam ni liberigas la seruron kaj provas veki ajnan procezon, kiu atendas legi el la dukto. Ni reiras al la komenco por vidi ĉu ni povis skribi tiom da bajtoj kiom ni bezonis. Se ĝi malsukcesas, tiam ni komencas novan registradciklon.

Kutime la parametro i_mode inodo estas uzata por konservi permesojn r, w и x. Sed en la kazo de duktoj, ni signalas, ke iu procezo atendas skribon aŭ legadon uzante bitojn IREAD и IWRITE respektive. La procezo metas la flagon kaj vokas sleep(), kaj estas atendite ke iu alia procezo estonte kaŭzos wakeup().

La vera magio okazas en sleep() и wakeup(). Ili estas efektivigitaj en slp.c, la fonto de la fama komento "Vi ne atendas tion kompreni". Feliĉe, ni ne devas kompreni la kodon, nur rigardu kelkajn komentojn:

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

La procezo kiu kaŭzas sleep() por aparta kanalo, povas poste esti vekita per alia procezo, kiu kaŭzos wakeup() por la sama kanalo. writep() и readp() kunordigi iliajn agojn per tiaj paritaj vokoj. rimarku tion pipe.c ĉiam donas prioritaton PPIPE kiam vokita sleep(), do jen sleep() povas esti interrompita de signalo.

Nun ni havas ĉion por kompreni la funkcion 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);
}

Vi eble trovos pli facile legi ĉi tiun funkcion de malsupre supren. La branĉo "legi kaj reveni" estas kutime uzata kiam estas iuj datumoj en la dukto. En ĉi tiu kazo, ni uzas legi() ni legas tiom da datumoj kiom disponeblas ekde la nuna f_offset legado, kaj poste ĝisdatigi la valoron de la responda ofseto.

En postaj legoj, la dukto estos malplena se la legita ofseto atingis i_size1 ĉe inodo. Ni restarigas la pozicion al 0 kaj provas veki ajnan procezon, kiu volas skribi al la dukto. Ni scias, ke kiam la transportilo estas plena, writep() endormiĝos ip+1. Kaj nun kiam la dukto estas malplena, ni povas veki ĝin por rekomenci sian skribciklon.

Se vi havas nenion por legi, do readp() povas agordi flagon IREAD kaj ekdormis plu ip+2. Ni scias, kio vekos lin writep(), kiam ĝi skribas iujn datumojn al la dukto.

Komentoj al readi() kaj writei() helpos vin kompreni tion anstataŭ pasi parametrojn per "u"Ni povas trakti ilin kiel normalajn I/O-funkciojn, kiuj prenas dosieron, pozicion, bufron en memoro, kaj kalkulas la nombron da bajtoj por legi aŭ skribi.

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

Pri la "konservativa" blokado, do readp() и writep() bloku la inodon ĝis ili finas sian laboron aŭ ricevas rezulton (tio estas, vokas wakeup). plock() и prele() labori simple: uzante malsaman aron de vokoj sleep и wakeup permesu al ni veki ajnan procezon, kiu bezonas la seruron, kiun ni ĵus liberigis:

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

Komence mi ne povis kompreni kial readp() ne kaŭzas prele(ip) antaŭ la voko wakeup(ip+1). La unua afero estas writep() kaŭzas en sia ciklo, ĉi tion plock(ip), kiu kondukas al blokiĝo se readp() ankoraŭ ne forigis mian blokadon, do iel la kodo devas funkcii ĝuste. Se vi rigardas wakeup(), tiam evidentiĝas, ke ĝi nur markas la dormantan procezon kiel preta por ekzekuti, tiel ke estonte sched() vere lanĉis ĝin. Do readp() kaŭzoj wakeup(), forigas la seruron, starigas IREAD kaj vokas sleep(ip+2)— ĉio ĉi antaŭe writep() rekomencas la ciklon.

Ĉi tio kompletigas la priskribon de transportiloj en la sesa eldono. Simpla kodo, vastaj konsekvencoj.

Sepa eldono de Unikso (januaro 1979) estis nova grava eldono (kvar jarojn poste) kiu lanĉis multajn novajn aplikojn kaj kerntrajtojn. Ĝi ankaŭ spertis signifajn ŝanĝojn lige kun la uzo de tipgisado, sindikatoj kaj tajpitaj montriloj al strukturoj. Tamen transporta kodo praktike senŝanĝe. Ni povas preterlasi ĉi tiun eldonon.

Xv6, simpla Unikso-simila kerno

Por krei la kernon Xv6 influite de la sesa eldono de Unikso, sed ĝi estas skribita en moderna C por funkcii per x86-procesoroj. La kodo estas facile legebla kaj komprenebla. Krome, male al Unikso-fontoj kun TUHS, vi povas kompili ĝin, modifi ĝin kaj ruli ĝin sur io alia ol PDP 11/70. Tial ĉi tiu kerno estas vaste uzata en universitatoj kiel eduka materialo pri operaciumoj. Fontoj estas sur Github.

La kodo enhavas klaran kaj pripenseman efektivigon pipo.c, subtenata de bufro en memoro anstataŭ inodo sur disko. Ĉi tie mi provizas nur la difinon de "struktura dukto" kaj la funkcion 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() fiksas la staton de la resto de la efektivigo, kiu inkluzivas la funkciojn piperead(), pipewrite() и pipeclose(). Fakta sistemvoko sys_pipe estas envolvaĵo efektivigita en sysfile.c. Mi rekomendas legi lian tutan kodon. La komplekseco estas je la nivelo de la fontkodo de la sesa eldono, sed ĝi estas multe pli facila kaj pli agrabla legi.

Linukso 0.01

Linukso 0.01 fontkodo troveblas. Estos instrua studi la efektivigon de duktoj en lia fs/pipe.c. Ĉi tio uzas inodon por reprezenti la dukto, sed la dukto mem estas skribita en moderna C. Se vi trairis 6-an eldonkodon, vi ne havos problemon ĉi tie. Jen kiel aspektas la funkcio 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;
}

Eĉ sen rigardi la strukturdifinojn, vi povas eltrovi kiel la inoda referenckalkulo estas uzata por kontroli ĉu skriba operacio rezultas en SIGPIPE. Krom labori bito-post-bajto, ĉi tiu funkcio estas facile kompari kun la ideoj supre priskribitaj. Eĉ logiko sleep_on/wake_up ne aspektas tiel fremda.

Modernaj Linukso-kernoj, FreeBSD, NetBSD, OpenBSD

Mi rapide trakuris kelkajn modernajn kernojn. Neniu el ili plu havas disk-efektivigon (ne surprize). Linukso havas sian propran efektivigon. Kvankam la tri modernaj BSD-kernoj enhavas efektivigojn bazitajn sur kodo kiu estis skribita fare de John Dyson, tra la jaroj ili fariĝis tro malsamaj unu de la alia.

Legi fs/pipe.c (sur Linukso) aŭ sys/kern/sys_pipe.c (sur *BSD), necesas vera dediĉo. La hodiaŭa kodo temas pri rendimento kaj subteno por funkcioj kiel vektora kaj nesinkrona I/O. Kaj la detaloj pri memoratribuo, seruroj kaj agordo de la kerno ĉiuj multe varias. Ĉi tio ne estas kion altlernejoj bezonas por enkonduka operaciuma kurso.

Ĉiuokaze, mi interesiĝis elfosi kelkajn malnovajn ŝablonojn (kiel generi SIGPIPE kaj reveni EPIPE skribante al fermita dukto) en ĉiuj ĉi tiuj malsamaj modernaj kernoj. Mi verŝajne neniam vidos komputilon PDP-11 en la reala vivo, sed estas ankoraŭ multe por lerni de kodo, kiu estis skribita jarojn antaŭ ol mi naskiĝis.

Artikolo skribita fare de Divi Kapoor en 2011:La Linuksa Kerno-Efektivigo de Pipoj kaj FIFOoj" provizas superrigardon pri kiel duktoj (ankoraŭ) funkcias en Linukso. A lastatempa kompromiso en Linukso ilustras duktomodelon de interago, kies kapabloj superas tiujn de provizoraj dosieroj; kaj ankaŭ montras kiom malproksime venis duktoj de la "tre konservativa ŝlosado" de la sesa eldono Uniksa kerno.

fonto: www.habr.com

Aldoni komenton