Cómo se implementan las tuberías de Unix


Este artículo describe la implementación de tuberías en el núcleo de Unix. Estaba un poco decepcionado con un artículo reciente titulado " ¿Cómo funcionan las tuberías en Unix?" " No se trataba del dispositivo interno. Me interesé y me enterré en las viejas fuentes para encontrar la respuesta.

¿De qué estamos hablando?


Las tuberías, "probablemente la invención más importante en Unix", es la característica definitoria de la filosofía subyacente de Unix de combinar pequeños programas, así como la línea de comandos familiar:

$ echo hello | wc -c
6

Esta funcionalidad depende de la llamada al sistema proporcionada por el kernel pipe, que se describe en las páginas de documentación de pipe (7) y pipe (2) :

Los transportadores proporcionan un canal de comunicación unidireccional entre procesos. La canalización tiene una entrada (final de escritura) y una salida (final de lectura). Los datos escritos en la entrada de la tubería se pueden leer.

La canalización se crea mediante una llamada pipe(2)que devuelve dos descriptores de archivo: uno se refiere a la entrada de la canalización, el segundo a la salida.

Los resultados de seguimiento del comando anterior demuestran la creación de una tubería y el flujo de datos a través de ella de un proceso a otro:

$ 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, "hello\n", 6)    = 6
[pid 2604796] dup2(3, 0)                = 0
[pid 2604796] execve("/usr/bin/wc", ["wc", "-c"], …)
[pid 2604796] read(0, "hello\n", 16384) = 6
[pid 2604796] write(1, "6\n", 2)        = 2

El proceso padre llama pipe()para obtener los descriptores de archivo adjuntos. Un proceso secundario escribe en un descriptor y otro proceso lee los mismos datos de otro descriptor. El contenedor que usa dup2 "renombra" los descriptores 3 y 4 para que coincidan con stdin y stdout.

Sin tuberías, el shell tendría que escribir el resultado de un proceso en un archivo y transferirlo a otro proceso para que lea los datos del archivo. Como resultado, gastaríamos más recursos y espacio en disco. Sin embargo, las canalizaciones son buenas no solo porque evitan el uso de archivos temporales:

, read(2) , . , write(2) , .

Al igual que el requisito POSIX, esta es una propiedad importante: la escritura en la tubería de hasta PIPE_BUFbytes (al menos 512) debe ser atómica para que los procesos puedan interactuar entre sí a través de la tubería de la misma manera que los archivos normales (que no ofrecen tales garantías).

Cuando se utiliza un archivo normal, un proceso puede escribirle todos sus datos de salida y transferirlo a otro proceso. O los procesos pueden funcionar en modo de paralelización rígida, utilizando un mecanismo de señalización externo (como un semáforo) para informarse mutuamente sobre la finalización de la escritura o la lectura. Los transportadores nos salvan todos estos problemas.

¿Qué estamos buscando?


Lo explicaré con mis dedos para que te resulte más fácil imaginar cómo puede funcionar el transportador. Necesitará asignar un búfer y algún estado en la memoria. Necesitará funciones para agregar y eliminar datos del búfer. Tomará algunos medios para llamar a las funciones durante las operaciones de lectura y escritura en los descriptores de archivo. Y se necesitan bloqueos para implementar el comportamiento especial descrito anteriormente.

Ahora estamos listos para interrogar a la luz brillante de las lámparas el código fuente del núcleo para confirmar o refutar nuestro vago modelo mental. Pero siempre prepárate para lo inesperado.

Donde estamos buscando


No sé dónde se encuentra mi copia del famoso libro " Lions book " con el código fuente de Unix 6, pero gracias a The Unix Heritage Society puedes buscar en línea incluso versiones anteriores de Unix en el código fuente .

Recorrer los archivos de TUHS es similar a visitar un museo. Podemos echar un vistazo a nuestra historia común, y respeto los muchos años de esfuerzos por recuperar todos estos materiales poco a poco de casetes e impresiones antiguas. Y soy muy consciente de esos fragmentos que aún faltan.

Habiendo satisfecho nuestra curiosidad con respecto a la historia antigua de los transportadores, podemos comparar los núcleos modernos.

Por cierto, pipees un número de llamada del sistema 42 en la tabla sysent[]. ¿Coincidencia?

Granos tradicionales de Unix (1970–1974)


No encontré rastro pipe(2)en PDP-7 Unix (enero de 1970), ni en la primera edición de Unix (noviembre de 1971), ni en el código fuente incompleto de la segunda edición (junio de 1972).

TUHS afirma que la tercera edición de Unix (febrero de 1973) fue la primera versión con ductos:

La tercera edición de Unix fue la última versión con un núcleo escrito en lenguaje ensamblador, pero la primera versión con canalizaciones. Durante 1973, se trabajó para mejorar la tercera edición, el núcleo se reescribió en C, por lo que apareció la cuarta edición de Unix.

Uno de los lectores encontró un escaneo de un documento en el que Doug McIlroy propuso la idea de "conectar programas por el principio de una manguera de jardín".


En el libro de Brian Kernighan " Unix: una historia y una memoria ", en la historia de la aparición de transportadores, este documento también se menciona: "... colgó en la pared de mi oficina en Bell Labs durante 30 años". Aquí hay una entrevista con McIlroy , y otra historia del trabajo de McIlroy escrito en 2014 :

Unix, , , , - , , . , . , , , . ? «» , , -, : « !».

. , , ( ), . . . API , .

Desafortunadamente, el código fuente del kernel para la tercera edición de Unix se pierde. Y aunque tenemos el código fuente para el núcleo de la cuarta edición escrito en C , lanzado en noviembre de 1973, fue lanzado unos meses antes del lanzamiento oficial y no contiene implementaciones de canalización. Es una pena que el código fuente de la legendaria función de Unix se pierda, posiblemente para siempre.

Tenemos el texto de la documentación pipe(2)de ambas versiones, por lo que puede comenzar buscando en la tercera edición de la documentación (para ciertas palabras, subrayadas "manualmente", una cadena de literales ^ H, ¡seguido de guiones bajos!). Este protocolo está pipe(2)escrito en ensamblador y devuelve solo un descriptor de archivo, pero ya proporciona la funcionalidad básica esperada:

La llamada al sistema de tuberías crea un mecanismo de entrada de salida llamado tubería. El descriptor de archivo devuelto se puede usar para operaciones de lectura y escritura. Cuando se escribe algo en la tubería, se almacenan hasta 504 bytes de datos, después de lo cual se detiene el proceso de escritura. Al leer desde una tubería, se toman datos almacenados en el búfer.

Al año siguiente, el núcleo se reescribió en C, y la tubería (2) en la cuarta edición encontró su aspecto moderno con el prototipo " pipe(fildes)":

pipe , . . - , , r1 (. fildes[1]), 4096 , . , r0 (. fildes[0]), .

, ( ) ( fork) read write.

El shell tiene una sintaxis para definir una matriz lineal de procesos conectados a través de una tubería.

Las llamadas de lectura de una tubería vacía (que no contiene datos almacenados en el búfer) que tiene un solo extremo (todos los descriptores de archivo de escritura están cerrados) devuelven "fin de archivo". La grabación de llamadas en una situación similar se ignora.

La implementación de la tubería más antigua que se conserva data de la quinta edición de Unix (junio de 1974), pero es casi idéntica a la que apareció en el próximo lanzamiento. Solo se agregaron comentarios, por lo que se puede omitir la quinta edición.

Sexta edición de Unix (1975)


Comenzamos a leer el código fuente de la sexta edición de Unix (mayo de 1975). En gran parte gracias a Lions, encontrarlo es mucho más fácil que el código fuente de versiones anteriores:

Durante muchos años, Lions ha sido el único documento central de Unix disponible fuera de los muros de Bell Labs. Aunque la licencia de la sexta edición permitió a los maestros usar su código fuente, la licencia de la séptima edición excluyó esta posibilidad, por lo que el libro se distribuyó en forma de copias ilegales escritas a máquina.

Hoy puede comprar una copia de reimpresión del libro, en cuya portada se muestran a los estudiantes en la fotocopiadora. Y gracias a Warren Tumi (quien lanzó el proyecto TUHS), puede descargar el archivo PDF con el código fuente de la sexta edición . Quiero darte una idea de cuánto esfuerzo tomó crear el archivo:

15 , Lions, . TUHS , . 1988- 9 , PDP11. , , /usr/src/, 1979- , . PWB, .

. , , += =+. - , - , .

Y hoy podemos leer en línea en TUHS el código fuente de la sexta edición del archivo, al que Dennis Ritchie echó una mano .

Por cierto, a primera vista, la característica principal del código C antes del período Kernigan y Richie es su brevedad . No tan a menudo, me las arreglo para incrustar fragmentos de código sin una edición extensa para ajustar un área de visualización relativamente estrecha en mi sitio.

Al comienzo de /usr/sys/ken/pipe.c hay un comentario explicativo (y sí, también hay / 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

El tamaño del búfer no ha cambiado desde la cuarta edición. Pero aquí, sin ninguna documentación pública, ¡vemos que una vez que las tuberías utilizaron los archivos como almacenamiento de respaldo!

En cuanto a los archivos LARG, corresponden al indicador de inodo LARG , que es utilizado por el "algoritmo de alto direccionamiento" para procesar bloques indirectos para soportar sistemas de archivos más grandes. Como Ken dijo que es mejor no usarlos, con mucho gusto aceptaré su palabra.

Aquí está la llamada real del sistema 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 comentario describe claramente lo que está sucediendo aquí. Pero comprender el código no es fácil, en parte debido a la forma con la ayuda de « un usuario de estructura u » y registra R0y R1transfiere parámetros de llamadas al sistema y valores de retorno.

Intentemos usar ialloc () para colocar el inodo (inode ) en el disco , y usar Fallo () para colocar dos archivos en la memoria . Si todo va bien, estableceremos banderas para definir estos archivos como los dos extremos de la tubería, los señalaremos al mismo inodo (cuyo recuento de referencia será 2) y marcaremos el inodo como modificado y utilizado. Presta atención a las llamadas a iput ()en rutas de error para disminuir el recuento de referencia en el nuevo inodo.

pipe()debe completar R0y R1devolver los números de descriptor de archivo para leer y escribir. falloc()devuelve un puntero a la estructura del archivo, pero también "devuelve" a través del u.u_ar0[R0]descriptor de archivo. Es decir, el código se guarda en un rdescriptor de archivo para leer y asigna un descriptor para escribir directamente u.u_ar0[R0]después de la segunda llamada falloc().

El indicador FPIPEque establecemos al crear la canalización controla el comportamiento de la función rdwr () en sys2.c , que llama a rutinas específicas de E / S de E / S:

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

Luego, la función readp()en pipe.clee los datos de la tubería. Pero es mejor comenzar con el seguimiento de la implementación writep(). Una vez más, el código se volvió más complicado debido a los detalles del acuerdo de transferencia de argumentos, pero se pueden omitir algunos detalles.

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

Queremos escribir bytes en la entrada de la tubería u.u_count. Primero necesitamos bloquear el inodo (ver abajo plock/ prele).

Luego verifique el recuento de referencia de inodo. Si bien ambos extremos de la tubería permanecen abiertos, el contador debe ser 2. Mantenemos un enlace (fuera rp->f_inode), por lo que si el contador es menor que 2, esto debería significar que el proceso de lectura ha cerrado su final de la tubería. En otras palabras, estamos tratando de escribir en una tubería cerrada, y esto es un error. El código de error EPIPEy la señal SIGPIPEaparecieron por primera vez en la sexta edición de Unix.

Pero incluso si el transportador está abierto, puede estar lleno. En este caso, retiramos el bloqueo y nos vamos a dormir con la esperanza de que otro proceso se lea desde la tubería y libere suficiente espacio en él. Habiendo despertado, volvemos al principio, nuevamente bloqueamos el bloqueo y comenzamos un nuevo ciclo de grabación.

Si hay suficiente espacio libre en la tubería, entonces le escribimos datos usando writei () . El parámetro i_size1en inodo (con una tubería vacía puede ser 0) indica el final de los datos que ya contiene. Si hay suficiente espacio de grabación, podemos llenar el transportador desde i_size1hastaPIPESIZ. Luego retiramos el bloqueo e intentamos despertar cualquier proceso que esté esperando la oportunidad de leer desde la tubería. Volvemos al principio para ver si logramos escribir tantos bytes como sea necesario. Si falla, entonces comenzamos un nuevo ciclo de grabación.

Por lo general i_mode, se usa un parámetro de inodo para almacenar permisos r, wy x. Pero en el caso de las tuberías, indicamos que algún proceso está esperando escribir o leer usando bits IREADy, IWRITErespectivamente. Un proceso establece una bandera y llama sleep(), y se espera que algún otro proceso llame en el futuro wakeup().

La verdadera magia ocurre en sleep()y wakeup(). Se implementan en slp.c, la fuente del famoso comentario, "No se espera que entiendas esto". Afortunadamente, no estamos obligados a comprender el código, solo vea algunos comentarios:

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

Un proceso que invoca sleep()para un canal en particular puede ser despertado por otro proceso que invocará wakeup()para el mismo canal. writep()y readp()coordinar sus acciones a través de tales llamadas emparejadas. Tenga en cuenta que pipe.csiempre se da prioridad PPIPEal llamar sleep(), para que todos sleep()puedan interrumpir la señal.

Ahora tenemos todo para entender la función 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);
}

Puede que le resulte más fácil leer esta función de abajo hacia arriba. La rama "leer y volver" generalmente se usa cuando hay algunos datos en la tubería. En este caso, usamos readi () para leer la mayor cantidad de datos disponibles a partir de la f_offsetlectura actual , y luego actualizar el valor del desplazamiento correspondiente.

En lecturas posteriores, la tubería estará vacía si el desplazamiento de lectura ha alcanzado el valor del i_size1inodo. Restablecemos la posición a 0 e intentamos despertar cualquier proceso que quiera escribir en la tubería. Sabemos que cuando el transportador esté lleno, se writep()quedará dormido ip+1. Y ahora que la tubería está vacía, podemos despertarla para que reanude su ciclo de grabación.

Si no hay nada que leer, readp()puede establecer una bandera IREADy quedarse dormidoip+2. Sabemos lo que lo despertará writep()cuando escriba algunos datos en la tubería.

Los comentarios sobre readi () y writei () ayudarán a comprender que, en lugar de pasar parámetros a través de " u", podemos tratarlos como funciones de E / S habituales que toman un archivo, posición, buffer en la memoria y cuentan la cantidad de bytes para leer o escribir .

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

En cuanto a la cerradura "conservadora", a continuación, readp()y el writep()bloque de nodo-i durante el tiempo que terminan un trabajo o no consiguen el resultado (es decir, la causa wakeup). plock()y prele()funcionan de manera simple: utilizan un conjunto diferente de llamadas sleepy wakeupnos permiten activar cualquier proceso que necesite un bloqueo que acabamos de eliminar:

/*
 * 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 principio no pude entender por qué readp()no llamó prele(ip)antes de la llamada wakeup(ip+1). Lo primero que writep()causa en su bucle es que plock(ip)conduce a un punto muerto si readp()aún no ha eliminado su bloqueo, por lo que el código debe funcionar de alguna manera correctamente. Si lo miras wakeup(), queda claro que solo marca el proceso de suspensión como listo para la ejecución, de modo que en el futuro sched()realmente lo inicie. Entonces readp()causa wakeup(), desbloquea, establece IREADy llama sleep(ip+2), todo esto antes de writep()reanudar el ciclo.

Esto completa la descripción de los transportadores en la sexta edición. Código simple, consecuencias de largo alcance.

Séptima Edición de Unix(Enero de 1979) fue la nueva versión principal (cuatro años después), en la que aparecieron muchas aplicaciones nuevas y propiedades del núcleo. Además, se han producido cambios significativos en relación con el uso de fundición de tipos, union'ov y punteros mecanografiados a las estructuras. Sin embargo, el código de la tubería no ha cambiado mucho. Podemos omitir esta edición.

Xv6, un núcleo simple en forma de Unix


La sexta edición de Unix influyó en la creación del núcleo Xv6 , pero está escrito en C moderno para ejecutarse en procesadores x86. El código es fácil de leer, está claro. Además, a diferencia de las fuentes Unix con TUHS, puede compilarlo, modificarlo y ejecutarlo en otra cosa además de PDP 11/70. Por lo tanto, este núcleo es ampliamente utilizado en las universidades como material educativo sobre sistemas operativos. Las fuentes están en Github .

El código contiene una implementación clara y bien pensada de pipe.c , respaldada por un búfer en la memoria en lugar de un inodo en el disco. Aquí solo doy la definición de "tubería estructural" y función 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()establece el estado del resto de la implementación, que incluye funciones piperead(), pipewrite()y pipeclose(). La llamada real del sistema sys_pipees un contenedor implementado en sysfile.c . Recomiendo leer todo su código. La complejidad está en el nivel de origen de la sexta edición, pero es mucho más fácil y más agradable de leer.

Linux 0.01


Puede encontrar el código fuente para Linux 0.01. Será instructivo estudiar la implementación de tuberías en su fs/ pipe.c. Aquí, el inodo se usa para representar la tubería, pero la tubería en sí está escrita en C. moderna. Si superaste el código de la sexta edición, no experimentarás dificultades. Así es como se ve la función 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;
}

Incluso sin mirar las definiciones de estructuras, puede descubrir cómo se usa el contador de referencia de inodo para verificar si la operación de escritura conduce SIGPIPE. Además del trabajo de bytes, esta función se correlaciona fácilmente con las ideas anteriores. Incluso la lógica sleep_on/ wake_upno se ve tan extraña.

Núcleos modernos de Linux, FreeBSD, NetBSD, OpenBSD


Rápidamente revisé algunos núcleos modernos. Ninguno de ellos ya tiene una implementación de disco (no es sorprendente). Linux tiene su propia implementación. Aunque los tres núcleos BSD modernos contienen implementaciones basadas en código escrito por John Dyson, a lo largo de los años se han vuelto demasiado diferentes entre sí.

Para leer fs/ pipe.c(en Linux) o sys/ kern/ sys_pipe.c(en * BSD), se requiere una verdadera dedicación. El rendimiento y el soporte para funciones como vector y E / S asíncrona son importantes en el código actual. Y los detalles de asignación de memoria, bloqueos y configuración del kernel: todo esto es muy diferente. Esto no es lo que las universidades necesitan para un curso introductorio sobre sistemas operativos.

En cualquier caso, fue interesante para mí descubrir varios patrones antiguos (por ejemplo, generar SIGPIPEy volver EPIPEal escribir en una tubería cerrada) en todos estos núcleos modernos tan diferentes. Probablemente nunca veré una computadora PDP-11 en vivo, pero todavía hay mucho que aprender del código que fue escrito unos años antes de mi nacimiento.

Escrito por Divi Kapoor en 2011, el artículo " La implementación del kernel de Linux de tuberías y FIFOs " proporciona una visión general de cómo funcionan (hasta ahora) las tuberías en Linux. Y la reciente confirmación de Linux ilustra un modelo de interacción canalizado cuyas capacidades exceden las capacidades de los archivos temporales; y también muestra cuán lejos han ido las tuberías del "bloqueo muy conservador" en la sexta edición del kernel de Unix.

All Articles