1、usbip 框架
参考:https://blog.csdn.net/litao31415/category_9292335.html
1.1 usbip 功能介绍
usbip
驱动提供了linux
下USB 透传(bypass
)的功能,或者说usb over tcp
,利用以太网,将usb 设备共享到另外一端。
usbip
分为两端:server
端和client
端,它们分别运行在不同的linux
主机中,其中运行server
端插入usb 设备(如U盘等),用于共享。此时client
端就可以 attach 到server
端,此时client端就能看到 u盘,它以为真的有一个 u盘插入本机,并为其安装驱动,效果跟在本机上直接插入U盘无异,实现了 u盘共享,或者USB延长器的功效。
1.2 usbip 使用介绍
内核的根目录下tools/usb/usbip
,这个是应用程序的,用来配置(控制)usbip
驱动。
usbip
有一套协议,协议文档在kernel 源码根目录下的drivers/usb/usbip/usbip_protocol.txt
,比较新的版本在Documentation/usb/usbip_protocol.txt
。
1.2.1 server 端
在server
端PC上:
(1)加载驱动,运行应用程序;
sudo modprobe usbip-host //加载server 端驱动
obj-$(CONFIG_USBIP_HOST) += usbip-host.o
usbip-host-y := stub_dev.o stub_main.o stub_rx.o stub_tx.
usbipd -D //运行usbipd程序,该守护进程用于建立TCP socket连接等
(2)插入一个U盘/鼠标,此时usbip list -l
应该能列举出设备,用于查看busid
:
usbip list -l
- busid 2-1.1 (046d:c077)
Logitech, Inc. : M105 Optical Mouse (046d:c077)
(3)绑定U盘,以便于共享,有complete
字眼说明绑定成功:
usbip bind -b 2-1.1 //将usb 绑定到usbip-host 驱动下
usbip: info: bind device on busid 2-1.1: complete
usbip unbind -b 2-1.1 //解绑,用于关闭usb的共享
1.2.2 client 端
在client
端 PC 上:
(1)加载驱动;
modprobe vhci-hcd
obj-$(CONFIG_USBIP_VHCI_HCD) += vhci-hcd.o
vhci-hcd-y := vhci_sysfs.o vhci_tx.o vhci_rx.o vhci_hcd.o
(2)列举出server
端已经导出的 usb 设备;
#usbip list –r <server端ip地址>
usbip list -r 192.168.100.191
(3)将server
端的 usb 设备attach
到本地client
;
#usbip attach -r <server端ip地址> -b <busid>
usbip attach -r 192.168.100.191 -b 2-1.1
usbip detach -p 0 #可以断开usb设备
2、usbip 实现
驱动源码目录(server 和 client):\drivers\usb\usbip
2.1 usbip 实现原理
server
和client
两边在恰当的时机分别隔断各自系统的 usb 通信流程,然后巧妙地交换数据,各自系统都察觉不到。
在C/S
模式中基本都是client
发出请求,阅读usbip工具代码可以知道,tcp 的创建和建立连接均在应用程序usbip
工具上创建,至于驱动需要使用 socket 时,是采取将 socket 的描述符传入内核,内核利用这个句柄,就能直接利用这个已打开的socket进行通信,无需再在内核建立连接之类的,linux内核会使用kernel_sendmsg
和kernel_recvmsg
发送和接收 tcp 数据。
client
端是使用“usbip attach -r 192.168.100.191 -b 2-1.1
”把usb 设备 attach 到本地的,其实这个工具的attach 操作就是类似于 usb 的“热插拔”,踢一下 vhci-hcd
虚拟出来的主机控制器的 root hub
(任何主机控制器都有一个根hub),这时 hub.c
就以为有真实的 usb设备插入。
只要我们模拟出root hub
端口号以及端口状态值给hub.c,就能蒙骗它,让它以为真的有硬件插入,此时hub.c就会发出枚举usb设备的“请求设备描述符”给root hub,最后给到urb_enqueue,vhci-hcd就是实现一个vhci_urb_enqueue,并注册到struct hc_driver对象的.urb_enqueue成员函数里,vhci_urb_enqueue的功能不是像真实主机控制器驱动那样,操控寄存器,操控DMA,而是通过socket发送出去,给server端的真实主机控制器那边接收,由于server端(usbip-host)真的存在有usb主机控制器,所以把从client(vhci-hcd)的socket发出来的usb数据接收到,重新组装好urb,通过usb core模块的usb_submit_urb接口传给真实的主机控制器里,就能跟接在host端pc的U盘通信了,既然链路已经通了,client端的U盘驱动的其他操作(写入U盘数据或者读取U盘里的文件等)就能按照上面的链路走了,能完全操控host端的真实的U盘了!
2.1 client 端
2.1.1 usbip attach
vhci_start()
函数的最后,注册了sysfs 的用户界面,用于配置 vhci-hcd
驱动。
usbip attach -r 192.168.100.191 -b 2-1.1 // usbip应用程序
== attach_store() // 内核驱动接口drivers/usb/usbip/vhci_sysfs.c
== 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");
== vhci_tx_loop(void *data);
== wait_event_interruptible(vdev->waitq_tx,
(!list_empty(&vdev->priv_tx) ||
!list_empty(&vdev->unlink_tx) ||
kthread_should_stop())); // 等待唤醒发送数据
== rh_port_connect(vdev, speed); // 踢一下vhci-hcd虚拟出来的主机控制器的root hub,让hub.c以为有真实的usb设备插入
== usb_hcd_poll_rh_status(struct usb_hcd *hcd);
== hcd->driver->hub_status_data(hcd, buffer);
== vhci_hub_status(struct usb_hcd *hcd, char *buf); // buf: a bitmap to show which port status has been changed
== usb_hcd_unlink_urb_from_ep(hcd, urb);
== usb_hcd_giveback_urb(hcd, urb, 0); //vhci_hub_status 有change 发生
== urb->complete(urb); //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); //枚举完毕后加载设备驱动
2.1.2 vhci_urb_enqueue
client
端通过 usb-storage
设备驱动与 U盘进行数据交流。
== usb_submit_urb(urb, GFP_KERNEL); // urb就是从上层驱动(usb-skeleton.c或者U盘驱动、hid键鼠驱动等)传下来
== usb_hcd_submit_urb(urb, mem_flags);
== hcd->driver->urb_enqueue(hcd, urb, mem_flags);
== vhci_urb_enqueue(struct usb_hcd *hcd, struct urb *urb, gfp_t mem_flags);
== usb_hcd_link_urb_to_ep(hcd, urb); // hcd主机控制器把urb放入端点队列中,等待底层发送
== vhci_tx_urb(urb, vdev);
== list_add_tail(&priv->list, &vdev->priv_tx); // 通过分配 struct vhci_priv 实例,填充 urb 并加入到 priv_tx 队列
== wake_up(&vdev->waitq_tx); // 唤醒发送线程 vhci_tx_loop() 进行发送
== vhci_tx_loop(); // 发送线程是没有跑的,只有唤醒事件发生才有发送数据
== vhci_send_cmd_submit(vdev);
== dequeue_from_priv_tx(vdev); // 从 priv_tx 队列提取出urb,并插入到另外一个 priv_rx 队列中,最后将urb通过IP发送(USBIP_CMD_SUBMIT)
== kernel_sendmsg(struct socket *sock, struct msghdr *msg,
struct kvec *vec, size_t num, size_t size); //发送数据
== sock_sendmsg(sock, msg);
// 接收线程是一直在跑的,除非有停止事件发生
== vhci_rx_loop(void *data);
== vhci_rx_pdu(ud);
== usbip_recv(ud->tcp_socket, &pdu, sizeof(pdu));
== sock_recvmsg(sock, &msg, MSG_WAITALL);
== vhci_recv_ret_submit(vdev, &pdu);
== pickup_urb_and_free_priv(vdev, pdu->base.seqnum); //从priv_rx队列中提取符合帧序号的urb(发送时和接收的均使用同一个urb对象,所以要利用seqnum来找回之前发送时用的那个urb,类似于“回话ID”的概念),并删除在priv_rx的节点
== usb_hcd_unlink_urb_from_ep(vhci_hcd_to_hcd(vhci_hcd), urb);
== usb_hcd_giveback_urb(vhci_hcd_to_hcd(vhci_hcd), urb, urb->status);
== urb->complete(urb); //返回给设备驱动程序
2.2 server 端
2.2.1 初始化
驱动初始化,创建sys 节点:
/sys/bus/usb/drivers/usbip-host # ls
bind match_busid module rebind uevent unbind
== usbip_host_init();
== usb_register_device_driver(&stub_driver, THIS_MODULE); // 注册一个device_driver
== driver_create_file(&stub_driver.drvwrap.driver, &driver_attr_match_busid); // 创建 match_busid 节点
== sysfs_create_file(&drv->p->kobj, &attr->attr); // driver 的kobj
== driver_create_file(&stub_driver.drvwrap.driver, &driver_attr_rebind); // 创建 rebind 节点
2.2.2 bind
当用户态使用工具“usbip bind -b <busid>
”时,usbip-host 驱动就会调用 rebind_store()
,它调用了device_attach(&bid->udev->dev)
,使得usb 设备(U盘/鼠标等)从原来的驱动(U盘驱动/HID鼠标驱动)中脱掉,然后改为挂到 usbip-host 驱动下,这样 server 这端的 u盘就能跟 usbip-host 驱动通信了,自然就能获取到跟 U盘通信的 urb 对象了,为后续通过 tcp传输 urb 打下了坚实的基础!
== rebind_store(struct device_driver *dev, const char *buf, size_t count);
== do_rebind(char *busid, struct bus_id_priv *busid_priv);
== device_attach(&busid_priv->udev->dev); // try to attach device to a driver
// 驱动重新绑定成功后,执行probe
== stub_probe(struct usb_device *udev);
== stub_device_alloc(udev); // 构建 struct stub_device *sdev;
== dev_set_drvdata(&udev->dev, sdev); // set private data to usb_device
== usb_hub_claim_port(udev->parent, udev->portnum,
(struct usb_dev_state *) udev); // 霸占一个usb hub的port
// struct attribute_group **dev_groups; 注意dev_group 的用法
struct usb_device_driver stub_driver = {
.name = "usbip-host",
.probe = stub_probe,
.disconnect = stub_disconnect,
#ifdef CONFIG_PM
.suspend = stub_suspend,
.resume = stub_resume,
#endif
.supports_autosuspend = 0,
.dev_groups = usbip_groups, //提供attr 文件系统操作接口
};
static ssize_t usbip_sockfd_store(struct device *dev, struct device_attribute *attr,
const char *buf, size_t count)
{
...
}
static DEVICE_ATTR_WO(usbip_sockfd); //对应 dev_attr_usbip_sockfd.attr,
static struct attribute *usbip_attrs[] = {
&dev_attr_usbip_status.attr,
&dev_attr_usbip_sockfd.attr,
&dev_attr_usbip_debug.attr,
NULL,
};
ATTRIBUTE_GROUPS(usbip); // 对应前面的.dev_groups = usbip_groups,
2.2.3 rx & tx
// 上层sys 节点操作 attr
== usbip_sockfd_store();
== socket = sockfd_lookup(sockfd, &err); // 从用户态获取到socket后,转换成内核适用的socket对象
== sdev->ud.tcp_socket = socket; // 并保存到本驱动的描述结构体(struct stub_device)中
== sdev->ud.sockfd = sockfd;
== sdev->ud.tcp_rx = kthread_get_run(stub_rx_loop, &sdev->ud,
"stub_rx"); //创建内核线程,接收来自vhci-hcd驱动的urb命令消息
== sdev->ud.tcp_tx = kthread_get_run(stub_tx_loop, &sdev->ud,
"stub_tx"); //创建内核线程,发送本地usb交互产生的urb消息给对端的vhci-hcd
== stub_rx_loop(void *data);
== stub_rx_pdu(ud);
== usbip_recv(ud->tcp_socket, &pdu, sizeof(pdu));
== sock_recvmsg(sock, &msg, MSG_WAITALL);
== stub_recv_cmd_submit(sdev, &pdu);
== priv->urbs[0] = usb_alloc_urb(np, GFP_KERNEL); //分配好一个空白的urb对象
== priv->urbs[i]->complete = stub_complete; //注册回调函数
== usb_submit_urb(priv->urbs[i], GFP_KERNEL); //提交urb,
usb 主机控制器驱动(hcd)的.urb_enqueue
就会收到提交的 urb,最后通过 DMA 把数据传给 SOC 的usb 控制器,给到U盘,U盘如果处理完了,就把数据发给 usb主机控制器驱动(hcd),最后归还端点(譬如调用usb_hcd_unlink_urb_from_ep()
和usb_hcd_giveback_urb()
),这时usb core 就会回调完成函数stub_complete()
,完成一次 U盘交互过程。
== stub_complete(struct urb *urb);
== list_move_tail(&priv->list, &sdev->priv_tx); //将承载有urb的链表节点(node)pop出去,然后改为放到发送链表priv_tx中,用于就绪从usbip-host发送到vhci-hcd
== wake_up(&sdev->tx_waitq); //唤醒发送线程,进行tcp组包和发送
== stub_tx_loop(void *data); // 将数据发送给client 端vhci
== stub_send_ret_submit(struct stub_device *sdev);
== dequeue_from_priv_tx(vdev); // 从 priv_tx 队列提取出urb
== kernel_sendmsg(struct socket *sock, struct msghdr *msg,
struct kvec *vec, size_t num, size_t size); //发送数据
== sock_sendmsg(sock, msg);
3、unlink 操作
unlink
的 urb 是 U盘驱动下发 usb_unlink_urb(struct urb *urb)
、 usb_kill_urb(struct urb *urb)
等时发出的,unlink 通常是出现在卸载驱动前,回收之前通过usb_submit_urb
但还没有 complete 的urb,或者出现异常了,需要 kill 掉之前的 urb 等情况。