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