Programación Bizarra
lunes, julio 04, 2011
miércoles, marzo 09, 2011
Las Interrupciones en los Procesadores 80x86
1. Origen de las interrupciones.
En las computadoras con procesadores 80x86 las interrupciones pueden ser originadas por la CPU (las excepciones), “disparando” el pin INTR (interrupciones enmascarables) o el NMI (interrupciones no enmascarables), o por las instrucciones INT 3, INT n, INTO o BOUND (interrupciones por software).
1.1. Excepciones.
Como ya se ha dicho, se nombra de esta manera a las interrupciones originadas por la CPU. INTEL las clasifica, según la dirección de retorno de la rutina o proceso que atiende la interrupción, de la siguiente manera:
Faults: la dirección de retorno es la de la instrucción que se ejecutaba cuando se originó la excepción. Esta excepción permite solucionar el problema y volver a ejecutar dicha instrucción.
Traps: la dirección de retorno es la de la instrucción que le sigue a la que se ejecutaba cuando se originó la excepción. Son usadas exclusivamente para propósito de depuración, como por ejemplo colocar un “breakpoint”.
Aborts: a veces no es posible determinar la dirección de retorno, por lo tanto tampoco es posible continuar con el programa luego de una excepción de esta clase. Son causadas por fallas graves, como errores en el hardware o entradas erróneas en las tablas del sistema.
Algunas excepciones generan un “error code” que proporcionan información sobre estas.
Vector | Mnemonico | Decripción | Tipo | Error code |
0 | #DE | Divide error | Fault | No |
1 | #DB | Debug | Fault/Trap | No |
2 | - | NMI interrupt | Interrupción | No |
3 | #BP | Breakpoint | Trap | No |
4 | #OF | Overflow | Trap | No |
5 | #BR | Range exceeded | Fault | No |
6 | #UD | Invalid opcode | Fault | No |
7 | #NM | No math coprocessor | Fault | No |
8 | #DF | Doble fault | Abort | Si |
9 | - | Coprocesor segment overrun | Fault | No |
10 | #TS | Invalid TSS | Fault | Si |
11 | #NP | Segment not present | Fault | Si |
12 | #SS | Stack-segment fault | Fault | Si |
13 | #GP | General protection | Fault | Si |
14 | #PF | Page fault | Fault | Si |
15 | - | (Intel reserved) | - | No |
16 | #MF | Math fault | Fault | No |
17 | #AC | Alignment check | Fault | Si |
18 | #MC | Machine check | Abort | No |
19 | #XF | Streaming SIMD extensions | Fault | No |
20-31 | - | (Intel reserved) | - | No |
32-255 | - | User defined | Interrupción | No |
1.2. Interrupciones externas.
En las PCs de IBM la CPU tiene conectado un controlador de interrupciones programable 8259 al pin INTR. Los distintos dispositivos se conectan a este chip (por las IRQ) y le solicitan que interrumpa al procesador cuando lo requieran. Este chip no solo debe interrumpir al procesador, sino que también debe indicarle cuál es el vector asociado al dispositivo (se hablará de los vectores luego) que solicitó la interrupción.
A partir de las ATs las PCs comenzaron a venir con dos 8259 conectados en cascada, permitiendo conectar hasta 15 dispositivos.
Estas interrupciones se pueden enmascarar indiscriminadamente desde el procesador, poniendo el flag IF en 0, o selectivamente desde el 8259.
Otra forma de generar interrupciones desde el exterior de la CPU es disparando por el pin NMI, esto ocurre cuando hay una falla de hardware, por ejemplo problemas con la memoria. En este caso el vector es 2 y no es posible enmascarar estas interrupciones.
1.3. Interrupciones por software.
Es posible generar interrupciones con el vector deseado mediante la instrucción INT n. Esto es útil para implementar “breakpoints” y “system calls”. Las system calls son un conjunto de rutinas y procesos que ofrece el sistema operativo a los programadores de aplicaciones para que tengan acceso restringido a los recursos del sistema.
Linux implementa las “system calls” en el vector 0x80, por lo tanto para invocarlas hay que hacerlo con la instrucción INT 0x80.
Anexo 1.A. El controlador de interrupciones 8259.
1.A.1 La interface de interrupción con el 80x86.
Los procesadores 80x86 poseen sólo una línea de entrada sobre la cual pueden recibir una señal de interrupción. El 8259 PIC – Programmable Interrupt Controller (Controlador de Interrupciones Programable) administra las múltiples interrupciones de los múltiples dispositivos, para entregarle las solicitudes al procesador 80x86 de a una. Para esto se lo coloca entre el microprocesador 80x86 y los dispositivos.
Conexión entre los dispositivos y el 80x86 a través del 8259 PIC |
1.A.2 El 8259 PIC (Programmable Interrupt Controller)
El 8259 PIC controla las interrupciones por hardware, permite acoplarse con otros 8259 en cascada soportando hasta 64 niveles de interrupción. La mayoría de las PC sólo tienen 2 de estos alojados en alguna dirección. Uno maneja los IRQ del 0 al 7 y el otro del 8 al 15, dando en total 15 líneas individuales de IRQ.
El PIC nos permite enmascarar IRQs individuales, esto significa que esas solicitudes no podrán llegar al procesador. Cómo hay 2 PICs alojados en diferentes direcciones, debemos determinar cual PIC necesitamos usar. El primer PIC, localizado en la dirección base 0x20h controla los IRQ 0 a IRQ 7. El segundo PIC está localizado en la dirección base 0xA0h, y controla las IRQs del 8 al 15. El formato de bits de la Operation Control Word 1 (OCW1 es la instrucción para enmascarar o desenmascarar las solicitudes) es el siguiente:
PIC maestro | PIC esclavo | ||||
Bit | Disable IRQ | Función | Bit | Disable IRQ | Función |
7 | IRQ7 | Puerto Paralelo | 7 | IRQ15 | Reservado. |
6 | IRQ6 | Disco Flexible | 6 | IRQ14 | Disco Rígido. |
5 | IRQ5 | Reservado/Tarjeta de sonido | 5 | IRQ13 | Coprocesador matemático. |
4 | IRQ4 | Puerto serie | 4 | IRQ12 | PS/2 Mouse |
3 | IRQ3 | Puerto serie | 3 | IRQ11 | Reservado. |
2 | IRQ2 | PIC2 | 2 | IRQ10 | Reservado. |
1 | IRQ1 | Teclado | 1 | IRQ9 | Redirección del IRQ2 |
0 | IRQ0 | Timer del sistema | 0 | IRQ8 | Reloj en tiempo real. |
Asignaciones típicas de IRQ y su correspondiente bit para enmascarar o desenmascarar las solicitudes. | |||||
Como se ve en la tabla y en la siguiente figura, el IRQ2 está conectado al PIC 2, así, si se enmascara este IRQ, se deshabilitarán las IRQ 8 a la 15.
Conexión en cascada entre 2 PICs |
1.A.3 Las Operaciones del PIC
La programación del 8259 consiste de dos acciones básicas:
Primero habilitamos o deshabilitamos cada fuente de interrupción independientemente escribiendo un valor en el registro de máscara de interrupción (IMR). El IMR es un registro del 8259 ubicado en el puerto 21h para el PIC maestro y ubicado en A1h para el PIC esclavo. Cada bit del IMR corresponde a una fuente, si el bit es 0, entonces su correspondiente interrupción esta habilitada. Si el bit del IMR es 1, entonces la fuente de interrupción se deshabilitada (se enmascara) y no puede generar interrupción.
La definición para cada uno de los bits del IMR es la siguiente: el bit 0 corresponde a la IRQ0, el bit 1 a la IRQ1 y así sucesivamente con todos los bits. Por ejemplo, si queremos deshabilitar las interrupciones de todos los dispositivos excepto el teclado. Esto pudiera lograrse con las siguientes instrucciones:
mov al, 0FDh
out 21h, al
O en el lenguaje C, pero manteniendo los valores anteriores del puerto 21h:
outportb(0x21,(inportb(0x21) & 0xFD);
La segunda acción programando el 8259 tiene que ver con indicarle el final de la rutina de servicio a interrupción. Esto se logra enviando al registro de comando del 8259, el comando de fin de interrupción (EOI), representado por 20h. Coincidentemente, a este registro se accede vía el puerto 20h para el PIC maestro y vía A0h para el PIC esclavo, de tal manera que la señal de fin interrupción se envía mandando el comando EOI, como se ejemplifica en las siguientes instrucciones:
mov al, 20h
mov 20h, al
Diagrama de Bloques del 8259 PIC |
Como se puede ver en la figura anterior, las 8 líneas individuales de Interrupt Request, primero pasan por el Interrupt Mask Register (IMR), para ver si han sido enmascaradas o no. Si están enmascaradas, entonces no serán procesadas hasta nuevo aviso. Por otro lado, si no están enmascaradas, estas registrarán sus solicitudes en el Interrupt Request Register (IRR).
El IRR mantendrá todas las solicitudes IRQs hasta que hayan sido tratadas apropiadamente. Si es necesario, este registro podrá ser leído colocando correctamente bits en la Operation Control World 3. El Priority Resolver, simplemente selecciona el IRQ con mayor prioridad. Aunque la más alta prioridad dentro de cada 8259 es la IRQ0 para el maestro e IRQ8 para el esclavo, la conexión a través de IRQ2 causa que IRQ8-IRQ15 sean las siguientes peticiones de prioridad más alta después de IRQ1. Así, el orden de prioridad por omisión para las peticiones de interrupción es IRQ0, IRQ1, IRQ8-IRQ15, IRQ3-IRQ7.
Una vez que el PIC ha determinado cual IRQ procesar, se lo debe decir al procesador, así llamará a la ISR de forma automática. Este proceso es realizado enviando una señal INT al procesador, es decir, es usada la línea INT del procesador. El procesador entonces terminará el proceso de la instrucción actual y reconocerá la solicitud INT con un pulso INTA (Interrupt Acknowledge).
Luego de recibir la señal INTA proveniente del procesador, la IRQ que el PIC está actualmente procesando es almacenada en el In-Service Register (ISR), el cual, como su nombre lo indica, muestra que IRQ está actualmente siendo servida.
Otro pulso INTA será enviado por el procesador, para decirle al PIC que coloque un puntero de 8 bits en el bus de datos, correspondiente al número de IRQ. Si una IRQ atendida por el PIC2 requiere el servicio, entonces el PIC2 enviará el puntero. El procesador maestro PIC1, seleccionará al PIC2 para que envíe el puntero, colocando la identificación de esclavo (Slave ID) del PIC2 en la linea de la cascada, el cual tiene un ancho de tres cables (CAS0, CAS1, CAS2) entre los PICs.
Los cinco (5) bits más significantes del puntero es colocado usando el Initialization Command Word 2 (ICW2). Este será 00001 para el PIC 1 y 01110 para el PIC2. Los 3 bits menos significantes serán enviados para indicar el número de IRQ que pide atención. Por ejemplo, Si el IRQ solicita atención, entonces, el puntero de 8 bits estará formado por 00001 en los 5 bits más significativos y 011 (IR3) para los bits menos significativos. Juntando esto quedará 00001011 o 0x0B, el cual es el vector de interrupciones del IRQ 3 (como se puede ver en la tabla de asignaciones típicas de IRQ mostrada anteriormente en este TP).
El mismo principio se aplica al PIC2. Si el IRQ 10 requiere un servicio, entonces será enviado el byte 01110010, el cual representa la interrupción 72h. Como la IRQ10 está en el PIC2, los 3 bits menos significativos usados serán 010.
Una vez que la ISR ha hecho todo lo necesario, enviará una End of Interrupt (EOI) hacia el PIC, el cual vuelve a poner en uso al In-Service Register. Si la solicitud proviene del PIC2, entonces, las señales EOIs se deberán enviar a ambos PICs. Luego el PIC determinará la próxima interrupción en prioridad y se repite el mismo proceso. Si no hay solicitudes de interrupción presentes, el PIC esperará por la próxima solicitud antes de interrumpir el procesador.
1.A.4 Redireccionamiento de las IRQ2/IRQ9
La IRQ 2 del PIC maestro se usa para conectar a otro PIC como esclavo, no permitiendo que otro dispositivo sea conectado a esta IRQ y la IRQ2 se desvía hacia la IRQ9 (en el segundo controlador). Un dispositivo de hardware usando la IRQ2, instalaría su ISR en INT 0x0A. Por lo tanto será usada una rutina ISR en INT 71h, la cual enviará una EOI al PIC2 y luego llamará a la ISR en INT 0x0A. Parte del código en assembler de la ISR para la IRQ9 sería como el siguiente:
MOV AL,20
OUT A0,AL ; envía EOI al PIC2
INT 0A ; llama a la ISR para la IRQ2
IRET
La rutina sólo envía una EOI al PIC2, como se espera que se ejecute una rutina ISR escrita para la IRQ2, esta se encargará de enviar una EOI al PIC1. Como el PIC2 es inicializado como esclavo sobre la IRQ2, cualquier solicitud usando el PIC2 no llamará a la rutina ISR para la IRQ2. El puntero de 8 bits provendrá del PIC2.
Anexo 1.B. Interrupciones en sistemas con más de un procesador.
1.B.1 SISTEMAS SMP
El sistema SMP – Symmetrical multiprocessing agrega unos pocos requerimientos al manejo de interrupciones al hardware y software respecto al sistema “normal” de UP – UniProcessing. Obtener ventajas del paralelismo requiere sincronización., por ejemplo sólo una CPU debe controlar una interrupción de una cierta interrupción de hardware. En segundo lugar, es necesario un mecanismo eficiente para pasar mensajes entre las CPUs, para planificar tareas entre las CPUs y para diferentes propósitos de sincronización.
1.B.2 APIC
Para permitir que se distribuyan completamente el control entre CPUs en un sistema SMP, Intel ha desarrollado I/O APIC (Advanced Programmable Interrupt Controller), el cuál remplaza el 8259A Programmable Interrupt Controller de los sistemas UP.
Un APICs local posee registros de 32-bits, un clock interno, un dispositivo temporizador (timer), y dos líneas de IRQ adicionales reservadas para interrupciones locales. Las interrupciones locales son usadas típicamente para reiniciar el sistema.
Las I/O APIC consisten en un conjunto de IRQs, una tabla de redirección de interrupciones de 24 entradas, registros programables y una unidad de mensajes para enviar y recibir mensajes APIC sobre el bus ICC (Interrupt Controler Communication). Cada entrada en la tabla de redirección pueden ser programadas individualmente para indicar el vector de interrupciones y la prioridad, la CPU destino y cómo se selecciona la CPU. La tabla es usada para trasladar un mensaje de cualquier IRQ externa mediante una señal a una o más unidades APIC locales vía el bus ICC.
Las solicitudes de interrupción pueden ser distribuidas a las CPUs de dos maneras diferentes: Modo Fijo (Fixed Mode) o Modo de Prioridad más baja (Lowest-Priority Mode). Un rasgo importante en el APIC es que permite a las CPUs generar interrupciones entre los procesadores (interprocessor interrupts - IPIs). La CPU puede almacenar el vector de interrupciones y el identificador de los objetivos de la APIC local en el registro ICR – Interrupt Local Register de su propia APIC. Entonces un mensaje es enviado por el bus ICC a la APIC local del objetivo, la cuál entonces provoca la interrupción correspondiente a su CPU.
Hay soporte para múltiples I/O APICs en el kernel 2.4.18-10.
2. Tratamiento de las interrupciones
2.1 Interrupt descriptor table (IDT).
Se trata de un arreglo de hasta 256 elementos (pueden ser menos) de 8 bytes cada uno. Los programadores de un sistema operativo pueden decidir ubicarlo en cualquier lugar de la memoria física, luego cargarán el registro “IDTR” con su dirección y la cantidad de bytes que ocupa menos 1. La instrucción para hacer esto es “LIDT”, pero solo puede ser ejecutada por los procesos o procedimientos que tienen el máximo privilegio.
Si el vector hace referencia a una entrada más allá del límite de la tabla, la excepción “general-protection exception” es generada.
Relación entre el registro IDTR y la IDT. |
2.2 Formas de atender a las interrupciones.
Los procesadores 80386 o superiores proporcionan dos formas básicas de atender a una interrupción dependiendo de su correspondiente entrada en la IDT, una interrupt-gate o trap-gate, o una task-gate. La diferencia es que de la primera forma no se produce un “context switch”.
Los siguientes campos forman parte de una interrupt-gate o trap-gate:
Segment selector: indica en qué segmento de memoria se encuentra la rutina que atiende a la interrupción.
Offset: indica en que posición del segmento está la primera instrucción de la rutina.
DPL: indica cuál debe ser el privilegio para acceder a la rutina. Este campo no es tenido en cuenta por el procesador cuando la interrupción es generada por hardware (ya sea interna o externa), sin embargo cuando la interrupción es generada por la instrucción “INT” es importante.
P: indica si el segmento al que apunta el campo “Segment selector” es válido. Si está en 0 y se produce la interrupción, se generará una excepción.
D: Indica si “la gate” apunta a código de 32 bits o 16 bits.
Las estructuras de la interrupt-gate y de la trap-gate. |
Cuando una interrupción ocurre y en la entrada de la IDT hay una iterrupt-gate o trap-gate el procesador guarda en la pila los flags, la dirección de la instrucción que se iba a ejecutar (determinada por los registros CS y EIP) y el código de error (si existe), en ese orden. Luego salta al procedimiento que atenderá la interrupción. Si se trata de una interrupt-gate las interrupciones se deshabilitarán automáticamente, se obtendría el mismo efecto si se pusiera una trap-gate y luego se invocara a la instrucción “CLI” (clear interrupt flag) al comienzo de la rutina que atiende la interrupción. Es responsabilidad de la persona que programó esta rutina preservar el estado de todos los registros. Al finalizar se deberá volver al procedimiento que se estaba ejecutando, mediante la instrucción “IRET” (esta instrucción es similar a “RET”, con la única diferencia que también restaurará los flags).
La pila antes y luego de que se produzca una interrupción atendida por una interrupt-gate o trap-gate. |
Si en la entrada hay una task-gate la interrupción será atendida por otro proceso, entonces se produce un context switch. En este caso el procesador realiza las siguientes tareas:
Todos los registros de la CPU que contienen información que le corresponde al proceso en ejecución son salvados automáticamente.
La información necesaria para que el procesador sepa hacia qué proceso volver cuando termine de atender la interrupción es almacenada.
El flag “NT” (nested task) se pone en 1 para indicar que hay una anidación de procesos (información que será necesaria para la intrucción “IRET”).
El proceso en ejecución toma el estado de “BUSY” (ocupado), significa que no podrá continuar hasta que termine el que atiende a la interrupción.
Los registros son cargados con la información que le corresponde al nuevo proceso y comienza la ejecución.
Al terminar se deberá volver con la instrucción “IRET”, que producirá un context switch al verificar que el flag “NT” está en 1.
Los siguientes campos forman parte de una task-gate:
Segment selector: indica el segmento que contiene información acerca del proceso.
DPL: indica cuál debe ser el privilegio para hacer un context switch hacia el proceso. Este campo no es tenido en cuenta por el procesador cuando la interrupción es generada por hardware (ya sea interna o externa), sin embargo cuando la interrupción es generada por la instrucción “INT” es importante.
P: indica si el segmento al que apunta el campo “Segment selector” es válido. Si está en 0 y se produce la interrupción, se generará una excepción.
La estructura de la task-gate. |
Las ventajas que ofrece atender una interrupción con otro proceso son que todos los registros son salvados automáticamente y que la rutina puede ser aislada otorgándole un espacio de direcciones lógicas distinto. Por otro lado la cantidad de información que el procesador guarda automáticamente en el context switch hace que pueda ser una opción mucho más lenta que la interrupt-gate o trap-gate.
2.3 Inicializando la IDT
Antes de que el Kernel habilite una interrupción, debe cargar la dirección inicial de la IDT en el registro idtr e inicializar todas las entradas de la tabla. Linux usa las interrupts gates para controlar interrupciones y las trap gates para controlar excepciones. Linux no usa task gates.
Interrupt Gate: Todos los controladores de interrupciones.
System Gate: 4 controladores de excepciones (3,4,5 y el vector 128)
Trap Gate: Todos los controladores de excepciones excepto el 4.
Insertar los gates en la IDT
set_intr_gate(n, addr)
Inserta una interrupt gate, el campo DPL (Descriptor Privilege Level = 0), entonces no puede ser accedido por programas en modo usuario.
set_system_gate(n, addr)
Inserta una system gate, el campo DPL = 3, esto significa que pueden ser accedidas por programas en modo usuario. Por ejemplo, en Linux, el vector 128 accedido por una system call iniciada por una INT 0x80. La INT3, INTO, BOUND y la INT 0x80 pueden ser accedidas por procesos en modo usuario.
set_trap_gate(n, addr)
Inserta una trap gate, DPL = 0. Son usadas para activar las rutinas de atención a excepciones.
Estas funciones insertan el gate descriptor en la n-ésima entrada de la IDT. El parámetro addr identifica el desplazamiento (offset) del segmento en el código del Kernel, el cual es la dirección base de la rutina de atención. Ejemplo
set_system_gate(0x80, &system_call); (en sched.c y traps.c)
Anexo 2.A. Interrupciones en Linux.
2.A.1 Linux System Calls
Se invocan ejecutando int $0x80, quiere decir que:
El número de vector de excepciones programadas es el 128.
La CPU cambia al modo kernel y ejecuta la función del kernel.
El proceso llamado pasa el número de system call (syscall number) identificando al system call en el registro eax (en los procesadores Intel).
El controlador syscall es responsable de:
Guardar los registros en la pila en modo kernel.
Invocar a la rutina de servicio de la syscall.
Salir llamando ret_from_sys_call().
La tabla para enviar system calls:
Asocia un número de syscall con la rutina de servicio correspondiente,
Almacenada en el arrar sys_call_table tiene NR_syscall entradas (256 máximo, el linux 2.4.18 tiene 237).
El n-ésimo registro contiene la dirección de la rutina de servicio de la syscall n.
2.A.2 Iniciando las System Calls
Cuando el sistema bootea, es llamada la función arch/i386/kernel/traps.c:trap_init(), esta coloca la entrada correspondiente al vector 0x80 (tipo 15, DPL 3) en la IDT (set_system_gate(0x80, &system_call);), apuntando a la dirección de la system_call en arch/i386/kernel/entry.S.
Cuando una aplicación en espacio de usuario hace una system call, los argumentos son pasados vía registro y la aplicación ejecuta la instrucción ‘int 0x80’. Esto causa una trap, se pasa a modo kernel, y el procesador salta a la system_call apuntada en entry.S.
2.A.3 System Calls Estáticas
Una de las características más importantes de Unix es la clara distinción entre el “espacio del kernel” y el “espacio del usuario”. Linux ofrece a los procesos que corren en el espacio del usuario un conjunto de interfaces para interactuar con dispositivos hardware como la CPU, discos, impresoras, etc. El sistema Linux implementa la mayoría de las interfaces entre procesos en modo usuario y dispositivos hardware con las llamadas System Calls emitidas al kernel. Una de las últimas versiones del Kernel de Linux (versión 2.4.18) tiene 237 system calls. Las system calls pueden ser llamadas también System Request o syscall.
En general, se supone que proceso no posee acceso al kernel. No puede acceder a la memoria del kernel y no puede llamar a funciones del kernel. El hardware de la CPU hace esto posible (esta forma de control se llama “modo protegido”).
Las System calls son una excepción a esta regla. Un proceso llena registros con los valores apropiados y entonces, llama a una instrucción especial que salta a una dirección definida en el Kernel. En las CPUs de Intel, esto es posible por la llamada interrupt 0x80. El hardware sabe que una vez que se salta a esta dirección, no se está corriendo en el restringido modo de usuario.
Para indicar al kernel el número de la system call, debe almacenarse dicho número en el registro EAX. Antes de cambiar al modo kernel, el procesador debe guardar todos los registros y derivarle la ejecución a la función del kernel apropiada, antes debe verificar que EAX no esté fuera de rango. El archivo uniste.h contiene los números de las system calls.
2.A.4 Bottom Halves
Bottom halves son la forma en que Linux retrasa el procesamiento. Se usan en los controladores de interrupciones. En un controlador de interrupciones, no se procesan otras interrupciones o eventos de ninguna clase mientras este se está ejecutando (las interrupciones son deshabilitadas), Si se quiere poder atender otras interrupciones mientras el controlador se está ejecutando y dejar el resto de la manipulación para más tarde (porque consume mucho tiempo), pero antes que el retorno al espacio de usuario, se debe crear una bottom half. A una Bottom Half también se la conoce como softirq.
Uno de los principales problemas con el manejo de las interrupciones es como realizar largas tareas dentro de un controlador de interrupciones. A veces, se necesita hacer un trabajo considerable como respuesta a una interrupción de un dispositivo, pero un controlador de interrupciones debe terminar rápidamente y no mantener bloqueadas las interrupciones por mucho tiempo. Estas dos necesidades (mucho trabajo y velocidad) entran en conflicto mutuamente.
La solución a este problema es dejar para el controlador de interrupciones lo inmediatamente necesario, usualmente leer algo del hardware o enviar algo al hardware, y entonces programar el control de nueva información para más tarde (en la “bottom half”) y retornar del controlador. El Kernel garantiza entonces que llamará a la bottom half tan pronto como sea posible, antes que se retorne a modo usuario, y cuando hace esto, todo lo permitido a los módulos del kernel estará permitido ahora.
Así los controladores de interrupciones están divididos en dos partes: una top y una bottom half (mitad de arriba y mitad de abajo). La top half es el verdadero controlador de interrupciones: a veces solo le dice al kernel que ejecute la bottom half, y sale. El kernel garantiza que la top half nunca reentrará. Si llega otra interrupción, será encolada hasta que la top half termine. Como la top half deshabilita las interrupciones, debe ser rápida.
La bottom half correrá antes que otra interrupción sea procesada, o su tiempo se haya terminado. Las interrupciones no son deshabilitadas mientras una bottom half está corriendo, entonces estas pueden hacer trabajos “lentos”.
2.A.5 Implementación
Puede haber hasta 32 controladores bottom half diferentes; bh_base es un vector de punteros a cada una de las rutinas bottom half del kernel; bh_active y bh_mask tienen los bits colocados (activados) de acuerdo a que controladores han sido instalados y cuales están activos. Si el bit N de bh_mask está colocado, entonces el N-ésimo elemento de bh_base contiene la dirección de la rutina bottom half. Si el bit N de bh_active está colocado, entonces la N-ésima rutina bottom half debe ser llamada tan pronto como el planificador crea razonable. Estos índices son definidos estáticamente, la rutina bottom half del timer posee la priridad más alta (índice 0), la rutina bottom half de la consola es la próxima en prioridad (índice 1), etc.
Algunas de las rutinas bottom half del kernel son específicas del dispositivo, pero otras son más genéricas. Por ejemplo, el controlador del TIMER es marcado como activo cada vez que el timer del sistema interrumpe y es usado para manejar los mecanismos de cola del temporizador del kernel.
Cada vez que un controlador de dispositivos, o alguna otra parte del kernel, necesita planificar trabajo para ser hecho después, este añade trabajo a la cola del sistema apropiada, por ejemplo a la cola del timer, y entonces le indica al kernel que alguna rutina bottom half necesitar ser hecha. Esto lo hace colocando el bit apropiado en bh_active. El bit 8 es colocado si el controlador ha encolado algo en la cola inmediata y desea que se ejecute y se procese el controlador bottom half inmediato. La máscara de bits bh_active es verificada al final de cada system call, justo antes que el control sea devuelto al proceso llamador. Si hay algún bit colocado, las rutinas bottom half activas son llamadas. Primero se verifica el bit 0, luego el bit 1, y así hasta el bit 31.
Cada vez que una rutina de la bottom half es llamada el bit en bh_active es borrado. Así, cada activación sólo causa una ejecución. bh_active es transitorio; sólo tiene significado entre llamadas al planificador (scheduler) y es una forma de no llamar rutinas bottom half cuando no tienen trabajo que hacer.
Las Bottom halves que están esperando serán ejecutadas sólo cuando uno de los siguientes eventos ocurre:
El kernel finaliza la atención de una excepción.
El kernel finaliza la atención de una system call.
El kernel finaliza la atención de una interrupción.
El kernel ejecuta la función schedule() para seleccionar un nuevo proceso.
2.A.6 Ejemplo
El kernel necesita un sistema de sincronización para: mantener la cuenta de la hora y fecha para ser usada por ejemplo por gettimeofday(), y mantener el temporizador que notifica al kernel o a un programa de usuario que un intervalo de tiempo ha terminado.
Cada interrupción del timer: Actualiza los “momentos” (jiffies); Actualiza la hora y fecha; Determina cuanto tiempo un proceso se ha estado ejecutando y lo precede su porción de tiempo ha terminado; Actualiza las estadísticas de los recursos usados; Invoca funciones para intervalos de tiempo terminados.
Cuando una señal sobre la IRQ 0 es generada, se invoca timer_interrupt(), deshabilitando las interrupciones (la flag SA_INTERRUPT es activada para indicarlo). Finalmente do_timer() es ejecutada. Simplemente incrementa los “momentos” y asigna otras tareas a los controladores bottom half. Las bottom half actualizan la fecha y hora, estadísticas, ejecuta sus funciones antes que su intervalo de tiempo finalice e invoca schedule() si es necesario, para replanificar procesos.
do_timer() corre con las interrupciones deshabilitadas, y debe ser ejecutado lo más rápidamente posible. Esta simplemente actualiza un valor fundamental mientras que delega otras actividades a dos bottom halves.
La timer_bh() asociada con la bottom half TIMER_BH invoca la update_times() y run_timer_list().
La función update_times() invocada por la bottom half TIMER_BH actualiza xtime deshabilitando las interrupciones.
Cada vez que algún código quiere planificar una bottom half para que sea ejecutada, este llama a mark_bh. En la vieja implementación de BH, mark_bh colocaba un bit en la máscara, permitiendo a la correspondiente rutina bottom half ser encontrada rápidamente en tiempo de ejecución. En los kernel modernos, esta sólo llama traklet_schedule para planificar la rutina de bottom half para la ejecución.
División entre espacios de usuario y de kernel. |
Un ejemplo de uso para las Bottom half es en las interfaces de red: una Top Half obtiene el paquete de datos y la top half la procesa.
2.A.7 Interrupciones de Software
La diferencia entre software interrupts y bottom halves es que las bottom halves son estrictamente serializadas (2 bottom halves no pueden ser ejecutadas al mismo tiempo), las software interrupt no es serializada, así se da la posibilidad de correr 2 mismas interrupciones en 2CPUs, en este caso una software interrupt puede ser reentrante.
2.A.8 Tasklet
Es un mecanismo similar a las bottom half, construidas sobre las software interrupts pero serializadas. 2 CPU pueden ejecutar 2 tasklets al mismo tiempo, pero esas tasklet deben ser diferentes.
2.A.9 LINUX 2.4 Bottom Half
Las Bottom half continúan en Linux 2.4, pero son construidas sobre las tasklets. No pueden correr al mismo tiempo en diferentes CPU, esto degrada el rendimiento significativamente de sistemas multiprocesadores.
2.A.10 Colas de Tareas
Las colas de tareas pueden ser pensadas como extensiones dinámicas de las viejas bottom halves.
2.A.11 Beneficios de usar Bottom Halves
Las Bottom halves son el mecanismo más viejo para diferir la ejecución de tareas al kernel y están disponibles desde el Linux 1.x.
La existencia de las bottom halves es muy importante para realizar responsabilidades del kernel atendiendo interrupciones de múltiples dispositivos rápidamente.
Los controladores de dispositivos y otras partes del kernel de linux pueden encolar el trabajo por hacer.
Anexo 2.B. Programa de ejemplo.
Es un programa muy sencillo que muestra cómo se construye la IDT, cómo se inicializan los 8259 y cómo se hacen las rutinas para atender a las interrupciones. Lo único que hace es esperar que pasen 20 segundos aproximadamente o que el usuario presione la tecla ESC. Consta de funciones escritas en C y en assembler y está hecho para correr en modo real (aunque luego pase a modo protegido), por eso es conveniente enlazarlo para DOS.
Archivo: ejprog.asm |
GLOBAL _read_cr0, _write_cr0, _lgdt, _lidt, _update_cs, _enable, _disable, \ _timer_isr, _kbd_isr EXTERN _timer_handler, _kbd_handler SEGMENT _TEXT PUBLIC CLASS=CODE USE16 _read_cr0: ; devuelve el contenido del registro cr0 mov eax, cr0 mov edx, eax shr edx, 16 retn ; dx:ax es contienen los datos que se devuelven _write_cr0: ; carga el registro cr0 con el parámetro push bp mov bp, sp mov eax, [ss:bp+4] mov cr0, eax pop bp retn _lgdt: ; carga el registro GDTR con el parámetro push bp mov bp, sp push bx mov bx, [ss:bp+4] lgdt [ds:bx] pop bx pop bp retn _lidt: ; carga el registro IDTR con el parámetro push bp mov bp, sp push bx mov bx, [ss:bp+4] lidt [ds:bx] pop bx pop bp retn _update_cs: ; carga el registro CS con el parámetro push bp mov bp, sp mov ax, [ss:bp+4] push ax push word .1 retf .1: pop bp retn _enable: ; pone en 1 el flag IF sti retn _disable: ; pone en 0 el flag IF cli retn _timer_isr: pushad ; salva todos los registros de uso general call _timer_handler ; llama a timer_handler() popad ; restaura todos los registros de uso general iretd _kbd_isr: pushad ; salva todos los registros de uso general call _kbd_handler ; llama a kbd_handler() popad ; restaura todos los registros de uso general iretd |
Archivo: ejprog.c |
#include <stdio.h> extern unsigned long read_cr0(); extern void write_cr0(unsigned long); extern void lgdt(struct GDTR*); extern void lidt(struct IDTR*); extern void update_cs(unsigned int); extern void enable(); extern void disable(); extern interrupt timer_isr(); extern interrupt kbd_isr(); typedef unsigned char byte; typedef unsigned int word; typedef unsigned long dword; /******************************************************************************/ /* Aquí se construye la GDT. Esta tabla define los segmentos en modo protegi- */ /* do. No se darán más detalles en este trabajo, sin embargo es posible en- */ /* contrar más información al respecto en los manuales de INTEL. */ /******************************************************************************/ #define ACS_CODE 0x9A #define ACS_DATA 0x92 #define ACS_STACK 0x92 struct DESCR_SEG { word limite, base_l; byte base_m, acceso, atributos, base_h; } gdt[4] = { 0 }; struct GDTR { word limite; dword base; } gdtr; void setup_GDT_entry (struct DESCR_SEG *item, dword base, dword limite, byte acceso) { item->limite = limite; item->base_l = base; item->base_m = base >> 16; item->acceso = acceso; } void setup_GDT() { setup_GDT_entry (&gdt[1], ((dword)_CS)<<4, 0xFFFF, ACS_CODE); setup_GDT_entry (&gdt[2], ((dword)_DS)<<4, 0xFFFF, ACS_DATA); setup_GDT_entry (&gdt[3], ((dword)_SS)<<4, 0xFFFF, ACS_STACK); gdtr.base = ((dword) _DS)<<4; gdtr.base += (word) gdt; gdtr.limite = sizeof(gdt)-1; lgdt(&gdtr); } /******************************************************************************/ /* Puertos */ #define PORT_8259M 0x20 #define PORT_8259S 0xA0 #define PORT_KBD_A 0x60 /* Palabras para los 8259 */ #define ICW1 0x11 #define ICW3M 0x04 #define ICW3S 0x02 #define ICW4 0x01 #define EOI 0x20 #define INT_GATE_CONFIG 0x8E00 #define CODE_SEG_SEL 0x08 /* Estructura de la interrupt-gates. */ struct int_gate { word offset_l, segmento, config, offset_h; }; /* Estructura del registro IDTR. */ struct IDTR { word limite; dword base; }; struct int_gate idt[0x22] = { 0 }; /* IDT */ struct IDTR idtr; /* IDTR */ byte old_IRQ_maskM, old_IRQ_maskS; volatile byte scancode=0; unsigned int ticks=0; /* Función llamada por timer_isr(). */ void timer_handler() { /* El BIOS ha configurado el timer para que envie interrupciones a una */ /* frecuencia de 18,2 Hz, por lo tanto en 20 segundos habra 364 inte- */ /* rrupciones aproximadamente. */ if (ticks++ == 364) scancode = 0x81; outportb (PORT_8259M, EOI); } /* Función llamada por kbd_isr(). */ void kbd_handler() { /* Leyendo el código de tecla. */ scancode = inportb (PORT_KBD_A); outportb (PORT_8259M, EOI); } void setup_IDT() { /* Cargando en la IDT las interrupt-gates para el timer y el teclado. */ idt[0x20].offset_l = (word) &timer_isr; idt[0x20].segmento = CODE_SEG_SEL; idt[0x20].config = INT_GATE_CONFIG; idt[0x21].offset_l = (word) &kbd_isr; idt[0x21].segmento = CODE_SEG_SEL; idt[0x21].config = INT_GATE_CONFIG; /* Cargando el registro IDTR. */ idtr.base = ((dword) _DS)<<4; idtr.base += (word) idt; idtr.limite = sizeof(idt)-1; lidt(&idtr); } void setup_PIC (byte vector_maestro, byte vector_esclavo) { outportb (PORT_8259M, ICW1); /* comienza la inicialización del 8259 */ outportb (PORT_8259S, ICW1); outportb (PORT_8259M+1,vector_maestro); /* vector base para el maestro */ outportb (PORT_8259S+1,vector_esclavo); /* vector base para el esclavo */ outportb (PORT_8259M+1,ICW3M); outportb (PORT_8259S+1,ICW3S); outportb (PORT_8259M+1,ICW4); outportb (PORT_8259S+1,ICW4); } int main() { word old_CS, old_DS, old_SS; printf("Programa de ejemplo: \"Atendiendo interrupciones\".\n\n"); printf("Para salir presione ESC o espere 20 segundos."); /* Construyendo la GDT. */ setup_GDT(); /* Enmascarando las interrupciones (flag IF = 0). */ disable(); /* Construyendo la IDT. */ setup_IDT(); /* Salvando las máscaras IRQ. */ old_IRQ_maskM = inportb (PORT_8259M+1); old_IRQ_maskS = inportb (PORT_8259S+1); /* Inicializando el PIC para que los vectores de las IRQ vayan desde el */ /* 0x20 hasta el 0x2F. */ setup_PIC (0x20, 0x28); /* Enmascarando todas las interrupciones excepto las del timer y las del */ /* teclado. */ outportb (PORT_8259M+1, 0xFC); outportb (PORT_8259S+1, 0xFF); /* Salvando el contenido de los registros de segmento. */ old_CS = _CS; old_DS = _DS; old_SS = _SS; /* Pasando a modo protegido. */ write_cr0 (read_cr0() | 1L); /* Cargando los registros de segmento con los valores para el modo pro- */ /* tegido. */ update_cs (0x08); _ES = _DS = 0x10; _SS = 0x18; /* IF = 1 */ enable(); /* Esperando por ESC. */ while (scancode!=0x81) {} /* IF = 0 */ disable(); /* Volviendo a modo real. */ write_cr0 (read_cr0() & 0xFFFFFFFEL); /* Restaurando los registro de segmento. */ update_cs (old_CS); _ES = _DS = old_DS; _SS = old_SS; /* Restaurando el IDTR. */ idtr.base = 0; idtr.limite = 0x3FF; lidt (&idtr); /* Inicializando el PIC como estaba antes. */ setup_PIC (0x08, 0x70); /* Restaurando las máscaras IRQ. */ outportb (PORT_8259M+1, old_IRQ_maskM); outportb (PORT_8259S+1, old_IRQ_maskS); /* IF = 1 */ enable(); return 0; } |
REFERENCIAS
Intel Architecture Software Developer’s Manual Volume 1
Intel Architecture Software Developer’s Manual Volume 3
webster.csu.cr.edu
www.beyondlogic.org
www.cources.ece.uiuc.edu/ece291/lecture/spring2003
apuntes de Computer Engineering II, Dr. Zbigniew Kalbarczyk, University of Illinois at Urbana- Champaign
wiki.cs.uiuc.edu/cs427
my.execpc.com/~greezer/osd/intr.
Linux Interrupts: The Basic Concepts, Mika J. Järvenpää, University of Helsinki, Department of Computer Science
The Linux Kernel, Boston University: Signals and Interrupts.
rdtb1.suwon.ac.kr
Real-Time & Multimedia Database Lab., The University of Suwon, Choi,Sook-Young
Guide to 80x86 assembly, Gavin Estey, 1995
1