Semáforos en
C para Linux
A veces es necesario que dos o más procesos
o hilos (threads) accedan a un recurso común (escribir en un
mismo fichero, leer la misma zona de memoria, escribir en la misma pantalla,
etc). El problema es que si lo hacen simultáneamente y de forma incontrolada,
pueden "machacar" el uno la operación del otro (y dejar el fichero
o la memoria con un contenido inservible o la pantalla ilegible).
Para evitar este problema, están los semáforos. Un semáforo
da acceso al recurso a uno de los procesos y se lo niega a los demás
mientras el primero no termine. Los semáforos, junto con la memoria
compartida y las colas de mensajes, son los
recursos compartidos que suministra UNIX para comunicación entre
procesos.
El funcionamiento del semáforo es como el de una variable contador.
Imaginemos que el semáforo controla un fichero y que inicialmente
tiene el valor 1 (está "verde"). Cuando un proceso quiere acceder
al fichero, primero debe decrementar el semáforo. El contador queda
a 0 y como no es negativo, deja que el proceso siga su ejecución
y, por tanto, acceda al fichero.
Ahora un segundo proceso lo intenta y para ello también decrementa
el contador. Esta vez el contador se pone a -1 y como es negativo, el semáforo
se encarga de que el proceso quede "bloqueado" y "dormido" en una cola de
espera. Este segundo proceso no continuará por tanto su ejecución
y no accederá al fichero.
Supongamos ahora que el primer proceso termina de escribir el fichero.
Al acabar con el fichero debe incrementar el contador del semáforo.
Al hacerlo, este contador se pone a 0. Como no es negativo, el semáforo
se encarga de mirar el la cola de procesos pendientes y "desbloquear" al
primer proceso de dicha cola. Con ello, el segundo proceso que quería
acceder al fichero continua su ejecución y accede al fichero.
Cuando este proceso también termine con el fichero, incrementa
el contador y el semáforo vuelve a ponerse a 1, a estar "verde".
Es posible hacer que el valor inicial del semáforo sea, por ejemplo,
3, con lo que pasarán los tres primeros procesos que lo intenten.
Pueden a su vez quedar muchos procesos encolados simultáneamente,
con lo que el contador quedará con un valor negativo grande. Cada
vez que un proceso incremente el contador (libere el recurso común),
el primer proceso encolado despertará. Los demás seguirán
dormidos.
Como vemos, el proceso de los semáforos requiere colaboración
de los procesos. Un proceso debe decrementar el contador antes de acceder
al fichero e incrementarlo cuando termine. Si los procesos no siguen este
"protocolo" (y pueden no hacerlo), el semáforo no sirve de nada.
Código de los Semáforos
Antes de nada, que quede claro que únicamente pretendo dar una
idea sencilla de como funcionan los semáforos. No detallo aquí
todas las opciones de las funciones a utilizar ni explico todas las posibilidades.
Tampoco puedo garantizar que la sintaxis de las funciones y de los parámetros
sea correcta al 100%, aunque sí suministro dos fuentes de ejemplo
que compilan y funcionan. Hay, por tanto, que leer este texto con intención
de hacer únicamente un uso sencillo de semáforos.
Para utilizar los semáforos en un programa, deben seguirse los
siguientes pasos:
- Obtener una clave de semáforo. En
general,
una clave
de recurso compartido, ya que la función que nos sirve para
obtener
dicha clave también vale para memoria
compartida y colas de mensajes.
Para
ello se utiliza la función key_t ftok(char *, int)
a la que se suministra como primer parámetro el nombre y path de
un fichero cualquiera que exista y como segundo un entero cualquiera.
Todos
los procesos que quieran compartir el semáforo, deben
suministrar
el mismo fichero y el mismo entero. En los programas de ejemplo sem1.c
y sem2.c se utilizan "/bin/ls" y el entero 33.
- Obtener un array de semáforos. La
función int
semget (key_t, int, int) nos permite obtener un array de
semáforos.
Se le pasa como primer parámetro la clave obtenida en el paso
anterior,
el segundo parámetro es el número de semáforos que
queremos y el tercer parámetro son flags.
Estos flags permiten poner los permisos de
acceso a
los
semáforos, similares a los ficheros, de lectura y escritura para
el usuario, grupo y otros. También lleva unos modificadores para
la obtención del semáforo.
En nuestro ejemplo pondremos 0600 |
IPC_CREATE
que indica permiso de lectura y escritura para el propietario y que los
semáforos se creen si no lo están al llamar a semget().
Es importante el 0 delante del 600, así el compilador de C
interpretará
el número en octal y pondrá correctamente los permisos.
La función semget() nos
devuelve un
identificador
del array de semáforos. - Uno de los
procesos debe inicializar el
semáforo.
La función a utilizar es int semctl (int, int, int, int).
El primer parámetro es el identificador del array de
semáforos
obtenido anteriormente, el segundo parámetro es el índice
del semáforo que queremos inicializar dentro del array de
semáforos
obtenido. Si sólo hemos pedido uno, el segundo parámetro
será 0.
El tercer parámetro indica qué
queremos
hacer con el semáforo. En función de su valor, los
siguientes
parámetros serán una cosa u otra. En nuestro ejemplo, que
queremos inicializar el semáforo, el valor del tercer
parámetro
es SETVAL.
El cuarto parámetro, aunque
está
definido
como un entero, en realidad admite una unión bastante liada. Si
no queremos complicarnos la vida, bastará pasar como cuarto
parámetro
un 1 si queremos el semáforo en "verde" o un 0 si lo queremos en
"rojo". En el código de ejemplo sem1.c y sem2.c
se utiliza la unión, pero el resultado es el mismo.
Debo advertir además que cuando
utilizé
semáforos con Solaris (versión de UNIX de Sun
Microsystems)
la unión estaba definida en los #include adecuados, pero
en linux (Mandrake), no lo está y hay que definirla en el
código
(según indica el man). - Ya
está todo preparado, ahora sólo
queda usar
los semáforos. El proceso que quiera acceder a un recurso
común
debe primero decrementar el semáforo. Para ello utilizará
la función int semop (int, strcut sembuf *, size_t). El
primer
parámetro es el identificador del array de semáforos
obtenido
con semget(). El segundo parámetro es un array de
operaciones
sobre el semáforo. Para decrementarlo, bastará con un
array
de una única posición. El tercer parámetro es el
número
de elementos en el array, es decir, 1.
La estructura del segundo parámetro
contiene
tres
campos:
- short sem_num que es el índice
del
array del
semáforo sobre el que queremos actuar. En nuestro caso, con un
sólo
semáforo, el índice será 0.
- short sem_op que es el valor en el que
queremos decrementar
el semáforo. En nuestro caso, -1.
- short sem_flg son flags que afectan a
la
operación.
En nuestro caso, para no complicarnos la vida, pondremos 0.
Al realizar esta operación, si el
semáforo
se vuelve negativo, nuestro proceso se quedará "bloqueado" hasta
que alguien incremente el semáforo y lo haga, como
mínimo,
0. - Cuando el proceso termine de usar el
recurso
común,
debe incrementar el semáforo. La función a utlizar es la
misma, pero poniendo 1 en el campo sem_op de la estructura strcut
sembuf.
Como ejemplo de todo esto, están los programitas
sem1.c
y
sem2.c que ya se han mencionado antes. Se compilan
com
make sem1 y
make sem2. Para ejecutarlos, debe ejecutarse
primero
sem1 en una ventana de shell y luego
sem2 en otra ventana
de shell distinta.
sem1 tiene un bucle infinito para entrar en el semáforo.
Escribe en pantalla cuando entra y cuando sale. Como el semáforo
está en "rojo" por defecto, sem1 entra en él y no sale
hasta que sem2 lo indica.
sem2 tiene un bucle de 1 a 10 en el que pone en verde el semáforo
y espera un segundo. El resultado es que sem1 entra en el semáforo,
queda "bloqueado" un segundo y sale del semáforo para volver a entrar
en él y repetir el proceso 10 veces.
Puesto que las llamadas a la función semop() son bastante
"engorrosas" por aquello de rellenar la estructura, suele ser útil
hacer un para de funciones espera_semaforo() y eleva_semaforo()
que realicen este código. Se les podría pasar el identificador
del semáforo y el índice dentro del array de semáforos.
Hay una serie de comandos útiles de unix para manejo
de recursos compartidos (semáfos, memoria compartida y colas).
Estadísticas y comentarios
Numero de visitas desde el 4 Feb 2007:
- Esta pagina este mes: 94
- Total de esta pagina: 183324
- Total del sitio: 44170730