基于Qemu初始化设备驱动程序

设备树

QEMU 可以把它模拟的机器细节信息全都导出到dtb格式的二进制文件中,并可通过 dtc (Device Tree Compiler)工具转成可理解的文本文件。

$ qemu-system-riscv64 -machine virt -machine dumpdtb=riscv64-virt.dtb -bios default
$ dtc -I dtb -O dts -o riscv64-virt.dts riscv64-virt.dtb
$ less riscv64-virt.dts
#就可以看到QEMU RV64 virt计算机的详细硬件(包括各种外设)细节,包括CPU,内存,串口,时钟和各种virtio设备的信息。

设备树的每个节点上都描述了对应设备的信息,如支持的协议是什么类型等等。而操作系统就是通过这些节点上的信息来实现对设备的识别的。具体而言,一个设备节点上会有几个标准属性,这里简要介绍我们需要用到的几个:

  • compatible:该属性指的是该设备的编程模型,一般格式为 "manufacturer,model",分别指一个出厂标签和具体模型。如 "virtio,mmio" 指的是这个设备通过 virtio 协议、MMIO(内存映射 I/O)方式来驱动。
  • model:指的是设备生产商给设备的型号。
  • reg:当一些很长的信息或者数据无法用其他标准属性来定义时,可以用 reg 段来自定义存储一些信息。

传递设备树信息

操作系统在启动后需要了解计算机系统中所有接入的设备,这就要有一个读取全部已接入设备信息的能力,而设备信息放在哪里,又是谁帮我们来做的呢?在 RISC-V 中,这个一般是由 bootloader,即 OpenSBI or RustSBI 固件完成的。它来完成对于包括物理内存在内的各外设的探测,将探测结果以 设备树二进制对象(DTB,Device Tree Blob) 的格式保存在物理内存中的某个地方。然后bootloader会启动操作系统,即把放置DTB的物理地址将放在 a1 寄存器中,而将会把 HART ID (HART,Hardware Thread,硬件线程,可以理解为执行的 CPU 核)放在 a0 寄存器上,然后跳转到操作系统的入口地址处继续执行。

extern "C" fn main(_hartid: usize, device_tree_paddr: usize) {
   ...
   init_dt(device_tree_paddr);
   ...
}

解析设备信息

Qemu中Virtio Over MMIO方式没有基于总线的设备探测机制。 所以操作系统采用Device Tree的方式来探测各种基于MMIO方式的virtio设备,从而操作系统能知道与设备相关的寄存器和所用的中断。基于MMIO方式的virtio设备提供了一组内存映射的控制寄存器,后跟一个设备特定的配置空间,在形式上是位于一个特定地址上的内存区域。一旦操作系统找到了这个内存区域,就可以获得与这个设备相关的各种寄存器信息。

  1. 根据传入的DTB的物理地址获取设备树信息
  2. 验证 Magic Number,这是为了保证系统可靠性,验证这段内存是否存放了设备树信息。
  3. 加载dbt数据,遍历dbt数据
  4. 在遍历过程中,一旦发现了一个支持 "virtio,mmio" 的设备(其实就是 QEMU 模拟的各种virtio设备),就进入下一步加载驱动的逻辑。
  5. virtio驱动程序的执行过程可参考:https://gitee.com/rcore-os/rCore-Tutorial-Book-v3/blob/main/source/chapter9/2device-driver-2.rst
fn init_dt(dtb: usize) {
    info!("device tree @ {:#x}", dtb);
    #[repr(C)]
    struct DtbHeader {
        be_magic: u32,
        be_size: u32,
    }
    let header = unsafe { &*(dtb as *const DtbHeader) };
    let magic = u32::from_be(header.be_magic);
    const DEVICE_TREE_MAGIC: u32 = 0xd00dfeed;
    assert_eq!(magic, DEVICE_TREE_MAGIC);
    let size = u32::from_be(header.be_size);
    let dtb_data = unsafe { core::slice::from_raw_parts(dtb as *const u8, size as usize) };
    let dt = DeviceTree::load(dtb_data).expect("failed to parse device tree");
    walk_dt_node(&dt.root); //遍历数据
}

//发现了一个支持 "virtio,mmio" 的设备,就进入下一步加载驱动的逻辑
fn walk_dt_node(dt: &Node) {
    if let Ok(compatible) = dt.prop_str("compatible") {
        if compatible == "virtio,mmio" {
            virtio_probe(dt);
        }
    }
    for child in dt.children.iter() {
        walk_dt_node(child);
    }
}
//对不同类型设备的处理
fn virtio_probe(node: &Node) {
    if let Some(reg) = node.prop_raw("reg") {
        let paddr = reg.as_slice().read_be_u64(0).unwrap();
        let size = reg.as_slice().read_be_u64(8).unwrap();
        let vaddr = paddr;
        info!("walk dt addr={:#x}, size={:#x}", paddr, size);
        let header = unsafe { &mut *(vaddr as *mut VirtIOHeader) };
        info!(
            "Detected virtio device with vendor id {:#X}",
            header.vendor_id()
        );
        info!("Device tree node {:?}", node);
        match header.device_type() {
            DeviceType::Block => virtio_blk(header),
            DeviceType::GPU => virtio_gpu(header),
            DeviceType::Input => virtio_input(header),
            DeviceType::Network => virtio_net(header),
            t => warn!("Unrecognized virtio device: {:?}",t),
        }
    }
}

Qemu启动参数设置

qemu-system-riscv64 \
		-machine virt \
		-serial mon:stdio \
		-bios default \
		-kernel $(kernel) \
		-drive file=$(img),if=none,format=raw,id=x0 \
		-device virtio-blk-device,drive=x0 \
		-device virtio-gpu-device \
		-device virtio-mouse-device \
		-device virtio-net-device,netdev=net0\
		-netdev tap,id=net0,"helper=/usr/lib/qemu/qemu-bridge-helper"

当添加网络设备时(-netdev),网络配置常见的有两种模式,一种是user模式,一种是tap模式。

user模式的客户机可以连通宿主机及外部网络。用户模式网络完全由QEMU模拟实现整个TCP/IP协议栈,并且使用这个协议栈提供一个虚拟的NAT网络,负责将qemu所模拟的系统网络请求转发到外部网卡上,从而实现网络通信。

tap模式表明在主机上增加一块虚拟网络设备,然后就可以象真实网卡一样配置它这种方式要比user mode复杂一些,但是设置好后 虚拟机<-->互联网虚拟机<-->主机通信都很容易。默认的网络配置脚本是 /etc/qemu-ifup,默认的网络解除配置脚本是 /etc/qemu-ifdown。 使用 script=no 或 downscript=no 禁用脚本执行。也可使用网络助手配置 TAP 接口并将其附加到网桥。 默认的网络助手可执行文件是 /path/to/qemu-bridge-helper,默认的网桥设备是 br0。

  1. 安装网桥工具
sudo apt install bridge-utils
sudo apt install uml-utilities
  1. 创建相应文件(*/bridge.conf),否则会出现以下错误
failed to parse default acl file `/etc/qemu/bridge.conf'
qemu-system-riscv64: bridge helper failed
  1. echo "allow br0" > /etc/qemu/bridge.conf否则出现以下错误
access denied by acl file
qemu-system-riscv64: bridge helper failed
failed to create tun device: Operation not permitted
qemu-system-riscv64: bridge helper failed

//解决方案
sudo chmod u+s /usr/lib/qemu/qemu-bridge-helper
failed to get mtu of bridge `br0': No such device
qemu-system-riscv64: bridge helper failed

//解决方案
sudo brctl addbr br0
sudo ip link set br0 up