ENUNCIADO
Codificad una función que se llame nuevosleep
a la que se le pase el número de segundos que tiene el programa
que esperar e imprima una cuenta atrás de esos segundos por la
salida estándar. Así:
nuevosleep(3);
Y sale:
3, [[un segundo...]] 2, [[un segundo...]], 1, [[un segundo...]] 0.
No se puede usar la función de biblioteca sleep
.
Podéis suponer que existe en el programa una función ya
definida
llamada void nonada(int)
, que no hace nada.
COMENTARIOS
Aunque no se os especificaba prototipo de la función
nuevosleep
,
de ser un poco aguilillas, deberíais haber mirado el de la función
sleep
. La función debe tener alguna manera de señalar
los posibles errores de su funcionamiento al programa. Para ello, ha
de devolver un int
. La práctica con 0.25 es la de
"Master of Puppets". Es la única que cumple con cuatro condiciones
irrenunciables:
- Se registra la señal SIGALRM.
- Se guarda y restaura la señal vieja.
- Se rellenan todos los campos de la estructura
sigaction
.
- El prototipo y el tratamiento de errores es el correcto para
una función de estas características.
#include <signal.h>
#include <stdio.h>
#include <unistd.h>
[...]
int nuevosleep(int segundos)
{
sigset_t conjunto_vacio;
struct sigaction accion_nueva, accion_vieja;
sigemptyset(&conjunto_vacio);
accion_nueva.sa_handler=nonada;
accion_nueva.sa_mask=conjunto_vacio;
accion_nueva.sa_flags=0;
if(sigaction(SIGALRM,&accion_nueva,&accion_vieja)==-1) return -1;
while(segundos>=0)
{alarm(1);
pause();
printf("%d\n",segundos);
segundos--;}
if(sigaction(SIGALRM,&accion_vieja,NULL)==-1) return -1;
return 0; /* Ejecución satisfactoria */}
void nonada()
{}
REVISIÓN (2006)
"Master of Puppets" de seguro no aprobaría esta pregunta si la hiciera
en el examen ahora así. Afortunadamente, las prácticas entregadas
mal
son como los delitos fiscales y prescriben al cabo del tiempo. Haré,
no obstante, penitencia por ello.
El problema radica en el conjunto formado por alarm()
y
pause()
. Será un problema recurrente en esta asignatura.
Al ser dos instrucciones diferentes su ejecución no es
atómica,
es decir, puede ser interrumpida y que el sistema cambie. De otro modo,
dependiendo del reparto de CPU podría ocurrir que:
- Se ejecutara
alarm()
. Se le quitara la CPU al proceso.
- Se recibiera la señal encargada,
SIGALRM
justo entre
las dos instrucciones.
- Se saltara a la manejadora
nonada
.
- Al volver de la manejadora, el proceso cae en el
pause()
pero, al haberse producido ya la señal,
se quedará ahí para siempre.
Bien es cierto que la probabilidad de que esto ocurra es muy, muy baja.
No obstante, hemos de buscar la perfección y la práctica se debe
hacer así:
#include <signal.h>
#include <stdio.h>
#include <unistd.h>
[...]
void nonada()
{}
int nuevosleep(int segundos)
{
sigset_t conjunto_vacio, conjunto_SIGALRM, conjunto_viejo,
conjunto_sin_SIGALRM;
struct sigaction accion_nueva, accion_vieja;
sigemptyset(&conjunto_SIGALRM);
sigaddset(&conjunto_SIGALRM,SIGALRM);
if (sigprocmask(SIG_BLOCK,&conjunto_SIGALRM,&conjunto_viejo)==-1)
return -1;
conjunto_sin_SIGALRM=conjunto_viejo;
sigdelset(&conjunto_sin_SIGALRM,SIGALRM);
sigemptyset(&conjunto_vacio);
accion_nueva.sa_handler=nonada;
accion_nueva.sa_mask=conjunto_vacio;
accion_nueva.sa_flags=0;
if(sigaction(SIGALRM,&accion_nueva,&accion_vieja)==-1) return -1;
while(segundos>=0)
{alarm(1);
sigsuspend(&conjunto_sin_SIGALRM); /* ¿Y el error? */
printf("%d\n",segundos);
segundos--;}
if(sigaction(SIGALRM,&accion_vieja,NULL)==-1) return -1;
if(sigprocmask(SIG_SETMASK,&conjunto_viejo,NULL)==-1) return -1;
return 0; /* Ejecución satisfactoria */
}
Algunos os preguntaréis por qué no se controla el error de
sigsuspend
. La razón es que al recibir SIGALRM
,
sigsuspend
da un error. Si hiciéramos el tratamiento de
errores estándar, aparecería este error sin en realidad haberlo.
Habría que
distinguir entre los verdaderos errores y estos falsos errores.
Para los más exquisitos gourmets de la programación, no
obstante, el tratamiento de errores podría ser:
if (sigsuspend(&conjunto_sin_SIGALRM)==-1 && errno!=EINTR) return -1;
Los errores que pueda dar sigsuspend
son
escasos y tampoco es tan crítico controlarlos.
MENCIONES ESPECIALES
- De Tres & Chicas (0.1 ptos.): por servir de base su código a
Master of Puppets.
ERRATA
- Marzo de 2003: De ShellCode y Brøk3r Dj (0.1 ptos.):
>> Creo que la solución de la práctica 5 tiene un error. En el código
>> de la función nuevosleep viene
>>
>> while(segundos>=0)
>> {alarm(1);
>> pause();
>> printf("%d\n",segundos);
>> segundos--;}
>>
>> con lo que se itera desde segundos hasta 0, es decir, segundos+1
>> iteraciones. Esto hace que la función no espere los segundos que se
>> le pasan como parámetro sino uno más.
>>
>> Una posible solución:
>>
>> while (segundos>0)
>> {
>> alarm(1);
>> printf("%d\n",segundos);
>> pause();
>> segundos--;
>> }
>> printf("0\n");
- La señal SIGALRM tiene como comportamiento por defecto
la muerte del proceso. Esto hace que si no se registra
la señal, aunque sea con una función que no haga nada,
el proceso que la reciba muera. Esto debió llevar a
frustración
a alguno de vosotros y, como solución, se os ocurre:
"¡que muera otro!". En un cruel acto de infanticidio propio
de Herodes el Grande, creáis un hijo para que reciba la
señal
y muera, mientras el padre espera pacientemente su muerte.
Problemas morales aparte :), esta solución no es válida por
la sobrecarga que supone la creación de un nuevo proceso y
el envío de una señal para algo tan sencillo como dormir:
void nuevosleep(int segundos)
{
int hijo, sennal;
if(segundos != 0)
do {
hijo = fork(); /* Y si falla, ¿qué? */
if(hijo != 0){ imprimir(segundos); segundos--; waitpid(hijo, &sennal, 0);}
else { segundos = 0; alarm(1); pause(); }
}while(segundos != 0);
printf("0.\n");
}
- Cuando se programa una función que puede ser usada por otros
programadores en otros programas, hay que intentar interferir
lo menos posible en la configuración que tenga dicho programador.
En el caso que nos ocupa, si hacemos algo como:
struct sigaction sig_alrm; /* Estr. para manejar la señal SIGALRM */
/* Relación de la señal SIGALRM con su función manejadora */
sig_alrm.sa_handler = nonada;
sig_alrm.sa_flags = SA_NODEFER; /* No tiene mucho sentido incluir este flag */
/* Disposición para el manejo de la señal SIGALRM */
if (sigaction (SIGALRM, &sig_alrm, NULL) == -1)
{
perror ("\nError al disponernos a manejar una señal ");
return -1;
}
y el programador que use nuevosleep
tiene registrada
la señal SIGALRM
, perderá el registro.
Por la misma
circunstancia, no se debe imprimir ningún mensaje de error.
Con devolver el código basta.
- Hoy hay para todos. La inocente orden:
fprintf(stdout,"\nMe queda %d segs.",tiempo);
esconde tras de sí dos objeciones: primera, la de la
perífrasis
innecesaria, pues fprintf(stdout,
... es
exactamente equivalente a printf(
..., que
es más corto. Segunda, tenéis costumbre de poner
el carácter
de salto de línea (\n
) al principio del
printf
. Aparte de lo extraño que resulta, pues en
una máquina de escribir no damos primero al retorno del
carro, escribimos y dejamos la línea a mitad,
el carácter de
retorno de carro, normalmente vacía el búfer de salida
hacia la pantalla, esto hace que se vea de inmediato la
salida y que no quede almacenada.
- Y seguimos... ¿Qué hace esto?
for(i=0;i<strlen(argv[1]);i++)
num_segs=num_segs+(argv[1][strlen(argv[1])-i-1]-48)*(int)pow(10,i);
Menos mal que luego viene un comentario que lo explica:
/* Cojo caracter a caracter de la cadena le resto 48 (caracter 0
en decimal y los multiplico por 1,10,100.....*/
Cuando dice "cojo", interpreto "cogemos" :). Hay una función
que hace
tan laborioso cometido y mejor. Se llama atoi
o,
incluso, se puede usar la poco conocida sscanf
. Ambas
transforman una cadena de caracteres en su valor numérico. Tampoco
está mal hacerse sus propias funciones ya hechas, pero mejor.
Al usar la función pow
, que sirve para elevar un
número a una determinada potencia, se está invocando
una función
de tratamiento de números en coma flotante que es, innecesaria y,
quizás muuuuy leeeeenta.
Así, sin red, se podría haber codificado mejor así:
for(i=num_segs=0;i<strlen(argv[1]);i++)
num_segs=10*num_segs+argv[1][i]-48;
- Suma y sigue:
void nuevosleep(int tiempo)
{
struct sigaction nuevo,viejo;
int i;
nuevo.sa_handler=nonada;
if(sigaction(SIGALRM,&nuevo,&viejo)==-1)
{
[...]
La estructura sigaction
tiene tres campos, todos
igual de importantes. Si no los rellenamos, nadie nos garantiza
qué va a haber en ellos ni qué va a interpretar la
función
sigaction
cuando los lea. Hay que rellenar los tres.
© 2006 Guillermo González Talaván.