我们在使用蓝牙的过程中, 当上层 应用 断开所有的 profile 后, 协议栈就会帮我们下发 disconnect 命令。本节就让笨叔, 带大家一起梳理这块内容,具体在协议栈如何处理的。
梳理开始前, 先思考一下。 我们为什么要梳理这块内容。 这个不应该是一个常规操作吗? 还需要梳理吗?
- 非也非也,请听我娓娓道来。
1. 为什么要梳理清楚 L2CAP 连接(ACL链路)超时处理逻辑?
L2CAP 的连接超时处理,不仅仅是个"定时器逻辑",而是:
整个蓝牙协议栈在资源管理、连接控制、安全性保障、以及系统功耗控制中的"中枢神经系统"。
下面我来从5个方面解释清楚它在协议栈中为何这么重要。
1. 蓝牙连接资源是"有限"的,需要靠 L2CAP 控制其释放
在蓝牙中,每条 ACL 链路占用资源包括:
- 控制器侧:连接上下文、信道信息、加密状态、时钟同步...
- Host 侧(协议栈):LCB、CCB、定时器、缓存队列、控制块结构...
这些资源非常有限,尤其在嵌入式设备中:
L2CAP 层必须主动感知"连接是否还在使用",从而合理断开未使用连接释放资源。
梳理超时处理逻辑,有助于理解:
- 哪些行为会被认定为"空闲"?
- 哪些信道不会触发断开?
- 是整个 ACL 链路断开,还是仅关闭某个信道?
2. 蓝牙协议栈的功耗控制依赖 L2CAP 的空闲感知机制
在 BLE 中,系统"保持连接"的代价比 Wi-Fi 小,但也不是零:
- 广播间隔、连接间隔、握手仍然需要耗电
- Android 中很多 BLE 外设(手环、门锁)其实只在需要时才通信
所以:
L2CAP 的空闲超时判断直接决定是否断开连接,从而达到省电目的。
比如:
l2cu_no_dynamic_ccbs()
就是发现没有动态信道后,考虑是否断开;- 配合
L2CA_SetIdleTimeoutByBdAddr()
动态调整断开策略。
3. ACL连接与应用层"业务活跃状态"并不同步,L2CAP需要"感知"空闲
有些情况如下:
- GATT连接已经建立,但用户没有发任何请求(无动态信道);
- 连接还在,但只是维持状态同步或偶尔广播;
这时,只有 L2CAP 层能根据是否有活跃的 CCB/FIXED_CHNL来判断是否真的需要继续保持连接。
如果不理会,会出现"连接假死"、"连接不释放"、"电池耗光"等问题。
4. 理解 L2CAP 的超时机制有助于掌握 Bluetooth 的连接生命周期设计思想
从 Host 层角度,整个l2cap连接生命周期基本是:
创建
→配置
→打开
→使用中
→空闲超时
→关闭
其中,"空闲超时"是"断开"阶段的主要触发点。这个阶段必须明确:
- 谁负责判断空闲?
- 是断动态信道?固定信道?整个连接?
- 配对中是否允许断开?
- 如何设置 timeout(API)?
因此:
理解超时机制,就是理解连接如何"自然死亡"的机制。
而不是"上层手动断开"的极端处理方式。
5. 安全和用户体验方面的逻辑也依赖这个判断链
- 如果在 Pairing 过程中就误判"超时"断开,可能导致设备连不上;
- 如果不设置合理 timeout,连接会"挂死",影响用户体验;
- 某些隐私或认证敏感设备(比如车钥匙、门锁),不能允许连接长时间闲置。
6. 总结:理解超时机制的价值
方面 | 原因说明 |
---|---|
资源管理 | 限定的连接资源,需及时释放未使用连接 |
功耗控制 | 超时断开连接节省功耗,是低功耗蓝牙核心设计理念 |
状态判定 | L2CAP 能准确判定"是否真正空闲",不依赖应用主动断开 |
生命周期理解 | L2CAP 是连接生命周期的"断开阶段"的判断核心 |
安全&体验 | 错误的超时设置会导致连接异常,影响配对、通信、稳定性 |
2. 调用路径
在实际协议栈中, 大部分会通过 如下 几个分支来触发 调用 检查是否要 启动 acl 链路断开策略。
shell
1. L2CA_SetIdleTimeoutByBdAddr -> l2cu_no_dynamic_ccbs
2. L2CA_SendFixedChnlData -> l2cu_no_dynamic_ccbs
3. L2CA_SetLeGattTimeout -> l2cu_no_dynamic_ccbs
4. l2cu_release_ccb -> l2cu_no_dynamic_ccbs
我们分别对 这四个函数展开讲解一下。
1. L2CA_SetIdleTimeoutByBdAddr
c
/*******************************************************************************
*
* Function L2CA_SetIdleTimeoutByBdAddr
*
* Description Higher layers call this function to set the idle timeout for
* a connection. The "idle timeout" is the amount of time that
* a connection can remain up with no L2CAP channels on it.
* A timeout of zero means that the connection will be torn
* down immediately when the last channel is removed.
* A timeout of 0xFFFF means no timeout. Values are in seconds.
* A bd_addr is the remote BD address. If bd_addr =
* RawAddress::kAny, then the idle timeouts for all active
* l2cap links will be changed.
*
* Returns true if command succeeded, false if failed
*
* NOTE This timeout applies to all logical channels active on the
* ACL link.
******************************************************************************/
/*
参数说明:
bd_addr:目标设备的蓝牙地址。如果是 RawAddress::kAny,表示设置所有当前连接的设备。
timeout:空闲超时的秒数,单位是秒:
0:一旦无通道立即断开。
0xFFFF:永不超时。
transport:传输类型(BR/EDR 还是 LE)。
*/
bool L2CA_SetIdleTimeoutByBdAddr(const RawAddress& bd_addr, uint16_t timeout,
tBT_TRANSPORT transport) {
if (bluetooth::shim::is_gd_l2cap_enabled()) {
// 如果启用了新的 Gabeldorsche L2CAP(简称 GD L2CAP),就走 shim 层封装后的新实现,不使用 legacy L2CAP。
return bluetooth::shim::L2CA_SetIdleTimeoutByBdAddr(bd_addr, timeout,
transport);
}
tL2C_LCB* p_lcb; // 定义一个指向 LCB(Link Control Block)的指针。每个连接的对端 BD_ADDR 都会有一个 LCB 实例管理状态。
if (RawAddress::kAny != bd_addr) { // 如果是设置 单个设备地址的超时(而不是全部)
p_lcb = l2cu_find_lcb_by_bd_addr(bd_addr, transport); // 查找该设备的 LCB 实例,如果找不到说明未连接或无效
if ((p_lcb) && (p_lcb->in_use) && (p_lcb->link_state == LST_CONNECTED)) { // LCB 找到了;该 LCB 处于活跃状态; 并且已经处于 连接状态
p_lcb->idle_timeout = timeout; // 设置该 LCB 的超时值(单位秒,会在后面转为 ms)
// 若当前该链路上没有动态信道(即 GATT、AVCTP 等都已释放)
// 立即触发检查:l2cu_no_dynamic_ccbs() 函数将分析是否要定时断开连接,内部会根据 fixed channel 状态判断 idle timeout
if (!p_lcb->ccb_queue.p_first_ccb) l2cu_no_dynamic_ccbs(p_lcb);
} else
return false; // 否则设置失败,返回 false
} else { // 进入此分支说明调用者希望 批量设置所有活动 L2CAP 链路的 idle timeout
int xx;
tL2C_LCB* p_lcb = &l2cb.lcb_pool[0]; // 遍历所有 l2cb.lcb_pool 中的 LCB 实例。l2cb.lcb_pool 是 LCB 的对象池数组
for (xx = 0; xx < MAX_L2CAP_LINKS; xx++, p_lcb++) {
// 对数组中所有 LCB 实例做处理
if ((p_lcb->in_use) && (p_lcb->link_state == LST_CONNECTED)) {
// 设置该连接的空闲超时时间。
p_lcb->idle_timeout = timeout;
// 同样,如果这个连接上已经没有任何动态信道,立即触发 idle 超时检查流程
if (!p_lcb->ccb_queue.p_first_ccb) l2cu_no_dynamic_ccbs(p_lcb);
}// 跳过未使用或未连接状态的 LCB
}
}
return true;
}
项目 | 内容 |
---|---|
函数目的 | 设置某连接的空闲超时时间,用于控制 ACL 链路是否在无信道使用时断开 |
控制对象 | 所有 L2CAP 信道(动态 + 固定)都未使用后,才进入超时计时逻辑 |
触发函数 | 如果立即没有 CCB,会调用 l2cu_no_dynamic_ccbs() 进一步决定是否启动断开定时器 |
关键用途 | - 精细控制连接生命周期- 动态管理资源释放时机- 实现智能断开策略- 节省功耗,提升系统效率 |
功能
该函数允许上层(如 GATT、AVCTP 等)设置某个 ACL 链路(由 bd_addr 指定)的"空闲超时时间"。
如果该链路上的所有 L2CAP 通道(固定 + 动态)都关闭了(空闲),并且没有禁止 idle timeout,
就会在 timeout 秒后自动断开该 ACL 链路(通过 l2cu_no_dynamic_ccbs 实现)。
简单说:
设置某个远端设备(由 Bluetooth 地址指定)的 ACL 连接的空闲超时时间。
如果一段时间内没有数据流动,根据这个超时设定,可以断开连接以节省资源(特别是 BLE 连接)。
典型调用时机
-
GATT(Generic Attribute Profile)建立连接后设置超时。比如连接上 BLE 设备后,应用想指定多长时间无业务通信就自动断开。
-
系统为了节能,在不同连接策略下动态调整超时时间。
典型调用位置(AOSP例子)
-
当 GATT Client 成功连接到 GATT Server 后 ,例如
gatt_cl.cc
里连接成功后就会设置:
L2CA_SetIdleTimeoutByBdAddr(bda, GATT_LINK_IDLE_TIMEOUT, BT_TRANSPORT_LE);
-
当 Pairing(配对)完成后修改超时设置。
真实场景举例
- 手机 App 连接智能手表完成数据同步后,可以调用此接口设置,比如 30 秒空闲无操作就断开 BLE,避免蓝牙资源浪费。
2. L2CA_SendFixedChnlData
-
作用 :往指定 固定信道 (fixed channel)发送数据(
p_buf
)到指定远端设备地址(rem_bda
)。 -
返回值:
-
L2CAP_DW_SUCCESS
:发送成功。 -
L2CAP_DW_FAILED
:发送失败。
-
c
// system/stack/l2cap/l2c_api.cc
/*******************************************************************************
*
* Function L2CA_SendFixedChnlData
*
* Description Write data on a fixed channel.
*
* Parameters: Fixed CID
* BD Address of remote
* Pointer to buffer of type BT_HDR
*
* Return value L2CAP_DW_SUCCESS, if data accepted
* L2CAP_DW_FAILED, if error
*
******************************************************************************/
uint16_t L2CA_SendFixedChnlData(uint16_t fixed_cid, const RawAddress& rem_bda,
BT_HDR* p_buf) {
if (bluetooth::shim::is_gd_l2cap_enabled()) { // 兼容新版 GD-L2CAP(Google的L2CAP重构版)
return bluetooth::shim::L2CA_SendFixedChnlData(fixed_cid, rem_bda, p_buf);
}
tL2C_LCB* p_lcb; // 声明链路控制块指针 p_lcb
tBT_TRANSPORT transport = BT_TRANSPORT_BR_EDR; // 默认传输类型 transport 设置为 BR/EDR(传统蓝牙)
// 如果 Fixed Channel ID 是 LE专用通道(ATT, SMP等),则改成 LE(低功耗蓝牙)传输
if (fixed_cid >= L2CAP_ATT_CID && fixed_cid <= L2CAP_SMP_CID)
transport = BT_TRANSPORT_LE; // ATT/SMP属于BLE专用固定通道
// 检查 fixed_cid 是否有效:
if ((fixed_cid < L2CAP_FIRST_FIXED_CHNL) ||
(fixed_cid > L2CAP_LAST_FIXED_CHNL) ||
(l2cb.fixed_reg[fixed_cid - L2CAP_FIRST_FIXED_CHNL].pL2CA_FixedData_Cb ==
NULL)) {
// 如果无效或没注册对应通道的处理回调
LOG_WARN("No service registered or invalid CID: 0x%04x", fixed_cid);
osi_free(p_buf); // 释放 p_buf(因为上层已经托管了这个内存,避免内存泄漏)
return (L2CAP_DW_FAILED); // 返回失败
}
if (!BTM_IsDeviceUp()) { // 检查蓝牙控制器是否就绪(比如初始化是否完成)。 如果没准备好:
LOG_WARN("Controller is not ready CID: 0x%04x", fixed_cid);
osi_free(p_buf);
return (L2CAP_DW_FAILED); // 同样,打印、释放、返回失败
}
// 查找远端设备对应的 Link Control Block(LCB),即当前ACL连接
p_lcb = l2cu_find_lcb_by_bd_addr(rem_bda, transport);
if (p_lcb == NULL || p_lcb->link_state == LST_DISCONNECTING) {
// 如果找不到 link,或者 link 正在断开(DISCONNECTING 状态)
/* if link is disconnecting, also report data sending failure */
LOG_WARN("Link is disconnecting or does not exist CID: 0x%04x", fixed_cid);
osi_free(p_buf); // 打印、释放、返回失败。
return (L2CAP_DW_FAILED); // 特别注意:这里即使 link 还存在,但一旦在断开中,禁止发送数据,防止 race condition
}
// 定义远端设备的固定通道支持掩码(bit mask)
// 这是为了确认对端是否支持要发送的 fixed_cid
tL2C_BLE_FIXED_CHNLS_MASK peer_channel_mask;
// 根据不同传输类型(BR/EDR vs LE),选择本地/对端的通道掩码
// Select peer channels mask to use depending on transport
if (transport == BT_TRANSPORT_LE)
peer_channel_mask = l2cb.l2c_ble_fixed_chnls_mask;
else
peer_channel_mask = p_lcb->peer_chnl_mask[0];
// 检查对方是否支持这个 fixed_cid
if ((peer_channel_mask & (1 << fixed_cid)) == 0) { // 如果不支持
LOG_WARN("Peer does not support fixed channel CID: 0x%04x", fixed_cid);
osi_free(p_buf);
return (L2CAP_DW_FAILED);
}
// 准备数据包
p_buf->event = 0; // 一般L2CAP层内部标志,无需事件触发
p_buf->layer_specific = L2CAP_FLUSHABLE_CH_BASED; // 表示可基于通道丢弃(flushable),优化性能
if (!p_lcb->p_fixed_ccbs[fixed_cid - L2CAP_FIRST_FIXED_CHNL]) {
// 如果这条 fixed_cid 通道对应的 Channel Control Block(CCB)还没初始化
// 尝试初始化一个固定通道 CCB
if (!l2cu_initialize_fixed_ccb(p_lcb, fixed_cid)) { // 如果初始化失败
LOG_WARN("No channel control block found for CID: 0x%4x", fixed_cid);
osi_free(p_buf);
return (L2CAP_DW_FAILED);
}
}
if (p_lcb->p_fixed_ccbs[fixed_cid - L2CAP_FIRST_FIXED_CHNL]->cong_sent) { // 检查当前通道是否拥塞(congested)
// 打印详细的拥塞状态(队列长度、配额大小),释放缓冲区并返回失败
LOG_WARN(
"Unable to send data due to congestion CID: 0x%04x xmit_hold_q.count: "
"%zu buff_quota: %u",
fixed_cid,
fixed_queue_length(
p_lcb->p_fixed_ccbs[fixed_cid - L2CAP_FIRST_FIXED_CHNL]
->xmit_hold_q),
p_lcb->p_fixed_ccbs[fixed_cid - L2CAP_FIRST_FIXED_CHNL]->buff_quota);
osi_free(p_buf);
return (L2CAP_DW_FAILED);
}
// 正常情况下,将数据包入队列
LOG_INFO("Enqueued data for CID: 0x%04x len:%hu", fixed_cid, p_buf->len);
l2c_enqueue_peer_data(p_lcb->p_fixed_ccbs[fixed_cid - L2CAP_FIRST_FIXED_CHNL],
p_buf); // 把包放到发送队列中
l2c_link_check_send_pkts(p_lcb, 0, NULL); // 检查链路是否可以发送数据,如果可以就触发发送
// If there is no dynamic CCB on the link, restart the idle timer each time
// something is sent
if (p_lcb->in_use && p_lcb->link_state == LST_CONNECTED &&
!p_lcb->ccb_queue.p_first_ccb) {
// 如果没有动态通道(动态CCB)存在,即只剩固定信道,那么每次发完数据需要重启Idle Timer, 防止 link 因"闲置"超时被系统断开
l2cu_no_dynamic_ccbs(p_lcb);
}
if (p_lcb->p_fixed_ccbs[fixed_cid - L2CAP_FIRST_FIXED_CHNL]->cong_sent) {
LOG_INFO("Link congested for CID: 0x%04x", fixed_cid);
// 如果发送完毕后,检测到发送队列已满(出现新拥塞),返回 拥塞状态
return (L2CAP_DW_CONGESTED);
}
return (L2CAP_DW_SUCCESS); // 一切顺利,最终返回成功
}
阶段 | 动作 |
---|---|
参数合法性检查 | fixed_cid,蓝牙模块状态,链路存在性检查 |
peer支持性检查 | 检查peer的fixed_cid支持情况 |
资源初始化 | 没有CCB则动态初始化 |
拥塞状态判断 | 发送前检查是否congestion |
发送动作 | 将数据包入队列,尝试发送 |
链路状态管理 | 没有动态CCB时重新管理Idle Timer |
发送后检测 | 发送后是否导致新的拥塞 |
返回值 | 根据情况返回 Success、Failed 或 Congested |
功能
通过指定的 Fixed Channel ID(CID)发送数据。
Fixed Channel 是 L2CAP 预定义的一些不需要动态分配的频道,比如:
-
0x0004
:ATT(GATT用) -
0x0001
:SMP(Security Manager Protocol,配对) -
0x003F
:LE Credit-Based Flow Control
典型调用时机
-
GATT 或 SMP 协议需要发送数据时。
-
应用层模块通过固定频道直接发包,比如发送 GATT 请求或应答。
典型调用位置(AOSP例子)
-
发送 GATT 请求 ,如
gatt_cl.cc
/gatt_sr.cc
中,发 GATT write 或 read 命令时,实际底层就是调用:L2CA_SendFixedChnlData(L2CAP_ATT_CID, bda, p_buf);
-
发送 SMP pairing request/response(在配对阶段,SMP 通过 L2CAP Fixed Channel 发送数据)。
真实场景举例
- 手机发起 GATT Read Request 查询蓝牙灯泡状态时,GATT模块组好一个ATT包,底层就是通过
L2CA_SendFixedChnlData
把数据发出去的。
这个函数, 我们真正的应该关注是在, 向固定通道发完数据后, 如果没有动态通道(动态CCB)存在,即只剩固定信道,那么每次发完数据需要重启Idle Timer, 防止 link 因"闲置"超时被系统断开。
3. L2CA_SetLeGattTimeout
c
system/stack/l2cap/l2c_api.cc
/*******************************************************************************
*
* Function L2CA_SetLeGattTimeout
*
* Description Higher layers call this function to set the idle timeout for
* a fixed channel. The "idle timeout" is the amount of time
* that a connection can remain up with no L2CAP channels on
* it. A timeout of zero means that the connection will be torn
* down immediately when the last channel is removed.
* A timeout of 0xFFFF means no timeout. Values are in seconds.
* A bd_addr is the remote BD address.
*
* Returns true if command succeeded, false if failed
功能:设置一个固定通道(这里主要是 GATT ATT 通道)的空闲超时时间。
空闲超时定义:如果一条连接上没有任何 L2CAP 通道,且达到超时时间,则断开连接。
特殊值:
0:立即断开。
0xFFFF:永不超时。
单位:秒。
返回值:是否设置成功。
*
******************************************************************************/
bool L2CA_SetLeGattTimeout(const RawAddress& rem_bda, uint16_t idle_tout) {
if (bluetooth::shim::is_gd_l2cap_enabled()) {
return bluetooth::shim::L2CA_SetLeGattTimeout(rem_bda, idle_tout);
}
constexpr uint16_t kAttCid = 4; // 定义常量,ATT协议固定占用 L2CAP 的 CID 0x0004, 后面直接用 kAttCid 来查找。
/* Is a fixed channel connected to the remote BDA ?*/
// 尝试通过 远端地址(rem_bda) 和 LE链路类型 找到对应的 LCB(Link Control Block)
// p_lcb 就是那条到远端设备的物理连接的信息结构体
tL2C_LCB* p_lcb = l2cu_find_lcb_by_bd_addr(rem_bda, BT_TRANSPORT_LE);
if (((p_lcb) == NULL) ||
(!p_lcb->p_fixed_ccbs[kAttCid - L2CAP_FIRST_FIXED_CHNL])) { // 检查连接是否有效: 如果没找到 p_lcb,说明没有这条连接; 或者,即便有连接,但本连接没有 ATT 固定通道控制块(fixed channel control block)
LOG(WARNING) << __func__ << " BDA: " << rem_bda
<< StringPrintf(" CID: 0x%04x not connected", kAttCid); // 如果上述检查失败,输出警告日志
return (false); // 然后直接 返回 设置失败
}
// 找到对应的 fixed_ccb(固定通道控制块),直接把它的 fixed_chnl_idle_tout 字段赋值为用户传入的 idle_tout,这样就把空闲超时时间设置上去了
p_lcb->p_fixed_ccbs[kAttCid - L2CAP_FIRST_FIXED_CHNL]->fixed_chnl_idle_tout =
idle_tout;
if (p_lcb->in_use && p_lcb->link_state == LST_CONNECTED &&
!p_lcb->ccb_queue.p_first_ccb) { // 即没有临时开的 L2CAP 动态通道,只剩下固定通道了
/* If there are no dynamic CCBs, (re)start the idle timer in case we changed
* it */
l2cu_no_dynamic_ccbs(p_lcb); // 通知 L2CAP 层重新计算和启动空闲定时器(Idle Timer),确保新的超时设置能立即生效
}
return true;
}
步骤 | 说明 |
---|---|
1 | 如果启用了 GD L2CAP,走 shim 接口。 |
2 | 查找对应的连接控制块(LCB)。 |
3 | 如果找不到连接或没有固定通道,返回失败。 |
4 | 设置 ATT 通道的空闲超时时间。 |
5 | 如果连接只剩下固定通道,重启 Idle Timer。 |
6 | 返回成功。 |
功能
设置针对 LE (Low Energy) 链路上 GATT 信道的专属超时时间。
这个超时专门用于控制GATT层面连接的生命周期,比如如果设备长时间无 GATT 活动,可以更快主动断开。
典型调用时机
-
蓝牙连接建立后,根据系统策略调整GATT信道专属超时。
-
高级应用要求不同设备设定不同的GATT超时时。
典型调用位置(AOSP例子)
-
比如:BLE连接 建立后,仅用于GATT服务交互(如读写特征值),没有别的动态通道。
-
上层可以通过这个接口设置「连接多久没用就自动断开」,节省功耗 ,避免链路空占。
-
在
gatt_main.cc
等文件中,GATT初始化连接后,会根据配置调用:
L2CA_SetLeGattTimeout(bda, timeout_in_seconds);
-
有些连接参数更新完成后(比如 PHY更新,MTU更新后),也可能根据新策略重新设置。
真实场景举例
- 连接智能手环后,如果只是后台保持状态同步,系统可以降低超时阈值,比如设置 GATT 空闲超时为 60秒,这样在用户长期不用时及时释放资源。
4. l2cu_release_ccb
- 释放一个动态信道的 CCB(Channel Control Block),是 L2CAP 信道生命周期中"清理收尾"的关键步骤。
c
// system/stack/l2cap/l2c_utils.cc
void l2cu_release_ccb(tL2C_CCB* p_ccb) {
tL2C_LCB* p_lcb = p_ccb->p_lcb;// 取出该信道所属的物理连接控制块(LCB, Link Control Block),一个 LCB 代表一个远端设备连接
tL2C_RCB* p_rcb = p_ccb->p_rcb; // 获取注册控制块(RCB, Registration Control Block),用于标识该信道对应的上层 PSM 注册信息
L2CAP_TRACE_DEBUG("l2cu_release_ccb: cid 0x%04x in_use: %u",
p_ccb->local_cid, p_ccb->in_use);
/* If already released, could be race condition */
if (!p_ccb->in_use) return; // 如果这个 CCB 已经被释放了,就直接退出(避免重复释放)
if (p_rcb && (p_rcb->psm != p_rcb->real_psm)) {
// 如果这是个临时映射的 PSM(例如 LE SMP 使用 0x0001 作为伪 PSM),则清除对应安全策略绑定;通常用于临时逻辑通道,如 LE 安全管理(SMP)通道
BTM_SecClrServiceByPsm(p_rcb->psm);
}
/* Free the timer */
alarm_free(p_ccb->l2c_ccb_timer); // 释放 CCB 上的定时器,防止内存泄露
p_ccb->l2c_ccb_timer = NULL; // L2CAP 信道可能设置有定时器,如响应超时或配置超时等
fixed_queue_free(p_ccb->xmit_hold_q, osi_free); // 清除待发送的数据队列(暂存的数据帧队列), 使用 osi_free 作为释放每个队列元素的回调
p_ccb->xmit_hold_q = NULL;
l2c_fcr_cleanup(p_ccb); // 清理 FCR(Flow Control and Retransmission)相关资源, 如果信道启用了增强重传模式/流控机制(如 AVCTP),此处会释放相关缓存/计时器等
/* Channel may not be assigned to any LCB if it was just pre-reserved */
if ((p_lcb) && ((p_ccb->local_cid >= L2CAP_BASE_APPL_CID))) {
l2cu_dequeue_ccb(p_ccb); // 如果这个 CCB 已经绑定在某个 LCB 上(动态信道),就将其从 LCB 的 CCB 队列中移除
/* Delink the CCB from the LCB */
p_ccb->p_lcb = NULL; // 然后断开与 LCB 的关联(从逻辑连接中解绑)
}
// 下面几行将 CCB 归还给"空闲池"
// 维护一个链表用于管理空闲的 CCB(资源池管理)。
// 提高资源重用效率,避免频繁 malloc/free
/* Put the CCB back on the free pool */
if (!l2cb.p_free_ccb_first) {
// 空闲链表为空
l2cb.p_free_ccb_first = p_ccb;
l2cb.p_free_ccb_last = p_ccb;
p_ccb->p_next_ccb = NULL;
p_ccb->p_prev_ccb = NULL;
} else {
// 挂在链表尾部
p_ccb->p_next_ccb = NULL;
p_ccb->p_prev_ccb = l2cb.p_free_ccb_last;
l2cb.p_free_ccb_last->p_next_ccb = p_ccb;
l2cb.p_free_ccb_last = p_ccb;
}
/* Flag as not in use */
p_ccb->in_use = false; // 标记为未使用状态,允许被下一次连接分配
// 清空远端通道 ID、本地信令 ID 以及"待移除"标记。
// Clear Remote CID and Local Id
p_ccb->remote_cid = 0;
p_ccb->local_id = 0;
p_ccb->pending_remove = false;
// 最后:连接层状态感知,决定是否要设置"链接空闲超时"或调整配额
/* If no channels on the connection, start idle timeout */
if ((p_lcb) && p_lcb->in_use) {
if (p_lcb->link_state == LST_CONNECTED) { // 如果 link 正常连接中.
if (!p_lcb->ccb_queue.p_first_ccb) { // 当前 link 上没有任何 CCB(即所有通道都断了)
// Closing a security channel on LE device should not start connection
// timeout
if (p_lcb->transport == BT_TRANSPORT_LE &&
p_ccb->local_cid == L2CAP_SMP_CID)
return; // 特殊 case:如果是 LE SMP 信道断开,不要触发超时计时器(可能仍有 ATT 保活)
// 否则,启动链接的 idle timer(如 L2CAP 闲置一段时间后断开连接)
l2cu_no_dynamic_ccbs(p_lcb);
} else {
// 如果还有通道存在,就调整带宽/通道配额(QoS 优化)
/* Link is still active, adjust channel quotas. */
l2c_link_adjust_chnl_allocation();
}
} else if (p_lcb->link_state == LST_CONNECTING) {
// 当 link 还处于"连接中"状态,但所有 CCB 都被释放(比如连接失败、超时),则主动断开 LE 链路
// 典型用于 ATT 建链失败或上层取消连接等场景。
if (!p_lcb->ccb_queue.p_first_ccb) {
if (p_lcb->transport == BT_TRANSPORT_LE &&
p_ccb->local_cid == L2CAP_ATT_CID) {
L2CAP_TRACE_WARNING("%s - disconnecting the LE link", __func__);
l2cu_no_dynamic_ccbs(p_lcb);
}
}
}
}
}
l2cu_release_ccb
是用于在 L2CAP 通道关闭后彻底释放信道控制块的函数,是连接生命周期中"善后阶段"的核心操作。
类型 | 说明 |
---|---|
功能 | 释放动态信道 CCB,清理资源、回收内存 |
输入 | tL2C_CCB* p_ccb ,表示要释放的 L2CAP 信道 |
最终效果 | 从 LCB 中移除通道,释放定时器、队列、状态,重置 CCB,更新空闲池 |
特别处理 | 对 SMP/ATT 的连接状态做了特殊保护(如不启动 idle timeout) |
常见触发点 | 信道断开、配置失败、连接超时、上层释放连接请求 |
功能:
-
释放一个动态 L2CAP 信道的控制块(CCB)。
-
断开与该通道关联的资源、队列、计时器等。
-
将 CCB 从 LCB 的队列中移除,使其可以被重用或销毁。
典型调用时机
l2cu_release_ccb()
的调用并不是主动触发的 API ,而是作为状态机驱动下的"清理收尾动作"出现。
调用源头函数(常见):
-
l2c_csm_execute(L2CAP 信道状态机执行器)
-
多种状态处理函数中在某些事件下调用:
-
L2CEVT_DISCONNECT_REQ
-
L2CEVT_DISCONNECT_IND
-
L2CEVT_TIMEOUT
-
-
-
l2c_csm_closed()
- 在信道关闭后,调用此函数进行 CCB 回收。
-
l2cu_disconnect_chnl()
- 在断开信道时,最终调用
l2cu_release_ccb()
来释放资源。
- 在断开信道时,最终调用
常见应用场景分析
场景编号 | 典型场景 | 说明 |
---|---|---|
1 | A2DP/AVRCP 等 L2CAP 信道连接断开 | 通道断开后,释放相应 CCB,释放内存、清理状态 |
2 | L2CAP 信道超时未响应 | 超时事件触发状态机跳转至 CLOSED,调用本函数清理 |
3 | 上层主动请求断开连接 | 上层通过 L2CA_DisconnectReq 发起断开,最后清理 CCB |
4 | 连接失败(配置阶段失败等) | 连接未建立成功,提前退出并释放 CCB |
5 | L2CAP 信道异常中止 | 如收到非法 L2CAP PDU 或远端突然断开等场景 |
5. 小结
函数 | 作用 | 常见调用时机 | 场景举例 |
---|---|---|---|
L2CA_SetIdleTimeoutByBdAddr |
设置ACL链路整体空闲超时 | GATT连接成功后,Pairing完成后 | 手机连接手表后30秒无通信断开 |
L2CA_SendFixedChnlData |
通过固定信道发送L2CAP数据包 | 发ATT读写命令,发SMP配对请求 | 手机读灯泡状态,发pairing包 |
L2CA_SetLeGattTimeout |
设置LE链路GATT专属超时时间 | GATT连接建立后,连接参数更新后 | 手环连接超时控制 |
3. 检查是否要断开 acl
shell
1. L2CA_SetIdleTimeoutByBdAddr -> l2cu_no_dynamic_ccbs
2. L2CA_SendFixedChnlData -> l2cu_no_dynamic_ccbs
3. L2CA_SetLeGattTimeout -> l2cu_no_dynamic_ccbs
4. l2cu_release_ccb -> l2cu_no_dynamic_ccbs
上面都是 通过 触发 l2cu_no_dynamic_ccbs 来定时检查 是否要断开acl, 本节就来梳理一下对应的逻辑。
1. l2cu_no_dynamic_ccbs
- 当一个连接(LCB)上已经没有动态信道(CCB)时,根据固定信道的闲置超时时间,决定是否要断开这条连接。
c
// system/stack/l2cap/l2c_utils.cc
void l2cu_no_dynamic_ccbs(tL2C_LCB* p_lcb /* 参数是指向 tL2C_LCB(L2CAP Link Control Block)的指针,表示当前要检查的连接对象 */) {
tBTM_STATUS rc; // 后面调用安全管理器断开连接时用到的返回值。
uint64_t timeout_ms = p_lcb->idle_timeout * 1000; // 初始化为LCB的默认空闲超时时间(秒转成毫秒)。
bool start_timeout = true; // 标记是否要启动超时定时器。
int xx; // 循环变量,后面遍历固定信道数组用
// 这段循环就是:在所有固定信道里,找出最长闲置时间作为连接的超时依据。
for (xx = 0; xx < L2CAP_NUM_FIXED_CHNLS; xx++) {
// 遍历所有固定信道(固定信道数量是常量 L2CAP_NUM_FIXED_CHNLS,比如典型的ATT/GATT信道)。
if ((p_lcb->p_fixed_ccbs[xx] != NULL)/*如果这个固定信道存在*/ &&
(p_lcb->p_fixed_ccbs[xx]->fixed_chnl_idle_tout * 1000 > timeout_ms /*并且它配置的闲置超时时间(毫秒)比当前记录的更长*/)) { // 那么就认为以这个固定信道的超时时间为准。
if (p_lcb->p_fixed_ccbs[xx]->fixed_chnl_idle_tout ==
L2CAP_NO_IDLE_TIMEOUT) { // 如果固定信道设置的是 永不超时(L2CAP_NO_IDLE_TIMEOUT)
// 打印日志,说明这个固定信道禁止空闲断开
L2CAP_TRACE_DEBUG("%s NO IDLE timeout set for fixed cid 0x%04x",
__func__, p_lcb->p_fixed_ccbs[xx]->local_cid);
start_timeout = false; // 于是设置 start_timeout = false,表示不启动定时器
}
// 更新 timeout_ms 为当前信道的空闲超时时间。
timeout_ms = p_lcb->p_fixed_ccbs[xx]->fixed_chnl_idle_tout * 1000;
}
}
/* If the link is pairing, do not mess with the timeouts */
// 如果正在配对(bonding),直接返回,不断开也不启动超时. 因为配对期间连接是有意义的,即使没有数据传输.
if (p_lcb->IsBonding()) return;
// 打日志,记录下with_active_local_clients(是否有主动使用ATT/GATT的本地客户端)
L2CAP_TRACE_DEBUG("l2cu_no_dynamic_ccbs() with_active_local_clients=%d",
p_lcb->with_active_local_clients);
// Inactive connections should not timeout, since the ATT channel might still
// be in use even without a GATT client. We only timeout if either a dynamic
// channel or a GATT client was used, since then we expect the client to
// manage the lifecycle of the connection.
// FOR T ONLY: We add the outer safety-check to only do this for LE/ATT, to
// minimize behavioral changes outside a dessert release. But for consistency
// this should happen throughout on U (i.e. for classic transport + other
// fixed channels too)
if (p_lcb->p_fixed_ccbs[L2CAP_ATT_CID - L2CAP_FIRST_FIXED_CHNL] != NULL) {
// 如果 ATT信道(0x0004)是打开的,
if (bluetooth::common::init_flags::finite_att_timeout_is_enabled() &&
!p_lcb->with_active_local_clients) {
// 并且全局开关 finite_att_timeout 打开,且本地没有活跃的GATT客户端.
// 则不设置超时,不断开
// (原因:即使没有GATT Client,还可能有其他用途,比如GATT Server还在等。
return;
}
}
// 如果超时时间是 0,立即断开连接.
if (timeout_ms == 0) {
// 打日志,表示准备断开。
L2CAP_TRACE_DEBUG(
"l2cu_no_dynamic_ccbs() IDLE timer 0, disconnecting link");
// 调用 btm_sec_disconnect() 通过安全管理器发起断链操作.
// 原因码是 HCI_ERR_PEER_USER,即用户主动断开
rc = btm_sec_disconnect(
p_lcb->Handle(), HCI_ERR_PEER_USER,
"stack::l2cap::l2c_utils::l2cu_no_dynamic_ccbs Idle timer popped");
// 然后根据断链函数返回结果分别处理
if (rc == BTM_CMD_STARTED) {
// 如果断开已经开始(但是异步进行中)
l2cu_process_fixed_disc_cback(p_lcb);
p_lcb->link_state = LST_DISCONNECTING; // 更新LCB状态为 LST_DISCONNECTING
timeout_ms = L2CAP_LINK_DISCONNECT_TIMEOUT_MS; // 并设置断链超时时间(短时间,比如2秒,防止卡死)
} else if (rc == BTM_SUCCESS) {
// 如果断链 立刻成功
l2cu_process_fixed_disc_cback(p_lcb);
/* BTM SEC will make sure that link is release (probably after pairing is
* done) */
p_lcb->link_state = LST_DISCONNECTING;
// 同样更新状态,但这里就不需要再启动新的超时定时器了
start_timeout = false;
} else if (p_lcb->IsBonding()) {
// 如果当前正在 Bonding
// 直接调用ACL层断开函数,进入断开流程。
acl_disconnect_from_handle(
p_lcb->Handle(), HCI_ERR_PEER_USER,
"stack::l2cap::l2c_utils::l2cu_no_dynamic_ccbs Bonding no traffic");
l2cu_process_fixed_disc_cback(p_lcb);
p_lcb->link_state = LST_DISCONNECTING;
timeout_ms = L2CAP_LINK_DISCONNECT_TIMEOUT_MS; // 超时时间设置为断开保护时长。
} else {
// 如果其他情况(比如没有Buffer可发断链包),
/* probably no buffer to send disconnect */
timeout_ms = BT_1SEC_TIMEOUT_MS; // 设置成1秒的保护超时。
}
}
// 最后,启动或取消定时器
if (start_timeout) {
// 启动定时器,超时后会回调 l2c_lcb_timer_timeout() 继续处理
LOG_INFO("trace_l2c_lcb_timer_timeout [%s:%d]", __func__, __LINE__);
alarm_set_on_mloop(p_lcb->l2c_lcb_timer, timeout_ms, l2c_lcb_timer_timeout,
p_lcb);
LOG_INFO("Started link IDLE timeout_ms:%lu", (unsigned long)timeout_ms);
} else {
// 否则,取消之前可能设置过的定时器。
alarm_cancel(p_lcb->l2c_lcb_timer);
}
}
如果超时后, 将触发 l2c_lcb_timer_timeout 调用
c
// system/stack/l2cap/l2c_main.cc
void l2c_lcb_timer_timeout(void* data) {
tL2C_LCB* p_lcb = (tL2C_LCB*)data;
l2c_link_timeout(p_lcb); // 最终调用 它来处理
}
2. l2c_link_timeout
我们先来看一下 l2c_link_timeout 函数做了那些事情:
这是 L2CAP 连接(ACL链路)超时处理函数,比较核心:
- 目的 :在某条 ACL物理链路 (
p_lcb
) 的超时事件到达时,进行处理。 - 参数 :
p_lcb
是指向当前超时链路的tL2C_LCB
结构体(L2CAP的"Link Control Block")。 - 触发场景 :
- 连接超时(比如蓝牙配对中对方长时间无回应)。
- 正常连接后因为空闲太久而超时。
- 正在断开但未完成,超时清理。
c
// system/stack/l2cap/l2c_link.cc
void l2c_link_timeout(tL2C_LCB* p_lcb) {
tL2C_CCB* p_ccb; // 指向某一条L2CAP Channel(逻辑信道,CCB是Channel Control Block)。
tBTM_STATUS rc; // 保存调用底层断链(btm_sec_disconnect)函数的返回状态
// 打印链路超时时,链路的状态和是否在Bonding过程(配对过程中)。
LOG_INFO("L2CAP - l2c_link_timeout() link state:%s is_bonding:%s",
link_state_text(p_lcb->link_state).c_str(),
logbool(p_lcb->IsBonding()).c_str());
/* If link was connecting or disconnecting, clear all channels and drop the
* LCB */
/*
第一部分:链路如果是连接中/断开中
如果链路状态是:
LST_CONNECTING_WAIT_SWITCH:连接中,等待role switch(主从角色切换)。
LST_CONNECTING:连接过程中。
LST_CONNECT_HOLDING:连接建立后,准备阶段。
LST_DISCONNECTING:正在断开。
说明连接还没成功或者正在断开,但超时了。
处理措施:
连接还没建好 or 正在断开又超时了?------强制清理掉所有channel,释放链路!
*/
if ((p_lcb->link_state == LST_CONNECTING_WAIT_SWITCH) ||
(p_lcb->link_state == LST_CONNECTING) ||
(p_lcb->link_state == LST_CONNECT_HOLDING) ||
(p_lcb->link_state == LST_DISCONNECTING)) {
p_lcb->p_pending_ccb = NULL; // 清空当前正在等待连接的channel。
/* For all channels, send a disconnect indication event through */
/* their FSMs. The CCBs should remove themselves from the LCB */
// 遍历所有挂在这条链路的L2CAP频道(CCB)。
for (p_ccb = p_lcb->ccb_queue.p_first_ccb; p_ccb;) {
tL2C_CCB* pn = p_ccb->p_next_ccb;
// 给每个CCB的状态机发送 L2CEVT_LP_DISCONNECT_IND 消息:
// 告诉Channel"链路层断了",每个Channel要自己清理自己。
l2c_csm_execute(p_ccb, L2CEVT_LP_DISCONNECT_IND, NULL);
p_ccb = pn;
}
/* Release the LCB */
l2cu_release_lcb(p_lcb); // 把这条链路 p_lcb 彻底释放掉(删掉)。
}
/*
第二部分:链路如果是已连接
*/
/* If link is connected, check for inactivity timeout */
if (p_lcb->link_state == LST_CONNECTED) { // 链路当前是 已经连接成功的状态。
/* If no channels in use, drop the link. */
if (!p_lcb->ccb_queue.p_first_ccb) { // 子情况一:没有任何Channel在用了
/*
ccb_queue 空了 -> 没有任何应用在用这个连接了(比如应用断开了所有L2CAP频道)。该断开物理链路了!
*/
uint64_t timeout_ms;
bool start_timeout = true;
LOG_WARN("TODO: Remove this callback into bcm_sec_disconnect");
rc = btm_sec_disconnect(
p_lcb->Handle(), HCI_ERR_PEER_USER/*传入理由是 HCI_ERR_PEER_USER(本机主动断开)*/,
"stack::l2cap::l2c_link::l2c_link_timeout All channels closed"/*并附带一些日志描述*/); // 请求底层断开ACL链路。
// 根据断链调用结果处理
if (rc == BTM_CMD_STORED) { // Security Manager暂时保存了断链命令,稍后处理。
/* Security Manager will take care of disconnecting, state will be
* updated at that time */
start_timeout = false;
} else if (rc == BTM_CMD_STARTED) { // 成功发起了断链 → 设置状态为"正在断开",并设定断开超时时间。
p_lcb->link_state = LST_DISCONNECTING;
timeout_ms = L2CAP_LINK_DISCONNECT_TIMEOUT_MS;
} else if (rc == BTM_SUCCESS) { // 已经断了,直接处理固定通道的断开回调,标记断开中。
l2cu_process_fixed_disc_cback(p_lcb);
/* BTM SEC will make sure that link is release (probably after pairing
* is done) */
p_lcb->link_state = LST_DISCONNECTING;
start_timeout = false;
} else if (rc == BTM_BUSY) { // 底层还在忙(比如配对流程还没结束),就不着急处理。
/* BTM is still executing security process. Let lcb stay as connected */
start_timeout = false;
} else if (p_lcb->IsBonding()) { // 如果正在Bonding配对中,主动发起ACL断开。
acl_disconnect_from_handle(p_lcb->Handle(), HCI_ERR_PEER_USER,
"stack::l2cap::l2c_link::l2c_link_timeout "
"Timer expired while bonding");
l2cu_process_fixed_disc_cback(p_lcb);
p_lcb->link_state = LST_DISCONNECTING;
timeout_ms = L2CAP_LINK_DISCONNECT_TIMEOUT_MS;
} else { // 其他意外情况(比如没资源发断开命令)→ 延时1秒后再检查。
/* probably no buffer to send disconnect */
timeout_ms = BT_1SEC_TIMEOUT_MS;
}
// 是否需要重新设置定时器?
if (start_timeout) {
// 如果需要(start_timeout为true),就重新设个超时定时器,继续监视。
alarm_set_on_mloop(p_lcb->l2c_lcb_timer, timeout_ms,
l2c_lcb_timer_timeout, p_lcb);
}
} else {
// 子情况二:还有Channel在用
// 检查一下是否有积压的包需要发送(比如之前被流控block住的)。
/* Check in case we were flow controlled */
l2c_link_check_send_pkts(p_lcb, 0, NULL);
}
}
}
整个 l2c_link_timeout()
流程可以概括为两种场景:
链路状态 | 处理逻辑 |
---|---|
正在连接/断开中 | 清空所有Channel,释放链路 |
已连接(但空闲) | 如果没Channel了,开始断链;如果还有Channel,就检查发包 |
而且在断链时,细分了各种不同的返回值处理,比如 BTM_CMD_STORED
、BTM_BUSY
等非常仔细。