管理帧的发送 — 控制面的帧处理
"数据帧是乘客,管理帧是塔台的调度指令——乘客只关心能不能到达,调度指令决定了飞机能不能起飞。"
¶ 本章导读
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、Action | supplicant 构建,经 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 | // wpa_supplicant/sme.c — sme_send_authentication() 算法选择段 |
主要功能:
- OPEN(默认):最简单的开放认证,两个空帧交换
- SAE(WPA3):Dragonfly 密钥交换,需要多轮 Commit/Confirm;仅当 BSS 的 RSN IE 声明 SAE AKM 且未被 DPP 覆盖、PMF 已启用时才激活;支持 PMKSA caching 加速重连
- FT(802.11r):快速 BSS 切换,前提是
prev_bssid_set且ft_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 | // wpa_supplicant/sme.c — sme_associate() FT IE 重排段 |
主要功能:
remove_ie从assoc_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_OPEN且key_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 + 扩展 EIDWLAN_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, ¶ms)将完整的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 | // MTK: os/linux/gl_cfg80211.c — mtk_cfg_deauth() 驱动就绪检查 |
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 | // wpa_supplicant/driver_i.h — 所有管理帧发送的统一收口 |
这个函数的设计体现了 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 | wpa_drv_send_mlme() [→ wpa_supplicant/driver_i.h] |
几个关键点:
wpa_driver_nl80211_send_mlme() 在最终下发前对 Auth 帧做了额外的合法性检查。以下是其中最核心的三段检查:
1 | // src/drivers/driver_nl80211.c — Auth 帧加密保护检查 |
nl80211_send_frame_cmd() 是用户态最后的包装函数,构造 NL80211_CMD_FRAME netlink 消息。它的 netlink 属性构建采用了 short-circuit 条件链——任一 nla_put_* 失败则跳 fail 标签:
1 | // src/drivers/driver_nl80211.c — netlink 属性打包 |
主要功能:
- 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 | // net/wireless/nl80211.c — nl80211_tx_mgmt() 内核侧入口 |
这是从通用层到厂商驱动层的 "最后一道关口"——三层校验确保只有合法的帧才能进入驱动:帧体非空、驱动支持、接口类型合法、离信道不冲突。通过这三层后,cfg80211_mlme_mgmt_tx() 调用驱动注册的 mgmt_tx 回调。
两个驱动的 mgmt_tx 回调注册位置分别是:QCOM 在 wlan_hdd_cfg80211_ops(core/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_ops(os/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_txAuth 快速通道服务于 P2P Auth、用户态 SME 等场景——这些场景下 Auth 帧以NL80211_CMD_FRAME的形式进入mgmt_tx回调,然后在 HDD 层被识别并分流到快速通道。
如果说 §3 描述的是将飞行计划通过专用网络(nl80211)递交到塔台的过程,那么从本节开始,我们走进 QCOM 这座塔台的调度室内部——看它如何用双通道处理不同紧急程度的调度指令。
QCOM 的管理帧发送有一个其他平台没有的架构亮点——双路径。
1 | // QCOM: core/hdd/src/wlan_hdd_p2p.c — __wlan_hdd_mgmt_tx() 双路径分流(源码约 130 行,保留核心分支逻辑) |
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 | /* ====== Layer 2: FT (Re)Assoc Response offload(SAP/GO 模式) ====== */ |
Layer 2 完成后,剩下的所有帧——Action、Deauth、Disassoc、Probe Resp、普通 Assoc Resp 等——全部落入 Layer 3 的 off_chan_tx 标签,进入标准通道。
1 | /* ====== Layer 3: Action 帧标准通道 ====== */ |
| 层级 | 触发条件 | 路径 | 设计意图 |
|---|---|---|---|
| Layer 1 | Auth 帧,非 PASN | sme_send_mgmt_tx 直达 PE | 时延敏感,跳过排队 |
| Layer 2 | FT (Re) Assoc Resp,SAP/GO 模式 | wlansap_update_ft_info 直通 hostapd | FT 信息需立即同步 |
| 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_tx→sme_send_mgmt_tx→ PE 层直发。Auth 帧跳过 P2P 组件、跳过 ROC 审批、跳过 Policy Manager,直接送达 PE 层。因为 Auth 帧对时延敏感(超时就断连),不能走标准的 "排队 + 审批" 流程。 - 路径 B — Action 帧标准通道:
__wlan_hdd_mgmt_tx→wlan_cfg80211_mgmt_tx(wlan_cfg80211_p2p.c)→ P2P 组件调度 → ROC(Remain On Channel)申请信道驻留 →wlan_mgmt_txrx_mgmt_frame_tx→wma_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 上发送。
注:图中展示 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 | Auth 帧快速通道完整调用链(13 步,全部验证通过) |
逐段解读:
Auth 帧进入 SME 层后(sme_send_mgmt_tx + sme_prepare_mgmt_tx),核心工作只有一件——把帧包成 sir_mgmt_msg 消息,通过 scheduler_post_message() 投递到 PE 的消息队列。注意这里用的是异步消息队列,不是直接函数调用,SME 投递完就返回,PE 在自己的上下文中处理。
1 | // core/sme/src/common/sme_api.c — sme_prepare_mgmt_tx() 消息打包 |
帧消息到达 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_frame | void * | 指向管理帧体的指针,由上层分配并拷贝 |
frm_len | uint16_t | 帧体长度(字节) |
vdev_id | uint8_t | 虚拟设备 ID,标识帧属于哪个 VAP |
chanfreq | uint16_t | 发送信道的中心频率(MHz),如 2412、5180 |
desc_id | uint16_t | 描述符 ID,用于 TX 完成时匹配回调 |
pdata | void * | 私有数据指针,传递给 TX 完成回调(如 lim_tx_complete) |
macaddr | uint8_t * | 目标 MAC 地址(DA),用于 peer 查找 |
tx_param | struct tx_send_params | 嵌套的发送参数,包含 retry_limit(4 位,重试次数上限)、mcs_mask(12 位,速率集掩码)、bw_mask(带宽)等 |
use_6mbps | uint8_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 | Action 帧标准通道调用链(9 步,全部验证通过) |
ROC 的核心逻辑在 p2p_process_mgmt_tx() 中,分三层判断。它先判断当前信道和目标信道是否一致——同信道直接调用 p2p_execute_tx_action_frame() 发帧,不需要 ROC。
需要切信道时,它查找已有的 ROC 上下文:如果已经有相同信道的 ROC 正在进行(ROC_STATE_REQUESTED 或 ROC_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_STARTED | Scan 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 | // MTK: os/linux/gl_cfg80211.c — _mtk_cfg80211_mgmt_tx() 核心路径 |
MSG_MGMT_TX_REQUEST 结构体成员一览:
| 字段 | 含义 |
|---|---|
rMsgHdr.eMsgId | 消息 ID = MID_MNY_AIS_MGMT_TX,固件据此识别消息类型 |
fgIsOffChannel | 是否需要 off-channel 发送 |
rChannelInfo | 目标信道信息(band + channel number) |
eChnlExt | 信道扩展模式(HT20/HT40/VHT80 等) |
u4Duration | off-channel 驻留时长(ms) |
fgNoneCckRate | 是否禁用 CCK 速率 |
fgIsWaitRsp | 是否等待对端响应帧 |
prMgmtMsduInfo | 指向 MSDU_INFO 的指针(含帧体 + cookie) |
u8Cookie | 上层下发的 cookie,用于 TX 完成回调匹配 |
ucBssIdx | BSS 索引,标识哪个虚拟接口 |
大帧阈值的计算方式值得一提: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 | // MTK: os/linux/gl_cfg80211.c — _mtk_cfg80211_mgmt_tx_via_data_path() |
这个函数的核心思路是 "蒙混过关":把管理帧包装成 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-while 的 break 比多个 goto 标签更简洁:
1 | // MTK: os/linux/gl_cfg80211.c — _mtk_cfg80211_mgmt_tx 错误处理 |
主要功能:
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 总结:双平台发送架构对比
| 维度 | QCOM | MTK |
|---|---|---|
| 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.0 与 qcacmn,MTK 的 gen4m,以及 w1.fi 提供的 wpa_supplicant。