USBIP解读及源码分析
+ -

USBIP整体架构

2021-05-06 2572 40

在普通的电脑上,想使用USB设备,必须将插入到主机。USBIP却可以通过网络,让主机访问其他主机上的外部设备,而用户程序完全感知不到区别。

usbip的文章在这里:https://pdfs.semanticscholar.org/c7c4/cb054d75810fdb0a2affa11c288b7687267f.pdf

USBIP整体架构

从体系机构的角度上来说,USB的设备和总线都是通过Host分出,host叫做主机控制器,一个主机控制器会有一个root hub,然后root hub再通过接口分出多个hub,最后,一个hub的一个端口都可以用来连接一个外部设备或是一个hub,形成一个以host为跟的树状结构,最终形成菊花链的接结构。而Host最终是作为一个PCI的设备,连接在PCI总线上。

USB从根HUB到最底端设备,最多不能超过7层。
20210505213001506501

在linux内核中,USB驱动的架构可以分成3层。

  • 最靠近用户层的是USB设备驱动,这是每个USB设备有独有的,在它之上可以直接与VFS交互,比如键盘和鼠标这些设备,USB设备驱动就足以处理外部设备的交互任务,因为他们发送过来的就只有是一些很简单的数据,但是如果是一个插在USB上的网卡等设备,则需要在USB设备驱动之上再覆盖一层网卡等驱动,用来解析USB总线传输过来的数据。它与下层的USB Core通过urb交换数据。
  • USB Core则是承上启下的一层,USB设备驱动通过urb的抽次昂概念来和底层交互,USB Core就用来解析urb,同时,还会包含Hub驱动等等一些USB驱动框架的其他主要部分。
  • 最下层的是主机控制器驱动,前面看到USB外部设备最终通过Host和总线、CPU交互,也就是说最底层还需要有一个主机控制器的驱动,它主要负责Host的相关工作,将USB设备的信息通过主机控制器向上传递软件层。这部分会有一个PCI驱动,然后控制Host硬件

20210505213015867079

理解了USB驱动的框架,USBIP的架构就比价容易了,主要部分也是两个,读取设备的主机端,设置一个虚拟的主机控制器接口VHCI,它不操纵底层的主机控制器,而是将上层的消息通过网络转发到真实连接设备的一端。而在真实端,通过一个驱动程序 stub driver,虚拟设备的操作转换成对真实USB设备的操作,实现移花接木的效果。

20210505213040623329

在Linux内核中,USBIP的源码写在drivers/usb/usbip目录下,在这些文件中,以stub开头的都是server端的代码,vhci开头的是client端的代码,其余是公共部分的代码。

下面从Stub和VHCI的角度来分析。

Stub端

主要结构体
usbip_common中有一个usbip_device结构,这是stub和vhci两边设备从后向出来的公共部分。这里有3个内核线程,一个socket,以及和eh相关的等待队列和操作集合。usbip_common中其他几个结构就不再叙述,比较简单

struct usbip_device {
    enum usbip_side side;
    enum usbip_device_status status;

    /* lock for status */
    spinlock_t lock;

    struct socket *tcp_socket;//用来通信的socket

    struct task_struct *tcp_rx;//收消息的线程
    struct task_struct *tcp_tx;//发送消息的线程

    unsigned long event;//记录事件
    struct task_struct *eh;//内核线程
    wait_queue_head_t eh_waitq;//eh等待队列

    struct eh_ops {
        void (*shutdown)(struct usbip_device *);
        void (*reset)(struct usbip_device *);
        void (*unusable)(struct usbip_device *);
    } eh_ops;
};

stub.h中定义了关键的结构体,他们的内容和含义如下:
stub_device是stub端的设备抽象,代表以一个外部设备

struct stub_device {
    struct usb_interface *interface;//接口描述符指针
    struct usb_device *udev;
    struct usbip_device ud;//对于stub和vhci两端的设备都做了抽象,用来表示两端交互中都需要的内容
    __u32 devid;
    spinlock_t priv_lock;
    struct list_head priv_init;//urb初始队列
    struct list_head priv_tx;//urb被提交之后
    struct list_head priv_free;//urb的内容被发送给了chci
    struct list_head unlink_tx;//unlink请求队列
    struct list_head unlink_free;//unlink请求被处理
    wait_queue_head_t tx_waitq;//等待队列
};

stub_priv被赋值给urb->priv字段,主要是给stub端来管理urb结构体

struct stub_priv {
    unsigned long seqnum;//给每个urb都有一个序列号
    struct list_head list;
    struct stub_device *sdev;
    struct urb *urb;
    int unlinking;//是否被unlink
};

stub_unlink表示一个urb的unlink请求

struct stub_unlink {
    unsigned long seqnum;//和stub_priv对于的序列号
    struct list_head list;
    __u32 status;//unlink是否成功
};

stub_main中有一个全局变量的数组,用来管理所有的设备

static struct bus_id_priv busid_table[MAX_BUSID];

其中一个设备用bus_id_priv来表示

struct bus_id_priv {
    char name[BUSID_SIZE];
    char status;
    int interf_count;
    struct stub_device *sdev;
    struct usb_device *udev;
    char shutdown_busid;
};

模块的初始化函数

先看stub_main中最后几行,有模块的的初始和卸载函数

module_init(usbip_host_init);
module_exit(usbip_host_exit);

usbip_host_init中主要函数有4个,做的工作如下

1、初始化全局变量busid_table
2、分配一个slab用来做stub_priv的分配工作
3、注册一个usb驱动,这里是stub的初始化,这里注册的驱动也是位于设备端,USB核心上层的USB驱动
4、创建两个sysfs文件,这两个文件的操作就在上面,rebind_store和store_match_busid、show_match_busid。

下面的退出函数usbip_host_exit同理,只是做了这几个函数的清理工作

先看看它创建的两个sysfs文件。这是给用户态的接口。一个是match_busid

static DRIVER_ATTR(match_busid, S_IRUSR | S_IWUSR, show_match_busid,store_match_busid);
涉及的函数是show_match_busid和store_match_busid,show_match_busid用来输出名字,store_match_busid则用来增加或是删除busid_table中的条目

另一个文件则是用来设置设备绑定的驱动。

这几个文件的操作都是围绕busid_table和它的几个操作函数开展开,也比较简单。整个文件的内容也分析完了。下面就还有一个关键的usb驱动,也就是Stub驱动,很显然,主要的工作都是通过这个驱动来开展的。

Stub驱动

stub_dev文件最底部有这个驱动

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,
};

从probe函数开始,这里只说一些和Stub驱动自身逻辑相关的重要部分,proe函数通过stub_device_alloc分配了一个stub_device函数。而这个stub_device_alloc函数中还有一个usbip_start_eh函数。用来创建了一个内核线程eh。

int usbip_start_eh(struct usbip_device *ud)
{
    init_waitqueue_head(&ud->eh_waitq);
    ud->event = 0;

    ud->eh = kthread_run(event_handler_loop, ud, "usbip_eh");
    if (IS_ERR(ud->eh)) {
        pr_warn("Unable to start control thread\n");
        return PTR_ERR(ud->eh);
    }

    return 0;
}
EXPORT_SYMBOL_GPL(usbip_start_eh);

前面看到usbip_device结构中有3个内核线程,这是其中一个,二且eh_waitq就是用来给eh睡眠的,出发事件写在usbip_event_happened函数中,只是检查usbip_device上是否有事件产生,event字段会否为0。

好,接着回到probe函数,里面有一个stub_add_files函数。可以知道这个函数同样创建了几个sysfs文件,他们为dev_attr_usbip_status、dev_attr_usbip_sockfd、dev_attr_usbip_debug。

其中比较关键的是dev_attr_usbip_sockfd,当写入数据为-1时执行关闭操作,另一条分支则会通过写入数据打开socket,赋值给tcp_socket字段,接着创建了两个内核线程。

看看这个stub_dev文件,剩下的代码都是处理断开和释放的函数了,这里就不分析,剩下的两个文件stub_rx和stub_tx可以知道这是和内核线程相关的了。所以说剩下的主要内容就是这两个内核线程。

rx和tx两个内核线程和stub_device中的5个链表

先看rx,主函数如下,沿着这个逻辑往下看

int stub_rx_loop(void *data)
{
    struct usbip_device *ud = data;

    while (!kthread_should_stop()) {
        if (usbip_event_happened(ud))
            break;

        stub_rx_pdu(ud);
    }

    return 0;
}

stub_rx_pdu函数中,调用usbip_recv收一个消息,usbip_header_correct_endian转换成小端,然后根据收到的消息来做不同的处理,如果是unlink消息则调用stub_recv_cmd_unlink,如果是urb的提交消息则调用stub_recv_cmd_submit。

说一下priv_init、priv_tx、priv_free这3个队列。stub中的urb通过stub_priv来进行管理。通过stub_priv,给每个urb分配了序列号,同时,通过stub_priv中的list字段,将urb连接到上述3个队列中,当urb被提交给USB Core,直到完成前,放在priv_init,提交完成后,还需要通过网络发送给vhci,发送之前放在priv_tx,发送之后放在priv_free。

所以说,stub_recv_cmd_unlink的行为就是遍历priv_init,找到那些还没有完成的urb,调用usb_unlink_urb让USB Core去取消他们。

stub_recv_cmd_submit函数,调用了stub_priv_alloc,这个函数的末尾将urb加入到了priv_init中。最后调用usb_submit_urb提交urb,注意这里的回调函数,是stub_complete。这个函数在urb执行完后执行下面的语句,将urb移入priv_tx

list_move_tail(&priv->list, &sdev->priv_tx);
然后,调用了wake_up,唤醒tx内核线程

wake_up(&sdev->tx_waitq);

接着看rx线程,主要是两个函数stub_send_ret_submit和stub_send_ret_unlink,因为这是一个发送消息的线程,所以发送的消息有两种,一个是USB设备的交互内容,一种是vhci发送的unlink消息的回复。

stub_send_ret_submit处理的就是USB设备的返回内容,dequeue_from_priv_tx将urb从priv_tx取下,放入priv_free。stub_send_ret_submit最后再将内容封装成usbip协议规定的样子,通过网络发送出去。

下面再介绍剩下的两个队列,unlink_tx和unlink_free,这是用来处理unlink的两个队列,一个stub_unlink结构表示一个unlink请求,unlink_tx存放还没有被回复的unlink请求,而unlink_free则是存放已经回复了的。

所以stub_send_ret_unlink中的dequeue_from_unlink_tx用来完成unlink的队列转换,剩下的代码就会通过网络发送回复消息

这样,stub这边的几个函数就都已经完了

VHCI端

这边的代码和上面已经非常相似了,抓住3个点

1、模块的初始化时注册的驱动程序,在usb_add_hcd中会调用reset和start函数,接着抓住urb的几个操作函数就可以了

static struct hc_driver vhci_hc_driver = {
    .description    = driver_name,
    .product_desc    = driver_desc,
    .hcd_priv_size    = sizeof(struct vhci_hcd),

    .flags        = HCD_USB2,

    .start        = vhci_start,
    .stop        = vhci_stop,

    .urb_enqueue    = vhci_urb_enqueue,
    .urb_dequeue    = vhci_urb_dequeue,

    .get_frame_number = vhci_get_frame_number,

    .hub_status_data = vhci_hub_status,
    .hub_control    = vhci_hub_control,
    .bus_suspend    = vhci_bus_suspend,
    .bus_resume    = vhci_bus_resume,
};

2、创建的几个sysfs文件。
3、3个内核线程的工作

HID人机交互QQ群:564808376    UAC音频QQ群:218581009    UVC相机QQ群:331552032    BOT&UASP大容量存储QQ群:258159197    STC-USB单片机QQ群:315457461    USB技术交流QQ群2:580684376    USB技术交流QQ群:952873936   

0 篇笔记 写笔记

USBIP整体架构
在普通的电脑上,想使用USB设备,必须将插入到主机。USBIP却可以通过网络,让主机访问其他主机上的外部设备,而用户程序完全感知不到区别。usbip的文章在这里:https://pdfs.semanticscholar.org/c7c4/cb054d75810fdb0a2affa11c288b76......
USBIP虚拟控制器和根集线器HUB的设备信息分析
USBIP是一套可以实现USB远程设备的本机映射,这样就像在本地操作目标USB设备一样。其原理是在本地端通过安装一个虚拟的USB根控制器,根集线器,同时虚拟出4个USB端口,当远程USB设备连接到远程机器时,可以把远程设备的USB信息通过网络传输到本地,在本地的虚拟USB端口虚拟一个相同的设备,这样......
USBIP虚拟控制器安装命令过程分析
USBIP虚拟控制器其自带的安装命令为:usbip.exe install -w可以看到,这是一个明显的自定义命令。这里一个比较有意思的是对命令行进行解析,使用的函数是getopt_long。在介绍这个命令之前,我们先介绍几个命令行参数的函数。命令行的长短项之分命令行分为长项和短项,我们一般使......
USBIP 设备树层次分析
通过前面知道,usbip_vhci工程实现的是pnp管理器虚拟出来的PDO的FDO,但同样也是这个FDO创建的PDO虚拟根USB控制器和其子设备HUB的驱动程序。USBIP设备硬件ID名称sys文件inf文件硬件IDusbip-win VHIC Rootusbip_vhic......
USBIP 驱动入口DriverEntry和vhci_driverUnload函数
无论该驱动被安装几次,DriverEntry和卸载函数只执行一次。DriverEntryWindows驱动和应用层一样,也有个入口函数,只是这个名字叫做DriverEntry。在DriverEntry中应实现的是该驱动各个IRP类类型的回调函数。PAGEABLE NTSTATUSDriverE......
USBIP 创建设备AddDevice
USBIP的AdDevice函数为vhci_add_device。DriverEntry(__in PDRIVER_OBJECT drvobj, __in PUNICODE_STRING RegistryPath){... drvobj->DriverExtension->......
USBIP 创建FDO设备和子设备PDO
设备创建由add_vdev函数实现,具体过程为:使用vdev_create创建FDO设备建立自己的设备链表将创建的FDO和PDO使用IoAttachDeviceToDeviceStack函数关联最后根据设备类型进行初始化设备层级及设备成员指针链表如下:static PAGEABLE NTST......
USBIP FDO和PDO设备类型及结构体大小
设备类型typedef enum { VDEV_ROOT,//虚拟根设备FDO VDEV_CPDO,//虚拟USB控制器PDO VDEV_VHCI,//USB控制器FDO VDEV_HPDO,//USB根HUB PDO VDEV_VHUB, //USB根HUB......
USBIP设备启动IRP_MN_START_DEVICE
在主功能号为IRP_MJ_PNP的子功能号里,使用IRP_MN_START_DEVICE执行设备的启动。DriverEntry函数中为:drvobj->MajorFunction[IRP_MJ_PNP] = vhci_pnp;vhci_pnp(__in PDEVICE_OBJECT de......
USBIP 虚拟根设备(VDEV_ROOT)FDO的初始化过程
AddDeviceUSBIP使用devcon安装根设备驱动后,会创建其对应的PDO,这时系统会加载我们的驱动调用AddDevice函数创建PDO,进入进行堆栈。devcon.exe install vaudio.inf "USBIPWIN oot"我们在之前的创建设备Add......
USBIP 虚拟控制器设备(VDEV_CPDO)PDO的初始化过程
IRP_MN_QUERY_ID/BusQueryDeviceIDPAGEABLE NTSTATUSpnp_query_id(pvdev_t vdev, PIRP irp, PIO_STACK_LOCATION irpstack){ NTSTATUS status = STATUS_......
USBIP 虚拟控制器设备(VDEV_VHCI)FDO的初始化过程
虚拟ROOT总线FDO创建了虚拟USB控制器PDO之后,系统通过各种IRP_MJ_PNP收集完物理设备的信息之后,开始根据其硬件ID进行设备驱动批配,批配成功后,装载驱动并调用其AddDevice之后,开始FDO的创建过程。通过前面的可知,USBIP实现的根驱动,USB控制器、HUB和设备PDO的S......
USBIP 集线器PDO(VDEV_HPDO)的初始化过程
USB集线器其实也是USB设备的一类,其设备分类分类为0x09。和USBIP虚拟的控制器(VDEV_CPDO)类似,USB控制器创建了其PDO之后,会通过IRP_MN_QUERY_DEVICE_RELATIONS返回USB集线器HUB的PDO设备列表。PNP管理器收到有新的设备之后,会开始收集这个P......
USBIP 虚拟集线器FDO子设备的管理
IRP_MN_QUERY_DEVICE_RELATIONS这得从IRP_MN_QUERY_DEVICE_RELATIONS来谈起,好像有点看的不是很明白。先把上一节的代码复制过来,看一下:static PAGEABLE NTSTATUSget_bus_relations_vhub(pvhub_d......
USBIP 虚拟集线器FDO(VDEV_VHUB )的初始化
AddDevice执行vhci_add_device,返回的设备类型为VDEV_VHUB,集线器HUB的FDO类型。然后初始化HUB的FDO,使用init_dev_vhub(vdev);函数实现static PAGEABLE voidinit_dev_vhub(pvdev_t vdev){ ......
关注公众号
取消
感谢您的支持,我会继续努力的!
扫码支持
扫码打赏,你说多少就多少

打开支付宝扫一扫,即可进行扫码打赏哦

您的支持,是我们前进的动力!