linux usb usbip驅動詳解(五)

      繼續講解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時就發送到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驅動的源碼分析。

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章