Programación concurrente en Linux

# Modelo de Entrada/Salida en Linux

Decíamos que un descriptor de fichero es un nombre lógico que se asocia a un fichero, y que es necesario resolver esta asociación para que cuando se ejecuta una llamada `read()` o `write()` la operación se realice satisfactoriamente.

Vimos que la biblioteca estándar establece los nombres `stdin`, `stdout` y `stderr` para los descriptores de entrada estándar, salida estándar y salida estándar de errores, respectivamente, y ahora sabemos que estos se identifican respectivamente por los enteros 0, 1 y 2 en la interfaz de llamadas al sistema.

Las llamadas al sistema usan el descriptor como un índice de la tabla de descriptores de ficheros del programa, que es donde se registra la asociación del descriptor con el dispositivo. Por lo tanto, los descriptores estándar ocupan las tres primeras posiciones de la tabla de descriptores. En principio, la asociación de estas tres posiciones se establece cuando el programa empieza a ejecutarse, ya veremos cómo.

Por supuesto, la tabla de descriptores contiene más posiciones que las reservadas para la entrada/salida estándar. La llamada al sistema `open()` reserva una nueva posición en la tabla para ubicar el descriptor del fichero y devuelve el índice de esa posición como identificador del descriptor. El descriptor que devuelve `open` será simplemente la primera posición libre de la tabla en orden ascendente. Eso significa que nuestros ejemplos de `stee` y `ctee` asignarán el descriptor 3 para el fichero que se pasa como parámetro (al menos cuando los ejecutamos como habitualmente, ya que el tema es algo más complicado). Un descriptor se libera con la llamada al sistema `close()`.

La llamada `open()` hace apuntar la posición reservada en la tabla de descriptores a una estructura (en una tabla de ficheros abiertos) donde se almacena la información necesaria poder gestionar las operaciones sobre ese descriptor, básicamente:

- El nombre del fichero, por ejemplo para poder identificarlo en caso de error.
- El modo de operación especificado como parámetro: solo lectura, solo escritura, ambas o append (añadir), aparte de otros más específicos, que el sistema verificará cuando se use el descriptor por `read`/`write`.
- Los apuntadores a las rutinas de entrada/salida del sistema operativo que habrán de ejecutarse en las operaciones que usen el descriptor. Obviamente, el sistema no ejecuta el mismo código cuando se lee del teclado, que es un dispositivo de caracteres con entrada por interrupción, que cuando se lee de un fichero en el disco, que está ubicado en algún lugar de un dispositivo de bloques que requiere DMA.
- La siguiente posición del fichero a la que se va a acceder. Esto no tiene sentido para un dispositivo como el teclado, pero sí para los ficheros y otros elementos que estudiaremos más adelante. Una llamada `read`/`write` actualiza este apuntador incrementándolo en el número de bytes leídos/escritos en la operación. Linux guarda memoria de esto porque sabe que los ficheros se acceden casi siempre de forma secuencial, evitando así trabajo al programador, que dispone de una llamada `lseek(2)` para posicionar el apuntador en un lugar determinado del fichero cuando quiera saltarse la disciplina de acceso secuencial.
- Un apuntador a otra estructura con la información del i-node del fichero y otra información que permite gestionar el acceso concurrente al fichero, como el número de descriptores que se refieren al fichero, tanto para lectura como para escritura.

La llamada al sistema `open()` lleva a cabo los siguientes pasos:

1. Comprueba que hay una entrada libre en la tabla de descriptores (es altamente improbable que esté llena).
2. Busca el nombre del fichero en el directorio correspondiente. Esto implica el acceso a cada directorio que forma parte de la ruta del fichero, y si el directorio no está cargado en memoria, su transferencia desde el dispositivo.
3. Comprueba los derechos de acceso al fichero por el propietario del programa.
4. Gestiona si el fichero se va a compartir y cómo.
5. Si los pasos anteriores han tenido éxito, se crea el descriptor del fichero, y se establecen valores iniciales para algunos parámetros, como el apuntador al comienzo del fichero.

El paso (4) depende del modo de operación (si en modo `append` o no) y de la naturaleza del fichero abierto. Por ejemplo, para un fichero compartido en modo `append` hay que gestionar la posición de escritura mediante un apuntador compartido, mientras que para otros modos la posición es de uso privado para el descriptor. Además, también se pueden usar concurrentemente por varios programas determinados elementos de comunicación (que siguen siendo ficheros) de los que nos ocuparemos en la última parte del curso, y que requieren compartir punteros de acceso para lectura y para escritura.

Los detalles de cómo se implementan y gestionan las estructuras de entrada/salida los puedes encontrar en la literatura específica sobre la implementación de UNIX o Linux, pero no son el objetivo de este curso. El modelo (simplificado) de funcionamiento de la entrada-salida que hemos descrito permite hacernos una idea de cómo actúan el resto de las llamadas al sistema que operan sobre ficheros. Por ejemplo, cuando un programa elimina un fichero (llamada al sistema `unlink()`) que está siendo usado por otro programa, la semántica de UNIX/Linux establece que el segundo programa puede seguir accediendo al fichero hasta que libera su descriptor (explícitamente con `close()` o cuando el programa acaba). Esto significa que `unlink()` marca el fichero como borrado y su nombre desaparece del directorio, pero no se elimina físicamente (es decir, se mantiene su i-node). La llamada `close()`, además de liberar el descriptor del fichero, decrementa la cuenta de referencias al fichero. Si la cuenta de referencias llega a cero, `close()` libera la entrada del directorio y la referencia al i-node del fichero. Recuerda que el i-node contiene un contador de enlaces al fichero desde directorios, de modo que el i-node (es decir, el fichero) solo se elimina cuando esta cuenta llega a cero.