0%

qemu usb 设备直通

USB控制器类型

简单地讲,OHCI、UHCI都是USB1.1的接口标准,而EHCI是对应USB2.0的接口标准最新的xHCI是USB3.0的接口标准

  • OHCI(Open Host Controller Interface)是支持USB1.1的标准,但它不仅仅是针对USB,还支持其他的一些接口,比如它还支持Apple的火线(Firewire,IEEE 1394)接口。与UHCI相比,OHCI的硬件复杂,硬件做的事情更多,所以实现对应的软件驱动的任务,就相对较简单。主要用于非x86的USB,如扩展卡、嵌入式开发板的USB主控。
  • UHCI(Universal Host Controller Interface),是Intel主导的对USB1.0、1.1的接口标准,与OHCI不兼容。UHCI的软件驱动的任务重,需要做得比较复杂,但可以使用较便宜、较简单的硬件的USB控制器。Intel和VIA使用UHCI,而其余的硬件提供商使用OHCI。
  • EHCI(Enhanced Host Controller Interface),是Intel主导的USB2.0的接口标准。EHCI仅提供USB2.0的高速功能,而依靠UHCI或OHCI来提供对全速(full-speed)或低速(low-speed)设备的支持。
  • xHCI(eXtensible Host Controller Interface),是最新最火的USB3.0的接口标准,它在速度、节能、虚拟化等方面都比前面3中有了较大的提高。xHCI支持所有种类速度的USB设备(USB 3.0 SuperSpeed, USB 2.0 Low-, Full-, and High-speed, USB 1.1 Low- and Full-speed)。xHCI的目的是为了替换前面3中(UHCI/OHCI/EHCI)。

usb 层级

用lsusb -t还可以看到USB设备的层级关系

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
[root@localhost xqk]# lsusb -t
/: Bus 02.Port 1: Dev 1, Class=root_hub, Driver=ehci-pci/2p, 480M
|__ Port 1: Dev 2, If 0, Class=Hub, Driver=hub/8p, 480M
|__ Port 1: Dev 3, If 0, Class=Mass Storage, Driver=usb-storage, 480M


Bus 02.Port 1
表示第二个USB主控制器,Port号为1

Dev 1, Class=root_hub
分配的设备号为1,类型是root_hub

Driver=ehci-pci/2p
root_hub的类型是ehci(usb 2.0),总共有两个port

|__ Port 1: Dev 2, If 0, Class=Hub, Driver=hub/8p, 480M
root_hub的其中一个port有个Hub设备,port的id是1,此Hub有8个port。

|__ Port 1: Dev 3, If 0, Class=Mass Storage, Driver=usb-storage, 480M
Hub的其中一个port有大容量USB设备,port的id为1.3(树状结构,依次以.作为低一级设备成员)

USB设备直通

虚拟机使用USB设备可以通过两种方式:

  • PCI 设备直通

    PCI usb card, 直接将 usb controller 直通给虚拟机

  • 设备直通

这里重点说下非PCI方式的usb 设备直通
qemu支持hostaddr,hostport以及vendorid,productid等信息的组合, 相对于libvirt,多了hostport的支持,hostport即是主机usb的port。

1
2
3
4
5
6
7
8
9
10
(1) vendorid+productid -- match for a specific device, pass it to
the guest when it shows up somewhere in the host.

(2) hostbus+hostport -- match for a specific physical port in the
host, any device which is plugged in there gets passed to the
guest.

(3) hostbus+hostaddr -- most useful for ad-hoc pass through as the
hostaddr isn't stable, the next time you plug in the device it
gets a new one ...

注意点

  • devnum会变化
    在host上同一个USB口上插拔U盘,会导致devnum的变化(增加),而port对应于USB的物理口,不会随着插拔U盘而变化,但是libvirt不支持传入port,只能自己适配开发。

  • 是否需要开启vt-d iommu
    通常情况下,直通物理设备(PCI)时需要开启vt-d,USB设备有些不同,USB设备是由controller控制,而各种类型的控制器都是qemu模拟的,只不过最终直接打开物理机上的USB设备而已,数据还是由qemu控制。因此USB设备直通不需要开启vt-d iommu。

  • No free USB ports
    模拟的控制器对应的port数量有限,如果要直通多个USB设备,就会造成port数目不够。piix3-uhci有两个port,ehci与nec-xhci有6个port(一般够用)。当数目不够时,可以添加hub设备来增加额外的port。

    1
    <hub type='usb'/>

    hub设备包含8个port。

  • 不要直通主机hub设备
    主机上的root_hub以及hub直通给虚拟机没有任何意义,一方面虚拟机中的root_hub是模拟控制器自带的,而hub是需要单独添加hub设备,另一方面直通hub设备,虚拟机占用主机hub,会导致主机上hub下的USB设备不可用。

qemu usb 非PCI 直通解读

qemu默认不支持 usb-host, 需要在编译时打开 --enable-libusb 编译, 依赖libusb 库.

按上一章的参数介绍, 简单看下代码结构
注意

USB设备是由controller控制,而各种类型的控制器都是qemu模拟的,只不过最终直接打开物理机上的USB设备而已,数据还是由qemu控制

qemu 命令行方式添加usb 直通设备

1
2
3
-device usb-host,hostbus=BUS,hostaddr=ADDR,id=[hostdev0]
-device usb-host,vendorid=VID,productid=PRID,id=[hostdev0]
# 对应lsusb的Bus xxx, Device xxx

注意上述命令需要以 sudo 运行 qemu 命令, 否则libusb 库无法打开给usb设备.

添加这个设备后, 最终会走到 TYPE_USB_HOST_DEVICE 设备的具现化函数中
TYPE_DEVICE -> TYPE_USB_DEVICE -> TYPE_USB_HOST_DEVICE

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
-+ usb_host_realize(USBDevice *udev, Error **errp)
\ - ldev = usb_host_find_ref(s->match.bus_num, s->match.addr);
"libusb 库函数查找 hostbus, hostaddr 匹配的usb设备"
| -+ usb_host_open(s, ldev, 0);
\ - bus_num = libusb_get_bus_number(dev); addr = libusb_get_device_address(dev);
s->dev = dev;
s->bus_num = bus_num;
s->addr = addr;
\ -+ usb_host_detach_kernel(s);
\ - libusb_detach_kernel_driver(s->dh, i); "从主机中解绑"
| - USBDevice *udev = USB_DEVICE(s);
| - usb_ep_init(udev);
| -+ usb_host_ep_update(s); "将设备绑定到 qemu 模拟的usb controller 上"
\ - libusb_get_active_config_descriptor(s->dev, &conf);
| -+ for i in conf->bNumInterfaces "遍历接口描述符"
\ - intf = &conf->interface[i].altsetting[0];
\ -+ for e in intf->bNumEndpoints "遍历端点描述符"
\ - endp = &intf->endpoint[e];
| - devep = endp->bEndpointAddress;
| - pid = (devep & USB_DIR_IN) ? USB_TOKEN_IN : USB_TOKEN_OUT; "是in 端点还是 out端点"
| - ep = devep & 0xf;
| -
| -+ usb_device_attach(udev, &local_err);
\ - USBPort *port = udev->port;
| -+ usb_attach(port);
\ - USBDevice *dev = port->dev;
| - port->ops->attach(port); "usb controller 注册的port, 如usb2.0 ehci"
| - dev->state = USB_STATE_ATTACHED;
| -+ usb_device_handle_attach(dev);
\ - klass->handle_attach(dev); "按设备类型进行, 不一定实现了这个方法"

这里重点说下 port->ops->attach(port) , 这个是由 qemu 模拟的usb controller 注册的port, 这里以usb2.0 的ehci 看下注册过程

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
void usb_ehci_realize(EHCIState *s, DeviceState *dev, Error **errp)
{
for (i = 0; i < s->portnr; i++) {
usb_register_port(&s->bus, &s->ports[i], s, i, &ehci_port_ops,
USB_SPEED_MASK_HIGH);
s->ports[i].dev = 0;
}
}
static USBPortOps ehci_port_ops = {
.attach = ehci_attach,
.detach = ehci_detach,
.child_detach = ehci_child_detach,
.wakeup = ehci_wakeup,
.complete = ehci_async_complete_packet,
};

所以 port->ops->attach(port) 指向了 ehci_port_ops.ehci_attach, 重点看下这个函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
static void ehci_attach(USBPort *port)
{
EHCIState *s = port->opaque;
uint32_t *portsc = &s->portsc[port->index];
const char *owner = (*portsc & PORTSC_POWNER) ? "comp" : "ehci";
...
*portsc |= PORTSC_CONNECT;
*portsc |= PORTSC_CSC; //该位表示 bus 上接入usb 设备
-+ ehci_raise_irq(s, USBSTS_PCD);
// 给guest 发送中断, 通知guest usb设备接入了, USBSTS_PCD 为usb的特定中断类型
\ ---+ qemu_set_irq(s->irq, level);
}

static void usb_ehci_sysbus_realize(DeviceState *dev, Error **errp)
{
SysBusDevice *d = SYS_BUS_DEVICE(dev);
EHCISysBusState *i = SYS_BUS_EHCI(dev);
EHCIState *s = &i->ehci;
usb_ehci_realize(s, dev, errp);
sysbus_init_irq(d, &s->irq); "中断的注册在这里"
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
    memory_region_init_io(&s->mem_opreg, OBJECT(dev), &ehci_mmio_opreg_ops, s,
"operational", s->portscbase);
s->async_bh = qemu_bh_new(ehci_work_bh, s);
static void ehci_opreg_write(void *ptr, hwaddr addr,
uint64_t val, unsigned size) {
...
qemu_bh_schedule(s->async_bh);
}
static void ehci_work_bh(void *opaque) {
-+ ehci_advance_async_state(ehci);
\ -+ ehci_advance_state(EHCIState *ehci, int async)
\ -+ ehci_state_execute(q);
\ -+ ehci_execute
\ -+ usb_handle_packet(p->queue->dev, &p->packet);
\ -+ usb_process_one(p);
\ -+ usb_device_handle_data(dev, p);
\ -+ klass->handle_data(dev, p);
\ -+ usb_host_handle_data(dev, p)
\ - libusb_fill_bulk_transfer()
| - libusb_submit_transfer(r->xfer);
}

从上述过程中可以大概了解到

  1. 传入的usb设备需要先借助libusb 库与host 进行解绑 detach, 此时该usb设备与host就没啥关系了
  2. 与host解绑后, 需要绑定到qemu模拟的usb 控制器上, 由qemu 模拟的usb控制器控制usb设备的行为, usb控制器有MemoryRegion等的设定, 以让 guest 进行io的模拟
  3. 中断相关的仍然是使用的qemu的虚拟的中断控制器进行中断的触发
  4. 数据流中最终起作用的是 usb 设备的ep 端点, 最终数据流通过 libusb_*_transfer 等数据相关的接口以ep参数控制对应的端点进行发送接收数据.