StratoVirt 的 virtio-blk 设备是如何实现的?

Lansie
发布于 2022-5-7 17:46
浏览
0收藏

StratoVirt 是开源在 openEuler 社区的面向云数据中心的企业级虚拟化平台,具备轻量低噪、软硬协同、Rust 语言级安全等关键技术竞争优势。

virtio-blk 是虚拟化 KVM 平台下虚拟磁盘的一种实现方式,本质上为一种半模拟技术。virtio-blk 设备中采用 io_event_fd 进行前端到后端通知,采用中断注入方式实现后端到前端的通知,并通过 IO 环(vring) 进行数据的共享。

基本原理
IO 总体流程可以分为以下几个步骤,如下图所示:

 StratoVirt 的 virtio-blk 设备是如何实现的?-鸿蒙开发者社区
1.Guest 内部下发 IO 请求,通过 Eventfd 唤醒 StratoVirt 的 IO 线程
2.IO 主线程从共享环中取出 IO 请求,下发异步 IO 系统调用
3.Host 执行具体 IO 操作
4.IO 处理完成后唤醒 IO 线程
5.IO 线程 IO 结果生成响应放入共享环,向 Guest 注入中断
6.Guest 内部处理 IO 中断
具体实现
virtio-blk 的具体代码实现位于 StratoVirt 项目的 /virtio/src/block.rs 文件中,相关细节可参考代码理解。代码架构如下:

virtio
├── Cargo.toml
└── src
    ├── balloon.rs
    ├── block.rs
    ├── console.rs
    ├── lib.rs
    ├── net.rs
    ├── queue.rs
    ├── rng.rs
    ├── vhost
    │   ├── kernel
    │   │   ├── mod.rs
    │   │   ├── net.rs
    │   │   └── vsock.rs
    │   ├── user
    │   │   ├── client.rs
    │   │   ├── message.rs
    │   │   ├── mod.rs
    │   │   └── sock.rs
    │   └── mod.rs
    └── virtio_mmio.rs
    └── virtio_pci.rs

StratoVirt 的 virtio crate 中的 lib.rs 中定义了为所有 virtio 设备定义的 VirtioDevice Trait。virtio-blk 设备实现了该 Trait。

当前 StratoVirt 中 virtio-blk 设备支持一个队列:request_queue。该队列负责 block 设备的初始化以及 IO 命令传输。StratoVirt 为该队列配置了对应的 event_fd 和 handler 函数。

定义 BlockIoHandler 作为 virtio-blk 设备事件处理的主体。

/// Control block of Block IO.
struct BlockIoHandler {
    /// The virtqueue.
    queue: Arc<Mutex<Queue>>,
    /// Eventfd of the virtqueue for IO event.
    queue_evt: EventFd,
    /// The address space to which the block device belongs.
    mem_space: Arc<AddressSpace>,
    /// The image file opened by the block device.
    disk_image: Option<Arc<File>>,
    /// The number of sectors of the disk image.
    disk_sectors: u64,
    /// Serial number of the block device.
    serial_num: Option<String>,
    /// if use direct access io.
    direct: bool,
    /// Aio context.
    aio: Option<Box<Aio<AioCompleteCb>>>,
    /// Bit mask of features negotiated by the backend and the frontend.
    driver_features: u64,
    /// The receiving half of Rust's channel to receive the image file.
    receiver: Receiver<SenderConfig>,
    /// Eventfd for config space update.
    update_evt: RawFd,
    /// Eventfd for device deactivate.
    deactivate_evt: RawFd,
    /// Callback to trigger an interrupt.
    interrupt_cb: Arc<VirtioInterrupt>,
    /// thread name of io handler
    iothread: Option<String>,
    /// Using the leak bucket to implement IO limits
    leak_bucket: Option<LeakBucket>,
}

其中包含了上述的一个 virtio 队列即 queue 变量,以及对应的触发事件描述符 (EventFd) queue_evt。队列使用了 Mutex 锁,保证在同一时刻只有一个使用者会对该队列进行操作,确保了多线程环境下的数据安全。

当该队列的事件描述符被触发只有,对应的处理函数 process_queue 会被调用。接下来结合代码讲解一下具体处理逻辑。

当该 handler 函数被触发时,首先从 virtio 队列中取出对应的元素,随后按照特定格式将取出的队列元素组合为 block 设备的 IO 请求。随后循环遍历执行 IO 请求,根据执行结果决定是否需要向 Guest 注入中断通知。

fn process_queue(&mut self) -> Result<bool> {
    /* 略 */
    // 从队列中取出元素
    while let Ok(elem) = queue.vring.pop_avail(&self.mem_space, self.driver_features) {
        /* 略 */
        // 将队列元素组合为IO请求
        match Request::new(&self.mem_space, &elem) {
            Ok(req) => {
                match req.out_header.request_type {
                    VIRTIO_BLK_T_IN | VIRTIO_BLK_T_OUT => {
                        last_aio_req_index = req_index;
                    }
                    _ => {}
                }
                req_queue.push(req);
                req_index += 1;
                done = true;
            }
            Err(ref e) => {
                //  If it fails, also need to free descriptor table entry.
                queue
                    .vring
                    .add_used(&self.mem_space, elem.index, 0)
                    .chain_err(|| "Failed to add used ring")?;
                need_interrupt = true;

                error!(
                    "failed to create block request, {}",
                    error_chain::ChainedError::display_chain(e)
                );
            }
        };
    }

    if let Some(disk_img) = self.disk_image.as_mut() {
        req_index = 0;
        // 循环遍历所有IO请求
        for req in merge_req_queue.iter() {
            if let Some(ref mut aio) = self.aio {
                let rw_len = match req.out_header.request_type {
                    VIRTIO_BLK_T_IN => u32::try_from(req.data_len)
                        .chain_err(|| "Convert block request len to u32 with overflow.")?,
                    _ => 0u32,
                };

                let aiocompletecb = AioCompleteCb::new(
                    self.queue.clone(),
                    self.mem_space.clone(),
                    req.desc_index,
                    rw_len,
                    req.in_header,
                    Some(self.interrupt_cb.clone()),
                    self.driver_features,
                );
                // 执行IO请求
                match req.execute(
                    aio,
                    disk_img,
                    self.disk_sectors,
                    &self.serial_num,
                    self.direct,
                    last_aio_req_index == req_index,
                    aiocompletecb,
                ) {
                    Ok(v) => {
                        if v == 1 {
                            /* 略 */
                            // 更新virtio队列
                            self.queue.lock().unwrap().vring.add_used(
                                &self.mem_space,
                                req.desc_index,
                                1,
                            ).chain_err(|| "Failed to add the request for block with device id to used ring")?;
                            // 如果执行成功,判断是否需要对Guest注入中断
                            if self
                                .queue
                                .lock()
                                .unwrap()
                                .vring
                                .should_notify(&self.mem_space, self.driver_features)
                            {
                                need_interrupt = true;
                            }
                        }
                    }
                    Err(ref e) => {
                        /* 略 */
                    }
                }
                req_index += 1;
            }
        }
    } else if !merge_req_queue.is_empty() {
        /* 略 */
    }

    if need_interrupt {
        // 如果需要对Guest注入中断,则调用对应总线的中断注入函数,通知Guest
        (self.interrupt_cb)(
            &VirtioInterruptType::Vring,
            Some(&self.queue.lock().unwrap()),
        )
        .chain_err(|| ErrorKind::InterruptTrigger("block", VirtioInterruptType::Vring))?;
    }

    Ok(done)
}​

virtio-blk 设备的 IO 请求通过调用 Host 上的系统调用来完成处理。IO 请求结构体代码如下:

#[derive(Clone)]
struct Request {
    desc_index: u16,
    out_header: RequestOutHeader,
    iovec: Vec<Iovec>,
    data_len: u64,
    in_header: GuestAddress,
}​

如果 virtio-blk 设备的命令行配置中指定 direct 为 on,则会调用异步 IO 系统调用:io_submit 和 io_getevents 进行 IO 请求的下发以及 IO 请求处理结果的获取;如果 virtio-blk 设备的命令行配置中指定 direct 为 off,则会调用同步 IO 系统调用:pread 和 pwrite。

性能优化
StratoVirt 还对 virtio-blk 设备进行了对应的优化处理。优化处理主要有两点。

第一点优化是对地址空间连续的 IO 请求进行了合并操作,减少了对 Host 上 IO 系统调用的调用次数,从而获得了一定程度的性能提升。具体代码处理逻辑如下:

fn merge_req_queue(&self, mut req_queue: Vec<Request>) -> Vec<Request> {
    if req_queue.len() == 1 {
        return req_queue;
    }

    req_queue.sort_by(|a, b| a.out_header.sector.cmp(&b.out_header.sector));
    let mut merge_req_queue = Vec::<Request>::new();
    let mut continue_merge: bool = false;

    for req in &req_queue {
        if continue_merge {
            if let Some(last_req) = merge_req_queue.last_mut() {
                if last_req.out_header.sector + last_req.get_req_sector_num()
                    != req.out_header.sector
                {
                    continue_merge = false;
                    merge_req_queue.push(req.clone());
                } else {
                    for iov in req.iovec.iter() {
                        let iovec = Iovec {
                            iov_base: iov.iov_base,
                            iov_len: iov.iov_len,
                        };
                        last_req.data_len += iovec.iov_len;
                        last_req.iovec.push(iovec);
                    }
                }
            }
        } else {
            merge_req_queue.push(req.clone());
        }
    }

    merge_req_queue
}​

 

第二点优化操作是对于异步 IO 系统调用的处理优化。下发 io_submit 系统调用之后,Host 会通过 io_getevents 系统调用通知 StratoVirt 上一次 IO 请求处理的结果。

如果采用 Epoll 的事件唤醒机制就会导致 IO 请求处理结果获取的速度变慢。因此 StratoVirt 的处理方式是在 IO 线程内每次先预先轮询一段时间,查看上一次 IO 请求处理是否完成,如果完成了则直接进行下一步;如果没有轮询成功则继续进行 Epoll 等待。具体代码如下:

pub fn iothread_run(&mut self) -> Result<bool> {
    if let Some(manager) = &self.manager {
        if manager.lock().unwrap().loop_should_exit() {
            manager.lock().unwrap().loop_cleanup()?;
            return Ok(false);
        }
    }
    let timeout = self.timers_min_timeout();

    if timeout == -1 {
        for _i in 0..AIO_PRFETCH_CYCLE_TIME {
            for (_fd, notifer) in self.events.read().unwrap().iter() {
                if notifer.io_poll {
                    if let EventStatus::Alive = notifer.status {
                        let handle = notifer.handlers[1].lock().unwrap();
                        match handle(self.ready_events[1].event_set(), notifer.raw_fd) {
                            None => {}
                            Some(_) => {
                                break;
                            }
                        }
                    }
                }
            }
        }
    }

    self.epoll_wait_manager(timeout)
}​

通过以上的优化措施之后,virtio-blk 设备性能就可以达到理论上限。经过测试,StratoVirt 的 virtio-blk 设备的磁盘 IO 性能与 Qemu 的 virtio-blk 设备的磁盘 IO 性能基本持平。

(来源公众号: openEuler )

分类
已于2022-5-7 17:46:20修改
收藏
回复
举报
回复
    相关推荐