【firecracker】virtio-mmio设备

  外部设备为CPU提供存储、网络等多种服务,是计算机系统中除运算功能之外最为重要的功能载体。CPU与外设之间通过某种协议传递命令和执行结果;virtio协议最初是为虚拟机外设而设计的IO协议,但是随着应用范围逐步扩展到物理机外设,virtio协议正朝着更适合物理机使用的方向而演进。

virtio设备运行原理

一、抽象原理

  对于采用virtio协议进行通信的CPU和外设,其抽象原理如下图所示。CPU与外设可以共同访问内存(例如外设以DMA方式访问内存);内存中存在一个称为环形队列(IO RING)的数据结构,根据存放对象不同,该队列可分成由IO请求组成的请求队列(Avail Queue)和由IO响应组成的响应队列(Used Queue)。一个IO的处理过程可以分成如下四步:

  • 第一步,应用程序下发IO时,CPU将IO请求放入环形结构(IO RING)的请求队列(Avail Queue)中并通知设备;
  • 第二步,设备收到通知后从请求队列中取出IO请求并在内部进行实际处理;
  • 第三步,设备将IO处理完成后,将结果作为IO响应放入响应队列(Used Queue)并以中断通知CPU;
  • 第四步,CPU从响应队列中取出IO处理结果并返回给应用程序。

二、总线协议

  virtio协议实现过程中,CPU与外设之间的通知机制以及外设访问内存方式由实际连接CPU与外设的总线协议决定,如下图所示。换句话说,virtio协议可以基于多种不同的总线协议来实现。虚拟化场景中,主要采用PCI总线协议和MMIO总线协议:采用PCI总线协议的virtio设备叫virtio-pci设备,它可以支持virtio设备的热插拔特性(基于PCI总线的设备热插拔机制),并可应用于真实物理外设;采用mmio总线协议的virito设备叫virito-mmio设备,它完全是针对虚拟机设计的,是一种轻量的虚拟总线机制,支持快速设备发现,但是无法使用在真实物理外设中。

三、队列结构与操作

  抛开总线协议所决定的通知机制及访存方式,virtio协议定义了明确的队列结构及操作流程。下面我们以virito-blk块设备为例来进一步分析。

  virtio-blk是一种存储设备,CPU发起的IO请求包含操作类型(读或写)、起始扇区(一个扇区为512节节,是块设备的存储单位)、内存地址、访问长度;请求处理完成后返回的IO响应仅包含结果状态(成功或失败)。如下示例图中,系统产生了一个IO请求(an example of IO),它在内存上的数据结构分为三个部分:Header,即请求头部,包含操作类型和起始扇区;Data,即数据区,包含地址和长度;Status,即结果状态。

  virtio-blk设备使用一个环形队列结构(IO RING),它由三段连续内存组成:Descriptor Table、Avail Queue和Used Queue:

  • Descriptor Table由固定长度(16字节)的Descriptor组成,其个数等于环形队列(IO RING)长度,其中每个Descriptor包含四个域:addr代表某段内存的起始地址,长度为8个字节;len代表某段内存的长度,本身占用4个字节(因此代表的内存段最大为4GB);flags代表内存段读写属性等,长度为2个字节;next代表下一个内存段对应的Descpriptor在Descriptor Table中的索引,因此通过next字段可以将一个请求对应的多个内存段连接成链表。
  • Avail Queue由头部的flags和idx域及entry数组(entry代表数组元素)组成:flags与通知机制相关;idx代表最新放入IO请求的编号,从零开始单调递增,将其对队列长度取余即可得该IO请求在entry数组中的索引;entry数组元素用来存放IO请求占用的首个Descriptor在Descriptor Table中的索引,数组长度等于环形队列长度(不开启event_idx特性)。
  • Used Queue由头部的flags和idx域及entry数组(entry代表数组元素)组成:flags与通知机制相关;idx代表最新放入IO响应的编号,从零开始单调递增,将其对队列长度取余即可得该IO响应在entry数组中的索引;entry数组元素主要用来存放IO响应占用的首个Descriptor在Descriptor Table中的索引(还有一个len域,virtio-blk并不使用), 数组长度等于环形队列长度(不开启event_idx特性)。
  • 环形队列结构(IO RING)被CPU和设备同见。仅CPU可见变量为free_head(空闲Descriptor链表头,初始时所有Descriptor通过next指针依次相连形成空闲链表)和last_used(当前已取的used元素位置)。仅设备可见变量为last_avail(当前已取的avail元素位置)。

  针对示例图中的IO请求,处理流程分析如下:

  • 第一步,CPU放请求。由于示例IO请求在内存中由Header、Data和Status三段内存组成,因此要从Descriptor Table中申请三个空闲项,每项指向一段内存,并将三段内存连接成链表。这里假设我们申请到了前三个Descriptor(free_head更新为3,表示下一个空闲项从索引3开始,因为0、1、2已被占用),那么会将第一个Descriptor的索引值0填入Aail Queue的第一个entry中,并将idx更新为1,代表放入1个请求;
  • 第二步,设备取请求。设备收到通知后,通过比较设备内部的last_avail(初始为0)和Avail Queue中的idx(当前为1)判断是否有新的请求待处理(如果last_vail小于Avail Queue中的idx,则有新请求)。如果有,则取出请求(更新last_avail为1 )并以entry的值为索引从Descriptor Table中找到请求对应的所有Descriptor来获知完整的请求信息。
  • 第三步,设备放响应。设备完成IO处理后(包括更新Status内存段内容),将已完成IO的Descriptor Table索引放入Used Queue对应的entry中,并将idx更新为1,代表放入1个响应;
  • 第四步,CPU取响应。CPU收到中断后,通过比较内部的last_used(初始化0)和Used Queue中的idx(当前为1)判断是否有新的响应(逻辑类似Avail Queue)。如果有,则取出响应(更新last_used为1)并将Status中断的结果返回应用,最后将完成响应对应的三项Descriptor以链表方式插入到free_head头部。

四、virtio-mmio后端代码解析

  完成virtio队列结构和操作流程分析后,我们可以结合virtio-mmio后端代码来进一步加深理解。在收到前端CPU的通知后(后续讨论mmio总线时将分析该通知过程),fc_vmm线程将对IO进行处理,核心处理函数为handle_event:

firecracker/devices/src/virtio/block.rs:

impl EpollHandler for BlockEpollHandler {                                           // EpollHandler是代表Epoll事件循环框架中的事件处理方法的一个trait。
                                                                                    //     Traint是Rust语言中对相同行为进行抽象的一种方式,是一组公共函数的集合
    fn handle_event(                                                                // EpollHandler这个trait只定义了一个函数handle_event,实现对具体Epoll事件
                                                                                    //     的处理功能
        &mut self,
        device_event: DeviceEvent,                                                  // device_event代表事件类别,这里为QUEUE_AVAIL_EVENT,即对virtio的avail
                                                                                    //     queue中的请求进行处理;另一个类别为RATE_LIMITER_EVENT,与QoS限速有关,
                                                                                    //     这里暂不作讨论
        ...
    ) -> result::Result<(), DeviceError> {
        match device_event {

            QUEUE_AVAIL_EVENT => {                                                  // 对avail queue进行处理
                ...
                if let Err(e) = self.queue_evt_read(){                              // self.queue_evt为向Epoll事件循环框架中注册的句柄,调用read函数读取句柄内容
                                                                                    //     后便可再次触发事件
                    ...
                } else if !self.rate_limiter.is_blocked() && self.process_queue(0){ // self.rate_limiter.is_blocked()用来判定是否达到上限限制。如果没有超过上限,则
                                                                                    //     调用process_queue()对ID为0的virtio队列进行请求处理。firecracker实现的
                                                                                    //     virtio-blk只支持单队列,所以这里仅处理0号队列。process_queue的内部实现我
                                                                                    //     们将在下面分析,总的来说,该函数实现了virito抽象原理中描述的第二步和第三步
                    self.signal_used_queue()                                        // 向前端CPU发送中断通知
                } else {
                    Ok(())
                }
            }
            ...
        }
    }
}

pub struct BlockEpollHandler { // BlockEpollHandler结构包含virtio-blk后端和事件处理相关的所有对象和方法
    queue: Vec<Queue>,         //     virtio-blk设备包含的所有virtio队列,这里只有一个
    mem: GuestMemory,          //     虚拟机内存对象,已经映射到firecracker用户态空间,可直接访问
    disk_image: File,          //     virtio-blk设备对应的后端文件,实际的存储点
    disk_nsectors: u64,        //     virtio-blk设备大小,以扇区为单位
    ...
    interrupt_evt: EventFd,    //     virtio队列对应的irq_fd,用来触发中断通知前端虚拟CPU
    queue_evt: EventFd,        //     virtio队列对应的io_event_fd,虚拟CPU通过它通知fc_vmm线程有请求待处理
    ...
}

impl BlockEpollHandler {
    fn process_queue(&mut self, queue_index: usize) -> bool { // 实现virtio抽象原理中的第二步和第三步
        let queue = &mut self.queues(queue_index);            // queue代表queue_index索引的virtio队列
        let mut used_any = false;                             // used_any代表是否成功处理请求

        while let some(head) = queue.pop(&self.mem) {         // 第二步,取出IO请求;每个请求对应由三项Descriptor
                                                              //     组成的链表(DescriptorChain),链表头部位于head中
            let len;
            match Request::parse(&head, &self.mem) {          // 由链表头部head开始,对请求进行解析,解析后获得完整
                                                              //     请求信息并将其保存到request对象中
                Ok(request) => {
                    ...
                    let status = match request.execute(       // 根据request对象中的请求信息进行实际的IO处理,例如针对
                                                              //     读请求,将从后端文件中读取实际数据到内存指定位置中。
                                                              //     该函数为同步操作,当读/写操作完成后,结果才会返回到status中

                        &mut self.disk_image,
                        self.disk_nsectors,
                        &self.mem,
                        &self.disk_image_id,
                    ){
                        Ok(l) => {
                            len = l;
                            VIRTIO_BLK_S_OK
                        }
                        ...
                    };
                    self.mem                                  // 将实际执行结果status填写到IO请求在虚拟机内存中的status字段
                        .write_obj_at_addr(status, request.status_addr)
                        .unwrap();
                }
                ...
            }
            queue.add_used(&self.mem, head.index, len);       // 第三步,放入IO响应
            used_any = true;                                  // used_any赋值为true,代表成功处理请求
        }

        used_any
    }

    fn signal_used_queue(&self) -> result::Result<(), DeviceError> { // 成功处理请求后以中断方式通知前端虚拟CPU
        ...
        self.interrupt_evt.write(1).map_err(...)                     // 通过写irq_fd借助KVM模块触发虚拟中断
    }
}

  我们继续深入看一下virtio队列结构相关代码:

firecracker/devices/src/virtio/queue.rs:

pub struct Queue {                 // firecracker实现的virtio环形队列结构                                                            
    max_size: u16,                 // 设备提供的最大队列长度                                                          
    pub size: u16,                 // 前端驱动设置的最大队列长度,小于max_size                                                    
    pub ready: bool,               // 队列是否已经配置完成                                                             
    pub desc_table: GuestAddress,  // Descriptor Table段在虚拟机内存中的起始地址                                                                                                                                                            
    pub avail_ring: GuestAddress,  // Avail Queue段在虚拟机内存中的起始地址                                                                               
    pub used_ring: GuestAddress,   // Used Queue段在虚拟机内存中的起始地址                                            

    next_avail: Wrapping<u16>,     // 设备可见的last_avail值                                           
    next_used: Wrapping<u16>,      // 下一个将填入的used entry索引                                             
}

impl Queue {
    …
    pub fn pop<'a, 'b>(&'a mut self, mem: &'b GuestMemory) -> Option<DescriptorChain<'b>> {
        if self.len(mem) == 0 {                                                                 // len会计算Avail Queue的idx和last_avail的差值,
                                                                                                //     如果为零,代表没有待处理的请求
            return None;                                                        
        }
        let index_offset = 4 + 2 * (self.next_avail.0 % self.actual_size());                    // 计算last_avail指向的entry项在Avail Queue中的偏移                                                           
        let desc_index: u16 = mem                                                               // 根据偏移读取entry的内容,即请求对应的首个Descriptor索引
            .read_obj_from_addr(self.avail_ring.unchecked_add(usize::from(index_offset)))
            .unwrap();                                                          

        DescriptorChain::checked_new(mem, self.desc_table, self.actual_size(), desc_index).map( // 根据首个Descriptor索引找到完整的DescriptorChain
            |dc| {                                                              
                self.next_avail += Wrapping(1);                                                 // last_avail值递增                      
                dc                                                              
            },                                                                  
        )                                                                       
    }
    …
}          

pub struct DescriptorChain<'a> {  // 每个IO请求对应一个DescriptorChain,含多个Descriptor。
                                  //     结构定义中的'a为Rust语言针对引用使用的生命周期注解,用
                                  //     来显式声明引用变量指向对象的作用域范围。这里主要说明
                                  //     DescriptorChain对象的作用域应当大于或等于mem的作用域。                                          
    mem: &'a GuestMemory,         // 对虚拟机内存对象的引用                                          
    desc_table: GuestAddress,     // Descriptor Table在虚拟机内存中的起始地址                                       
    queue_size: u16,              // 队列长度                              
    ttl: u16,                     // chain链表长度                          
    pub index: u16,               // DescriptorChain中当前Descptor的索引,可通过next_descriptor()更新                                                     
    pub addr: GuestAddress,       // 当前Descriptor的addr字段                                                                                 
    pub len: u32,                 // 当前Descriptor的len字段                                                          
    pub flags: u16,               // 当前Descriptor的flags字段                                                                                     
    pub next: u16,                // 当前Descriptor的next字段                              
}

impl<'a> DescriptorChain<'a> {                                                   
    fn checked_new(                                                                   // 根据index索引创建一个新的DescriptorChain                               
        mem: &GuestMemory,                                                      
        desc_table: GuestAddress,                                               
        queue_size: u16,                                                        
        index: u16,                                                             
    ) -> Option<DescriptorChain> {                                              
        if index >= queue_size {                                                      // 如果索引值大于队列长度,说明索引值无效                                     
            return None;                                                        
        }                                                                       

        let desc_head = match mem.checked_offset(desc_table, (index as usize) * 16) { // 判断索引位置是否在有效的虚拟机内存区间内
            Some(a) => a,                                                       
            None => return None,                                                
        };                                                                       
        mem.checked_offset(desc_head, 16)?;                                           // 判断整个Descriptor(16字段)是否在有效的虚拟机内存区间内                                 

        let desc = match mem.read_obj_from_addr::<Descriptor>(desc_head) {            // 从内存中读取Descriptor内容    
            Ok(ret) => ret,                                                     
            …                                                                   
        };                                                                      
        let chain = DescriptorChain {                                           
            mem,                                                                 
            desc_table,                                                         
            queue_size,                                                         
            ttl: queue_size,                                                    
            index,                                                              
            addr: GuestAddress(desc.addr as usize),                             
            len: desc.len,                                                      
            flags: desc.flags,                                                  
            next: desc.next,                                                    
         };                                                                      

        if chain.is_valid() {                                                   
            Some(chain)
        } else {                                                                
            None                                                                
        }                                                                       
    }
    …
}
firecracker/devices/src/virtio/block.rs:

struct Request {                // 该结构描述一个virtio-blk请求,可参考前面数据结构部分的介绍                                      
    request_type: RequestType,  // 请求类型,如读、写、flush等                                                
    sector: u64,                // 请求访问的起始扇区                                 
    data_addr: GuestAddress,    // 数据区在虚拟机内存中的起始地址                                               
    data_len: u32,              // 数据区的长度                                   
    status_addr: GuestAddress,  // 存放请求处理结果的status内存地址                                               
}

impl Request {                                                                  
    fn parse(avail_desc: &DescriptorChain, mem: &GuestMemory) -> result::Result<Request, Error> { // 从DescriptorChain中解析出完整的请求信息
        …                                                                                                                                            
        let mut req = Request {                                                 
            request_type: request_type(&mem, avail_desc.addr)?,                                   // 先从DescriptorChain的第一项即请求的head段中解析出操作类型               
            sector: sector(&mem, avail_desc.addr)?,                                               // 和访问起始扇区
            data_addr: GuestAddress(0),                                          
            data_len: 0,                                                        
            status_addr: GuestAddress(0),                                       
        };                                                                       

        let data_desc;                                                          
        let status_desc;                                                         
        let desc = avail_desc                                                                     // desc指向链表的第二个Descriptor                                                
            .next_descriptor()                                                   
            .ok_or(Error::DescriptorChainTooShort)?;                            

        if !desc.has_next() {                                                                     // 如果链表只有两项,
            status_desc = desc;                                                                   // 说明第二项是status段的Descriptor,
            // Only flush requests are allowed to skip the data descriptor.     
            if req.request_type != RequestType::Flush {                                           // 且请求类型为flush。该类型可以不带数据段  
                return Err(Error::DescriptorChainTooShort);                     
            }                                                                   
        } else {                                                                                  // 否则,
            data_desc = desc;                                                                     // 第二项desc代表数据段Descriptor,
            status_desc = data_desc                                                               // 下一项(即第三项)代表结果段Descriptor
                .next_descriptor()                                              
                .ok_or(Error::DescriptorChainTooShort)?;                         
            …                                                                 

            req.data_addr = data_desc.addr;                                                       // 将数据段Descriptor中的地址填入req中
            req.data_len = data_desc.len;                                                         // 将数据段Descriptor中的长度填入req中
        }                                                                       
        …                                                                                                                                          
        req.status_addr = status_desc.addr;                                                       // 将结果段Descriptor中的地址填入req中

        Ok(req)                                                                 
    }     

    fn execute<T: Seek + Read + Write>(                              // 处理请求                                      
        &self,                                                                  
        disk: &mut T,                                                           
        disk_nsectors: u64,                                                      
        mem: &GuestMemory,                                                      
        disk_id: &Vec<u8>,                                                      
    ) -> result::Result<u32, ExecuteError> {                                     
        …                                                                                                                                              
        disk.seek(SeekFrom::Start(self.sector << SECTOR_SHIFT))      // 将文件偏移到请求指定的扇区位置
            .map_err(ExecuteError::Seek)?;                                      

        match self.request_type {                                               
            RequestType::In => {                                     // 对于读请求,读取文件内容到指定内存位置                                        
                mem.read_to_memory(self.data_addr, disk, self.data_len as usize)
                   .map_err(ExecuteError::Read)?;                              
                …                                  
               return Ok(self.data_len);                                       
            }                                                                    
            RequestType::Out => {                                    // 对于写请求,从指定内存处将数据写入到文件中
                mem.write_from_memory(self.data_addr, disk, self.data_len as usize)
                   .map_err(ExecuteError::Write)?;                             
                …                                
            }                                                                                                                                     
            …                                                                    
        };                                                                      
        Ok(0)                                                                   
    }                                                                            
}            

初始化流程与mmio总线机制

  在讨论完virtio运行时原理后,我们再来分析一下初始化流程。它与virtio底层所采用的总线协议是强相关的,因此我们也会讨论总线机制的实现。整个初始化流程又可分为virtio设备自身初始化以及前端虚拟CPU与后端设备的协商过程两个部分。

一、设备自身初始化

  firecracker采用了一种针对虚拟化的简单总线协议-mmio作为实现virtio的基础。mmio总线预留了0xD000000~0xFFFFFFFF的地址空间作为所有mmio设备的配置空间,并使用5~15号irq作为所有mmio设备可使用的中断号;每个mmio设备通过虚拟机内核启动时的命令行参数来上报设备资源信息(如配置空间地址范围和使用的中断号),这是一种静态的设备发现机制,它不像PCI设备的总线枚举机制那么灵活,因此不支持设备热插拔等高级特性。

  firecracker中定义一个全局对象MMIODeviceManager来管理所有mmio设备,virtio-mmio设备的初始化通过register_virtio_device函数进行:

pub struct MMIODeviceManager {                                                     
    pub bus: devices::Bus,      // mmio总线对象,内部通过BTree来组织所有mmio设备对象                                         
    guest_mem: GuestMemory,     // 虚拟机内存对象                                               
    mmio_base: u64,             // mmio总线配空间起始地址,X86架构下为0xD0000000                                    
    irq: u32,                   // mmio总线中断资源起始编号,X86架构下为5                          
    last_irq: u32,              // mmio总线中断资源结束编号,X86架构下为15                             
    id_to_dev_info: HashMap<(DeviceType, String), MMIODeviceInfo>,  // 记录设备信息的哈希表            
}

impl MMIODeviceManager {                                                        
    …               
    pub fn register_virtio_device(                                                         // 注册一个新的virito-mmio设备                                      
        &mut self,                                                                         // MMIODeviceManager对象  
        vm: &VmFd,                                                                         // KVM虚拟机对象      
        device: Box<devices::virtio::VirtioDevice>,                                        // virtio设备对象,注意virtio设备对象是包含在virito-mmio对象中的                         
        cmdline: &mut kernel_cmdline::Cmdline,                                             // 虚拟机内核启动参数                             
        type_id: u32,                                                                      // virtio设备对象类别,例如TYPE_BLOCK代表virtio-blk设备
        device_id: &str,                                                                   // virtio设备对象ID     
    ) -> Result<u64> {                                                          
        …                                                                     
        let mmio_device = devices::virtio::MmioDevice::new(self.guest_mem.clone(), device) // 根据virtio设备对象创建virtio-mmio对象,后续将深入分析
            .map_err(Error::CreateMmioDevice)?;                                 
        for (i, queue_evt) in mmio_device.queue_evts().iter().enumerate() {                 // 为每个virtio队列向KVM内核注册io_event_fd,它是KVM向
                                                                                            //     用户态VMM程序提供的一种事件通知机制
            let io_addr = IoEventAddress::Mmio(                                                               
                self.mmio_base + u64::from(devices::virtio::NOTIFY_REG_OFFSET),             // self.mmio_base为分配给当前mmio设备的mmio配置空间的起
                                                                                            //     始地址。从起始位置偏移NOTIFY_REG_OFFSET是虚拟CPU
                                                                                            //     向后端的通知地址,真正通知过程中CPU会向该地址写入
                                                                                            //     队列号
            );                                                                  

            vm.register_ioevent(queue_evt.as_raw_fd(), &io_addr, i as u32)                  // 向KVM注册io_event_fd,传入队列eventfd、通知地址、写入
                                                                                            //     内容。当虚拟CPU向该地址写入确定内容时,KVM就触发
                                                                                            //     对eventfd的写操作,进而唤醒等待该eventfd的处理线程
                .map_err(Error::RegisterIoEvent)?;                              
        }                                                                       

        if let Some(interrupt_evt) = mmio_device.interrupt_evt() {  
            vm.register_irqfd(interrupt_evt.as_raw_fd(), self.irq)                          // 向KVM注册irqfd,这是反向由后端给前端CPU发送中断通知,
                .map_err(Error::RegisterIrqFd)?;                                            //     后端只需要往irqfd中写入数据,就会通过KVM向前端发中断
        }                                                                       

        self.bus                                                                            // 向mmio总线中添加当前mmio设备
            .insert(Arc::new(Mutex::new(mmio_device)), self.mmio_base, MMIO_LEN)            // 以配置空间作为BTree的Key索引,MMIO_LEN为4K
            .map_err(Error::BusError)?;                                         

        #[cfg(target_arch = "x86_64")]                                          
        cmdline                                                                             // 向虚拟机内核启动命令行中插入字段,便于前端发现设备                                                                 
            .insert(                                                            
                "virtio_mmio.device",                                                       // virtio_mmio.device代表virtio-mmio设备对象
                &format!("{}K@0x{:08x}:{}", MMIO_LEN / 1024, self.mmio_base, self.irq),     // 设备资源信息,例如第一个设备为"4K@0xD0000000:5",
// 含议为配置空间从0xD0000000开始,长度4K,中断irq号为5
            )                                                                    
           .map_err(Error::Cmdline)?;                                          
        let ret = self.mmio_base;                                               

        self.id_to_dev_info.insert(                                                         // 向哈希表中记录当前设备信息       
            (DeviceType::Virtio(type_id), device_id.to_string()),               
            MMIODeviceInfo {                                                    
                addr: ret,                                                      
                len: MMIO_LEN,                                                  
                irq: self.irq,                                                  
            },                                                                  
        );                                                                      

        self.mmio_base += MMIO_LEN;                                                         // 将mmio_base加上4K,表明当前设备占用4K                                          
        self.irq += 1;                                                                      // 将中断irq号加1,表明当前设备占用1个irq

        Ok(ret)                                                                 
    }           
}

  通过上述代码,我们看到MMIODeviceManager对象在注册设备过程中使用了几个核心概念:devices:;Bus、devices::virtio::VirtioDevice、 devices::virtio::MmioDevice。这些类型包含在firecracker的一个名为devices的子模块中,它是firecracker对设备模型概念的抽象与实现,下面我们深入分析一下该模块。

  首先firecracker的设备模型中定义了总线和设备两个抽象概念,它们在Rust中的实现如下:

firecracker/devices/src/bus.sr:

pub struct Bus {                                          // 总线下的设备以BTree组织                                                               
    devices: BTreeMap<BusRange, Arc<Mutex<BusDevice>>>,   // 以设备配置空间范围BusRange作为BTree的Key索引                     
}

impl Bus {                                                                                             
    pub fn new() -> Bus {                                                       
        Bus {                                                                   
            devices: BTreeMap::new(),                                            
        }                                                                       
    }

    pub fn insert(&mut self, device: Arc<Mutex<BusDevice>>, base: u64, len: u64) -> Result<()> {
        …
    }

    pub fn read(&self, addr: u64, data: &mut [u8]) -> bool {     // 对总线地址的读操作(通常由CPU发起,回顾前文对CPU运行时介绍),             
        if let Some((offset, dev)) = self.get_device(addr) {     // 会转换成对总线上对就设备的读操作(使用相对偏移)        
            dev.lock()                                                           
                .expect("Failed to acquire device lock")                        
                .read(offset, data);                                            
            true                                                                 
        } else {                                                                
            false                                                               
        }                                                                        
    }                                                                           

    pub fn write(&self, addr: u64, data: &[u8]) -> bool {        // 对总线地址的写操作               
        if let Some((offset, dev)) = self.get_device(addr) {                    
            dev.lock()                                                          
                .expect("Failed to acquire device lock")                         
                .write(offset, data);                                           
            true                                                                
        } else {                                                                 
            false                                                               
        }                                                                       
    }                                                                           
}

pub trait BusDevice: AsAny + Send {                         // 总线上的设备被定义成一个trait,包括一组公共的操作                                               
    fn read(&mut self, offset: u64, data: &mut [u8]) {}     // 设备内的读操作,向上对接总线上的读操作                                                       
    fn write(&mut self, offset: u64, data: &[u8]) {}        // 设备内的写操作,向上对接总线上的写操作                                        
    fn interrupt(&self, irq_mask: u32) {}                   // 设备触发中断         
}

  基于抽象的总线和设备概念,virtio-mmio设备是对设备概念的扩展,是一种具体的设备实现方式:

firecracker/devices/src/virtio/mmio.rs:

pub struct MmioDevice {                                                            
    device: Box<VirtioDevice>,            // 包含的virtio设备对象,该对象实现VirtioDevice这个trait                                                   
    device_activated: bool,               // 设备是否被激活,即完成与前端的握手                              
    …                                                
    queue_select: u32,                    // 队列选择器,代表当前要操作的队列                           
    …                                             
    interrupt_evt: Option<EventFd>,       // 中断eventfd,如前文述将传递给KVM作为irqfd                                     
    driver_status: u32,                   // 前端驱动状态                         
    …                                                         
    queues: Vec<Queue>,                   // 队列数组                               
    queue_evts: Vec<EventFd>,             // 队列eventfd数组,将传递给KVM作为io_event_fd                                   
    mem: Option<GuestMemory>,             // 虚拟机内存对象                                          
}

impl MmioDevice {                                                                               
    pub fn new(mem: GuestMemory, device: Box<VirtioDevice>) -> std::io::Result<MmioDevice> {
        let mut queue_evts = Vec::new();                                           
        for _ in device.queue_max_sizes().iter() {  // 根据virtio设备的队列数来生成队列eventfd数组                              
            queue_evts.push(EventFd::new()?)                                        
        }                                                                          
        let queues = device                         // 生成队列数组
            .queue_max_sizes()                                                      
            .iter()                                                                
            .map(|&s| Queue::new(s))                                               
            .collect();                                                             
        Ok(MmioDevice {                                                            
            device,                                                                
            device_activated: false,                                                
            …                                              
            queue_select: 0,                                                    
            …                  
            interrupt_evt: Some(EventFd::new()?),                                
            driver_status: DEVICE_INIT,                                         
            …                                                
            queues,                                                             
            queue_evts,                                                         
            mem: Some(mem),                                                     
        })                                                                      
    } 
}

impl BusDevice for MmioDevice {                        // MmioDevice作为BusDevice的扩展,内部实现了对virtio的操作
                                                       // 我们将在下节前后端协商部分分析这部分内容                                    
    fn read(&mut self, offset: u64, data: &mut [u8]) {
        …
    }
    fn write(&mut self, offset: u64, data: &[u8]) {
        …
    }
    fn interrupt(&self, irq_mask: u32) {
        …
    }
}

pub trait VirtioDevice: Send {  // VirtioDevice定义了所有virtio设备都需要实现的公共接口,
                                // 请参考接口上方的描述文档                                              
    /// The virtio device type.                                                  
    fn device_type(&self) -> u32;                                               

    /// The maximum size of each queue that this device supports.               
    fn queue_max_sizes(&self) -> &[u16];                                        

    /// The set of feature bits shifted by `page * 32`.                         
    fn features(&self, page: u32) -> u32 {                                      
        let _ = page;                                                           
        0                                                                        
    }                                                                           

    /// Acknowledges that this set of features should be enabled.                
    fn ack_features(&mut self, page: u32, value: u32);                          

    /// Reads this device configuration space at `offset`.                      
    fn read_config(&self, offset: u64, data: &mut [u8]);                        

    /// Writes to this device configuration space at `offset`.                  
    fn write_config(&mut self, offset: u64, data: &[u8]);                       

    /// Activates this device for real usage.                                   
    fn activate(                                                                 
        &mut self,                                                              
        mem: GuestMemory,                                                       
        interrupt_evt: EventFd,                                                  
        status: Arc<AtomicUsize>,                                               
        queues: Vec<Queue>,                                                     
        queue_evts: Vec<EventFd>,                                                
    ) -> ActivateResult;                                                        

    /// Optionally deactivates this device and returns ownership of the guest memory map, interrupt
    /// event, and queue events.                                                
    fn reset(&mut self) -> Option<(EventFd, Vec<EventFd>)> {                    
        None                                                                     
    }                                                                           
}

  virtio-blk设备作一virito设备的一种类型,它将扩展VirtioDevice,并实现VritioDevice定义的接口:

firecracker/devices/src/virtio/block.rs:

pub struct Block {                       // virtio-blk设备                                                                                            
    disk_image: Option<File>,            // 后端文件                                                                             
    disk_nsectors: u64,                  // 块设备大小                                                                       
    …                                                                                     
    config_space: Vec<u8>,               // blk配置空间,对前端呈现块设备大小、队列数等                                                                          
    epoll_config: EpollConfig,           // 向epoll事件循环框架传递处理对象                                                                         
    …                                                                          
}

impl VirtioDevice for Block {            // 实现VirtioDevice定义的公共接口,这里仅举了两个例子                                          
    fn device_type(&self) -> u32 {       // 返回设备类型为TYPE_BLOCK                                            
     TYPE_BLOCK                                                              
    }                                                                           

    fn queue_max_sizes(&self) -> &[u16] { //返回每个队列最大长度,这里只有一个队列且长度为256                                      
        QUEUE_SIZES                                                             
    }
    …
}

二、前后端协商(握手)

  上节讲述了firecracker后端如何初始化一个virtio-mmio设备,本节将讨论前后端的协商流程。

  虚拟机内部系统通过内核命令行参数识别virtio-mmio设备并调用对应的驱动函数对设备进行驱动。通过命令行参数(如virtio-mmio.device 4K@0xD0000000:5),前端系统可知配置空间地址范围(如0xD0000000~0xD0000FFF)和中断资源(如irq为5),接着便可以通过读写配置空间与设备进行协商操作。Virtio-mmio设备的配置空间概览如下:

  前端通过配置空间中的DEVICE ID可以获知具体的设备类别(例如virtio-blk的ID为2),并通过DEVICE FEATURE和DRIVER FEATURE与后端进行特性协商。此后最为重要的动作便是为VIRTIO环形队列申请内存,并将内存地址填入到配置空间相应字段中。最后向DEVICE STATUS中写入DRIVER OK告诉后端驱动初始化完成。

  回顾前文对虚拟CPU原理的介绍,当CPU读写MMIO地址空间时将从Guest模式退出到firecracker中并调用mmio总线的读写函数进行处理。接着mmio总线的读写操作将转到地址对应的mmio设备中进行读写,我们深入代码来看一下mmio设备的读写操作:

firecracker/devices/src/virtio/mmio.rs:

impl BusDevice for MmioDevice {       
    fn read(&mut self, offset: u64, data: &mut [u8]) {  // 大家可以对照上面的配置空间解析图来理解代码的含义                                                   
        match offset {                                                                                          
            0x00...0xff if data.len() == 4 => {                                                                
                let v = match offset {                                                                         
                    0x0 => MMIO_MAGIC_VALUE,                                                                   
                    0x04 => MMIO_VERSION,                                       
                    0x08 => self.device.device_type(),                          
                    0x0c => VENDOR_ID, // vendor id                                                            
                    0x10 => {                                                   
                        let mut features = self.device.features(self.features_select);                         
                        if self.features_select == 1 {                          
                            features |= 0x1; // enable support of VirtIO Version 1                              
                        }                                                                                      
                        features                                                                               
                    }                                                           
                    0x34 => self.with_queue(0, |q| u32::from(q.get_max_size())),
                    0x44 => self.with_queue(0, |q| q.ready as u32),             
                    0x60 => self.interrupt_status.load(Ordering::SeqCst) as u32,                               
                    0x70 => self.driver_status,                                 
                    0xfc => self.config_generation,                                                             
                    _ => {                                                      
                        …                                                                               
                    }                                                                                           
                };                                                                                             
               LittleEndian::write_u32(data, v);                                                               
            }                                                                   
            0x100...0xfff => self.device.read_config(offset - 0x100, data),                                    
            _ => {                                                                                              
                …                                                                                           
            }                                                                                                   
        };                                                                                                     
    }       

    fn write(&mut self, offset: u64, data: &[u8]) {                              
        fn hi(v: &mut GuestAddress, x: u32) {                                   
            *v = (*v & 0xffff_ffff) | (u64::from(x) << 32)                      
        }                                                                        

        fn lo(v: &mut GuestAddress, x: u32) {                                   
            *v = (*v & !0xffff_ffff) | u64::from(x)                             
        }                                                                       

        match offset {                                                          
            0x00...0xff if data.len() == 4 => {                                 
                let v = LittleEndian::read_u32(data);                           
                match offset {                                                  
                    0x14 => self.features_select = v,                           
                    0x20 => {                                                   
                        if self                                                 
                            .check_driver_status(DEVICE_DRIVER, DEVICE_FEATURES_OK | DEVICE_FAILED)
                        {                                                       
                            self.device.ack_features(self.acked_features_select, v);
                        } else {                                                
                            …                                                   
                            return;                                             
                        }                                                        
                    }                                                           
                    0x24 => self.acked_features_select = v,                     
                    0x30 => self.queue_select = v,                              
                    0x38 => self.update_queue_field(|q| q.size = v as u16),     
                    0x44 => self.update_queue_field(|q| q.ready = v == 1),      
                    0x64 => {                                                    
                        if self.check_driver_status(DEVICE_DRIVER_OK, 0) {      
                            self.interrupt_status                               
                               .fetch_and(!(v as usize), Ordering::SeqCst);    
                        }                                                       
                    }                                                           
                    0x70 => self.update_driver_status(v),                          // 更新设备状态,需要进一步打开                  
                    0x80 => self.update_queue_field(|q| lo(&mut q.desc_table, v)),
                    0x84 => self.update_queue_field(|q| hi(&mut q.desc_table, v)),
                    0x90 => self.update_queue_field(|q| lo(&mut q.avail_ring, v)),
                    0x94 => self.update_queue_field(|q| hi(&mut q.avail_ring, v)),
                    0xa0 => self.update_queue_field(|q| lo(&mut q.used_ring, v)),
                    0xa4 => self.update_queue_field(|q| hi(&mut q.used_ring, v)),
                    _ => {                                                      
                        …
                        return;                                                 
                    }                                                           
                }                                                               
            }                                                                   
            0x100...0xfff => {                                                  
                if self.check_driver_status(DEVICE_DRIVER, DEVICE_FAILED) {     
                    self.device.write_config(offset - 0x100, data)              
                } else {                                                         
                    …
                    return;                                                     
                }                                                               
            }                                                                    
            _ => {                                                              
                …                                                               
                return;                                                          
            }                                                                   
        }                                                                       
    } 
}

  当前端更新设备状态为DRIVER OK时,后端将配合执行设备激活动作,下面仍以virito-blk为例进行分析:

firecracker/devices/src/virtio/mmio.rs:

    fn update_driver_status(&mut self, v: u32) {                                                                                 
        match !self.driver_status & v {                                          
            …                                                                
            DEVICE_DRIVER_OK                                                    
            if self.driver_status                                            
            == (DEVICE_ACKNOWLEDGE | DEVICE_DRIVER | DEVICE_FEATURES_OK) =>
            {                                                                   
                self.driver_status = v;                                          
                if !self.device_activated && self.are_queues_valid() {          
                    if let Some(ref interrupt_evt) = self.interrupt_evt {       
                        if let Some(mem) = self.mem.take() {                     
                            self.device                                         
                                .activate(                                      
                                    mem,                                        
                                    interrupt_evt.try_clone().expect("Failed to clone eventfd"),
                                    self.interrupt_status.clone(),              
                                    self.queues.clone(),                         
                                    self.queue_evts.split_off(0),               
                                )                                               
                                .expect("Failed to activate device");           
                            self.device_activated = true;                       
                        }                                                       
                    }                                                           
                }                                                                
            }                                                                                                                                  
            …                                                                  
        }                                                                       
    }
firecracker/devices/src/virtio/block.rs:

    fn activate(                                                                 // 激活virtio-blk设备                                   
        &mut self,                                                              
        mem: GuestMemory,                                                        //     虚拟机内存对象                                                 
        interrupt_evt: EventFd,                                                  //     中断irqfd,用来触发虚拟中断通知前端
        …                                               
        queues: Vec<Queue>,                                                      //     virtio队列,这里实际只有一个
        mut queue_evts: Vec<EventFd>,                                            //     virtio队列的io_event_fd,供前端通知后端
    ) -> ActivateResult {                                                       
        …                                                                        
        if let Some(disk_image) = self.disk_image.take() {                       
        let queue_evt = queue_evts.remove(0);                               
        let queue_evt_raw_fd = queue_evt.as_raw_fd();                       

        …              
        let handler = BlockEpollHandler {                                        // 构建一个BlockEpollHandler对象,参考前文运行时代码解析                      
            queues,                                                          
            mem,                                                            
            disk_image,                                                     
            disk_nsectors: self.disk_nsectors,                               
            …                                        
            interrupt_evt,                                                  
            queue_evt,                                                      
            …                                                  
        };                                                                  

        self.epoll_config                                                         // 注意,整个激活动作是在vCPU线程上下文执行的,BlockEpollHandler                                             
            .sender                                                               // 对象也在vCPU线程构建,但是BlockEopllHanler是在fc_vmm线程中被
            .send(Box::new(handler))                                              // 调用的,这样做的好处是可以减少vCPU退出时间。因此这里要把
            .expect("Failed to send through the channel");                        // BlockEpollHandler对象通过channel发送给fc_vmm线程

        epoll::ctl(                                                                // 向fc_vmm线程中的epoll事件循环框架添加队列的eventfd,这样当前端
            self.epoll_config.epoll_raw_fd,                                        // 借助KVM的io_event_fd便可以唤醒fc_vmm线程。fc_vmm线程首次处理队
            epoll::ControlOptions::EPOLL_CTL_ADD,                                  // 列事件时会从channel中读出BlockEpollHandler对象,并调用handle_event
            queue_evt_raw_fd,                                                      // 函数处理队列请求;后续处理事件,fc_vmm线程可直接使用该对象
            epoll::Event::new(epoll::Events::EPOLLIN, self.epoll_config.q_avail_token),
        )                                                                   
        .map_err(...)?;                                                                
        …                                                                                                                                    
        return Ok(());                                                      
    }                                                                                                                                                  

  至此,我们已经完成对firecracker中的virtio-mmio设备设计和实现思路(以virtio-blk为例,virtio-net实现本质是相同)的分析,这部分是firecracker中实现最复杂的部分,但是相比传统qemu-kvm中virtio-pci实现,已经作了大量精简。从运行时同步IO操作的实现方式可以看出firecracker的IO性能是非常糟糕的,相同队列并不支持IO的并行处理。不过,在serverless轻量化的场景中,系统中可能会存在成千上万的firecracker实例,单个实例具备非常高的IO性能并没有多大意义,因为此时整个系统IO吞吐量已经成为瓶颈。但是如果想把firecracker应用到通用虚拟化场景中,那对IO系统的改造将是一个大工程。


转载请注明:吴斌的博客 » 【firecracker】virtio-mmio设备