这里记录一下virtiofsd用到的重要的vhost库
1. vhost
vhost库支持三种使用方式:
- vhost: linux 内核实现的
- vhost-user: 用户态实现的
- vDPA: 硬件实现的
架构图:
在vhost场景下
virtio设备在linux kernel里面, vmm通过ioctl管理这个virtio设备, 比如说vhost-net, vhost-scsi, vhost-vsock. 建立好vqueue之后, guest的driver和host kernel的vhost-net直接交换数据, 没有通过vmm, 没有syscall.
在vhost-user场景下
VMM里面的Master负责建立vqueue, 使用vqueue的是slave. master和slave之间通过unix socket来通信, 在virtiofsd的实现里, 这个socket由slave来listen.
因为vhost-user使用的是trait抽象, 这个库并不依赖底层的vhost-user实现.
2. vhost-user-backend
这个crate依赖
- vhost
- virtio-bindings
- virtio-queue
vhost-user-backend实现了vhost-user协议的service框架, 用户可以用它实现vhost-user的服务. 提供的API如下
- daemon控制对象(
VhostUserDaemon
), 用于开始/停止daemon - vhost-user backend trait(
VhostUserBackendMut
), 用来处理vhost-user控制消息和virtio消息 - 访问vqueue的trait(
VringT
), 有三个实现VringState
,VringMutex
andVringRwLock
.
3. virtio-queue
virtio规范里定义了两类vqueue的格式, split和packed. 这里只支持split模式.
virtio-queue这个crate是给virtio device用的, 比如virtio-blk, 或者virtio-vsock.
3.1. 设备侧使用virtio queue
virtio设备需要有类似物理设备的寄存器, 包括下面的信息:
- 设备状态"寄存器"
- 功能bit flag
- 通知"寄存器"
- 一个或多个vqueue
在split模式下, vqueue包括三部分:
- 描述符表
- available ring
- used ring
在启动VM之前, VMM会做下面的配置:
- 初始化vqueue
- 注册本device到MMIO总线, 这样guest driver可以通过MMIO读写这个设备
- 注册关联的fd来进行中断注入, 即device通知driver一些event
VM启动后, guest driver就开始和VMM模拟的device来交互了:
- 协商支持的feature
- 协商vqueue参数, 比如size, 状态, 地址等
- 激活vqueue, VMM可以在这里才创建vqueue的handler线程开始干活
3.2. 通知
vqueue的操作逻辑是guest driver产生descriptor, vmm的virtio device handler来消费descriptor. descriptor用链表管理.一方操作完成后, 要通知另外一方. 在KVM辅助下,
- 比如driver通知device, 就是写个notification地址, 这个地址在初始化的时候就已经被KVM关联到了ioevent fd, 那么写地址相当于写fd, 这个fd在VMM的device代码里有handler监听, 通知就过去了.
- device通知driver叫中断注入, 也是通过KVM关联的irqfd完成的.
3.3. 数据传输
按照上图所示, 数据流程如下:
- driver有数据(buffer)要写, driver准备descriptor, driver写descriptor到绿色descriptor table, 得到index
- driver写上面得到的index到紫色available ring
- driver更新available ring的idx
- driver通知device
- device先读available ring 的idx
- device从avaliable ring的idx得到descriptor的index
- device从descriptor table读index对应的descriptor
- device消费完这个descriptor之后, 比如把数据发送到tap设备, device写used ring. 内容包括刚才这个descriptor的idx和已经写了的字节数
- device更新used ring的idx
- deice发used buffer事件通知到driver
3.4. 描述符格式
上面绿色ring存储的是描述符, 描述符有4个域:
- addr: 指向真正的数据buffer
- len: 数据buffer的长度
- flag: 一些有用的flag, 比如buffer是不是device writable
- next: 指向下个描述符
3.5. vqueue对象
- Queue → it is used for the single-threaded context.
- QueueSync → it is used for the multi-threaded context, and is simply a wrapper over an
Arc<Mutex<Queue>>
. - QueueState -> 用于保存和回复queue状态
3.6. 其他抽象
- Descriptor → which mostly offers accessors for the members of the Descriptor.
- DescriptorChain → provides accessors for the DescriptorChain’s members and an Iterator implementation for iterating over the DescriptorChain, there is also an abstraction for iterators over just the device readable or just the device writable descriptors (DescriptorChainRwIter).
- AvailIter - is a consuming iterator over all available descriptor chain heads in the queue.
3.7. 通知抑制
有时候不需要每个动作都通知. 比如当driver 处理used ring期间, 就不需要再收到device发来的used buffer事件. 规范里通知抑制是这样描述的:
vQueue用下面的步骤来处理available ring:
- device 先关闭通知来让driver直到device正在处理available ring, 此时device不想要事件. device调用
Queue::disable_notification
来关闭通知. - device用AvailIter 来处理available ring
- device调用
Queue::enable_notification
来打开通知
下面这段没看明白, vqueue还能用在driver端? driver端不是在guest里面的driver吗?
On the driver side, the
Queue
provides theneeds_notification
method which should be used each time the device adds a new entry to the used ring. Depending on theused_event
value and on the last used value (signalled_used
),needs_notification
returns true to let the device know it should send a notification to the guest.
4. vm-memory
VMM里面的各种组件, 比如boot loader, 各种virtio backend, 都需要访问VM的物理地址. 这个vm-memory提供了trait抽象, 把VM内存的提供者和消费者分离开来.
vm-memory的设计原则:
- 给使用者定义了访问VM物理内存的API
- 但没给提供者定义如何提供物理内存的API, 这样提供者实现的时候更灵活
4.1. 地址空间抽象
AddressValue
: stores the raw value of an address. Typicallyu32
,u64
orusize
are used to store the raw value.Address
: implementation ofAddressValue
.Bytes
: trait for volatile access to memory. TheBytes
trait can be parameterized with types that represent addresses, in order to enforce that addresses are used with the right "kind" of volatile memory.VolatileMemory
: basic implementation of volatile access to memory. ImplementsBytes<usize>
.
这里只定义了消费地址空间的方法, 并没有定义管理地址空间(create, delete, insert, remove)的方法
4.2. VM物理地址空间
GuestAddress
: represents a guest physical address (GPA). guest物理地址空间, 应该就是u64GuestMemoryRegion
: represents a continuous region of the VM memory. 连续的地址区间GuestMemory
: represents a collection ofGuestMemoryRegion
objects.- hide the detail of accessing physical addresses (for example complex hierarchical structures).
- map an address request to a
GuestMemoryRegion
object and relay the request to it. - handle cases where an access request is spanning two or more
GuestMemoryRegion
objects.
4.3. mmap实现了GuestMemory
这里的mmap
实现, 把VM的物理地址mapping到当前进程.
MmapRegion
: implementation of mmap a continuous range of physical memory with methods for accessing the mapped memory.GuestRegionMmap
: implementation ofGuestMemoryRegion
providing a wrapper used to map VM's physical address into a(mmap_region, offset)
tupleGuestMemoryMmap
: implementation ofGuestMemory
that manages a collection ofGuestRegionMmap
objects for a VM.
使用时
let guest_memory_mmap: GuestMemoryMmap = ...
let addr: GuestAddress = ...
let buf = &mut [0u8; 5];
let result = guest_memory_mmap.write(buf, addr);