# Escritorio virtual de Windows 10 (I)

*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.* *En esta parte veremos como crear la máquina virtual e instalar Windows 10.* *Si te has perdido algún artículo anterior de esta historia, el primero lo tienes* [*aquí*](/iommu-primer-asalto) *y el segundo* [*aquí*](/iommu-la-maldicion-de-la-vga).

---

Como he comentado en artículos anteriores, quiero instalar Windows 10 en una máquina virtual sobre Ubuntu 18.04 LTS. Como estoy interesado en conseguir el máximo rendimiento gráfico en Windows, quiero que la máquina virtual tenga acceso exclusivo a la tarjeta gráfica, dejando para Ubuntu la gráfica integrada de Intel que viene con la placa madre.

Para que esta configuración funcione es necesario:

1. Una CPU y una placa madre con soporte para la tecnología VT-d —o Intel® Virtualization Technology for Directed I/O, que es el nombre que Intel le da a la tecnología de la IOMMU—. En algunos casos hay que activar dicho soporte desde la BIOS / UEFI.
    
2. Una tarjeta gráfica con una ROM que soporte UEFI. La mía es una Geforce GTX 950, pero cualquier tarjeta gráfica de los últimos años sirve.
    

# Instalación del software

Para empezar necesitamos *QEMU/KVM*, *Libvirt*, *OVMF* y *Virtual Machine Manager*:

```sh
sudo apt-get install qemu-kvm ovmf libvirt-clients \
libvirt-daemon-system bridge-utils virt-manager
```

[*Virtual Machine Manager*](https://virt-manager.org/) (o *virt-manager*) será la aplicación que utilizaremos para configurar y lanzar la máquina virtual. Básicamente, se trata de una aplicación de escritorio para gestionar máquinas virtuales a través de [*Libvirt*](https://libvirt.org/), por lo que esta última se instará automáticamente como dependencia del primero.

Para que podamos lanzar máquinas virtuales con *virt-manager* necesitamos que nuestro usuario esté dentro del grupo `libvirt`. Podemos añadirlo así:

```sh
sudo adduser $(id -un) libvirt
```

Obviamente, tendremos que cerrar la sesión y volver a iniciarla para que el cambio tenga efecto.

Nuestra máquina virtual necesita un [firmware UEFI](https://es.wikipedia.org/wiki/Extensible_Firmware_Interface) para arrancar —como el de cualquier placa madre real— por lo que emplearemos el del proyecto [*OVMF*](https://github.com/tianocore/tianocore.github.io/wiki/OVMF) (siglas de Open Virtual Machine Firmware) que es un firmware UEFI especialmente preparado para máquinas virtuales *QEMU/KVM*.

Desde [el repositorio de Gerd Hoffmann](https://www.kraxel.org/repos/jenkins/edk2/) se puede obtener la última versión compilada de *OVMF*. De hecho eso fue lo que hice la primera vez, al configurar una máquina virtual de esta manera en una Ubuntu 16.04 LTS. Sin embargo, actualmente no debería haber ningún problema con la versión de *OVMF* empaquetada con cualquier distribución moderna, así que nos saltaremos ese paso.

# Activar IOMMU

Lo primero es activar el soporte de [IOMMU](/iommu-primer-asalto) en el núcleo. Para eso editamos `/etc/default/grub` y añadimos `intel_iommu=on` a la variable `GRUB_CMDLINE_LINUX_DEFAULT`. Después ejecutamos:

```sh
sudo update-grub
```

para actualizar la configuración del gestor de arranque con la nueva opción.

En algunos casos se ha informado de problemas durante el arranque o al reproducir sonido a través del HDMI de la gráfica integrada. Los efectos pueden ser diversos y diferentes entre sistemas. Por ejemplo, algunos informes hablan específicamente de problemas al usar el audio HDMI en procesadores de la micro-arquitectura Haswell, pero no siempre se pueden conectar estos problemas con un hardware concreto.

El asunto es que en caso de problemas es buena idea probar a desactivar la IOMMU para la gráfica integrada, utilizando la opción `intel_iommu=on,igfx_off`. De hecho mi configuración es:

```sh
GRUB_CMDLINE_LINUX_DEFAULT="intel_iommu=on,igfx_off quiet splash"
```

Además, se sabe que hay una importante pérdida de rendimiento en los procesadores Sandy Bridge al activar IOMMU. En ese caso se recomienda usar la opción `intel_iommu=pt` en lugar de `intel_iommu=on`. En la [documentación de Linux](https://www.kernel.org/doc/Documentation/Intel-IOMMU.txt) hay más información sobre la configuración de IOMMU en Intel.

Una vez actualizada la configuración del gestor de arranque, debemos reiniciar el sistema y comprobar que el soporte de IOMMU está activado examinando los registros del sistema:

```sh
journalctl -b | grep DMAR
```

Vamos por el buen camino si en la salida del comando anterior vemos una línea que dice:

```plaintext
kernel: DMAR: IOMMU enabled
```

Si hemos usado la opción `igfx_off`, además debe haber otra con lo siguiente:

```plaintext
kernel: DMAR: Disable GFX device mapping
```

Si observamos que el soporte de IOMMU no se activa, puede ser que se nos hayamos olvidado activar el soporte de VT-d en la configuración de la BIOS/UEFI de nuestro sistema. O tal vez nuestra CPU o nuestra placa madre no tengan soporte para VT-d.

# Registro de dispositivos en VFIO

Las aplicaciones de virtualización como *QEMU/KVM* necesitan acceso al dispositivo que se quiere mapear. Para hacerlo utilizan las facilidades de [VFIO](https://www.kernel.org/doc/Documentation/vfio.txt), que es un controlador de dispositivo de Linux cuya función es ofrecer a los procesos en modo usuario acceso directo a los dispositivos. Este tipo de acceso es lo que necesita la aplicación de virtualización, pero también puede ser útil para desarrollar controladores de dispositivo que se ejecuten en modo no privilegiado.

VFIO no puede ofrecer a las aplicaciones acceso a cualquier dispositivo del sistema. Solo lo hace a aquellos que han sido registrados previamente en VFIO. Y para que eso sea posible, el dispositivo no puede estar siendo usado por otro controlador.

Pongamos por caso que, como queremos hacer *VGA passthrough*, nos interesa registrar nuestra tarjeta gráfica en VFIO. Lo primero es obtener la lista de dispositivos, ejecutando el comando:

```sh
lspci -nn
```

El resultado será similar al de la siguiente imagen. La línea marcada corresponde con la tarjeta gráfica, instada en mi ordenador, que quiero utilizar con la máquina virtual:

![Ejemplo al ejecutar el comando "lspci -nn"](https://cdn.hashnode.com/res/hashnode/image/upload/v1637025881816/xQ_STZhvXM.png align="center")

El texto `[10de:1002]` del final de la línea marcada nos indica que el fabricante de esa tarjeta gráfica tiene el identificador `0x10DE` y que el modelo se identifica como `0x1402`. Así que para registrar esta tarjeta habría que indicarle al controlador VFIO estos identificadores de la siguiente manera:

```sh
echo 10DE 1402 |sudo tee /sys/bus/pci/drivers/vfio-pci/new_id
```

El problema es que esto hay que hacerlo antes de iniciar la máquina virtual y para cada dispositivo que queremos asignarle. En mi caso no solo es la tarjeta gráfica, también el dispositivo de audio HDMI de la misma tarjeta —justo en la línea siguiente— y un concentrador raíz USB —el ASM1042 SuperSpeed USB Host Controller— para poder conectar directamente un teclado y ratón USB, en caso de que hiciera falta.

Además, antes de registrar estos dispositivos en VFIO, hay que desregistrarlos del controlador de dispositivo que Linux le haya asignado para usarlos en el sistema anfitrión. En el caso particular de las tarjetas gráficas, es incluso mejor descargar el controlador de dispositivo correspondiente, porque no suelen llevar muy bien que les asignen y les quiten dispositivos en caliente durante la ejecución.

Para no hacerlo a mano en cada ocasión, es interesante tener un *hook* de *Libvirt* que lo haga automáticamente cada vez que se inicia la máquina virtual.

# Instalar el libvirt hook

En el repositorio [jesustorresdev/virtual-desktop](https://github.com/jesustorresdev/virtual-desktop) he ido poniendo scripts y archivos de configuración relevantes para este proyecto. Dentro del directorio `libvirt-hooks` está el script `qemu`, que es el *hook* que se encargará del registro en VFIO durante el arranque de la máquina virtual.

Se puede instalar así:

```sh
sudo install -m755 libvirt-hooks/qemu /etc/libvirt/hooks
```

[Libvirt admite 5 scripts de hook](https://libvirt.org/hooks.html#script-names). El script `qemu` se ejecuta cuando libvirt inicia, detiene o migra una máquina virtual de *QEMU*; que es exactamente nuestro caso. Eso significa que si tenemos varias máquinas virtuales de QEMU y queremos hacer cosas diferentes para cada una, debemos ponerlo todo en el mismo script `/etc/libvirt/hooks/qemu`.

Del script hay dos funciones a las que merece la pena echar un vistazo. Una es `vfio_bind_devices()` que, dado el identificador de un dispositivo, lo registra en VFIO para hacer *PCI passthrough*.

```python
def vfio_bind_devices(device_ids):
    """Bind the specified devices to the Linux VFIO driver
    :param device_ids: List of addresses of devices to bind to VFIO.
    :return: Dictionary with information about the device bound.
    """

    vfio_loaded = False
    nvidia_loaded = True

    devices = {}
    for id in device_ids:
        device_path = '/sys/bus/pci/devices/%s' % id
        try:
            device_driver = os.path.basename(os.readlink(device_path + '/driver'))
        except OSError:
            device_driver = None

        # Ignore devices already bound to VFIO driver because the system
        # crashes sometimes after a few bind/unbind cycles
        if device_driver != 'vfio-pci':
            device_info = {
                'path': device_path,
                'driver': device_driver
            }
            with open(device_path + '/vendor', 'r') as f:
                device_info['vendor'] = f.read()
            with open(device_path + '/device', 'r') as f:
                device_info['model'] = f.read()
            devices[id] = device_info
        else:
            vfio_loaded = True

    # Load vfio-pci module, if needed
    if not vfio_loaded and devices:
        subprocess.check_call(['modprobe', 'vfio-pci'])

    for id, device_info in devices.iteritems():
        # Unbind the device if it is bound to other driver
        if device_info['driver'] is not None:

            # Unload the NVIDIA driver instead of unbind the device
            if device_info['driver'] == 'nvidia':

                # Hotplug support of graphics card isn't good. Further, I 
                # guess that question 9 applies here:
                # http://vfio.blogspot.com.es/2014/08/vfiovga-faq.html
                # The driver locks the VGA arbiter, freezing the VM on its 
                # first access to VGA resources.

                # That shouldn't happen but...
                # https://bbs.archlinux.org/viewtopic.php?pid=1508940#p1508940
                if nvidia_loaded:
                    subprocess.call(['rmmod', 'nvidia_drm'])
                    subprocess.call(['rmmod', 'nvidia_modeset'])
                    subprocess.call(['rmmod', 'nvidia_uvm'])
                    subprocess.check_call(['rmmod', 'nvidia'])
                    nvidia_loaded = False
            else:
                with open(device_info['path'] + '/driver/unbind', 'w') as f:
                    f.write(id)

        # Bind the device to VFIO driver
        with open('/sys/bus/pci/drivers/vfio-pci/new_id', 'w') as f:
            f.write("%s %s" % (device_info['vendor'], device_info['model']))

    return devices
```

Da una idea de todos los pasos que habría que hacer si quisiéramos hacerlo a mano. Trata el caso de las tarjetas gráficas NVIDIA de forma especial, puesto que con esas GPU es recomendable descargar los módulos correspondientes del núcleo.

La otra función es el método `LibvirtHook.on_host_prepare()`, que contiene el código que llama a `vfio_bind_devices()` antes del inicio de la máquina virtual de nombre "hoth", que es el nombre mi máquina virtual con Windows.

```python
def on_hoth_prepare(self):
    """Hook method to 'prepare' the start of a VM named 'hoth'
    """
    # Bind the PCI devices to passthrough to VFIO driver
    device_ids = [
        format_device_id(**address.attrib)
        for address in self.object_description.findall("devices/hostdev[@type='pci']/source/address")
    ]
    vfio_bind_devices(device_ids)

    # Start all the networks where the VM will be connected
    network_names = [
        source.get('network')
        for source in self.object_description.findall("devices/interface[@type='network']/source")
    ]
    for network_name in network_names:
        network = self.virt_connection.networkLookupByName(network_name)
        if network and not network.isActive():
            network.create()
```

Si nuestra máquina virtual se llama "foo", solo necesitamos renombrar el método como `on_foo_prepare()`.

Este método no solo registra los dispositivos en VFIO para hacer *PCI passthrough*. También busca las redes virtuales a las que está conectada la máquina virtual y le indica a libvirt que las active.

# Liberar la gráfica

El script anterior únicamente podrá descargar el controlador de la tarjeta gráfica si no es la que estamos usando actualmente para el escritorio de Linux. Podemos consultar qué gráfica está utilizando actualmente nuestro entorno gráfico ejecutando:

```sh
$ prime-select query  
nvidia
```

Si estamos usando la tarjeta gráfica, podemos seleccionar la gráfica integrada ejecutando:

```sh
prime-select intel
```

Después tendremos que reiniciar el sistema o salir de la sesión de escritorio y reiniciar el gestor de pantalla:

```sh
sudo systemctl restart display-manager
```

# Crear la máquina virtual

Hecho todo esto, podemos lanzar *virt-manager* para crear nuestra máquina virtual. Seleccionamos la opción de menú **Archivo/Nueva máquina virtual**, se nos abrirá el asistente y seguimos los pasos indicados:

1. Instalación mediante **Medio de instalación local**.
    
2. Seleccionamos **Utilizar imagen ISO** y seleccionamos la imagen ISO con el contenido del CD de instalación de Windows.
    
3. Asignamos el número de CPU disponibles y la cantidad de memoria RAM. En mi caso la máquina virtual tiene 4 CPU y 8 GB de RAM. Sin embargo, posteriormente veremos cómo indicar que la cantidad real asignada sea menor.
    
4. Indicamos el tamaño del disco duro de la máquina virtual. Yo opté por usar un [volumen lógico LVM](https://es.wikipedia.org/wiki/Logical_Volume_Manager), dentro del mismo grupo de volumen que contiene los volúmenes lógicos de mi sistema anfitrión Linux. Aunque no voy a detenerme en explicar cómo se hace.
    
5. Por defecto la imagen del disco duro se guarda en un archivo con formato [**qcow2**](https://en.wikipedia.org/wiki/Qcow). Eso es interesante si queremos ahorrar espacio, dado que empieza siendo un archivo muy pequeño y su tamaño aumenta dinámicamente según va ocupando espacio la máquina virtual. Pero si lo que nos interesa es el mejor rendimiento, mejor optar por el formato **raw**. En ese caso elegimos **Seleccionar o crear almacenaje personalizado** y le pedimos crear un nuevo volumen. Nos dejará escoger el nombre del archivo, formato y tamaño.
    

![Crear un volumen del almacenamiento en formato "raw"](https://cdn.hashnode.com/res/hashnode/image/upload/v1637025881798/g-_tXyrttk.png align="center")

En la última ventana marcamos **Personalizar antes de instalar** y finalmente le damos un nombre.

![Último paso en el asistente de creación de la máquina virtual](https://cdn.hashnode.com/res/hashnode/image/upload/v1637025881790/L29rJwKqgX.png align="center")

Al pulsar en finalizar se abre la ventana de detalles de la nueva máquina virtual.

![Detalles generales de configuración antes de iniciar la instalación](https://cdn.hashnode.com/res/hashnode/image/upload/v1637025881796/bKjX02RrOO.png align="center")

Seleccionamos el firmware UEFI, en lugar de BIOS, y el modelo del *chipset*. La configuración más segura es con [i440FX](https://es.wikipedia.org/wiki/Intel_440FX). A mí no me fue posible iniciar la instalación con Q35, puesto que con ese *chipset* la interfaz con los discos duros es AHCI y la versión de OVMF que estaba utilizando no lo soportaba. Entonces intenté añadir un chip PIIX4, para tener una interfaz IDE a la que conectar el disco duro, pero Windows no era capaz de verlo durante la instalación.

En principio, la primera opción debería ser i440FX, dejando Q35 para los casos en los que el primero no funciona. Por ejemplo, si queremos instalar una máquina virtual con macOS.

Aceptamos y avanzamos a la configuración de las CPU:

![Detalles sobre la configuración de las CPU antes de iniciar la instalación](https://cdn.hashnode.com/res/hashnode/image/upload/v1637025881780/KWw4__4YCR.png align="center")

Aquí lo adecuado es indicar como modelo **host-passthrough**. Esta opción no está en la lista desplegable, así que tendremos que escribirla a mano. Básicamente, le dice a *KVM* que "pase" a la máquina virtual la CPU del anfitrión tal cual, sin modificaciones.

Además, se puede modificar la topología, es decir, cómo se exponen las CPU a la máquina virtual. Como se puede ver he optado porque la máquina virtual tenga una sola CPU, con 4 núcleos y un hilo de ejecución en cada uno.

Lo siguiente es modificar la configuración del disco duro principal: **IDE Disco 1**.

![Detalles sobre la configuración del disco duro antes de iniciar la instalación](https://cdn.hashnode.com/res/hashnode/image/upload/v1637025881782/Y8vH_WqLP8.png align="center")

Por defecto *QEMU* emula una interfaz IDE para el acceso al disco duro. Sin embargo, se puede mejorar el rendimiento desplegando **Opciones avanzadas** y cambiando **Bus de disco** a SCSI. Luego se pulsa en **Agregar hardware** y se añade un **Controlador** del tipo **SCSI** y modelo **VirtIO SCSI**.

![Añadir un controlador de disco VirtIO SCSI](https://cdn.hashnode.com/res/hashnode/image/upload/v1637025881792/griz0jJQE7.png align="center")

[VirtIO](https://es.wikibooks.org/wiki/QEMU/Dispositivos/Virtio) es una interfaz de disco paravirtualizada. Es decir, no emula una interfaz de disco real, como IDE o SCSI; sino que la máquina virtual sabe que es una interfaz por software diseñada para virtualización. El resultado es un mayor rendimiento. Sin embargo, Windows no sabe acceder a estos dispositivos si no se le proporcionan controladores adecuados durante la instalación.

Para que estos controladores estén disponibles necesitamos una segunda unidad de CDROM. Pulsamos en **Agregar hardware** y añadimos **Almacenamiento** de tipo de dispositivo CDROM y tipo de bus IDE.

![Añadir una unidad de CDROM para los controladores VirtIO](https://cdn.hashnode.com/res/hashnode/image/upload/v1637025881788/r_MS7uapzW.png align="center")

Hay que indicar que use la imagen ISO con los controladores VirtIO. La última versión se puede descargar desde [aquí](https://fedorapeople.org/groups/virt/virtio-win/direct-downloads/latest-virtio/).

Finalmente, pulsamos en iniciar la instalación.

Si el firmware UEFI no inicia la instalación, sino que nos deja en una interfaz de línea de comandos como la siguiente:

![Interfaz de línea de comandos de OVMF](https://cdn.hashnode.com/res/hashnode/image/upload/v1637025881773/L-Rg8RKi0.png align="center")

tendremos que buscar y lanzar nosotros mismos el arranque del instalador, ubicado seguramente en `FS0:\EFI\BOOT\BOOTX64`:

![Pasos para localizar el arranque del instalado de la ISO de Windows](https://cdn.hashnode.com/res/hashnode/image/upload/v1637025881784/YY7f6W87cY.png align="center")

Llegados a este punto la instalación debería proceder con normalidad. Cuando nos pregunte dónde instalar Windows, pulsamos en la opción de **Cargar controlador**, buscamos la unidad de CDROM con los controladores VirtIO y navegamos por las carpetas hasta `viosci\w10\amd64`, para instalar los controladores de 64 bits para Windows 10.

Una vez instalado el controlador, el instalador nos dejará seleccionar el disco duro virtual como unidad de destino y completar la instalación.

---

*En la* [*siguiente parte*](/escritorio-virtual-de-windows-10-parte-2) *veremos algunos ajustes interesantes para optimizar la máquina virtual y detalles adicionales para trabajar con el escritorio virtual de forma más cómoda.*
