Com s'implementen les pipelines a Unix

Com s'implementen les pipelines a Unix
Aquest article descriu la implementació de pipelines al nucli Unix. Em va decebre una mica que un article recent titulat "Com funcionen les pipelines a Unix?"va resultar no sobre l'estructura interna. Vaig tenir curiositat i vaig buscar fonts antigues per trobar la resposta.

De què estem parlant?

Els pipelines, "probablement l'invent més important d'Unix", són una característica definitòria de la filosofia Unix subjacent d'enllaçar petits programes, així com un signe familiar a la línia d'ordres:

$ echo hello | wc -c
6

Aquesta funcionalitat depèn de la trucada del sistema proporcionada pel nucli pipe, que es descriu a les pàgines de documentació canonada (7) и canonada (2):

Les pipelines proporcionen un canal unidireccional per a la comunicació entre processos. La canalització té una entrada (extrem d'escriptura) i una sortida (extrem de lectura). Les dades escrites a l'entrada de la canalització es poden llegir a la sortida.

El pipeline es crea mitjançant la trucada pipe(2), que retorna dos descriptors de fitxer: un que fa referència a l'entrada del pipeline, el segon a la sortida.

La sortida de traça de l'ordre anterior mostra la creació de la canalització i el flux de dades a través d'ell d'un procés a un altre:

$ 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

El procés dels pares crida pipe()per obtenir descriptors de fitxers muntats. Un procés fill escriu en un identificador i un altre procés llegeix les mateixes dades d'un altre identificador. El shell utilitza dup2 per "canviar el nom" dels descriptors 3 i 4 per fer coincidir stdin i stdout.

Sense canonades, l'intèrpret d'ordres hauria d'escriure el resultat d'un procés a un fitxer i passar-lo a un altre procés per llegir les dades del fitxer. Com a resultat, malgastaríem més recursos i espai en disc. Tanmateix, les canalitzacions són bones no només perquè us permeten evitar l'ús de fitxers temporals:

Si un procés intenta llegir des d'una canalització buida, aleshores read(2) bloquejarà fins que les dades estiguin disponibles. Si un procés intenta escriure a una canalització completa, aleshores write(2) bloquejarà fins que s'hagin llegit prou dades de la canalització per dur a terme l'escriptura.

Igual que el requisit POSIX, aquesta és una propietat important: escriure al pipeline fins a PIPE_BUF els bytes (almenys 512) han de ser atòmics perquè els processos es puguin comunicar entre ells a través de la canalització d'una manera que els fitxers normals (que no ofereixen aquestes garanties) no poden.

Quan s'utilitza un fitxer normal, un procés pot escriure-hi tota la seva sortida i passar-la a un altre procés. O els processos poden funcionar en un mode altament paral·lel, utilitzant un mecanisme de senyalització extern (com un semàfor) per notificar-se mútuament quan s'ha completat una escriptura o lectura. Els transportadors ens estalvien tota aquesta molèstia.

Què estem buscant?

Ho explicaré en termes senzills perquè us sigui més fàcil imaginar com pot funcionar una cinta transportadora. Haureu d'assignar un buffer i algun estat a la memòria. Necessitareu funcions per afegir i eliminar dades de la memòria intermèdia. Necessitareu alguns mitjans per cridar funcions durant les operacions de lectura i escriptura dels descriptors de fitxers. I necessitareu panys per implementar el comportament especial descrit anteriorment.

Ara estem preparats per interrogar el codi font del nucli sota una llum brillant per confirmar o desmentir el nostre model mental vague. Però sempre estigueu preparats per a l'inesperat.

On estem buscant?

No sé on és la meva còpia del famós llibre "Llibre dels lleons"amb codi font Unix 6, però gràcies a La Societat del Patrimoni Unix podeu cercar en línia a codi font fins i tot versions més antigues d'Unix.

Passejar pels arxius del TUHS és com visitar un museu. Podem mirar la nostra història compartida, i tinc respecte pels molts anys d'esforç per recuperar tot aquest material a poc a poc de cintes i gravats antigues. I sóc molt conscient d'aquells fragments que encara falten.

Després d'haver satisfet la nostra curiositat sobre la història antiga dels transportadors, podem mirar els nuclis moderns per comparar-los.

Per cert, pipe és el número de trucada del sistema 42 a la taula sysent[]. Coincidència?

Nuclis Unix tradicionals (1970–1974)

No he trobat cap rastre pipe(2) ni dins PDP-7 Unix (gener de 1970), ni a primera edició d'Unix (novembre de 1971), ni en el codi font incomplet segona edició (juny de 1972).

TUHS afirma que tercera edició d'Unix (febrer de 1973) es va convertir en la primera versió amb transportadors:

La 1973a edició d'Unix va ser l'última versió amb un nucli escrit en llenguatge assemblador, però també la primera versió amb pipelines. Durant l'any XNUMX es va treballar per millorar la tercera edició, el nucli es va reescriure en C, i així va aparèixer la quarta edició d'Unix.

Un lector va trobar un escaneig d'un document en què Doug McIlroy proposava la idea de "connectar programes com una mànega de jardí".

Com s'implementen les pipelines a Unix
Al llibre de Brian KernighanUnix: una història i una memòria", en la història de l'aparició dels transportadors, també s'esmenta aquest document: "... va penjar a la paret del meu despatx als laboratoris Bell durant 30 anys". Aquí entrevista amb McIlroy, i una altra història de El treball de McIlroy, escrit el 2014:

Quan va sortir Unix, la meva fascinació per les corrutines em va portar a demanar a l'autor del sistema operatiu, Ken Thompson, que permetés que les dades escrites en un procés anessin no només al dispositiu, sinó també a un altre procés. Ken va decidir que era possible. Tanmateix, com a minimalista, volia que totes les funcions del sistema tinguessin un paper important. Escriure directament entre processos és realment un gran avantatge respecte escriure en un fitxer intermedi? Va ser només quan vaig fer una proposta específica amb el nom enganxós "pipeline" i una descripció de la sintaxi per a la interacció entre processos que Ken finalment va exclamar: "Ho faré!"

I ho va fer. Una tarda fatídica, Ken va canviar el nucli i l'intèrpret d'ordres, va arreglar diversos programes estàndard per estandarditzar com acceptaven l'entrada (que podria provenir d'un pipeline) i també va canviar els noms dels fitxers. L'endemà, les canonades van començar a utilitzar-se molt àmpliament en aplicacions. A finals de setmana, els secretaris els feien servir per enviar documents dels processadors de textos a la impressora. Una mica més tard, Ken va substituir l'API i la sintaxi originals per embolicar l'ús de canonades amb convencions més netes, que s'han utilitzat des de llavors.

Malauradament, el codi font de la tercera edició del nucli Unix s'ha perdut. I encara que tenim el codi font del nucli escrit en C quarta edició, publicat el novembre de 1973, però va sortir diversos mesos abans del llançament oficial i no conté implementacions de pipeline. És una llàstima que el codi font d'aquesta llegendària funció Unix es perdi, potser per sempre.

Tenim documentació de text per pipe(2) de les dues versions, de manera que podeu començar cercant la documentació tercera edició (per a determinades paraules, subratllades “manualment”, una cadena de literals ^H, seguida d'un guió baix!). Aquest proto-pipe(2) està escrit en llenguatge assemblador i només retorna un descriptor de fitxer, però ja proporciona la funcionalitat bàsica esperada:

Trucada del sistema tub crea un mecanisme d'entrada/sortida anomenat pipeline. El descriptor de fitxer retornat es pot utilitzar per a operacions de lectura i escriptura. Quan s'escriu alguna cosa a la canalització, s'emmagatzemen fins a 504 bytes de dades, després del qual se suspèn el procés d'escriptura. Quan es llegeix des de la canalització, s'eliminen les dades de la memòria intermèdia.

L'any següent el nucli s'havia reescrit en C, i pipe(2) a la quarta edició va adquirir el seu aspecte modern amb el prototip "pipe(fildes)'

Trucada del sistema tub crea un mecanisme d'entrada/sortida anomenat pipeline. Els descriptors de fitxers retornats es poden utilitzar en operacions de lectura i escriptura. Quan s'escriu alguna cosa a la canalització, s'utilitza el maneig retornat a r1 (resp. fildes[1]), emmagatzemat a 4096 bytes de dades, després del qual se suspèn el procés d'escriptura. Quan es llegeix des de la canalització, el controlador que torna a r0 (resp. fildes[0]) pren les dades.

Se suposa que un cop definit un pipeline, dos (o més) processos de comunicació (creats per trucades posteriors a forquilla) transferirà dades de la canalització mitjançant trucades llegir и escriure.

El shell té una sintaxi per definir una matriu lineal de processos connectats per una canalització.

Les trucades per llegir des d'una canalització buida (que no conté dades en memòria intermèdia) que només té un extrem (tots els descriptors de fitxers d'escriptura estan tancats) retornen "final del fitxer". Les trucades per escriure en una situació similar s'ignoren.

Més aviat implementació preservada del gasoducte es refereix a la cinquena edició d'Unix (juny de 1974), però és gairebé idèntic al que va aparèixer en el següent llançament. S'acaben d'afegir comentaris, així que podeu saltar la cinquena edició.

Sisena edició d'Unix (1975)

Comencem a llegir el codi font Unix sisena edició (maig de 1975). En gran part gràcies a Lleons és molt més fàcil de trobar que les fonts de versions anteriors:

Durant molts anys el llibre Lleons va ser l'únic document del nucli Unix disponible fora de Bell Labs. Tot i que la llicència de la sisena edició permetia als professors utilitzar el seu codi font, la llicència de la setena edició excloïa aquesta possibilitat, de manera que el llibre es va distribuir en forma de còpies mecanografiades il·legals.

Avui podeu comprar una reimpressió del llibre, la portada del qual mostra els estudiants en una fotocopiadora. I gràcies a Warren Toomey (que va iniciar el projecte TUHS) us podeu descarregar Fitxer PDF amb codi font per a la sisena edició. Vull donar-vos una idea de quant esforç s'ha fet per crear el fitxer:

Fa més de 15 anys, vaig escriure una còpia del codi font donat Lleons, perquè no m'agradava la qualitat de la meva còpia d'un nombre desconegut d'altres còpies. TUHS encara no existia i no tenia accés a les fonts antigues. Però l'any 1988 vaig trobar una cinta antiga de 9 pistes que contenia una còpia de seguretat d'un ordinador PDP11. Era difícil saber si funcionava, però hi havia un arbre /usr/src/ intacte en el qual la majoria dels fitxers estaven etiquetats amb l'any 1979, que fins i tot llavors semblava antic. Era la setena edició o la seva derivada PWB, com jo creia.

Vaig prendre la troballa com a base i vaig editar manualment les fonts a la sisena edició. Part del codi es va mantenir igual, però alguns s'han hagut d'editar lleugerament, canviant el testimoni += modern pel =+ obsolet. Algunes coses es van esborrar simplement, i algunes van haver de ser reescrites completament, però no massa.

I avui podem llegir en línia a TUHS el codi font de la sisena edició de arxiu, al qual va col·laborar Dennis Ritchie.

Per cert, a primera vista, la característica principal del codi C abans del període de Kernighan i Ritchie és la seva brevetat. No sol ser capaç d'inserir fragments de codi sense una edició extensa per adaptar-se a una àrea de visualització relativament estreta del meu lloc.

Al principi /usr/sys/ken/pipe.c hi ha un comentari explicatiu (i sí, n'hi ha més /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 mida del buffer no ha canviat des de la quarta edició. Però aquí veiem, sense cap documentació pública, que les pipelines van utilitzar els fitxers com a emmagatzematge de còpia de seguretat!

Pel que fa als fitxers LARG, corresponen a bandera d'inode GRAN, que fa servir l'"algorisme d'adreçament gran" per processar blocs indirectes per donar suport a sistemes de fitxers més grans. Com que Ken va dir que era millor no utilitzar-los, estaré encantat de prendre la seva paraula.

Aquí teniu la trucada del sistema real 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;
}

El comentari descriu clarament el que està passant aquí. Però entendre el codi no és tan fàcil, en part per la manera "struct user u» i registres R0 и R1 es transmeten els paràmetres de crida al sistema i els valors de retorn.

Anem a provar amb ialloc() posar al disc inode (handle d'índex), i amb l'ajuda falloc() - Posa dos a la memòria dossier. Si tot va bé, establirem senyals per identificar aquests fitxers com a dos extrems de la canalització, els apuntarem al mateix inode (el nombre de referències del qual s'establirà en 2) i marcarem l'inode com a modificat i en ús. Preste atenció a les peticions a Jo poso() en camins d'error per reduir el recompte de referències al nou inode.

pipe() ha de passar R0 и R1 retornar els números descriptors del fitxer per llegir i escriure. falloc() retorna un punter a l'estructura del fitxer, però també "retorna" via u.u_ar0[R0] i un descriptor de fitxer. És a dir, el codi desa r descriptor de fitxer per llegir i assigna un descriptor de fitxer per escriure directament des u.u_ar0[R0] després de la segona convocatòria falloc().

Bandera FPIPE, que establim en crear el pipeline, controla el comportament de la funció rdwr() a sys2.ccrida a rutines d'E/S específiques:

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

Després la funció readp() в pipe.c llegeix dades del pipeline. Però és millor rastrejar la implementació a partir de writep(). De nou, el codi s'ha tornat més complex a causa de les convencions de passar arguments, però es poden ometre alguns detalls.

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

Volem escriure bytes a l'entrada del pipeline u.u_count. Primer hem de bloquejar l'inode (vegeu a continuació plock/prele).

A continuació, comprovem el comptador de referència de l'inode. Mentre els dos extrems de la canonada romanguin oberts, el comptador hauria de ser igual a 2. Tenim un enllaç (des de rp->f_inode), per tant, si el comptador és inferior a 2, ha de significar que el procés de lectura ha tancat el seu final de la canalització. En altres paraules, estem intentant escriure en una canalització tancada i això és un error. Codi d'error per primera vegada EPIPE i senyal SIGPIPE va aparèixer a la sisena edició d'Unix.

Però fins i tot si el transportador està obert, pot estar ple. En aquest cas, alliberem el bloqueig i anem a dormir amb l'esperança que un altre procés llegeixi de la canalització i hi alliberi prou espai. Un cop despertats, tornem al principi, tornem a penjar el pany i comencem un nou cicle d'enregistrament.

Si hi ha prou espai lliure a la canalització, llavors escrivim dades amb ell escriurei()... Paràmetre i_size1 inode (si la canalització està buida, pot ser igual a 0) indica el final de les dades que ja conté. Si hi ha prou espai de gravació, podem omplir la canalització des i_size1 до PIPESIZ. A continuació, alliberem el bloqueig i intentem despertar qualsevol procés que estigui esperant per llegir des de la canalització. Tornem al principi per veure si vam poder escriure tants bytes com ens calgués. Si no té èxit, comencem un nou cicle de gravació.

Normalment el paràmetre i_mode inode s'utilitza per emmagatzemar permisos r, w и x. Però en el cas de pipelines, senyalem que algun procés està esperant una escriptura o lectura mitjançant bits IREAD и IWRITE respectivament. El procés estableix la bandera i truca sleep(), i s'espera que algun altre procés en el futur provoqui wakeup().

La veritable màgia passa a sleep() и wakeup(). S'implementen a slp.c, la font del famós comentari "No s'espera que ho entenguis". Afortunadament, no hem d'entendre el codi, només cal que mireu alguns comentaris:

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

El procés que provoca sleep() per a un canal concret, més tard pot ser despertat per un altre procés, que provocarà wakeup() per al mateix canal. writep() и readp() coordinar les seves accions mitjançant aquestes trucades aparellades. tingues en compte que pipe.c sempre dóna prioritat PPIPE en trucar sleep(), doncs això és tot sleep() pot ser interromput per un senyal.

Ara ho tenim tot per entendre la funció 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);
}

Pot ser que us resulti més fàcil llegir aquesta funció de baix a dalt. La branca "llegir i retornar" s'utilitza normalment quan hi ha algunes dades en el pipeline. En aquest cas, fem servir readi() llegim tantes dades com estigui disponible a partir de l'actual f_offset lectura i, a continuació, actualitzeu el valor de la compensació corresponent.

En lectures posteriors, la canalització estarà buida si s'ha arribat al desplaçament de lectura i_size1 a l'inode. Reiniciem la posició a 0 i intentem despertar qualsevol procés que vulgui escriure a la canalització. Sabem que quan la cinta transportadora està plena, writep() s'adormirà ip+1. I ara que la canalització està buida, podem despertar-la per reprendre el seu cicle d'escriptura.

Si no tens res a llegir, doncs readp() pot posar una bandera IREAD i adormir-se ip+2. Sabem què el despertarà writep(), quan escriu algunes dades al pipeline.

Comentaris a readi() i writei() t'ajudarà a entendre que en lloc de passar paràmetres mitjançant "u"Podem tractar-los com a funcions d'E/S normals que prenen un fitxer, una posició, un buffer a la memòria i compten el nombre de bytes per llegir o escriure.

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

Pel que fa al bloqueig “conservador”, doncs readp() и writep() bloqueja l'inode fins que acabin el seu treball o rebin un resultat (és a dir, trucar wakeup). plock() и prele() treballar simplement: utilitzant un conjunt diferent de trucades sleep и wakeup permeten despertar qualsevol procés que necessiti el bloqueig que acabem de llançar:

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

Al principi no podia entendre per què readp() no provoca prele(ip) abans de la trucada wakeup(ip+1). El primer és writep() provoca en el seu cicle, això plock(ip), que porta a un punt mort si readp() encara no he eliminat el meu bloqueig, així que d'alguna manera el codi ha de funcionar correctament. Si mireu wakeup(), llavors queda clar que només marca el procés de son com a llest per executar-se, de manera que en el futur sched() realment el va llançar. Tan readp() causes wakeup(), treu el pany, posa IREAD i trucades sleep(ip+2)- tot això abans writep() reprèn el cicle.

Això completa la descripció dels transportadors a la sisena edició. Codi senzill, conseqüències de gran abast.

Setena edició d'Unix (gener de 1979) va ser una nova versió important (quatre anys més tard) que va introduir moltes aplicacions noves i funcions del nucli. També va experimentar canvis significatius en relació amb l'ús de la fosa de tipus, unions i punters mecanografiats a estructures. malgrat això codi transportador pràcticament sense canvis. Ens podem saltar aquesta edició.

Xv6, un nucli simple semblant a Unix

Per crear el nucli Xv6 influenciat per la sisena edició d'Unix, però està escrit en C modern per funcionar amb processadors x86. El codi és fàcil de llegir i comprensible. A més, a diferència de les fonts Unix amb TUHS, podeu compilar-lo, modificar-lo i executar-lo en una altra cosa que no sigui un PDP 11/70. Per tant, aquest nucli s'utilitza àmpliament a les universitats com a material educatiu sobre sistemes operatius. Fonts estan a Github.

El codi conté una implementació clara i reflexiva canonada.c, recolzat per un buffer a la memòria en lloc d'un inode al disc. Aquí només proporciono la definició de "conducte estructural" i la funció 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() estableix l'estat de la resta de la implementació, que inclou les funcions piperead(), pipewrite() и pipeclose(). Trucada del sistema real sys_pipe és un embolcall implementat a sysfile.c. Recomano llegir el seu codi sencer. La complexitat és al nivell del codi font de la sisena edició, però és molt més fàcil i agradable de llegir.

Linux 0.01

Es pot trobar el codi font de Linux 0.01. Serà instructiu estudiar la implantació de canonades en el seu fs/pipe.c. Això fa servir un inode per representar el pipeline, però el pipeline en si està escrit en C modern. Si us heu avançat al codi de la XNUMXa edició, aquí no tindreu cap problema. Així es veu la funció 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;
}

Sense ni tan sols mirar les definicions de l'estructura, podeu esbrinar com s'utilitza el recompte de referència de l'inode per comprovar si una operació d'escriptura resulta en SIGPIPE. A més de treballar byte a byte, aquesta funció és fàcil de comparar amb les idees descrites anteriorment. Fins i tot la lògica sleep_on/wake_up no sembla tan estrany.

Nuclis Linux moderns, FreeBSD, NetBSD, OpenBSD

Vaig recórrer ràpidament alguns nuclis moderns. Cap d'ells ja no té una implementació de disc (no és sorprenent). Linux té la seva pròpia implementació. Tot i que els tres nuclis BSD moderns contenen implementacions basades en codi escrit per John Dyson, amb els anys s'han tornat massa diferents entre si.

Llegir fs/pipe.c (a Linux) o sys/kern/sys_pipe.c (a *BSD), es necessita una dedicació real. El codi d'avui tracta sobre el rendiment i el suport per a funcions com ara l'E/S vectorial i asíncrona. I els detalls de l'assignació de memòria, els bloquejos i la configuració del nucli varien molt. Això no és el que necessiten les universitats per a un curs d'introducció als sistemes operatius.

De totes maneres, m'interessava desenterrar alguns patrons antics (com generar SIGPIPE i tornar EPIPE quan s'escriu en un pipeline tancat) en tots aquests nuclis moderns diferents. Probablement mai no veuré un ordinador PDP-11 a la vida real, però encara hi ha molt per aprendre del codi que es va escriure anys abans de néixer.

Un article escrit per Divi Kapoor el 2011:La implementació del nucli de Linux de canonades i FIFO" proporciona una visió general de com funcionen (encara) les pipelines a Linux. A confirmació recent a Linux il·lustra un model d'interacció pipeline, les capacitats del qual superen les dels fitxers temporals; i també mostra fins a quin punt han arribat les pipelines del "bloqueig molt conservador" de la sisena edició del nucli Unix.

Font: www.habr.com

Afegeix comentari