目录
一、概述
二、代码部分
1、Virtio 前端
(1) User Space
(2) Kernel Space
2、Virtio 后端
三、运行
QEMU Version:qemu-7.2.0
Linux Version:linux-5.4.239
一、概述
本篇文章的主要内容是使用Virtio前后端数据传输的机制实现一个计算阶乘的小程序,主要功能是在Virtio driver中传递一个整数到Virtio device,在Virtio device中计算这个整数的阶乘,计算完成后再将计算结果传递给Virtio driver,下面是代码部分。
二、代码部分
代码主要分为两个部分,分别是Virtio前端(Guest Os)和Virtio后端(QEMU),而Virtio前端又分User Space和Kernel Space。
1、Virtio 前端
(1) User Space
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <string.h>
#include <stdint.h>
int main(int argc, char *argv[])
{
int fd, retvalue;
uint32_t factorial[1];
if(argc != 2) {
printf("ERROR: please enter two parameters!\n");
return -1;
}
factorial[0] = atoi(argv[1]); /* string to number */
fd = open("/dev/virtio_misc", O_RDWR);
if(fd < 0) {
printf("ERROR: virtio_misc open failed!\n");
return -1;
}
retvalue = write(fd, factorial, sizeof(factorial));
if(retvalue < 0) {
printf("ERROR: write failed!\r\n");
close(fd);
return -1;
}
close(fd);
return 0;
}
(2) Kernel Space
linux-5.4.239/drivers/virtio/Makefile
......
obj-y += virtio_test.o
......
linux-5.4.239/include/uapi/linux/virtio_ids.h
#ifndef _LINUX_VIRTIO_IDS_H
#define _LINUX_VIRTIO_IDS_H
/*
* Virtio IDs
*
*/
......
#define VIRTIO_ID_TEST 45 /* virtio test */
#endif /* _LINUX_VIRTIO_IDS_H */
linux-5.4.239/include/uapi/linux/virtio_test.h
#ifndef _LINUX_VIRTIO_TEST_H_
#define _LINUX_VIRTIO_TEST_H_
#include <linux/types.h>
#include <linux/virtio_types.h>
#include <linux/virtio_ids.h>
#include <linux/virtio_config.h>
#define VIRTIO_TEST_F_CAN_PRINT 0
struct virtio_test_config {
__u32 num_pages;
__u32 actual;
};
struct virtio_test_stat {
__virtio16 tag;
__virtio64 val;
} __attribute__((packed));
#endif
linux-5.4.239/drivers/virtio/virtio_test.c
#include <linux/virtio.h>
#include <linux/virtio_test.h>
#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/init.h>
#include <linux/miscdevice.h>
#define MISC_NAME "virtio_misc"
#define MISC_MINOR 144
struct test_request {
__virtio32 arg1;
char arg2[32];
};
struct test_response {
__virtio32 ret;
};
struct virtio_test {
struct test_request req;
struct test_response res;
struct virtio_device *vdev;
struct virtqueue *factorial_vq;
};
static struct virtio_test *vt_dev;
static void print_response_data(struct virtio_test *vt)
{
printk("virtio response ret is %d\n",vt->res.ret);
}
/* Called from virtio device, in IRQ context */
static void test_request_done(struct virtqueue *vq)
{
uint32_t len;
struct virtio_test *vt;
printk(" %s called, line: %d \n", __func__, __LINE__);
do {
virtqueue_disable_cb(vq);
while ((vt = virtqueue_get_buf(vq, &len)) != NULL) {
// request packet will be completed by response packet
print_response_data(vt);
}
if (unlikely(virtqueue_is_broken(vq)))
break;
} while (!virtqueue_enable_cb(vq));
}
static void build_test_request(struct virtio_test *vt, uint32_t num)
{
vt->req.arg1 = num;
strncpy(vt->req.arg2, "hello back end!",
sizeof(vt->req.arg2));
}
static void virtio_test_submit_request(uint32_t num)
{
struct virtqueue *vq;
struct virtio_test *vt;
struct scatterlist out_sg, in_sg, *sgs[2];
int num_out = 0, num_in = 0;
vt = vt_dev;
vq = vt->factorial_vq;
build_test_request(vt, num);
sg_init_one(&out_sg, &vt->req, sizeof(vt->req));
sgs[num_out++] = &out_sg;
sg_init_one(&in_sg, &vt->res, sizeof(vt->res));
sgs[num_out + num_in++] = &in_sg;
/* We should always be able to add one buffer to an empty queue. */
virtqueue_add_sgs(vq, sgs, num_out, num_in, vt, GFP_ATOMIC);
virtqueue_kick(vq);
}
static int init_vqs(struct virtio_test *vt)
{
int err, nvqs;
struct virtqueue *vqs[1];
vq_callback_t *callbacks[] = { test_request_done };
const char * const names[] = { "virtio_test"};
nvqs = virtio_has_feature(vt->vdev, VIRTIO_TEST_F_CAN_PRINT) ? 1 : 0;
err = virtio_find_vqs(vt->vdev, nvqs, vqs, callbacks, names, NULL);
if (err)
return err;
vt->factorial_vq = vqs[0];
return 0;
}
static void remove_common(struct virtio_test *vt)
{
vt->vdev->config->reset(vt->vdev);
vt->vdev->config->del_vqs(vt->vdev);
}
static int virtio_misc_open(struct inode *inode, struct file *filp)
{
return 0;
}
static int virtio_misc_release(struct inode *inode, struct file *filp)
{
return 0;
}
static ssize_t virtio_misc_write(struct file *filp, const char __user *buf,
size_t count, loff_t *ppos)
{
int ret;
uint32_t factorial[1];
ret = copy_from_user(factorial, buf, count);
if(ret < 0)
return -EINVAL;
virtio_test_submit_request(factorial[0]);
return 0;
}
struct file_operations virtio_misc_fops = {
.owner = THIS_MODULE,
.open = virtio_misc_open,
.release = virtio_misc_release,
.write = virtio_misc_write,
};
static struct miscdevice virtio_miscdev = {
.minor = MISC_MINOR,
.name = MISC_NAME,
.fops = &virtio_misc_fops,
};
static int virttest_probe(struct virtio_device *vdev)
{
int err;
struct virtio_test *vt;
if (!vdev->config->get) {
return -EINVAL;
}
vdev->priv = vt = kmalloc(sizeof(*vt), GFP_KERNEL);
if (!vt) {
err = -ENOMEM;
goto out;
}
vt->vdev = vdev;
err = init_vqs(vt);
if (err)
goto out_free_vt;
virtio_device_ready(vdev);
vt_dev = vt;
/* misc driver registered */
err = misc_register(&virtio_miscdev);
if(err < 0) {
printk( "misc register is failed\n");
goto out_free_misc;
}
printk( "misc register has succeeded\n");
return 0;
out_free_misc:
misc_deregister(&virtio_miscdev);
out_free_vt:
kfree(vt);
out:
return err;
}
static void virttest_remove(struct virtio_device *vdev)
{
struct virtio_test *vt = vdev->priv;
remove_common(vt);
kfree(vt);
vt_dev = NULL;
misc_deregister(&virtio_miscdev);
}
static struct virtio_device_id id_table[] = {
{ VIRTIO_ID_TEST, VIRTIO_DEV_ANY_ID },
{ 0 },
};
static unsigned int features[] = {
VIRTIO_TEST_F_CAN_PRINT,
};
static struct virtio_driver virtio_test_driver = {
.feature_table = features,
.feature_table_size = ARRAY_SIZE(features),
.driver.name = KBUILD_MODNAME,
.driver.owner = THIS_MODULE,
.id_table = id_table,
.probe = virttest_probe,
.remove = virttest_remove,
};
module_virtio_driver(virtio_test_driver);
MODULE_DEVICE_TABLE(virtio, id_table);
MODULE_DESCRIPTION("Virtio test driver");
MODULE_LICENSE("GPL");
下面对 virtio_test.c 文件中的virtio_test_submit_request函数进行解释,函数如下:
static void virtio_test_submit_request(uint32_t num)
{
struct virtqueue *vq;
struct virtio_test *vt;
struct scatterlist out_sg, in_sg, *sgs[2];
int num_out = 0, num_in = 0;
vt = vt_dev;
vq = vt->factorial_vq;
build_test_request(vt, num);
sg_init_one(&out_sg, &vt->req, sizeof(vt->req));
sgs[num_out++] = &out_sg;
sg_init_one(&in_sg, &vt->resp, sizeof(vt->resp));
sgs[num_out + num_in++] = &in_sg;
/* We should always be able to add one buffer to an empty queue. */
virtqueue_add_sgs(vq, sgs, num_out, num_in, vt, GFP_ATOMIC);
virtqueue_kick(vq);
}
virtio_test_submit_request函数主要用来构建前端请求包并将数据数据添加到Vring中,然后通知QEMU后端,参数num是用户传递的一个参数,进入到函数里面, build_test_request 函数用来构建请求包。
sg_init_one(&out_sg, &vb->req, sizeof(vb->req));
sgs[num_out++] = &out_sg;
sg_init_one(&in_sg, &vb->res, sizeof(vb->res));
sgs[num_out + num_in++] = &in_sg;
Virtio前后端数据传输是通过Linux内核中的scatter-gather(SG)列表来进行管理的。scatter-gather列表是一种数据结构,用于将多个不连续的内存块组合成一个逻辑上的连续块,以便进行数据传输。
sg_init_one
函数初始化两个SG条目out_sg
和in_sg
,分别指向vb->req
和vb->res
,并设置其大小为sizeof(vb->req)
和sizeof(vb->res)
。vb->req
的内容即为一个请求数据包,用于写入到后端设备,而vb->res
则是用来存放从设备接收到的数据。
sgs[num_out++] = &out_sg
是将out_sg
的地址添加到sgs
数组中。num_out
是一个索引,表示添加到列表中的输出SG条目的数量。通过num_out++
,确保下一个输出SG条目将被添加到数组的下一个位置。
sgs[num_out + num_in++] = &in_sg
则是将in_sg的地址
添加到sgs
数组中,添加的位置是基于已添加的num_out
和num_in
之和,这里num_in 初始化为 0,所以 in_sg
被添加到了out_sg
的后面,在这里sgs
数组的前半部分也就是sgs[0]用于存储输出SG条目,而后半部分sgs[1]用于存储输入SG条目。通过num_in++
,确保下一个输入SG条目被添加到sgs的适当位置。
virtqueue_add_sgs(vq, sgs, num_out, num_in, vt, GFP_ATOMIC);
vq
: 指向一个virtqueue
结构体的指针,这个结构体就是host和guest之间通信的一个虚拟队列。
sgs
: 指向一个scatterlist
结构体数组的指针,表示scatterlist
元素指向内存中的一个物理地址非连续区域,也就是上面填充的sgs[2]数组。
num_out
: 指定了sgs
数组中用于输出的scatterlist
的数量。
num_in
: 指定了sgs
数组中用于输入的scatterlist
的数量。
vt
: struct virtio_test 类型的一个结构体。
GFP_ATOMIC
: 表示这个操作应该在原子上下文中进行,不能睡眠(即不能等待I/O操作或内存分配)。
virtqueue_add_sgs 函数主要将一组散列列表添加到虚拟队列vq
中,而在virtqueue_add_sgs函数中又会调用virtqueue_add函数,用来将新的数据更新到 vring_virtqueue->vring的具体实现。
最后在调用 virtqueue_kick 函数通知QEMU 后端有数据更新了。
2、Virtio 后端
qemu-7.2.0/hw/virtio/meson.build
......
virtio_ss.add(when: 'CONFIG_VIRTIO_TEST', if_true: files('virtio-test.c'))
......
qemu-7.2.0/hw/virtio/Kconfig
config VIRTIO_TEST
bool
default y
depends on VIRTIO
qemu-7.2.0/include/standard-headers/linux/virtio_ids.h
#ifndef _LINUX_VIRTIO_IDS_H
#define _LINUX_VIRTIO_IDS_H
/*
* Virtio IDs
*
*/
......
#define VIRTIO_ID_TEST 45 /* virtio test */
......
#endif /* _LINUX_VIRTIO_IDS_H */
qemu-7.2.0/include/standard-headers/linux/virtio_test.h
#ifndef _LINUX_VIRTIO_TEST_H
#define _LINUX_VIRTIO_TEST_H
#include "standard-headers/linux/types.h"
#include "standard-headers/linux/virtio_types.h"
#include "standard-headers/linux/virtio_ids.h"
#include "standard-headers/linux/virtio_config.h"
#define VIRTIO_TEST_F_CAN_PRINT 0
struct virtio_test_config {
uint32_t num_pages;
uint32_t actual;
uint32_t event;
};
struct virtio_test_stat {
__virtio16 tag;
__virtio64 val;
} QEMU_PACKED;
#endif
qemu-7.2.0/include/hw/virtio/virtio-test.h
#ifndef QEMU_VIRTIO_TEST_H
#define QEMU_VIRTIO_TEST_H
#include "standard-headers/linux/virtio_test.h"
#include "hw/virtio/virtio.h"
#define TYPE_VIRTIO_TEST "virtio-test-device"
#define VIRTIO_TEST(obj) \
OBJECT_CHECK(VirtIOTest, (obj), TYPE_VIRTIO_TEST)
typedef struct VirtIOTest {
VirtIODevice parent_obj;
VirtQueue *ivq;
uint32_t host_features;
QEMUTimer *stats_timer;
uint32_t actual;
uint32_t event;
uint32_t num_pages;
size_t stats_vq_offset;
VirtQueueElement *stats_vq_elem;
} VirtIOTest;
#endif
qemu-7.2.0/hw/virtio/virtio.c
const char *virtio_device_names[] = {
......
[VIRTIO_ID_TEST] = "virtio-test"
};
qemu-7.2.0/hw/virtio/virtio-test.c
#include "qemu/osdep.h"
#include "qemu/log.h"
#include "qemu/iov.h"
#include "hw/virtio/virtio.h"
#include "sysemu/kvm.h"
#include "sysemu/hax.h"
#include "exec/address-spaces.h"
#include "qapi/error.h"
#include "qapi/qapi-events-misc.h"
#include "qapi/visitor.h"
#include "qemu/error-report.h"
#include "hw/virtio/virtio-bus.h"
#include "hw/virtio/virtio-access.h"
#include "migration/migration.h"
#include "hw/virtio/virtio-test.h"
static uint32_t Queue_Size = 128;
struct test_request {
uint32_t arg1;
char arg2[32];
};
struct test_response {
uint32_t ret;
};
static uint32_t factorial(uint32_t n) {
uint32_t result = 1;
for (uint32_t i = 1; i <= n; i++) {
result *= i;
}
return result;
}
static void print_req_and_build_resp_pack(struct test_request *req, struct test_response *res)
{
qemu_log("QEMU: >>> get arg1 [ %d ] form the front end <<<\n", req->arg1);
qemu_log("QEMU: >>> get arg2 [ %s ] form the front end <<<\n", req->arg2);
res->ret = factorial(req->arg1);
}
static void virtio_test_handle_output(VirtIODevice *vdev, VirtQueue *vq)
{
struct test_request req;
struct test_response res;
VirtQueueElement *elem;
size_t offset = 0;
for (;;) {
elem = virtqueue_pop(vq, sizeof(VirtQueueElement));
if (!elem)
return;
if (!iov_to_buf(elem->out_sg, elem->out_num, offset, &req, sizeof(req))) {
qemu_log("QEMU ERROR: iov_to_buf function failed.\n");
virtqueue_detach_element(vq, elem, 0);
continue;
}
print_req_and_build_resp_pack(&req, &res);
iov_from_buf(elem->in_sg, elem->in_num, offset, &res, sizeof(res));
virtqueue_push(vq, elem, sizeof(res));
virtio_notify(vdev, vq);
g_free(elem);
}
}
static void virtio_test_get_config(VirtIODevice *vdev, uint8_t *config_data)
{
VirtIOTest *dev = VIRTIO_TEST(vdev);
struct virtio_test_config config;
config.actual = cpu_to_le32(dev->actual);
config.event = cpu_to_le32(dev->event);
memcpy(config_data, &config, sizeof(struct virtio_test_config));
}
static void virtio_test_set_config(VirtIODevice *vdev,
const uint8_t *config_data)
{
VirtIOTest *dev = VIRTIO_TEST(vdev);
struct virtio_test_config config;
memcpy(&config, config_data, sizeof(struct virtio_test_config));
dev->actual = le32_to_cpu(config.actual);
dev->event = le32_to_cpu(config.event);
}
static uint64_t virtio_test_get_features(VirtIODevice *vdev, uint64_t f,
Error **errp)
{
VirtIOTest *dev = VIRTIO_TEST(vdev);
f |= dev->host_features;
virtio_add_feature(&f, VIRTIO_TEST_F_CAN_PRINT);
return f;
}
static void virtio_test_device_realize(DeviceState *dev, Error **errp)
{
VirtIODevice *vdev = VIRTIO_DEVICE(dev);
VirtIOTest *s = VIRTIO_TEST(dev);
virtio_init(vdev, VIRTIO_ID_TEST, sizeof(struct virtio_test_config));
s->ivq = virtio_add_queue(vdev, Queue_Size, virtio_test_handle_output);
}
static void virtio_test_device_unrealize(DeviceState *dev)
{
VirtIODevice *vdev = VIRTIO_DEVICE(dev);
virtio_cleanup(vdev);
}
static int virtio_test_post_load_device(void *opaque, int version_id)
{
return 0;
}
static const VMStateDescription vmstate_virtio_test_device = {
.name = "virtio-test-device",
.version_id = 1,
.minimum_version_id = 1,
.post_load = virtio_test_post_load_device,
.fields = (VMStateField[]) {
VMSTATE_UINT32(actual, VirtIOTest),
VMSTATE_END_OF_LIST()
},
};
static const VMStateDescription vmstate_virtio_test = {
.name = "virtio-test",
.minimum_version_id = 1,
.version_id = 1,
.fields = (VMStateField[]) {
VMSTATE_VIRTIO_DEVICE,
VMSTATE_END_OF_LIST()
},
};
static Property virtio_test_properties[] = {
DEFINE_PROP_END_OF_LIST(),
};
static void virtio_test_class_init(ObjectClass *klass, void *data)
{
DeviceClass *dc = DEVICE_CLASS(klass);
VirtioDeviceClass *vdc = VIRTIO_DEVICE_CLASS(klass);
dc->props_ = virtio_test_properties;
dc->vmsd = &vmstate_virtio_test;
set_bit(DEVICE_CATEGORY_MISC, dc->categories);
vdc->realize = virtio_test_device_realize;
vdc->unrealize = virtio_test_device_unrealize;
vdc->get_config = virtio_test_get_config;
vdc->set_config = virtio_test_set_config;
vdc->get_features = virtio_test_get_features;
vdc->vmsd = &vmstate_virtio_test_device;
}
static const TypeInfo virtio_test_info = {
.name = TYPE_VIRTIO_TEST,
.parent = TYPE_VIRTIO_DEVICE,
.instance_size = sizeof(VirtIOTest),
.class_init = virtio_test_class_init,
};
static void virtio_register_types(void)
{
type_register_static(&virtio_test_info);
}
type_init(virtio_register_types)
下面对virtio-test.c文件中的virtio_test_handle_output函数进行分析,如下:
static void virtio_test_handle_output(VirtIODevice *vdev, VirtQueue *vq)
{
struct test_request req;
struct test_response res;
VirtQueueElement *elem;
size_t offset = 0;
for (;;) {
elem = virtqueue_pop(vq, sizeof(VirtQueueElement));
if (!elem)
return;
if (!iov_to_buf(elem->out_sg, elem->out_num, offset, &req, sizeof(req))) {
qemu_log("QEMU ERROR: iov_to_buf function failed.\n");
virtqueue_detach_element(vq, elem, 0);
continue;
}
print_req_and_build_resp_pack(&req, &res);
iov_from_buf(elem->in_sg, elem->in_num, offset, &res, sizeof(res));
virtqueue_push(vq, elem, sizeof(res));
virtio_notify(vdev, vq);
g_free(elem);
}
}
VirtQueueElement 结构体:
typedef struct VirtQueueElement
{
unsigned int index;
unsigned int len;
unsigned int ndescs;
unsigned int out_num;
unsigned int in_num;
hwaddr *in_addr;
hwaddr *out_addr;
struct iovec *in_sg;
struct iovec *out_sg;
} VirtQueueElement;
struct iovec {
void *iov_base;
size_t iov_len;
};
VirtQueueElement 结构体如上所示,in_addr和 out_addr保存的是guest的物理地址,而in_sg和out_sg中的地址是host的虚拟地址,物理地址和虚拟地址之间需要进行映射。
index:
记录该buffer的首个物理内存块对应的描述符在描述符表中的下标,因为一个buffer数据可能由多个物理内存保存。
out_num/in_num:
表示输出和输入块的数量。一个buffer可能包含可读区和可写区,因为一个buffer由多个物理块组成,有的物理块是可读而有的物理块是可写,out_num表示可读块的数量,而in_num表示可写块的数量。
in_addr/out_addr:
记录的是可读块和可写块的物理地址(客户机的物理地址)。因为in_addr/out_addr是客户机的物理地址,如果host要访问这些地址,则需要将Guest物理地址映射成Host的虚拟地址。
in_sg/out_sg:
根据上面的分析,in_sg和out_sg就是保存的对应Guest物理块在Host的虚拟地址和长度。
elem = virtqueue_pop(vq, sizeof(VirtQueueElement));
virtqueue_pop函数主要功能为:
1、以 vq->last_avail_idx为索引从VRingAvail的ring数组中获取一个buffer head索引,并赋值到elem.index,然后获取各个guest物理buffer的相关信息。
2、将可写的物理buffer地址(客户机物理地址)记录到in_addr数组中,而可读的记录到out_addr数组中,并记录in_num和out_num,直到最后一个desc记录完毕。
3、获取完成后再将in_addr和out_addr映射成虚拟地址,并记录到in_sg和out_sg数组中,这样才可以在host中访问到。
调用virtqueue_pop函数之后,QEMU后端就已经获取了buffer的相关信息,继续分析
if (!iov_to_buf(elem->out_sg, elem->out_num, offset, &req, sizeof(req))) {
......
print_req_and_build_resp_pack(&req, &res);
iov_from_buf(elem->in_sg, elem->in_num, offset, &res, sizeof(res));
iov_to_buf
函数用于将 iovec
结构体数组中的数据复制到用户提供的缓冲区中,函数参数解释如下:
elem->out_sg
:指向iovec
结构体数组的指针。
elem->out_num
:指定了elem->out_sg
数组中iovec
结构体的数量。
offset
:指定了从哪个位置开始复制数据。
&req
:存放Guest
前端request
的缓冲区指针,把从iovec
数组中读取的数据复制到这个缓冲区中。
sizeof(req)
:这个参数指定了目标缓冲区req
的大小,即函数最多可以复制多少字节到req
中。
函数会从 elem->out_sg
指向的 iovec
数组开始,跳过 offset
指定的字节数,然后将数据复制到 req
指向的缓冲区中,直到达到 req
的大小限制或所有 iovec
中的数据都被复制完毕为止。
经过前面的分析,输出项out_sg
指向的地址的内容就读取到了req
结构体中,然后读取req结构体中的内容即可读取前端的数据,在这里是调用print_req_and_build_resp_pack函数,获取req中的数据计算阶乘,并初始化好struct test_response
为返回前端数据做准备。
iov_from_buf(elem->in_sg, elem->in_num, offset, &resp, sizeof(resp));
elem->in_sg
:指向一个iovec
数组的指针,用于存储数据的分段信息。
elem->in_num
:表示elem->in_sg
数组中可以使用的iovec的数量。
offset
:从缓冲区开始复制的偏移量。
&resp
:将数据从res复制到iov向量列表中去。
sizeof(resp)
:res
的大小。
和iov_to_buf函数的操作相反,iov_from_buf
函数是将一段数据buf(res)的内容复制到由 iovec
数组描述的内存区域中去,也就是elem->in_sg
中。
到目前为止就完成了根据前端传递来的数据计算阶乘,并将response包放入了in_sg中,然后调用virtqueue_push函数取消之前物理内存映射到虚拟内存的操作,并更新vring_ used表,如下:
void virtqueue_push(VirtQueue *vq, const VirtQueueElement *elem,
unsigned int len)
{
RCU_READ_LOCK_GUARD();
virtqueue_fill(vq, elem, len, 0);
virtqueue_flush(vq, 1);
}
最后再调用 virtio_notify 函数告诉前端传递过来的数据已经处理完毕了,然后前端再做一些其它的处理。
三、运行
在运行 qemu 时需要加上 -device virtio-test-device
参数,例如:
......
-machine virt \
-machine gic_version=3 \
-smp 4 \
-m 1024 \
-display none -nographic \
-device virtio-test-device \
......
如果编译成功运行 User Space 程序即可,运行结果如下: