virtio内容众多,代码分布于qemu,linux,dpdk等中,而且分为frontend和backend,可以运行于userspace也可以运行于kernelspace,极其难以理解,不看代码只看原理性文档往往流于表面,只有真正看懂了代码才能理解virtio。
以qemu和linux中的virtio-net举例分析代码,这儿只分析qemu部分virtio代码,在qemu中创建一个virtio-net设备,tap作为backend,有2个queue,那么qemu中tx和rx各2个,再加1个controll queue就得创建5个queue了
代码语言:javascript复制-netdev tap,id=hostnet0,queues=2
-device virtio-net-pci,host_mtu=1450,mq=on,vectors=5,netdev=hostnet0,id=net0,mac=fa:16:3e:d8:fe:81,bus=pci.0,addr=0x3
我们先看数据结构NetClientState,重点关注peer和incoming_queue
代码语言:javascript复制struct NetClientState {
NetClientInfo *info;
int link_down;
QTAILQ_ENTRY(NetClientState) next;
NetClientState *peer;
NetQueue *incoming_queue;
char *model;
char *name;
char info_str[256];
unsigned receive_disabled : 1;
NetClientDestructor *destructor;
unsigned int queue_index;
unsigned rxfilter_notify_enabled:1;
int vring_enable;
int vnet_hdr_len;
bool is_netdev;
QTAILQ_HEAD(, NetFilterState) filters;
};
先看qemu中的参数-netdev,创建了2个TAPState,每个TAPState包含一个NetClientState,函数qemu_net_client_setup参数peer为空,所以创建的ncs中peer为空。
代码语言:javascript复制net_tap_fd_init
└─qemu_new_net_client
└─qemu_net_client_setup//这个函数参数peer为NULL
再看qemu的参数-device中有netdev=hostnet0,而-netdev中有id=hostnet0,根据name找到了刚才tap创建的2个NetClientState和queue个数为2
代码语言:javascript复制typedef struct NICPeers {
NetClientState *ncs[MAX_QUEUE_NUM];
int32_t queues;
} NICPeers;
typedef struct NICConf {
MACAddr macaddr;
NICPeers peers;
int32_t bootindex;
} NICConf;
#define DEFINE_NIC_PROPERTIES(_state, _conf)
DEFINE_PROP_MACADDR("mac", _state, _conf.macaddr),
DEFINE_PROP_NETDEV("netdev", _state, _conf.peers)
static Property virtio_net_properties[] = {
DEFINE_NIC_PROPERTIES(VirtIONet, nic_conf),
}
set_netdev
└─qemu_find_net_clients_excep
根据queue个数生成2个NetClientState,现在有2个NetClientState,成对peer互相指向对方
代码语言:javascript复制virtio_net_device_realize
└─qemu_new_nic
└─qemu_net_client_setup//和tap init相比,peer不为NULL,是tap的NetClientState
qemu对virtio-net-pci的初始化
代码语言:javascript复制virtio_device_realize
├─virtio_net_pci_realize
├─virtio_bus_device_plugged
| ├─virtio_pci_device_plugged//初始化virtio-pci设备
| └─vdev->dma_as = &address_space_memory;//分配dma_as
└─memory_listener_register
└─virtio_memory_listener_commit
└─virtio_init_region_cache
virtio_net_device_realize
├─virtio_init
├─virtio_net_add_queue
| └─virtio_add_queue//rx和tx的handle_output分别是virtio_net_handle_rx和virtio_net_handle_tx_bh
├─qemu_bh_new//生成一个qemu bh virtio_net_tx_bh
└─virtio_add_queue//控制qemu是virtio_net_handle_ctrl
guest把分配的vring地址传递给qemu
代码语言:javascript复制virtio_pci_common_write
└─virtio_queue_set_rings
└─virtio_init_region_cache
└─address_space_cache_init//这个函数等qemu内存虚拟化时一块分析
给kvm注册一个eventfd,等guest kick时,kvm通知qemu进程
代码语言:javascript复制virtio_pci_common_write
└─virtio_pci_start_ioeventfd
└─virtio_bus_start_ioeventfd
└─virtio_device_start_ioeventfd_impl
├─virtio_bus_set_host_notifier
| └─virtio_pci_ioeventfd_assign
| └─memory_region_add_eventfd
└─event_notifier_set_handler(virtio_queue_host_notifier_read)
guest里的virtio-net通知tx或者rx写mmio,带着vector号,kvm通知qemu
代码语言:javascript复制virtio_pci_notify_write
└─virtio_queue_notify
├─handle_output
└─event_notifier_set
virtio_queue_host_notifier_read
└─virtio_queue_notify_vq
└─handle_output
不管是发送还是接收,都是guest设置好vring中的address,是guest的physical address,需要qemu调用address_space_map转换成自己的virtual address。
收发包的原理就是tap收到发往guest,从guest收到发往tap,tap和virtio-net-pci peer互相指,从自己NetClientState的peer找到对端的NetClientState,然后找到NetClientState中的incoming_queue,incoming_queue中deliver调用receive函数。
qemu读tap,然后调用virtio_net_receive,如果virtio_net_receive不能receive,等guest kick时再用virtio_net_handle_rx收包。
代码语言:javascript复制tap_send
└─qemu_send_packet_async
└─qemu_send_packet_async_with_flags
└─qemu_net_queue_send
└─调用到virtio-net的virtio_net_receive
guest kick时接收,virtio_net_handle_rx is handle_outpu,rxvq是VRING_DESC_F_WRITE,DMA_DIRECTION_FROM_DEVICE,站在guest角度看问题,从qemu到guest内存
代码语言:javascript复制virtio_net_handle_rx
└─qemu_flush_queued_packets
└─qemu_flush_or_purge_queued_packets
└─qemu_net_queue_flush
└─qemu_net_queue_deliver
└─qemu_deliver_packet_iov
└─nc_sendv_compat
└─virtio_net_receive
virtio_net_receive_rcu
├─virtio_net_process_rss选择接收的queue
| └─virtqueue_pop
| └─virtqueue_split_pop
| └─virtqueue_map_desc
| └─dma_memory_map
| └─address_space_map
├─virtqueue_fill
├─virtqueue_flush
└─virtio_notify
guest kick时发送virtio_net_handle_tx_bh is handle_output
代码语言:javascript复制#guest kick时发送virtio_net_handle_tx_bh is handle_output
virtio_net_handle_tx_bh
└─virtio_net_tx_bh
├─virtio_net_flush_tx
| ├─virtqueue_pop
| | └─virtqueue_split_pop
| | └─virtqueue_map_desc
| | └─dma_memory_map
| | └─address_space_map
| ├─qemu_sendv_packet_async
| | └─qemu_net_queue_send_iov
| | └─qemu_net_queue_deliver_iov
| | └─qemu_deliver_packet_iov
| | └─调用到了对端的peer_receive就是tap_receive
| | └─tap_write_packet
| └─qemu_net_queue_flush
| └─qemu_net_queue_deliver
└─bh的回调函数virtio_net_tx_complete
├─virtqueue_push
| ├─virtqueue_fill
| └─virtqueue_flush
| └─vring_used_idx_set
└─virtio_notify
记录dirty page用于live migration
代码语言:javascript复制virtqueue_fill
├─virtqueue_unmap_sg
| └─dma_memory_unmap
| └─address_space_unmap
| └─invalidate_and_set_dirty
└─virtqueue_split_fill
└─vring_used_write
└─address_space_write_cached
├─flatview_write_continue
| └─invalidate_and_set_dirty
└─address_space_cache_invalidate
- virtio-net tx时如何选择queue号的?
tx时virtnet_select_queue cpu和queue有一个对应关系,是virtnet_set_affinity分配好的,或者用内核的xps确定queue号。
- rx时又如何选择queue呢?
virtio_net_process_rss选择接收的queue。rx时看virtio-net-pci用什么中断模块,如果用msix可能每个queue有自己的vector号,中断来了直接定位queue,如果共享只能遍历所有queue了。
- 那么vhost-net/vhost-user/vhost-vdpa时有时怎么处理NetClientState的?
和原来一样没有什么变化
- qemu做virtio写元数据时如何和guest保证cache coherence?
一个host cpu写,另一个guest host已经读到cache中,怎么搞?
address_space_cache_invalidate
- qemu做virtio写virtio vring里buf时如果更新dirty page的?
invalidate_and_set_dirty
等补充内容
vIOMMU时virtio地址怎么转换的?