Me pregunto cómo se implementa internamente sleep/nanosleep. Considere este código:
{ // on a thread other than main() thread while(1) { //do something sleep(1); } }¿Estaría la CPU cambiando de contexto constantemente para comprobar si se ha realizado la suspensión de 1 segundo (es decir, una espera interna ocupada)?
Dudo que funcione de esta manera, demasiada ineficiencia. Pero entonces, ¿cómo funciona?
La misma pregunta se aplica a nanosleep.
Nota: si esto es específico de la implementación/SO, entonces, ¿cómo puedo implementar un esquema más eficiente que no conduzca a un cambio de contexto constante?
La forma típica de implementar sleep() y nanosleep() es convertir el argumento en cualquier escala que use el programador del sistema operativo (mientras se redondea hacia arriba) y agregarle la hora actual para formar una "hora de activación absoluta"; luego dígale al programador que no le dé tiempo de CPU al subproceso hasta después de que se haya alcanzado ese "tiempo de activación absoluto". No se trata de una espera ocupada.
Tenga en cuenta que, independientemente de la escala que utilice el programador del sistema operativo, normalmente depende del hardware disponible y/o que se utilice para controlar el tiempo. Puede ser menor que un nanosegundo (p. ej., APIC local en 80x86 que se usa en el "modo de fecha límite de TSC") o tan grande como 100 ms.
También tenga en cuenta que el sistema operativo garantiza que la demora no será menor de lo que solicita; pero normalmente no hay garantía de que no sea más largo y, en algunos casos (p. ej., subproceso de baja prioridad en un sistema muy cargado), el retraso puede ser mucho mayor de lo solicitado. Por ejemplo, si solicita dormir durante 123 nanosegundos, es posible que duerma durante 2 ms antes de que el programador decida que puede darle tiempo de CPU, y luego podrían pasar otros 500 ms antes de que el programador realmente le dé tiempo de CPU (por ejemplo, porque otros subprocesos están utilizando la CPU).
Algunos sistemas operativos pueden intentar reducir este problema de "dormir mucho más de lo solicitado", y algunos sistemas operativos (por ejemplo, diseñados para tiempo real) pueden proporcionar algún tipo de garantía (con restricciones, por ejemplo, sujeto a la prioridad del subproceso) por el tiempo mínimo entre demoras. vencimiento y recuperar la CPU. Para hacer esto, el sistema operativo/núcleo convertiría el argumento en cualquier escala que use el programador del sistema operativo (redondeando hacia abajo y sin redondear hacia arriba) y puede restar una pequeña cantidad "por si acaso"; para que el planificador active el subproceso justo antes de que expire el retraso solicitado (y no después); y luego, cuando al subproceso se le da tiempo de CPU (después del costo del cambio de contexto al subproceso, y posiblemente después de precargar varias líneas de caché que se garantiza que el subproceso usará), el núcleo esperaría brevemente hasta que la demora realmente haya expirado. Esto permite que el núcleo devuelva el control al subproceso muy cerca para retrasar la caducidad.
Por ejemplo, si solicita dormir durante 123 nanosegundos, es posible que el programador no le dé tiempo de CPU durante 100 nanosegundos, luego podría pasar 10 nanosegundos cambiando a su hilo, luego podría estar ocupado esperando los 13 nanosegundos restantes. Incluso en este caso (donde se realiza la espera ocupada), normalmente no esperará durante toda la duración del retraso. Sin embargo, si la demora es extremadamente corta, el kernel solo hará la espera final ocupada.
Finalmente, hay un caso especial que vale la pena mencionar. En los sistemas POSIX sleep(0); normalmente se abusa de él como yield() . No estoy muy seguro de qué tan legítima es esta práctica: es imposible que un programador admita algo como yield() a menos que ese programador esté dispuesto a perder el tiempo de la CPU haciendo un trabajo sin importancia mientras espera un trabajo más importante.
La especificación POSIX de sleep y nanosleep dicen (énfasis mío)
La función sleep() hará que se suspenda la ejecución del subproceso de llamada hasta que haya transcurrido el número de segundos en tiempo real especificado por el argumento segundos o se entregue una señal al subproceso de llamada y su acción sea invocar una función de captura de señal o para terminar el proceso. El tiempo de suspensión puede ser más largo que el solicitado debido a la programación de otra actividad por parte del sistema.
(Fuente: http://pubs.opengroup.org/onlinepubs/9699919799/functions/sleep.html .)
y
La función nanosleep() hará que se suspenda la ejecución del subproceso actual hasta que haya transcurrido el intervalo de tiempo especificado por el argumento rqtp o se entregue una señal al subproceso que llama, y su acción es invocar una función de captura de señal o terminar el proceso. El tiempo de suspensión puede ser mayor que el solicitado debido a que el valor del argumento se redondea a un múltiplo entero de la resolución de suspensión o debido a la programación de otra actividad por parte del sistema. Pero, salvo el caso de ser interrumpido por una señal, el tiempo de suspensión no será inferior al tiempo especificado por rqtp, medido por el reloj del sistema CLOCK_REALTIME.
(Fuente: http://pubs.opengroup.org/onlinepubs/9699919799/functions/nanosleep.html ).
Lo leí para decir que un sistema compatible con POSIX no puede usar un bucle ocupado para sleep o nanosleep . El subproceso de llamada debe suspenderse de la ejecución.
La implementación exacta no está garantizada aquí, pero puede esperar algunas propiedades.
Por lo general, sleep (3) es bastante inexacto y, como los estados de Linux 'man sleep 3', podrían incluso implementarse utilizando SIGALM (señales). Así que definitivamente no se trata de rendimiento. Definitivamente no se trata de bloqueos de giro, por lo que no puede ser intensivo en CPU.
nanosleep es un animal bastante diferente que podría implementarse incluso usando spinlocks. Lo que es más importante, al menos en Linux, nanosleep man está en la sección 2, que es una llamada al sistema, por lo que al menos debería incluir el cambio al modo kernel. ¿Realmente necesitas su alta resolución?
ACTUALIZAR
Como veo su comentario, recomiendo el uso de select() como man select 3 estados:
#include <stdio.h> #include <stdlib.h> #include <sys/time.h> #include <sys/types.h> #include <unistd.h> int main(void) { fd_set rfds; struct timeval tv; int retval; /* Watch stdin (fd 0) to see when it has input. */ FD_ZERO(&rfds); FD_SET(0, &rfds); /* Wait up to five seconds. */ tv.tv_sec = 5; tv.tv_usec = 0; retval = select(1, &rfds, NULL, NULL, &tv); /* Don't rely on the value of tv now! */ if (retval == -1) perror("select()"); else if (retval) printf("Data is available now.\n"); /* FD_ISSET(0, &rfds) will be true. */ else printf("No data within five seconds.\n"); exit(EXIT_SUCCESS); }Es una mecánica comprobada si necesita dormir en un hilo para algún evento y este evento podría estar vinculado al descriptor de archivo.