安卓蓝牙开发流程(Android 11中的Gabeldorsche架构 蓝牙引擎)

本文件概述了我们在开发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绑定将从其代码生成器中自动生成,对代码生成器的任何修正都将系统地适用于生成的所有数据包代码。


您可以还会对下面的文章感兴趣

使用微信扫描二维码后

点击右上角发送给好友