管理帧的发送 — 控制面的帧处理

"数据帧是乘客,管理帧是塔台的调度指令——乘客只关心能不能到达,调度指令决定了飞机能不能起飞。"


本章导读

WiFi 的帧分为三大类:数据帧(运乘客)、管理帧(发指令)、控制帧(自动化信号)。上一章我们讲完了连接的全流程,但你有没有想过——那些 Auth、Assoc、Action 帧是怎么从用户态一路下发到固件的?

这就是本章的主题:管理帧在控制面的完整发送路径。从 supplicant 构建帧体开始,穿过 nl80211 的内核边界,进入 QCOM 和 MTK 各自的驱动世界,最终到达固件。

本章你将学到:

  • 管理帧三分类框架:supplicant 发 / 驱动自主管 / 硬件不可见
  • supplicant 侧的帧构建:Auth、Assoc、Deauth/Disassoc、通用 MLME 四个核心函数
  • nl80211 下发路径:从用户态到内核的完整调用链
  • QCOM 驱动的管理帧双路径:Auth 快速通道 vs Action 标准通道
  • MTK 驱动的管理帧发送:单入口 + 大帧直通优化

接收路径(Action 帧分类分发、QCOM/MTK 的 RX 架构对比、硬件不可见帧)将在下一章《管理帧的接收与分发》独立讲解。


1 管理帧三分类:谁发什么?

在深入代码之前,先建立一个全局框架。802.11 管理帧按 "谁负责发送" 可以分为三类:

分类帧类型发送者说明
用户态发起Auth、Assoc Req、Deauth/Disassoc、Actionsupplicant 构建,经 nl80211 下发驱动supplicant 是 "飞行计划的制定者"
驱动自主管理Beacon、Probe Resp驱动 / 固件自主生成和维护塔台内部的自动化系统——不用每次都请示调度中心
硬件不可见ACK、RTS/CTS、PS-Poll固件 / 硬件直接处理,驱动看不到跑道上的全自动信号灯——软件层连帧长什么样都不知道

这条边界清晰:supplicant 只发 STA 主动发起的帧;驱动和固件管 Beacon/Probe Resp 和所有硬件帧。理解了这个框架,再看后面的代码时,你就知道每一段代码在塔台的哪一层、管哪些指令。

管理帧三分类与发送全景

:图中底部 cfg80211_rx_mgmt() 为管理帧接收路径入口,完整接收流程详见下一章《管理帧的接收与分发》。


2 supplicant 侧的帧构建:飞行计划怎么写?

2.1 Auth 帧构建:sme_send_authentication()

Auth 帧的构建从 sme_send_authentication() 开始(wpa_supplicant/sme.c)。这个函数根据当前网络的安全类型选择认证算法。核心逻辑是算法选择分支——根据 ssid->key_mgmt 决定 params.auth_alg

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// wpa_supplicant/sme.c — sme_send_authentication() 算法选择段
params.auth_alg = WPA_AUTH_ALG_OPEN; // 默认:开放认证

#ifdef CONFIG_SAE
if (wpa_key_mgmt_sae(ssid->key_mgmt)) { // WPA3-SAE
rsn = wpa_bss_get_rsne(wpa_s, bss, ssid, false);
if (rsn && wpa_parse_wpa_ie(rsn, 2 + rsn[1], &ied) == 0 &&
wpa_key_mgmt_sae(ied.key_mgmt)) {
params.auth_alg = WPA_AUTH_ALG_SAE; // SAE(多轮 Commit/Confirm)
// 注:源码中 DPP 优先级高于 SAE,且无 PMF 时跳过 SAE(省略 FILS SHA384/PFS、DPP、OWE 等路径)
}
}
#endif

#ifdef CONFIG_IEEE80211R
if (md && wpa_s->sme.prev_bssid_set && wpa_s->sme.ft_used) {
// 注:源码还检查 MD 匹配 (os_memcmp) 和 FT 密钥存在性 (wpa_sm_has_ft_keys),此处省略
params.auth_alg = WPA_AUTH_ALG_FT; // 802.11r 快速切换
params.ie = wpa_s->sme.ft_ies; // 预构建的 FT IE
}
#endif
// ...省略 FILS SHA384、FILS PFS、DPP、OWE 等路径...

主要功能:

  • OPEN(默认):最简单的开放认证,两个空帧交换
  • SAE(WPA3):Dragonfly 密钥交换,需要多轮 Commit/Confirm;仅当 BSS 的 RSN IE 声明 SAE AKM 且未被 DPP 覆盖、PMF 已启用时才激活;支持 PMKSA caching 加速重连
  • FT(802.11r):快速 BSS 切换,前提是 prev_bssid_setft_used——说明之前已经在这个 Mobility Domain 里完成过初始认证
  • FILS(802.11ai):极简初始链路建立,适用于 IoT 场景;通过 cache_id 复用 PMKSA

函数构建好 Auth 帧体后,调用 wpa_drv_authenticate() 下发。

2.2 Assoc 帧构建:sme_associate()

Assoc Req 是管理帧中最 "胖" 的帧——它要携带 RSN IE(安全能力)、HT Cap IE(802.11n 能力)、VHT Cap IE(802.11ac 能力)、HE Cap IE(802.11ax 能力)等一系列信息元素。

sme_associate()wpa_supplicant/sme.c)的核心工作是把 sme_send_authentication() 阶段预构建好的各类 Capability IE(存放在 wpa_s->sme.assoc_req_ie 中)打包进 wpa_driver_associate_params。以下是 FT(802.11r)路由下的 IE 重排逻辑——最能体现 assoc_req_ie 缓冲区操作的精髓:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// wpa_supplicant/sme.c — sme_associate() FT IE 重排段
#ifdef CONFIG_IEEE80211R
if (auth_type == WLAN_AUTH_FT && wpa_s->sme.ft_ies) {
// 步骤1:从 assoc_req_ie 中移除 RSNE、MDE(Mobility Domain Element,移动域元素)、FTE(Fast BSS Transition Element,快速 BSS 切换元素)——将被 FT 版本覆盖
remove_ie(wpa_s->sme.assoc_req_ie,
&wpa_s->sme.assoc_req_ie_len, WLAN_EID_RSN);
remove_ie(wpa_s->sme.assoc_req_ie,
&wpa_s->sme.assoc_req_ie_len, WLAN_EID_MOBILITY_DOMAIN);
remove_ie(wpa_s->sme.assoc_req_ie,
&wpa_s->sme.assoc_req_ie_len, WLAN_EID_FAST_BSS_TRANSITION);
// 步骤2:用 os_memmove 腾出空间,按规范顺序(RSNE→MDE→FTE)插入
os_memmove(wpa_s->sme.assoc_req_ie + wpa_s->sme.ft_ies_len,
wpa_s->sme.assoc_req_ie, wpa_s->sme.assoc_req_ie_len);
// 步骤3:逐个复制 RSNE、MDE、FTE 到缓冲区头部
os_memcpy(wpos, pos, 2 + pos[1]); // RSNE
// ...省略 MDE/FTE 复制及 RM Enabled Capabilities 插入...
params.wpa_ie = wpa_s->sme.assoc_req_ie;
params.wpa_ie_len = wpa_s->sme.assoc_req_ie_len;
}
#endif

主要功能:

  • remove_ieassoc_req_ie 缓冲区中删除指定 EID 的 IE,同时减少 assoc_req_ie_len(原地修改,无返回值)
  • IE 打包不是简单的 memcpy 追加——FT 路由下需要先移除旧 IE,再用 memmove 腾空间,按 802.11 规范顺序(RSNE → MDE → FTE)重新插入
  • 除此之外,FILS、OWE、DPP PFS、MSCS、Multi-AP 等多种认证类型各有独立 IE 追加路径——它们的工作模式相同:构造 IE wpabuf → 追加到 assoc_req_ie 尾部 → 增加 assoc_req_ie_len
  • 以 OWE(Opportunistic Wireless Encryption,机会无线加密,RFC 8110 定义的无预配置安全机制)为例:当 auth_type == WLAN_AUTH_OPENkey_mgmt == WPA_KEY_MGMT_OWE 时,sme_associate() 根据配置中的 ssid->owe_group 确定 DH 组(若上次关联收到 GROUP_NOT_SUPPORTED 状态码则自动升级到下一个组)
  • 随后调用 owe_build_assoc_req() 构造 OWE DH Parameter IE(扩展 IE,EID 255 + 扩展 EID WLAN_EID_EXT_OWE_DH_PARAM,内含 STA 的 DH 公钥),追加到 assoc_req_ie 尾部
  • 追加前检查缓冲区容量,追加后释放临时 wpabuf。与 FT 的 memmove 重排路径相比,OWE 路径简单直接:不删旧 IE、不腾空间、只追加一个新的扩展 IE
  • IE 追加失败处理:assoc_req_ie 定长,追加前检查若溢出则打印错误日志并放弃本次关联(return)或跳过该 IE(DPP PFS 走 pfs_fail 回退,不带 PFS 继续)
  • 最终通过 wpa_drv_associate(wpa_s, &params) 将完整的 assoc_req_ie 下发驱动,并注册 sme_assoc_timer 超时定时器

2.3 Deauth/Disassoc 帧:终止飞行计划的撤销指令

Auth 和 Assoc 是 "建立连接",Deauth 和 Disassoc 是 "断开连接"。在 P2P 等非标准场景下,它们的 TX 路径与 Action 帧、PASN 帧一样走 wpa_drv_send_mlme()NL80211_CMD_FRAME 下发(标准 STA 断连走 wpa_drv_deauthenticate() 专用命令)。

但与 Auth/Assoc 有一个关键差异:没有 SAE/FT/FILS 那样的多轮算法选择。Deauth 帧只需要携带一个 2 字节的 Reason Code(如 WLAN_REASON_DEAUTH_LEAVING = 3 表示 STA 主动离开)。

Deauth/Disassoc 的 RX 路径值得注意——Auth/Assoc 通常只有 STA 发起,但 Deauth/Disassoc 可以从 AP 侧主动发起。AP 发来的 Deauth 帧经驱动 RX 路径上报 supplicant 后,触发 wpa_supplicant_event_disassoc_finish() 进行状态清理:

  • PSK 不匹配检测(4-Way Handshake 失败时判断密码是否错误)
  • 自动重连决策(快速重连同一 BSS vs 全扫描):仅当原因为可恢复(disconnect_reason_recoverable())且该 BSS 未被黑名单(disallowed_bssid())或临时禁用(wpas_temp_disabled())拦下时才快速重连,否则退化全扫描
  • 密钥清除(wpa_clear_keys() 清零 PTK/GTK)+ 状态机清零(wpa_supplicant_mark_disassoc()

RX 路径的完整分析(QCOM/MTK 的 Deauth RX 上报流程、nl80211 事件分发到 supplicant 的链路)详见下一章《管理帧的接收与分发》Deauth RX 上报相关小节。

MTK 在驱动侧注册了专门的 mtk_cfg_deauth() 回调,它的实现非常薄——只做驱动就绪检查和 P2P 设备类型校验,实际帧构建委托给 mtk_p2p_cfg80211_deauth()

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// MTK: os/linux/gl_cfg80211.c — mtk_cfg_deauth() 驱动就绪检查
int mtk_cfg_deauth(struct wiphy *wiphy,
struct net_device *dev,
struct cfg80211_deauth_request *req)
{
struct GLUE_INFO *prGlueInfo = NULL;
WIPHY_PRIV(wiphy, prGlueInfo);

if (!wlanIsDriverReady(prGlueInfo, WLAN_DRV_READY_CHECK_WLAN_ON |
WLAN_DRV_READY_CHECK_HIF_SUSPEND)) {
DBGLOG(REQ, WARN, "driver is not ready\n");
return -EFAULT;
}
// ...省略 P2P 设备校验...
return mtk_p2p_cfg80211_deauth(wiphy, dev, req);
}

QCOM 没有注册独立的 deauth cfg80211_ops 回调——supplicant 侧通过 wpa_drv_send_mlme() 将 Deauth 帧包装为 NL80211_CMD_FRAME 下发,从而进入 QCOM 的 __wlan_hdd_mgmt_tx mgmt_tx 回调。在 __wlan_hdd_mgmt_tx 内部,Deauth 帧(subtype=0xC0)不命中 Auth 快速通道(subtype=0xB0),走标准 wlan_cfg80211_mgmt_tx 路径。

2.4 通用 MLME 发送包装:wpa_drv_send_mlme()

wpa_drv_send_mlme() 是 Action 帧、P2P 管理帧、PASN 认证帧以及(非标准路径下的)Deauth/Disassoc 帧的统一收口函数,它是一个单行跳转的内联包装:

1
2
3
4
5
6
7
8
9
10
11
// wpa_supplicant/driver_i.h — 所有管理帧发送的统一收口
static inline int wpa_drv_send_mlme(struct wpa_supplicant *wpa_s,
const u8 *data, size_t data_len, int noack,
unsigned int freq, unsigned int wait)
{
if (wpa_s->driver->send_mlme)
return wpa_s->driver->send_mlme(wpa_s->drv_priv,
data, data_len, noack,
freq, NULL, 0, 0, wait, -1); // NULL,0,0: bssid/bssid_len/no_cck
return -1;
}

这个函数的设计体现了 supplicant 的驱动抽象层思想:它不关心底层是 nl80211 还是其他内核接口——只通过 wpa_s->driver->send_mlme 函数指针间接调用。

调用方包括 P2P 管理帧发送、PASN(Pre-Association Security Negotiation,802.11az 定义的关联前安全协商,用于在关联之前建立安全测距通道)认证帧发送、以及通过 send_mlme 下发的 Deauth/Disassoc 帧。

注意:Auth 帧走 wpa_drv_authenticate()NL80211_CMD_AUTHENTICATE),Assoc 帧走 wpa_drv_associate()NL80211_CMD_ASSOCIATE),不走本路径。这种间接调用让 supplicant 不绑定具体内核接口,更换驱动后端(如从 nl80211 换成 wext)时无需修改上层代码。


3 nl80211 如何把飞行计划递交到塔台?

范围说明:Auth 帧和 Assoc 帧在标准 supplicant 路径中走各自的 nl80211 专用命令(NL80211_CMD_AUTHENTICATE / NL80211_CMD_ASSOCIATE),不走本节讨论的 NL80211_CMD_FRAME 路径。本节讨论的 NL80211_CMD_FRAME 路径适用于 Action 帧、P2P 管理帧、PASN 认证帧及 Deauth/Disassoc 帧。

从用户态的 wpa_drv_send_mlme() 到内核态的 cfg80211_ops->mgmt_tx(),管理帧要穿过用户态 / 内核态的边界。完整调用链:

1
2
3
4
5
6
7
8
wpa_drv_send_mlme()                                [→ wpa_supplicant/driver_i.h]
→ driver_nl80211_send_mlme() [→ src/drivers/driver_nl80211.c]
→ wpa_driver_nl80211_send_mlme() [→ src/drivers/driver_nl80211.c]
Auth 帧追加检查:Shared Key / SAE 频段 / PASN
→ nl80211_send_frame_cmd() [→ src/drivers/driver_nl80211.c]
=== userspace/kernel boundary (netlink socket) ===
→ nl80211_tx_mgmt() → cfg80211_mlme_mgmt_tx()
→ rdev->ops->mgmt_tx() [→ 各厂商驱动的 mgmt_tx 回调]

几个关键点:

wpa_driver_nl80211_send_mlme() 在最终下发前对 Auth 帧做了额外的合法性检查。以下是其中最核心的三段检查:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// src/drivers/driver_nl80211.c — Auth 帧加密保护检查
if (WLAN_FC_GET_TYPE(fc) == WLAN_FC_TYPE_MGMT &&
WLAN_FC_GET_STYPE(fc) == WLAN_FC_STYPE_AUTH) {
u16 auth_alg = le_to_host16(mgmt->u.auth.auth_alg);
u16 auth_trans = le_to_host16(mgmt->u.auth.auth_transaction);
// 只有 Shared Key 认证的第 4 帧(WEP 加密挑战文本)需要加密
if (auth_alg != WLAN_AUTH_SHARED_KEY || auth_trans != 3)
encrypt = 0;
}

// SAE 频段兜底:用户态 SAE 时自动获取关联频率
if (freq == 0 &&
(drv->capa.flags & WPA_DRIVER_FLAGS_SAE) &&
!(drv->capa.flags & WPA_DRIVER_FLAGS_SME))
freq = nl80211_get_assoc_freq(drv);

// PASN 离信道强制:检测到 PASN 认证帧时允许 off-channel
if (data_len >= IEEE80211_HDRLEN + 2 &&
WPA_GET_LE16(data + IEEE80211_HDRLEN) == WLAN_AUTH_PASN &&
!offchanok)
offchanok = 1;

nl80211_send_frame_cmd() 是用户态最后的包装函数,构造 NL80211_CMD_FRAME netlink 消息。它的 netlink 属性构建采用了 short-circuit 条件链——任一 nla_put_* 失败则跳 fail 标签:

1
2
3
4
5
6
7
8
9
10
11
12
13
// src/drivers/driver_nl80211.c — netlink 属性打包
if (!(msg = nl80211_cmd_msg(bss, 0, NL80211_CMD_FRAME)) ||
(freq && nla_put_u32(msg, NL80211_ATTR_WIPHY_FREQ, freq)) ||
(wait && nla_put_u32(msg, NL80211_ATTR_DURATION, wait)) ||
(offchanok && ((drv->capa.flags & WPA_DRIVER_FLAGS_OFFCHANNEL_TX) ||
drv->test_use_roc_tx) &&
nla_put_flag(msg, NL80211_ATTR_OFFCHANNEL_TX_OK)) ||
(no_cck && nla_put_flag(msg, NL80211_ATTR_TX_NO_CCK_RATE)) ||
(no_ack && nla_put_flag(msg, NL80211_ATTR_DONT_WAIT_FOR_ACK)) ||
// ...省略 CSA offsets...
nla_put(msg, NL80211_ATTR_FRAME, buf_len, buf))
goto fail;
// send_and_recv_resp() → cookie 管理,支持后续取消等待帧

主要功能:

  • Auth 帧检查的三个维度:加密保护(Shared Key)、频段正确性(SAE)、离信道能力(PASN)
  • Netlink 属性打包的核心参数:频率(WIPHY_FREQ)、信道驻留时长(DURATION)、离信道许可(OFFCHANNEL_TX_OK)、帧体(FRAME
  • Cookie 机制:send_and_recv_resp() 获取 cookie 后记录到 send_frame_cookies[] 环形队列,供后续 nl80211_frame_wait_cancel() 取消

Cookie 的存储和匹配机制值得展开:send_and_recv_resp() 返回的 cookie 被存入 send_frame_cookies[] 环形队列(队列大小由 MAX_SEND_FRAME_COOKIES 常量定义)。nl80211_frame_wait_cancel() 在用户态需要取消等待时遍历该环形队列,通过 cookie 匹配找到对应的 netlink 请求并取消等待。

Netlink 消息的 ACK 策略由 no_ack 参数控制,最终通过 NL80211_ATTR_DONT_WAIT_FOR_ACK flag 传递到底层:当上层不需要等待对端确认时(如发送 Disassoc 帧后 STA 即将离开),dont_wait_for_ack = 1,驱动发出帧后立即返回;当需要确认时(如 Action 帧握手),dont_wait_for_ack = 0,驱动会等待对端的响应帧或超时。

过了内核态的 nl80211_tx_mgmt() 之后,帧就进入了 cfg80211_mlme_mgmt_tx(),然后真正的分叉从这里开始——各厂商驱动的 mgmt_tx 回调接管。但在进入厂商世界之前,nl80211_tx_mgmt() 作为内核侧的最后一道关口——塔台安检,做了三层校验:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
// net/wireless/nl80211.c — nl80211_tx_mgmt() 内核侧入口
// 第一层:参数有效性检查
if (!info->attrs[NL80211_ATTR_FRAME]) // 帧体必须存在
return -EINVAL;
if (!rdev->ops->mgmt_tx) // 驱动必须支持 mgmt_tx
return -EOPNOTSUPP;

// 第二层:接口类型白名单(STA/ADHOC/P2P/AP/Mesh 等允许,其他拒绝)
switch (wdev->iftype) {
case NL80211_IFTYPE_STATION: // STA 模式
case NL80211_IFTYPE_AP: // AP 模式
case NL80211_IFTYPE_P2P_CLIENT: // ...省略其他允许的类型...
break;
default:
return -EOPNOTSUPP;
}

// 第三层:wdev_lock 保护下的 off-channel 权限检查
wdev_lock(wdev);
if (params.offchan &&
!cfg80211_off_channel_oper_allowed(wdev, chandef.chan)) {
wdev_unlock(wdev);
return -EBUSY; // 离信道操作冲突,拒绝
}
// ...省略 MLO link_id 校验...
wdev_unlock(wdev);

params.buf = nla_data(info->attrs[NL80211_ATTR_FRAME]);
params.len = nla_len(info->attrs[NL80211_ATTR_FRAME]);
err = cfg80211_mlme_mgmt_tx(rdev, wdev, &params, &cookie); // 进入通用层

这是从通用层到厂商驱动层的 "最后一道关口"——三层校验确保只有合法的帧才能进入驱动:帧体非空、驱动支持、接口类型合法、离信道不冲突。通过这三层后,cfg80211_mlme_mgmt_tx() 调用驱动注册的 mgmt_tx 回调。

两个驱动的 mgmt_tx 回调注册位置分别是:QCOM 在 wlan_hdd_cfg80211_opscore/hdd/src/wlan_hdd_cfg80211.c:27309)中将 .mgmt_tx = wlan_hdd_mgmt_tx,该 wrapper 内部调用本节将剖析的 __wlan_hdd_mgmt_tx()wlan_hdd_p2p.c:272)。

MTK 则在 mtk_cfg_opsos/linux/gl_init.c:1262)中将 .mgmt_tx = mtk_cfg_mgmt_tx,经驱动就绪检查和 P2P/STA 分流后进入 mtk_cfg80211_mgmt_tx()(§5 详述)。两个驱动通过同一个 cfg80211_ops 接口契约接入内核框架——表面统一,底层路径截然不同。


4 QCOM 为什么需要双路径?快速通道和标准通道怎么分流?

说明:标准 STA 的 Auth 帧在 supplicant 侧走 wpa_drv_authenticate()NL80211_CMD_AUTHENTICATE → 驱动 auth() 回调的专用路径,不走 mgmt_tx。本章讨论的 __wlan_hdd_mgmt_tx Auth 快速通道服务于 P2P Auth、用户态 SME 等场景——这些场景下 Auth 帧以 NL80211_CMD_FRAME 的形式进入 mgmt_tx 回调,然后在 HDD 层被识别并分流到快速通道。

如果说 §3 描述的是将飞行计划通过专用网络(nl80211)递交到塔台的过程,那么从本节开始,我们走进 QCOM 这座塔台的调度室内部——看它如何用双通道处理不同紧急程度的调度指令。

QCOM 的管理帧发送有一个其他平台没有的架构亮点——双路径

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
// QCOM: core/hdd/src/wlan_hdd_p2p.c — __wlan_hdd_mgmt_tx() 双路径分流(源码约 130 行,保留核心分支逻辑)
static int __wlan_hdd_mgmt_tx(struct wiphy *wiphy, struct wireless_dev *wdev,
struct ieee80211_channel *chan, bool offchan,
unsigned int wait,
const u8 *buf, size_t len, bool no_cck,
bool dont_wait_for_ack, u64 *cookie)
{
QDF_STATUS status;
struct hdd_adapter *adapter = WLAN_HDD_GET_PRIV_PTR(wdev->netdev);
struct hdd_context *hdd_ctx = WLAN_HDD_GET_CTX(adapter);
uint8_t type, sub_type;

// 从 FC 字段提取帧类型和子类型
type = WLAN_HDD_GET_TYPE_FRM_FC(buf[0]);
sub_type = WLAN_HDD_GET_SUBTYPE_FRM_FC(buf[0]);

/* 源码注释:Auth 走 sme_send_mgmt_tx 无需 Policy Manager;
* wlan_cfg80211_mgmt_tx 需要 ROC 和 Policy Manager 审批 */

/* ====== Layer 1: Auth 快速通道 ====== */
if ((adapter->device_mode == QDF_STA_MODE ||
adapter->device_mode == QDF_SAP_MODE ||
adapter->device_mode == QDF_P2P_CLIENT_MODE ||
adapter->device_mode == QDF_P2P_GO_MODE ||
adapter->device_mode == QDF_NAN_DISC_MODE) &&
(type == SIR_MAC_MGMT_FRAME &&
sub_type == SIR_MAC_MGMT_AUTH)) {

// 例外1:PASN Auth → 需要 off-channel,goto off_chan_tx
if (len > (sizeof(struct wlan_frame_hdr) + WLAN_AUTH_FRAME_MIN_LEN)) {
uint16_t auth_algo = *(uint16_t *)(buf + sizeof(struct wlan_frame_hdr));
if (auth_algo == eSIR_AUTH_TYPE_PASN)
goto off_chan_tx;
}

// 例外2:FT Auth in SAP/GO → 标记 offload 状态后仍走快速通道
if ((adapter->device_mode == QDF_SAP_MODE ||
adapter->device_mode == QDF_P2P_GO_MODE) &&
(len > (sizeof(struct wlan_frame_hdr) + WLAN_AUTH_FRAME_MIN_LEN))) {
uint16_t auth_algo = *(uint16_t *)(buf + sizeof(struct wlan_frame_hdr));
if (auth_algo == eSIR_FT_AUTH) {
struct hdd_ap_ctx *ap_ctx = WLAN_HDD_GET_AP_CTX_PTR(adapter);
ap_ctx->during_auth_offload = false;
}
}

// 直达 SME → PE,绕过 Policy Manager 和 ROC
status = sme_send_mgmt_tx(hdd_ctx->mac_handle,
adapter->vdev_id, buf, len);
return QDF_IS_STATUS_SUCCESS(status) ? 0 : -EINVAL;
// 注意:快速通道失败不 fallback,直接返回错误
}

Layer 1 完成了 Auth 帧的分流:非 PASN Auth 走 PE 直发快速通道,PASN Auth 通过 goto off_chan_tx 跳到标准通道。接下来 Layer 2 处理 FT (Re) Assoc Response 的 offload 场景——当 SAP/GO 模式收到携带 FT IE 的关联响应帧时,需要将 FT 信息直通 hostapd。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
/* ====== Layer 2: FT (Re)Assoc Response offload(SAP/GO 模式) ====== */
if ((adapter->device_mode == QDF_SAP_MODE ||
adapter->device_mode == QDF_P2P_GO_MODE) &&
(type == SIR_MAC_MGMT_FRAME) &&
(sub_type == SIR_MAC_MGMT_ASSOC_RSP ||
sub_type == SIR_MAC_MGMT_REASSOC_RSP)) {
const uint8_t *assoc_resp =
&((struct ieee80211_mgmt *)buf)->u.assoc_resp.variable[0];
uint32_t assoc_resp_len = len - WLAN_ASSOC_RSP_IES_OFFSET
- sizeof(struct wlan_frame_hdr);
struct hdd_ap_ctx *hdd_ap_ctx = WLAN_HDD_GET_AP_CTX_PTR(adapter);
uint32_t ft_info_len = 0;
// 无 FT IE → 走标准通道
if (!wlan_get_ie_ptr_from_eid(DOT11F_EID_FTINFO,
assoc_resp, assoc_resp_len))
goto off_chan_tx;
// 有 FT IE → 通过 wlansap_update_ft_info 直通 hostapd
void *ft_info = hdd_filter_ft_info(assoc_resp, len, &ft_info_len);
if (!ft_info || !ft_info_len)
return -EINVAL;
status = wlansap_update_ft_info(hdd_ap_ctx->sap_context,
((struct ieee80211_mgmt *)buf)->da,
ft_info, ft_info_len, 0);
qdf_mem_free(ft_info);
return QDF_IS_STATUS_SUCCESS(status) ? 0 : -EINVAL;
}

Layer 2 完成后,剩下的所有帧——Action、Deauth、Disassoc、Probe Resp、普通 Assoc Resp 等——全部落入 Layer 3 的 off_chan_tx 标签,进入标准通道。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
    /* ====== Layer 3: Action 帧标准通道 ====== */

off_chan_tx:
struct wlan_objmgr_vdev *vdev;
vdev = hdd_objmgr_get_vdev_by_user(adapter, WLAN_OSIF_P2P_ID);
if (!vdev) {
hdd_err("vdev is NULL");
return -EINVAL;
}
status = wlan_cfg80211_mgmt_tx(vdev, chan, offchan, wait, buf,
len, no_cck, dont_wait_for_ack, cookie);
hdd_objmgr_put_vdev_by_user(vdev, WLAN_OSIF_P2P_ID);
return 0;
}
层级触发条件路径设计意图
Layer 1Auth 帧,非 PASNsme_send_mgmt_tx 直达 PE时延敏感,跳过排队
Layer 2FT (Re) Assoc Resp,SAP/GO 模式wlansap_update_ft_info 直通 hostapdFT 信息需立即同步
Layer 3所有其他管理帧wlan_cfg80211_mgmt_tx 标准通道完整流程,需 ROC 审批

这段代码是整个双路径设计的精髓,核心逻辑分为三层判断:

第一层判断 Auth 快速通道是否命中:

  • 设备模式必须是 STA / SAP / P2P Client / P2P GO / NAN 中的一种
  • 帧类型是管理帧,子类型是 Auth(SIR_MAC_MGMT_AUTH
  • 两个例外:PASN Auth(eSIR_AUTH_TYPE_PASN)通过 goto off_chan_tx 显式跳过快速通道(因为需要 off-channel);FT Auth 在 SAP/GO 模式下设置 during_auth_offload = false 后仍走快速通道

第二层处理 FT (Re) Assoc Response 的 offload:SAP/GO 模式下发 (Re) Assoc Response 时,检测是否携带 FT IE(DOT11F_EID_FTINFO)。有 FT IE 的帧通过 wlansap_update_ft_info() 直通 hostapd(不走 cfg80211);无 FT IE 的普通 Assoc Resp 走 goto off_chan_tx 进入标准通道。

第三层是标准通道 fallback,对应 off_chan_tx 标签:所有不命中前两层的帧(Action、Deauth、Disassoc、Probe Resp、普通 Assoc Resp 等)全部走到 wlan_cfg80211_mgmt_tx(),进入 P2P 组件 → ROC 申请 → WMI 下发的标准流程。

  • 路径 A — Auth 快速通道__wlan_hdd_mgmt_txsme_send_mgmt_tx → PE 层直发。Auth 帧跳过 P2P 组件、跳过 ROC 审批、跳过 Policy Manager,直接送达 PE 层。因为 Auth 帧对时延敏感(超时就断连),不能走标准的 "排队 + 审批" 流程。
  • 路径 B — Action 帧标准通道__wlan_hdd_mgmt_txwlan_cfg80211_mgmt_txwlan_cfg80211_p2p.c)→ P2P 组件调度 → ROC(Remain On Channel)申请信道驻留 → wlan_mgmt_txrx_mgmt_frame_txwma_mgmt_unified_cmd_send → WMI 下发固件。Action 帧要走完整流程:申请信道、获得 Policy Manager 批准、在指定信道上驻留(ROC)、然后才发送。

异常路径:Auth 快速路径失败时 sme_send_mgmt_tx 直接返回 -EINVAL不会 fallback 到 off_chan_tx。这不是 bug——Auth 帧对时延极度敏感,如果 PE 层直发都失败了,再去排队走完整流程已经没意义。唯一命中 off_chan_tx 的 Auth 场景是 PASN 认证帧(通过显式 goto off_chan_tx 跳过快速路径),因为 PASN 需要在 off-channel 上发送。

QCOM 双路径分叉流程

:图中展示 Layer 1(Auth 快速通道)和 Layer 3(标准通道)的主分流逻辑。Layer 2(FT Reassoc Response offload)仅在 SAP/GO 模式下触发——通过 wlan_get_ie_ptr_from_eid(DOT11F_EID_FTINFO) 检测 FT IE,命中后调用 wlansap_update_ft_info() 直通 hostapd。FT IE 检测的完整代码分析见上方「Layer 2: FT (Re) Assoc Response offload」代码块。

双路径的精髓在于:把帧按紧急程度分流,不搞一刀切。这比所有帧走同一个队列的设计更合理。

4.1 Auth 快速通道:完整调用链追踪

如果把 QCOM 驱动比作塔台的调度室,Auth 快速通道就是直通跑道控制器的热线电话——不排队、不等审批,拿起就拨。下面追踪这条热线从拨号到接通的每一步。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
Auth 帧快速通道完整调用链(13 步,全部验证通过)

__wlan_hdd_mgmt_tx() [core/hdd/src/wlan_hdd_p2p.c:272]
│ 命中 Auth 分支(dev_mode 合法 && subType==AUTH && 非 PASN)

└─► sme_send_mgmt_tx() [core/sme/src/common/sme_api.c:5251]
│ 获取全局锁,委托 sme_prepare_mgmt_tx()

└─► sme_prepare_mgmt_tx() [core/sme/src/common/sme_api.c:5220]
│ 分配 sir_mgmt_msg,填充 msg->type = eWNI_SME_SEND_MGMT_FRAME_TX

└─► scheduler_post_message(SME → PE) [SME 到 PE 的跨模块消息队列]
│ 异步投递,PE 的 lim_process_messages() 收到消息

=== 跨模块边界(SME → PE) ===

▼ lim_process_messages()
case eWNI_SME_SEND_MGMT_FRAME_TX: [core/mac/src/pe/lim/lim_process_message_queue.c:1796]

└─► lim_send_mgmt_frame_tx() [core/mac/src/pe/lim/lim_send_management_frames.c:6972]
│ SAE 重试判断 / FT pre-auth 状态更新

└─► lim_send_frame() [core/mac/src/pe/lim/lim_send_management_frames.c:6928]
│ MLD→Link 地址转换、添加管理帧序列号、分配 CDS

└─► lim_tx_mgmt_frame() [core/mac/src/pe/lim/lim_send_management_frames.c:6712]
│ 查 PE session、确定最低 TX 速率

=== 跨模块边界(PE → WMA) ===

▼ wma_tx_frameWithTxComplete() [wma/inc/wma_types.h:469](宏)
│ 展开为 wma_tx_packet() 调用

└─► wma_tx_packet() [core/wma/src/wma_data.c:2259]
│ 查 peer、构建 wmi_mgmt_params

└─► wlan_mgmt_txrx_mgmt_frame_tx() [umac/cmn_services/mgmt_txrx/dispatcher/src/wlan_mgmt_txrx_utils_api.c:458]
│ 获取 txrx descriptor、注册回调

└─► wma_mgmt_unified_cmd_send() [core/wma/src/wma_mgmt.c:4106]

├── WMI 路径(新芯片 Helium/Pronto):
│ └─► wmi_mgmt_unified_cmd_send()
│ [wmi/src/wmi_unified_api.c:665]
│ → WMI_MGMT_TX_SEND_CMDID → 固件

└── HTT 路径(老芯片 Rome):
└─► cdp_mgmt_send_ext() → 固件

逐段解读:

Auth 帧进入 SME 层后(sme_send_mgmt_tx + sme_prepare_mgmt_tx),核心工作只有一件——把帧包成 sir_mgmt_msg 消息,通过 scheduler_post_message() 投递到 PE 的消息队列。注意这里用的是异步消息队列,不是直接函数调用,SME 投递完就返回,PE 在自己的上下文中处理。

1
2
3
4
5
6
7
8
9
10
// core/sme/src/common/sme_api.c — sme_prepare_mgmt_tx() 消息打包
msg->type = eWNI_SME_SEND_MGMT_FRAME_TX;
msg->msg_len = msg_len;
msg->vdev_id = vdev_id;
msg->data = (uint8_t *)msg + sizeof(*msg); // 帧体紧跟在消息头后面
qdf_mem_copy(msg->data, buf, len);

sch_msg.type = eWNI_SME_SEND_MGMT_FRAME_TX;
sch_msg.bodyptr = msg;
scheduler_post_message(QDF_MODULE_ID_SME, QDF_MODULE_ID_PE, ...);

帧消息到达 PE 层后(lim_send_frame + lim_tx_mgmt_frame),lim_send_mgmt_frame_tx() 先做 Auth 算法的差异化处理——SAE 调用 lim_handle_sae_auth_retry() 管理多轮握手状态,FT 更新 pre-auth 节点状态为 eLIM_MLM_AUTHENTICATED_STATE

SAE 重试机制的具体逻辑:lim_handle_sae_auth_retry() 仅在 STA/P2P Client 模式下生效。它根据当前 MLM 状态选择重试次数来源——初始 SAE 认证在 eLIM_MLM_WT_SAE_AUTH_STATE 状态时调用 wlan_mlme_get_sae_auth_retry_count(),漫游场景下调用 wlan_mlme_get_sae_roam_auth_retry_count()

如果重试次数为 0,SAE 重试被禁用,函数直接返回。否则将 Auth 帧体拷贝到 sae_retry->sae_auth 缓冲区,设置 sae_auth_max_retry 字段,启动 g_lim_periodic_auth_retry_timer 定时器(超时值取自 sae_auth_failure_timeout 配置)。

定时器到期后,PE 重新发送缓存的 Auth 帧:每重发一次 sae_auth_max_retry 递减 1,归零即停止重试并调用 lim_sae_auth_cleanup_retry() 清缓冲、停定时器;若中途收到对端响应,状态机离开 eLIM_MLM_WT_SAE_AUTH_STATE,同样走清理路径。

然后 lim_send_frame() 做三个通用动作:MLD 地址转换(MLO 场景下将 MLD MAC 转为 Link MAC)、序列号递增(lim_add_mgmt_seq_num)、分配 CDS 数据包。

MLD 地址转换由 lim_update_mld_to_link_address() 实现,但仅在 wlan_cm_is_sae_auth_addr_conversion_required() 返回 true 时执行——该函数检查当前是否处于 MLO 连接建立后的状态。单链路场景下,整个 MLD 地址转换逻辑是 no-op。

在 MLO 激活时:STA 模式下,用户态在前两轮(第 1、3 个 SAE Auth 帧)给出 MLD 地址,驱动将其替换为对应 peer 的 Link MAC 地址;SAP 模式下,地址转换发生在后两轮(第 2、4 个帧),驱动从 pre_auth_node 中查找 peer 的 Link MAC。

最后 lim_tx_mgmt_frame() 查找 PE session、确定该 BSS 的最低 TX 速率。

帧继续下到 WMA 层(wma_tx_packet → 固件),这里是整个 TX 路径的汇合点——Auth 快速通道和 Action 标准通道在这里汇入同一条 WMI 下发路径。wma_tx_packet() 是 WMA 层的中央 TX 分发器,这样 Auth 快通道和 Action 标准通道在 WMI 下发层面得到了统一,上层路径的分叉不影响固件侧的接收逻辑。

它先填充 wmi_mgmt_params 各字段,再根据 DA/SA 查 peer 对象(先查 i_addr1,再查 i_addr2,最后查 MLD 地址)。

wmi_mgmt_params(定义于 qca-wifi-host-cmn/wmi/inc/wmi_unified_param.h)的关键字段如下:

字段类型含义
tx_framevoid *指向管理帧体的指针,由上层分配并拷贝
frm_lenuint16_t帧体长度(字节)
vdev_iduint8_t虚拟设备 ID,标识帧属于哪个 VAP
chanfrequint16_t发送信道的中心频率(MHz),如 2412、5180
desc_iduint16_t描述符 ID,用于 TX 完成时匹配回调
pdatavoid *私有数据指针,传递给 TX 完成回调(如 lim_tx_complete
macaddruint8_t *目标 MAC 地址(DA),用于 peer 查找
tx_paramstruct tx_send_params嵌套的发送参数,包含 retry_limit(4 位,重试次数上限)、mcs_mask(12 位,速率集掩码)、bw_mask(带宽)等
use_6mbpsuint8_t是否强制使用 6 Mbps 基本速率(OFDM 最低保护速率)

填充完 wmi_mgmt_params 后,调用 wlan_mgmt_txrx_mgmt_frame_tx() 进入统一的管理帧下发通道。注意 Auth 帧注册了两个回调:lim_tx_complete(下载完成回调)和 lim_auth_tx_complete_cnf(OTA 发送确认回调)——后者用来判断 Auth 帧是否真的 "飞出去了"。

WMI vs HTT 分叉wma_mgmt_unified_cmd_send() 内部根据芯片能力分叉——新芯片(Helium/Pronto)检查 wmi_service_mgmt_tx_wmi 标志,走 WMI 的 wmi_mgmt_unified_cmd_send();老芯片(Rome)走 HTT 的 cdp_mgmt_send_ext()。这解释了为什么同一份驱动代码能支持多代硬件。

两条通道的差异不止在「走哪个函数」,更在「发送完成后如何回报上层」:WMI 是控制面通道,帧被打包成 WMI_MGMT_TX_SEND_CMDID 命令下发,固件在 WMI 服务上下文里处理,完成通知以 WMI event 的形式回传,与其它控制命令共用一条消息队列。

HTT 则是数据面通道,cdp_mgmt_send_ext() 进入 DP 层后为帧分配 ol_tx_desc 并把 ext_tid 标记为 HTT_TX_EXT_TID_MGMT,完成通知走 HTT 的 TX completion——而且只有当 ota_ack_cb 回调已注册时才置位 do_tx_complete,否则连 OTA 发送确认都不会产生。

所以同一份驱动代码能覆盖多代硬件,代价是完成通知的时机和可靠性在两代芯片上并不对等。

4.2 Action 标准通道:ROC 申请流程详解

Action 帧(以及 P2P、TDLS 等非 Auth 管理帧)走的是标准通道。从 off_chan_tx 标签开始,不是直接发帧,而是先进 P2P 组件排队。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
Action 帧标准通道调用链(9 步,全部验证通过)

wlan_cfg80211_mgmt_tx() [os_if/p2p/src/wlan_cfg80211_p2p.c:421]
│ Policy Manager 检查(DNBS 信道冲突等)

└─► ucfg_p2p_mgmt_tx() [components/p2p/dispatcher/src/wlan_p2p_ucfg_api.c:297]
│ 分配 tx_action_context、cookie 管理、P2P IE 随机化
│ scheduler_post_message(HDD → P2P, msg.type = P2P_MGMT_TX)

=== 跨模块边界(HDD → P2P 组件) ===

▼ p2p_process_cmd() [components/p2p/core/src/wlan_p2p_main.c:918]
case P2P_MGMT_TX:

└─► p2p_process_mgmt_tx() [components/p2p/core/src/wlan_p2p_off_chan_tx.c:3159]

├── 同信道 或 不需要 off-channel:
│ └─► p2p_execute_tx_action_frame() [p2p_off_chan_tx.c:1806]
│ → p2p_mgmt_tx() [p2p_off_chan_tx.c:1181]

└── 需要 off-channel:
└─► p2p_roc_req_for_tx_action() [p2p_off_chan_tx.c]
│ ROC 申请 → Policy Manager 审批
│ ROC 驻留完成后,scan 事件(p2p_process_ready_on_channel_evt)
│ 触发 TX 上下文出队,回调 p2p_execute_tx_action_frame()

└─► p2p_execute_tx_action_frame() [p2p_off_chan_tx.c:1806]
│ 构建帧体(NoA 更新、RMf 封装、P2P IE 修改)

└─► p2p_mgmt_tx() [p2p_off_chan_tx.c:1181]
│ 查 peer、设置 retry limit、回调注册

└─► wlan_mgmt_txrx_mgmt_frame_tx()
→ wma_mgmt_unified_cmd_send()
→ WMI/HTT → 固件

ROC 的核心逻辑在 p2p_process_mgmt_tx() 中,分三层判断。它先判断当前信道和目标信道是否一致——同信道直接调用 p2p_execute_tx_action_frame() 发帧,不需要 ROC。

需要切信道时,它查找已有的 ROC 上下文:如果已经有相同信道的 ROC 正在进行(ROC_STATE_REQUESTEDROC_STATE_STARTED),就把 TX 上下文挂到 tx_q_roc 等待队列;如果已有 ROC 已经驻留在目标信道(ROC_STATE_ON_CHAN),就重启 ROC 定时器后直接发帧。

只有以上都不匹配时,才调用 p2p_roc_req_for_tx_action() 发起新的 ROC 申请——这个函数会通过 Policy Manager 申请信道使用权,获得批准后回调 p2p_execute_tx_action_frame() 实际发帧。

为何须经 Policy Manager 审批?P2P 组件只掌握自己 vdev 的信道,全局信道视图只在 Policy Manager——DNBS 检测(policy_mgr_is_chan_ok_for_dnbs)要枚举并发 SAP/GO 连接的运行信道,判断切到目标信道是否会打断进行中的数据流。

ROC 的状态机(定义于 p2p_roc.h)共有 4 个状态:

状态含义转换触发
ROC_STATE_IDLE未启动或已完成初始状态;ROC 结束或取消后回到此状态
ROC_STATE_REQUESTED已向 Scan Manager 提交信道切换请求p2p_process_roc_req() 调用 p2p_scan_start() 后设置
ROC_STATE_STARTEDScan Manager 返回启动事件p2p_process_scan_start_evt() 回调收到 start event 后设置
ROC_STATE_ON_CHAN已驻留在目标信道上p2p_process_ready_on_channel_evt() 收到 SCM 的 foreign channel 事件后设置

状态流转为:IDLE →(p2p_process_roc_req 成功)→ REQUESTED →(scan start 事件)→ STARTED →(ready on channel 事件)→ ON_CHAN。进入 ON_CHAN 状态后,ROC 定时器启动(时长由上层 wait 参数决定),定时器到期或帧发送完成后回到 IDLE。整个过程就像塔台临时占用一条跑道:申请获批后进驻,发完帧或驻留超时就归还跑道、回到 IDLE

如果 ROC 申请本身失败了——p2p_roc_req_for_tx_action() 返回非成功状态——p2p_process_mgmt_tx() 会跳到 fail 标签执行清理:先调 p2p_send_tx_conf(tx_ctx, false) 向上层上报发送失败,再从 p2p_idr 表中用 qdf_idr_remove() 注销这条 TX 上下文的 cookie,最后 qdf_mem_free() 依次释放帧体和 tx_action_context 本身。

这一回退路径保证失败时既不泄漏帧缓冲区、也不留悬空 cookie——若只释放内存而不注销 cookie,后续取消等待时 p2p_find_tx_ctx() 就会在 idr 表里找不到上下文,留下悬挂引用。

p2p_process_roc_req() 在发起 ROC 申请时还会执行几项关键操作:调用 qdf_runtime_pm_prevent_suspend() 阻止系统在 ROC 期间进入 suspend;初始化 ROC 超时定时器 roc_timer(回调为 p2p_roc_timeout(),超时后强制回收信道);注册管理帧 RX 回调 p2p_mgmt_rx_ops() 以便在 ROC 期间接收对端响应帧。


5 MTK 的管理帧发送:一个窗口怎么处理所有帧?

如果说 QCOM 的塔台有三个窗口(Auth 快速通道、FT offload、标准通道),MTK 的塔台就只有一个窗口——所有管理帧都从这里过,但帧太大的走旁边的货运通道。

MTK 的管理帧发送比 QCOM 简单得多——一个入口,按帧大小分叉。

mtk_cfg80211_mgmt_tx()os/linux/gl_cfg80211.c)是所有管理帧发送的唯一起点。它调用内部实现 _mtk_cfg80211_mgmt_tx()

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
// MTK: os/linux/gl_cfg80211.c — _mtk_cfg80211_mgmt_tx() 核心路径
// 注:大帧优化由 CFG_SUPPORT_TX_MGMT_USE_DATAQ 编译选项控制,此处假设已启用
// 1. 大帧判断(阈值动态计算)
u2MgmtTxMaxLen = prGlueInfo->prAdapter->chip_info->cmd_max_pkt_size
- prGlueInfo->prAdapter->chip_info->u2CmdTxHdrSize;
// USB 额外扣除 terminator 长度
#if defined(_HIF_USB)
u2MgmtTxMaxLen -= LEN_USB_UDMA_TX_TERMINATOR;
#endif
if (len > u2MgmtTxMaxLen)
return _mtk_cfg80211_mgmt_tx_via_data_path(...); // 走数据面直通

// 2. 分配 MBOX 消息体
prMsgTxReq = cnmMemAlloc(prGlueInfo->prAdapter, RAM_TYPE_MSG,
sizeof(struct MSG_MGMT_TX_REQUEST));

// 3. 填充 MSG_MGMT_TX_REQUEST 字段
prMsgTxReq->fgIsOffChannel = (offchan) ? TRUE : FALSE;
kalChannelFormatSwitch(NULL, chan, &prMsgTxReq->rChannelInfo);
prMsgTxReq->u4Duration = wait;
prMsgTxReq->fgNoneCckRate = (no_cck) ? TRUE : FALSE;
prMsgTxReq->fgIsWaitRsp = (dont_wait_for_ack) ? FALSE : TRUE;
prMsgTxReq->u8Cookie = *cookie;
prMsgTxReq->rMsgHdr.eMsgId = MID_MNY_AIS_MGMT_TX;
prMsgTxReq->ucBssIdx = wlanGetBssIdx(wdev->netdev);

// 4. 帧体拷贝到 MSDU(预留 MAC_TX_RESERVED_FIELD 头部空间 + cookie 尾部空间)
prMgmtFrame = cnmMgtPktAlloc(prGlueInfo->prAdapter,
(int32_t)(len + sizeof(uint64_t) + MAC_TX_RESERVED_FIELD));
pucFrameBuf = (uint8_t *)(prMgmtFrame->prPacket + MAC_TX_RESERVED_FIELD);
kalMemCopy(pucFrameBuf, buf, len);
*pu8GlCookie = *cookie; // cookie 存在帧尾部,供 TX 完成回调匹配
prMgmtFrame->u2FrameLength = len;

// 5. 投递 MBOX 消息到固件
mboxSendMsg(prGlueInfo->prAdapter, MBOX_ID_0,
(struct MSG_HDR *)prMsgTxReq, MSG_SEND_METHOD_BUF);

MSG_MGMT_TX_REQUEST 结构体成员一览:

字段含义
rMsgHdr.eMsgId消息 ID = MID_MNY_AIS_MGMT_TX,固件据此识别消息类型
fgIsOffChannel是否需要 off-channel 发送
rChannelInfo目标信道信息(band + channel number)
eChnlExt信道扩展模式(HT20/HT40/VHT80 等)
u4Durationoff-channel 驻留时长(ms)
fgNoneCckRate是否禁用 CCK 速率
fgIsWaitRsp是否等待对端响应帧
prMgmtMsduInfo指向 MSDU_INFO 的指针(含帧体 + cookie)
u8Cookie上层下发的 cookie,用于 TX 完成回调匹配
ucBssIdxBSS 索引,标识哪个虚拟接口

大帧阈值的计算方式值得一提:u2MgmtTxMaxLen 不是硬编码的 1600——它由 chip_info->cmd_max_pkt_size 减去 u2CmdTxHdrSize 动态计算得到。这两个值在芯片初始化时从固件 capability 中读取,不同芯片可能不同。USB 接口还需额外扣除 LEN_USB_UDMA_TX_TERMINATOR,因为 USB UDMA 传输在每个包的尾部需要 terminator 字段。

MTK 的亮点在大帧优化:当管理帧大小超过 MBOX 消息队列的承载上限时,MTK 不走 MBOX 通道,而是直接调用 _mtk_cfg80211_mgmt_tx_via_data_path()——绕过 MCU,走数据面的 kalHardStartXmit 直通硬件。

这是 MTK 独有的优化:数据通道的 DMA 能力远强于控制通道的 MBOX,大帧走数据面更快。(注:此优化由 CFG_SUPPORT_TX_MGMT_USE_DATAQ 编译选项控制,本代码库中该宏值为 0,整条路径默认未编译启用。)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
// MTK: os/linux/gl_cfg80211.c — _mtk_cfg80211_mgmt_tx_via_data_path()
// 大帧直通数据面:绕过 MCU MBOX,走 kalHardStartXmit
// 注:此函数位于 #if CFG_SUPPORT_TX_MGMT_USE_DATAQ 内,而该宏在本代码库为 0(config.h:2527),
// 整条大帧直通路径实际被编译剔除。以下代码精简自源码:
// kalPacketAlloc (gl_kal.c:1066) / GLUE_SET_PKT_FLAG (gl_os.h:1258 宏) / kalHardStartXmit (gl_kal.c:3692)
// 真实存在;但 GLUE_SET_PKT_COOKIE 与 ENUM_PKT_802_11_MGMT 全树无定义(仅死代码引用 gl_cfg80211.c:2764-2765),
// 即便启用该宏也无法编译——此路径在当前代码库中已废弃。
int _mtk_cfg80211_mgmt_tx_via_data_path(
struct GLUE_INFO *prGlueInfo, struct wireless_dev *wdev,
const u8 *buf, size_t len, u64 u8GlCookie)
{
struct sk_buff *prSkb = NULL;
uint8_t *pucRecvBuff = NULL;
uint8_t ucBssIndex;

// 1. 分配标准 Linux sk_buff(而非 MTK 私有的 MSDU_INFO)
prSkb = kalPacketAlloc(prGlueInfo, len, TRUE, &pucRecvBuff);
kalMemCopy(pucRecvBuff, buf, len);
skb_put(prSkb, len);

// 2. 伪装成数据帧:设置 netdev + 标记 ENUM_PKT_802_11_MGMT
prSkb->dev = wdev->netdev;
GLUE_SET_PKT_FLAG(prSkb, ENUM_PKT_802_11_MGMT); // 标记为管理帧

// 3. 嵌入 cookie 到 skb 私有区域(供 TX 完成时匹配)
GLUE_SET_PKT_COOKIE(prSkb, u8GlCookie);

// 4. 走数据面发送路径 kalHardStartXmit
ucBssIndex = wlanGetBssIdx(wdev->netdev);
return kalHardStartXmit(prSkb, wdev->netdev, prGlueInfo, ucBssIndex);
}

这个函数的核心思路是 "蒙混过关":把管理帧包装成 sk_buff(Linux 网络栈的标准数据包结构),挂到 wdev->netdev 上,打上 ENUM_PKT_802_11_MGMT 标记,然后直接调用 kalHardStartXmit()——这个函数原本是数据帧的发送入口。

固件在数据面接收路径上看到 ENUM_PKT_802_11_MGMT 标记后,就知道这不是普通数据帧,转而按管理帧处理。

MTK 的错误处理采用了经典的 do { ... } while (FALSE) 模式——所有资源分配后检查失败时 break,循环结束后统一清理。MTK 选择 do-while 而非 QCOM 的 goto 标签,是因为 MTK 的资源分配层级浅(最多 2 层),do-whilebreak 比多个 goto 标签更简洁:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// MTK: os/linux/gl_cfg80211.c — _mtk_cfg80211_mgmt_tx 错误处理
do {
if ((wiphy == NULL) || ...) break; // 参数校验
prMsgTxReq = cnmMemAlloc(...); // 分配消息结构体
if (prMsgTxReq == NULL) { ... break; } // 分配失败
if (prMsgTxReq->prMgmtMsduInfo == NULL) { // MSDU 分配失败
... break;
}
// ...正常路径...
i4Rslt = 0;
} while (FALSE);

// 清理:逆序释放(先 MSDU,后父结构体)
if ((i4Rslt != 0) && (prMsgTxReq != NULL)) {
if (prMsgTxReq->prMgmtMsduInfo != NULL)
cnmMgtPktFree(prGlueInfo->prAdapter, prMsgTxReq->prMgmtMsduInfo);
cnmMemFree(prGlueInfo->prAdapter, prMsgTxReq);
}

主要功能:

  • do-while(FALSE) 模式让所有错误路径汇聚到同一个出口,避免深层嵌套 if/else
  • 清理代码按分配逆序释放资源——先子后父,与 QCOM 的 goto 标签清理模式是两种常见的内核错误处理风格
  • 特例:大帧直通路径(_mtk_cfg80211_mgmt_tx_via_data_path)在分配 prMsgTxReq 之前就 return,直接绕过清理代码——资源尚未分配,不会泄漏

MBOX 投递之后,固件的处理与 TX 完成通知走这样一条链路:mboxSendMsg()MSG_MGMT_TX_REQUEST 消息投递到 MBOX_ID_0 后,消息通过硬件邮箱中断通知固件。

固件的 MBOX RX 中断处理程序根据消息头中的 eMsgId == MID_MNY_AIS_MGMT_TX 识别为管理帧发送请求,从 MSDU 中取出帧体,根据 rChannelInfo 指定的信道调度 TX。发送完成后,固件构造一条包含原始 u8Cookie 的 MBOX 响应消息回传给驱动。

驱动侧 MBOX RX 路径收到响应后,通过 cookie 匹配找到对应的原始请求上下文,触发 TX 完成处理。

值得注意的是,MTK 在帧缓冲区的尾部额外预留了 sizeof(uint64_t) 的空间存储 cookie(见 _mtk_cfg80211_mgmt_tx() 代码中的 *pu8GlCookie = *cookie),这意味着 cookie 同时存在于消息结构体(prMsgTxReq->u8Cookie)和帧数据尾部(pu8GlCookie)两个独立位置。

消息体中的 cookie 用于 MBOX 协议的请求 - 响应匹配,帧尾部的 cookie 用于底层 DMA 完成中断后的回调匹配,形成了双保险。

固件侧收到 MBOX 消息后的处理如下:hem_mbox.c 中的消息分发表将 MID_MNY_AIS_MGMT_TX 路由到 aisFsmRunEventMgmtFrameTx()mgmt/ais_fsm.c:8124)。该函数从 MSG_MGMT_TX_REQUEST 中取出 ucBssIdx 定位 AIS(Auth/Assoc Infrastructure Service)状态机实例,然后根据 fgIsOffChannel 分两路处理:

  • 同信道:直接调用 aisFuncTxMgmtFrame()ais_fsm.c:8274)。该函数先通过 prWlanHdr->aucAddr1 查找目标 STA record,若为 MLO 场景还做 MLD→Link MAC 地址转换;随后调用 nicTxConfigPktControlFlag() 配置强制发送标志,注册 TX 完成回调 aisFsmRunEventMgmtFrameTxDone()ais_fsm.c:8205),最后将帧推入硬件 TX 队列。值得注意的是,如果上一次 prMgmtTxMsdu 尚未传输完成,aisFuncTxMgmtFrame() 会先通过 kalIndicateMgmtTxStatus() 通知上层前一个帧发送失败,再替换为新的帧——这是 MTK 的 "无排队、丢旧换新" 策略,与 QCOM 的排队模型形成对比。

  • 离信道:调用 aisFunHandleOffchnlTxReq()ais_fsm.c:8036)将 TX 请求包装为 AIS_OFF_CHNL_TX_REQ_INFO 结构体,插入 rTxReqLink 链表尾部;随后通过 aisFsmRunEventRemainOnChannel() 触发 AIS 状态机进入 AIS_STATE_REQ_REMAIN_ON_CHANNEL,向固件 CNM 发出 MID_MNY_CNM_CH_REQ 信道请求。当固件 CNM 完成硬件 RF 切信道、发出 MSG_CH_GRANT 批准消息(aisFsmRunEventChGrant())后,状态机转入 AIS_STATE_OFF_CHNL_TX,由 aisState_OFF_CHNL_TX()rTxReqLink 链表头取出待发送请求,复用 aisFuncTxMgmtFrame() 完成实际发送——同信道和离信道最终汇入同一条 nicTxConfigPktControlFlag 下发路径,就像塔台的同信道窗口和货运通道,包裹最终都进同一个分拣口。

TX 完成时,硬件触发中断,aisFsmRunEventMgmtFrameTxDone() 被回调。它从帧缓冲区尾部取出 cookie(prMsduInfo->prPacket + u2FrameLength + MAC_TX_RESERVED_FIELD),通过 kalIndicateMgmtTxStatus() 将 TX 结果(成功 / 失败 + cookie)回传给驱动层,驱动侧的 MBOX RX 路径再通过 cookie 匹配通知上层。

与 QCOM 的对比:MTK 的单入口在固件侧同样保持了简洁——同信道和离信道汇入同一个 aisFuncTxMgmtFrame(),而 QCOM 的 Auth 和 Action 在固件侧分别走 PE 直发和 WMI 下发两条路径。MTK 的 "丢旧换新" 队列策略(不发排队,直接替换)也体现了其 "简洁优先" 的设计哲学——牺牲对多帧并发的支持,换取了零排队延迟和更简单的状态管理。


6 总结:双平台发送架构对比

维度QCOMMTK
TX 入口数量双路径(Auth 快速 / Action 标准)单入口(mtk_cfg80211_mgmt_tx
Auth 帧处理走 SME/PE 直达,绕过 ROC与其他帧同路径
大帧处理正常 WMI 下发> 阈值走数据通道直通(编译选项启用时)
ROC 管理Policy Manager 审批MBOX 直接下发
通信机制WMI(控制面)MBOX 消息队列
错误处理goto 标签清理do-while (FALSE) 统一出口
TX 完成通知WMI event + lim_tx_complete 回调链(下载完成 + OTA 确认双回调)MBOX response + cookie 匹配(消息体 cookie + 帧尾 cookie 双保险)

QCOM 的发送设计哲学:用复杂度换灵活性。Auth 快速通道让时延敏感的 Auth 帧跳过排队,Action 标准通道为需要信道管理的帧提供完整的 ROC 审批流程。双路径让不同类型的帧各得其所——就像大型机场的塔台,紧急航班有专用跑道,普通航班按部就班排队。

MTK 的发送设计哲学:用简洁性换可靠性。单入口让代码路径短、状态管理简单,大帧直通是点睛之笔——控制面的帧走数据面的 DMA 通道,反映了 MTK 对硬件能力务实的利用。

两套架构,同一个目标:确保每一条 "调度指令"(管理帧)都能准确、及时地送达固件,再由固件发射到空中。从 supplicant 这个飞行计划的制定者,到 nl80211 的内核关口,再到 QCOM/MTK 各自的塔台调度——这就是 WiFi 管理帧控制面发送的全貌。


下一章:管理帧从对端到达后,固件如何上报?QCOM 的两层分发和 MTK 的单层分派各有什么优劣?那些 RTS/CTS/ACK 帧为什么你永远看不到它们的代码?


协议依据:IEEE 802.11-2020 §9.3.3(管理帧格式)、§11.3(Auth/Assoc 流程)、§10.3(Action 帧 category 定义)。源码路径见各代码块注释。

本文源码来自 QCOM 的 qcacld-3.0qcacmn,MTK 的 gen4m,以及 w1.fi 提供的 wpa_supplicant。