qemu/hw/virtio/virtio-mem-pci.c
David Hildenbrand 177f9b1ee4 virtio-mem: Expose device memory dynamically via multiple memslots if enabled
Having large virtio-mem devices that only expose little memory to a VM
is currently a problem: we map the whole sparse memory region into the
guest using a single memslot, resulting in one gigantic memslot in KVM.
KVM allocates metadata for the whole memslot, which can result in quite
some memory waste.

Assuming we have a 1 TiB virtio-mem device and only expose little (e.g.,
1 GiB) memory, we would create a single 1 TiB memslot and KVM has to
allocate metadata for that 1 TiB memslot: on x86, this implies allocating
a significant amount of memory for metadata:

(1) RMAP: 8 bytes per 4 KiB, 8 bytes per 2 MiB, 8 bytes per 1 GiB
    -> For 1 TiB: 2147483648 + 4194304 + 8192 = ~ 2 GiB (0.2 %)

    With the TDP MMU (cat /sys/module/kvm/parameters/tdp_mmu) this gets
    allocated lazily when required for nested VMs
(2) gfn_track: 2 bytes per 4 KiB
    -> For 1 TiB: 536870912 = ~512 MiB (0.05 %)
(3) lpage_info: 4 bytes per 2 MiB, 4 bytes per 1 GiB
    -> For 1 TiB: 2097152 + 4096 = ~2 MiB (0.0002 %)
(4) 2x dirty bitmaps for tracking: 2x 1 bit per 4 KiB page
    -> For 1 TiB: 536870912 = 64 MiB (0.006 %)

So we primarily care about (1) and (2). The bad thing is, that the
memory consumption *doubles* once SMM is enabled, because we create the
memslot once for !SMM and once for SMM.

Having a 1 TiB memslot without the TDP MMU consumes around:
* With SMM: 5 GiB
* Without SMM: 2.5 GiB
Having a 1 TiB memslot with the TDP MMU consumes around:
* With SMM: 1 GiB
* Without SMM: 512 MiB

... and that's really something we want to optimize, to be able to just
start a VM with small boot memory (e.g., 4 GiB) and a virtio-mem device
that can grow very large (e.g., 1 TiB).

Consequently, using multiple memslots and only mapping the memslots we
really need can significantly reduce memory waste and speed up
memslot-related operations. Let's expose the sparse RAM memory region using
multiple memslots, mapping only the memslots we currently need into our
device memory region container.

The feature can be enabled using "dynamic-memslots=on" and requires
"unplugged-inaccessible=on", which is nowadays the default.

Once enabled, we'll auto-detect the number of memslots to use based on the
memslot limit provided by the core. We'll use at most 1 memslot per
gigabyte. Note that our global limit of memslots accross all memory devices
is currently set to 256: even with multiple large virtio-mem devices,
we'd still have a sane limit on the number of memslots used.

The default is to not dynamically map memslot for now
("dynamic-memslots=off"). The optimization must be enabled manually,
because some vhost setups (e.g., hotplug of vhost-user devices) might be
problematic until we support more memslots especially in vhost-user backends.

Note that "dynamic-memslots=on" is just a hint that multiple memslots
*may* be used for internal optimizations, not that multiple memslots
*must* be used. The actual number of memslots that are used is an
internal detail: for example, once memslot metadata is no longer an
issue, we could simply stop optimizing for that. Migration source and
destination can differ on the setting of "dynamic-memslots".

Message-ID: <20230926185738.277351-17-david@redhat.com>
Reviewed-by: Maciej S. Szmigiero <maciej.szmigiero@oracle.com>
Reviewed-by: Michael S. Tsirkin <mst@redhat.com>
Signed-off-by: David Hildenbrand <david@redhat.com>
2023-10-12 14:15:22 +02:00

221 lines
7.4 KiB
C

/*
* Virtio MEM PCI device
*
* Copyright (C) 2020 Red Hat, Inc.
*
* Authors:
* David Hildenbrand <david@redhat.com>
*
* This work is licensed under the terms of the GNU GPL, version 2.
* See the COPYING file in the top-level directory.
*/
#include "qemu/osdep.h"
#include "virtio-mem-pci.h"
#include "hw/mem/memory-device.h"
#include "qapi/error.h"
#include "qapi/qapi-events-machine.h"
#include "qapi/qapi-events-misc.h"
static void virtio_mem_pci_realize(VirtIOPCIProxy *vpci_dev, Error **errp)
{
VirtIOMEMPCI *mem_pci = VIRTIO_MEM_PCI(vpci_dev);
DeviceState *vdev = DEVICE(&mem_pci->vdev);
virtio_pci_force_virtio_1(vpci_dev);
qdev_realize(vdev, BUS(&vpci_dev->bus), errp);
}
static void virtio_mem_pci_set_addr(MemoryDeviceState *md, uint64_t addr,
Error **errp)
{
object_property_set_uint(OBJECT(md), VIRTIO_MEM_ADDR_PROP, addr, errp);
}
static uint64_t virtio_mem_pci_get_addr(const MemoryDeviceState *md)
{
return object_property_get_uint(OBJECT(md), VIRTIO_MEM_ADDR_PROP,
&error_abort);
}
static MemoryRegion *virtio_mem_pci_get_memory_region(MemoryDeviceState *md,
Error **errp)
{
VirtIOMEMPCI *pci_mem = VIRTIO_MEM_PCI(md);
VirtIOMEM *vmem = &pci_mem->vdev;
VirtIOMEMClass *vmc = VIRTIO_MEM_GET_CLASS(vmem);
return vmc->get_memory_region(vmem, errp);
}
static void virtio_mem_pci_decide_memslots(MemoryDeviceState *md,
unsigned int limit)
{
VirtIOMEMPCI *pci_mem = VIRTIO_MEM_PCI(md);
VirtIOMEM *vmem = VIRTIO_MEM(&pci_mem->vdev);
VirtIOMEMClass *vmc = VIRTIO_MEM_GET_CLASS(vmem);
vmc->decide_memslots(vmem, limit);
}
static unsigned int virtio_mem_pci_get_memslots(MemoryDeviceState *md)
{
VirtIOMEMPCI *pci_mem = VIRTIO_MEM_PCI(md);
VirtIOMEM *vmem = VIRTIO_MEM(&pci_mem->vdev);
VirtIOMEMClass *vmc = VIRTIO_MEM_GET_CLASS(vmem);
return vmc->get_memslots(vmem);
}
static uint64_t virtio_mem_pci_get_plugged_size(const MemoryDeviceState *md,
Error **errp)
{
return object_property_get_uint(OBJECT(md), VIRTIO_MEM_SIZE_PROP,
errp);
}
static void virtio_mem_pci_fill_device_info(const MemoryDeviceState *md,
MemoryDeviceInfo *info)
{
VirtioMEMDeviceInfo *vi = g_new0(VirtioMEMDeviceInfo, 1);
VirtIOMEMPCI *pci_mem = VIRTIO_MEM_PCI(md);
VirtIOMEM *vmem = &pci_mem->vdev;
VirtIOMEMClass *vpc = VIRTIO_MEM_GET_CLASS(vmem);
DeviceState *dev = DEVICE(md);
if (dev->id) {
vi->id = g_strdup(dev->id);
}
/* let the real device handle everything else */
vpc->fill_device_info(vmem, vi);
info->u.virtio_mem.data = vi;
info->type = MEMORY_DEVICE_INFO_KIND_VIRTIO_MEM;
}
static uint64_t virtio_mem_pci_get_min_alignment(const MemoryDeviceState *md)
{
return object_property_get_uint(OBJECT(md), VIRTIO_MEM_BLOCK_SIZE_PROP,
&error_abort);
}
static void virtio_mem_pci_size_change_notify(Notifier *notifier, void *data)
{
VirtIOMEMPCI *pci_mem = container_of(notifier, VirtIOMEMPCI,
size_change_notifier);
DeviceState *dev = DEVICE(pci_mem);
char *qom_path = object_get_canonical_path(OBJECT(dev));
const uint64_t * const size_p = data;
qapi_event_send_memory_device_size_change(dev->id, *size_p, qom_path);
g_free(qom_path);
}
static void virtio_mem_pci_unplug_request_check(VirtIOMDPCI *vmd, Error **errp)
{
VirtIOMEMPCI *pci_mem = VIRTIO_MEM_PCI(vmd);
VirtIOMEM *vmem = &pci_mem->vdev;
VirtIOMEMClass *vpc = VIRTIO_MEM_GET_CLASS(vmem);
vpc->unplug_request_check(vmem, errp);
}
static void virtio_mem_pci_get_requested_size(Object *obj, Visitor *v,
const char *name, void *opaque,
Error **errp)
{
VirtIOMEMPCI *pci_mem = VIRTIO_MEM_PCI(obj);
object_property_get(OBJECT(&pci_mem->vdev), name, v, errp);
}
static void virtio_mem_pci_set_requested_size(Object *obj, Visitor *v,
const char *name, void *opaque,
Error **errp)
{
VirtIOMEMPCI *pci_mem = VIRTIO_MEM_PCI(obj);
DeviceState *dev = DEVICE(obj);
/*
* If we passed virtio_mem_pci_unplug_request_check(), making sure that
* the requested size is 0, don't allow modifying the requested size
* anymore, otherwise the VM might end up hotplugging memory before
* handling the unplug request.
*/
if (dev->pending_deleted_event) {
error_setg(errp, "'%s' cannot be changed if the device is in the"
" process of unplug", name);
return;
}
object_property_set(OBJECT(&pci_mem->vdev), name, v, errp);
}
static void virtio_mem_pci_class_init(ObjectClass *klass, void *data)
{
DeviceClass *dc = DEVICE_CLASS(klass);
VirtioPCIClass *k = VIRTIO_PCI_CLASS(klass);
PCIDeviceClass *pcidev_k = PCI_DEVICE_CLASS(klass);
MemoryDeviceClass *mdc = MEMORY_DEVICE_CLASS(klass);
VirtIOMDPCIClass *vmdc = VIRTIO_MD_PCI_CLASS(klass);
k->realize = virtio_mem_pci_realize;
set_bit(DEVICE_CATEGORY_MISC, dc->categories);
pcidev_k->revision = VIRTIO_PCI_ABI_VERSION;
pcidev_k->class_id = PCI_CLASS_OTHERS;
mdc->get_addr = virtio_mem_pci_get_addr;
mdc->set_addr = virtio_mem_pci_set_addr;
mdc->get_plugged_size = virtio_mem_pci_get_plugged_size;
mdc->get_memory_region = virtio_mem_pci_get_memory_region;
mdc->decide_memslots = virtio_mem_pci_decide_memslots;
mdc->get_memslots = virtio_mem_pci_get_memslots;
mdc->fill_device_info = virtio_mem_pci_fill_device_info;
mdc->get_min_alignment = virtio_mem_pci_get_min_alignment;
vmdc->unplug_request_check = virtio_mem_pci_unplug_request_check;
}
static void virtio_mem_pci_instance_init(Object *obj)
{
VirtIOMEMPCI *dev = VIRTIO_MEM_PCI(obj);
VirtIOMEMClass *vmc;
VirtIOMEM *vmem;
virtio_instance_init_common(obj, &dev->vdev, sizeof(dev->vdev),
TYPE_VIRTIO_MEM);
dev->size_change_notifier.notify = virtio_mem_pci_size_change_notify;
vmem = &dev->vdev;
vmc = VIRTIO_MEM_GET_CLASS(vmem);
/*
* We never remove the notifier again, as we expect both devices to
* disappear at the same time.
*/
vmc->add_size_change_notifier(vmem, &dev->size_change_notifier);
object_property_add_alias(obj, VIRTIO_MEM_BLOCK_SIZE_PROP,
OBJECT(&dev->vdev), VIRTIO_MEM_BLOCK_SIZE_PROP);
object_property_add_alias(obj, VIRTIO_MEM_SIZE_PROP, OBJECT(&dev->vdev),
VIRTIO_MEM_SIZE_PROP);
object_property_add(obj, VIRTIO_MEM_REQUESTED_SIZE_PROP, "size",
virtio_mem_pci_get_requested_size,
virtio_mem_pci_set_requested_size, NULL, NULL);
}
static const VirtioPCIDeviceTypeInfo virtio_mem_pci_info = {
.base_name = TYPE_VIRTIO_MEM_PCI,
.parent = TYPE_VIRTIO_MD_PCI,
.generic_name = "virtio-mem-pci",
.instance_size = sizeof(VirtIOMEMPCI),
.instance_init = virtio_mem_pci_instance_init,
.class_init = virtio_mem_pci_class_init,
};
static void virtio_mem_pci_register_types(void)
{
virtio_pci_types_register(&virtio_mem_pci_info);
}
type_init(virtio_mem_pci_register_types)