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.

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 hardwareProblema que resuelveLo verás en
Estructura de la E/SHablar con dispositivos lentos sin que la CPU se quede esperandoControladores y búferes
Sistema de interrupcionesEnterarse de que ha pasado algo sin estar mirando todo el ratoRSI, polling, vector
Jerarquía de memoriasQue la CPU no espere a una memoria lentaCaché y reemplazo
Protección del hardwareQue ningún programa haga lo que no debeModo 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.

Estructura de la E/S La CPU y la memoria principal comparten el bus del sistema con tres controladores (disco, impresora y teclado), cada uno con su búfer local y su dispositivo. La CPU deja órdenes y datos en el búfer del controlador de la impresora y el controlador avisa a la CPU con una interrupción cuando termina. La E/S por dentro CPU Memoria principal bus del sistema Controlador de disco búfer local Disco Controlador de impresora búfer local Impresora Controlador de teclado búfer local Teclado interrupción órdenes y datos al búfer
Los dispositivos no van directos al bus: cada uno tiene su controlador con un búfer, y el controlador avisa con una interrupción.

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

El viaje de una interrupción Línea de tiempo de la CPU: el proceso A se ejecuta, llega la interrupción de la impresora, la CPU guarda el estado, ejecuta la rutina de servicio de la impresora, restaura el estado y el proceso A sigue como si nada. El viaje de una interrupción Proceso A (VS Code) guarda estado RSI de la impresora restaura estado Proceso A sigue ① llega la señal de la impresora tiempo ② SIEMPRE antes de la RSI ③ atiende al dispositivo ④ todo como estaba ⑤ A ni se entera
Fíjate en el orden: el estado se guarda antes de la RSI y se restaura después, por eso A no nota nada.

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:

  1. Termina la instrucción que estaba ejecutando.
  2. Guarda el estado de la CPU (contador de programa, registros…) para poder continuar después.
  3. Ejecuta la rutina de servicio de interrupción (RSI): el código del SO que sabe atender a ese dispositivo.
  4. Restaura el estado guardado.
  5. 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 frente a interrupciones vectorizadas A la izquierda, la CPU pregunta a disco, impresora y teclado uno a uno quién ha interrumpido. A la derecha, la impresora envía el número 7, que es el índice de su entrada en el vector de interrupciones, y la CPU salta directamente a la RSI de la impresora. Polling: la CPU pregunta Vectorizadas: el dispositivo da su número CPU disco impresora teclado ¿eres tú? ¿y tú? pregunta a todos, uno a uno: lento y gasta CPU impresora vector de interrupciones 5 → RSI disco 6 → RSI teclado 7 → RSI impresora 8 → … envía «7» RSI impresora salta directo a su rutina
Con polling la CPU busca al culpable; con vectorizadas el culpable se presenta con su número, que es una posición del vector.
  • 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.

PollingVectorizadas
Quién identifica al dispositivoLa CPU, preguntandoEl dispositivo, con su número
CosteCrece con el número de dispositivosUna consulta al vector
Qué hace falta en memoriaNada especialEl 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 tres tipos de evento Tres eventos activan el SO: una interrupción hardware de la impresora, una llamada al sistema write y una excepción. Cada uno entra por su entrada del vector de interrupciones y salta a su RSI. Las llamadas al sistema y las excepciones son interrupciones software, provocadas por la propia CPU. El SO es reactivo: despierta con un evento Interrupción hardware la impresora ha terminado Llamada al sistema write() que esconde printf() Excepción dividir entre 0, memoria prohibida vector de interrupciones entrada 1 → entrada 2 → entrada 3 → RSI de la impresora RSI de llamadas RSI de excepciones interrupciones software: las provoca la propia CPU (INT, TRAP, SYSCALL)
Las tres entran por el mismo sitio, el vector; la diferencia es quién las provoca.

Los eventos son de tres tipos, y cada uno activa una RSI distinta, a través del vector de interrupciones:

EventoQuién lo provocaEjemplo con la práctica
Interrupción hardwareUn dispositivo de E/SLa impresora ha terminado una página
Llamada al sistemaEl propio programa, a propósitoEl write() que hay dentro de printf()
ExcepciónLa CPU, al detectar un errorDividir 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.

E/S síncrona frente a asíncrona Dos líneas de la CPU en casillas de 10 ms con los datos del simulador. Síncrona: AA seguido de seis interrogaciones tres veces y después doce B, 360 ms. Asíncrona: mientras la impresora trabaja, la CPU ejecuta B, y al final quedan seis casillas libres, 240 ms. Las interrupciones llegan a los 80, 160 y 240 ms. Síncrona (polling): 360 ms, 50 % de CPU útil A A ? ? ? ? ? ? A A ? ? ? ? ? ? A A ? ? ? ? ? ? B B B B B B B B B B B B Asíncrona (interrupciones): 240 ms, 75 % de CPU útil A A B B B B B B A A B B B B B B A A · · · · · · 80 ms 160 ms 240 ms 0 60 120 180 240 300 360 ms A prepara una página B compila CPU preguntando CPU libre interrupción: la impresora acabó
Mismo trabajo, misma impresora: lo único que cambia es qué hace la CPU mientras imprime.

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:

  1. El proceso A pide la E/S con una llamada al sistema.
  2. El SO atiende la llamada y dialoga con el controlador (le deja la página y la orden).
  3. Como A tiene que esperar, el SO hace un cambio de contexto y pone en la CPU al proceso B.
  4. Cuando la impresora termina, genera una interrupción.
  5. 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 jerarquía de memorias Pirámide de seis niveles: registros, caché, memoria principal, disco sólido, disco duro magnético y cinta magnética. Hacia arriba, más rápida y más cara por byte; hacia abajo, más capacidad y más barata. Una línea separa lo que se borra al apagar (registros, caché, RAM) de lo que se conserva. Jerarquía de memorias Registros unos bytes Caché KiB – MiB Memoria principal (RAM) GiB Disco sólido (SSD) cientos de GiB Disco duro magnético TiB Cinta magnética decenas de TiB se borra al apagar ↑ se conserva ↓ más rápida más cara por byte más capacidad más barata por byte
Cuanto más arriba, más rápida, más cara y más pequeña: por eso se guarda arriba solo lo que más se usa.

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:

UnidadQué es
BitGuarda uno de dos valores (0 o 1)
Byte8 bits (en el pasado hubo bytes de otros tamaños)
PalabraDe 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íticaQué bloque sale
FIFOEl más antiguo (el que entró antes)
LRUEl que hace más tiempo que no se usa
LFUEl que se usa con menos frecuencia
Al azarUno 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:

FIFO frente a LRU en una caché de tres bloques Los doce accesos del simulador (A B C A D A B E A B C A) en una caché de tres bloques. A la izquierda FIFO, con 3 aciertos; a la derecha LRU, con 5. Las filas resaltadas son los dos accesos a A en los que LRU acierta y FIFO falla. FIFO · sale el que entró antes LRU · sale el que lleva más sin usarse pide caché después (izquierda = próxima víctima) pide caché después 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 3 aciertos de 12 · 38,5 ns de media 5 aciertos de 12 · 30,2 ns de media
Mira las filas resaltadas: FIFO echa a A por ser la más antigua aunque se use todo el rato; LRU la mantiene.

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.

El modo dual en acción Dos franjas, modo usuario arriba (bit 1) y modo núcleo abajo (bit 0). La CPU arranca en modo núcleo, el SO cede el control a tu programa en modo usuario, printf llama a write y la instrucción syscall cambia automáticamente a modo núcleo, el SO habla con la E/S y vuelve a modo usuario para que tu programa siga. Modo usuario · bit de modo = 1 Modo núcleo · bit = 0 arranque tu programa: printf() write: el SO y la E/S sigue tu programa el SO cede el control syscall / trap cambio automático vuelve al usuario Al núcleo solo se vuelve con una interrupción, una llamada al sistema o una excepción.
Tu programa nunca baja a la franja oscura por su cuenta: la puerta es syscall, y dentro manda el código del SO.

¿Cuándo se cambia de modo? Sigue el dibujo:

  1. La CPU arranca en modo privilegiado (el SO se carga con todo el poder).
  2. Cuando el SO cede el control a un programa, antes cambia a modo usuario.
  3. 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 in y out): 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.

Protección de memoria con base y límite Memoria de 0 a 64000 con el SO, el proceso 1, el proceso 2 de impresión (de 40000 a 52000) y el proceso 3. Con base 40000 y límite 12000, cada dirección d del proceso 2 se comprueba: si d es mayor o igual que la base y menor que base más límite, se accede; si no, salta una excepción y el acceso no sale al bus. Registros base y límite SO 0 proceso 1 20000 proceso 2 (impresión) 40000 proceso 3 52000 64000 base = 40000 límite = 12000 el proceso 2 pide la dirección d ¿d ≥ base? sí ¿d < base + límite? sí acceso a memoria excepción no sale al bus no no 45000 → pasa · 53000 → excepción (es del proceso 3) El circuito solo se activa en modo usuario.
Dos comparaciones en hardware en cada acceso: con 53000 el proceso 2 se saldría de su zona y pisaría al proceso 3.

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:

El temporizador El proceso A está en un bucle infinito. El contador del temporizador baja de 5 a 0 con cada pulso de reloj; al llegar a 0 genera una interrupción, entra el SO y le da la CPU al proceso B. El temporizador: nadie se queda la CPU proceso A: while (1) { } ← bucle infinito SO proceso B 5 4 3 2 1 0 contador del temporizador (baja con cada pulso de reloj): llega a 0 → interrupción ① antes de ceder la CPU, el SO pone el contador ② A no lo puede parar: cambiar el temporizador es una instrucción privilegiada ③ con la interrupción, el SO recupera el control y le da la CPU a B
Aunque A no suelte nunca la CPU, el temporizador garantiza que el SO vuelve a mandar.
  • 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

ComponenteQué esEjemplos
Programas del sistemaUtilidades 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 serviciosLo que se ejecuta en modo privilegiado
Llamadas al sistemaLa forma de pedirle algo al núcleoopen, 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íaLlamadas
Procesosfork (crear), exit (terminar), wait
Memoriasbrk (pedir más memoria)
Archivosopen, close, creat, read, write, mkdir, lockf (bloquear un fichero)
Redessocket (crear una conexión), close
Protecciónchmod (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:

  1. Al encender el equipo, la CPU empieza a ejecutar en un punto fijo de la memoria.
  2. Ahí hay una ROM con una pequeña rutina de arranque (que ya sabe leer de los dispositivos de E/S).
  3. Esa rutina busca en qué dispositivo está el cargador del SO (boot loader) y lo carga en memoria.
  4. 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:

Cinco estructuras de sistema operativo Cinco columnas separadas por la línea entre modo usuario y modo núcleo: monolítico, con todo el núcleo en un bloque; por capas, de L0 hardware a L5 aplicaciones; micronúcleo, con un núcleo mínimo y servicios en modo usuario que se comunican con mensajes; módulos cargables, con un núcleo al que se enchufan módulos en marcha; e híbrido, que combina varias. modo usuario ↑ modo núcleo ↓ Monolítico UNIX clásico Por capas THE (años 60) Micronúcleo Mach (1980) Módulos cargables Linux, Solaris Híbrido Windows, macOS app app todo el núcleo en un solo bloque L5 apps L4 buffering L3 consola L2 memoria L1 procesos L0 hardware servicios app mínimo mensajes apps núcleo módulos en marcha servicios monolítico + módulos + ideas µ Lo que cambia: cuánto código va en modo núcleo y cómo se organiza
Fíjate en cuánto queda por debajo de la línea: el micronúcleo deja allí lo mínimo; el monolítico, todo.
EstructuraIdeaEjemplo
MonolíticaEs 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 separadoUNIX clásico
Por capasUna jerarquía de niveles en la que cada uno usa los servicios del de abajoTHE (años 60): L0 hardware, L1 planificación de procesos, L2 memoria paginada, L3 consola, L4 buffering, L5 aplicaciones
MicronúcleoEn el núcleo, solo lo imprescindible; el resto de servicios son módulos en espacio de usuario que se comunican con mensajesMach (1980)
Módulos cargablesMódulos de código con una interfaz conocida que se incorporan al núcleo en tiempo de ejecución y se llaman entre ellosLinux, Solaris
HíbridaCombina varias de las anterioresLinux (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
VentajasSe 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
InconvenienteLa 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:

  1. 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.
  2. En la línea asíncrona, la B rellena exactamente esos huecos. Cambia COMPILAR a 200 y verás que B ya no cabe en los huecos y la línea crece.
  3. 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.
  4. Pon HUECOS = 4 y 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

ConceptoEn una frase
Controlador de E/SConecta el dispositivo al bus, tiene un búfer local y avisa con interrupciones
Interrupción y RSILa CPU guarda su estado, ejecuta la rutina del SO y continúa
Polling / vectorizadasPreguntar a todos frente a recibir un número que indexa el vector de interrupciones
SO reactivoSolo se activa con interrupciones hardware, llamadas al sistema o excepciones
E/S asíncronaLa CPU hace otra cosa mientras la E/S trabaja: más rendimiento
Jerarquía de memoriasMá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 dualPrivilegiado (todo) y usuario (excepción si intenta una instrucción privilegiada)
ProtecciónE/S privilegiada, base y límite para la memoria, temporizador para la CPU
ComponentesProgramas del sistema, núcleo y llamadas al sistema
EstructurasMonolítica, por capas, micronúcleo, módulos cargables e híbrida

Tres ideas para llevarte

  1. El SO no vigila: le avisan. Interrupciones, llamadas al sistema y excepciones son los únicos momentos en los que el SO se ejecuta.
  2. 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.
  3. 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 →

Sigue aprendiendo

Deja una respuesta

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *