Linux USB 探测→枚举→RNDIS 驱动匹配 全流程笔记

📄 Linux USB 探测→枚举→RNDIS 驱动匹配 全流程笔记

一句话总结:本文以 RNDIS 设备为线索,从 USB 物理层的 D+/D- 上拉电阻检测开始,逐层追踪到 Linux 内核的 hub 枚举、驱动匹配、net_device 注册、carrier 状态管理,完整呈现了"插上手机到网口能通信"的全链路原理与代码调用路径。

流程图

#mermaid-svg-y8ZUW0PfKTgZhIyr{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#ccc;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-y8ZUW0PfKTgZhIyr .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-y8ZUW0PfKTgZhIyr .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-y8ZUW0PfKTgZhIyr .error-icon{fill:#a44141;}#mermaid-svg-y8ZUW0PfKTgZhIyr .error-text{fill:#ddd;stroke:#ddd;}#mermaid-svg-y8ZUW0PfKTgZhIyr .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-y8ZUW0PfKTgZhIyr .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-y8ZUW0PfKTgZhIyr .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-y8ZUW0PfKTgZhIyr .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-y8ZUW0PfKTgZhIyr .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-y8ZUW0PfKTgZhIyr .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-y8ZUW0PfKTgZhIyr .marker{fill:lightgrey;stroke:lightgrey;}#mermaid-svg-y8ZUW0PfKTgZhIyr .marker.cross{stroke:lightgrey;}#mermaid-svg-y8ZUW0PfKTgZhIyr svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-y8ZUW0PfKTgZhIyr p{margin:0;}#mermaid-svg-y8ZUW0PfKTgZhIyr .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#ccc;}#mermaid-svg-y8ZUW0PfKTgZhIyr .cluster-label text{fill:#F9FFFE;}#mermaid-svg-y8ZUW0PfKTgZhIyr .cluster-label span{color:#F9FFFE;}#mermaid-svg-y8ZUW0PfKTgZhIyr .cluster-label span p{background-color:transparent;}#mermaid-svg-y8ZUW0PfKTgZhIyr .label text,#mermaid-svg-y8ZUW0PfKTgZhIyr span{fill:#ccc;color:#ccc;}#mermaid-svg-y8ZUW0PfKTgZhIyr .node rect,#mermaid-svg-y8ZUW0PfKTgZhIyr .node circle,#mermaid-svg-y8ZUW0PfKTgZhIyr .node ellipse,#mermaid-svg-y8ZUW0PfKTgZhIyr .node polygon,#mermaid-svg-y8ZUW0PfKTgZhIyr .node path{fill:#1f2020;stroke:#ccc;stroke-width:1px;}#mermaid-svg-y8ZUW0PfKTgZhIyr .rough-node .label text,#mermaid-svg-y8ZUW0PfKTgZhIyr .node .label text,#mermaid-svg-y8ZUW0PfKTgZhIyr .image-shape .label,#mermaid-svg-y8ZUW0PfKTgZhIyr .icon-shape .label{text-anchor:middle;}#mermaid-svg-y8ZUW0PfKTgZhIyr .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-y8ZUW0PfKTgZhIyr .rough-node .label,#mermaid-svg-y8ZUW0PfKTgZhIyr .node .label,#mermaid-svg-y8ZUW0PfKTgZhIyr .image-shape .label,#mermaid-svg-y8ZUW0PfKTgZhIyr .icon-shape .label{text-align:center;}#mermaid-svg-y8ZUW0PfKTgZhIyr .node.clickable{cursor:pointer;}#mermaid-svg-y8ZUW0PfKTgZhIyr .root .anchor path{fill:lightgrey!important;stroke-width:0;stroke:lightgrey;}#mermaid-svg-y8ZUW0PfKTgZhIyr .arrowheadPath{fill:lightgrey;}#mermaid-svg-y8ZUW0PfKTgZhIyr .edgePath .path{stroke:lightgrey;stroke-width:2.0px;}#mermaid-svg-y8ZUW0PfKTgZhIyr .flowchart-link{stroke:lightgrey;fill:none;}#mermaid-svg-y8ZUW0PfKTgZhIyr .edgeLabel{background-color:hsl(0, 0%, 34.4117647059%);text-align:center;}#mermaid-svg-y8ZUW0PfKTgZhIyr .edgeLabel p{background-color:hsl(0, 0%, 34.4117647059%);}#mermaid-svg-y8ZUW0PfKTgZhIyr .edgeLabel rect{opacity:0.5;background-color:hsl(0, 0%, 34.4117647059%);fill:hsl(0, 0%, 34.4117647059%);}#mermaid-svg-y8ZUW0PfKTgZhIyr .labelBkg{background-color:rgba(87.75, 87.75, 87.75, 0.5);}#mermaid-svg-y8ZUW0PfKTgZhIyr .cluster rect{fill:hsl(180, 1.5873015873%, 28.3529411765%);stroke:rgba(255, 255, 255, 0.25);stroke-width:1px;}#mermaid-svg-y8ZUW0PfKTgZhIyr .cluster text{fill:#F9FFFE;}#mermaid-svg-y8ZUW0PfKTgZhIyr .cluster span{color:#F9FFFE;}#mermaid-svg-y8ZUW0PfKTgZhIyr div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(20, 1.5873015873%, 12.3529411765%);border:1px solid rgba(255, 255, 255, 0.25);border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-y8ZUW0PfKTgZhIyr .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#ccc;}#mermaid-svg-y8ZUW0PfKTgZhIyr rect.text{fill:none;stroke-width:0;}#mermaid-svg-y8ZUW0PfKTgZhIyr .icon-shape,#mermaid-svg-y8ZUW0PfKTgZhIyr .image-shape{background-color:hsl(0, 0%, 34.4117647059%);text-align:center;}#mermaid-svg-y8ZUW0PfKTgZhIyr .icon-shape p,#mermaid-svg-y8ZUW0PfKTgZhIyr .image-shape p{background-color:hsl(0, 0%, 34.4117647059%);padding:2px;}#mermaid-svg-y8ZUW0PfKTgZhIyr .icon-shape .label rect,#mermaid-svg-y8ZUW0PfKTgZhIyr .image-shape .label rect{opacity:0.5;background-color:hsl(0, 0%, 34.4117647059%);fill:hsl(0, 0%, 34.4117647059%);}#mermaid-svg-y8ZUW0PfKTgZhIyr .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-y8ZUW0PfKTgZhIyr .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-y8ZUW0PfKTgZhIyr :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;}#mermaid-svg-y8ZUW0PfKTgZhIyr .physical>*{fill:#1a5276!important;color:#fff!important;stroke:#2980b9!important;}#mermaid-svg-y8ZUW0PfKTgZhIyr .physical span{fill:#1a5276!important;color:#fff!important;stroke:#2980b9!important;}#mermaid-svg-y8ZUW0PfKTgZhIyr .physical tspan{fill:#fff!important;}#mermaid-svg-y8ZUW0PfKTgZhIyr .kernel>*{fill:#4a235a!important;color:#fff!important;stroke:#8e44ad!important;}#mermaid-svg-y8ZUW0PfKTgZhIyr .kernel span{fill:#4a235a!important;color:#fff!important;stroke:#8e44ad!important;}#mermaid-svg-y8ZUW0PfKTgZhIyr .kernel tspan{fill:#fff!important;}#mermaid-svg-y8ZUW0PfKTgZhIyr .driver>*{fill:#7b241c!important;color:#fff!important;stroke:#c0392b!important;}#mermaid-svg-y8ZUW0PfKTgZhIyr .driver span{fill:#7b241c!important;color:#fff!important;stroke:#c0392b!important;}#mermaid-svg-y8ZUW0PfKTgZhIyr .driver tspan{fill:#fff!important;}#mermaid-svg-y8ZUW0PfKTgZhIyr .result>*{fill:#145a32!important;color:#fff!important;stroke:#27ae60!important;}#mermaid-svg-y8ZUW0PfKTgZhIyr .result span{fill:#145a32!important;color:#fff!important;stroke:#27ae60!important;}#mermaid-svg-y8ZUW0PfKTgZhIyr .result tspan{fill:#fff!important;}#mermaid-svg-y8ZUW0PfKTgZhIyr .problem>*{fill:#7d6608!important;color:#fff!important;stroke:#f39c12!important;}#mermaid-svg-y8ZUW0PfKTgZhIyr .problem span{fill:#7d6608!important;color:#fff!important;stroke:#f39c12!important;}#mermaid-svg-y8ZUW0PfKTgZhIyr .problem tspan{fill:#fff!important;} 1.USB物理插入

D+上拉→电平变化
2.xHCI/EHCI控制器

PORTSC寄存器CSC置位
3.HCD中断→hub_irq

调度hub_event
4.hub_port_connect

端口复位+高速握手
5.usb_new_device

读描述符+分配地址
6.device_add

USB接口注册→总线match
7.rndis_driver匹配

调用usbnet_probe
8.rndis_bind

INIT→Query MAC→SetFilter
9.register_netdev

创建/sys/class/net/usb0
10.等待INDICATE_STATUS

媒体连接通知
11.netif_carrier_on

carrier=1 → LOWER_UP
12.ifconfig up成功

state UP 可通信

内容梳理

一、物理层:USB 插入的电信号检测

我是带着"内核怎么知道我插了 USB"这个问题出发的。USB 2.0 的检测机制非常简洁:

  • 主机端:D+ 和 D- 各接 15kΩ 下拉到地,平时两条线都是低电平(SE0 状态)
  • 设备端:全速/高速设备在 D+ 上拉 1.5kΩ 到 3.3V,低速设备在 D- 上拉
  • 插入瞬间:对应数据线被拉高,主机 PHY 的比较器检测到电平变化,端口状态寄存器(PORTSC)的 Connect Status Change(CSC)位被置 1
  • 如果是高速设备,后续还有 Chirp K/J 握手协商至 480Mbps,但这对驱动透明

二、内核中断与 Hub 枚举

物理信号触发硬件中断后,Linux 内核的处理链路:

  1. HCD 中断服务 (如 xhci_irq())→ 识别端口事件 → 调用 usb_hcd_poll_rh_status() 或通过 event ring 触发 hub 处理
  2. hub_irq() 解析 Hub 中断端点返回的状态位图 → 调度 hub_event 到 kworker 线程
  3. hub_port_connect_change() → 获取端口状态、清除 CSC 位、执行端口复位(hub_port_reset(),驱动 D+/D- 到 SE0 至少 50ms)、完成高速 Chirp 协商
  4. usb_alloc_dev() 分配 struct usb_device,读取设备描述符前 8 字节获取 EP0 最大包长,分配唯一 USB 地址,再读取完整描述符
  5. usb_new_device()device_add() 将 USB 设备注册到 Linux 设备模型,此时会触发总线 match,遍历已注册的 usb_driver

关键代码在 drivers/usb/core/hub.c:5183hub_port_connect)和 drivers/usb/core/hub.c:2513usb_new_device)。

三、RNDIS 驱动匹配与网络设备注册

rndis_host 驱动复用了 usbnet 框架的 usbnet_probe()。调用链:

复制代码
usbnet_probe()                         // usbnet.ko --- 通用框架
  ├─ alloc_etherdev()                  // 分配 net_device
  ├─ strcpy(net->name, "usb%d")       // 命名模板
  ├─ info->bind() = rndis_bind()      // rndis_host.ko --- RNDIS 协议
  │    ├─ usbnet_generic_cdc_bind()    // cdc_ether.ko --- CDC 公共层
  │    ├─ rndis_command(INIT)          // 发送 REMOTE_NDIS_INITIALIZE_MSG
  │    ├─ rndis_query(MAC)             // 获取 MAC 地址
  │    └─ rndis_command(SET FILTER)    // 启用数据包过滤
  └─ register_netdev(net)              // → 创建 /sys/class/net/usb0

三个模块的依赖关系:rndis_host.ko → cdc_ether.ko → usbnet.ko。Kconfig 中 RNDIS_HOST 必须 select CDCETHER,因为 generic_rndis_bind() 内部调用了 cdc_ether.c 导出的 usbnet_generic_cdc_bind()

四、/sys/class/net/usb0 的创建原理

/sys/class/net/usb0 并非驱动手动创建,而是内核设备模型的 class 机制自动完成:

  1. 内核启动时 netdev_kobject_init() 调用 class_register(&net_class),其中 net_class.name = "net",创建了 /sys/class/net/ 目录
  2. register_netdevice()netdev_register_kobject() 设置 dev->class = &net_class,然后调用 device_add()
  3. device_add() 中的 device_add_class_symlinks() 自动在 /sys/class/net/ 下创建指向设备实际路径的符号链接
  4. 同时 net_class_groups 提供的属性组自动创建了 addresscarrierstatistics/ 等 sysfs 文件

五、carrier 状态管理

这是我学习过程中最绕的部分,需要区分三个概念:

概念 含义 设置方式
IFF_UP(管理状态) 管理员是否启用了接口 ifconfig updev_open()ndo_open()
carrier(链路状态) 物理/协议层是否连通 驱动调用 netif_carrier_on/off()
operstate(操作状态) 接口实际可用性 内核根据 IFF_UP + carrier 自动计算

关键顿悟ifconfig up 成功 ≠ 接口可用。如果 carrier=0,即使 IFF_UP 已置位,operstate 也是 DOWN,ip link 显示 NO-CARRIER

对于 RNDIS 设备,carrier 从 0 变 1 的唯一方式 是:设备发送 REMOTE_NDIS_INDICATE_STATUS_MSG(携带 RNDIS_STATUS_MEDIA_CONNECT),主机驱动收到后调用 netif_carrier_on()

但 Linux 的 rndis_host.c 从 2005 年就没修这个 bugrndis_msg_indicate() 收到 RNDIS_STATUS_MEDIA_CONNECT 后只打 dev_info("rndis media connect")不会调用 netif_carrier_on()rndis_status() 也只打 debug 日志,源码里留着 // FIXME do like cdc_status() 的注释。所以 carrier 永远是 0。

六、ifconfig up 的内核调用路径

用户态 ifconfig usb0 192.1.2.3 up 进入内核后分两条路径:

  • SIOCSIFADDRsock_ioctl() → inet_ioctl() → devinet_ioctl() → inet_set_ifa()(纯 IP 层,不涉及驱动)
  • SIOCSIFFLAGS (IFF_UP)sock_ioctl() → dev_ioctl() → dev_change_flags() → __dev_change_flags() → dev_open() → ndo_open() = usbnet_open()(提交 RX URB、启动 tasklet、开启发送队列)

七、USB 设备拓扑命名

内核日志中的 1-1:1.0 格式为 <总线>-<端口路径>:<配置>.<接口>

  • 1-1:USB 总线 1 的端口 1
  • :1.0:配置 1、接口 0

在 sysfs 中体现为 /sys/bus/usb/devices/1-1:1.0/

八、时序问题与经验教训

系统启动脚本在 /sys/class/net/usb0 出现后立即 ifconfig up,但此时手机可能还未发送 RNDIS_STATUS_MEDIA_CONNECT,导致 carrier=0。随后 carrier 自动变 1(如果驱动正确实现了的话),接口早已自动 UP。手动再次 ifconfig up 只是碰巧看到了已经就绪的状态,并非手动命令的功劳。

正确的自动化脚本应等待 carrier=1 后再执行后续逻辑:

bash 复制代码
while [ ! -d /sys/class/net/usb0 ]; do sleep 0.1; done
for i in $(seq 1 100); do
    if [ "$(cat /sys/class/net/usb0/carrier 2>/dev/null)" = "1" ]; then
        break
    fi
    sleep 0.1
done
ifconfig usb0 1.2.3.4 up

九、RNDIS 协议交换流程

  1. 主机发送 REMOTE_NDIS_INITIALIZE_MSG → 设备返回 INITIALIZE_CMPLT(协商版本、MTU)
  2. 主机发送 REMOTE_NDIS_QUERY_MSG (OID_802_3_PERMANENT_ADDRESS) → 获取 MAC
  3. 主机发送 REMOTE_NDIS_SET_MSG (OID_GEN_CURRENT_PACKET_FILTER) → 启用数据通路
  4. 设备主动发送 REMOTE_NDIS_INDICATE_STATUS_MSG (RNDIS_STATUS_MEDIA_CONNECT) → 主机 carrier on
  5. 数据平面:RNDIS_PACKET_MSG 封装以太网帧在 Bulk 端点上传输(36 字节帧头 + 以太网帧)

总结与展望

总结

  • 物理到软件全链路:D+/D- 上拉电阻 → PORTSC 寄存器 → HCD 中断 → hub_event → 枚举 → 驱动匹配 → net_device 注册,每层都有清晰的源码对应
  • carrier ≠ UP:管理状态(IFF_UP)和链路状态(carrier)是独立的两个维度,operstate 由二者共同决定
  • rndis_host.c 有已知缺陷rndis_msg_indicate() 未调用 netif_carrier_on()rndis_status() 留了 2005 年的 FIXME,导致 RNDIS 设备 carrier 永远为 0
  • 设备模型自动化/sys/class/net/usb0 由 class 机制自动创建,驱动只负责填 net_device 并调 register_netdev()
  • 时序是排查问题的关键/sys/class/net/usb0 存在 ≠ carrier=1,必须等待 INDICATE 消息到达

展望

  • 深入理解 Linux 设备模型 :建议阅读 drivers/base/core.cbus.cdd.c,理解 bus_type/device/device_driver 三件套
  • 对比其他 USB 网卡驱动cdc_ether.cusbnet_cdc_status() 正确实现了 carrier 更新,可以对比 rndis_host.c 看差异
  • 推荐阅读源码优先级usbnet.c(框架)→ cdc_ether.c(CDC 公共层)→ rndis_host.c(RNDIS 协议)→ hub.c(枚举)
  • 推荐书籍:《Linux Device Drivers》第 3 版第 13 章(USB 驱动)、《USB Complete》第 4 版(USB 协议硬件视角)
  • 推荐实验 :用 dmesg -w + ip monitor link + watch -n 0.5 cat /sys/class/net/usb0/carrier 三窗口同时监控,插拔手机观察时序
相关推荐
长葡萄的叶子1 小时前
Transformer:让机器读懂上下文的艺术
笔记·transformer
相醉为友1 小时前
Trae IDE WSL2/SSH 环境网络故障排查笔记
ide·笔记·ssh
程序猿编码2 小时前
子域猎手:一款高性能DNS枚举工具的设计与实现
linux·c++·python·c·dns
Full Stack Developme2 小时前
Linux cd /abc 与 cd /abc/ 区别
linux·运维·服务器
想吃火锅10052 小时前
【leetcode】20.有效的括号js
linux·javascript·leetcode
问心无愧05132 小时前
ctfshow web入门114
android·前端·笔记
十月的皮皮2 小时前
C语言学习笔记20260614-数组奇偶数调整3种方法
c语言·笔记·学习
buhuizhiyuci2 小时前
【Linux篇】数字世界程序运行寻找地址的指南针——环境变量的详解
linux·运维·服务器
Shadow(⊙o⊙)2 小时前
信号1.0,信号概念、signal()处理、前后台进程、闹钟设置、初识信号三张表。
linux·运维·服务器·开发语言·c++