Linux USB 探测→枚举→RNDIS 驱动匹配 全流程笔记
📄 Linux USB 探测→枚举→RNDIS 驱动匹配 全流程笔记
一句话总结:本文以 RNDIS 设备为线索,从 USB 物理层的 D+/D- 上拉电阻检测开始,逐层追踪到 Linux 内核的 hub 枚举、驱动匹配、net_device 注册、carrier 状态管理,完整呈现了"插上手机到网口能通信"的全链路原理与代码调用路径。
流程图
内容梳理
一、物理层: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 内核的处理链路:
- HCD 中断服务(如
xhci_irq())→ 识别端口事件 → 调用usb_hcd_poll_rh_status()或通过 event ring 触发 hub 处理 - hub_irq() 解析 Hub 中断端点返回的状态位图 → 调度
hub_event到 kworker 线程 - hub_port_connect_change() → 获取端口状态、清除 CSC 位、执行端口复位(
hub_port_reset(),驱动 D+/D- 到 SE0 至少 50ms)、完成高速 Chirp 协商 - usb_alloc_dev() 分配
struct usb_device,读取设备描述符前 8 字节获取 EP0 最大包长,分配唯一 USB 地址,再读取完整描述符 - usb_new_device() →
device_add()将 USB 设备注册到 Linux 设备模型,此时会触发总线 match,遍历已注册的 usb_driver
关键代码在 drivers/usb/core/hub.c:5183(hub_port_connect)和 drivers/usb/core/hub.c:2513(usb_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 机制自动完成:
- 内核启动时
netdev_kobject_init()调用class_register(&net_class),其中net_class.name = "net",创建了/sys/class/net/目录 register_netdevice()中netdev_register_kobject()设置dev->class = &net_class,然后调用device_add()device_add()中的device_add_class_symlinks()自动在/sys/class/net/下创建指向设备实际路径的符号链接- 同时
net_class_groups提供的属性组自动创建了address、carrier、statistics/等 sysfs 文件
五、carrier 状态管理
这是我学习过程中最绕的部分,需要区分三个概念:
| 概念 | 含义 | 设置方式 |
|---|---|---|
| IFF_UP(管理状态) | 管理员是否启用了接口 | ifconfig up → dev_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 年就没修这个 bug:rndis_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 进入内核后分两条路径:
- SIOCSIFADDR:
sock_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 后再执行后续逻辑:
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 协议交换流程
- 主机发送
REMOTE_NDIS_INITIALIZE_MSG→ 设备返回INITIALIZE_CMPLT(协商版本、MTU) - 主机发送
REMOTE_NDIS_QUERY_MSG (OID_802_3_PERMANENT_ADDRESS)→ 获取 MAC - 主机发送
REMOTE_NDIS_SET_MSG (OID_GEN_CURRENT_PACKET_FILTER)→ 启用数据通路 - 设备主动发送
REMOTE_NDIS_INDICATE_STATUS_MSG (RNDIS_STATUS_MEDIA_CONNECT)→ 主机 carrier on - 数据平面:
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.c、bus.c、dd.c,理解bus_type/device/device_driver三件套 - 对比其他 USB 网卡驱动:
cdc_ether.c的usbnet_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三窗口同时监控,插拔手机观察时序
AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。
更多推荐



所有评论(0)