On 01/17/2013 06:29 PM, Rusty Russell wrote: > This is mainly to test the drivers/vhost/vringh.c code, but it also > uses the drivers/virtio/virtio_ring.c code for the guest side. vringh_test.c does not compile here: (This series on top of 9a9284153d965a57edc7162a8e57c14c97f3a935) $ cd tools/virtio $ make cc -g -O2 -Wall -I. -I ../../usr/include/ -Wno-pointer-sign -fno-strict-overflow -MMD vringh_test.c -o vringh_test In file included from ./linux/vringh.h:1:0, from ./../../drivers/vhost/vringh.c:6, from vringh_test.c:7: ./linux/../../../include/linux/vringh.h:27:28: fatal error: uapi/linux/uio.h: No such file or directory compilation terminated. make: *** [vringh_test] Error 1 > Usage for testing the basic implementation: > > ./vringh_test > # Test with indirect descriptors > ./vringh_test --indirect > # Test with indirect descriptors and event indexex > ./vringh_test --indirect --eventidx > > You can run a parallel stress test by adding --parallel to any of the > above options. > > eg ./vringh_test --parallel: > Using CPUS 0 and 3 > Guest: notified 10107974, pinged 107970 > Host: notified 108158, pinged 3172148 > Time: R=17.659 U=6.640 S=6.640 > > ./vringh_test --eventidx --parallel: > Using CPUS 0 and 3 > Guest: notified 156357, pinged 156251 > Host: notified 156251, pinged 78179 > Time: R=4.518 U=3.536 S=3.536 > > Signed-off-by: Rusty Russell <rusty@xxxxxxxxxxxxxxx> > --- > tools/virtio/Makefile | 4 +- > tools/virtio/vringh_test.c | 591 ++++++++++++++++++++++++++++++++++++++++++++ > 2 files changed, 593 insertions(+), 2 deletions(-) > create mode 100644 tools/virtio/vringh_test.c > > diff --git a/tools/virtio/Makefile b/tools/virtio/Makefile > index d1d442e..b928c3e 100644 > --- a/tools/virtio/Makefile > +++ b/tools/virtio/Makefile > @@ -1,5 +1,5 @@ > all: test mod > -test: virtio_test > +test: virtio_test vringh_test > virtio_test: virtio_ring.o virtio_test.o > CFLAGS += -g -O2 -Wall -I. -I ../../usr/include/ -Wno-pointer-sign -fno-strict-overflow -MMD > vpath %.c ../../drivers/virtio > @@ -7,6 +7,6 @@ mod: > ${MAKE} -C `pwd`/../.. M=`pwd`/vhost_test > .PHONY: all test mod clean > clean: > - ${RM} *.o vhost_test/*.o vhost_test/.*.cmd \ > + ${RM} *.o vringh_test virtio_test vhost_test/*.o vhost_test/.*.cmd \ > vhost_test/Module.symvers vhost_test/modules.order *.d > -include *.d > diff --git a/tools/virtio/vringh_test.c b/tools/virtio/vringh_test.c > new file mode 100644 > index 0000000..f3868f4 > --- /dev/null > +++ b/tools/virtio/vringh_test.c > @@ -0,0 +1,591 @@ > +/* Simple test of virtio code, entirely in userpsace. */ > +#define _GNU_SOURCE > +#include <sched.h> > +#include <err.h> > +#include <linux/kernel.h> > +#include <linux/err.h> > +#include <../../drivers/vhost/vringh.c> > +#include <../../drivers/virtio/virtio_ring.c> > +#include <sys/types.h> > +#include <sys/stat.h> > +#include <sys/mman.h> > +#include <sys/wait.h> > +#include <fcntl.h> > + > +#define USER_MEM (1024*1024) > +void *__user_addr_min, *__user_addr_max; > +void *__kmalloc_fake, *__kfree_ignore_start, *__kfree_ignore_end; > +static u64 user_addr_offset; > + > +#define RINGSIZE 256 > +#define ALIGN 4096 > + > +static void never_notify_host(struct virtqueue *vq) > +{ > + abort(); > +} > + > +static void never_callback_guest(struct virtqueue *vq) > +{ > + abort(); > +} > + > +static inline bool getrange_iov(u64 addr, struct vringh_range *r) > +{ > + r->start = (u64)(unsigned long)__user_addr_min - user_addr_offset; > + r->end_incl = (u64)(unsigned long)__user_addr_max - 1 - user_addr_offset; > + r->offset = user_addr_offset; > + return true; > +} > + > +struct guest_virtio_device { > + struct virtio_device vdev; > + int to_host_fd; > + unsigned long notifies; > +}; > + > +static void parallel_notify_host(struct virtqueue *vq) > +{ > + struct guest_virtio_device *gvdev; > + > + gvdev = container_of(vq->vdev, struct guest_virtio_device, vdev); > + write(gvdev->to_host_fd, "", 1); > + gvdev->notifies++; > +} > + > +#define NUM_XFERS (10000000) > + > +/* We aim for two "distant" cpus. */ > +static void find_cpus(unsigned int *first, unsigned int *last) > +{ > + unsigned int i; > + > + *first = -1U; > + *last = 0; > + for (i = 0; i < 4096; i++) { > + cpu_set_t set; > + CPU_ZERO(&set); > + CPU_SET(i, &set); > + if (sched_setaffinity(getpid(), sizeof(set), &set) == 0) { > + if (i < *first) > + *first = i; > + if (i > *last) > + *last = i; > + } > + } > +} > + > +static int parallel_test(unsigned long features) > +{ > + void *host_map, *guest_map; > + int fd, mapsize, to_guest[2], to_host[2]; > + unsigned long xfers = 0, notifies = 0, receives = 0; > + unsigned int first_cpu, last_cpu; > + cpu_set_t cpu_set; > + > + /* Create real file to mmap. */ > + fd = open("/tmp/vringh_test-file", O_RDWR|O_CREAT|O_TRUNC, 0600); > + if (fd < 0) > + err(1, "Opening /tmp/vringh_test-file"); > + > + /* Extra room at the end for some data, and indirects */ > + mapsize = vring_size(RINGSIZE, ALIGN) > + + RINGSIZE * 2 * sizeof(int) > + + RINGSIZE * 6 * sizeof(struct vring_desc); > + mapsize = (mapsize + getpagesize() - 1) & ~(getpagesize() - 1); > + ftruncate(fd, mapsize); > + > + /* Parent and child use separate addresses, to check our mapping logic! */ > + host_map = mmap(NULL, mapsize, PROT_READ|PROT_WRITE, MAP_SHARED, fd, 0); > + guest_map = mmap(NULL, mapsize, PROT_READ|PROT_WRITE, MAP_SHARED, fd, 0); > + > + pipe(to_guest); > + pipe(to_host); > + > + CPU_ZERO(&cpu_set); > + find_cpus(&first_cpu, &last_cpu); > + printf("Using CPUS %u and %u\n", first_cpu, last_cpu); > + fflush(stdout); > + > + if (fork() != 0) { > + struct vringh vrh; > + bool notify = false; > + int status; > + > + /* We are the host: never access guest addresses! */ > + munmap(guest_map, mapsize); > + > + __user_addr_min = host_map; > + __user_addr_max = __user_addr_min + mapsize; > + user_addr_offset = host_map - guest_map; > + assert(user_addr_offset); > + > + close(to_guest[0]); > + close(to_host[1]); > + > + vring_init(&vrh.vring, RINGSIZE, host_map, ALIGN); > + vringh_init_user(&vrh, features, RINGSIZE, true, > + vrh.vring.desc, vrh.vring.avail, vrh.vring.used); > + CPU_SET(first_cpu, &cpu_set); > + if (sched_setaffinity(getpid(), sizeof(cpu_set), &cpu_set)) > + err(1, "Could not set affinity to cpu %u", first_cpu); > + > + while (xfers < NUM_XFERS) { > + struct iovec host_riov[2], host_wiov[2]; > + struct vringh_iov riov, wiov; > + char buf[5]; > + u16 head; > + int rlen, err; > + > + riov.iov = host_riov; > + riov.max = ARRAY_SIZE(host_riov); > + riov.allocated = false; > + > + wiov.iov = host_wiov; > + wiov.max = ARRAY_SIZE(host_wiov); > + wiov.allocated = false; > + > + err = vringh_getdesc_user(&vrh, &riov, &wiov, getrange_iov, > + &head, GFP_KERNEL); > + if (err == 0) { > + char buf[128]; > + > + if (notify) { > + write(to_guest[1], "", 1); > + notifies++; > + notify = false; > + } > + > + if (vringh_notify_enable_user(&vrh)) > + continue; > + > + /* Swallow all notifies at once. */ > + if (read(to_host[0], buf, sizeof(buf)) < 1) > + break; > + > + vringh_notify_disable_user(&vrh); > + receives++; > + continue; > + } > + if (err != 1) > + errx(1, "vringh_getdesc_user: %i", err); > + > + /* We simply copy bytes. */ > + rlen = vringh_iov_pull_user(&riov, buf, sizeof(buf)); > + if (rlen < 0) > + errx(1, "vringh_iov_pull_user: %i", rlen); > + err = vringh_iov_push_user(&wiov, buf, rlen); > + if (err != rlen) > + errx(1, "vringh_iov_push_user: %i", err); > + xfers++; > + assert(wiov.i == wiov.max); > + > + err = vringh_complete_user(&vrh, head, rlen, ¬ify); > + if (err != 0) > + errx(1, "vringh_complete_user: %i", err); > + } > + > + if (notify) { > + write(to_guest[1], "", 1); > + notifies++; > + notify = false; > + } > + wait(&status); > + if (!WIFEXITED(status)) > + errx(1, "Child died with signal %i?", WTERMSIG(status)); > + if (WEXITSTATUS(status) != 0) > + errx(1, "Child exited %i?", WEXITSTATUS(status)); > + printf("Host: notified %lu, pinged %lu\n", notifies, receives); > + return 0; > + } else { > + struct guest_virtio_device gvdev; > + struct virtqueue *vq; > + unsigned int *data; > + struct vring_desc *indirects; > + unsigned int finished = 0; > + > + /* We pass sg[]s pointing into here, but we need RINGSIZE+1 */ > + data = guest_map + vring_size(RINGSIZE, ALIGN); > + indirects = (void *)data + (RINGSIZE + 1) * 2 * sizeof(int); > + > + /* We are the guest. */ > + munmap(host_map, mapsize); > + > + close(to_guest[1]); > + close(to_host[0]); > + > + gvdev.vdev.features[0] = features; > + gvdev.to_host_fd = to_host[1]; > + > + CPU_SET(first_cpu, &cpu_set); > + if (sched_setaffinity(getpid(), sizeof(cpu_set), &cpu_set)) > + err(1, "Could not set affinity to cpu %u", first_cpu); > + > + vq = vring_new_virtqueue(0, RINGSIZE, ALIGN, &gvdev.vdev, true, > + guest_map, parallel_notify_host, > + never_callback_guest, "guest vq"); > + > + /* Don't kfree indirects. */ > + __kfree_ignore_start = indirects; > + __kfree_ignore_end = indirects + RINGSIZE * 6; > + > + while (xfers < NUM_XFERS) { > + struct scatterlist sg[6]; > + unsigned int num_sg, len; > + int *din, *dout, err; > + > + /* Consume bufs. */ > + while ((din = virtqueue_get_buf(vq, &len)) != NULL) { > + dout = din + 1; > + assert(*dout == *din); > + assert(len == 4); > + finished++; > + } > + > + /* Produce a buffer. */ > + din = data + (xfers % (RINGSIZE + 1)) * 2; > + dout = din + 1; > + > + *din = xfers; > + switch ((xfers / sizeof(*din)) % 3) { > + case 0: > + /* Nasty three-element sg list. */ > + sg_init_table(sg, num_sg = 3); > + sg_set_buf(&sg[0], (void *)din, 1); > + sg_set_buf(&sg[1], (void *)din + 1, 2); > + sg_set_buf(&sg[2], (void *)din + 3, 1); > + sg_init_table(sg + num_sg, num_sg); > + sg_set_buf(&sg[num_sg+0], (void *)dout, 1); > + sg_set_buf(&sg[num_sg+1], (void *)dout + 1, 2); > + sg_set_buf(&sg[num_sg+2], (void *)dout + 3, 1); > + break; > + case 1: > + sg_init_table(sg, num_sg = 2); > + sg_set_buf(&sg[0], (void *)din, 1); > + sg_set_buf(&sg[1], (void *)din + 1, 3); > + sg_init_table(sg + num_sg, num_sg); > + sg_set_buf(&sg[num_sg+0], (void *)dout, 1); > + sg_set_buf(&sg[num_sg+1], (void *)dout + 1, 3); > + break; > + case 2: > + sg_init_table(sg, num_sg = 1); > + sg_set_buf(&sg[0], (void *)din, 4); > + sg_init_table(sg + num_sg, num_sg); > + sg_set_buf(&sg[num_sg+0], (void *)dout, 4); > + break; > + } > + > + /* May allocate an indirect, so force it to allocate > + * user addr */ > + __kmalloc_fake = indirects + (xfers % RINGSIZE) * 6; > + err = virtqueue_add_buf(vq, sg, num_sg, num_sg, din, > + GFP_KERNEL); > + if (err == -ENOSPC) { > + char buf[128]; > + > + if (!virtqueue_enable_cb_delayed(vq)) > + continue; > + /* Swallow all notifies at once. */ > + if (read(to_guest[0], buf, sizeof(buf)) < 1) > + break; > + > + receives++; > + virtqueue_disable_cb(vq); > + continue; > + } > + > + if (err) > + errx(1, "virtqueue_add_buf: %i", err); > + > + xfers++; > + virtqueue_kick(vq); > + } > + > + /* Any extra? */ > + while (finished != xfers) { > + char buf[128]; > + int *din, *dout; > + unsigned int len; > + > + /* Consume bufs. */ > + din = virtqueue_get_buf(vq, &len); > + if (din) { > + dout = din + 1; > + assert(*dout == *din); > + assert(len == 4); > + finished++; > + continue; > + } > + > + if (!virtqueue_enable_cb_delayed(vq)) > + continue; > + if (read(to_guest[0], buf, sizeof(buf)) < 1) > + break; > + > + receives++; > + virtqueue_disable_cb(vq); > + } > + > + printf("Guest: notified %lu, pinged %lu\n", > + gvdev.notifies, receives); > + return 0; > + } > +} > + > +int main(int argc, char *argv[]) > +{ > + struct virtio_device vdev; > + struct virtqueue *vq; > + struct vringh vrh; > + struct scatterlist guest_sg[RINGSIZE]; > + struct iovec host_riov[2], host_wiov[2]; > + struct vringh_iov riov, wiov; > + char buf[28]; > + u16 head; > + int err; > + unsigned i; > + bool notify = false; > + void *ret; > + > + vdev.features[0] = 0; > + > + if (argv[1] && strcmp(argv[1], "--indirect") == 0) { > + vdev.features[0] |= (1 << VIRTIO_RING_F_INDIRECT_DESC); > + argv++; > + } > + > + if (argv[1] && strcmp(argv[1], "--eventidx") == 0) { > + vdev.features[0] |= (1 << VIRTIO_RING_F_EVENT_IDX); > + argv++; > + } > + > + if (argv[1] && strcmp(argv[1], "--parallel") == 0) > + return parallel_test(vdev.features[0]); > + > + if (posix_memalign(&__user_addr_min, PAGE_SIZE, USER_MEM) != 0) > + abort(); > + __user_addr_max = __user_addr_min + USER_MEM; > + memset(__user_addr_min, 0, vring_size(RINGSIZE, ALIGN)); > + > + /* Set up guest side. */ > + vq = vring_new_virtqueue(0, RINGSIZE, ALIGN, &vdev, true, > + __user_addr_min, > + never_notify_host, never_callback_guest, > + "guest vq"); > + > + /* Set up host side. */ > + vring_init(&vrh.vring, RINGSIZE, __user_addr_min, ALIGN); > + vringh_init_user(&vrh, vdev.features[0], RINGSIZE, true, > + vrh.vring.desc, vrh.vring.avail, vrh.vring.used); > + > + /* No descriptor to get yet... */ > + err = vringh_getdesc_user(&vrh, &riov, &wiov, getrange_iov, > + &head, GFP_KERNEL); > + if (err != 0) > + errx(1, "vringh_getdesc_user: %i", err); > + > + /* Guest puts in a descriptor. */ > + memcpy(__user_addr_max - 1, "a", 1); > + sg_init_table(guest_sg, 1); > + sg_set_buf(&guest_sg[0], __user_addr_max - 1, 1); > + sg_init_table(guest_sg+1, 1); > + sg_set_buf(&guest_sg[1], __user_addr_max - 3, 2); > + > + /* May allocate an indirect, so force it to allocate user addr */ > + __kmalloc_fake = __user_addr_min + vring_size(RINGSIZE, ALIGN); > + err = virtqueue_add_buf(vq, guest_sg, 1, 1, &err, GFP_KERNEL); > + if (err) > + errx(1, "virtqueue_add_buf: %i", err); > + __kmalloc_fake = NULL; > + > + /* Host retreives it. */ > + riov.iov = host_riov; > + riov.max = ARRAY_SIZE(host_riov); > + riov.allocated = false; > + > + wiov.iov = host_wiov; > + wiov.max = ARRAY_SIZE(host_wiov); > + wiov.allocated = false; > + > + err = vringh_getdesc_user(&vrh, &riov, &wiov, getrange_iov, > + &head, GFP_KERNEL); > + if (err != 1) > + errx(1, "vringh_getdesc_user: %i", err); > + > + assert(riov.max == 1); > + assert(riov.iov[0].iov_base == __user_addr_max - 1); > + assert(riov.iov[0].iov_len == 1); > + assert(wiov.max == 1); > + assert(wiov.iov[0].iov_base == __user_addr_max - 3); > + assert(wiov.iov[0].iov_len == 2); > + > + err = vringh_iov_pull_user(&riov, buf, 5); > + if (err != 1) > + errx(1, "vringh_iov_pull_kern: %i", err); > + assert(buf[0] == 'a'); > + assert(riov.i == 1); > + assert(vringh_iov_pull_kern(&riov, buf, 5) == 0); > + > + memcpy(buf, "bcdef", 5); > + err = vringh_iov_push_user(&wiov, buf, 5); > + if (err != 2) > + errx(1, "vringh_iov_push_user: %i", err); > + assert(memcmp(__user_addr_max - 3, "bc", 2) == 0); > + assert(wiov.i == 1); > + assert(vringh_iov_push_kern(&wiov, buf, 5) == 0); > + > + /* Host is done. */ > + err = vringh_complete_user(&vrh, head, err, ¬ify); > + if (err != 0) > + errx(1, "vringh_complete_user: %i", err); > + > + /* Guest should see used token now. */ > + __kfree_ignore_start = __user_addr_min + vring_size(RINGSIZE, ALIGN); > + __kfree_ignore_end = __kfree_ignore_start + 1; > + ret = virtqueue_get_buf(vq, &i); > + if (ret != &err) > + errx(1, "virtqueue_get_buf: %p", ret); > + assert(i == 2); > + > + /* Guest puts in a huge descriptor. */ > + sg_init_table(guest_sg, RINGSIZE); > + for (i = 0; i < RINGSIZE; i++) { > + sg_set_buf(&guest_sg[i], > + __user_addr_max - USER_MEM/4, USER_MEM/4); > + } > + > + /* Fill contents with recognisable garbage. */ > + for (i = 0; i < USER_MEM/4; i++) > + ((char *)__user_addr_max - USER_MEM/4)[i] = i; > + > + /* This will allocate an indirect, so force it to allocate user addr */ > + __kmalloc_fake = __user_addr_min + vring_size(RINGSIZE, ALIGN); > + err = virtqueue_add_buf(vq, guest_sg, RINGSIZE, 0, &err, GFP_KERNEL); > + if (err) > + errx(1, "virtqueue_add_buf (large): %i", err); > + __kmalloc_fake = NULL; > + > + /* Host picks it up (allocates new iov). */ > + riov.iov = host_riov; > + riov.max = ARRAY_SIZE(host_riov); > + riov.allocated = false; > + > + wiov.iov = host_wiov; > + wiov.max = ARRAY_SIZE(host_wiov); > + wiov.allocated = false; > + > + err = vringh_getdesc_user(&vrh, &riov, &wiov, getrange_iov, > + &head, GFP_KERNEL); > + if (err != 1) > + errx(1, "vringh_getdesc_user: %i", err); > + > + assert(riov.allocated); > + assert(riov.iov != host_riov); > + assert(riov.max == RINGSIZE); > + > + assert(!wiov.allocated); > + assert(wiov.max == 0); > + > + /* Pull data back out (in odd chunks), should be as expected. */ > + for (i = 0; i < RINGSIZE * USER_MEM/4; i += 3) { > + err = vringh_iov_pull_user(&riov, buf, 3); > + if (err != 3 && i + err != RINGSIZE * USER_MEM/4) > + errx(1, "vringh_iov_pull_user large: %i", err); > + assert(buf[0] == (char)i); > + assert(err < 2 || buf[1] == (char)(i + 1)); > + assert(err < 3 || buf[2] == (char)(i + 2)); > + } > + assert(wiov.i == wiov.max); > + > + kfree(riov.iov); > + > + /* Test weird (but legal!) indirect. */ > + if (vdev.features[0] & (1 << VIRTIO_RING_F_INDIRECT_DESC)) { > + struct vring_virtqueue *vvq = to_vvq(vq); > + char *data = __user_addr_max - USER_MEM/4; > + struct vring_desc *d = __user_addr_max - USER_MEM/2; > + unsigned int n = vvq->free_head; > + > + /* Force creation of direct, which we modify. */ > + vvq->indirect = false; > + > + sg_init_table(guest_sg, 4); > + sg_set_buf(&guest_sg[0], d, sizeof(*d)*2); > + sg_set_buf(&guest_sg[1], d + 2, sizeof(*d)*1); > + sg_set_buf(&guest_sg[2], data + 6, 4); > + sg_set_buf(&guest_sg[3], d + 3, sizeof(*d)*3); > + > + err = virtqueue_add_buf(vq, guest_sg, 4, 0, &err, GFP_KERNEL); > + if (err) > + errx(1, "virtqueue_add_buf (indirect): %i", err); > + > + /* They're used in order, but double-check... */ > + assert(vvq->vring.desc[n].addr == (unsigned long)d); > + assert(vvq->vring.desc[n+1].addr == (unsigned long)(d+2)); > + assert(vvq->vring.desc[n+2].addr == (unsigned long)data + 6); > + assert(vvq->vring.desc[n+3].addr == (unsigned long)(d+3)); > + vvq->vring.desc[n].flags |= VRING_DESC_F_INDIRECT; > + vvq->vring.desc[n+1].flags |= VRING_DESC_F_INDIRECT; > + vvq->vring.desc[n+3].flags |= VRING_DESC_F_INDIRECT; > + > + /* First indirect */ > + d[0].addr = (unsigned long)data; > + d[0].len = 1; > + d[0].flags = VRING_DESC_F_NEXT; > + d[0].next = 1; > + d[1].addr = (unsigned long)data + 1; > + d[1].len = 2; > + d[1].flags = 0; > + > + /* Second indirect */ > + d[2].addr = (unsigned long)data + 3; > + d[2].len = 3; > + d[2].flags = 0; > + > + /* Third indirect */ > + d[3].addr = (unsigned long)data + 10; > + d[3].len = 5; > + d[3].flags = VRING_DESC_F_NEXT; > + d[3].next = 1; > + d[4].addr = (unsigned long)data + 15; > + d[4].len = 6; > + d[4].flags = VRING_DESC_F_NEXT; > + d[4].next = 2; > + d[5].addr = (unsigned long)data + 21; > + d[5].len = 7; > + d[5].flags = 0; > + > + /* Host picks it up (allocates new iov). */ > + riov.iov = host_riov; > + riov.max = ARRAY_SIZE(host_riov); > + riov.allocated = false; > + > + wiov.iov = host_wiov; > + wiov.max = ARRAY_SIZE(host_wiov); > + wiov.allocated = false; > + > + err = vringh_getdesc_user(&vrh, &riov, &wiov, getrange_iov, > + &head, GFP_KERNEL); > + if (err != 1) > + errx(1, "vringh_getdesc_user: %i", err); > + > + if (head != n) > + errx(1, "vringh_getdesc_user: head %i not %i", head, n); > + > + assert(riov.max == 7); > + assert(riov.allocated); > + err = vringh_iov_pull_user(&riov, buf, 29); > + assert(err == 28); > + > + /* Data should be linear. */ > + for (i = 0; i < err; i++) > + assert(buf[i] == i); > + kfree(riov.iov); > + } > + > + /* Don't leak memory... */ > + vring_del_virtqueue(vq); > + free(__user_addr_min); > + > + return 0; > +} > -- Asias _______________________________________________ Virtualization mailing list Virtualization@xxxxxxxxxxxxxxxxxxxxxxxxxx https://lists.linuxfoundation.org/mailman/listinfo/virtualization