继续讲解vhci-hcd驱动。
上一篇文章讲了vhci-hcd的初始化流程,本文讲解usbip attach -r
我们知道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时就发送到enqueue
vhci_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驱动的源码分析。