Bloqueo de archivos

7959048El bloqueo de archivos es una parte muy importante de los sistemas operativos multitareas y multiusuario. Con frecuencia,  los programas necesitan guardar datos, normalmente a través de los archivos, y es muy importante que estos programas dispongan de algún modo para controlar un archivo. Así se podrá actualizar el archivo de un modo seguro, o un programa secundario podrá dejar de intentar leer un archivo que se encuentra en un estado de transición mientras otro programa está escribiendo en él.

Linux dispone de varias prestaciones que puede usar para bloquear los archivos. El método más sencillo es una técnica para crear bloqueos de archivos de un modo atómico, de manera que no pueda tener lugar nada más mientras se está creando el bloqueo. De esta manera un programa dispone de un método para crear archivos que van a ser únicos con toda seguridad y que ningún otro programa podrá haber creado al mismo tiempo.

El segundo método es más avanzado, permite a los programas bloquear partes de un archivo para el acceso exclusivo. Existen dos modos para conseguir este segundo método del bloqueo. Estudiaremos con detalle uno de ellos, ya que el otro es muy similar, sólo se diferencian mínimamente en la interfaz de programación.

 Creación de archivos de bloqueo

Muchas aplicaciones sólo han de poder crear un archivo de bloqueo para un recurso. Después, otros programas pueden comprobar el archivo para ver si se les permite el acceso al recurso. Normalmente, estos archivos de bloqueo se encuentran en una ubicación especial con un nombre que lo relaciona con el recurso que controla. Por ejemplo, cuando se está usando un módem, Linux crea un archivo de bloqueo, normalmente usando un directorio dentro de /var/spool.

Recuerde que los archivos de bloqueo actúan únicamente como indicadores, los programas han de cooperar para usarlos. Se denominan bloqueos opcionales, frente a los bloqueos obligatorios, en los que el sistema hará cumplir el bloqueo.

Para crear un archivo con el fin de usarlo como un indicador de bloqueo, puede usar la llamada al sistema open definida en fcntl.h (que ya vimos en el capítulo anterior) con los indicadores O_CREAT y O_EXCL. Esto le permite comprobar que el archivo no existe todavía y después crearlo en una operación única y atómica.

Creación de un archivo de bloqueo

Podemos verlo en acción creando el programa lock1.c:

bloqueo_listado

Cómo funciona

bloqueo_explicaciones

En los sistemas Linux, el error 17 se refiere a EEXIST, un error usado para indicar que un archivo ya existe. Los números de error se definen en la cabecera errno.h o, más común, por los archivos incluidos por él. En este caso, la definición, realmente en /usr/include/asm-generic/errno-base.h, lee

#define EEXIST 17 /* File exists */

Se trata de un error apropiado para un fallo open (O_CREAT | O_EXCL).

Si lo único que necesita un programa es un recurso exclusivamente durante un breve período de su ejecución, lo cual se suele denominar sección crítica, debería crear un archivo de bloqueo usando el sistema de llamada open antes de introducir la sección crítica, y usar la llamada al sistema unlink para eliminarlo después, cuando el programa salga de la sección crítica.

Puede demostrar cómo cooperan los programas con un mecanismo de bloqueo escribiendo un sencillo programa y ejecutando dos copias al mismo tiempo. Usará la llamada getpid, que le mostramos en el capítulo 4, la cual enviará el identificador del proceso, un número único para cada uno de los programas en ejecución.

Archivos de bloqueo cooperativos

Presentamos a continuación el código del programa de comprobación, lock2.c

lock2_listado

Antes de ejecutar el programa, debería usar primero el comando:

rm -f /tmp/LCK.test2

para comprobar que no existe.

Ejecutamos dos copias y nos da la salida siguiente:

lock2_salida

Esto nos muestra cómo cooperan las dos invocaciones del mismo programa. Si lo intenta, verá, casi con toda seguridad, diferentes identificadores de procesos en la salida, pero el comportamiento del programa será el mismo.

Cómo funciona

lock2_explicacion

lock2_explicacion2

lock2_explicacion3lock2_explicacion4

Al tratarse de una demostración, vamos a esperar poco tiempo. Cuando el programa haya acabado con el recurso, eliminará el archivo de bloqueo, desactivando así el bloqueo. El archivo de bloqueo actúa como un semáforo binario,  proporcionando a cada programa una respuesta sí o no para poder usar ese recurso.

lock2_nota

Regiones de bloqueo

 La creación de archivos de bloqueo es apropiada para controlar el acceso exclusivo a recursos como puertos en serie o archivos a los que rara vez se accede, pero no es tan correcta para acceder a grandes archivos compartidos. Imagine que se dispone de un gran archivo que escribe un programa pero que actualiza varios programas al mismo tiempo. Es posible que ocurra si un programa está registrando datos que se obtienen continuamente durante un largo periodo de tiempo y muchos otros programas lo están procesando. Los programas de procesamiento no pueden esperar a que acabe el programa de registro (porque se ejecuta continuamente) de manera que necesitan un modo de cooperación para proporcionar un acceso simultáneo al mismo archivo.

Puede gestionar esta situación bloqueando ciertas regiones de un archivo, de manera que el resto de programas puedan accederá las secciones no bloqueadas del archivo. Este proceso se denomina bloqueo de un segmento o región de archivos. Linux dispone (como mínimo) de los métodos para hacerlo usando la llamada al sistema fcntl y usando la llamada lockf. En primer lugar estudiaremos la interfaz fcntl porque es la más usada. lockf es bastante parecida y, en Linux, suele ser una alternativa a fcntl. Sin embargo, los mecanismos, de bloqueo fcntl y lockf no trabajan conjuntamente, sino que usan diferentes implementaciones subyacentes, por eso no debería mezclar nunca ambos tipos de llamadas. Sea cual sea la elegida.

La definición de fcntl es:

regiones_bloqueo_sintaxis

fcntl funciona con descriptores de archivos abiertos y, dependiendo del parámetro command, puede realizar diferentes tareas. Las tres opciones de comando para el bloqueo de archivos que merece la pena mencionar son:

bloqueo_explicacion1

bloqueo_explicacion2

bloqueo_explicacion3

bloqueo_explicacion4Cada uno de los bytes de un archivo puede disponer únicamente de un tipo de archivo y una única vez, y debe contener bloqueo del acceso compartido, bloque del acceso exclusivo, o desbloqueo. Veamos las combinaciones de comandos y opciones que existen para la llamada fcntl.

 El comando F_GETLK

El primer comando es F_GETLK. obtiene información de bloqueo sobre el archivo que fildes (el primer parámetro) ha abierto. No intenta bloquear el archivo. El proceso de llamada transmite información sobre el tipo de bloqueo que desearía crear, y fcntl usado junto con el comando F_GETLK envía cualquier información que pudiese evitar el bloqueo.

F_GETLK_tabla

Un proceso puede usar la llamada F_GETLK para determinar el estado actual de los bloqueos en una región del archivo. Debería configurar la estructura flock para indicar el tipo de archivo que requiere y para definir la región que le interesa. la llamada fcntl envía un valor distinto de -1 si tiene éxito, y llenará a la estructura flock de información relevante. Si el bloqueo tiene éxito, la estructura flock no varía. Si la llamada F_FETLK no puede obtener información, envía -1 para indicar el fallo.

Si la llamada F_GETLK tiene éxito (envía un valor distinto de -1), la aplicación de llamada debe comprobar los contenidos de la estructura flock para determinar si fue modificada. Como el valor l_pid comienza con el proceso de bloqueo (si se encontró alguno), es un campo que merece la pena comprobar para determinar si se ha modificado la estructura flock.

El comando F_SETLK

Este comando intenta bloquear o desbloquear una parte del archivo al que hace referencia fildes. Los valores usados en la estructura flock (distintos de los usados por F_GETK) los vemos en la siguiente tabla:

F_GETLK_tabla2

 Al igual que con F_GETLK, la región que se va a desbloquear la definen los valores de los campos l_start, l_whence, y l_len de la estructura flock. Si el bloqueo tiene éxito, fcntl envía un valor diferente a -1, si falla, enviará -1. La función regresa de manera inmediata.

El comando F_SETLKW

El comando F_SETLKW es igual que el comando F_SETLK salvo porque si no le es posible obtener el bloqueo, la llamada esperará hasta que pueda. Una vez que la llamada a comenzado la espera, regresará únicamente cuando haya obtenido el bloqueo o cuando tenga lugar una señal.

Todos los bloqueos que realice un programa en un archivo determinado desaparecen de manera automática cuando se cierra el descriptor de archivo pertinente. También tiene lugar de manera  automática cuando se finaliza el programa.

Uso de read y write con el bloqueo

Al usar el bloqueo sobre ciertas regiones de un archivo, es muy importante usar las llamadas read y write de bajo nivel para acceder a los datos del archivo, en vez de la llamada fread y fwrite de alto nivel. Esto es necesario porque fread y fwrite guardan en la biblioteca los datos leídos o escritos, de manera que si ejecutamos una llamada fread para leer los 100 primeros bytes de un archivo es posible (y casi seguro) que lea más de 100 bytes y que guarde los datos adicionales en una biblioteca. Si el programa usa después fread para leer los 100 siguientes bytes, leerá datos que ya han sido guardados dentro de la biblioteca y no permitirá que una llamada read de bajo nivel obtenga más datos de un archivo.

Para entendernos, por ejemplo, tenemos dos programas que desean actualizar el mismo archivo. Supongamos que el archivo está compuesto por 200 bytes de datos, todo ello ceros. El primer programa comienza y obtiene un bloqueo de escritura sobre 100 primeros bytes del archivo. Después utiliza fread leerá hasta BUFSIZ bytes de una sola vez, de manera que lo que realmente lee es todo el archivo en una memoria, pero sólo transmite nuevamente al programa los 100 primeros bytes.

Después se inicia el segundo programa que contiene un bloqueo write sobre los segundos 100 bytes del programa. Esto tiene éxito porque el primer programa bloqueó únicamente los 100 primeros bytes. El segundo escribe varios números dos en los bytes 100 al 199, cierra el archivo, lo desbloquea y se cierra. Después el primer programa bloquea los 100 segundos bytes del archivo y llama a fread para que los lea. Como la biblioteca ya guardó los datos en una memoria, lo que realmente ve el programa son 100 bytes de ceros, no los 100 números dos que realmente existen en el archivo que está en el disco duro. Este problema no tiene lugar si se usa read y write.

Bloquear un archivo con fcntl

Vamos a ver cómo funciona un archivo de bloqueo que llamaremos: lock3.c. Para probar el bloqueo, necesita dos programas: uno para realizar el bloqueo y otro para la comprobación. Veamos el primer programa que se ocupa del bloqueo:

  1. Empiece con los includes y declaraciones de variables:fcntl_listado1
  2. Abra un descriptor de archivo:fcntl_listado2
  3. Añada algún dato al archivo:fnctl_listado3
  4. Configure la región 1 con un bloqueo compartido, desde el byte 10 al 30:fc ntl_listado4
  5. Establezca un bloqueo exclusivo en la región 2, desde el byte 40 al 50:fcntl_listado5
  6. Ahora bloqueamos el archivo:fcntl_listado6
  7. Espere un momento:fcntl_listado7

Cómo funciona

En primer lugar el programa crea un archivo, lo abre con acceso de lectura y escritura, y después completa el archivo con datos. Más tarde configura dos regiones: la primera desde el byte 10 al 30, con un bloqueo compartido (read) y la segunda del byte 40 al 50, con un bloqueo exclusivo (write). Después pide a fcntl que bloquee las dos regiones y espera durante un minuto antes de cerrar el archivo y salir.

fnctl_como

Por sí mismo, este programa no es muy útil. Necesitamos un segundo programa que compruebe los bloqueos, lock4.c

 Comprobar los bloqueos de un archivo

En este ejemplo, escribiremos un programa que comprueba las diferentes clases de bloqueos que se necesitarán en diferentes regiones de un archivo:

  1. Como de costumbre, comenzamos con include y las declaraciones:lock4_1
  2. Abra un descriptor de archivos:lock4_2
  3. Configuramos la región que desee comprobar:bloqueo_3
  4. Compruebe ahora el bloqueo del archivo:lock_4
  5. Repetimos ahora la comprobación con un bloqueo compartido (read). Configure de nuevo la región que desea comprobar:lock4_5
  6. Comprobamos de nuevo el bloqueo del archivo:lock4_6

Para comprobar el bloqueo, hay que ejecutar en primer lugar el programa lock3, y después ejecutar el programa lock4 para comprobar el archivo bloqueado. Para hacerlo, ejecute el programa lock3 en un segundo plano, con el comando siguiente:

pantalla_bloqueo1

El incitador de comando regresa porque lock3 se está ejecutando en un segundo plano, justamente después hay que ejecutar el programa lock4.

La salida que obtendremos será (he omitido algunas partes para abreviarlo).

pantalla_bloqueo2

Cómo funciona

como_lock4_1como_lock4_2Una vez que se ha completado el programa lock4, tiene que esperar algunos segundos hasta que lock3 completa su llamada sleep y se cierra.

Competir por un bloqueo

Hemos visto cómo se comprueban los bloqueos existentes en un archivo, vamos a ver lo que ocurre cuando dos programas compiten por conseguir un bloqueo sobre la misma sección de un archivo. Volveremos a usar el programa lock3 para bloquear el archivo, y un nuevo programa, lock5, que lo intentará bloquear. Para completar el ejemplo también añadiremos a lock5 algunas solicitudes de desbloqueo.

Competición por los bloqueos

lock5 intenta, en vez de comprobar el estado del bloqueo de diferentes partes del archivo, bloquear regiones de un archivo que ya están bloqueadas:

  1. Comenzamos como de costumbre con los includes de rigor y las declaraciones, después abriremos un descriptor de archivo:competir_bloqueo1
  2. El resto del programa especifica diferentes regiones del archivo e intenta llevar a cabo diferentes operaciones de bloqueo:competicion_bloqueo_2competicion_bloqueo_3competicion_bloqueo_4

Si ejecutamos primero el programa lock3 en un segundo plano, ejecute justo después este programa muevo:

competición_bloqueo_salida

Cómo funciona

lock5_explicacion_pag1

lock5_explicacion_pag2

Otros comandos de bloqueo

Existe un segundo método para bloquear los archivos, la función lockf, que también opera usando los descriptores de archivo. El prototipo es:

otro_comando_bloqueo1

 function puede adoptar los siguientes valores:

otro_comando_bloqueo2

lockf dispone de una interfaz más sencilla que fcntl, principalmente porque dispone de menos funcionalidad y flexibilidad. Para usar la función, debe buscar el inicio de la región que desea bloquear y después solicitarla con el número de bytes a bloquear.

Al igual que el método fcntl de bloqueo de archivos, todos los bloqueos son opcionales, en realidad no evitan la escritura ni la lectura del archivo. Son los programas los que deben ocuparse de los bloqueos. Aún no se ha definido el efecto de unir los bloqueos fcntl y lockf. Lo mejor es que elija uno de los métodos y use siempre el mismo.

Punto muerto

No podíamos acabar el tema de los bloqueos sin mencionar antes a los puntos muertos. Supongamos que dos programas desean actualizar el byte 2 en primer lugar, y después el byte 1. El programa B intente actualizar el byte 1 en primer lugar, y después el byte 2. Ambos programas empiezan al mismo tiempo. El programa A bloquea el byte 2 y el programa B bloquea el byte 1. El programa A intenta bloquear el byte 1. Como ya ha sido bloqueado por el programa B, el programa A espera. El programa B intenta bloquear el byte 2. Como ya ha sido bloqueado por el programa, el programa B también espera.

Esta situación, en la que ninguno de los programas puede tener éxito, se denomina punto muerto o bloqueo fatal. Es un problema común con las aplicaciones de bases de datos en las que muchos usuarios intentan acceder a los mismos datos. La mayoría de bases de datos relacionales de distribución comercial detectan los puntos muertos y los eliminan automáticamente, pero el núcleo de Linux no lo hace. Hace falta una intervención externa, finalizando quizás uno de los programas a la fuerza, para organizar todo el lío resultante.

Los programadores deben tener en cuenta esta situación. Cuando dispone de varios programas esperando a los bloqueos, tiene que considerar concienzudamente la posibilidad de que tenga lugar un punto muerto. En este ejemplo resulta bastante sencillo evitarlo, con solo que, ambos programas bloqueen los bytes necesarios en el mismo orden, o usar una región de bloqueo mayor.

atras2

Responder

Introduce tus datos o haz clic en un icono para iniciar sesión:

Logo de WordPress.com

Estás comentando usando tu cuenta de WordPress.com. Cerrar sesión / Cambiar )

Imagen de Twitter

Estás comentando usando tu cuenta de Twitter. Cerrar sesión / Cambiar )

Foto de Facebook

Estás comentando usando tu cuenta de Facebook. Cerrar sesión / Cambiar )

Google+ photo

Estás comentando usando tu cuenta de Google+. Cerrar sesión / Cambiar )

Conectando a %s