Cerramos el módulo 5 con una pregunta incómoda: durante cuatro lecciones hemos estado fingiendo aislamiento. ProtectSystem=strict finge un sistema de ficheros de solo lectura, PrivateTmp finge un /tmp propio, las capabilities fingen un root recortado. Todo eso funciona, pero todo eso lo decide el mismo núcleo que ejecuta el proceso que queremos contener: si ese núcleo cae, cae todo con él.

La virtualización responde con una idea que en 1974 ya estaba formalizada y que hoy sostiene toda la informática en la nube: darle a un sistema operativo entero la ilusión de que tiene una máquina para él solo. No un directorio propio: una CPU propia, una memoria física propia, un disco propio, una tarjeta de red propia, y su propio núcleo en modo privilegiado. Si meteo-api se ejecuta dentro de una máquina virtual y alguien consigue root y rompe el núcleo de esa máquina virtual, todavía le queda una frontera entera por delante.

En esta lección vas a entender qué significa exactamente virtualizar, por qué durante veinte años x86 fue la peor arquitectura posible para hacerlo y las tres soluciones que se inventaron, cómo se virtualiza cada uno de los tres recursos que estudiamos en el módulo 2 —CPU, memoria y E/S—, y qué precio se paga por todo ello, con números. Terminaremos levantando una réplica de pruebas de meteo-01 con KVM y libvirt, y midiendo la sobrecarga real. Los contenedores, que son otra respuesta al mismo problema con un compromiso completamente distinto, son la lección siguiente; aquí solo diremos por qué una máquina virtual aísla más.

Contenido

  1. Qué significa virtualizar una máquina completa
  2. El problema histórico: consolidación, aislamiento y aprovechamiento
  3. Los criterios de Popek y Goldberg
  4. Por qué x86 no era virtualizable, y las tres respuestas
  5. Hipervisores de tipo 1 y de tipo 2
  6. Virtualización de la CPU: vCPU, sobresuscripción y steal time
  7. Virtualización de la memoria: EPT/NPT, ballooning y sobrecompromiso
  8. Virtualización de la E/S: emulación, virtio y asignación directa
  9. Redes virtuales: puente, NAT y red interna
  10. El disco virtual: formatos, instantáneas y clonado
  11. Migración en vivo
  12. Práctica con KVM y libvirt: una réplica de meteo-01
  13. Seguridad del aislamiento: superficie del hipervisor y escapes
  14. Cuándo virtualizar y cuándo no

Qué significa virtualizar una máquina completa

En 01-01 definimos el sistema operativo como una máquina extendida: convierte el hardware incómodo en abstracciones cómodas (procesos, ficheros, sockets). Un hipervisor hace algo diferente y, en cierto modo, más humilde: no ofrece abstracciones nuevas, sino más copias de la misma máquina.

Capa Qué multiplexa Qué le ofrece a su cliente
Sistema operativo El hardware entre procesos Abstracciones: procesos, ficheros, sockets
Hipervisor El hardware entre sistemas operativos Más hardware (virtual): CPU, RAM, discos, NIC

De ahí el vocabulario:

  • Anfitrión (host): la máquina física y el software que gestiona el reparto.
  • Hipervisor o VMM (Virtual Machine Monitor): la capa que crea y controla las máquinas virtuales.
  • Invitado (guest): el sistema operativo que se ejecuta dentro de una máquina virtual, sin saber que lo hace.

Esa última frase es la clave de todo: el invitado no está portado ni modificado. Un Debian instalado en una máquina virtual es el mismo Debian que instalarías en hierro. Ejecuta su propio proceso init, su propio planificador, sus propias tablas de páginas y sus propios controladores. Cree que /dev/vda es un disco. Cree que tiene 4 CPU. Cree que el arranque de la BIOS lo hizo una placa base.

graph TD
    subgraph Anfitrion["Máquina física meteo-host"]
        HW["Hardware: 16 núcleos, 64 GB RAM, NVMe, NIC 10G"]
        HV["Hipervisor (KVM + QEMU)"]
        subgraph VM1["VM: meteo-01"]
            K1["Núcleo Linux invitado"]
            P1["meteo-api · ingestor · agregador"]
        end
        subgraph VM2["VM: meteo-01-pruebas"]
            K2["Núcleo Linux invitado"]
            P2["Procesos de pruebas"]
        end
        HW --> HV
        HV --> K1
        HV --> K2
        K1 --> P1
        K2 --> P2
    end

Fíjate en la diferencia esencial con lo que veremos en 06-02: aquí hay dos núcleos invitados independientes, cada uno con su propia gestión de memoria, su propio planificador y su propia tabla de procesos. En un contenedor solo hay un núcleo, el del anfitrión, compartido por todos.

El problema histórico: consolidación, aislamiento y aprovechamiento

La virtualización no nació por elegancia teórica, sino por dinero. A finales de los 90 y principios de los 2000, el modelo dominante era un servicio, un servidor físico, por una razón muy sensata que ya conoces del módulo 5: si dos aplicaciones comparten máquina, comparten núcleo, comparten sistema de ficheros y comparten destino. Un fallo de la aplicación A tumbaba a la aplicación B.

El resultado medido en centros de datos reales de la época era demoledor: utilización media de CPU entre el 5 % y el 15 %. Es decir, se compraban, alimentaban, refrigeraban y administraban máquinas que estaban paradas el 90 % del tiempo, porque había que dimensionarlas para el pico y aislarlas por seguridad.

Aplicado a Meteora, el cálculo es fácil de ver:

Escenario Máquinas físicas Utilización media Coste relativo
Un servicio por servidor (meteo-api, ingestor, agregador, pruebas, CI, réplica) 6 ~8 % 100 %
Seis máquinas virtuales sobre un anfitrión potente 1 (+1 de respaldo) ~45 % ~35 %

La virtualización resolvió simultáneamente tres cosas que hasta entonces estaban en conflicto:

  • Consolidación. Muchas cargas en menos hierro, con un ahorro directo en compra, espacio de bastidor, electricidad y refrigeración.
  • Aislamiento. Sin renunciar a la separación: cada carga sigue teniendo su propio sistema operativo, y un núcleo invitado que entra en pánico no arrastra a los demás.
  • Aprovechamiento y flexibilidad. Redimensionar una máquina virtual es editar un fichero XML; añadir un disco es un comando; clonar un servidor entero son segundos. Con hierro, todo eso son semanas y una orden de compra.

A eso se sumó un cuarto beneficio que resultó ser casi tan importante: la máquina pasó a ser un fichero. Una copia de seguridad completa, una instantánea antes de una actualización arriesgada, un entorno de pruebas idéntico al de producción, mover una carga de un servidor a otro sin apagarla... todo eso solo es posible cuando el "servidor" es un conjunto de ficheros y un poco de estado.

Los criterios de Popek y Goldberg

En 1974, Gerald Popek y Robert Goldberg publicaron el artículo que sigue definiendo qué es un hipervisor de verdad. Establecieron tres propiedades que debe cumplir:

Criterio Qué exige Qué pasa si falla
Equivalencia El invitado debe comportarse igual que sobre hardware real (salvo tiempos y recursos disponibles) Habría que modificar el sistema operativo invitado: ya no es virtualización pura
Control de recursos El hipervisor mantiene el control absoluto de los recursos: el invitado nunca puede tomar por su cuenta lo que no se le ha dado Un invitado podría acaparar o acceder a memoria de otro: adiós al aislamiento
Eficiencia La mayoría de las instrucciones deben ejecutarse directamente en la CPU física, sin intervención del hipervisor Ya no es virtualización, es emulación: 10 a 100 veces más lento

El tercer criterio es el que separa la virtualización de la emulación. QEMU en modo emulación pura puede ejecutar un binario ARM sobre un x86 traduciendo cada instrucción: cumple equivalencia y control, pero es lentísimo. Un hipervisor debe conseguir que el código del invitado corra sobre el silicio real, y solo intervenir en los momentos peligrosos.

Instrucciones privilegiadas y sensibles

Para formalizar cuándo eso es posible, Popek y Goldberg clasificaron las instrucciones de una arquitectura. Recupera de 01-06 la distinción entre modo usuario y modo núcleo:

  • Instrucciones privilegiadas: provocan una excepción (trap) si se ejecutan en modo usuario. Por ejemplo, cargar la tabla de páginas o deshabilitar interrupciones.
  • Instrucciones sensibles a la configuración: cambian el estado de los recursos del sistema (activar interrupciones, modificar la tabla de descriptores, cambiar de modo).
  • Instrucciones sensibles al comportamiento: su resultado depende del estado del sistema, por ejemplo de en qué anillo de privilegio se está ejecutando.

Y entonces enunciaron su teorema, que es de una sencillez preciosa:

Se puede construir un hipervisor para una arquitectura si el conjunto de instrucciones sensibles es un subconjunto de las instrucciones privilegiadas.

El mecanismo que hay detrás se llama trap-and-emulate (atrapar y emular) y funciona así:

  1. El sistema operativo invitado se ejecuta desanillado: no en el anillo 0 que él cree tener, sino en un anillo menos privilegiado.
  2. Mientras ejecuta instrucciones inocuas (aritmética, saltos, accesos a su memoria), corre a velocidad nativa. Aquí se cumple la eficiencia.
  3. Cuando ejecuta una instrucción sensible —que por hipótesis es privilegiada—, la CPU lanza una excepción que captura el hipervisor.
  4. El hipervisor emula el efecto de esa instrucción sobre el estado virtual del invitado y le devuelve el control. Aquí se cumplen la equivalencia y el control de recursos.

Si alguna instrucción es sensible pero no privilegiada, se rompe todo: el invitado la ejecuta sin que nadie se entere y obtiene un resultado que revela o altera el estado real de la máquina.

Por qué x86 no era virtualizable, y las tres respuestas

En 2000, John Scott Robin y Cynthia Irvine publicaron un análisis del conjunto de instrucciones de Intel Pentium y encontraron 17 instrucciones sensibles no privilegiadas. x86 fallaba el teorema.

El ejemplo canónico es POPF (pop flags): saca un valor de la pila y lo carga en el registro de banderas, que incluye el bit IF de habilitación de interrupciones. En modo núcleo, POPF cambia IF. En modo usuario, POPF ignora silenciosamente ese bit y no provoca ninguna excepción. Es sensible (afecta a la configuración) pero no privilegiada (no lanza trap). Un núcleo invitado desanillado que intenta deshabilitar interrupciones con POPF cree que lo ha hecho, y no ha pasado nada. A partir de ahí el invitado funciona mal de formas imposibles de depurar.

Otro ejemplo aún más directo: SGDT, SIDT, SLDT y STR leen los registros de tablas de descriptores y funcionan en modo usuario. Un invitado puede leerlos y descubrir que sus tablas no están donde él las puso: sabe que está virtualizado, y peor, ve datos reales del anfitrión.

La industria dio tres respuestas, cronológicamente y con filosofías opuestas.

Respuesta 1: traducción binaria dinámica (VMware, 1999)

VMware Workstation resolvió el problema sin tocar el hardware ni el invitado: en lugar de ejecutar directamente el código del núcleo invitado, lo traduce sobre la marcha a un bloque equivalente y seguro, que se guarda en una caché de traducción.

  • El código de modo usuario del invitado se ejecuta directamente, sin traducir: no es sensible.
  • El código de modo núcleo del invitado se traduce bloque a bloque. Las instrucciones inocuas se copian tal cual; las 17 problemáticas se sustituyen por llamadas al hipervisor.
  • Los bloques traducidos se cachean, de modo que un bucle se traduce una vez y se ejecuta miles.

Fue una hazaña de ingeniería y funcionó, con una penalización del 5 % al 20 % según la carga; su coste fue una complejidad brutal y mucha sensibilidad a las cargas con abundante código de núcleo.

Respuesta 2: paravirtualización (Xen, 2003)

Xen tomó el camino contrario: si el problema es que ciertas instrucciones no atrapan, no las usemos. Se modifica el sistema operativo invitado para que, allí donde haría una operación privilegiada, llame explícitamente al hipervisor mediante una hipercall —exactamente el mismo concepto que una llamada al sistema de 01-06, pero un nivel más abajo—.

Concepto Quién llama A quién Mecanismo
Llamada al sistema Proceso de usuario Núcleo syscall
Hipercall Núcleo invitado Hipervisor Instrucción de trap explícita

Ventajas: rendimiento excelente (sobrecarga del 1 % al 5 %) y diseño más simple. Inconveniente decisivo: hay que modificar el invitado. Con Linux era viable (código abierto), con Windows no. Xen paravirtualizado exigía núcleos específicos.

La paravirtualización desapareció como técnica de virtualización de CPU, pero sobrevivió con enorme éxito en la E/S: virtio, que verás más abajo, es exactamente esta idea aplicada a discos y tarjetas de red, y hoy es el estándar de facto.

Respuesta 3: virtualización asistida por hardware (Intel VT-x y AMD-V, 2005-2006)

La solución definitiva fue arreglar la arquitectura. Intel VT-x y AMD-V (SVM) añadieron un nuevo eje de privilegio perpendicular a los anillos:

  • Modo raíz VMX (root): donde se ejecuta el hipervisor. Tiene sus cuatro anillos 0-3.
  • Modo no raíz VMX (non-root): donde se ejecuta el invitado. También tiene sus cuatro anillos 0-3.

Esto es lo importante y lo que suele explicarse mal: el núcleo invitado se ejecuta en el anillo 0, como él espera, pero en modo no raíz. No está desanillado. No hay que traducir nada. Y en modo no raíz, las instrucciones sensibles sí provocan una salida al hipervisor, incluidas las 17 traidoras. El teorema de Popek y Goldberg se cumple por decreto del silicio.

Al modo raíz se le llama coloquialmente "anillo -1", la expresión que ya adelantamos en 01-06: un nivel de privilegio por debajo del anillo 0 del invitado.

El estado de cada máquina virtual vive en una estructura en memoria llamada VMCS (Virtual Machine Control Structure) en Intel, o VMCB en AMD. Contiene tres cosas:

  • El estado del invitado: registros, punteros de tablas de páginas, estado de control. Se guarda al salir y se restaura al entrar.
  • El estado del anfitrión: dónde volver cuando el invitado salga.
  • Los campos de control: qué eventos deben provocar una salida. Aquí se decide, por ejemplo, si CPUID sale al hipervisor o si las escrituras a CR3 se gestionan solas.

El ciclo de vida es un bucle entre dos instrucciones y un evento:

sequenceDiagram
    participant HV as Hipervisor (modo raíz)
    participant CPU as CPU
    participant G as Invitado (modo no raíz)
    HV->>CPU: VMLAUNCH / VMRESUME
    CPU->>G: Carga estado desde VMCS y ejecuta
    Note over G: Código nativo a toda velocidad
    G->>CPU: Instrucción sensible / E/S / interrupción
    CPU->>HV: VM exit (guarda estado en VMCS)
    Note over HV: Emula la operación
    HV->>CPU: VMRESUME

El VM exit es la unidad de coste de toda la virtualización moderna, y conviene tener el número en la cabeza:

Operación Coste aproximado
Instrucción aritmética < 1 ciclo (con paralelismo)
Fallo de caché a RAM ~200-300 ciclos
Llamada al sistema (syscall) ~100-150 ciclos
VM exit + VM entry ~1.000-1.500 ciclos (bajó desde ~4.000 en las primeras generaciones)

Con una CPU a 3 GHz, 1.200 ciclos son 0,4 microsegundos. Parece poco, pero si una carga provoca 100.000 salidas por segundo, se está gastando un 4 % de un núcleo entero solo en entrar y salir. Por eso todo el diseño de la virtualización moderna consiste en eliminar salidas: EPT las elimina para los fallos de página, virtio las reduce agrupando operaciones de E/S, y SR-IOV las elimina del todo para la red.

Hipervisores de tipo 1 y de tipo 2

La clasificación clásica, también de Goldberg, distingue según qué hay debajo del hipervisor.

Aspecto Tipo 1 (bare-metal) Tipo 2 (alojado)
Se ejecuta sobre El hardware desnudo Un sistema operativo anfitrión
Controladores de dispositivo Propios (o de un dominio de servicio) Los del sistema anfitrión
Rendimiento Máximo Bueno, con una capa más
Superficie de ataque Mínima La del anfitrión completo
Arranque Es lo primero que arranca Se lanza como una aplicación más
Uso típico Producción, centros de datos, nube Escritorio, desarrollo, laboratorio
Ejemplos VMware ESXi, Xen, Microsoft Hyper-V, KVM VirtualBox, VMware Workstation, Parallels, QEMU sin KVM

En la práctica, esta frontera se ha vuelto borrosa, y KVM es el ejemplo perfecto. KVM (Kernel-based Virtual Machine) no es un hipervisor separado: es un módulo del núcleo Linux (kvm.ko más kvm-intel.ko o kvm-amd.ko) que convierte al propio núcleo Linux en hipervisor de tipo 1.

La jugada es elegantísima, y encaja con todo lo que vimos en el módulo 2: Linux ya sabe planificar tareas sobre CPU, ya sabe gestionar memoria virtual, ya sabe hablar con los discos y las tarjetas de red. Un hipervisor necesita exactamente eso. ¿Para qué reescribirlo? KVM solo añade lo que falta: la gestión de la VMCS y el modo raíz.

Con KVM, una máquina virtual es un proceso normal de Linux, y cada vCPU es un hilo de ese proceso. Esto tiene consecuencias que se pueden comprobar:

# Una VM en ejecución aparece como un proceso más
ps -eo pid,comm,nlwp,pcpu --sort=-pcpu | head -5
#   PID COMMAND         NLWP %CPU
#  4211 qemu-system-x86    7  185

# Sus hilos: uno por vCPU más los de E/S
ps -L -p 4211 -o tid,comm | head

Qué muestra. nlwp 7 indica siete hilos: cuatro vCPU y tres auxiliares. El %CPU de 185 significa que la VM está usando el equivalente a 1,85 núcleos físicos. Y como es un proceso normal, se le pueden aplicar todas las herramientas del curso: nice y chrt para su prioridad (02-02), cgroups para limitar su memoria y su CPU (06-02), taskset para fijarlo a unos núcleos concretos, y hasta el OOM killer puede matarlo, con el efecto dramático de apagar de golpe un servidor virtual entero.

Bajo KVM, el complemento habitual es QEMU, que aporta lo que KVM no hace: emular la placa base, la BIOS/UEFI, el reloj, el bus PCI y los dispositivos. La división del trabajo es limpia: KVM ejecuta las instrucciones del invitado a velocidad nativa; QEMU atiende los VM exits que corresponden a dispositivos.

Virtualización de la CPU: vCPU, sobresuscripción y steal time

Cada vCPU de una máquina virtual es, en KVM, un hilo del proceso QEMU. Y como cualquier hilo, lo planifica el planificador del anfitrión, el CFS-EEVDF que estudiamos en 02-02. Hay entonces dos niveles de planificación superpuestos:

  1. El planificador del invitado reparte sus procesos entre sus 4 vCPU. Cree que son CPU reales y siempre disponibles.
  2. El planificador del anfitrión reparte las vCPU de todas las máquinas virtuales entre los 16 núcleos físicos.

Como el invitado no sabe nada del segundo nivel, puede tomar decisiones malas: por ejemplo, hacer spin en un spinlock (03-04) esperando a otra vCPU que el anfitrión ha desalojado y no volverá a ejecutarse en 10 milisegundos. Este problema, el lock holder preemption, es la razón de que los núcleos modernos usen spinlocks paravirtualizados que ceden al hipervisor.

Sobresuscripción

Sobresuscribir es asignar más vCPU en total que núcleos físicos hay. Con 16 núcleos físicos y 6 máquinas virtuales de 4 vCPU cada una, hay 24 vCPU sobre 16 núcleos: una ratio de 1,5:1.

Funciona por la misma razón por la que funciona la multiprogramación: casi ninguna carga usa su CPU todo el tiempo. meteo-api está la mayor parte del tiempo bloqueado en accept() esperando peticiones.

Ratio vCPU:pCPU Situación típica Riesgo
1:1 o menos Cargas críticas y sensibles a la latencia Ninguno, pero se desaprovecha
2:1 a 4:1 Servidores generales, entornos de pruebas Aceptable si se vigila el steal time
> 8:1 Escritorios virtuales, cargas muy ociosas Latencias erráticas

La memoria, en cambio, no se sobresuscribe con la misma alegría, porque un proceso que no usa CPU sí sigue ocupando su RAM.

Steal time: el reloj que delata

¿Cómo sabe un invitado que está esperando CPU? El hipervisor se lo cuenta. Linux expone el tiempo robado: el tiempo durante el cual una vCPU estaba lista para ejecutar pero el anfitrión no le dio núcleo físico.

top -bn1 | head -3
# %Cpu(s): 12,3 us,  3,1 sy,  0,0 ni, 68,2 id,  1,4 wa,  0,0 hi,  0,0 si, 15,0 st

Qué significa st. Ese 15,0 st es la cifra más importante para diagnosticar una máquina virtual lenta. Dice que el 15 % del tiempo de CPU que el invitado creía tener se lo ha quedado otro. Y la consecuencia práctica es brutal para el diagnóstico:

  • Si dentro de la VM ves latencias altas y %us bajo, %wa bajo y st alto, el problema no está dentro de tu máquina: está en el anfitrión, sobresuscrito. Nada que optimices en tu código lo va a arreglar.
  • Un st sostenido por encima del 5 % ya es señal de contención; por encima del 10 % es un problema.
  • En la nube pública, st alto en instancias baratas es a menudo el producto que has comprado: los tipos de instancia con CPU "ampliable" funcionan así.

Tres piezas más que conviene conocer: la fijación (pinning) ata cada vCPU a un núcleo físico concreto, elimina la migración entre núcleos, conserva la localidad de caché y de NUMA y reduce el jitter, por lo que es la norma en cargas sensibles a la latencia; el CPUID enmascarado filtra las capacidades que anuncia la CPU para que una VM no descubra instrucciones (AVX-512, por ejemplo) que no existirán en el destino si se migra; y el reloj debe ser paravirtualizado (kvm-clock), porque un invitado al que desalojan no puede fiarse de contar ciclos.

Virtualización de la memoria: EPT/NPT, ballooning y sobrecompromiso

Este es, conceptualmente, el punto más bonito de la lección, y se apoya directamente en la paginación de 02-04.

Recuerda el esquema normal: un proceso usa direcciones virtuales, y la MMU las traduce a direcciones físicas recorriendo una tabla de páginas de cuatro niveles, con la TLB cacheando el resultado.

Ahora aparece un problema nuevo: el invitado tiene sus propias tablas de páginas y traduce de virtual-invitado a físico-invitado. Pero el "físico" del invitado no es físico de verdad: es una ficción que el hipervisor debe traducir a físico-anfitrión o máquina. Hay dos traducciones encadenadas:

Dirección virtual del proceso invitado
        ↓ (tablas de páginas del invitado)
Dirección "física" del invitado  ← ficticia
        ↓ (traducción del hipervisor)
Dirección física real de la máquina

Tablas sombra: la solución histórica

Antes de que el hardware ayudara, el hipervisor mantenía tablas de páginas sombra (shadow page tables): tablas reales que traducían directamente de virtual-invitado a físico-real, combinando las dos traducciones de antemano. La MMU usaba la tabla sombra y no se enteraba de nada.

El coste era terrible: el hipervisor debía marcar las tablas del invitado como solo lectura para enterarse de cada modificación. Cada vez que el invitado creaba o modificaba una entrada —cosa que un sistema operativo hace constantemente—, se producía un fallo de página, un VM exit y un trabajo de sincronización. En cargas con muchas creaciones de procesos, la penalización podía superar el 40 %.

EPT y NPT: la traducción anidada en hardware

Intel EPT (Extended Page Tables) y AMD NPT/RVI resolvieron el problema añadiendo a la MMU una segunda tabla de páginas, gestionada por el hipervisor, que traduce de físico-invitado a físico-real. Ahora la MMU hace las dos traducciones sola, sin intervención de nadie.

La ventaja es enorme: el invitado puede modificar sus tablas de páginas libremente, sin VM exits. Los fallos de página del invitado los resuelve el invitado. El hipervisor solo interviene cuando falla la segunda tabla.

El precio es el número de accesos a memoria cuando falla la TLB. Con paginación de cuatro niveles, un recorrido normal cuesta 4 accesos. Con dos niveles anidados, cada uno de esos 4 accesos requiere a su vez su propio recorrido de 4 niveles:

Escenario Accesos a memoria por fallo de TLB
Sin virtualizar (4 niveles) 4
Con EPT (4 × 4 + 4) hasta 24

Por eso el consejo práctico de 02-04 se vuelve más importante dentro de una máquina virtual: usar páginas enormes (huge pages) de 2 MB reduce los niveles de ambas tablas y multiplica el alcance de la TLB. En una base de datos virtualizada, activar hugepages puede dar entre un 5 % y un 15 % de mejora.

Ballooning: recuperar memoria de un invitado

Un invitado que ha usado memoria no la devuelve: su núcleo la conserva en su caché de página, porque desde su punto de vista es gratis. El anfitrión ve esa memoria como usada aunque nadie la necesite.

La solución es astuta y se llama globo de memoria (balloon driver). Es un controlador paravirtualizado dentro del invitado que, a petición del hipervisor, pide memoria a su propio núcleo y no la usa para nada. Al "inflarse", el núcleo invitado sufre presión de memoria y libera cachés o hace swap, exactamente como estudiamos en 02-04; las páginas que el globo obtiene se comunican al hipervisor, que las reasigna a otras máquinas virtuales. Al desinflarse, el globo las libera y el invitado recupera memoria. Es una forma de que el hipervisor pida cooperación al invitado en lugar de robarle páginas a ciegas.

Deduplicación: KSM

KSM (Kernel Samepage Merging) recorre la memoria buscando páginas idénticas entre distintas máquinas virtuales y las fusiona en una sola copia física marcada como copia-en-escritura (el mismo COW de fork que vimos en 02-01). Si diez VM ejecutan el mismo Debian, sus binarios en memoria son idénticos y se ahorra mucho: en granjas homogéneas se han medido ahorros del 30 % al 50 %.

Dos advertencias serias. Cuesta CPU: el demonio ksmd escanea continuamente, y se controla con /sys/kernel/mm/ksm/pages_to_scan y sleep_millisecs. Y tiene implicaciones de seguridad: la fusión abre un canal encubierto medible por tiempo —escribir en una página fusionada tarda más, porque hay que copiarla—, lo que permite a una VM deducir qué páginas tiene otra. En entornos multiinquilino, KSM se desactiva.

Sobrecompromiso y su riesgo

Igual que con la CPU, se puede prometer más RAM de la que hay: 8 VM de 8 GB sobre un anfitrión de 48 GB. Funciona mientras las VM no usen todo lo prometido a la vez.

El riesgo es cualitativamente peor que con la CPU. Si falta CPU, todo va lento. Si falta RAM, el anfitrión empieza a hacer swap de páginas de invitados, y eso es catastrófico: el invitado no sabe que su "RAM" está en un disco, así que sus propias decisiones de gestión de memoria se vuelven absurdas —puede estar haciendo swap de sus procesos a la vez que el anfitrión hace swap de él, el fenómeno llamado double paging—. Y si se agota del todo, el OOM killer del anfitrión mata un proceso QEMU: una máquina virtual entera desaparece de golpe, como si le hubieran quitado el cable de alimentación.

Regla práctica: sobrecomprometer CPU sí, con vigilancia del steal time; sobrecomprometer memoria solo con margen amplio, ballooning activo y monitorización, y nunca en máquinas críticas.

Virtualización de la E/S: emulación, virtio y asignación directa

Retomamos aquí lo de 02-07: controladores, interrupciones y DMA. Hay tres estrategias, de peor a mejor rendimiento.

  1. Dispositivo emulado

QEMU emula un dispositivo real y conocido: una tarjeta de red Intel e1000, un controlador IDE, una tarjeta gráfica Cirrus. El invitado usa su controlador estándar, sin saber nada.

Su ventaja es la compatibilidad absoluta: cualquier sistema operativo, por antiguo que sea, arranca. Su coste es desastroso: cada acceso a un registro del dispositivo —cada outb, cada escritura a memoria mapeada— provoca un VM exit, y enviar un paquete de red puede costar 10 o 15 salidas.

  1. Paravirtualizado: virtio

Aquí revive la idea de Xen. virtio define una interfaz explícitamente virtual: el invitado sabe que está virtualizado y usa un controlador diseñado para hablar con un hipervisor de la forma más barata posible.

El mecanismo central es la virtqueue: un anillo de descriptores en memoria compartida entre invitado e hipervisor, con la misma filosofía que los anillos de DMA de 02-07.

  1. El invitado escribe varias peticiones en el anillo. Sin ningún VM exit: es memoria compartida.
  2. Cuando quiere avisar, hace una sola notificación (kick), que sí provoca una salida.
  3. El hipervisor procesa el lote entero y notifica la finalización con una interrupción virtual.

Ese "una salida por lote en lugar de una por operación" es todo el truco, y es exactamente la misma lógica de amortización que NAPI en 02-07. Los dispositivos habituales son virtio-net, virtio-blk, virtio-scsi y virtio-balloon. En Linux vienen en el núcleo desde hace más de quince años, y por eso los discos de una VM moderna se llaman /dev/vda y no /dev/sda.

Una vuelta de tuerca más es vhost: mover el procesamiento del anillo del espacio de usuario (QEMU) al núcleo del anfitrión (vhost-net), eliminando cambios de contexto adicionales.

  1. Asignación directa: passthrough, IOMMU y SR-IOV

La opción extrema: darle a la máquina virtual el dispositivo físico. El invitado habla con el hardware real con su controlador nativo, sin capa intermedia.

Esto solo es seguro gracias a la IOMMU que estudiamos en 02-07. Sin ella, el dispositivo hace DMA a direcciones físicas reales y una VM podría programarlo para que escribiera en la memoria del anfitrión: un escape total. Con IOMMU (VT-d o AMD-Vi), el dispositivo tiene su propia traducción de direcciones y solo puede tocar la memoria de su VM. Como adelantamos entonces, esta es la pieza que hace viable el passthrough.

El problema del passthrough puro es que un dispositivo se da a una sola VM. SR-IOV (Single Root I/O Virtualization) lo resuelve en el propio hardware: una tarjeta de red compatible se presenta como una función física (PF) y hasta 64 o 128 funciones virtuales (VF), cada una con sus colas y su dirección MAC. Cada VF se asigna a una VM distinta, y el reparto lo hace el silicio de la tarjeta.

Estrategia Sobrecarga Rendimiento típico (red 10G) Migración en vivo Cuándo usarla
Emulado (e1000) Muy alta 1-2 Gbit/s, CPU alta Compatibilidad, sistemas antiguos
virtio + vhost Baja (~5-10 %) 8-9,5 Gbit/s Caso general: por defecto
Passthrough / SR-IOV Casi nula (<2 %) ~9,9 Gbit/s, latencia mínima No (o muy complicada) NFV, alto rendimiento, baja latencia

La columna de la migración es la que decide en la práctica: la asignación directa ata la VM a ese hardware concreto, y con ello se pierde la flexibilidad que es la mitad del valor de virtualizar.

Redes virtuales: puente, NAT y red interna

El hipervisor implementa un conmutador virtual en software, al que se conectan las interfaces de las VM. Hay tres topologías básicas.

Modo Qué hace Dirección IP de la VM Visible desde la red física Uso
Puente (bridge) La VM se conecta al mismo segmento L2 que el anfitrión Del DHCP de la red real Servidores en producción
NAT El anfitrión traduce direcciones; red privada interna Privada (p. ej. 192.168.122.x) No (solo salida) Escritorio, desarrollo
Red interna / aislada Solo comunicación entre VM del mismo anfitrión Privada, sin salida No Laboratorios, redes de pruebas

Para una réplica de meteo-01 que debe recibir lecturas de las estaciones, el modo puente es el correcto: la VM aparece en la red como una máquina más, con su propia IP.

Y aquí una advertencia de seguridad que enlaza con 05-03: el cortafuegos del anfitrión no ve el tráfico entre VM del mismo puente. Dos máquinas virtuales conectadas al mismo puente se hablan a nivel de enlace sin pasar por nftables del anfitrión. Las consecuencias:

  • El cortafuegos de cada invitado sigue siendo obligatorio; no basta con uno perimetral.
  • Para filtrar en el puente hacen falta mecanismos específicos (ebtables, nftables en la familia bridge, filtros de libvirt o grupos de seguridad del proveedor).
  • Segmentar con redes virtuales separadas por función es mejor que confiar en reglas: la red de gestión no debe compartir puente con la red de datos.

El disco virtual: formatos, instantáneas y clonado

El disco de una máquina virtual es normalmente un fichero en el anfitrión, aunque también puede ser un volumen LVM (04-03) o un LUN de una cabina.

Formato Aprovisionamiento fino Instantáneas Rendimiento Notas
RAW Solo con ficheros dispersos No (salvo LVM/btrfs debajo) Máximo Simple; el más rápido
QCOW2 , internas y encadenadas Bueno (5-10 % menos) Estándar en KVM; compresión y cifrado
VMDK Bueno VMware
VHDX Bueno Hyper-V

El aprovisionamiento fino (thin provisioning) significa que un disco declarado de 500 GB ocupa al principio unos pocos megabytes y crece según se escribe. Ahorra muchísimo espacio, pero introduce un riesgo operativo real: se pueden crear diez discos de 500 GB sobre un almacén de 2 TB, y el día que todos crezcan, el almacén se llena y todas las VM se paran a la vez. Es exactamente el mismo sobrecompromiso de antes, aplicado al disco, y requiere la misma vigilancia.

Instantáneas y su coste

Una instantánea (snapshot) congela el estado del disco en un instante. En QCOW2 se implementa creando un fichero nuevo que solo guarda los bloques modificados y encadenándolo sobre el anterior, que pasa a ser de solo lectura.

Esto tiene dos consecuencias que se subestiman siempre. La primera es que cada lectura puede requerir recorrer la cadena: con 5 instantáneas encadenadas, leer un bloque puede implicar mirar en 5 ficheros, y el rendimiento se degrada de forma acumulativa. La segunda es que una instantánea no es una copia de seguridad: depende del fichero base, así que si se corrompe el disco original la instantánea no sirve de nada, y si se guarda en el mismo almacenamiento un fallo del almacén se lleva las dos. Las copias 3-2-1 de 05-03 siguen siendo obligatorias.

La regla operativa es clara: las instantáneas son para ventanas cortas —"voy a actualizar el núcleo, si falla revierto"— y se consolidan o borran en horas, no en meses.

Plantillas y clonado enlazado

Una plantilla es una imagen base preparada (sistema instalado, actualizado, endurecido y "limpiada" de identificadores únicos, con virt-sysprep). A partir de ella:

  • Clonado completo: se copia el disco entero. Independiente, ocupa lo suyo, tarda.
  • Clonado enlazado (linked clone): se crea un QCOW2 nuevo con la plantilla como base de solo lectura. Ocupa megabytes y se crea en un segundo; solo se escriben las diferencias.

Detente un momento en el clonado enlazado, porque es exactamente la misma idea que verás en 06-02 con las imágenes de contenedores y overlayfs: una capa base compartida de solo lectura y una capa de escritura por instancia. La técnica es tan buena que aparece en los dos mundos.

Migración en vivo

Mover una máquina virtual en ejecución de un anfitrión a otro, sin apagarla y con una interrupción de decenas de milisegundos, es probablemente la característica más impresionante de la virtualización, y la que hace viable el mantenimiento de un centro de datos sin ventanas de parada.

Lo que hay que trasladar es: el estado de la CPU (registros, unos kilobytes: trivial), el estado de los dispositivos (pequeño), el disco (si el almacenamiento es compartido, nada que copiar: es el motivo de que existan las SAN y las cabinas) y la memoria, que es el reto: 16 GB por una red de 10 Gbit/s son unos 13 segundos en el mejor caso, y durante esos 13 segundos la VM sigue modificando páginas.

La técnica estándar es la precopia iterativa:

  1. Ronda 0: se copian todas las páginas de memoria mientras la VM sigue ejecutándose.
  2. Rondas 1..n: se copian solo las páginas ensuciadas durante la ronda anterior (el hipervisor las rastrea con bits de "sucio").
  3. Cada ronda es más pequeña que la anterior, porque hay menos tiempo para ensuciar.
  4. Parada final (stop-and-copy): cuando el conjunto pendiente baja de un umbral, se pausa la VM, se copian las últimas páginas y el estado de CPU, y se arranca en el destino. Es la única interrupción real: típicamente 50-300 ms.

El caso patológico se llama conjunto de trabajo caliente: si la VM ensucia páginas más rápido de lo que la red las copia —una base de datos con escrituras intensas—, las rondas no convergen y la migración no termina nunca. Las soluciones son estrangular la CPU del invitado para que ensucie menos, o cambiar de estrategia a poscopia: mover la VM inmediatamente y traer las páginas del origen bajo demanda, lo que reduce el tiempo total pero introduce un riesgo grave —si la red cae a mitad, la VM queda partida entre dos máquinas y se pierde—.

Requisitos que hay que tener presentes: CPU compatibles entre origen y destino (de ahí el enmascarado de CPUID), almacenamiento compartido o migración de disco añadida, red común, y nada de passthrough, por lo que ya vimos.

Práctica con KVM y libvirt: una réplica de meteo-01

libvirt es la capa de gestión que unifica el trato con hipervisores distintos (KVM, Xen, LXC, ESXi) tras una misma API, un demonio (libvirtd) y una herramienta de línea de comandos (virsh). Define cada VM como un XML de dominio.

Primero, comprobar que la máquina puede virtualizar:

# ¿La CPU tiene extensiones de virtualización?
grep -c -E 'vmx|svm' /proc/cpuinfo      # >0 → sí (vmx=Intel, svm=AMD)

# ¿Están cargados los módulos de KVM?
lsmod | grep kvm
# kvm_intel   372736  0
# kvm        1028096  1 kvm_intel

# Diagnóstico completo de libvirt
virt-host-validate qemu
#   QEMU: Checking for hardware virtualization    : PASS
#   QEMU: Checking if device /dev/kvm exists      : PASS
#   QEMU: Checking for cgroup 'cpu' controller    : PASS
#   QEMU: Checking for secure guest support       : WARN

Qué hace. vmx/svm en /proc/cpuinfo indica soporte de VT-x o AMD-V; si no aparece, casi siempre está desactivado en la BIOS, no ausente. virt-host-validate recorre todos los requisitos —módulos, permisos de /dev/kvm, controladores de cgroup, IOMMU— y es lo primero que hay que ejecutar cuando algo no arranca.

Ahora creamos la réplica de pruebas. La idea es tener un meteo-01-pruebas idéntico en configuración al de producción, donde ensayar actualizaciones y restauraciones sin tocar el servicio real:

sudo virt-install \
  --name meteo-01-pruebas \
  --memory 4096 \
  --vcpus 2,maxvcpus=4 \
  --cpu host-passthrough \
  --disk path=/var/lib/libvirt/images/meteo-01-pruebas.qcow2,size=60,format=qcow2,bus=virtio,cache=none,discard=unmap \
  --disk path=/var/lib/libvirt/images/meteo-01-datos.qcow2,size=200,format=qcow2,bus=virtio,cache=none \
  --network bridge=br0,model=virtio \
  --os-variant debian12 \
  --graphics none \
  --console pty,target_type=serial \
  --location 'https://deb.debian.org/debian/dists/stable/main/installer-amd64/' \
  --extra-args 'console=ttyS0,115200n8'

Opción por opción, y por qué:

  • --memory 4096 y --vcpus 2,maxvcpus=4: 4 GB y 2 vCPU, con margen para llegar a 4 en caliente sin recrear la VM.
  • --cpu host-passthrough: expone la CPU real del anfitrión tal cual, con todas sus instrucciones. Da el máximo rendimiento y impide migrar a un anfitrión con CPU distinta. Para producción con migración se usaría un modelo genérico.
  • Dos discos: uno de sistema (60 GB) y otro de datos (200 GB) para /var/lib/meteora. La separación es la misma decisión de 04-03: un llenado de los datos no debe impedir arrancar.
  • bus=virtio: los discos paravirtualizados de los que hablamos; aparecerán como /dev/vda y /dev/vdb.
  • cache=none: crítico para la integridad. Desactiva la caché de página del anfitrión para ese disco, de modo que un fsync() del invitado (04-05) llegue de verdad al hardware. Con cache=writeback va más rápido, pero un corte eléctrico del anfitrión puede corromper el sistema de archivos del invitado y hacer inútil el journaling.
  • discard=unmap: propaga el TRIM del invitado al fichero QCOW2, de modo que borrar dentro de la VM libera espacio de verdad en el anfitrión. Sin esto, un disco fino solo crece.
  • --network bridge=br0,model=virtio: puente para que la réplica reciba lecturas de las estaciones, con NIC paravirtualizada.
  • --graphics none + --console pty,target_type=serial: sin escritorio; consola serie, que es lo que quieres en un servidor y lo que permite ver los mensajes de arranque.

Y las operaciones cotidianas:

virsh list --all                          # dominios definidos y su estado
virsh start meteo-01-pruebas              # arrancar
virsh console meteo-01-pruebas            # entrar por consola serie (salir: Ctrl+])
virsh shutdown meteo-01-pruebas           # apagado ordenado (ACPI, el invitado colabora)
virsh destroy meteo-01-pruebas            # ¡corte de corriente! solo si no responde
virsh dumpxml meteo-01-pruebas > repl.xml # exportar la definición completa
virsh setmem meteo-01-pruebas 6G --live   # ajustar memoria en caliente (ballooning)
virsh domblkstat meteo-01-pruebas vda     # estadísticas de E/S del disco
virsh snapshot-create-as meteo-01-pruebas antes-actualizar --disk-only --atomic

Dos advertencias importantes. virsh destroy no borra la VM: la apaga bruscamente, equivalente a tirar del cable, con el consiguiente riesgo de corrupción; borrar la definición es virsh undefine. Y el virsh shutdown requiere que el invitado tenga instalado qemu-guest-agent o al menos responda a ACPI; sin eso, no ocurre nada y hay gente que concluye erróneamente que la VM está colgada.

Para el aprovisionamiento inicial sin instalador interactivo, lo estándar es partir de una imagen de nube oficial y configurarla con cloud-init: usuarios, claves SSH, paquetes y ficheros se declaran en un YAML que se inyecta en el primer arranque. Es la herramienta que convierte una plantilla genérica en meteo-01-pruebas ya configurado, y la veremos con un ejemplo completo en El Sistema Operativo en la Nube.

Seguridad del aislamiento: superficie del hipervisor y escapes

La promesa de la virtualización es que un compromiso total del invitado no alcanza al anfitrión. ¿Hasta qué punto es cierta?

La superficie de ataque del hipervisor es todo aquello que un invitado malicioso puede tocar:

Superficie Ejemplo Mitigación
Instrucciones que provocan VM exit Manejadores de CPUID, MSR, E/S Código muy auditado; poco frecuente
Dispositivos emulados El controlador de disquete, USB, red, VGA de QEMU La mayor fuente de CVE: quitar lo que no se use
Interfaces paravirtualizadas virtio, balloon, canales del agente Validación estricta de descriptores
Canales laterales del hardware Spectre, Meltdown, L1TF, MDS, Foreshadow Microcódigo, l1tf=flush, desactivar SMT
Gestión API de libvirt, consolas, plano de control Autenticación y red de gestión separada

El caso histórico que todo el mundo cita es VENOM (CVE-2015-3456), un desbordamiento en el controlador de disquete virtual de QEMU. Un invitado con root podía escapar al anfitrión, y la lección fue humillante y valiosísima: la vulnerabilidad estaba en un dispositivo que nadie usaba desde hacía veinte años y que estaba ahí "por compatibilidad". Es el principio de superficie mínima de 05-01, aplicado a la definición de la VM: quita el disquete, el USB, el audio y la VGA de tus servidores.

Las medidas de endurecimiento del anfitrión son, en el fondo, las mismas del módulo 5 aplicadas al proceso QEMU: se ejecuta como usuario libvirt-qemu sin privilegios, con sVirt (SELinux o AppArmor) etiquetando cada dominio de forma que un QEMU comprometido no pueda tocar los discos de otra VM, con seccomp filtrando sus syscalls y con la IOMMU acotando el DMA. La defensa en profundidad no desaparece por virtualizar: se aplica un nivel más abajo.

Por qué una VM aísla más que un contenedor

Este es el punto que hay que llevarse de la lección, y que prepara la siguiente:

  • En una máquina virtual, la frontera es el hipervisor. Un atacante que consigue root en el invitado y rompe el núcleo invitado entero aún tiene delante una interfaz estrecha y muy auditada: unos cuantos manejadores de VM exit y unos dispositivos virtio.
  • En un contenedor, la frontera es el núcleo del anfitrión, y su interfaz es la tabla de llamadas al sistema: entre 300 y 400 puntos de entrada, muchos con historial de fallos.

La comparación cuantitativa es brutal: cientos de syscalls frente a un puñado de manejadores. Por eso, cuando hay que ejecutar código de terceros no confiable —el caso típico de la nube pública, o de un sistema que compila código de clientes—, la respuesta correcta sigue siendo una máquina virtual, o una de las tecnologías intermedias que veremos en 06-03.

Cuándo virtualizar y cuándo no

La sobrecarga real, medida sobre KVM moderno con virtio y EPT:

Recurso Sobrecarga típica Condición
CPU (cálculo puro) 1-3 % Sin sobresuscripción
Memoria (acceso) 2-8 % Peor con muchos fallos de TLB; mejora con huge pages
Memoria (consumo) +200-500 MB por VM El núcleo invitado y su caché
Disco secuencial 5-10 % Con virtio-blk y cache=none
Disco aleatorio (IOPS) 10-25 % Lo más castigado
Red (throughput) 5-15 % Casi nulo con SR-IOV
Latencia de red +20-50 µs Relevante solo en cargas muy sensibles

Con esos números en la mano:

Virtualiza cuando necesites aislamiento fuerte entre cargas, ejecutar sistemas operativos distintos, consolidar servidores infrautilizados, poder migrar en vivo para mantener el hierro sin paradas, tener instantáneas y entornos de pruebas idénticos a producción, o ejecutar código que no controlas.

No virtualices cuando la carga necesite todo el hardware (una base de datos que exprime un NVMe, un nodo de cálculo científico), cuando la latencia sea crítica hasta el microsegundo (trading, telecomunicaciones sin SR-IOV), cuando el acceso al hardware sea especial (tarjetas de adquisición, GPU sin virtualización), o cuando el objetivo real sea empaquetar y desplegar una aplicación: para eso los contenedores son incomparablemente más ligeros, y es justo la lección siguiente.

Y un consejo de arquitectura: no es una elección binaria. El patrón más extendido hoy es contenedores dentro de máquinas virtuales: la VM aporta la frontera de seguridad fuerte entre inquilinos o entornos, y el contenedor aporta la densidad y la velocidad de despliegue dentro de esa frontera.

Errores Comunes y Consejos

Creer que la virtualización es "lenta". Lo era en 2004, con traducción binaria y tablas sombra. Hoy, una carga de CPU pura pierde un 1-3 %. Lo que sí sigue costando es la E/S mal configurada: usar dispositivos emulados en lugar de virtio puede multiplicar por cinco el consumo de CPU de la red.

Ignorar el st de top. Es el primer indicador que hay que mirar en una VM lenta y el único que dice "el problema no está aquí dentro". Muchas horas de optimización se han perdido tuneando una aplicación cuando lo que había era un anfitrión sobresuscrito.

Sobrecomprometer memoria como se sobrecompromete CPU. No son equivalentes. Falta de CPU es lentitud; falta de RAM es el OOM killer del anfitrión matando una VM entera. Deja siempre margen para el propio anfitrión (unos 2-4 GB) y monitoriza.

Usar cache=writeback en producción. Es tentador porque los benchmarks mejoran mucho, pero rompe la garantía de durabilidad de fsync() que estudiamos en 04-05: el invitado cree que ha escrito y el dato está en la RAM del anfitrión. Un corte eléctrico corrompe el sistema de archivos del invitado. Usa cache=none (o directsync) para datos que importan.

Acumular instantáneas. Cada eslabón de la cadena añade indirección en cada lectura, y una cadena de diez instantáneas puede reducir el rendimiento a la mitad. Y no son copias de seguridad: dependen del fichero base.

Confundir virsh destroy con borrar. destroy es "tirar del cable"; undefine es borrar la definición. Aprender esto por las malas con un dominio de producción es un rito de paso que conviene evitar.

Dejar dispositivos emulados innecesarios. El disquete, el USB, el audio y la tarjeta gráfica de un servidor virtual solo aportan superficie de ataque (recuerda VENOM). Revisa el XML del dominio y quita todo lo que no uses.

Olvidar el cortafuegos dentro de la VM. El tráfico entre VM del mismo puente no pasa por nftables del anfitrión. Cada invitado necesita su propia política, con policy drop como en 05-03.

Consejo: mide siempre dentro y fuera. Cuando una VM va lenta, compara sus métricas con las del anfitrión (vmstat, iostat del anfitrión, virsh domstats). La mitad de los problemas de rendimiento en virtualización se diagnostican mirando en el nivel equivocado.

Consejo: activa huge pages para cargas con mucha memoria. Con EPT, un fallo de TLB puede costar hasta 24 accesos a memoria. Las páginas de 2 MB reducen drásticamente esa presión, y en bases de datos virtualizadas la mejora es medible.

Ejercicios

Ejercicio 1: aplicar el teorema de Popek y Goldberg

Una arquitectura ficticia NOVA-32 tiene estas instrucciones:

Instrucción Qué hace ¿Lanza excepción en modo usuario?
LDTAB Carga el registro de tabla de páginas
RDTAB Lee el registro de tabla de páginas No
SETINT Habilita/deshabilita interrupciones
RDMODE Devuelve el anillo actual de ejecución No
ADDW Suma dos registros No
IOWR Escribe en un puerto de E/S

(a) Clasifica cada instrucción como privilegiada, sensible (y de qué tipo) o inocua. (b) ¿Cumple NOVA-32 el teorema? Justifícalo. (c) Para cada instrucción problemática, describe un escenario concreto en el que rompa la equivalencia o el control de recursos. (d) Propón las tres soluciones históricas aplicadas a este caso y di cuál elegirías si el invitado es un sistema propietario del que no tienes el código.

Ejercicio 2: dimensionar y diagnosticar un anfitrión para Meteora

Un anfitrión tiene 16 núcleos físicos, 64 GB de RAM y un NVMe. Hay que alojar: meteo-01 (producción: 8 vCPU, 24 GB), meteo-01-pruebas (4 vCPU, 8 GB), un servidor de integración continua (4 vCPU, 8 GB, a ráfagas), un servidor de métricas (2 vCPU, 4 GB) y dos VM de laboratorio (2 vCPU, 4 GB cada una).

(a) Calcula la ratio de sobresuscripción de CPU y de memoria, y di si te parecen aceptables, justificándolo. (b) Dentro de meteo-01 observas %Cpu(s): 9,1 us, 2,0 sy, 71,0 id, 0,6 wa, 17,3 st: interpreta el diagnóstico y di qué tres acciones concretas tomarías, en orden. (c) ¿Qué configuración de disco y de red darías a meteo-01 frente a las VM de laboratorio, y por qué? (d) ¿Activarías KSM? Razona la respuesta considerando el ahorro esperable y el riesgo.

Ejercicio 3: decidir la estrategia de E/S y de migración

Meteora quiere que la réplica de meteo-01 pueda migrarse en vivo entre dos anfitriones para poder actualizar el hardware sin parar el servicio. El ingestor recibe unas 8.000 lecturas por segundo (24 bytes cada una) y meteo-api sirve tráfico HTTPS.

(a) Elige la estrategia de E/S de red y justifícala frente a las otras dos, considerando el requisito de migración. (b) Estima el tiempo de la fase de precopia si la VM tiene 24 GB de RAM y la red de migración es de 10 Gbit/s, y explica de qué depende que la migración converja. (c) Enumera los requisitos que deben cumplir los dos anfitriones para que la migración sea posible. (d) Describe qué pasaría si además se hubiera asignado una VF de SR-IOV a la VM, y cómo lo resolverías.

Soluciones

Solución 1

(a) Clasificación:

Instrucción Clasificación
LDTAB Privilegiada y sensible a la configuración (cambia el estado del sistema). Correcta: atrapa.
RDTAB Sensible al comportamiento, no privilegiada. Problemática.
SETINT Privilegiada y sensible a la configuración. Correcta.
RDMODE Sensible al comportamiento, no privilegiada. Problemática.
ADDW Inocua.
IOWR Privilegiada y sensible (control de recursos). Correcta.

(b) No cumple el teorema. El conjunto de instrucciones sensibles {LDTAB, RDTAB, SETINT, RDMODE, IOWR} no está contenido en el de privilegiadas {LDTAB, SETINT, IOWR}: RDTAB y RDMODE se salen. Con trap-and-emulate puro, esas dos se ejecutarían sin que el hipervisor se enterara.

(c) Escenarios concretos:

  • RDTAB: el núcleo invitado carga su tabla de páginas con LDTAB (que atrapa y el hipervisor emula, apuntando en realidad a la tabla sombra). Después la lee con RDTAB para comprobarla y obtiene la dirección de la tabla real del anfitrión, no la que él escribió. Rompe la equivalencia (el comportamiento difiere del hardware real) y además filtra información del anfitrión, debilitando el control de recursos.
  • RDMODE: el núcleo invitado se cree en el anillo 0 y comprueba su nivel con RDMODE; obtiene, por ejemplo, "anillo 1". Un núcleo que verifica su propio privilegio antes de hacer algo crítico entrará en una rama de error o entrará en pánico. Rompe la equivalencia y, además, permite al invitado detectar que está virtualizado, cosa relevante en análisis de malware.

(d) Las tres soluciones aplicadas:

  • Traducción binaria dinámica: se escanea el código de núcleo del invitado y se sustituyen RDTAB y RDMODE por saltos al hipervisor, que devuelve los valores virtuales que el invitado espera. Funciona sin tocar el invitado ni el hardware, con coste de complejidad y de rendimiento.
  • Paravirtualización: se modifica el núcleo invitado para que no use RDTAB ni RDMODE, sino hipercalls equivalentes. Rendimiento óptimo, pero exige el código fuente del invitado.
  • Asistencia por hardware: se añade un modo no raíz en el que RDTAB y RDMODE provocan salida al hipervisor. Es la solución limpia, pero requiere una revisión de la arquitectura.

Elección con un invitado propietario sin código fuente: traducción binaria dinámica, que es la única de las dos primeras que no exige modificarlo. Si se puede rediseñar el silicio, la asistencia por hardware es superior en todo (más simple, más rápida y más segura), y es exactamente el camino que siguió x86 entre 1999 y 2006.

Solución 2

(a) Ratios.

  • CPU: 8 + 4 + 4 + 2 + 2 + 2 = 22 vCPU sobre 16 núcleos → 1,375:1. Perfectamente razonable: está dentro del rango conservador, y varias de esas cargas (CI, laboratorio) son intermitentes.
  • Memoria: 24 + 8 + 8 + 4 + 4 + 4 = 52 GB comprometidos sobre 64 GB. No hay sobrecompromiso, pero el margen es de 12 GB, del cual hay que descontar el propio anfitrión (2-4 GB) y las estructuras de QEMU (unos 200-500 MB por VM, es decir 1,2-3 GB). El margen real queda en torno a 5-8 GB: es ajustado pero viable; no admite añadir más VM sin sobrecomprometer.

(b) Interpretación del top. st 17,3 con us 9,1 y id 71,0 es un diagnóstico de libro: el invitado no está saturado, pero no recibe la CPU que cree tener. El wa 0,6 descarta que sea disco. Por tanto el problema está en el anfitrión, no en meteo-01: hay contención de CPU por otras VM. Acciones, en orden:

  1. Mirar el anfitrión, no el invitado: vmstat 1, virsh domstats --cpu-total de todas las VM, para identificar quién está consumiendo. Lo más probable es que sea el servidor de CI, que trabaja a ráfagas y satura.
  2. Contener al culpable limitando su CPU: virsh schedinfo ci --set cpu_quota=... o, mejor, CPUQuota vía cgroups (06-02). El objetivo es que la carga elástica no dañe a la crítica.
  3. Priorizar y fijar producción: subir el peso de CPU de meteo-01 (cpu_shares) y considerar pinning de sus 8 vCPU a núcleos concretos, reservando el resto para las demás, para eliminar la contención de forma estructural en lugar de reactiva.

(c) Configuración diferenciada.

meteo-01 (producción) VM de laboratorio
Disco RAW o QCOW2 con cache=none, io=native, bus=virtio, discos separados sistema/datos QCOW2 con clonado enlazado, cache=writeback
Red virtio + vhost-net en puente, para recibir de las estaciones NAT o red interna aislada
Instantáneas Solo ventanas cortas; copias 3-2-1 reales Libres, es su utilidad principal

La justificación es la integridad frente a la velocidad: en producción, cache=none garantiza que fsync() llegue al hardware y que el journaling de ext4 cumpla su promesa; en laboratorio, la pérdida de datos ante un corte es irrelevante y el clonado enlazado ahorra espacio y tiempo.

(d) KSM: no, o con matices. El ahorro esperable es alto porque las seis VM comparten distribución (Debian estable) y por tanto muchas páginas de binarios idénticas: cabría esperar entre un 20 % y un 40 % de reducción del consumo, que aquí sería valioso dado el margen ajustado. Pero: el anfitrión no está sobrecomprometido, así que el beneficio no es necesario ahora mismo; ksmd consume CPU precisamente en una máquina que ya muestra contención de CPU; y KSM abre un canal encubierto entre VM. Decisión: no activarlo por defecto. Si en el futuro hiciera falta sobrecomprometer memoria, se activaría con pages_to_scan bajo y solo entre VM del mismo nivel de confianza, nunca si hubiera cargas de terceros.

Solución 3

(a) Estrategia de E/S de red: virtio-net con vhost-net.

  • Frente al dispositivo emulado (e1000): con 8.000 lecturas/s más el tráfico HTTPS, la emulación provocaría del orden de decenas de miles de VM exits por segundo, con un coste de ~1.200 ciclos cada uno; se desperdiciaría una fracción notable de un núcleo solo en salidas. Descartado por rendimiento.
  • Frente a SR-IOV / passthrough: daría el mejor rendimiento y la latencia más baja, pero impide la migración en vivo, que es el requisito explícito del enunciado. Descartado por requisito funcional.
  • virtio + vhost-net deja la sobrecarga en el 5-15 % con migración plenamente soportada. Y el volumen no es exigente: 8.000 × 24 B = 192 KB/s de datos útiles, ridículo para cualquier enlace moderno; lo que importa aquí es la tasa de paquetes y de interrupciones, que es justo lo que virtio amortiza al agrupar en la virtqueue.

(b) Tiempo de precopia y convergencia. 24 GB = 192 Gbit. A 10 Gbit/s teóricos, con un aprovechamiento realista del 80 % (8 Gbit/s), la ronda 0 tarda unos 24 segundos. Las rondas siguientes copian solo las páginas ensuciadas durante la anterior.

La convergencia depende de comparar dos velocidades: la tasa de ensuciado (páginas modificadas por segundo × 4 KB) frente al ancho de banda de migración. Si meteo-01 ensucia 200 MB/s y la red transfiere 1 GB/s, cada ronda es cinco veces menor que la anterior y en 4-5 rondas se baja del umbral: converge y la parada final es de decenas de milisegundos. Si meteo-01 estuviera reescribiendo continuamente una caché grande en /dev/shm/meteora-cache a más velocidad que la red, no convergería nunca. En ese caso: aumentar el ancho de banda de migración, estrangular la CPU del invitado (auto-converge), activar compresión, o pasar a poscopia asumiendo su riesgo.

(c) Requisitos de los dos anfitriones:

  1. CPU compatible: mismo fabricante y modelo presentado al invitado. Con --cpu host-passthrough la migración solo funciona entre CPU idénticas; para migrar de verdad hay que usar un modelo de CPU común que enmascare CPUID al mínimo común denominador.
  2. Almacenamiento compartido (SAN, NFS, Ceph) accesible por ambos con la misma ruta; si no, hay que añadir migración de disco, que multiplica el tiempo.
  3. Red común entre ambos anfitriones, idealmente una red de migración dedicada para no competir con el tráfico de servicio, y el puente br0 definido con el mismo nombre en los dos.
  4. Versiones compatibles de QEMU y libvirt (se migra hacia adelante, no hacia atrás), y comunicación autenticada entre los libvirtd.
  5. Sin dispositivos asignados directamente y sin recursos locales del anfitrión (ISO montadas desde rutas locales, por ejemplo).

(d) Con una VF de SR-IOV asignada, la migración en vivo no es posible directamente, porque el estado del dispositivo físico está dentro de la tarjeta y no es transferible, y porque el invitado tiene cargado un controlador específico de ese hardware. Soluciones, de más simple a más compleja:

  • No usar SR-IOV y quedarse en virtio: es la decisión correcta aquí, porque el volumen de Meteora no lo exige.
  • Vinculación (bonding) de la VF con una virtio-net dentro del invitado: antes de migrar se desengancha la VF, el tráfico sigue por virtio durante la migración, y en el destino se vuelve a enganchar una VF. Funciona, pero añade complejidad de configuración.
  • Aceptar una migración con parada (apagar, mover, arrancar), lo que incumple el requisito.

Conclusión

Virtualizar es darle a un sistema operativo entero la ilusión de una máquina propia, y la diferencia con lo que hace un sistema operativo es que el hipervisor no ofrece abstracciones nuevas, sino más copias de la misma máquina. Nació por dinero —utilizaciones del 5-15 % en centros de datos donde había que dedicar un servidor por servicio para aislarlos— y resolvió a la vez la consolidación, el aislamiento y la flexibilidad, con un efecto colateral que resultó decisivo: la máquina pasó a ser un fichero, y con ello llegaron las instantáneas, las plantillas, el clonado y la migración.

El marco teórico son los criterios de Popek y Goldberg —equivalencia, control de recursos y eficiencia, que es lo que separa la virtualización de la emulación— y su teorema: hace falta que las instrucciones sensibles sean un subconjunto de las privilegiadas para poder aplicar trap-and-emulate. x86 fallaba con 17 instrucciones sensibles no privilegiadas, de las que POPF es el ejemplo canónico: en modo usuario ignora el bit de interrupciones sin lanzar excepción. Las tres respuestas fueron la traducción binaria dinámica de VMware (sin tocar hardware ni invitado, con coste de complejidad), la paravirtualización de Xen (hipercalls, rendimiento excelente, pero hay que modificar el invitado; hoy sobrevive en la E/S) y la asistencia por hardware de VT-x y AMD-V, que añadió el modo raíz y no raíz —el "anillo -1"— con la VMCS guardando el estado y el VM exit como unidad de coste, unos 1.000-1.500 ciclos que todo el diseño posterior se ha dedicado a evitar.

La clasificación en tipo 1 y tipo 2 sigue siendo útil, pero KVM la difumina: al ser un módulo del núcleo Linux, convierte a Linux en hipervisor reutilizando su planificador, su gestor de memoria y sus controladores, de modo que una VM es un proceso y cada vCPU es un hilo —con todo lo que eso implica: nice, cgroups, taskset y hasta el OOM killer se le aplican—. Sobre los tres recursos: en CPU, dos niveles de planificación superpuestos, sobresuscripción razonable de 2:1 a 4:1 y el st de top como el indicador que dice "el problema no está aquí dentro"; en memoria, la doble traducción resuelta históricamente con tablas sombra y hoy con EPT/NPT, que elimina VM exits a cambio de hasta 24 accesos por fallo de TLB —de ahí la importancia de las huge pages—, más ballooning, KSM con su canal encubierto y un sobrecompromiso mucho más peligroso que el de CPU; y en E/S, la escalera de emulación, virtio (por defecto, porque amortiza salidas agrupando en la virtqueue) y passthrough/SR-IOV con IOMMU, que da el máximo rendimiento a cambio de perder la migración.

Alrededor, las piezas prácticas: redes en puente, NAT o aisladas —con la advertencia de que nftables del anfitrión no ve el tráfico entre VM del mismo puente—, discos con aprovisionamiento fino y su riesgo de llenado simultáneo, instantáneas que degradan el rendimiento y no son copias de seguridad, clonado enlazado que anticipa exactamente el modelo de capas de los contenedores, y la migración en vivo por precopia iterativa, cuyo reto es la memoria y cuyo enemigo es un conjunto de trabajo que se ensucia más rápido de lo que la red copia. En KVM y libvirt, todo eso se maneja con virsh y virt-install, donde las decisiones que de verdad importan son bus=virtio, cache=none por integridad y discard=unmap por espacio.

Y la conclusión de seguridad, que es la que enlaza con lo que viene: la superficie del hipervisor es estrecha pero real —VENOM estaba en el controlador de disquete que nadie usaba—, se endurece con las mismas armas del módulo 5 aplicadas a QEMU (usuario sin privilegios, sVirt, seccomp, IOMMU), y aísla más que un contenedor por una razón cuantificable: la interfaz que un invitado hostil puede atacar son unos pocos manejadores de VM exit, frente a las 300-400 llamadas al sistema que expone un núcleo compartido.

Ahora bien: todo esto tiene un precio que no es el porcentaje de CPU, sino el peso. Cada máquina virtual carga con un núcleo entero, un init, un sistema de ficheros completo y 200-500 MB de RAM solo para existir; arranca en decenas de segundos; y su imagen ocupa gigabytes. Si lo que quieres no es ejecutar otro sistema operativo, sino simplemente empaquetar tu aplicación con sus dependencias y limitar lo que ve y lo que consume, estás pagando por un núcleo que no necesitas.

¿Y si en lugar de duplicar el núcleo, aprovecháramos que el núcleo que ya tienes sabe mentirle a un proceso sobre qué ficheros existen, qué procesos hay, qué red ve y cuánta memoria puede usar? Eso no requiere hipervisor: requiere espacios de nombres y grupos de control, dos mecanismos que ya están dentro de Linux y que llevamos citando desde el módulo 3. Es la lección siguiente: Contenedores: Namespaces y cgroups.

Fundamentos de Sistemas Operativos

Módulo 1: Introducción a los Sistemas Operativos

Módulo 2: Gestión de Recursos

Módulo 3: Concurrencia

Módulo 4: Estructuras de Archivos

Módulo 5: Protección y Seguridad del Sistema

Módulo 6: Virtualización y Contenedores

Módulo 7: Administración y Diagnóstico en la Práctica

© Copyright 2026. Todos los derechos reservados