继续讲解vhci-hcd驱动。
上一篇文章讲了vhci-hcd的初始化流程,本文讲解usbip attach -r <server端ip地址> -b <busid>时驱动做了什么内容。
我们知道vhci_start()函数的最后,注册了sysfs的用户界面,用于配置vhci-hcd驱动,我们直接阅读drivers/usb/usbip/vhci_sysfs.c的attach的操作:
/* Sysfs entry to establish a virtual connection */ /* * To start a new USB/IP attachment, a userland program needs to setup a TCP * connection and then write its socket descriptor with remote device * information into this sysfs file. * * A remote device is virtually attached to the root-hub port of @rhport with * @speed. @devid is embedded into a request to specify the remote device in a * server host. * * write() returns 0 on success, else negative errno. */ static ssize_t attach_store(struct device *dev, struct device_attribute *attr, const char *buf, size_t count) { struct socket *socket; int sockfd = 0; __u32 port = 0, pdev_nr = 0, rhport = 0, devid = 0, speed = 0; struct usb_hcd *hcd; struct vhci_hcd *vhci_hcd; struct vhci_device *vdev; struct vhci *vhci; /* * @rhport: port number of vhci_hcd * @sockfd: socket descriptor of an established TCP connection * @devid: unique device identifier in a remote host * @speed: usb device speed in a remote host */ if (sscanf(buf, "%u %u %u %u", &port, &sockfd, &devid, &speed) != 4) return -EINVAL; ... rhport = port_to_rhport(port); ... hcd = platform_get_drvdata(vhcis->pdev); ... vhci_hcd = hcd_to_vhci_hcd(hcd); vhci = vhci_hcd->vhci; vdev = &vhci->vhci_hcd_hs->vdev[rhport]; /* Extract socket from fd. */ socket = sockfd_lookup(sockfd, &err); .. /* now need lock until setting vdev status as used */ vdev->devid = devid; vdev->speed = speed; vdev->ud.sockfd = sockfd; vdev->ud.tcp_socket = socket; vdev->ud.status = VDEV_ST_NOTASSIGNED; ... vdev->ud.tcp_rx = kthread_get_run(vhci_rx_loop, &vdev->ud, "vhci_rx"); vdev->ud.tcp_tx = kthread_get_run(vhci_tx_loop, &vdev->ud, "vhci_tx"); rh_port_connect(vdev, speed); return count; }为了方便描述,删除了代码中的错误处理、自旋锁初始化和usb3.0部分以及默认只有一个hcd控制器等。可以看出从上层(用户态)得到buf字符串数据,使用sscanf格式化后初始化虚拟root hub的端口(usb hub有多个USB口)、usb速度、socket描述符(句柄)以及设备id(devid)。
static inline __u32 port_to_rhport(__u32 port) { return port % VHCI_HC_PORTS; }因为vhci-hcd被设计为最多支持VHCI_HC_PORTS(8)个端口。
socket = sockfd_lookup(sockfd, &err);则是将用户态下的socket句柄转换成内核适用的socket描述符,供内核调用kernel_recvmsg和kernel_sendmsg时使用。因为前面说过,tcp的建立连接是在应用层做的,底层驱动只是使用已经打开了的socket链路。 然后使用kthread_get_run()创建了两个内核线程vhci_rx_loop和vhci_tx_loop,在shell中使用top命令能看到进程名称为"vhci_rx"和"vhci_tx"。这两个内核线程很重要,基本上USBIP_CMD_SUBMIT、USBIP_RET_SUBMIT、USBIP_CMD_UNLINK和USBIP_RET_UNLINK命令都是靠这两个线程处理。下文会专门分析这两个线程。
最后是调用rh_port_connect(vdev, speed);这个函数功能十分重要,就是前文说的“踢一下vhci-hcd虚拟出来的主机控制器的root hub,让hub.c以为有真实的usb设备插入”:
void rh_port_connect(struct vhci_device *vdev, enum usb_device_speed speed) { struct vhci_hcd *vhci_hcd = vdev_to_vhci_hcd(vdev); struct vhci *vhci = vhci_hcd->vhci; int rhport = vdev->rhport; u32 status; unsigned long flags; usbip_dbg_vhci_rh("rh_port_connect %d\n", rhport); spin_lock_irqsave(&vhci->lock, flags); status = vhci_hcd->port_status[rhport]; status |= USB_PORT_STAT_CONNECTION | (1 << USB_PORT_FEAT_C_CONNECTION); switch (speed) { case USB_SPEED_HIGH: status |= USB_PORT_STAT_HIGH_SPEED; break; case USB_SPEED_LOW: status |= USB_PORT_STAT_LOW_SPEED; break; default: break; } vhci_hcd->port_status[rhport] = status; spin_unlock_irqrestore(&vhci->lock, flags); usb_hcd_poll_rh_status(vhci_hcd_to_hcd(vhci_hcd)); }该函数主要是设置了一下状态status |= USB_PORT_STAT_CONNECTION | (1 << USB_PORT_FEAT_C_CONNECTION) | USB_PORT_STAT_HIGH_SPEED,最后调用usb_hcd_poll_rh_status(vhci_hcd_to_hcd(vhci_hcd));进行触发。usb_hcd_poll_rh_status是usb core的接口,我们不去分析,作用就是刚刚说的“踢一下hcd的root bub”,此时就会去回调struct hc_driver vhci_hc_driver的.hub_status_data,对于vhci-hcd实例为vhci_hub_status():
/* * Returns 0 if the status hasn't changed, or the number of bytes in buf. * Ports are 0-indexed from the HCD point of view, * and 1-indexed from the USB core pointer of view. * * @buf: a bitmap to show which port status has been changed. * bit 0: reserved * bit 1: the status of port 0 has been changed. * bit 2: the status of port 1 has been changed. * ... */ static int vhci_hub_status(struct usb_hcd *hcd, char *buf) { struct vhci_hcd *vhci_hcd = hcd_to_vhci_hcd(hcd); struct vhci *vhci = vhci_hcd->vhci; int retval = DIV_ROUND_UP(VHCI_HC_PORTS + 1, 8); int rhport; int changed = 0; unsigned long flags; memset(buf, 0, retval); spin_lock_irqsave(&vhci->lock, flags); if (!HCD_HW_ACCESSIBLE(hcd)) { usbip_dbg_vhci_rh("hw accessible flag not on?\n"); goto done; } /* check pseudo status register for each port */ for (rhport = 0; rhport < VHCI_HC_PORTS; rhport++) { if ((vhci_hcd->port_status[rhport] & PORT_C_MASK)) { /* The status of a port has been changed, */ usbip_dbg_vhci_rh("port %d status changed\n", rhport); buf[(rhport + 1) / 8] |= 1 << (rhport + 1) % 8; changed = 1; } } if ((hcd->state == HC_STATE_SUSPENDED) && (changed == 1)) usb_hcd_resume_root_hub(hcd); done: spin_unlock_irqrestore(&vhci->lock, flags); return changed ? retval : 0; }其实作用就是把virtual root hub的端口号上报给“hcd框架”,hcd框架就会回调struct hc_driver vhci_hc_driver的.hub_control,对于我们vhci-hcd实例就是vhci_hub_control()函数:
hub.c的hub_event -> hub_port_init -> usb_control_msg -> usb_alloc_urb被执行,调用usb_submit_urb后最终会回调vhci_urb_enqueue() 上面的流程描述了hub.c检测到设备插入后,做什么。 紧跟着,对于usb_submit_urb则有这样的流程(主要关注“根hub”的urb路径): usb_submit_urb ->usb_hcd_submit_urb(urb, mem_flags); ->rh_urb_enqueue //如果判断是根hub,即is_root_hub() is true就走这个分支 ->rh_call_control ->hub_control回调 //对于vhci-hcd为vhci_hub_control |->status = hcd->driver->urb_enqueue(hcd, urb, mem_flags);//不是root hub,而是诸如U盘驱动、usb-skeleton.c等接口驱动产生的urb时就发送到enqueuevhci_hub_control()函数比较复杂!目的是回应“hcd框架”针对virtual root hub端口号检测到有设备插入的这件事作出反馈,譬如根据下发hub特有的请求回应相应数据,或者假装设置标志等等,譬如:
GetHubDescriptor DeviceRequest | USB_REQ_GET_DESCRIPTOR GetHubStatus GetPortStatus SetPortFeature等等hub请求,这样才能模拟出一个“接近真实usb hub”的hub,不然“hcd框架”会看出端倪,就虚拟不了hcd了。具体不分析了,有兴趣的读者可以研究一下,因为分析这个必然会牵涉到usb core中hcd.c和hub.c这两块硬骨头,我还是回归主题——继续vhci-hcd驱动本身。
有个问题,就是当root hub检测到有usb设备插入,最后是怎么加载U盘驱动的?我做了简单的代码走读,通过枚举到的PID/VID信息匹配到U盘驱动或者HID键鼠驱动等,就回调相应驱动的probe驱动入口了,最后就能看到/dev/sda或者/dev/input/even0了:
rh_port_connect ->usb_hcd_poll_rh_status //hcd.c ->hcd->driver->hub_status_data(hcd, buffer)//vhci_hub_status ->usb_hcd_unlink_urb_from_ep(hcd, urb); ->usb_hcd_giveback_urb(hcd, urb, 0) ->usb_giveback_urb_bh();//tasklet_hi_schedule(&bh->bh); ->__usb_hcd_giveback_urb(urb); ->urb->complete(urb);//hub_irq ->hub_irq //hub.c usb_fill_int_urb(hub->urb, hdev, pipe, *hub->buffer, maxp, hub_irq, ->kick_hub_wq(hub); ->hub_event //INIT_WORK(&hub->events, hub_event); ->port_event(hub, i); ->hub_port_connect_change ->hub_port_connect ->hub_port_init ->usb_new_device(udev); ->usb_enumerate_device(udev);//开始枚举 ->device_add(&udev->dev);//枚举完毕后加载设备驱动device_add函数会出发总线的通知链发送通知,最终会调用总线的match方法 usb设备和驱动一旦match,则会调用驱动的drvwrap.driver.probe方法: 若是设备则通过driver.c的usb_register_device_driver函数调用usb_probe_device方法 若是接口则通过driver.c的usb_register_driver函数调用usb_probe_interface方法 假设是U盘接入,则调用mass_storage驱动的probe,并在probe中使用usb_alloc_urb分配urb,最后usb_submit_urb提交urb。 此时会进入绑定mass_storage驱动的hcd的urb_enqueue,由于此处是vhci-hcd,故会进入vhci_urb_enqueue()回调。
最重要的函数到了!
是struct hc_driver vhci_hc_driver的.urb_enqueue回调。即vhci_urb_enqueue(),我们在前面的文章也简单用文字描述过,就是U盘驱动或者USB骨架驱动(usb-skeleton.c)在读写设备时,离不开urb的创建和提交(usb_submit_urb):
static inline void usb_fill_bulk_urb(struct urb *urb, struct usb_device *dev, unsigned int pipe, void *transfer_buffer, int buffer_length, usb_complete_t complete_fn, void *context) { urb->dev = dev; urb->pipe = pipe; urb->transfer_buffer = transfer_buffer; urb->transfer_buffer_length = buffer_length; urb->complete = complete_fn; urb->context = context; } //简单举例,摘自usb-skeleton.c urb = usb_alloc_urb(0, GFP_KERNEL); ... usb_fill_bulk_urb(urb, dev->udev,... ); urb->transfer_flags |= URB_NO_TRANSFER_DMA_MAP; ... retval = usb_submit_urb(urb, GFP_KERNEL);usb_submit_urb()是异步函数,usb_fill_bulk_urb()有注册完成函数,urb提交后usb_submit_urb()立马返回,usb设备处理完后usb core就会回调urb->complete()。
跟踪usb_submit_urb()代码,我们发现urb最终是去到hcd驱动注册的.urb_enqueue,对于我们vhci-hcd就是static int vhci_urb_enqueue(struct usb_hcd *hcd, struct urb *urb, gfp_t mem_flags)了,看到struct urb *urb参数没?这个urb就是从上层驱动(usb-skeleton.c或者U盘驱动、hid键鼠驱动等)传下来的。也就是说该urb包含了usb通信数据,我们要做usb共享(透传),第一步就是截获上层驱动的usb通信数据,通过tcp发送出去。
vhci_urb_enqueue()函数里比较关键的函数是:usb_hcd_link_urb_to_ep(hcd, urb)和vhci_tx_urb(urb, vdev),usb_hcd_link_urb_to_ep()是usb core的api,目的是hcd主机控制器把urb放入端点队列中,等待底层发送。而vhci_tx_urb()则是通过分配struct vhci_priv实例,填充urb并加入到priv_tx队列,最后唤醒发送线程(就是上文说的vhci_tx_loop())进行发送。发送线程这边则通过dequeue_from_priv_tx从priv_tx队列提取出urb,并插入到另外一个priv_rx队列中, 最后将urb通过IP发送(USBIP_CMD_SUBMIT)。细节自行阅读代码,尤其是怎么从urb获取到有用数据,然后填充到USBIP_CMD_SUBMIT命令的字段中。
接收线程(就是上文说的vhci_rx_loop())则通过pickup_urb_and_free_priv,从priv_rx队列中提取符合帧序号的urb(发送时和接收的均使用同一个urb对象,所以要利用seqnum来找回之前发送时用的那个urb,类似于“回话ID”的概念),并删除在priv_rx的节点。紧跟着,通过socket对urb进行接收,接收完成后,调用usb_hcd_unlink_urb_from_ep()和usb_hcd_giveback_urb()将urb从usb core归还给设备驱动,完成一次urb的处理:
static void vhci_recv_ret_submit(struct vhci_device *vdev, struct usbip_header *pdu) { ... ... spin_lock_irqsave(&vhci->lock, flags); usb_hcd_unlink_urb_from_ep(vhci_hcd_to_hcd(vhci_hcd), urb); spin_unlock_irqrestore(&vhci->lock, flags); usb_hcd_giveback_urb(vhci_hcd_to_hcd(vhci_hcd), urb, urb->status); usbip_dbg_vhci_rx("Leave\n"); }归还后,usb core就会回调由U盘驱动、usb-skeleton.c等所注册的完成函数(urb->complete())。
因为urb的发送和接收过程基本上使用“生产者消费者模式”,就以文字描述,不具体分析代码了,读者可自行阅读。注意linux内核的链表操作,譬如list_move_tail()是将一个urb对象从一个发送队列priv_tx中取下来,然后放到接收队列priv_rx里。
另外,当处于控制传输阶段(使用0号端点),而且设备地址为0时,vhci_urb_enqueue()中对在这个阶段的特殊的“标准请求”(如用于分配usb设备地址的USB_REQ_SET_ADDRESS请求等)并没有发送到远端的server,而是直接本地处理掉了(goto no_need_xmit),马上归还urb给U盘驱动:
/* * The enumeration process is as follows; * * 1. Get_Descriptor request to DevAddrs(0) EndPoint(0) * to get max packet length of default pipe * * 2. Set_Address request to DevAddr(0) EndPoint(0) * */ if (usb_pipedevice(urb->pipe) == 0) { ... ... switch (ctrlreq->bRequest) { case USB_REQ_SET_ADDRESS: /* set_address may come when a device is reset */ dev_info(dev, "SetAddress Request (%d) to port %d\n", ctrlreq->wValue, vdev->rhport); usb_put_dev(vdev->udev); vdev->udev = usb_get_dev(urb->dev); spin_lock(&vdev->ud.lock); vdev->ud.status = VDEV_ST_USED; spin_unlock(&vdev->ud.lock); if (urb->status == -EINPROGRESS) { /* This request is successfully completed. */ /* If not -EINPROGRESS, possibly unlinked. */ urb->status = 0; } goto no_need_xmit; case USB_REQ_GET_DESCRIPTOR: if (ctrlreq->wValue == cpu_to_le16(USB_DT_DEVICE << 8)) usbip_dbg_vhci_hc( "Not yet?:Get_Descriptor to device 0 (get max pipe size)\n"); usb_put_dev(vdev->udev); vdev->udev = usb_get_dev(urb->dev); goto out; default: /* NOT REACHED */ dev_err(dev, "invalid request to devnum 0 bRequest %u, wValue %u\n", ctrlreq->bRequest, ctrlreq->wValue); ret = -EINVAL; goto no_need_xmit; ... ... ... no_need_xmit: usb_hcd_unlink_urb_from_ep(hcd, urb); no_need_unlink: spin_unlock_irqrestore(&vhci->lock, flags); if (!ret) usb_hcd_giveback_urb(hcd, urb, urb->status); return ret; }也能理解,毕竟我们操作的,其实是远端的U盘,事实上,远端的U盘在插入到它自己的hcd主机控制器时,就经历了“分配usb设备地址”的阶段,无需再下发该请求到远端了。
最后总结一下,usb_hcd_link_urb_to_ep()与usb_hcd_unlink_urb_from_ep()/usb_hcd_giveback_urb()才是关键!当然,还有urb取出数据填充到USBIP_CMD_SUBMIT命令字段,以及从socket接收到USBIP_RET_SUBMIT的命令字段后怎么填充回urb也有细节需要注意。
下一篇文章讲解usbip-host.ko驱动的源码分析。