Ahora tenemos un controlador de ratón usable bastante perfecto. Si realmente fueras a probarlo y usarlo en todos los casos eventualmente encontrarías un par de problemas con el. Unos pocos programas no trabajarán con ya que todavía no soporta E/S asíncrona.
Primero déjanos mirar los fallos. El más obvio no es realmente un fallo del controlador sino un fallo al considerar las consecuencias. Imagínate que accidentalmente golpees fuerte el ratón y lo envíes deslizándose sobre la mesa. La rutina de interrupción del ratón añadirá todo el movimiento y lo reportará en pasos de 127 hasta que lo haya reportado todo. Claramente hay un punto lejano desde el cual el valor del movimiento del ratón no es reportado. Necesitamos añadir esto como un límite al manejador de interrupciones:
static void ourmouse_interrupt(int irq, void *dev_id, struct pt_regs *regs) { char delta_x; char delta_y; unsigned char new_buttons; delta_x = inb(OURMOUSE_BASE); delta_y = inb(OURMOUSE_BASE+1); new_buttons = inb(OURMOUSE_BASE+2); if(delta_x || delta_y || new_buttons != mouse_buttons) { /* Algo ha pasado */ spin_lock(&mouse_lock); mouse_event = 1; mouse_dx += delta_x; mouse_dy += delta_y; if(mouse_dx < -4096) mouse_dx = -4096; if(mouse_dx > 4096) mouse_dx = 4096; if(mouse_dy < -4096) mouse_dy = -4096; if(mouse_dy > 4096) mouse_dy = 4096; mouse_buttons = new_buttons; spin_unlock(&mouse_lock); wake_up_interruptible(&mouse_wait); } }
Añadiendo estos chequeos limitamos el rango de movimiento acumulado a algo sensible.
El segundo fallo es un poco más disimulado, y quizás porque es un fallo común. Recuerda, dije que esperando en el bucle por el manejador de lecturas tenía un fallo. Piensa en qué pasa cuando ejecutamos:
while(!mouse_event) {
y una interrupción ocurre aquí, en este punto. Esto causa un movimento del ratón y despierta la cola.
interruptible_sleep_on(&mouse_wait);
Ahora dormimos en la cola. Perdimos el despertar y la aplicación no verá el evento hasta que ocurra el siguiente evento del ratón. Esto llevará justamente a la instancia suelta cuando un botón del ratón se retrasa. Las consecuencias para el usuario serán bastante indetectables con un controlador de ratón. Con otros controladores este fallo podría ser mucho más severo.
Hay dos formas de solucionar esto. La primera es deshabilitar las interrupciones mientras el testeo y mientras que dormimos. Esto funciona porque cuando una tarea duerme cesa de deshabilitar las interrupciones, y cuando se reinicia las deshabilita otra vez. Nuestro código entonces se convierte en:
save_flags(flags); cli(); while(!mouse_event) { if(file->f_flags&O_NDELAY) { restore_flags(flags); return -EAGAIN; } interruptible_sleep_on(&mouse_wait); if(signal_pending(current)) { restore_flags(flags); return -ERESTARTSYS; } } restore_flags(flags);
Esta es la aproximación bruta. Funciona pero significa que gastamos un montón de tiempo adicional cambiando las interrupciones de habilitadas a deshabilitadas. También afecta a las interrupciones globalmente y tiene malas propiedades en máquinas multiprocesadores donde el apagar las interrupciones no es una operación simple, sino que significa hacerlo en cada procesador, esperando por ellos para que deshabiliten las interrupciones y repliquen.
El problema real es la carrera entre la prueba de eventos y el dormir. Podemos eliminar esto usando directamente las funciones de planificación. Realmente esta es la forma que generalmente deberíamos de usar para una interrupción.
struct wait_queue wait = { current, NULL }; add_wait_queue(&mouse_wait, &wait); set_current_state(TASK_INTERRUPTIBLE); while(!mouse_event) { if(file->f_flags&O_NDELAY) { remove_wait_queue(&mouse_wait, &wait); set_current_state(TASK_RUNNING); return -EWOULDBLOCK; } if(signal_pending(current)) { remove_wait_queue(&mouse_wait, &wait); current->state = TASK_RUNNING; return -ERESTARTSYS; } schedule(); set_current_state(TASK_INTERRUPTIBLE); } remove_wait_wait(&mouse_wait, &wait); set_current_state(TASK_RUNNING);
A primera vista esto probablemente parezca magia profunda. Para entender cómo trabaja esto necesitas entender cómo trabajan la planificación y los eventos en Linux. Teniendo un buen dominio de esto es una de las claves para escribir controladores de dispositivos eficientes y claros.
add_wait_queue hace lo que su nombre sugiere. Añade una entrada a la lista mouse_wait. La entrada en este caso es la entrada para nuestro proceso actual (current es el puntero de la tarea actual).
Por lo tanto, empezamos añadiendo una entrada para nosotros mismos en la lista mouse_wait. Esto de cualquier forma no nos pone a dormir. Meramente estamos unidos a la lista.
A continuación establecemos nuestro status a TASK_INTERRUPTIBLE. Otra vez esto no significa que no estamos dormidos. Este flag dice lo que debería de pasar la siguiente vez que el proceso duerma. TASK_INTERRUPTIBLE dice que el proceso no debería de ser replanificado. Él se ejecutará desde ahora hasta que duerma y entonces necesitará ser despertado.
La llamada wakeup_interruptible en el manejador de interrupciones puede ahora ser explicada con más detalle. Esta función es también muy simple. Va a través de la lista de procesos en la tarea que le es dada y cualquiera que esté marcada como TASK_INTERRUPTIBLE la cambia a TASK_RUNNING y dice al núcleo que son ejecutables nuevos procesos.
Detrás de todos los envoltorios en el código original lo que está sucediendo es esto:
Nos añadimos nosotros mismos a la cola de espera del ratón
Nos marcamos como durmiendo
Preguntamos al núcleo para planificar tareas otra vez
El núcleo ve que estamos durmiento y planifica algún otro.
La interrupción del ratón establece nuestro estado a TASK_RUNNING y destaca que el núcleo debería replanificar tareas
El núcleo ve que estamos ejecutándonos otra vez y continúa nuestra ejecución
Esto es porque funciona la aparentemente magia. Porque nos marcamos como TASK_INTERRUPTIBLE y nos añadimos a la cola antes de chequear si hay eventos pendientes, la condición de carrera es eliminada.
Ahora si ocurre una interrupción después de que chequeemos el estado de la cola y antes de llamar a la función schedule en orden a dormir, las cosas resultan. En vez de perder un evento, estamos volviendo a establecer TASK_RUNNING por la interrupción del ratón. Todavía llamamos a schedule pero el continuará ejecutando nuestra tarea. Volvemos a través del bucle y esta vez quizás exista un evento.
No habrá siempre un evento. Entonces nos volveremos a establecer a TASK_INTERRUPTIBLE antes de continuar el bucle. Otro proceso haciendo una lectura quizás haya limpiado el flag de eventos y si es así necesitaremos regresar a dormir otra vez. Eventualmente obtendremos nuestro evento y salimos.
Finalmente cuando salimos del bucle nos quitamos de la cola mouse_wait, ya que no estamos más interesados en eventos del ratón, y ahora nos volvemos a establecer a TASK_RUNNABLE ya que todavía no queremos ir a dormir otra vez.
Nota | |
---|---|
Este no es un tópico fácil. No tengas miedo de releer la descripción unas pocas veces y también de mirar en otros controladores de dispositivos para ver si funciona. Finalmente si todavía no puedes cogerlo, puedes usar el código como modelo para escribir otros controladores de dispositivos y confiar en mí. |