本文件概述了我们在开发Gabeldorsche(GD)蓝牙栈时的一些架构考虑。
线程模型
首先,GD协议栈并没有建立在线程的概念上。相反,它与处理程序一起工作。然而,由于GD最终运行在操作系统上,因此在实现处理程序抽象之前,它仍然需要与进程和线程进行交互。
进程
一般来说,在GD的运行环境中存在三种类型的进程。
应用程序进程:包括第三方应用程序、其他系统组件,如音频和电信服务,它们与蓝牙堆栈进程API交互,通过各种RPC/IPC方法定义,如Binder、Socket IPC、gRPC、DBUS.等等,使用AIDL或Protobuf等语言。对于安卓应用,尽管API是在AIDL中定义的,但一些锅炉板代码被包裹在Java库中,通过框架/基础/核心/java/android/蓝牙中的代码暴露出来,作为安卓SDK发布给开发者。
硬件抽象层(HAL)进程:来自供应商分区的一个或多个进程,因此是由硬件决定的。它们通过一组硬件抽象API与蓝牙堆栈进程进行交互,这些硬件抽象API通过RPC/IPC方法定义,如Binder、Socket IPC、DBUS等,使用HIDL等语言。在Android上,这将是实现HIDL APIs的HAL进程,如IBluetoothHci和IBluetoothAudioProvider。
蓝牙堆栈进程:通常是一个单一的进程,在主机控制器接口(HCI)和蓝牙SDK APIs下面实现各种蓝牙协议和配置文件。一方面,它为来自应用程序进程的请求提供服务;另一方面,它通过与HAL进程的交互转发这些请求。在Android上,这个进程通常在AID_BLUETOOTH(通常是1002)下运行,进程名称为 "com.android.bluetooth"。该进程在Java中启动,通过JNI加载本地库。其他不使用Java虚拟机的系统可能有一个纯本地进程。由于各种原因,这个进程中可能存在多个线程。GD栈完全在这个进程中运行。
蓝牙堆栈进程中的线程
目前,蓝牙堆栈中线程优化的目标是。
尽可能地减少线程的数量,以简化同步操作
在独立的线程中做阻塞的I/O操作
尝试将I/O操作转移到轮询模式,这样我们就可以在主线程上使用事件驱动的方法来与之互动
将报警和定时器机制转移到其调用线程中,以避免单独的报警线程
隔离各个组件,以便每个组件可以单独启动和停止,而不需要终止主线程
倾向于数据传递而不是线程间的数据共享,以减少锁定和竞赛条件
经过上述优化,我们在本地代码中留下了五个主要的线程类型。
主线程:是蓝牙堆栈中的主要工作主体。该线程的执行环境被进一步划分为驻扎在各个模块中的处理程序。如果运行平台上的性能受到限制,这个线程可以进一步划分为更小的线程。部署者只需要将处理程序绑定到不同的线程上,这不应该影响整体的操作。
JNI线程:在本地线程中,我们将Java层视为一个独立的应用程序,因为其线程模块完全不同。因此,我们在这两层之间放一个线程来缓冲任何阻塞操作。
HCI线程(或其他HW I/O线程):这个线程负责与硬件I/O进行死循环,可能会有潜在的阻塞性。因此,它有一个独立的线程,以避免阻塞主线程。
音频工作者线程:负责音频编码和解码操作,需要更精确的执行时间。这种工作者有其独立的线程,以避免受到主线程的影响。
Socket I/O线程:与使用BluetootSocket接口的各种应用进行通信。由于潜在的I/O延迟,它有一个独立的线程。
数据流图
不同组件之间的功能调用被抽象为通过队列传递的控制包(功能关闭)。组件之间的数据流是通过队列发送的数据包,使用Reactor信号。它们将合并到每个组件的输入队列中。我们定义了三种类型的队列。
非阻塞队列: 当用户试图在它是空的时候去排队,或者在它是满的时候去排队,它将立即返回。一个线程内的所有队列都必须是无阻塞的,因为否则会出现死锁。
堵塞的队列: 当用户试图在队列为空时取消排队,或在队列为满时进行排队时,它将被阻塞,直到其他线程使队列成为可写/可读。它可以作为一种流量控制机制,以避免来自用户线程的太多数据包。
Leaky queue : 和非阻塞队列一样,但是当队列满了,用户试图重新排队的时候,它就会冲掉。这对音频编码很有用。
线程模型
构建模块
模块
GD中的代码被打包成C++对象,称为模块。一个模块规范了GD代码的以下方面。
依赖关系。一个模块通过实现ListDependencies()提供自己对其他模块的依赖。
生命周期。一个模块必须实现Start()和Stop()生命周期方法。
线程模块。模块基类通过GetHandler()为代码执行环境提供一个处理程序。
度量。一个模块可以通过DumpState()为dumpsys转储其状态信息。
见其定义:https://android.googlesource.com/platform/system/bt/+/master/gd/module.h
处理程序
与android.os.Handler类似,bluetooth::os::Handler提供了一个连续的执行环境,同时将线程的概念隐藏在执行代码中。
通过将执行环境划分为更小的区域,Handler在以下方面有利于开发。
由于顺序执行上下文,对锁的需求较少
更小的上下文导致更容易管理代码流
与线程分离使系统部署者有更多的自由来调整底层线程分配。例如,对于没有完整线程实现的实时操作系统,可以使用处理程序来提供一个接近线程的执行环境。
当然,使用Handler也有弊端,开发者应该谨慎对待。
警告:尽管多个Handler可以绑定到同一个线程,但Handler并不能保证代码在不同的Handler之间顺序执行,即使它们在同一个线程上。
警告:在绑定到同一线程的处理程序之间进行锁定可能会导致死锁。
警告:数据必须在处理程序之间进行复制,以避免死锁和竞赛条件。
见其定义:https://android.googlesource.com/platform/system/bt/+/master/gd/os/handler.h
反应器
bluetooth::os:Reactor实现了Reactor设计模式,即通过同步事件解复用器将并发的事件解复用到通过Dispatcher注册的请求处理程序列表中。
在通用的Linux操作系统中,如Android,我们使用文件描述符来实现它,如处理程序的eventfd、报警器的timerfd和数据处理管道的socketfd。在文件描述符的背景下,事件被归纳为两种类型。
OnReadReady:意味着解复用器有一些事件给处理程序,处理程序可以从底层事件队列中读取至少一个事件。这通常与EPOLLIN、EPOLLHUP、EPOLLRDHUP和EPOLLERR有关。
OnWriteReady:意味着解复用器已经准备好从该处理程序中消费更多的事件,并且该处理程序可以将至少一个事件写入底层队列中。
这种模式自然地创造了从一个队列到另一个队列的背压,而没有任何额外的信号机制。当在像我们这样的网络栈中使用时,它简化了信令代码流。
见其定义:https://android.googlesource.com/platform/system/bt/+/master/gd/os/reactor.h

Reactor的一个纯数据用例是Reactive Queue,见其定义:https://android.googlesource.com/platform/system/bt/+/master/gd/os/queue.h
数据包定义语言(PDL
数据包解析和序列化一直是任何网络堆栈的一个重要部分。它通常是与远程设备接口的第一个代码片段。在过去,这是用STREAM_TO_UNIT8或UINT8_TO_STREAM等宏手动实现的。这种手工方法很繁琐,也很容易出错。为了解决这个问题,我们创建了一个数据包定义语言,将网络数据包结构定义为比特级。C++头文件和Python绑定将从其代码生成器中自动生成,对代码生成器的任何修正都将系统地适用于生成的所有数据包代码。








