6. Anexo: Descriptores de ficheros y redirecciones

Para facilitar el acceso a los distintos recursos del sistema (zona de almacenamiento en disco, teclado, pantalla, socket,…), el kernel de Linux vincula cada recurso con un fichero, real (espacio en disco) o virtual (en memoria, como /dev y /proc). Ejemplos habituales son:

Recurso

Fichero

Directorio

/directorio

Pantalla

/dev/ttyXX

Sockets TCP

/proc/net/tcp

Fichero regular

/dir/file

Teclado (según USB…)

/dev/uinput

Proceso con PID xxxx

/proc/xxxx

Disco duro

/dev/sda

Ratón

/dev/psaux

Caché ARP

/proc/net

Cuando un proceso necesita acceder a un recurso, debe realizar las siguientes operaciones:

  1. Abrir el fichero correspondiente a dicho recurso: el proceso indica al sistema la ruta del fichero, especificando su intención de abrirlo para lectura (obtener información del recurso), escritura (enviar información al recurso) o lectura/escritura; el sistema concederá el tipo de acceso solicitado según las características del recurso (teclado, pantalla, …) y los permisos que el usuario efectivo del proceso posea sobre el fichero. Como respuesta, el sistema devolverá al proceso el descriptor (o descriptor de fichero) con el que el proceso podrá acceder al fichero.

  2. Acceder al fichero/recurso usando el descriptor suministrado por el sistema: el proceso indicará el descriptor al kernel, éste localiza el fichero y accede al recurso asociado.

    Recurso

    kernel

    Fichero

    proceso

    Descriptor

Un descriptor no es más que un número entero «n», con las siguientes características:

  • Cada proceso tiene su propia tabla de descriptores para acceder a los recursos/ficheros; el fichero que un proceso tenga asociado al descriptor «n» es independiente del que puedan tener asociado para ese mismo descriptor los demás procesos del sistema. El conjunto de descriptores que tiene asociados un determinado proceso puede obtenerse con ls /proc/PID/fd/, cambiando PID por el identificador de dicho proceso.

    Proceso con PID xxxx

    Fichero

    Descriptor

    Fichero_A

    0

    Fichero_A

    1

    Fichero_A

    2

  • Se dice que un descriptor es de entrada, salida o entrada/salida según el fichero al que esté asociado en ese instante (es posible cambiar el fichero al que está asociado un descriptor) haya sido abierto por el proceso que posee dicho descriptor para lectura, escritura o lectura/escritura, respectivamente.

Al trabajar con un intérprete de comandos encontramos que, de manera habitual, los comandos suelen imprimir su información en pantalla y obtenerla del teclado. Este funcionamiento se debe a los dos motivos siguientes:

  1. Muchas aplicaciones, especialmente pensadas para ser usadas en modo consola, son programadas para que, por defecto (sus procesos):

    • Obtengan datos del (recurso asociado al) descriptor 0: dado que éste es el descriptor del que convencionalmente las aplicaciones obtienen información, este descriptor suele denominarse «entrada estándar (a pesar de este nombre, dado su uso especial, internamente los descriptores 0, 1 y 2 son de entrada/salida).

    • Envíen su información al (recurso asociado al) descriptor 1: dado que éste es el descriptor al que convencionalmente las aplicaciones envían su información, este descriptor suele denominarse «salida estándar».

    • Envíen su información de errores al (recurso asociado al) descriptor 2: dado que éste es el descriptor al que convencionalmente las aplicaciones envían su información de errores, este descriptor suele denominarse «salida de error estándar».

    Esta configuración «estándar» es la empleada por los comandos POSIX. Así, por ejemplo, los comandos echo cadena o cat fichero están preparados para imprimir (enviar la información) en el recurso asociado al descriptor 1 (salida estándar), o el comando cat (sin argumentos) está diseñado para obtener la información del recurso asociado al descriptor 0 (entrada estándar).

  2. De forma habitual, el proceso (shell) de cualquier consola en modo comandos asocia el descriptor:

    • 0: al recurso «teclado (siempre, a través del fichero asociado a esos dispositivos (/dev/uinput,…), que puede variar según su tipo).

    • 1: al recurso «pantalla».

    • 2: al recurso «pantalla».

    Por omisión, cuando un proceso padre crea un proceso hijo, el proceso hijo sólo dispone de los descriptores 0, 1 y 2, cada uno asociado al mismo recurso que en el proceso padre (el proceso hijo hereda la asociación descriptor-fichero para esos tres descriptores, el resto de descriptores no se «heredan»). Todo ello lleva a que en una consola de comandos, las órdenes echo cadena o cat fichero suelan imprimir por pantalla (recurso del descriptor 1), o el comando cat (sin argumentos) obtenga la información del teclado (recurso del descriptor 0), al heredar esa asociación descriptor-fichero del shell (proceso padre).

Sobre este comportamiento habitual, los intérpretes de comandos de Linux permiten:

  • Modificar el fichero (recurso) asociado a cada descriptor estándar 0, 1 o 2.

  • Asociar ficheros a los descriptores no estándar (3 y posteriores).

Para ello, la sintaxis a aplicar varía según el proceso sobre el que queramos establecer la nueva asociación fichero-descriptor sea el propio proceso shell que estamos utilizando, o sobre un comando (proceso hijo) invocado desde este shell (en programación C, por ejemplo, se realizaría con la función open, entre otras).

6.1. Asociación para el Proceso Shell

Para ello se emplea el comando exec, mediante las siguientes sintaxis:

Sintaxis

Funcionalidad

exec n< fichero

Asocia el descriptor de entrada n con fichero (archivo regular). Para abreviar, en todo el texto se usará el calificativo “descriptor de entrada” para indicar que en ese instante (puede modificarse) el descriptor está asociado con un fichero abierto para “lectura”. Igual para salida (escritura) y entrada/salida.

exec n> fichero

Asocia el descriptor de salida n con fichero

exec n<> fichero

Asocia el descriptor de entrada y salida n con fichero

exec n<&m

Asocia el descriptor n con el mismo fichero al que actualmente (en el momento de ejecutar este comando) está asociado el descriptor m (duplicado de descriptor).

exec n<&-

Cierra el descriptor n (elimina su asociación con el fichero al que actualmente esté vinculado)

Por ejemplo, el siguiente comando permite asociar el archivo /tmp/fichero con el descriptor 4:

exec 4< /tmp/fichero

A partir de ello,  cada vez que queramos que un comando realice operaciones de lectura/escritura sobre ese archivo, podremos aplicar los operadores de redirección (detallados más abajo) con dicho descriptor (en lugar de usar el nombre del archivo).  Por ejemplo, para que el comando ls envíe su información a ese archivo:

ls >&4

6.2. Asociación para un comando (proceso hijo) invocado desde el shell

Ello se consigue mediante la técnica de «redirección», basada en la siguiente sintaxis (es importante que no haya espacios entre n y op):

comando  [n]op  fichero/descriptor

donde:

  • comando: comando sobre el que aplicar la redirección. Recuerde que, conforme a la sintaxis de los comandos simples, la redirección puede escribirse tanto después como antes del comando.

  • n (número entero, opcional): descriptor (asociado actualmente o no a algún fichero en el shell actual). POSIX exige que se soporten, al menos, los valores 0, 1,…, 9. El valor por omisión depende del operador empleado.

  • op: operador de redirección. Si se «escapa» el descriptor n se usará el descriptor por omisión (e.g. echo \2>fichero asumirá el descriptor 1). Si se escapa el operador de redirección, no se aplicará redirección alguna (e.g., echo 2\>fichero imprime 2\>fichero al recurso del descriptor 1)

  • fichero/descriptor: a asociar (según indique el operador) con el descriptor n.

Al aplicar esa redirección sobre el comando, para el descriptor n, el comando (el proceso hijo que se crea) no heredará (aún en el caso de ser 0, 1 o 2) la asociación descriptor-fichero del proceso shell padre, sino que asociará a dicho descriptor n el fichero explícitamente indicado.

A continuación se resumen los operadores de redirección definidos por el estándar POSIX, agrupadas según operen sobre descriptores para entrada (lectura de fichero) o salida (escritura en fichero):

Redirecciones de entrada: por omisión, toman n=0 (entrada estándar).

Redirección

El shell invoca comando como proceso hijo, configurándolo para que:

cmd [n]< fich

Asocie el descriptor de entrada (recuérde que, para abreviar, se está usando el calificativo «descriptor de entrada» para indicar que en ese instante el descriptor está asociado con un fichero abierto para lectura, pero puede cambiar) n con fich (archivo regular). Por omisión n=0, esto es, cmd < fich hace que para el proceso cmd, el descriptor 0 quede asociado a fich para lectura.

cmd [n]<&m

Asocie el descriptor de entrada n con el mismo fichero al que actualmente está asociado el descriptor m. Dicho fichero estará así referenciado desde dos descriptores (duplicado de descriptores). m es obligatorio.

cmd [n]<&-

Cierre el descriptor de entrada n (elimina su asociación con el fichero al que actualmente esté vinculado). Si el descriptor no está asociado con ningún fichero, dará error.

cmd [n]<< delim

Asocie el descriptor de entrada n con el recurso especial Here-Document (texto empotrado). Tras ejecutar la orden, aparecerá en consola el carácter >. El texto introducido a partir de entonces será guardado en el recurso Here-Document, terminando cuando se introduzca una línea que sólo contenga (incluidos espacios en blanco) la cadena delim (delimitador) y se pulse nueva línea. En dicho momento, cmd será ejecutado (pudiendo acceder al contenido del recurso Here-Document a través del descriptor n). Si al escribir la orden, la cadena delim se introduce entre comillas, el shell las eliminará, de modo que para terminar de escribir en el recurso Here-Document, habrá que escribir la cadena delim sin comillas.

cmd <<- delim

Ídem <<, pero las tabulaciones añadidas al principio de línea será omitidas, no insertándose en el recurso Here-Document.

Redirecciones de salida: por omisión, toman n=1 (salida estándar).

Redirección

El shell invoca comando como proceso hijo, configurándolo para que:

cmd [n]> fich

Asocie el descriptor de salida n con fich (archivo regular). Por omisión n=1, esto es, cmd > fich hace que para el proceso cmd, el descriptor 1 quede asociado a fich. Si fich no existe, será creado; si existe, será limpiado previamente. Salvo que se haya activado en el shell la opción «no sobrescribir» con el comando set -C (recuerde que el comando set permite modificar el comportamiento del shell, incluyendo opciones y el valor de sus variables) en cuyo caso dará error.

cmd [n]>| fich

Ídem >, sin depender de la opción «no sobrescribir» (set -C).

cmd [n]>> fich

Ídem >, sin limpiar previamente el fichero si existe (insertando al final del contenido existente).

cmd [n]>& m

Asocie el descriptor de salida n con el mismo fichero al que está asociado el descriptor m. Dicho fichero estará así referenciado desde dos descriptores (duplicado de descriptores). m es obligatorio.

cmd [n]>& -

Cierre el descriptor de salida n (elimina su asociación con el fichero al que actualmente esté vinculado). Si el descriptor no está asociado con ningún fichero, dará error.

Redirecciones de entrada/salida: por omisión, toman n=1 (salida estándar).

Redirección

El shell invoca comando como proceso hijo, configurándolo para que:

cmd [n]<>fichero

Asocie el descriptor n de entrada y salida con fichero (archivo regular, será creado si no existe).

Además de las anteriores, existe un tipo especial de redirección denominado tuberíapipeline (o pipe), en la cual el «recurso» sería esa «tubería» que conecta dos descriptores. Esta redirección se basa en la siguiente sintaxis:

comando1  |  comando2

bajo la cual, el descriptor 0 de comando2 se asociaría con el descriptor 1 de comando1 (esto es, lo que comando1 envíe a su salida estándar será redirigido a la entrada estándar de comando2). Pueden usarse dos o más comandos separados por |.

A continuación se proponen varios ejemplos de redirección y su explicación:

cat < fichero

Por omisión cat tiene el recurso «teclado» asociado al descriptor 0 (POSIX). Con este comando, cat es invocado para que su descriptor 0 quede asociado al recurso fichero, de modo que cat toma la entrada de dicho fichero.

ls / > fichero

ls es invocado para que su descriptor 1 esté asociado a fichero (y no a la «pantalla» del shell desde el que es invocado), esto es, ls envía su salida a fichero.

cat << fin

Toda la información escrita tras invocar el comando es guardada en el recurso especial Here-Document, pasándose al comando cat cuando se introduzca la línea fin y se pulse nueva línea.

ls | more

El descriptor de salida de ls se conecta con el de entrada de more, de modo que toda la salida de ls de entrega a more como entrada.

Adicionalmente a lo anterior, deben realizarse las siguientes aclaraciones sobre las redirecciones:

  1. Las redirecciones son aplicables a cualquier comando, incluso a subshells (shell abierto desde otro shell), de modo que todos los comandos abiertos desde dicho subshell heredarán por defecto la asociación de los descriptores 0, 1 y 2.

  2. El orden de lectura de los comandos es importante. Por ejemplo, (cat << fin) < fichero no hará que fichero se pase a cat, dado que < fichero no se aplicará hasta que no haya terminado el comando cat. Por ejemplo:

    ls > fichero
    
    #"Ctrl-D" termina (EOF)
    cat
    
    #Teclear fin e intro para terminar
    cat << fin
    ...
    fin
    
    #Teclear fin e intro para terminar
    # no muestra el fichero
    (cat << fin) < fichero
    
  3. El orden en el que se escriban las redirecciones es importante, pudiendo cambiar el resultado. Por ejemplo:

    ls /tmp 2> file 1>&2

    Sucedería lo siguiente (en este orden):

    • El descriptor 2 se asocia con file.

    • El descriptor 1 se asocia con el mismo fichero al que está asociado el descriptor 2, luego con file también.

    • Se ejecuta el comando ls, que enviará sus salidas estándar 1 y de errores 2 a file. En un comando simple, el comando siempre se ejecuta tras aplicar las posibles expansiones/sustituciones e interpretar las redirecciones (leyéndose en orden de izquierda a derecha).

    1>&2 ls /tmp 2> file

    Sucederá lo siguiente (en este orden):

    • El descriptor 1 se asocia con el mismo fichero (o recurso, e.g. pantalla) al que actualmente esté asociado el descriptor 2.

    • El descriptor 2 se asocia con file.

    • Se ejecuta el comando ls, que enviará su salida de errores a file, y su salida estándar 1 con ese recurso (posiblemente pantalla) al que inicialmente estuviese asociado el descriptor 2.

  4. Para redireccionar una información a «ninguna parte» se usa el fichero nulo /dev/null (fichero que podría considerarse asociado al recurso virtual «destructor de información») . Por ejemplo, el siguiente comando haría toda la información que ls envíe a la salida estándar y a la salida de errores sea enviada a /dev/null (se elimina):

    ls -l /usr > /dev/null 2>&1
    

TAREAS

Ejecute y analice el funcionamiento de las redirecciones empleadas en los siguientes comandos:

cat << END > fichero

Lee información del teclado, hasta que se introduce una línea con END. Entonces copia toda la información tecleada al archivo fichero.

ls -l /tmp > fich 2> err

Redirige la salida estándar al archivo fich y la salida de error al fichero err.

ls -l /tmp > fich 2>&1

Redirige las salidas estándar y de error al archivo fich.

ls 2> /dev/null 1>&2

Redirige las salidas estándar y de error al archivo nulo.

Por último, advertir que la explicación anterior corresponde al estándar POSIX. Algunos intérpretes de comandos como bash soportan otros operadores (además de los POSIX). Por ejemplo, para asociar los descriptores 1 y 2 con el recurso fich, con bash se podría realizar con los siguientes comandos (no POSIX):

# bashism (no POSIX)
# Asocia los descriptores 1 y 2 a "fich"
# Hay 2 alternativas equivalentes
cmd &> fich
cmd >& fich

Mientras que el equivalente POSIX es:

# POSIX
# Asocia los descriptores 1 y 2 a "fich"
cmd > fich 2>&1