Como el geekismo de este blog ha bajado mucho últimamente, creo que es hora de contar alguna cosilla friki.
El grueso de la funcionalidad de observación, aquí en el NOT, se obtiene ejecutando programas disponibles en el shell de unas estaciones Linux. Esto nos permite hacer (con bastante rapidez) shell scripts que ejecuten tareas habituales, como las de obtención de imágenes de calibración, observación múltiple de un objeto para aplicar dithering, etc.
Hoy nos ha llegado el jefe (que de astrofísica sabrá un huevo, pero de ordenadores, el pobre...) preguntando si podemos hacer que una tarea se ejecute en segundo plano cada (digamos) 15 segundos dentro de esos scripts. Empezamos diciéndole que sí, pero yo que me huelo las dificultades de sus ideas a la milla, le pregunto: "cuando dices que hay que hacerlo cada 15 segundos, te refieres que la tarea tiene que empezar cada 15 segundos exactamente?". La respuesta, como me temía, fue... "sí". Nuestra respuesta fue: "olvídalo". Solemos hacerlo cuando no estamos seguros de si habrá una manera fácil y queremos desanimarle de entrada ;)
Si alguno no ha captado la sutileza, no es lo mismo ejecutar algo cada 15 segundos que ejecutar, dejar pasar 15 segundos y ejecutar de nuevo. Obsérvese:
El diagrama no necesita mucha explicación. Representa la ejecución de dos tareas y el tiempo crece hacia la derecha. Vemos que la tarea en la parte de arriba se ejecuta cada 5 unidades de tiempo (digamos, segundos), mientras que la segunda se ejecuta cada 5.2 unidades de tiempo. Lo primero es lo que quiere el jefe. En el segundo caso, ponemos a ejecutar la tarea y entonces indicamos que queremos esperar 5 segundos. Ese 0.2 de unidad de tiempo extra se debe a lo que tarda el sistema en hacer cosas (empezar a ejecutar la tarea que va a segundo plano, devolvernos el control, etc) y lo he representado con lo que va desde un "inicio de tarea" hasta las líneas punteadas.
Si hemos dicho que las unidades de tiempo son 5 segundos, puede sorprendernos que el ordenador tarde 2 décimas en permitirnos continuar. En realidad es una exageración, que nos permite ver enseguida los efectos del retraso acumulado por cada ejecución. Imaginemos que podemos obtener información horaria del ordenador por cada ejecución, con precisión de segundos. Si el retraso fuera de 2 centésimas de segundo, veríamos los efectos en menos de 5 minutos. Aún si fuera de 2 milésimas de segundo, en menos de tres cuartos de hora. Es un claro ejemplo de que una piedrecita no hace nada, pero poco a poco haces una montaña. Y en aplicaciones de tiempo real "hard" (no es nuestro caso), es totalmente inaceptable.
El problema que se nos presenta, en particular, es que las herramientas estándar que nos ofrece el sistema no incluyen ningún temporizador, que yo sepa. Todo lo más podemos usar sleep, que sufre el problema de acumulación de retraso. Hay maneras de reducir el impacto del retraso, como solicitar una espera ligeramente menor que el tiempo acordado, o ligeramente mayor, según veamos la evolución del sistema. Pero eso es más bien un parche que una solución y depende muchísimo de la precisión conque podamos especificar las esperas al sistema, y en el caso de sleep la precisión es de segundos. No nos sirve.
Lo que sí puede hacer el shell (tanto bash como tcsh, los dos que usamos) es "capturar" señales y responder a ellas. Claro que necesitamos primero algo que envíe esa señal y que además lo haga en el momento adecuado.
Hemos optado por escribir un pequeño programa en C para esta tarea:
#include <signal.h>
#include <sys/types.h>
#include <sys/time.h>
#include <unistd.h>
int parent;
void lanza(int a) {
kill(parent, SIGALRM);
}
int main() {
struct itimerval timer;
parent = getppid();
timer.it_value.tv_sec = 5;
timer.it_value.tv_usec = 0;
timer.it_interval.tv_sec = 5;
timer.it_interval.tv_usec = 0;
setitimer(ITIMER_REAL, &timer, NULL);
signal(SIGALRM, lanza);
while(1) {
pause();
}
}
Este programa es muy sencillote y no hace otra cosa que enviar una señal (ALRM, en este caso) cada 5 segundos. Si lo fuésemos a usar al final, probablemente lo modificaría para tomar el tiempo de espera como argumento. La función que usa el programa para establecer la temporización es setitimer
, a la que hemos indicado como modo de funcionamiento ITIMER_REAL
, ya que queremos que el "cronómetro" funcione en "tiempo real" (esto quiere decir "todo el tiempo", en lugar de, por ejemplo, sólo cuando el programa tiene el procesador).
La señal se repetirá cada 5 segundos mientras no especifiquemos lo contrario (y aquí tenemos un bucle infinito :P), siendo el único limitante la precisión interna del propio sistema operativo, cosa de la que no podemos escapar. En caso de que la ejecución se retrase un poco, no habrá acumulaciones.
El script que he usado para probarlo es éste:
#!/bin/bash
function printdate () {
date
}
trap printdate 14
./rt &
echo "Sleeping..."
while true
do
wait
done
"rt" es el nombre del programa en C y "14" es el número de la señal SIGALRM.
¿Alguna otra idea feliz?