El hardware que sostiene al sistema operativo — teoría con la práctica de IC2 en la impresora, una interrupción que lo para todo y una caché que acierta (y el bucle infinito que no colgó mi portátil)
La noche antes de entregar la práctica de IC2 hice dos cosas a la vez: mandé a imprimir el PDF del enunciado y compilé mi programa en VS Code. El programa tenía un while al que se me olvidó ponerle el incremento, así que se quedó dando vueltas para siempre. El ventilador se puso a tope, pero el portátil no se colgó: la impresora siguió sacando páginas, el ratón se movía y pude abrir el administrador de tareas para matar el proceso. En ese momento me pareció magia. Hoy sé que no lo era: era el hardware echándole una mano al sistema operativo.
En el tema anterior vimos qué es un sistema operativo: la interfaz entre tú y la máquina y el árbitro que reparte los recursos. Pero un árbitro necesita silbato, tarjetas y un reloj. Hoy vemos esas herramientas: cómo se comunica la CPU con los dispositivos, qué es una interrupción, por qué hay tantas memorias distintas, cómo impide el hardware que un programa haga trampas y de qué piezas está hecho el SO por dentro. Al final tienes los ejercicios tipo examen para comprobar que lo tienes.
Tabla de Contenidos
Por qué el SO necesita ayuda del hardware
El sistema operativo es software: se ejecuta en la misma CPU que tu programa. Mientras tu bucle infinito ocupa la CPU, el SO no se está ejecutando. ¿Cómo recupera el control? ¿Cómo se entera de que la impresora ha terminado una página? ¿Cómo evita que tu programa escriba encima de la memoria de Spotify?
Él solo no puede. Necesita que el hardware le dé cuatro cosas, y son justo los cuatro bloques de este tema:
| Pieza del hardware | Problema que resuelve | Lo verás en |
|---|---|---|
| Estructura de la E/S | Hablar con dispositivos lentos sin que la CPU se quede esperando | Controladores y búferes |
| Sistema de interrupciones | Enterarse de que ha pasado algo sin estar mirando todo el rato | RSI, polling, vector |
| Jerarquía de memorias | Que la CPU no espere a una memoria lenta | Caché y reemplazo |
| Protección del hardware | Que ningún programa haga lo que no debe | Modo dual, base y límite, temporizador |
Después veremos los componentes del SO (programas del sistema, núcleo y llamadas al sistema) y las estructuras con las que se organiza su código.
La estructura de la E/S: controladores y búferes
Tu impresora no está conectada directamente a la CPU. Entre las dos hay una pieza intermedia que hace casi todo el trabajo: el controlador de E/S.
Fíjate en el dibujo. La CPU y la memoria principal están colgadas del bus del sistema, y cada dispositivo se engancha a ese bus a través de su controlador. Estas son las cuatro frases que caen en el examen, con nuestras palabras:
- Los dispositivos se conectan al bus a través de controladores de E/S.
- La CPU se comunica con los controladores mediante instrucciones especiales o mediante direcciones de memoria concretas (lo que se llama E/S mapeada en memoria).
- Cada controlador tiene un búfer local: la CPU deja ahí los datos que hay que enviar y recoge de ahí los que llegan.
- El controlador avisa a la CPU con una interrupción cuando termina una operación o cuando llegan datos nuevos.
En el ejemplo de la impresora: la CPU deja la primera página en el búfer del controlador y le da la orden de imprimir. A partir de ahí, el controlador se encarga de mover el papel y el cabezal. La CPU queda libre. Cuando la página sale, el controlador dice «¡he terminado!» con una interrupción.
El controlador es hardware (una placa o un chip). El driver que instalas en Windows es software: el trozo del SO que sabe hablar con ese controlador. No los confundas en el test.
Las interrupciones
Una interrupción es una señal que hace que la CPU deje lo que está haciendo para atender un evento urgente y, después, vuelva a lo que hacía. Es el mecanismo que permite que el SO se entere de las cosas sin vigilarlas.
Qué pasa cuando llega una
Mira el dibujo de izquierda a derecha. Tu VS Code (el proceso A) está usando la CPU y llega la señal de la impresora. La CPU:
- Termina la instrucción que estaba ejecutando.
- Guarda el estado de la CPU (contador de programa, registros…) para poder continuar después.
- Ejecuta la rutina de servicio de interrupción (RSI): el código del SO que sabe atender a ese dispositivo.
- Restaura el estado guardado.
- El proceso A sigue exactamente por donde iba, sin enterarse de nada.
La RSI (rutina de servicio de interrupción) es el código del sistema operativo que atiende un tipo concreto de interrupción. No la escribe cada aplicación: es parte del SO.
Decir que el estado de la CPU se guarda al final de la RSI. Se guarda antes: si la RSI empezara a usar los registros sin guardarlos, machacaría los datos de A y A no podría continuar.
¿Quién ha sido? Polling e interrupciones vectorizadas
Al ordenador le llegan interrupciones del disco, del teclado, de la tarjeta de red, de la impresora… Cuando suena una, la CPU tiene que saber qué dispositivo la ha provocado para ejecutar la RSI correcta. Hay dos formas:
- Polling (método primitivo): la CPU pregunta a todos los dispositivos, uno a uno, «¿has sido tú?». Funciona, pero gasta tiempo, y más cuantos más dispositivos haya.
- Interrupciones vectorizadas (método avanzado): el dispositivo que interrumpe envía un número por el bus de datos. Ese número es un índice en el vector de interrupciones, una zona de la memoria principal que contiene las direcciones de todas las RSI. La CPU mira la posición indicada y salta directa a la rutina.
En el dibujo, la impresora manda un 7; la posición 7 del vector apunta a la RSI de la impresora. Sin preguntar a nadie.
| Polling | Vectorizadas | |
|---|---|---|
| Quién identifica al dispositivo | La CPU, preguntando | El dispositivo, con su número |
| Coste | Crece con el número de dispositivos | Una consulta al vector |
| Qué hace falta en memoria | Nada especial | El vector de interrupciones |
«vectorizada» viene de «vector». Si en una pregunta aparece «número» o «índice», están hablando de interrupciones vectorizadas.
El SO es reactivo: tres tipos de evento
Aquí está la clave de la anécdota. El sistema operativo no se activa por sí solo: se activa cuando ocurre un evento que tiene que atender. Por eso se dice que tiene un comportamiento reactivo. Entre evento y evento, la CPU es de los programas de usuario.
Los eventos son de tres tipos, y cada uno activa una RSI distinta, a través del vector de interrupciones:
| Evento | Quién lo provoca | Ejemplo con la práctica |
|---|---|---|
| Interrupción hardware | Un dispositivo de E/S | La impresora ha terminado una página |
| Llamada al sistema | El propio programa, a propósito | El write() que hay dentro de printf() |
| Excepción | La CPU, al detectar un error | Dividir entre cero o tocar memoria prohibida |
Las llamadas al sistema y las excepciones son interrupciones software: se gestionan igual que las del hardware, pero las provoca la propia CPU. Para provocar una a propósito existe una instrucción de máquina específica: INT en Intel, TRAP en Motorola, SYSCALL en MIPS.
En el fondo, todo lo que despierta al SO entra por la misma puerta, el vector de interrupciones. Lo único que cambia es quién llama a la puerta.
Sincronización entre la E/S y el SO
Tu proceso pide imprimir una página. La impresora tarda 60 ms. ¿Qué hace el SO mientras tanto?
Síncrona frente a asíncrona
- De forma síncrona: el SO deja la CPU esperando hasta que llega el aviso de la E/S. Si encima no hay interrupciones, la CPU se pasa ese tiempo preguntando «¿ya has terminado?» (espera activa).
- De forma asíncrona: el SO cede la CPU a otros procesos mientras la E/S trabaja, y la interrupción de fin de E/S le avisa cuando toca volver.
El dibujo sale del simulador de la sección «Compruébalo con Python», con los mismos datos: tres páginas de 20 ms de CPU y 60 ms de impresora, y un compilador que necesita 120 ms de CPU.
Mismo trabajo, misma impresora. En la síncrona, las casillas ámbar son tiempo de CPU tirado a la basura preguntando; en la asíncrona, ese hueco lo aprovecha el compilador. Resultado: la forma asíncrona saca más rendimiento al sistema (240 ms frente a 360 ms, y un 75 % de CPU útil frente a un 50 %).
Paso a paso, en la asíncrona:
- El proceso A pide la E/S con una llamada al sistema.
- El SO atiende la llamada y dialoga con el controlador (le deja la página y la orden).
- Como A tiene que esperar, el SO hace un cambio de contexto y pone en la CPU al proceso B.
- Cuando la impresora termina, genera una interrupción.
- El SO atiende la interrupción y decide reanudar al proceso A.
Decir que es asíncrona «cuando el SO espera a que termine la E/S». Es justo al revés: esperar es síncrono; seguir con otra cosa es asíncrono.
Colas de espera por E/S
¿Y si pides imprimir y la impresora ya está ocupada con el trabajo de otro? La petición no se pierde: el SO mantiene una cola de espera para cada dispositivo, donde guarda en orden las peticiones pendientes. Cada vez que una operación termina (con su interrupción), el SO le pasa al dispositivo la siguiente petición de la cola. Es exactamente la cola de impresión que ves en Windows cuando mandas tres PDF seguidos.
La jerarquía de memorias
Ahora el otro gran problema: la CPU es muchísimo más rápida que la memoria principal, y la memoria principal muchísimo más rápida que un disco. Los medios de almacenamiento se organizan en una jerarquía según su coste y su velocidad.
Velocidad, coste y capacidad
La regla de la pirámide es una sola: cuanto más arriba, más rápida, más cara por byte y más pequeña. Los registros están en la cima (dentro de la CPU, unos pocos bytes); la cinta magnética, en la base (enorme, barata y lentísima). La línea discontinua separa lo que se pierde al apagar (registros, caché, RAM) de lo que se conserva.
Como en esta parte del tema salen tamaños, repasa las unidades, que también caen:
| Unidad | Qué es |
|---|---|
| Bit | Guarda uno de dos valores (0 o 1) |
| Byte | 8 bits (en el pasado hubo bytes de otros tamaños) |
| Palabra | De 8 a 64 bits, según la CPU |
| 1 KiB (binario) | 2¹⁰ = 1024 bytes |
| 1 KB (decimal) | 10³ = 1000 bytes |
La «i» de KiB, MiB y GiB significa «binario» (potencias de 2). Sin «i», el prefijo es decimal: 1 KB son 1000 bytes, 1 KiB son 1024.
El principio de caché y las políticas de reemplazo
¿Cómo se gestiona la jerarquía? Con una idea muy sencilla, el principio de caché: guardar en la memoria más rápida la información que se usa con más frecuencia. Si casi todo lo que la CPU pide está en la caché, casi nunca tiene que esperar a la RAM.
El principio no se limita a la memoria caché del procesador: el SO lo usa para la caché de disco (bloques del disco guardados en RAM) y tu navegador para la caché de páginas web (páginas guardadas en tu disco).
El problema es que la caché es pequeña y se acaba llenando. Cuando llega un bloque nuevo, hay que descartar otro, y la regla que decide cuál se llama política de caché o política de reemplazo:
| Política | Qué bloque sale |
|---|---|
| FIFO | El más antiguo (el que entró antes) |
| LRU | El que hace más tiempo que no se usa |
| LFU | El que se usa con menos frecuencia |
| Al azar | Uno cualquiera |
Para que se vea la diferencia, el simulador hace pasar los mismos 12 accesos por una caché de 3 bloques con FIFO y con LRU:
Mira las dos filas resaltadas. El bloque A se pide una y otra vez, pero FIFO lo echa en cuanto es el más antiguo, porque solo mira cuándo entró. LRU mira cuándo se usó por última vez, así que mantiene a A dentro. Resultado: 3 aciertos con FIFO y 5 con LRU.
Pensar que cuando la caché se llena «se vacía entera y se vuelve a cargar». No: se descarta un bloque, el que diga la política de reemplazo.
Protección del hardware
Volvamos al bucle infinito. Para que el SO funcione, hay que impedir que los programas de usuario hagan libremente ciertas cosas:
- Acceder a la memoria del SO y a la de otros programas.
- Acceder directamente a los dispositivos de E/S.
- Usar la CPU todo el tiempo que quieran.
Fíjate en que cada punto es un recurso: la memoria, la E/S y la CPU. El hardware ofrece una protección para cada uno, y todas se apoyan en una idea base: el modo dual.
El modo dual de operación
La CPU define un repertorio de instrucciones privilegiadas y funciona en dos modos:
- Modo privilegiado (también llamado supervisor, sistema o núcleo): se puede ejecutar cualquier instrucción.
- Modo no privilegiado o modo usuario: si se intenta ejecutar una instrucción privilegiada, la CPU interrumpe la ejecución y genera una excepción.
Un bit del procesador indica en qué modo está: 0 para el modo privilegiado y 1 para el modo usuario.
¿Cuándo se cambia de modo? Sigue el dibujo:
- La CPU arranca en modo privilegiado (el SO se carga con todo el poder).
- Cuando el SO cede el control a un programa, antes cambia a modo usuario.
- Solo se vuelve a modo privilegiado cuando el SO recupera el control: con una interrupción, una llamada al sistema o una excepción.
Y aquí entra la llamada al sistema. La instrucción de llamada al sistema (syscall, trap…) cambia automáticamente a modo privilegiado. Es la forma en que un programa de usuario pasa voluntariamente a modo privilegiado, pero con una condición: lo que se ejecuta a continuación es código del SO, que no está bajo su control. Es como un torno con tarjeta: puedes pasar, pero solo por la puerta que el SO ha preparado.
«en modo privilegiado solo se pueden ejecutar instrucciones privilegiadas». Falso: en modo privilegiado se puede ejecutar todo, las privilegiadas y las normales. Cualquier frase con «solo» sobre el modo privilegiado suele ser trampa.
Protección de la E/S
Hay dos formas de acceder a la E/S, y las dos se protegen:
- Con instrucciones especiales (como
inyout): esas instrucciones son privilegiadas, solo funcionan con el bit de modo a 0. - A través de la memoria (E/S mapeada en memoria): el acceso a las direcciones que usa la E/S está prohibido en modo usuario.
Así, tu programa no puede hablar con la impresora directamente: tiene que pedírselo al SO con una llamada al sistema.
Protección de la memoria: registros base y límite
La idea es una pareja de registros, base y límite, que delimitan la zona de memoria principal en la que el proceso puede trabajar. Cuando la CPU está en modo usuario, un circuito comprueba en cada acceso que la dirección es legal. Si no lo es, el acceso no sale al bus: se produce una excepción y el SO toma el control.
Con base 40000 y límite 12000, el proceso 2 puede usar de la 40000 a la 51999. Comprueba las dos direcciones del dibujo a mano:
- 45000: 45000 ≥ 40000 sí, y 45000 < 40000 + 12000 = 52000 también. Acceso permitido.
- 53000: 53000 ≥ 40000 sí, pero 53000 < 52000 no. Excepción: esa dirección es del proceso 3.
La comparación es «menor que base + límite», no «menor o igual». La dirección 52000 ya es la primera que no pertenece al proceso 2.
Protección de la CPU: el temporizador
El último recurso es el tiempo de CPU, y aquí está la explicación de mi bucle infinito. El objetivo es evitar que un proceso acapare indefinidamente la CPU, por ejemplo al entrar en un bucle infinito. La solución es el temporizador:
- Contiene un contador que el SO inicializa con el valor que quiera antes de ceder la CPU.
- El contador baja con cada pulso de reloj del sistema.
- Cuando llega a cero, genera una interrupción, y con ella el SO recupera el control.
Mi programa nunca soltaba la CPU por su cuenta, pero no hacía falta: cada pocos milisegundos el temporizador saltaba, el SO entraba y repartía la CPU entre la impresión, el ratón y el administrador de tareas. Y mi programa no podía desactivar el temporizador porque cambiarlo es una instrucción privilegiada.
Las tres protecciones se apoyan en el modo dual. Sin él, cualquier programa podría ejecutar out, cambiar los registros base y límite o apagar el temporizador.
Componentes del sistema operativo
Ya conoces el hardware. Ahora, ¿de qué está hecho el SO? Se suele dividir en tres componentes.
Programas del sistema, núcleo y llamadas al sistema
| Componente | Qué es | Ejemplos |
|---|---|---|
| Programas del sistema | Utilidades básicas que vienen con el SO y servicios que se ejecutan en segundo plano (servicios, subsistemas o demonios). Son lo que el usuario percibe como «el sistema operativo» | Escritorio y shell, ls, cp, mkdir, vi, el compilador y el depurador, el sistema de impresión, las copias de seguridad |
| Núcleo (kernel) | La parte del SO que reside en memoria de forma permanente y atiende las llamadas al sistema y demás eventos. El resto de utilidades se apoya en sus servicios | Lo que se ejecuta en modo privilegiado |
| Llamadas al sistema | La forma de pedirle algo al núcleo | open, read, write, fork |
En la teoría de la asignatura se habla casi siempre del núcleo, y ese es el que verás en los próximos temas.
Una llamada al sistema se ve distinta según desde dónde la mires:
- Para el procesador, es una instrucción especial (
syscall,int,trap…) que cambia a modo privilegiado. - Para el programador, es una subrutina que escribe en su código fuente, como
fd = open("mifichero", O_RDONLY);en UNIX. El compilador y la biblioteca la acaban convirtiendo en la instrucción especial, con los argumentos que hagan falta.
¿Cómo llegan esos argumentos al núcleo? De tres formas: en registros de la CPU (lo más típico), en una tabla en memoria principal cuya dirección se pasa en un registro, o en la pila.
Y una de examen: la biblioteca estándar de C no pertenece al SO y se ejecuta en modo usuario. Cuando mi práctica hacía printf(), ese código era de la biblioteca; solo al final printf() llamaba a write() para escribir en la consola, y ahí sí se entraba en el núcleo.
Las llamadas al sistema de UNIX se agrupan por lo que gestionan:
| Categoría | Llamadas |
|---|---|
| Procesos | fork (crear), exit (terminar), wait |
| Memoria | sbrk (pedir más memoria) |
| Archivos | open, close, creat, read, write, mkdir, lockf (bloquear un fichero) |
| Redes | socket (crear una conexión), close |
| Protección | chmod (cambiar permisos), chown (cambiar propietario) |
Muchas de estas ya las usaste en C. Si quieres verlas en acción, repasa llamadas al sistema para ficheros y procesos en C.
El arranque del sistema
¿Y cómo llega el núcleo a la memoria? Así es un arranque típico:
- Al encender el equipo, la CPU empieza a ejecutar en un punto fijo de la memoria.
- Ahí hay una ROM con una pequeña rutina de arranque (que ya sabe leer de los dispositivos de E/S).
- Esa rutina busca en qué dispositivo está el cargador del SO (boot loader) y lo carga en memoria.
- El cargador instala el núcleo, y se siguen cargando módulos y servicios hasta que el SO está totalmente operativo.
Estructuras del sistema operativo
Por último, ¿cómo se organiza el código del núcleo? Hay cinco estructuras clásicas. Lo que las diferencia es cuánto código va en modo privilegiado y cómo se reparte:
| Estructura | Idea | Ejemplo |
|---|---|---|
| Monolítica | Es la más simple: toda la funcionalidad del núcleo en un único espacio de direcciones, «todo en un mazacote». No se puede cambiar una parte por separado | UNIX clásico |
| Por capas | Una jerarquía de niveles en la que cada uno usa los servicios del de abajo | THE (años 60): L0 hardware, L1 planificación de procesos, L2 memoria paginada, L3 consola, L4 buffering, L5 aplicaciones |
| Micronúcleo | En el núcleo, solo lo imprescindible; el resto de servicios son módulos en espacio de usuario que se comunican con mensajes | Mach (1980) |
| Módulos cargables | Módulos de código con una interfaz conocida que se incorporan al núcleo en tiempo de ejecución y se llaman entre ellos | Linux, Solaris |
| Híbrida | Combina varias de las anteriores | Linux (monolítico y modular), Windows, macOS e iOS, Android |
El micronúcleo merece un momento, porque trae dos palabras que caen: mecanismos y políticas.
- Política: la estrategia para administrar un recurso (FIFO, SJF, LRU, tiempo compartido…).
- Mecanismo: las estructuras y objetos que sirven para aplicar una política (colas de espera, mapas de bits, el despachador de procesos, el temporizador…).
Lo deseable es separarlos, para reutilizar los mecanismos con otras políticas. El micronúcleo lo lleva al extremo: mecanismos dentro del micronúcleo y políticas fuera.
| Micronúcleo | |
|---|---|
| Ventajas | Se crean servicios nuevos sin tocar el núcleo; puede haber varias versiones de un servicio (por ejemplo, varios sistemas de ficheros); es más fácil de portar a otra arquitectura; más seguridad y fiabilidad, porque hay menos código en modo privilegiado |
| Inconveniente | La comunicación entre módulos con mensajes penaliza el rendimiento |
Errores típicos del tema
«la CPU guarda su estado al terminar la RSI». Se guarda antes de ejecutarla y se restaura al acabar.
«el principio de caché es guardar lo más usado en la memoria de más capacidad». Es en la de menor tiempo de acceso, que además es la más pequeña.
«los registros base y límite delimitan la zona de memoria secundaria». Delimitan memoria principal. Y el circuito que los comprueba se activa en modo usuario, no en modo supervisor.
«la biblioteca de C forma parte del SO y se ejecuta en modo núcleo». No pertenece al SO y se ejecuta en modo usuario; solo la llamada al sistema que hay dentro entra en el núcleo.
«las llamadas al sistema se implementan con interrupciones hardware». Son interrupciones software: las provoca la propia CPU con una instrucción especial.
Compruébalo con Python
IC2 es una asignatura de papel, pero dos ideas de este tema se pueden medir: cuánto gana la E/S asíncrona y cuánto ayuda una buena política de caché. Estos dos simuladores son cortos, usan datos fijos y su salida es la de una ejecución real. Cópialos en un archivo .py y ejecútalos.
La impresora y el compilador
El primero reproduce la noche de la práctica: el proceso A imprime 3 páginas (20 ms de CPU y 60 ms de impresora cada una) y el proceso B compila (120 ms de CPU). Cada carácter de la línea de la CPU son 10 ms.
# Simulador de E/S: imprimes la práctica de IC2 mientras VS Code compila
# A = trabajo de impresión (3 páginas); B = compilador. 1 carácter = 10 ms de CPU
PAGINAS = 3
CPU_PAGINA = 20 # ms de CPU que A necesita para preparar cada página
IMPRESORA = 60 # ms que tarda la impresora en imprimir una página
COMPILAR = 120 # ms de CPU que necesita B en total
SONDEO = 5 # con polling, la CPU pregunta a la impresora cada 5 ms
def linea_cpu(asincrona):
cpu = []
falta_b = COMPILAR // 10 # trozos de 10 ms que le quedan a B
for _ in range(PAGINAS):
cpu += ['A'] * (CPU_PAGINA // 10) # A prepara la página y se la da al controlador
for _ in range(IMPRESORA // 10): # mientras tanto, la impresora trabaja sola
if asincrona and falta_b: # asíncrona: el SO le da la CPU a B
cpu.append('B')
falta_b -= 1
elif asincrona:
cpu.append('.') # nadie necesita la CPU: está libre
else:
cpu.append('?') # síncrona: la CPU pregunta "¿ya has terminado?"
return ''.join(cpu) + 'B' * falta_b # lo que le quede a B va al final
for modo in ('síncrona', 'asíncrona'):
cpu = linea_cpu(modo == 'asíncrona')
total = len(cpu) * 10 # ms hasta que A y B han terminado
util = (cpu.count('A') + cpu.count('B')) * 10
preguntas = cpu.count('?') * 10 // SONDEO # cada ? son 10 ms preguntando cada 5 ms
avisos = PAGINAS if modo == 'asíncrona' else 0 # una interrupción por página impresa
print(f'{modo:<13} {cpu}')
print(f'{"":<13} total {total} ms · útil {util} ms · uso {util * 100 // total} %'
f' · preguntas {preguntas} · interrupciones {avisos}')
Al ejecutarlo, sale esto:
síncrona AA??????AA??????AA??????BBBBBBBBBBBB
total 360 ms · útil 180 ms · uso 50 % · preguntas 36 · interrupciones 0
asíncrona AABBBBBBAABBBBBBAA......
total 240 ms · útil 180 ms · uso 75 % · preguntas 0 · interrupciones 3
Comprobemos las cuentas. En la síncrona, la línea tiene 36 casillas: 36 × 10 = 360 ms. La CPU solo trabaja en las 6 casillas de A y las 12 de B: 18 × 10 = 180 ms útiles, y 180 / 360 = 50 %. Las 18 casillas ? son 180 ms preguntando cada 5 ms: 180 / 5 = 36 preguntas. En la asíncrona, la línea mide 24 casillas (240 ms), los mismos 180 ms útiles dan 180 / 240 = 75 %, y hay una interrupción por página, 3 en total.
El corazón del simulador es el bucle que decide qué hace la CPU mientras imprime:
for _ in range(IMPRESORA // 10): # mientras tanto, la impresora trabaja sola
if asincrona and falta_b: # asíncrona: el SO le da la CPU a B
cpu.append('B')
falta_b -= 1
elif asincrona:
cpu.append('.') # nadie necesita la CPU: está libre
else:
cpu.append('?') # síncrona: la CPU pregunta "¿ya has terminado?"
Son tres ramas, y cada una es un concepto del tema: con E/S asíncrona y trabajo pendiente, el SO hace un cambio de contexto a B; con E/S asíncrona y sin nada que hacer, la CPU queda libre (los puntos del final); con E/S síncrona, la CPU se queda en espera activa preguntando al controlador.
La caché con FIFO y con LRU
El segundo simulador pasa 12 peticiones de bloques por una caché de 3 huecos. Mirar la caché cuesta 1 ns; si el bloque no está, hay que ir además a la RAM, 50 ns más.
# Simulador de caché: el compilador pide bloques de memoria y la caché solo guarda 3
ACCESOS = ['A', 'B', 'C', 'A', 'D', 'A', 'B', 'E', 'A', 'B', 'C', 'A']
HUECOS = 3
T_CACHE = 1 # ns en mirar la caché (se mira siempre)
T_RAM = 50 # ns extra si el bloque no está y hay que ir a la RAM
def simula(politica):
cache, aciertos, filas = [], 0, [] # cache[0] es la próxima víctima
for b in ACCESOS:
if b in cache:
aciertos += 1
marca = 'acierto'
if politica == 'LRU': # LRU: el bloque usado pasa al final (el más reciente)
cache.remove(b)
cache.append(b)
else:
marca = 'fallo'
if len(cache) == HUECOS: # llena: sale cache[0]
cache.pop(0) # FIFO = el que entró antes; LRU = el que lleva más sin usarse
cache.append(b)
filas.append(f"{b} {' '.join(cache):<6} {marca:<8}")
return aciertos, filas
fifo, lru = simula('FIFO'), simula('LRU')
print(f'{"FIFO":<20}LRU')
for f, l in zip(fifo[1], lru[1]):
print(f'{f:<20}{l}'.rstrip())
for nombre, (aciertos, _) in (('FIFO', fifo), ('LRU', lru)):
tasa = aciertos / len(ACCESOS)
medio = T_CACHE + (1 - tasa) * T_RAM # fallo = mirar la caché + ir a la RAM
print(f'{nombre:<5} aciertos {aciertos}/{len(ACCESOS)} = {tasa * 100:.0f} % · tiempo medio {medio:.1f} ns')
print(f'con un 95 % de aciertos: {T_CACHE + 0.05 * T_RAM:.1f} ns · sin caché: {T_RAM} ns')
Esta es su salida real:
FIFO LRU A A fallo A A fallo B A B fallo B A B fallo C A B C fallo C A B C fallo A A B C acierto A B C A acierto D B C D fallo D C A D fallo A C D A fallo A C D A acierto B D A B fallo B D A B fallo E A B E fallo E A B E fallo A A B E acierto A B E A acierto B A B E acierto B E A B acierto C B E C fallo C A B C fallo A E C A fallo A B C A acierto FIFO aciertos 3/12 = 25 % · tiempo medio 38.5 ns LRU aciertos 5/12 = 42 % · tiempo medio 30.2 ns con un 95 % de aciertos: 3.5 ns · sin caché: 50 ns
Las cuentas del tiempo medio: con FIFO, 3 aciertos de 12 dejan 9 fallos, así que 1 + (9 / 12) × 50 = 1 + 37,5 = 38,5 ns. Con LRU, 5 aciertos dejan 7 fallos: 1 + (7 / 12) × 50 = 1 + 29,17 ≈ 30,2 ns. La línea final pone la cifra en contexto: con un 95 % de aciertos, 1 + 0,05 × 50 = 3,5 ns, unas 14 veces más rápido que ir siempre a la RAM.
La única diferencia entre las dos políticas está en estas líneas:
if b in cache:
aciertos += 1
marca = 'acierto'
if politica == 'LRU': # LRU: el bloque usado pasa al final (el más reciente)
cache.remove(b)
cache.append(b)
else:
marca = 'fallo'
if len(cache) == HUECOS: # llena: sale cache[0]
cache.pop(0) # FIFO = el que entró antes; LRU = el que lleva más sin usarse
cache.append(b)
Con FIFO, un acierto no cambia nada, así que cache[0] sigue siendo el bloque que entró antes. Con LRU, cada acierto mueve el bloque al final de la lista, y cache[0] pasa a ser el que lleva más tiempo sin usarse. La víctima siempre sale de la posición 0; lo que cambia es qué significa estar en la posición 0.
Qué observar:
- En la línea síncrona, las
?aparecen siempre en grupos de 6: son los 60 ms de cada página en los que la CPU solo espera. - En la línea asíncrona, la
Brellena exactamente esos huecos. CambiaCOMPILARa 200 y verás que B ya no cabe en los huecos y la línea crece. - En la sexta petición (A), FIFO falla y LRU acierta. Busca en la salida qué bloque estaba en
cache[0]justo antes en cada política. - Pon
HUECOS = 4y vuelve a ejecutar: con más espacio suben los aciertos de las dos políticas, porque hay que echar bloques menos veces, pero LRU sigue por delante.
Resumen
| Concepto | En una frase |
|---|---|
| Controlador de E/S | Conecta el dispositivo al bus, tiene un búfer local y avisa con interrupciones |
| Interrupción y RSI | La CPU guarda su estado, ejecuta la rutina del SO y continúa |
| Polling / vectorizadas | Preguntar a todos frente a recibir un número que indexa el vector de interrupciones |
| SO reactivo | Solo se activa con interrupciones hardware, llamadas al sistema o excepciones |
| E/S asíncrona | La CPU hace otra cosa mientras la E/S trabaja: más rendimiento |
| Jerarquía de memorias | Más arriba, más rápida, más cara y más pequeña |
| Principio de caché | Lo más usado, en la memoria más rápida; si se llena, decide la política de reemplazo |
| Modo dual | Privilegiado (todo) y usuario (excepción si intenta una instrucción privilegiada) |
| Protección | E/S privilegiada, base y límite para la memoria, temporizador para la CPU |
| Componentes | Programas del sistema, núcleo y llamadas al sistema |
| Estructuras | Monolítica, por capas, micronúcleo, módulos cargables e híbrida |
Tres ideas para llevarte
- El SO no vigila: le avisan. Interrupciones, llamadas al sistema y excepciones son los únicos momentos en los que el SO se ejecuta.
- La protección es hardware. El modo dual, los registros base y límite y el temporizador hacen que el SO mande incluso cuando no se está ejecutando.
- La velocidad sale de la jerarquía. Una caché pequeña con una buena política hace que una memoria lenta parezca rápida.
Ahora ponte a prueba con los ejercicios tipo examen de este tema: un cuestionario interactivo con todos los formatos del examen, cálculos de E/S, de caché y de registros base y límite, y el chuletario. En el siguiente tema entraremos en los procesos y los hilos: qué es exactamente eso que el SO interrumpe, guarda y reanuda todo el rato.
Siguiente paso: ejerciciosEl hardware que sostiene al sistema operativo — ejercicios tipo examen con la impresora, la caché que acierta y los registros base y límite (y la RSI que me costó medio punto)¿Te ha servido este artículo? Así puedes apoyar SergioLearns →




El hardware que sostiene al sistema operativo — teoría con la práctica de IC2 en la impresora, una interrupción que lo para todo y una caché que acierta (y el bucle infinito que no colgó mi portátil)