跳到主要内容

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 模型上:

  1. 底层(Radio / Link Controller / LMP):这些通常是在硬件固件里实现的。作为内核驱动开发者,我们通常只跟上面的一层打交道,不直接碰这里。
  2. HCI(Host Controller Interface):这是内核与硬件对话的桥梁。也是本章的重头戏。
  3. L2CAP(Logical Link Control and Adaptation Protocol):相当于传输层。它负责把数据打包、分段、重组,并提供多路复用。
  4. 应用层协议:建立在 L2CAP 之上,比如 RFCOMM(模拟串口)、SDP(服务发现)、BNEP(网络封装)等。

💡 类比时刻:三次回收

你可以把蓝牙协议栈想象成一栋办公大楼

  1. 建立映射

    • 底层硬件是大楼的地基和管道(电、水、通风)。
    • HCI一楼的前台。所有的信件、指令都要通过前台转交给大楼内部,或者从内部发出去。前台不关心信件内容,只负责传输。
    • L2CAP每层楼外的收发室。它负责把大包裹拆成小包裹(分包),或者把小包裹拼成大包裹(重组),并根据门牌号(Channel ID)分发到不同的房间(Socket)。
    • 上层协议(RFCOMM 等)具体的房间。有的房间专门负责打电话(音频),有的负责发邮件(数据传输)。
  2. 揭示距离: 但这个类比有个地方不对:前台(HCI)通常不只一个。你的电脑可能内置了一个蓝牙适配器(USB),又插了一个 USB 蓝牙 dongle。内核里的 HCI 层得同时管理这两个「前台」,而且它们之间是完全独立的。此外,L2CAP 的「收发室」比现实中的更智能,它能协商包的大小(MTU),还能保证顺序——这在 UDP 邮寄系统里是不存在的。

  3. 回收验证: 当你后面看到 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 层定义了四种数据包类型,这四种包构成了内核与硬件之间的全部对话:

  1. HCI_COMMAND_PKT:主机发给硬件的命令。比如「开始扫描附近的设备」或者「连接到 XX:XX:XX:XX:XX:XX」。
  2. HCI_ACLDATA_PKT异步数据。这是传输文件的主体,对应 ACL 链路。
  3. HCI_SCODATA_PKT同步数据。主要是音频(SCO 链路)。
  4. 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)。它干了这几件事:

  1. 创建会话:生成一个 bnep_session 对象,记录连接状态。
  2. 注册网络设备:调用 register_netdev(),这就是为什么你会看到 bnep0 出现在 ip link 列表里。
  3. 启动内核线程:创建一个叫 kbnepd 的守护线程。这个线程就是一个死循环,专门负责收包 (bnep_rx_frame) 和发包 (bnep_tx_frame)。
    • 收到包 -> 去掉 BNEP 头 -> 剩下的就是纯以太网帧 -> 扔给 netif_rx() 进入 TCP/IP 栈。
    • 要发包 -> 加上 BNEP 头 -> 扔给 L2CAP -> 扔给 HCI -> 发射。

数据流向全景图

让我们追踪一个 IP 包是如何通过蓝牙发出的:

  1. 应用层ping 192.168.1.2
  2. TCP/IP 栈:构造 ICMP 包,查找路由表,发现要从 bnep0 出去。
  3. BNEPbnep_netdev_xmit() 被调用。它把以太网帧封装成 BNEP 格式。
  4. L2CAP:BNEP 调用 L2CAP Socket 发送接口。L2CAP 加上自己的头(包含 CID),并可能切分片段。
  5. HCI:L2CAP 调用 HCI 的发送接口。HCI 把数据包挂在 cmd_q 或直接通过 hci_send_frame() 发给硬件驱动。
  6. 硬件驱动(如 btusb):把 SKB 转换成 USB URB,飞向 USB 蓝牙 dongle。
  7. 空中接口:蓝牙芯片把数据变成 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.46LoWPAN。那是物联网的世界,那里的协议更精简,对资源更抠门,但设计哲学与蓝牙有着异曲同工之妙。