Escritorio virtual de Windows 10 (y II)

Photo by XPS on Unsplash

Escritorio virtual de Windows 10 (y II)

Vamos a ver algunos consejos para optimizar la máquina virtual creada en el artículo anterior. Este artículo pertenece a una serie donde se explica como instalar Windows 10 en una máquina virtual asignándole una de las GPU del equipo de forma exclusiva, para obtener un rendimiento gráfico similar al que tendría en una máquina real. Si te has perdido algún artículo anterior de esta historia, el primero lo tienes aquí.


Una vez apagada la máquina virtual, si seleccionamos en el menú el icono con una exclamación —con el texto de ayuda Mostrar detalles del hardware virtual— podemos ver la configuración definitiva y ajustarla.

Hardware de la máquina virtual tras la instalación

Podemos quitar las unidades CDROM o, en todo caso, dejar solo una por lo que pueda surgir. También se puede quitar el dispositivo tipo Serial y añadir uno de tipo Tableta. La tableta nos permite evitar problemas con el posicionamiento del puntero y usar el ratón con la pantalla de la máquina virtual sin antes tener que hacer clic.

Si hemos dejado una unidad CDROM, podemos cambiar su Bus de disco a SCSI. Eso nos permite eliminar también los dispositivos Controller IDE.

Rendimiento del almacenamiento

En las Opciones de Rendimiento del SCSI Disco se puede indicar el Modo caché como none y el Modo E/S como native.

El primero evita que el anfitrión consuma memora cacheando las operaciones de E/S de la máquina virtual. Esto también lo hace el sistema operativo de la máquina virtual, por lo que no tiene sentido hacerlo dos veces. Quizás fuera útil si pensáramos lanzar varias máquinas virtuales que accedan al mismo tiempo a los mismos dispositivos de almacenamiento.

El segundo activa el uso de llamadas al sistema nativas de Linux para la E/S asíncrona al archivo con el contenido del disco duro virtual. El valor por defecto usa hilos y operaciones síncronas.

La combinación de estas dos opciones debería ofrecernos mejor rendimiento que las opciones por defecto.

Dentro de la máquina virtual, en el intérprete de comandos de Windows, es interesante ejecutar este comando:

fsutil behavior query DisableDeleteNotify

Si devuelve 0, nos indica que cuando un bloque del almacenamiento deja de usarse, Windows envía al dispositivo el comando TRIM para indicarle que ha quedado libre.

Si estamos utilizando una imagen de disco en formato raw, esto no tendrá ningún efecto. Pero con qcow2 el archivo reducirá su tamaño, al necesitar menos espacio para almacenar la imagen. Si empleamos un volumen lógico LVM sobre un dispositivo SSD, como es mi caso, el comando TRIM se propagará hasta el SSD, evitando la degradación del rendimiento del dispositivo.

Si hiciera falta, para activarlo hay que ejecutar:

fsutil behavior set DisableDeleteNotify 0

Rendimiento de la interfaz de red

La tarjeta de red de la máquina virtual es el dispositivo con el nombre NIC <MAC>. Por lo general se trata de una emulación de una tarjeta Intel e1000 o una Realtek RTL8139. Se puede obtener mejor rendimiento cambiando el Modelo de dispositivo a virtio, que es una interfaz de red paravirtualizada, como la que usamos con el disco duro virtual. Lo único es que es necesario iniciar la máquina virtual e instalar los controladores de la carpeta NetKVM de la ISO de controladores VirtIO.

Por defecto la interfaz de red de la máquina virtual está conectada a una red virtual llamada "default", en la que también está el equipo anfitrión. En esta red la máquina virtual recibe una IP privada y su acceso a Internet se realiza a través del anfitrión utilizando NAT.

Todo esto se puede simplificar mucho cambiando Fuente de red a Dispositivo anfitrión <interfaz>: macvtap y Modo de fuente a Puente. Usando macvtap la máquina virtual pasa a algo así como a compartir la la tarjeta de red "<interfaz>" del equipo anfitrión. La red le asignará a la máquina virtual una IP como si fuera una máquina real y de esta forma puede conectarse al resto de equipos de la red y a Internet sin usar al anfitrión como intermediario.

En mi caso, al utilizar macvtap en Ubuntu 16.04, la interfaz de red funcionaba en el primer arranque pero nunca en posteriores. Se debía a un conflicto con NetworkManager, que decidía por su cuenta bajar la interfaz macvtap anfitrión. Se resuelve editando el archivo /etc/NetworkManager/NetworkManager.conf para añadir lo siguiente:

[keyfile]  
unmanaged-devices=interface-name:macvtap0

para que NetworkManager no intente gestionar esa interfaz. En Ubuntu 18.04, por el momento no he tenido ningún problema similar, de modo que no parece que haga falta este truco.

Vídeo QXL

En el dispositivo Video Cirrus se puede cambiar el modelo a QXL. El modelo Video Cirrus emula una tarjeta gráfica VGA del fabricante Cirrus Logic. Mientras que QXL es un dispositivo de vídeo paravirtualizado, por lo que ofrece mejor rendimiento. Además, mientras que con la emulación VGA estamos limitados a una resolución 800x600, con QXL tenemos automáticamente 1024x768. Será necesario iniciar la máquina virtual e instalar los controladores de la carpeta qxldod de la ISO de controladores VirtIO.

Asignación de memoria RAM

En la configuración de memoria podemos configurar cuánta memoria pensará la máquina virtual que tiene —la Asignación máxima— y cuanta memoria del anfitrión puede realmente usar como máximo —la Asignación actual—.

Por ejemplo, mi máquina virtual supone que tiene 8GB de RAM, pero el sistema anfitrión nunca le asignará más de 4 GB de memoria.

Configuración de la asignación de memoria

Desde el punto de vista del sistema operativo, la máquina virtual es un proceso más. Cuando se inicia, pide al sistema operativo la cantidad de memoria indicada en Asignación máxima como memoria RAM para la máquina. El sistema operativo le reserva el espacio de direcciones pero realmente solo le asigna la memoria según va a accediendo a ella. Es decir, que la memoria se asigna a la máquina virtual a demanda.

Si miramos el hardware detectado por nuestro sistema operativo, seguramente observaremos un dispositivo desconocido que se corresponde con el dispositivo balón de memoria —en inglés, balloon device—. Para que funcione adecuadamente es necesario instalar en la máquina virtual el controlador balloon de la ISO de controladores VirtIO.

Con el controlador del balón de memoria instalado, cuando el sistema operativo arranca, dicho controlador reserva una porción de la memoria de la máquina para que nunca pueda usarse. ¿Cuánta memoria reserva?. La necesaria para que el sistema nunca pueda utilizar más memoria que la cantidad indicada en Asignación actual.

Es decir, que la máquina virtual piensa que la memoria instalada es Asignación máxima, pero nunca podrá emplear más de Asignación actual de la memoria del sistema anfitrión.

La cantidad indicada en "Asignación actual" se puede cambiar en tiempo de ejecución desde el anfitrión mediante el comando virsh:

sudo virsh setmem <máquina_virtual> 2G --live

Si el nuevo valor es mayor, el balón se encogerá para que el sistema de la máquina virtual pueda consumir más memoria. Obviamente, nunca podremos asignar un valor superior a Asignación actual.

Por el contrario, si el nuevo valor es menor, el balón crecerá y devolverá la memoria adicional que reserve al sistema anfitrión, haciendo que haya menos memoria para la máquina virtual.

Si no nos interesa este comportamiento, basta con que no instalemos el controlador del balón de memoria. O que pongamos en Asignación máxima y en Asignación actual la misma cantidad. Sin embargo, hay que tener presente que durante el arranque, Windows pone a ceros toda la memoria de la máquina, haciendo que el sistema anfitrión tenga que asignarle efectivamente la memoria indicada en "Asignación máxima". El controlador del balón de memoria nos permite obligar después a Windows a devolver buena parte de la memoria, hasta lo indicado en Asignación actual.

Espacio de intercambio en la máquina virtual

En algunos foros se sugiere desactivar el espacio de intercambio —o swap—  en el sistema operativo de la máquina virtual. La justificación es que el sistema anfitrión ya tiene su espacio de intercambio donde pueda intercambiar cualquier porción de la memoria, incluida la asignada a las máquinas virtuales.

Pero lo cierto es que nadie mejor que el sistema operativo de la máquina virtual para saber qué partes intercambiar primero, por lo que es mejor que tenga su propio espacio de intercambio.

Al reducir la cantidad de memoria disponible con el balloon device, obligamos al sistema operativo de la máquina virtual a decidir qué partes de la memoria son menos importantes respecto al rendimiento, para liberarlas o intercambiarlas primero.

Kernal SamePage Merging (KSM)

En algunos foros se recomienda activar KSM en el sistema Linux anfitrión. KSM es una tecnología creada originalmente para intentar maximizar el número de máquinas virtuales en un mismo equipo. Básicamente, consiste en un hilo del núcleo que recorre la memoria buscando regiones con el mismo contenido. Cuando las encuentra, ahorra memoria liberando los duplicados y quedándose solo con una copia.

Por tanto, parece una tecnología interesante para evitar que las máquinas virtuales con Windows acaparen demasiada memoria cuando la llenan de ceros durante el arranque. De hecho Red Hat hizo un experimento donde pudo lanzar hasta 52 Windows con 1 GB en un servidor con solamente 16 GB.

Sin embargo, hay que tener presente que recorrer la memoria de esta manera no es gratis. KSM sacrifica tiempo de CPU para maximizar el número de máquinas virtuales. Por eso yo no he activado y probado KSM, hasta el momento.

Afinidad de los procesadores

Las CPU de la máquina virtual se implementan como hilos de ejecución en el anfitrión. Para mejorar el rendimiento se puede vincular cada uno de esos hilos a una CPU real, evitando que migren de CPU a criterio del sistema, lo que dificulta aprovechar adecuadamente las memorias caché.

Lamentablemente, para configurar esta funcionalidad no podemos usar la interfaz gráfica de usuario de virt-manager. Tenemos que ejecutar:

sudo virsh edit <máquina_virtual>

desde la línea de comandos y modificar el XML —que describe la máquina virtual—  a mano para añadir antes de la etiqueta <os> lo siguiente:

<cputune>  
  <vcpupin vcpu='0' cpuset='0'/>  
  <vcpupin vcpu='1' cpuset='1'/>  
  <vcpupin vcpu='2' cpuset='2'/>  
  <vcpupin vcpu='3' cpuset='3'/>  
</cputune>

ADVERTENCIA: Ojo con olvidar el comando sudo. Si nos olvidamos de sudo al ejecutar virsh, el comando puede fallar con el error error: Domain not found por no encontrar la máquina virtual o acabaremos editando una máquina virtual diferente a la que hemos configurado en virt-manager.

En mi sistema las CPU de la 0 a la 3 corresponde con los núcleos del primero al cuarto. Mientras que las CPU de la 4 a la 7 son hilos de esos mismos núcleos. Como hemos configurado la máquina virtual con 4 núcleos de 1 hilo, vinculamos cada uno de esos 4 núcleos virtuales con uno de los 4 núcleos de la CPU real.

La topología de las CPU reales de nuestro sistema se puede conocer mirando /proc/cpuinfo en el sistema anfitrión.

Huge Pages

Además, se puede indicar que el proceso de la máquina virtual use páginas de memoria de gran tamaño o huge pages. El tamaño de página típico son 4 KB, pero algunas CPU permiten tamaños superiores —en la familia x86 se admiten páginas de 4 kB, 2 MB y 1 GB, según el modelo de CPU— lo que evita consultar algunos niveles de más en la tabla de página, durante la traducción de las direcciones virtuales y permite consumir menos entradas de la TLB.

Para activar su uso, primero hay que editar el XML de la máquina virtual:

sudo virsh edit <máquina_virtual>

y añadir lo siguiente antes de la etiqueta <os>:

<memoryBacking>  
  <hugepages/>  
</memoryBacking>

Las huge pages pueden no estar disponibles cuando se necesitan debido a la fragmentación de la memoria. Por eso, si se quieren emplear, hay que configurar el sistema para reservar la cantidad necesaria durante el arranque:

echo "vm.nr_hugepages=2048" | sudo tee /etc/sysctl.d/hugepages.conf

Donde 2048 páginas de 2 MB son 4096 MB, suficiente para una máquina virtual de 4 GB. Sin embargo, hay que tener presente que la memoria reservada no puede ser usada con otro propósito ni puede ser intercambiada. Como mi sistema solo tiene 16 GB, yo he optado por no activar el uso de huge pages. Obviamente, tendría sentido activarlo si dispusiera de mucha más memoria.

Asignar a la máquina virtual la GPU

Llegados a este punto, antes de continuar, lo primero que deberíamos hacer es crear un punto de restauración en Windows. Así, si las cosas se ponen muy mal, no tendremos que volver a empezar desde cero.

Volvemos a la configuración de la máquina virtual, hacemos clic en + Agregar hardware y seleccionamos Dispositivo PCI anfitrión. Hay que escoger la tarjeta gráfica que queremos asignar. En mi caso es el dispositivo que dice 0000:01:00:0 NVIDIA Corporation GM206 [Geforce GTX 950].

Añadir la tarjeta gráfica como dispositivo PCI del anfitrión

Lo mismo se hace con el dispositivo justo debajo —el 0000:01:00:1— que es la salida de audio digital de la tarjeta gráfica a través del conector HDMI. A fin de cuentas, ambos dispositivos están en la misma tarjeta y no es conveniente asignar uno a la máquina virtual y el otro no.

Yo también hice lo mismo con el dispositivo 0000:05:00:0 ASMedia Technology Inc. ASM1042 SuperSpeed USB Host Controller para tener puertos USB donde conectar directamente un teclado, un ratón, un gamepad o un pendrive; si hiciera falta.

En principio para una tarjeta gráfica AMD esto sería todo. Al iniciar la máquina virtual ya no debería inicializarse en el monitor virtual en virt-manager. En su lugar debemos ver el arranque en el monitor físico conectado a la tarjeta gráfica. Una vez haya arrancado Windows, vamos a la web de fabricante de la GPU, bajamos los controladores para el dispositivo y los instalamos. Es preferible seleccionar el controlador adecuado nosotros mismos antes que utilizar la autodetección, ya que esto último puede dar problemas.

Sin embargo, con Windows 10 algunos usuarios informan de que pueden ser necesarios algunos trucos adicionales. En mi caso intenté una actualización de Windows 8.1 a Windows 10 que no se completaba, si no editaba el XML de la máquina virtual

sudo virsh edit <máquina_virtual>

para eliminar toda la etiqueta <hyperv> en <features> y añadir lo siguiente:

<kvm>  
  <hidden state='on'/>  
</kvm>

De hecho mi etiqueta <features> es así:

<features>  
  <acpi/>  
  <apic/>  
  <pae/>  
  <kvm>  
    <hidden state='on'/>  
  </kvm>  
</features>

Cuando se tiene una GPU de NVIDIA hace falta cambiar lo mismo, independientemente de la versión de Windows, para que funcione. También se recomienda buscar la etiqueta <timer name=’hypervclock’ ... > dentro de <clock> y dejarla de esta manera...

<timer name='hypervclock' present='no'>

...o borrarla.

Con estos cambios el arranque debe ocurrir como hemos descrito anteriormente. Después solo tendremos que buscar el controlador adecuado e instalarlo.

Activar el uso de Message Signaled Interrupts (MSI)

MSI es una alternativa al mecanismo tradicional de señalar las interrupciones mediante líneas dedicadas, que puede proporcionar una pequeña mejora de rendimiento.

Para activar su uso con nuestra tarjeta gráfica, básicamente tenemos que:

  1. Comprobar en el Administrador de dispositivos que la tarjeta soporta MSI. Lo más probable es que sí.

  2. Ejecutar regedit en Windows, localizar la clave de registro del dispositivo y añadir allí la configuración que activa el uso de MSI para el dispositivo.

Todos los detalles del proceso están perfectamente descritos en el primer mensaje de este hilo en un foro.

Detalles finales

Si todo ha ido bien, ya tenemos una máquina virtual con una GPU real. Como ya no estamos usando el visor de virt-manager, quizás haga falta conectar temporalmente un teclado y un ratón a los puertos USB reales que le hemos asignado.

Teclado y ratón compartido

Eso de tener dos teclados no es muy práctico a largo plazo, por lo que utilizo Synergy para compartir el teclado y el ratón del anfitrión con la máquina virtual. También permite compartir el portapapeles entre ambos sistemas.

En conf/synergy.conf dentro de mí repositorio hay un ejemplo de mi configuración de Synergy. Synergy hace que al llegar al borde derecho de mi monitor en el sistema anfitrión, este aparezca por la izquierda en el monitor de la máquina virtual.

Este comportamiento a veces da problemas con juegos o con herramientas de edición 3D. Para esos casos he configurado la combinación de teclas Alt+W para que al pulsarla se confine el ratón al escritorio del sistema en el que esté en ese momento. Así el ratón y la entrada de teclado no puede cambiar de un sistema a otro por error.

Synergy usa la red para conectar servidor y cliente, pero al usar macvtap, anfitrión y máquina virtual no pueden. Por eso he añadido a la máquina virtual una nueva red virtual privada que solo la conecta con el anfitrión. Es a través de esa red aislada por la que conecta el cliente con el servidor de Synergy.

Monitor compartido

En mi caso no tengo un monitor para cada sistema. Tengo dos para ambos. La gráfica integrada se conecta a ellos mediante salidas DVI, mientras que la gráfica asignada a la máquina virtual utiliza las salidas HDMI. Cambiando la entrada que me interesa en cada monitor, puedo elegir si quiero ver el escritorio de la máquina virtual o del anfitrión.

Tengo en mente un proyecto para poder hacer eso empleando combinaciones de teclas, evitando usar los incómodos controles de los monitores. Sin embargo, ya veremos si algún día me pongo a ello 😉.

Sonido

El sonido es seguramente el aspecto con el que menos satisfecho estoy. Después de probar muchas alternativas, creo que las mejores opciones son:

  • Conectar directamente una tarjeta de sonido o unos auriculares en un puerto USB asignado a la máquina virtual por PCI Passthrough. Sin duda es así como se consigue la mejor calidad de sonido. También sirve la salida de audio HDMI de la tarjeta gráfica asignada. Se puede escuchar por los altavoces del monitor —si los tiene— o conectar algunos por la salida de audio que los monitores suelen traer. Si no queremos tener dos parejas de altavoces, una para cada sistema, ni estar conectando y desconectado cables según nos interese, podemos comprar un mezclador barato para combinar ambas salidas de audio y tener solo unos altavoces conectados.

  • Usar una tarjeta de sonido virtual de red, como la del proyecto duncanthrax/scream. La idea de la tarjeta virtual de red es instalar en Windows un controlador de dispositivo de tarjeta de sonido que realmente envíe el audio por red a otro equipo para su reproducción. En ese sentido, el proyecto scream proporciona tanto los controladores para Windows como el servidor para Linux. El resultado es bastante bueno, prácticamente sin distorsiones ni latencia.

  • Añadir un dispositivo de sonido emulado a la máquina virtual y que QEMU haga el resto. Esta es la solución típica. Necesita muchos ajustes y al final el resultado no es muy satisfactorio. Pero voy a explicar como lo he configurado yo.

Primero hay que añadir un dispositivo de sonido a la máquina virtual. En mi caso, tras varias pruebas, he dejado el modelo ICH9, que es bastante moderno y funciona perfectamente.

Luego hay que indicar a QEMU como reproducir en el anfitrión el sonido del dispositivo emulado. Simplemente, editamos el XML de la máquina virtual:

sudo virsh edit <máquina_virtual>

y añadimos lo siguiente al final:

<qemu:commandline>  
  <qemu:env name='QEMU_AUDIO_DRV' value='pa'/>  
  <qemu:env name='QEMU_PA_SERVER' value='127.0.0.1'/>  
  <qemu:env name='QEMU_PA_SAMPLES' value='1024'/>  
</qemu:commandline>

Esto hace que QEMU use el servidor pulseaudio del anfitrión para reproducir el sonido, al igual que el resto de aplicaciones de Linux.

El valor de QEMU_PA_SAMPLES permite controlar el tamaño del buffer de muestras. Con valores muy altos, se consigue una calidad similar a la que se obtiene con Scream, al menos durante los primeros minutos. Sin embargo, el retardo es excesivo, haciendo imposible ver un vídeo. Con 1024 el retardo es inapreciable, pero la calidad del sonido no es tan alta.

Por defecto pulseaudio no acepta conexiones de red por TCP. Para resolverlo hay que editar /etc/pulse/default.pa y descomentar la línea del módulo module-native-protocol-tcp y que quede así:

load-module module-native-protocol-tcp auth-ip-acl=127.0.0.1

Por defecto, si tenemos activado el monitor virtual VNC, Libvirt ignorará lo indicado en QEMU_AUDIO_DRV para redirigir todo el sonido por VNC, tanto si el cliente VNC sabe reproducir el audio como si no es de esta manera. Es necesario editar /etc/libvirt/qemu.conf y asegurarnos que la siguiente línea está descomentada y aparece de esta forma:

vnc_allow_host_audio = 1

para que Libvirt haga siempre lo indicado por QEMU_AUDIO_DRV.

Por último, hay que indicar a Windows que la frecuencia de muestreo por defecto del dispositivo de audio es 44100 Hz, que es la frecuencia de muestreo por defecto en Linux. El valor por defecto en Windows es 48000 Hz. Si la frecuencia de muestreo en Windows y en Linux no coincide, oiremos los sonidos distorsionados. Los pasos son:

Configuración de la frecuencia de muestreo en Windows 10

  1. En la máquina virtual hay que buscar el icono del altavoz en la bandeja del sistema, hacer clic con el botón derecho del ratón y seleccionar Abrir Configuración de sonido.

  2. Hacer clic en Panel de control de sonido.

  3. En Reproducción seleccionar Altavoces (High Definition Audio Device) y luego hacer clic en propiedades.

  4. Hacer clic en la pestaña Opciones avanzadas.

  5. Hacer clic en la lista de selección y escoger 16 bits, 44100 Hz (Calidad de CD).

  6. Aplicar y salir.

Y ya solo queda reproducir algo para ver como suena. En mi caso el sonido va acompañado a ratos de unos clics. Dependiendo de para qué vayamos a usar la máquina virtual, es posible que nos interese probar otra solución de las comentadas.

Evitar el reinicio de Xorg

En el artículo anterior vimos que si cambiamos la tarjeta gráfica usada por nuestro escritorio en Linux mediante el comando prime-select, estamos obligados a salir de la sesión y reiniciar el gestor de pantalla; para posteriormente volver a iniciar sesión en el escritorio y lanzar la máquina virtual, de manera que esta pueda usar la tarjeta gráfica liberada.

Como esto puede resultar bastante tedioso si estamos cambiamos de tarjeta gráfica con frecuencia, podemos optar por ejecutar el escritorio en el sistema Linux siempre en la tarjeta gráfica integrada. De modo que la otra tarjeta gráfica estará disponible, por lo general, para ejecutar la máquina virtual.

En caso de querer aprovechar la tarjeta gráfica no discreta con algún programa del escritorio Linux —por ejemplo, un juego o alguna otra aplicación que se beneficie de utilizar un hardware gráfico más potente, cuando no la está utilizando la máquina virtual— podemos utilizar el comando primusrun:

primusrun <comando>

O, en caso de querer desactivar VSync, lo utilizaríamos así:

vblank_mode=0 primusrun <comando>

También se puede usar nvidia-xrun, un script que similar a primusrun, pero que es específico para tarjetas gráficas de NVIDIA y que suele ofrecer mejor rendimiento.

VFIO-Tools

Una alternativa muy interesante a mis scripts es VFIO-Tools. Básicamente, se trata de una solución más modular, que facilita mucho crear scripts de hook personalizados que se ejecuten cuando la máquina virtual se inicia o detiene.

Algunos de los scripts que trae de serie hacen cosas como: configurar las huge pages —por lo que es importante desactivarlo si no nos interesa esta funcionalidad— ajustar la prioridad de los hilos que la máquina virtual usa para simular las CPU virtuales —lo que puede mejorar los tiempos de respuesta— y conmutar la entrada del monitor para que muestre la máquina virtual automáticamente cuando esta se inicia.