Cómo se implementan las canalizaciones en Unix

Cómo se implementan las canalizaciones en Unix
Este artículo describe la implementación de canalizaciones en el kernel de Unix. Me decepcionó un poco que un artículo reciente titulado "¿Cómo funcionan las canalizaciones en Unix?" resultó no sobre la estructura interna. Sentí curiosidad y busqué en fuentes antiguas para encontrar la respuesta.

¿Qué es?

Las canalizaciones son "probablemente el invento más importante de Unix", una característica definitoria de la filosofía subyacente de Unix de crear pequeños programas y el eslogan familiar de la línea de comandos:

$ 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 tubo (7) и tubo (2):

Las canalizaciones proporcionan un canal unidireccional para la comunicación entre procesos. La canalización tiene una entrada (extremo de escritura) y una salida (extremo de lectura). Los datos escritos en la entrada de la canalización se pueden leer en la salida.

La canalización se crea llamando pipe(2), que devuelve dos descriptores de archivo: uno hace referencia a la entrada de la canalización y el segundo a la salida.

La salida de seguimiento del comando anterior muestra la creación de una canalización 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, "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 proceso padre llama pipe()para obtener descriptores de archivos adjuntos. Un proceso hijo escribe en un descriptor y otro proceso lee los mismos datos de otro descriptor. El shell "cambia el nombre" de los descriptores 2 y 3 con dup4 para que coincida con stdin y stdout.

Sin canalizaciones, el shell tendría que escribir la salida de un proceso en un archivo y canalizarla a otro proceso para leer los datos del archivo. Como resultado, desperdiciaríamos más recursos y espacio en disco. Sin embargo, las canalizaciones son buenas para algo más que evitar archivos temporales:

Si un proceso intenta leer de una canalización vacía, entonces read(2) se bloqueará hasta que los datos estén disponibles. Si un proceso intenta escribir en una canalización completa, entonces write(2) se bloqueará hasta que se hayan leído suficientes datos de la canalización para completar la escritura.

Al igual que el requisito POSIX, esta es una propiedad importante: escribir en la canalización hasta PIPE_BUF bytes (al menos 512) deben ser atómicos para que los procesos puedan comunicarse entre sí a través de la tubería de una manera que los archivos normales (que no brindan tales garantías) no pueden.

Con un archivo regular, un proceso puede escribir toda su salida y pasarla a otro proceso. O los procesos pueden operar en un modo paralelo duro, utilizando un mecanismo de señalización externo (como un semáforo) para informarse entre sí sobre la finalización de una escritura o lectura. Los transportadores nos salvan de todas estas molestias.

¿Qué estamos buscando?

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

Ahora estamos listos para interrogar el código fuente del núcleo bajo la luz de una lámpara brillante para confirmar o refutar nuestro vago modelo mental. Pero siempre esté preparado para lo inesperado.

¿Dónde estamos mirando?

No sé dónde está mi copia del famoso libro.libro de leones« con el código fuente de Unix 6, pero gracias a La Sociedad de Herencia de Unix se puede buscar en línea código fuente incluso versiones anteriores de Unix.

Pasear por los archivos de TUHS es como visitar un museo. Podemos mirar nuestra historia compartida y tengo respeto por los años de esfuerzo para recuperar todo este material poco a poco de viejos casetes e impresos. Y soy muy consciente de esos fragmentos que aún faltan.

Una vez satisfecha nuestra curiosidad sobre la historia antigua de las tuberías, podemos mirar núcleos modernos para comparar.

Por cierto, pipe es la llamada al sistema número 42 en la tabla sysent[]. ¿Coincidencia?

Núcleos de Unix tradicionales (1970-1974)

no encontre ningun rastro pipe(2) ni en PDP-7Unix (enero de 1970), ni en primera edición Unix (noviembre de 1971), ni en código fuente incompleto segunda edicion (junio de 1972).

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

La tercera edición de Unix fue la última versión con kernel escrito en ensamblador, pero también la primera versión con pipelines. Durante 1973 se estaba trabajando para mejorar la tercera edición, se reescribió el núcleo en C y así nació la cuarta edición de Unix.

Un lector encontró un escaneo de un documento en el que Doug McIlroy proponía la idea de "conectar programas como una manguera de jardín".

Cómo se implementan las canalizaciones en Unix
En el libro de Brian KernighanUnix: una historia y una memoria”, la historia de la aparición de los transportadores también menciona este documento: “... colgó en la pared de mi oficina en Bell Labs durante 30 años”. Aquí entrevista con McIlroyy otra historia de El trabajo de McIlroy, escrito en 2014:

Cuando apareció Unix, mi pasión por las corrutinas me hizo pedirle al autor del sistema operativo, Ken Thompson, que permitiera que los datos escritos en algún proceso fueran no solo al dispositivo, sino también a la salida de otro proceso. Ken pensó que era posible. Sin embargo, como minimalista, quería que todas las funciones del sistema desempeñaran un papel importante. ¿La escritura directa entre procesos es realmente una gran ventaja sobre la escritura en un archivo intermedio? Y solo cuando hice una propuesta específica con el pegadizo nombre "tubería" y una descripción de la sintaxis de la interacción de los procesos, Ken finalmente exclamó: "¡Lo haré!".

Y lo hizo. Una tarde fatídica, Ken cambió el kernel y el shell, arregló varios programas estándar para estandarizar la forma en que aceptan la entrada (que podría provenir de una tubería) y cambió los nombres de los archivos. Al día siguiente, los oleoductos fueron muy utilizados en las aplicaciones. Al final de la semana, las secretarias los usaron para enviar documentos de los procesadores de texto a la impresora. Un poco más tarde, Ken reemplazó la API y la sintaxis originales para envolver el uso de canalizaciones con convenciones más limpias que se han utilizado desde entonces.

Lamentablemente, el código fuente de la tercera edición del kernel de Unix se ha perdido. Y aunque tenemos el código fuente del kernel escrito en C cuarta edición, que se lanzó en noviembre de 1973, pero salió unos meses antes del lanzamiento oficial y no contiene la implementación de oleoductos. Es una pena que el código fuente de esta característica legendaria de Unix se haya perdido, quizás para siempre.

Tenemos texto de documentación para pipe(2) de ambas versiones, por lo que puede comenzar buscando en la documentación tercera edicion (para ciertas palabras, subrayado "manualmente", una cadena de literales ^H seguidos de un guión bajo). Este proto-pipe(2) está escrito en ensamblador y devuelve solo un descriptor de archivo, pero ya proporciona la funcionalidad principal esperada:

Llamada al sistema tubo crea un mecanismo de E/S llamado canalización. El descriptor de archivo devuelto se puede utilizar para operaciones de lectura y escritura. Cuando se escribe algo en la canalización, almacena en búfer hasta 504 bytes de datos, después de lo cual se suspende el proceso de escritura. Al leer de la canalización, se toman los datos almacenados en búfer.

Al año siguiente, el kernel había sido reescrito en C y pipe(2) cuarta edición adquirió su aspecto moderno con el prototipo "pipe(fildes)»:

Llamada al sistema tubo crea un mecanismo de E/S llamado canalización. Los descriptores de archivo devueltos se pueden usar en operaciones de lectura y escritura. Cuando se escribe algo en la canalización, se usa el descriptor devuelto en r1 (resp. fildes[1]), almacenado en búfer hasta 4096 bytes de datos, después de lo cual se suspende el proceso de escritura. Al leer de la canalización, el descriptor devuelto a r0 (resp. fildes[0]) toma los datos.

Se supone que una vez que se ha definido una canalización, dos (o más) procesos interactivos (creados por invocaciones posteriores tenedor) pasará datos de la canalización mediante llamadas leer и escribir.

El shell tiene una sintaxis para definir una matriz lineal de procesos conectados a través de una canalización.

Las llamadas para leer desde una canalización vacía (que no contiene datos almacenados en búfer) que solo tiene un extremo (todos los descriptores de archivo de escritura están cerrados) devuelven "fin de archivo". Las llamadas de escritura en una situación similar se ignoran.

más temprano implementación de tubería preservada relata a la quinta edición de Unix (junio de 1974), pero es casi idéntico al que apareció en la siguiente versión. Solo se agregaron comentarios, por lo que se puede omitir la quinta edición.

Sexta edición de Unix (1975)

Comenzando a leer el código fuente de Unix sexta edición (mayo de 1975). en gran parte gracias a Leones es mucho más fácil de encontrar que las fuentes de versiones anteriores:

Durante muchos años el libro Leones fue el único documento sobre el kernel de Unix disponible fuera de Bell Labs. Aunque la licencia de la sexta edición permitía a los profesores utilizar su código fuente, la licencia de la séptima edición excluía esta posibilidad, por lo que el libro se distribuyó en copias mecanografiadas ilegales.

Hoy puede comprar una copia reimpresa del libro, cuya portada muestra a los estudiantes en la fotocopiadora. Y gracias a Warren Toomey (quien inició el proyecto TUHS), puedes descargar Sexta Edición Fuente PDF. Quiero darle una idea de cuánto esfuerzo se dedicó a crear el archivo:

Hace más de 15 años, escribí una copia del código fuente provisto en Leonesporque no me gustó la calidad de mi copia de un número desconocido de otras copias. TUHS aún no existía y no tenía acceso a las fuentes antiguas. Pero en 1988 encontré una cinta antigua con 9 pistas que tenía una copia de seguridad de una computadora PDP11. Era difícil saber si funcionaba, pero había un árbol /usr/src/ intacto en el que la mayoría de los archivos estaban marcados como 1979, que incluso entonces parecían antiguos. Era la séptima edición, o un derivado de PWB, pensé.

Tomé el hallazgo como base y edité manualmente las fuentes al estado de la sexta edición. Parte del código permaneció igual, parte tuvo que ser editado ligeramente, cambiando el token += moderno al obsoleto =+. Algo simplemente se eliminó y algo tuvo que ser reescrito por completo, pero no demasiado.

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

Por cierto, a primera vista, la característica principal del código C antes del período de Kernighan y Ritchie es su brevedad. No es frecuente que pueda insertar fragmentos de código sin una edición exhaustiva para que se ajusten a un área de visualización relativamente estrecha en mi sitio.

Temprano /usr/sys/ken/pipe.c hay un comentario explicativo (y si, hay mas /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í vemos, sin ninguna documentación pública, que las canalizaciones alguna vez usaron archivos como almacenamiento alternativo!

En cuanto a los archivos LARG, corresponden a bandera de inodo LARG, que es utilizado por el "algoritmo de direccionamiento grande" para procesar bloques indirectos para admitir sistemas de archivos más grandes. Dado que Ken dijo que es mejor no usarlos, con gusto aceptaré su palabra.

Aquí está la verdadera llamada al 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 no es tan fácil de entender el código, en parte por cómo "usuario de estructura u» y registros R0 и R1 Se pasan los parámetros de llamada al sistema y los valores de retorno.

Probemos con ialloc() colocar en disco inodo (inodo), y con la ayuda fallac() - almacenar dos expediente. Si todo va bien, estableceremos indicadores para identificar estos archivos como los dos extremos de la canalización, los apuntaremos al mismo inodo (cuya cantidad de referencias se convierte en 2) y marcaremos el inodo como modificado y en uso. Preste atención a las solicitudes de pongo() en rutas de error para disminuir el recuento de referencias en el nuevo inodo.

pipe() vencido a través de R0 и R1 devolver números de descriptor de archivo para lectura y escritura. falloc() devuelve un puntero a una estructura de archivo, pero también "devuelve" a través de u.u_ar0[R0] y un descriptor de archivo. Es decir, el código se almacena en r descriptor de archivo para leer y asigna un descriptor para escribir directamente desde u.u_ar0[R0] después de la segunda llamada falloc().

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

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

Entonces la función readp() в pipe.c lee los datos de la canalización. Pero es mejor rastrear la implementación a partir de writep(). Una vez más, el código se ha vuelto más complicado debido a la naturaleza de la convención de paso 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 canalización. u.u_count. Primero necesitamos bloquear el inodo (ver abajo plock/prele).

Luego verificamos el recuento de referencia de inodo. Siempre que ambos extremos de la tubería permanezcan abiertos, el contador debe ser 2. Nos aferramos a un enlace (desde rp->f_inode), por lo que si el contador es inferior a 2, esto debería significar que el proceso de lectura ha cerrado su final de canalización. En otras palabras, estamos tratando de escribir en una canalización cerrada, lo cual es un error. primer código de error EPIPE y señal SIGPIPE apareció en la sexta edición de Unix.

Pero incluso si el transportador está abierto, puede estar lleno. En este caso, liberamos el bloqueo y nos vamos a dormir con la esperanza de que otro proceso lea desde la canalización y libere suficiente espacio en él. Cuando nos despertamos, volvemos al principio, volvemos a colgar la cerradura y comenzamos un nuevo ciclo de escritura.

Si hay suficiente espacio libre en la canalización, escribimos datos en él usando escribiri(). Parámetro i_size1 el inode'a (con un conducto vacío puede ser igual a 0) apunta al final de los datos que ya contiene. Si hay suficiente espacio para escribir, podemos llenar la canalización desde i_size1 a PIPESIZ. Luego liberamos el bloqueo e intentamos despertar cualquier proceso que esté esperando para leer desde la canalización. Volvemos al principio para ver si conseguimos escribir tantos bytes como necesitábamos. Si no, comenzamos un nuevo ciclo de grabación.

Por lo general, parámetro i_mode inode se utiliza para almacenar permisos r, w и x. Pero en el caso de las tuberías, indicamos que algún proceso está esperando una escritura o lectura usando bits IREAD и IWRITE respectivamente. El proceso establece la bandera y llama sleep(), y se espera que en el futuro algún otro proceso llame wakeup().

La verdadera magia sucede en sleep() и wakeup(). Se implementan en slp.c, la fuente del famoso comentario "No se espera que entiendas esto". Afortunadamente, no tenemos que entender el código, solo mira 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) /* … */

El proceso que llama sleep() para un canal en particular, puede ser despertado más tarde por otro proceso, que llamará wakeup() por el mismo canal. writep() и readp() coordinar sus acciones a través de tales llamadas emparejadas. tenga en cuenta que pipe.c priorizar siempre PPIPE cuando se llama sleep(), así que todo sleep() puede ser interrumpido por una 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 devolver" generalmente se usa cuando hay algunos datos en la canalización. En este caso, usamos leer() leer tantos datos como estén disponibles a partir del actual f_offset leer y luego actualizar el valor del desplazamiento correspondiente.

En lecturas posteriores, la canalización estará vacía si el desplazamiento de lectura ha alcanzado i_size1 en el inodo. Restablecemos la posición a 0 e intentamos despertar cualquier proceso que quiera escribir en la canalización. Sabemos que cuando el transportador está lleno, writep() quedarse dormido en ip+1. Y ahora que la canalización está vacía, podemos activarla para reanudar su ciclo de escritura.

Si no hay nada que leer, entonces readp() puede establecer una bandera IREAD y quedarse dormido en ip+2. Sabemos lo que lo despertará writep()cuando escribe algunos datos en la canalización.

Comentarios sobre leer() y escribiri() le ayudará a comprender que en lugar de pasar parámetros a través de "u» podemos tratarlos como funciones de E/S regulares que toman un archivo, una posición, un búfer 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 al bloqueo "conservador", entonces readp() и writep() bloquear inodos hasta que finalicen u obtengan un resultado (es decir, llamar wakeup). plock() и prele() trabajo simple: usando un conjunto diferente de llamadas sleep и wakeup nos permite despertar cualquier proceso que necesite el bloqueo que acabamos de liberar:

/*
 * 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 podía entender por qué readp() no causa prele(ip) antes de la llamada wakeup(ip+1). La primera cosa writep() llamadas en su ciclo, esto plock(ip), lo que resulta en un interbloqueo si readp() aún no ha eliminado su bloque, por lo que el código debe funcionar correctamente de alguna manera. Si miras wakeup(), queda claro que solo marca el proceso de dormir como listo para ejecutarse, para que en el futuro sched() realmente lo lanzó. Entonces readp() causas wakeup(), desbloquea, establece IREAD y causas sleep(ip+2)- todo esto antes writep() reinicia el ciclo.

Esto completa la descripción de las tuberías en la sexta edición. Código simple, implicaciones de largo alcance.

Séptima Edición Unix (enero de 1979) fue un nuevo lanzamiento importante (cuatro años después) que introdujo muchas aplicaciones nuevas y características del kernel. También ha sufrido cambios significativos en relación con el uso de fundición tipográfica, uniones y punteros tipificados a estructuras. Sin embargo código de tuberías prácticamente no cambió. Podemos saltarnos esta edición.

Xv6, un núcleo simple similar a Unix

Para crear un núcleo Xv6 influenciado por la sexta edición de Unix, pero escrito en C moderno para ejecutarse en procesadores x86. El código es fácil de leer y comprensible. Además, a diferencia de las fuentes de Unix con TUHS, puede compilarlo, modificarlo y ejecutarlo en algo que no sea PDP 11/70. Por lo tanto, este núcleo es muy utilizado en las universidades como material didáctico sobre sistemas operativos. Fuentes están en Github.

El código contiene una implementación clara y reflexiva. tubo.c, respaldado 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 la 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 de todo el resto de la implementación, que incluye funciones piperead(), pipewrite() и pipeclose(). La llamada al sistema real sys_pipe es un contenedor implementado en archivosys.c. Recomiendo leer todo su código. La complejidad está al nivel del código fuente de la sexta edición, pero es mucho más fácil y agradable de leer.

Linux 0.01

Puede encontrar el código fuente para Linux 0.01. Será instructivo estudiar la implementación de gasoductos en su fs/pipe.c. Aquí, se usa un inodo para representar la canalización, pero la canalización en sí está escrita en C moderno. Si ha pirateado el código de la sexta edición, no tendrá ningún problema aquí. 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 estructura, puede descubrir cómo se usa el recuento de referencia de inodo para verificar si una operación de escritura da como resultado SIGPIPE. Además del trabajo byte a byte, esta función es fácil de comparar con las ideas anteriores. Incluso la lógica sleep_on/wake_up no parece tan extraño.

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

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

Leer fs/pipe.c (en Linux) o sys/kern/sys_pipe.c (en *BSD), se necesita verdadera dedicación. El rendimiento y la compatibilidad con funciones como la E/S vectorial y asíncrona son importantes en el código actual. Y los detalles de asignación de memoria, bloqueos y configuración del núcleo varían mucho. Esto no es lo que las universidades necesitan para un curso de introducción a los sistemas operativos.

En cualquier caso, me resultó interesante desenterrar algunos patrones antiguos (por ejemplo, generar SIGPIPE y volver EPIPE al 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 se escribió unos años antes de que yo naciera.

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

Fuente: habr.com

Añadir un comentario