14.7 Linux Bluetooth 子系统
上一节我们还在为网络延迟焦虑,讨论了怎么用 Busy Poll 这种「硬核」手段压榨系统的最后一点性能。这是一种通过放弃 CPU 休息时间来换取速度的极端策略。
现在,让我们把紧绷的神经稍微放松一点,把视线从千兆网线上移开,投向另一种完全不同的网络世界——无线个人区域网(PAN)。这里没有光纤直连的狂暴速度,也没有 HFT(高频交易)的微秒级焦虑,但这里有着无处不在的连接需求:你的无线鼠标、你的运动手环、你的无线耳机。
这一节,我们来聊聊 Bluetooth,以及 Linux 是如何通过 BlueZ 协议栈来支撑这个庞大而复杂的生态的。
背景与动机
如果你在 1994 年告诉工程师说「以后我们要把耳机的线给剪了」,他们可能会觉得你疯了。但在那一年,爱立信确实启动了一个名为「Bluetooth」的项目。起初,它的目标非常卑微:替代 RS-232 数据线。谁也没想到,这玩意儿后来演变成了连接几十亿设备的无线标准。
现在的 Bluetooth 已经不是简单的「无线串口」了。它工作在免费的 2.4GHz ISM 频段(和 Wi-Fi、微波炉挤在一起),由 Bluetooth SIG(Special Interest Group)维护。虽然我们常开玩笑说蓝牙是「断连之王」,但在近距离、低功耗的场景下,它依然是无可争议的王者。
Linux 内核对蓝牙的支持始于 2001 年的内核 2.4.6,那个项目叫 BlueZ。这也是目前几乎所有 Linux 发行版默认的蓝牙协议栈。理解 BlueZ 的架构,对于调试各种莫名其妙的连接问题、或者开发基于蓝牙的应用至关重要。
协议栈全景:分层与职责
蓝牙的协议栈长得有点像 TCP/IP,但它更复杂,因为它要处理音频流、设备发现、文件传输等各种完全不同的事情。我们可以把它粗略地映射到 OSI 模型上:
- 底层(Radio / Link Controller / LMP):这些通常是在硬件或固件里实现的。作为内核驱动开发者,我们通常只跟上面的一层打交道,不直接碰这里。
- HCI(Host Controller Interface):这是内核与硬件对话的桥梁。也是本章的重头戏。
- L2CAP(Logical Link Control and Adaptation Protocol):相当于传输层。它负责把数据打包、分段、重组,并提供多路复用。
- 应用层协议:建立在 L2CAP 之上,比如 RFCOMM(模拟串口)、SDP(服务发现)、BNEP(网络封装)等。
💡 类比时刻:三次回收
你可以把蓝牙协议栈想象成一栋办公大楼。
建立映射:
- 底层硬件是大楼的地基和管道(电、水、通风)。
- HCI是一楼的前台。所有的信件、指令都要通过前台转交给大楼内部,或者从内部发出去。前台不关心信件内容,只负责传输。
- L2CAP是每层楼外的收发室。它负责把大包裹拆成小包裹(分包),或者把小包裹拼成大包裹(重组),并根据门牌号(Channel ID)分发到不同的房间(Socket)。
- 上层协议(RFCOMM 等)是具体的房间。有的房间专门负责打电话(音频),有的负责发邮件(数据传输)。
揭示距离: 但这个类比有个地方不对:前台(HCI)通常不只一个。你的电脑可能内置了一个蓝牙适配器(USB),又插了一个 USB 蓝牙 dongle。内核里的 HCI 层得同时管理这两个「前台」,而且它们之间是完全独立的。此外,L2CAP 的「收发室」比现实中的更智能,它能协商包的大小(MTU),还能保证顺序——这在 UDP 邮寄系统里是不存在的。
回收验证: 当你后面看到
hci_dev结构体时,你会意识到那其实就是前台员工的档案;而l2cap_chan就是收发室的登记簿。如果前台罢工了(驱动加载失败),不管楼上房间多豪华,大楼都是瘫痪的。
Linux 内核中的 BlueZ 架构
BlueZ 不是一个单一的模块,它是一套组件。
核心代码位于 net/bluetooth/ 目录下。如果你不想把头发抓秃,最好先搞清楚这几个关键角色:
- HCI Core (
hci_core.c,mgmt.c):这是蓝牙控制器的大脑,负责管理设备、连接和调度。 - Socket 层 (
af_bluetooth.c):提供 BSD Socket API,让用户空间程序能像操作 TCP/IP 那样操作蓝牙。 - L2CAP (
l2cap*.c):处理逻辑链路控制,这是所有数据流的必经之路。 - 驱动层 (
drivers/bluetooth/):这是内核和硬件握手的地方。比如btusb.c是通用的 USB 蓝牙驱动,hci_uart.c是基于串口的驱动(很多嵌入式板子用这个)。
核心机制拆解:HCI 层
HCI 是 Host(主机)和 Controller(控制器)之间的接口。在 Linux 内核里,每一个蓝牙物理适配器都由一个 struct hci_dev 结构体表示。
这个结构体非常庞大(超过 100 个成员),它是内核蓝牙世界的「身份证」。我们挑几个最重要的字段来看:
struct hci_dev {
char name[8];
unsigned long flags;
__u8 bus;
bdaddr_t bdaddr;
__u8 dev_type;
struct work_struct rx_work;
struct work_struct cmd_work;
struct sk_buff_head rx_q;
struct sk_buff_head raw_q;
struct sk_buff_head cmd_q;
int (*open)(struct hci_dev *hdev);
int (*close)(struct hci_dev *hdev);
int (*flush)(struct hci_dev *hdev);
int (*send)(struct sk_buff *skb);
...
};
让我们来「人肉」解析一下这些字段:
bdaddr:这是蓝牙设备的 MAC 地址,48 位,全球唯一。你可以在/sys/class/bluetooth/hci0/address看到它。bus:这个控制器挂在哪里?是 USB (HCI_USB)?还是 PCIe (HCI_PCI)?或者是 UART (HCI_UART)?驱动程序在初始化时会填这个。flags:描述状态。比如HCI_UP表示设备已启动,HCI_INIT表示正在初始化。- 工作队列 (
rx_work,cmd_work):蓝牙处理大量异步事件。内核不能在硬件中断里干太多活,所以把收到的数据包(rx_q)和待发送的命令(cmd_q)放到队列里,交由work_struct慢慢处理。 - 回调函数 (
open,close,send):这是驱动开发者的舞台。当你写一个蓝牙驱动时,你的主要工作就是实现这些函数,告诉内核怎么把数据塞给你的硬件。
数据包的四种旅程
HCI 层定义了四种数据包类型,这四种包构成了内核与硬件之间的全部对话:
HCI_COMMAND_PKT:主机发给硬件的命令。比如「开始扫描附近的设备」或者「连接到 XX:XX:XX:XX:XX:XX」。HCI_ACLDATA_PKT:异步数据。这是传输文件的主体,对应 ACL 链路。HCI_SCODATA_PKT:同步数据。主要是音频(SCO 链路)。HCI_EVENT_PKT:硬件发给主机的事件。比如「扫描到了一个设备」或者「连接完成」。
交互流程是这样的:
当你运行 hcitool scan 时,内核会通过 hci_send_cmd() 发一个 HCI_COMMAND_PKT 给硬件。硬件不会马上回复,它会在扫描到设备后,通过中断把一个 HCI_EVENT_PKT 扔回来,这个包会被 hci_event_packet() 接收并处理。这里没有同步等待,全是异步回调。
向上对接:L2CAP 层
HCI 层把数据从硬件拿上来后,就扔给了 L2CAP 层。L2CAP 是蓝牙协议栈的中枢神经。
它的工作不仅仅是传输,还包括:
- 分段与重组(SAR):底层 HCI 只能传输有限大小的包,L2CAP 负责把你的 1MB 文件切成小块,发过去再拼起来。
- 多路复用:你在用蓝牙听歌(A2DP),同时鼠标在动(HID)。L2CAP 通过 Channel ID (CID) 把这两个数据流分开。
内核用 struct l2cap_chan 表示一个通道。
当 HCI 收到 ACL 数据后,会调用 hci_acldata_packet() -> l2cap_recv_acldata()。这个函数会检查数据包的头部:
struct l2cap_hdr {
__le16 len; // 长度
__le16 cid; // 通道 ID
} __attribute__ ((packed));
- 如果
cid == 0x0001,说明这是信令通道,也就是控制命令。比如另一个设备想连你,它发的连接请求(L2CAP_CONN_REQ)就会走这个通道。内核会调用l2cap_sig_channel()处理,创建一个新的l2cap_chan,状态设为BT_OPEN,然后开始配置。 - 如果
cid是别的值,那就直接把数据扔给对应的 Socket。
实战:BNEP 与蓝牙 IP
蓝牙不仅仅是为了传文件或音频,它还能跑 IP。这就是 BNEP (Bluetooth Network Encapsulation Protocol) 干的事。它把以太网帧塞进蓝牙 L2CAP 通道里,让你能通过蓝牙把两台电脑连成一个局域网。
这比用 PPP 拨号要高效得多。
回到那个「办公大楼」的类比: BNEP 就是在大楼之间架起了一条货运专线。不管你是发快递(IP 包)还是发信件,只要装进集装箱(以太网帧),扔给专线,就能运到另一栋大楼。另一栋大楼的收发室(L2CAP)收到后,拆箱,再分发给具体房间。
怎么玩转 BNEP?
你不需要写代码,Linux 有现成的工具 pand (BlueZ PAN daemon)。
服务端(充当路由器/热点):
pand --listen --role=NAP
# NAP = Network Access Point
客户端(连接热点):
pand --connect 00:1A:7D:DA:71:13
这一连上,你的机器里会多出一个虚拟网卡 bnep0。
这时候,你可以像配置普通网卡一样给它配 IP:
ifconfig bnep0 192.168.1.1 netmask 255.255.255.0 up
背后的机制
当 pand --connect 执行时,用户空间程序会创建一个 L2CAP Socket 并发起连接。服务端收到请求后,会向内核发送一个 BNEPCONNADD 的 IOCTL 命令。
内核里处理这个命令的是 bnep_add_connection() (net/bluetooth/bnep/core.c)。它干了这几件事:
- 创建会话:生成一个
bnep_session对象,记录连接状态。 - 注册网络设备:调用
register_netdev(),这就是为什么你会看到bnep0出现在ip link列表里。 - 启动内核线程:创建一个叫
kbnepd的守护线程。这个线程就是一个死循环,专门负责收包 (bnep_rx_frame) 和发包 (bnep_tx_frame)。- 收到包 -> 去掉 BNEP 头 -> 剩下的就是纯以太网帧 -> 扔给
netif_rx()进入 TCP/IP 栈。 - 要发包 -> 加上 BNEP 头 -> 扔给 L2CAP -> 扔给 HCI -> 发射。
- 收到包 -> 去掉 BNEP 头 -> 剩下的就是纯以太网帧 -> 扔给
数据流向全景图
让我们追踪一个 IP 包是如何通过蓝牙发出的:
- 应用层:
ping 192.168.1.2 - TCP/IP 栈:构造 ICMP 包,查找路由表,发现要从
bnep0出去。 - BNEP:
bnep_netdev_xmit()被调用。它把以太网帧封装成 BNEP 格式。 - L2CAP:BNEP 调用 L2CAP Socket 发送接口。L2CAP 加上自己的头(包含 CID),并可能切分片段。
- HCI:L2CAP 调用 HCI 的发送接口。HCI 把数据包挂在
cmd_q或直接通过hci_send_frame()发给硬件驱动。 - 硬件驱动(如
btusb):把 SKB 转换成 USB URB,飞向 USB 蓝牙 dongle。 - 空中接口:蓝牙芯片把数据变成 2.4GHz 的无线电波,发向另一个设备。
接收则是反过来:空中 -> 硬件 -> HCI (hci_rx_work) -> L2CAP (l2cap_recv_acldata) -> BNEP (bnep_rx_frame) -> TCP/IP 栈 -> 应用。
扩展特性与工具
随着标准演进,L2CAP 也不再是当年的那个「简单的 UDP」了。内核 2.6.36 引入了 Extended Features (eL2CAP):
- ERTM (Enhanced Retransmission Mode):带重传的可靠传输。这就类似 TCP 了,不仅仅是 UDP 那样「尽力而为」。
- Streaming Mode (SM):流模式,为了实时性牺牲可靠性。
- FCS (Frame Check Sequence):给每个包加校验和,防止数据传错。
这些特性对于某些对可靠性要求极高的医疗设备(HDP Profile)是必须的。
最后,怎么排查问题?别指望 dmesg 告诉你一切,你需要一套瑞士军刀:
hciconfig:查看和配置 HCI 设备。hciconfig hci0 up启动设备。hcitool:调试利器。hcitool scan扫描,hcitool lescan扫低功耗设备。hcidump/btmon:抓包神器。就像 Wireshark 一样,但它抓的是空中(或 HCI 层)的蓝牙原始数据。如果你想看为什么鼠标连不上,跑一下btmon,一目了然。
本章回响
我们完成了从底层硬件驱动到上层 IP 协议的完整穿越。
表面上看,这一节是在讲「蓝牙怎么连上网」,实际上我们在看一个完整的网络子系统是如何分层协作的。
- HCI 层屏蔽了硬件差异(USB, UART, SDIO)。
- L2CAP 层提供了多路复用和基础传输服务。
- BNEP 层把异构的蓝牙链路伪装成了标准的以太网设备,让现有的 TCP/IP 协议栈无需修改即可运行。
还记得开头那个「办公大楼」的类比吗?现在你应该明白,Linux 蓝牙子系统之所以复杂,是因为它不仅要管理大楼的前台(HCI),还要管理收发室(L2CAP),甚至还要在大楼之间架设货运专线(BNEP)。每一层都有自己独立的状态机和协议,但又紧密咬合。
下一节,我们将从个人区域网(PAN)跳到更广阔、但也更底层的无线领域——IEEE 802.15.4 和 6LoWPAN。那是物联网的世界,那里的协议更精简,对资源更抠门,但设计哲学与蓝牙有着异曲同工之妙。