Cuando un proceso solicita el núcleo para hacer algo que es actualmente imposible pero que quizás sea posible más tarde, el proceso es puesto a dormir y es despertado cuando la solicitud tiene más probabilidades de ser satisfecha. Uno de los mecanismos del núcleo usados para esto es llamado 'cola de espera'.
La implementación de Linux nos permite despertar usando la bandera
TASK_EXCLUSIVE. Con las colas de espera, también puedes usar una cola bien conocida y entonces simplificar
sleep_on/sleep_on_timeout/interruptible_sleep_on/interruptible_sleep_on_timeout, o puedes definir tu propia cola de espera y usar
add/remove_wait_queue para añadir y quitarte desde ella y
wake_up/wake_up_interruptible para despertar cuando se necesite.
Un ejemplo del primer uso de las colas de espera es la interacción entre el asignador de páginas (en
mm/page_alloc.c:alloc_pages()) y el demonio del núcleo
kswapd (en
mm/vmscan.c:kswap()), por medio de la cola de espera
kswapd_wait, declarada en
mm/vmscan.c; el demonio
kswapd duerme en esta cola, y es despertado cuando el asignador de páginas necesita liberar algunas páginas.
Un ejemplo del uso de una cola de espera autónoma es la interacción entre la solicitud de datos de un proceso de usuario a través de la llamada al sistema
read(2) y el núcleo funcionando en el contexto de interrupción para suministrar los datos. Un manejador de interrupciones quizás se parezca a (
drivers/char/rtc_interrupt() simplificado):
static DECLARE_WAIT_QUEUE_HEAD(rtc_wait);
void rtc_interrupt(int irq, void *dev_id, struct pt_regs *regs)
{
spin_lock(&rtc_lock);
rtc_irq_data = CMOS_READ(RTC_INTR_FLAGS);
spin_unlock(&rtc_lock);
wake_up_interruptible(&rtc_wait);
}
Por lo tanto, el manejador de interrupciones obtiene los datos leyendo desde algún puerto de E/S específico del dispositivo (la macro
CMOS_READ() devuelve un par de
outb/inb) y entonces despierta a quien esté durmiendo en la cola de espera
rtc_wait.
Ahora, la llamada al sistema
read(2) puede ser implementada como:
ssize_t rtc_read(struct file file, char *buf, size_t count, loff_t *ppos)
{
DECLARE_WAITQUEUE(wait, current);
unsigned long data;
ssize_t retval;
add_wait_queue(&rtc_wait, &wait);
current->state = TASK_INTERRUPTIBLE;
do {
spin_lock_irq(&rtc_lock);
data = rtc_irq_data;
rtc_irq_data = 0;
spin_unlock_irq(&rtc_lock);
if (data != 0)
break;
if (file->f_flags & O_NONBLOCK) {
retval = -EAGAIN;
goto out;
}
if (signal_pending(current)) {
retval = -ERESTARTSYS;
goto out;
}
schedule();
} while(1);
retval = put_user(data, (unsigned long *)buf);
if (!retval)
retval = sizeof(unsigned long);
out:
current->state = TASK_RUNNING;
remove_wait_queue(&rtc_wait, &wait);
return retval;
}
Lo que pasa en
rtc_read() es esto:
- Declaramos un elemento de la cola de espera apuntando al contexto del proceso actual.
- Añadimos este elemento a la cola de espera rtc_wait
- Marcamos el actual contexto como TASK_INTERRUPTIBLE lo que significa que no será replanificado después de la próxima vez que duerma.
- Chequeamos si no hay datos disponibles; si los hay empezamos, copiamos los datos a la memoria intermedia del usuario, nos marcamos como TASK_RUNNING, nos quitamos de la cola de espera y regresamos.
- Si todavía no hay datos, chequeamos cuando el usuario especificó una E/S no bloqueante, y si es así entonces fallamos con EAGAIN (el cual es el mismo que EWOULDBLOCK)
- También chequeamos si hay alguna señal pendiente y si por lo tanto informamos a las "capas superiores" para reinicializar la llamada al sistema si es necesario. Por "si es necesario" yo entiendo los detalles de la disposición de la señal tal como están especificadas en la llamada al sistema sigaction(2).
- Entonces "salimos", esto es, nos dormimos, hasta que sea despertado por el manejador de interrupciones. Si no nos marcamos como TASK_INTERRUPTIBLE entonces el planificador nos podrá planificar tan pronto como los datos estean disponibles, causando así procesamiento no necesario.
Es también valioso apuntar que, usando una cola de espera, es bastante más fácil implementar la llamada al sistema
poll(2):
static unsigned int rtc_poll(struct file *file, poll_table *wait)
{
unsigned long l;
poll_wait(file, &rtc_wait, wait);
spin_lock_irq(&rtc_lock);
l = rtc_irq_data;
spin_unlock_irq(&rtc_lock);
if (l != 0)
return POLLIN | POLLRDNORM;
return 0;
}
Todo el trabajo es realizado por la función independiente del dispositivo
poll_wait() la cual hace las manipulaciones necesarias en la lista de espera; todo lo que necesitamos hacer es apuntarla a la cola de espera la cual es despertada por nuestro manejador de interrupciones específico del dispositivo.