管理帧的接收与分发 — 控制面的帧处理

"发送是塔台下指令,接收是塔台听回报——来机怎么分类、怎么处理、怎么上报,决定了调度系统的响应速度。"


本章导读

上一章(管理帧的发送 — 控制面的帧构建与下发)我们追踪了管理帧的发送路径:从 supplicant 构建 Auth/Assoc/Deauth 帧,经 nl80211 穿过内核边界,进入 QCOM 的双路径(Auth 快速通道 / Action 标准通道)和 MTK 的单入口 + 大帧直通优化,最终到达固件。

本章聚焦硬币的另一面——管理帧的接收与分发。对端发来的管理帧到达固件后,如何穿越 WMI/MBOX 上报到驱动?驱动如何按 category 分类——哪些本地消化、哪些上报 supplicant?QCOM 的两层分发和 MTK 的单层分派各有什么优劣?最后聊一类特殊的帧:RTS/CTS/ACK——为什么你永远看不到它们的代码?

如果说发送是塔台发出调度指令,那么接收就是塔台接收入港航班信息。QCOM 的接收系统像大型国际机场的行李转盘分拣 + 登机口验票两层——第一层行李转盘(mgmt_txrx 回调表)分流,第二层登机口验票(PE/LIM category switch)细查。MTK 则像区域机场的只有登机口验票(单层分派)——固件 ROM 先筛一轮,驱动认识的就地处理,不认识的一律上报。

本章覆盖四个主题:

  • Action 帧的接收入口:固件 WMI 事件 → wma_mgmt_rx_process()wlan_mgmt_txrx_rx_frame_handler() 的完整链路
  • Action 帧 20+ category 的分发逻辑:谁本地处理、谁上报 supplicant
  • QCOM vs MTK 的接收架构对比:两层分发 vs 单层分派,各自的取舍
  • 那些你永远看不到的帧:RTS/CTS/ACK/PS-Poll 为什么必须在硬件层处理

前情提要:如果你还没读上一篇(管理帧的发送 — 控制面的帧构建与下发),建议先了解管理帧三分类框架和 supplicant 侧的帧构建逻辑(尤其是 Auth/Assoc/Deauth 的 TX 路径),因为理解接收路径的前提是知道这些帧 "从哪里来"。如果你已经清楚 supplicant 如何通过 wpa_drv_send_mlme() 下发管理帧,可以直接从下面的 Action 帧接收入口开始。


1 Action 帧的接收入口:固件如何把帧送到分发器?

在深入 category 分发逻辑之前,必须搞清楚一个问题:Action 帧从对端发过来之后,在到达 lim_process_action_frame() 之前,经历了哪些环节?答案是两层转发——固件上报后的第一个处理函数不是 lim_process_action_frame()

QCOM 完整链路(4 步,全部验证通过):

1
2
3
4
5
6
7
8
9
10
11
12
对端 Action 帧 OTA 到达

▼ [固件] WMI 事件上报(WMI_MGMT_RX_EVENTID)——固件完成 FCS 校验,帧体 + 元数据打包为 WMI 事件

▼ wma_mgmt_rx_process() [core/wma/src/wma_mgmt.c:3863]
│ · 提取帧体 + 元数据,分配 nbuf,转交 mgmt_txrx_rx_handler()(逐函数展开见 §3.1

▼ wlan_mgmt_txrx_rx_frame_handler() [umac/cmn_services/mgmt_txrx/core/src/wlan_mgmt_txrx_main.c:1747]
│ · frm_type 计算 + 回调表分发(15 步完整解读见 §3.23.3

▼ lim_process_action_frame() [core/mac/src/pe/lim/lim_process_action_frame.c:1706]
· switch (action_hdr->category) → 本地处理 or 上报 supplicant(四阶段见 §2.2

关键点:lim_process_action_frame() 不是固件上报后的第一个函数——在它之前还有 wma_mgmt_rx_process()(WMI 事件提取)和 wlan_mgmt_txrx_rx_frame_handler()(mgmt_txrx 子系统 frm_type 计算 + 回调表分发)两层中转。如果 Action 帧的 frm_type 被计算为 MGMT_FRM_UNSPECIFIED(即 category 不被 mgmt_txrx 子系统识别),它会在 wlan_mgmt_txrx_rx_frame_handler 中被直接丢弃,根本不会到达 lim_process_action_frame()

MTK 完整链路

1
2
3
4
5
6
7
8
9
10
对端 Action 帧 OTA 到达

▼ [固件 ROM] nicRxProcessActionFrame() [include/nic/nic_rx.h — 仅声明,实现在 WiFi 固件 blob 中]
│ 固件 ROM 侧完成第一轮过滤后,按 category 分派到驱动侧 handler

▼ 驱动侧 category 分派(在固件返回后执行):
│ ├── P2P Actionp2pFuncValidateRxActionFrame() [mgmt/p2p_func.c]
│ ├── WNM ActionwnmWNMAction() [mgmt/wnm.c]
│ └── 其他/未识别 → aisFuncValidateRxActionFrame() [mgmt/ais_fsm.c]
│ └→ kalIndicateRxMgmtFrame() → cfg80211 → supplicant

MTK 的链路比 QCOM 短一截——因为没有 mgmt_txrx 子系统这层抽象,固件 ROM 直接按 category 分派,驱动不认识的就走 aisFuncValidateRxActionFrame 兜底上报。详细对比见下文 §2.4(MTK 分发链路)、§3.5(架构分析)和 §5(全景对比)。


2 Action 帧分类与分发逻辑:进港航班怎么分?

Action 帧是最复杂的管理帧类别——802.11 定义了 20+ 种 category,每种 category 下又有不同的 action field。驱动和 supplicant 各管各的,分发逻辑决定了哪些帧在本地消化、哪些上报。

2.1 哪些 Action 帧由驱动本地处理?

以下 category 的 Action 帧不会上报 supplicant,由驱动本地处理:

Category处理方式
Spectrum Mgmt0802.11h 测量 / TPC(QCOM 本地处理)
QoS / WMM1 / 17ADDTS/DELTS/QoS Map Configure(双平台本地)
Block Ack3ADDBA Request/Response(QCOM 软件处理;MTK 完全固件 offload)
HT7SM Power Save(QCOM 本地)
SA Query8直接回复或状态机处理(PMF 驱动层职责)
VHT21Operating Mode Notification / GID Management(QCOM 本地)

这些帧的共同点:它们改变的是驱动内部的链路状态(Block Ack 窗口、QoS 参数、信道测量),和 supplicant 的状态机无关。比如 Block Ack 协商——驱动知道要不要开聚合、开多大的窗口,不需要问 supplicant。

以上 category 数值依据 IEEE 802.11-2024 §9.4.1.11(Action field category 编码表),与 QCOM 代码中的 ACTION_CATEGORY_* 常量一一对应。

2.2 哪些 Action 帧必须上报 supplicant?

Category为什么上报
P2P Public Action4P2P 发现 / 协商逻辑在 supplicant,驱动只负责转发
TDLS12TDLS 直连管理在 supplicant
Vendor Specific127 / 126供应商私有扩展,可能包含 supplicant 关心的内容

除此之外,任何驱动不认识的 category 都会作为 fallback 上报 supplicant——这是安全兜底策略。

这段分发逻辑在代码中是一个巨大的 switch (action_hdr->category) 语句——802.11 标准定义了 20+ 种 category,驱动必须对每一个做明确裁决。为便于理解,以下将其拆为 4 个阶段逐段解读。

阶段 1:入关安检——长度与 PMF 双校验

帧在进入 category 分发之前,必须先通过两道安检。长度校验防止后续 switch 语句越界读取 action_hdr 字段;PMF 保护检查确保要求受保护的 category(SA Query、FT Action、Protected Public Action)的帧确实携带了有效的完整性保护,未受保护的帧直接丢弃。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// QCOM: core/mac/src/pe/lim/lim_process_action_frame.c — 阶段 1:入关安检
void lim_process_action_frame(struct mac_context *mac_ctx,
uint8_t *rx_pkt_info, struct pe_session *session)
{
uint8_t *body_ptr = WMA_GET_RX_MPDU_DATA(rx_pkt_info);
tpSirMacActionFrameHdr action_hdr = (tpSirMacActionFrameHdr) body_ptr;
tpSirMacMgmtHdr mac_hdr_11w = WMA_GET_RX_MAC_HEADER(rx_pkt_info);
uint32_t frame_len = WMA_GET_RX_PAYLOAD_LEN(rx_pkt_info);

// 第一关:长度校验——帧长不够 Action Frame Hdr 就直接丢
if (frame_len < sizeof(*action_hdr)) {
pe_debug("frame_len %d less than Action Frame Hdr size", frame_len);
return;
}

// 第二关:PMF 保护帧检查——RMF category 的帧必须受保护
if (wlan_mgmt_is_rmf_mgmt_action_frame(action_hdr->category) &&
lim_drop_unprotected_action_frame(mac_ctx, session,
mac_hdr_11w, action_hdr->category))
return;

这两关是帧进入 category 分发前的「安检」:长度校验防止后续 switch 语句越界读取 action_hdr 的字段;PMF 保护检查确保要求受保护的 category(如 SA Query、FT Action、Protected Public Action)的帧确实携带了有效的完整性保护——未受保护的帧直接丢弃,不给攻击者绕过保护机制的机会。

阶段 2:本地降落——驱动内部消化

通过安检后,switch 进入 category 分发。以下 category 在 PE 层本地处理,不需要惊动 supplicant:

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
// 第三关:category 分发 —— switch (action_hdr->category)
switch (action_hdr->category) {

/* ====== 本地处理:驱动层直接消化 ====== */

case ACTION_CATEGORY_SPECTRUM_MGMT: // 0 — 802.11h 测量/TPC
switch (action_hdr->actionID) {
case ACTION_SPCT_MSR_REQ: __lim_process_measurement_request_frame(...); break;
case ACTION_SPCT_TPC_REQ: __lim_process_tpc_request_frame(...); break;
}
break;

case ACTION_CATEGORY_QOS: // 1
if ((session->limQosEnabled) || (action_hdr->actionID == QOS_MAP_CONFIGURE)) {
switch (action_hdr->actionID) {
case QOS_ADD_TS_REQ: __lim_process_add_ts_req(...); break;
case QOS_ADD_TS_RSP: __lim_process_add_ts_rsp(...); break;
case QOS_DEL_TS_REQ: __lim_process_del_ts_req(...); break;
case QOS_MAP_CONFIGURE: __lim_process_qos_map_configure_frame(...); break;
}
}
break;

case ACTION_CATEGORY_BACK: // 3 — Block Ack(ADDBA/DELBA)
// ...addba/delba handler...
break;

case ACTION_CATEGORY_HT: // 7 — SM Power Save
if (LIM_IS_AP_ROLE(session) &&
action_hdr->actionID == SIR_MAC_SM_POWER_SAVE)
__lim_process_sm_power_save_update(...);
break;

case ACTION_CATEGORY_SA_QUERY: // 8 — PMF SA Query
switch (action_hdr->actionID) {
case SA_QUERY_REQUEST: __lim_process_sa_query_request_action_frame(...); break;
case SA_QUERY_RESPONSE: __lim_process_sa_query_response_action_frame(...); break;
}
break;

case ACTION_CATEGORY_WMM: // 17 — WMM ADDTS/DELTS(与 QoS 逻辑相同)
// ...同 QoS 的 ADD_TS_REQ/RSP/DEL_TS/MAP_CONFIGURE 二级分发...
break;

case ACTION_CATEGORY_VHT: // 21 — VHT OPMode / GID
if (session->vhtCapability) {
switch (action_hdr->actionID) {
case SIR_MAC_VHT_OPMODE_NOTIFICATION: __lim_process_operating_mode_action_frame(...); break;
case SIR_MAC_VHT_GID_NOTIFICATION: __lim_process_gid_management_action_frame(...); break;
}
}
break;

这些 category 的共同特征:它们改变的都是驱动内部的链路状态,与 supplicant 的状态机无关。Spectrum Mgmt 管理信道测量和 TPC 报告,QoS/WMM 管理流优先级和 ADDTS 协商,Block Ack 管理聚合窗口,HT/VHT 管理 MIMO 工作模式,SA Query 是 PMF 安全机制的驱动层闭环——所有这些处理都在 PE 层完成,帧不会上报到用户空间。

阶段 3:转国际航班——上报 supplicant

以下 category 驱动不自行处理,统一通过 lim_send_sme_mgmt_frame_ind() 上报,让 supplicant 决策:

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
53
54
55
56
57
58
59
60
61
62
63
/* ====== 上报 Supplicant:驱动透传,上层决策 ====== */

case ACTION_CATEGORY_PUBLIC: // 4 — Public Action(ECSA/GAS/DPP 三路分流)
// ECSA → lim_process_ext_channel_switch_action_frame 本地处理(改驱动信道状态,不报 supplicant)
// GAS Initial Request → wlan_son_anqp_frame 拦截 + 继续上报;GAS Resp/Comeback → 上报
// DPP(OUI 匹配 dpp_oui)→ 上报;其他 Vendor Specific Public Action → 丢弃
break;

case ACTION_CATEGORY_RRM: // 5 — Radio Measurement(rrmEnable 且 DHCP 完成后本地处理测量请求/邻居报告;AP 角色转发 Neighbor Req/Radio Meas Rpt)
// ...rrmEnable 时本地处理 Radio Meas Req/Link Meas Req/Neighbor Report,AP 角色转发 Neighbor Req/Radio Meas Rpt...
break;

case ACTION_CATEGORY_PROTECTED_DUAL_OF_PUBLIC_ACTION: // 9 — 受保护的 Public Action
// GAS Initial/Response/Comeback → lim_send_sme_mgmt_frame_ind 上报
break;

case ACTION_CATEGORY_WNM: // 10 — BTM/Notification/Timing Meas
switch (action_hdr->actionID) {
case WNM_BSS_TM_QUERY:
case WNM_BSS_TM_REQUEST:
case WNM_BSS_TM_RESPONSE:
if (cfg_p2p_is_roam_config_disabled(...) && /* P2P 会话活跃 */)
break; // P2P 场景下不处理 BTM
// fallthrough
case WNM_NOTIF_REQUEST:
case WNM_NOTIF_RESPONSE:
mac_hdr = WMA_GET_RX_MAC_HEADER(rx_pkt_info);
rssi = WMA_GET_RX_RSSI_NORMALIZED(rx_pkt_info);
lim_send_sme_mgmt_frame_ind(mac_ctx, mac_hdr->fc.subType,
(uint8_t *) mac_hdr,
frame_len + sizeof(tSirMacMgmtHdr),
session->vdev_id,
WMA_GET_RX_FREQ(rx_pkt_info), rssi, RXMGMT_FLAG_NONE);
break;
}
break;

case ACTION_CATEGORY_RVS: // 19
case ACTION_CATEGORY_FST: // 18 — FST Session Transfer
mac_hdr = WMA_GET_RX_MAC_HEADER(rx_pkt_info);
lim_send_sme_mgmt_frame_ind(mac_ctx, mac_hdr->fc.subType,
(uint8_t *)mac_hdr,
frame_len + sizeof(tSirMacMgmtHdr),
session->vdev_id,
WMA_GET_RX_FREQ(rx_pkt_info),
WMA_GET_RX_RSSI_NORMALIZED(rx_pkt_info),
RXMGMT_FLAG_NONE);
break;

case SIR_MAC_ACTION_VENDOR_SPECIFIC_CATEGORY: // 127
case SIR_MAC_PROT_ACTION_VENDOR_SPECIFIC_CATEGORY: // 126
mac_hdr = WMA_GET_RX_MAC_HEADER(rx_pkt_info);
if (!qdf_mem_cmp(session->self_mac_addr, &mac_hdr->da[0],
sizeof(tSirMacAddr))) {
lim_send_sme_mgmt_frame_ind(mac_ctx, mac_hdr->fc.subType,
(uint8_t *)mac_hdr,
frame_len + sizeof(tSirMacMgmtHdr),
session->vdev_id,
WMA_GET_RX_FREQ(rx_pkt_info),
WMA_GET_RX_RSSI_NORMALIZED(rx_pkt_info),
RXMGMT_FLAG_NONE);
}
break;

上报策略的核心是「驱动不认识的不瞎处理」——驱动只透传,决策留给上层。

WNM(BTM/Notification)交给 supplicant 决策,驱动不预判是否切换。唯一的例外是 BTM 被「扣下」:当 STA 角色收到 BTM Query/Request/Response、且此刻存在活跃的 P2P client 或 P2P GO 连接时,cfg_p2p_is_roam_config_disabled() 判定为真,switch 直接 break 丢弃,不进上报分支。因为 BTM Request 是漫游触发器——它要求 STA 评估切走;而并发模式下切走 infra 连接会连带打断 P2P 链路(信道跳变 + 关联重建),驱动宁可暂时扣下 BTM,等 P2P 会话结束再放行给 supplicant。

Vendor Specific 需 DA 匹配确认后才上报——只有发给本机的供应商帧才上报,防止广播帧泛滥。

Public Action(category 4)是三个上报 category 里最值得拆的,因为它一个 category 内部就有三路分流,并非「全量透传」。第一路 ECSA(Extended Channel Switch Announcement)走 lim_process_ext_channel_switch_action_frame() 本地处理——信道切换声明改变的是驱动自己的 MAC 信道状态,函数内部还要拿 wlan_reg_chan_opclass_to_freq() 换算目标信道、校验 regulatory/DFS 合法性与并发会话冲突,这一切都在驱动层闭环,上报 supplicant 反而多余。

第二路 GAS(Initial Request/Response、Comeback Request/Response)是 ANQP 查询的载体:GAS Initial Request 先被 SON 模块 wlan_son_anqp_frame() 拦一份做采集,再继续上报——HS2.0/Passpoint 的 ANQP 状态机与凭据匹配整段逻辑住在 supplicant,驱动只搭桥不决策。

第三路 DPP(Wi-Fi Easy Connect 扫码配网)借道 Vendor Specific Public Action:驱动拿 OUI 与 dpp_oui({0x50, 0x6F, 0x9A, 0x1A})比对,匹配就上报,不匹配直接丢。真正「全量透传」的只有 RVS/FST。

这些 category 统一走 lim_send_sme_mgmt_frame_ind() → SME → HDD → cfg80211_rx_mgmt() → supplicant 上报通道。

阶段 4:拒绝入境——静默丢弃

1
2
3
4
5
6
    default:
// 未识别 category → 仅打日志,静默丢弃,不触发上报
pe_warn_rl("Action category: %d not handled", action_hdr->category);
break;
}
}

default 分支是安全兜底:驱动不认识的新 category 不应该猜测如何处理,沉默丢弃比错误处理更安全。注意这里与 MTK 的策略相反——MTK 对未识别帧选择全量上报 supplicant(见 §2.4)。

四阶段小结:入关安检(长度 + PMF)→ 本地降落(QoS/Spectrum/WMM/HT/VHT/SA Query/Block Ack,PE 层闭环)→ 转国际航班(WNM/Public/Vendor/RVS/FST,lim_send_sme_mgmt_frame_ind 上报)→ 拒绝入境(default 静默丢弃)。部分 category 内部还有二级 switch (action_hdr->actionID) 按具体 action 字段细分,如 QoS 下的 ADDTS Req/Rsp/DEL TS/Map Configure。

2.3 哪些帧走了特殊分发路径?

FT Auth(802.11r 认证帧):QCOM 对 FT 有特殊处理——FT Auth 帧(认证帧,Auth alg 字段标记 FT)在 Auth 帧层面就被拦截了(lim_process_ft_auth_frame()lim_process_auth_frame.c),不走 Action 帧的分发表。注意它与 category 6 的 FT Action 帧(重关联阶段的 FT Request/Response)是两回事。

原因在于 FT(Fast BSS Transition,802.11r)的认证和重关联高度耦合:FT 协议将 4-Way Handshake 的密钥派生前置到 Auth 阶段完成。Auth 帧中携带 R0KH-ID、R1KH-ID、PMK-R0 Name 等 FT 信息元素,PE 层必须在 Auth 帧交换时就完成 FT 序列号校验和 PMK-R0/R1 密钥层次派生。

如果 FT 的密钥派生被延后到常规 Action 帧分发路径,FT 重关联会因缺少 Auth 阶段建立的密钥上下文而失败。因此 QCOM 在 lim_process_auth_frame() 中直接检测 FT Auth 帧并调用 lim_process_ft_auth_frame(),将整个 FT 流程在 PE 层闭环。

FT Action(category 6):与 FT Auth 走的是完全不同的路径,而且双平台对它的态度截然相反。category 6 的 FT Request/Response/Confirm/Ack(over-the-DS FT 的资源请求与确认)在 QCOM 的 mgmt_txrx 层就被 mgmt_get_ft_action_subtype()wlan_mgmt_txrx_main.c:630)映射为 4 个 frm_type 槽位(MGMT_ACTION_FT_REQUEST/RESPONSE/CONFIRM/ACKwlan_mgmt_txrx_utils_api.h:766),但 lim_process_action_frame() 的 category switch 里没有 category 6 的 case——它到不了 PE/LIM 的第二层分发;这 4 个槽位也没有任何组件向 mgmt_rx_comp_cb 注册回调,因此 FT Action 帧在 QCOM 驱动 RX 路径上实际是「映射了但无人消费」:QCOM 的 802.11r FT 走 Auth 帧路径(lim_process_ft_auth_frame)加 Reassoc 帧,over-the-DS 的 FT Action 通道在驱动层是空缺的,FT Action 的最终消费者其实落到了 supplicant 侧的 ft_rx_action()(见 §3.4)。MTK 恰好相反,有一个专门的 roaming_fsm.c 消费 FT Action(CATEGORY_FT_ACTION=6include/nic/mac.h:916):TX 侧 roamingFsmSendFtActionFrame()mgmt/roaming_fsm.c:151)构造 FT Request,RX 侧 roamingFsmRunEventRxFtAction()mgmt/roaming_fsm.c:421)校验 FT Response 并在 ROAMING_STATE_WAIT_FT_RESPONSE 状态推进漫游 FSM。一个「映射后丢弃」、一个「专门 FSM 消费」,FT Action 是双平台分歧最大的一个 category。

WNM(category 10):双平台策略相反。QCOM 上报 supplicant(让上层决定是否处理 BTM Request);MTK 本地处理(wnmWNMAction()mgmt/wnm.c),在驱动层直接应答。

进港航班(收到的 Action 帧)分三类处理:本地降落的(驱动本地处理,不需要出关)、转国际航班的(上报 supplicant,需要上层决策)、VIP 专机(FT Auth 走特殊通道,不经过常规航班分拣)。

2.4 MTK 如何分发收到的 Action 帧?

与 QCOM 的两层分发(mgmt_txrx 回调表 + PE/LIM category switch)不同,MTK 的 Action 帧处理采用单层 category 分派——固件 ROM 完成第一轮过滤后,驱动侧直接按 category 调用对应 handler。

入口是 nicRxProcessActionFrame()include/nic/nic_rx.h),注意这个函数只有声明、没有驱动侧实现体——它是固件 ROM 中的 blob 函数,在固件侧完成 WLAN Action Frame Header 的解析和第一轮 category 筛分。驱动侧能看到的是固件返回后的 category 分派结果,由以下几个 handler 接管:

P2P Action → p2pFuncValidateRxActionFrame()mgmt/p2p_func.c,签名:void p2pFuncValidateRxActionFrame(struct ADAPTER *prAdapter, struct SW_RFB *prSwRfb, u_int8_t fgIsDevInterface, uint8_t ucRoleIdx)): 处理所有 P2P Public Action 帧(category 4),包括 GO Negotiation、Provision Discovery、Invitation 等。

其中 fgIsDevInterface 标记是否为 P2P Device 接口(用于判断是否走 Device FSM 而非 Role FSM),ucRoleIdx 是 P2P Role 索引(用于定位对应的 P2P Role FSM 状态机)。

函数内部再做 P2P 子类型的二级分发——根据 action_hdr->ucCategoryucAction → OUI/Type 判断是 P2P 还是 DPP,然后调用对应的 P2P FSM 状态机处理函数。与 QCOM 的最大差异:MTK 的 P2P Action 帧不走 mgmt_txrx 回调表,直接从固件 ROM 分派到 p2pFuncValidateRxActionFrame,少了一层抽象。

WNM Action → wnmWNMAction()mgmt/wnm.c,约 45 行):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// MTK: mgmt/wnm.c — wnmWNMAction() category 10 分发
void wnmWNMAction(struct ADAPTER *prAdapter, struct SW_RFB *prSwRfb)
{
struct WLAN_ACTION_FRAME *prRxFrame;
prRxFrame = (struct WLAN_ACTION_FRAME *)prSwRfb->pvHeader;

switch (prRxFrame->ucAction) {
case ACTION_WNM_TIMING_MEASUREMENT_REQUEST:
wnmTimingMeasRequest(prAdapter, prSwRfb); // 本地 Time Measurement 处理
break;
case ACTION_WNM_BSS_TRANSITION_MANAGEMENT_RSP:
wnmMulAPAgentRecvBTMResponse(prAdapter, prSwRfb); // AP 模式 BTM Response
break;
case ACTION_WNM_BSS_TRANSITION_MANAGEMENT_REQ:
wnmRecvBTMRequest(prAdapter, prSwRfb); // BTM offload 本地处理
// 若 CFG_SUPPORT_802_11V_BTM_OFFLOAD 未开启 → fallback 到 aisFuncValidateRxActionFrame 上报
break;
case ACTION_WNM_NOTIFICATION_REQUEST:
default:
// 其他 WNM action → 走 AIS FSM 兜底上报
aisFuncValidateRxActionFrame(prAdapter, prSwRfb);
break;
}
}

与 QCOM 的 WNM 策略对比:QCOM 的 WNM Action 帧全量上报 supplicantlim_send_sme_mgmt_frame_ind),让上层决定是否处理 BTM Request;MTK 默认驱动层本地处理 BTMwnmRecvBTMRequest,需开启 CFG_SUPPORT_802_11V_BTM_OFFLOAD 编译选项,否则同样走 aisFuncValidateRxActionFrame 上报),只有驱动不认识的 WNM sub-action 才上报。这个差异反映了两种设计哲学:QCOM 倾向 "上层决策",MTK 倾向 "驱动自治"。

兜底:aisFuncValidateRxActionFrame()mgmt/ais_fsm.c)——所有不命中 P2P 和 WNM 的 Action 帧最终落在这里处理。这个函数本身只有约 17 行有效代码,它不做分发,而是做最后一公里上报:校验 BSS index 是否为 AIS 网络类型后,通过 kalIndicateRxMgmtFrame() 将帧体原样透传给 cfg80211。注意该函数只接收 2 个参数——prGlueInfoprAdapter->prGlueInfo 获取,ucBssIndexprSwRfb->prStaRec->ucBssIndex 提取。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// MTK: mgmt/ais_fsm.c:8394 — 分发链末端:未识别帧上报 wpa_supplicant
void aisFuncValidateRxActionFrame(struct ADAPTER *prAdapter,
struct SW_RFB *prSwRfb)
{
uint8_t ucBssIndex = 0;

if (prSwRfb->prStaRec)
ucBssIndex = prSwRfb->prStaRec->ucBssIndex;

if (!IS_BSS_INDEX_AIS(prAdapter, ucBssIndex)) {
DBGLOG(AIS, LOUD,
"Use default, invalid index = %d\n", ucBssIndex);
return;
}

kalIndicateRxMgmtFrame(prAdapter, prAdapter->prGlueInfo,
prSwRfb, ucBssIndex);
}

kalIndicateRxMgmtFrame()os/linux/gl_kal.c:7736)签名:void kalIndicateRxMgmtFrame(struct ADAPTER *prAdapter, struct GLUE_INFO *prGlueInfo, struct SW_RFB *prSwRfb, uint8_t ucBssIndex)。内部通过 wlanGetNetDev(prGlueInfo, ucBssIndex) 获取 net_device,然后调用 cfg80211_rx_mgmt() 完成上报。

QCOM vs MTK Action 帧处理对比

维度QCOMMTK
分发层数两层(mgmt_txrx frm_type + PE/LIM category)单层(固件 ROM → category handler)
分发入口wlan_mgmt_txrx_rx_frame_handler → 回调表 → lim_process_action_framenicRxProcessActionFrame(ROM blob)→ category 分派
WNM 策略全量上报 supplicant驱动本地处理 BTM,未识别 sub-action 才上报
P2P 路径mgmt_txrx 回调表 → P2P 专属 handler固件 ROM → p2pFuncValidateRxActionFrame 直连
未识别帧PE 层 default 静默丢弃AIS FSM aisFuncValidateRxActionFrame 兜底上报
代码规模lim_process_action_frame.c ~500 行分散在 3 个文件,各 < 50 行

设计哲学差异:QCOM 的 lim_process_action_frame 是一个 "全能分发器"——500 行 switch 覆盖 20+ category,本地处理和上报在同一个函数内裁决。MTK 则把分发逻辑 "打散" 到不同文件的 handler 函数中——P2P 一个文件、WNM 一个文件、兜底上报一个文件。这种设计的优点是每个 handler 职责单一、好维护;缺点是新增 category 需要同时修改固件 ROM 的分派表和驱动侧的 handler 注册。


3 管理帧接收后怎么分发?QCOM 和 MTK 谁更复杂?

管理帧的接收链路比发送链路多一层 "中转"——固件不是直接把帧丢给驱动的,而是通过 WMI 事件上报,驱动再从事件中提取帧体和元数据。理解这层中转,是理解整个 RX 路径的关键。

3.1 QCOM 的接收链路经过哪些关键函数?

QCOM 的接收链路从固件上报到 PE 层分发,总共经过 3 个关键函数:

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
固件 OTA 接收管理帧
│ FCS 校验(CRC 失败的帧在固件层丢弃)

WMI 事件上报(WMI_MGMT_RX_EVENTID)
│ 固件将帧体 + 元数据(RSSI/信道/时间戳/PN params/状态标志)打包为 WMI 事件

wma_mgmt_rx_process() [core/wma/src/wma_mgmt.c:3863] ← 固件上报后的【第一个】处理函数
│ static 函数,由 WMI 事件处理器回调

├─► wmi_extract_mgmt_rx_params() 从 WMI 事件中解析出 mgmt_rx_event_params
│ · buf_len / bufp(帧体指针) + 信道频率 / RSSI / tsf_delta / pn_params
│ · status 标志(CRC/MIC/PN/DECRYPT 错误标志位)

├─► 校验 buf_len <= data_len(防越界)

├─► 信道号→频率转换(兼容老固件的 channel number 模式)
│ wlan_reg_legacy_chan_to_freq()

├─► qdf_nbuf_alloc(roundup(buf_len + 100, 4)) ← 多分配 100 字节
│ 预留原因:1) RSN IE 补全(部分 AP 的 IE 长度缺 2 字节)
2) PMF 帧 CCMP 头裁剪时避免 OOB

├─► wma_mem_endianness_based_copy() 将帧体拷贝到 nbuf(处理大小端差异)

└─► mgmt_txrx_rx_handler(psoc, wbuf, mgmt_rx_params) ← 实际调用 wlan_mgmt_txrx_rx_frame_handler

▼ [mgmt_txrx 子系统]
wlan_mgmt_txrx_rx_frame_handler() [umac/cmn_services/mgmt_txrx/core/src/wlan_mgmt_txrx_main.c:1747]
│ 管理帧接收的【中央调度器】

├─► 帧类型校验(必须是 MGMT 或 CTRL 帧)
├─► 地址有效性检查(from_addr / bssid 至少一个合法)
├─► Beacon/Probe Resp 地址修复(from_addr 或 bssid 不全时互填)
├─► HT Control 字段偏移计算(Order bit 检测)
├─► 受保护帧 IV/CCMP Header 偏移计算(WEP bit + EXT IV 检测)
├─► mgmt_txrx_get_frm_type(mgmt_subtype, mpdu_data_ptr) ← 见下文详解
├─► 重复帧检测(mgmt_txrx_frame_is_duplicate)
├─► WMI RX error 状态检查(PN/CRC/DECRYPT/MIC 错误日志)
├─► 查 mgmt_rx_comp_cb[frm_type] 回调表 + MGMT_FRAME_TYPE_ALL 通配 handler
└─► 遍历 handler 链表,clone buffer → rx_cb() 逐个分发

关键澄清wlan_mgmt_txrx_rx_frame_handler 不是固件上报后的第一个函数——wma_mgmt_rx_process() 才是。wma_mgmt_rx_process() 是静态函数,由 WMI 事件处理器通过 wmi_unified_register_event_handler(WMI_MGMT_RX_EVENTID, wma_mgmt_rx_process) 注册。它的职责是 "翻译"——把 WMI 事件格式的帧数据转换为 mgmt_txrx 子系统能理解的 qdf_nbuf_t + mgmt_rx_event_params 格式。

3.2 如何根据帧类型查回调表?

wlan_mgmt_txrx_rx_frame_handler 中最关键的步骤是计算 frm_type——它决定了帧将被哪个 handler 处理。mgmt_txrx_get_frm_type() 的实现(精简版:省略 frm_type 局部变量和 ATIM subtype,保留核心 subtype→frm_type 映射):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// QCOM: umac/cmn_services/mgmt_txrx/core/src/wlan_mgmt_txrx_main.c — mgmt_txrx_get_frm_type()
static enum mgmt_frame_type
mgmt_txrx_get_frm_type(uint8_t mgmt_subtype, uint8_t *mpdu_data_ptr)
{
switch (mgmt_subtype) {
case MGMT_SUBTYPE_ASSOC_REQ: return MGMT_ASSOC_REQ;
case MGMT_SUBTYPE_ASSOC_RESP: return MGMT_ASSOC_RESP;
case MGMT_SUBTYPE_REASSOC_REQ: return MGMT_ASSOC_REQ; // Reassoc 复用 Assoc 的 frm_type
case MGMT_SUBTYPE_REASSOC_RESP: return MGMT_REASSOC_RESP;
case MGMT_SUBTYPE_PROBE_REQ: return MGMT_PROBE_REQ;
case MGMT_SUBTYPE_PROBE_RESP: return MGMT_PROBE_RESP;
case MGMT_SUBTYPE_BEACON: return MGMT_BEACON;
case MGMT_SUBTYPE_DISASSOC: return MGMT_DISASSOC;
case MGMT_SUBTYPE_AUTH: return MGMT_AUTH;
case MGMT_SUBTYPE_DEAUTH: return MGMT_DEAUTH;

case MGMT_SUBTYPE_ACTION:
case MGMT_SUBTYPE_ACTION_NO_ACK:
// 关键:Action 帧需要二级查表——按 category → action 映射到更细粒度的 frm_type
return mgmt_txrx_get_action_frm_subtype(mpdu_data_ptr);

default: return MGMT_FRM_UNSPECIFIED;
}
}

对于 Action 帧,mgmt_txrx_get_action_frm_subtype() 做二级映射(22 个 category,各有专属 helper):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 二级映射:category → frm_type(关键 category 示例)
switch (action_hdr->action_category) {
case ACTION_CATEGORY_SPECTRUM_MGMT: → mgmt_get_spec_mgmt_action_subtype(action_code)
case ACTION_CATEGORY_QOS: → mgmt_get_qos_action_subtype(action_code)
case ACTION_CATEGORY_BACK: → mgmt_get_back_action_subtype(action_code) // Block Ack
case ACTION_CATEGORY_PUBLIC: → mgmt_get_public_action_subtype(action_code) // P2P
case ACTION_CATEGORY_SA_QUERY: → mgmt_get_sa_query_action_subtype(action_code)
case ACTION_CATEGORY_WNM: → mgmt_get_wnm_action_subtype(action_code)
case ACTION_CATEGORY_TDLS: → mgmt_get_tdls_action_subtype(action_code)
case ACTION_CATEGORY_VENDOR_SPECIFIC: → MGMT_ACTION_CATEGORY_VENDOR_SPECIFIC
case ACTION_CATEGORY_VENDOR_SPECIFIC_PROTECTED: → MGMT_ACTION_CATEGORY_VENDOR_SPECIFIC_PROTECTED
// ...省略 DLS/HT/VHT/MESH/SELF_PROTECTED/WMM/FST/RVS/USIG/PROTECTED_EHT ...
default: → MGMT_FRM_UNSPECIFIED // 未识别 → 后续被丢弃
}

frm_type 的两个关键设计

  1. Action 帧的二级映射:非 Action 帧直接从 subtype 得到 frm_type(O (1)),Action 帧需要先解析 category 字节再做二级 switch——这意味着整个 Action 帧的 category 分发在回调表层面就开始了
  2. MGMT_FRM_UNSPECIFIED 的后果:如果 frm_type 为 UNSPECIFIED,wlan_mgmt_txrx_rx_frame_handler 直接 qdf_nbuf_free(buf) 并返回 QDF_STATUS_E_FAILURE——帧永远不会到达 PE/LIM 层

上面的「省略」其实藏着两层设计取舍,值得补全。mgmt_txrx_get_action_frm_subtype()wlan_mgmt_txrx_main.c:1235)的完整枚举是 22 个 case + 1 个 default,按「是否走二级 helper」分成三类:

  • 20 个 category 走二级 helper:Spectrum Mgmt、FT(ACTION_FAST_BSS_TRNST)、QoS、DLS、Block Ack、Public、RRM、HT、SA Query、Protected Dual、WNM、TDLS、Mesh、Self Protected、WMM、VHT、FST、RVS、USIG、Protected EHT——各自调 mgmt_get_*_action_subtype(action_code),把 category 映射到更细的 frm_type。这意味着第一层回调表的「谁想要这帧」其实在 action_code 粒度就回答了:比如 SA Query Request 和 SA Query Response 是两个不同的 frm_type 槽位,PE 层还没出场,帧的去向已经分好了
  • 2 个 category 直接落常量:Vendor Specific(127)和 Vendor Specific Protected(126)不经过 helper,直接映射 MGMT_ACTION_CATEGORY_VENDOR_SPECIFIC(_PROTECTED)。原因是它们的进一步区分靠 OUI 字节 + 业务逻辑(DPP?P2P?厂商私有?),mgmt_txrx 层拿不到也不该拿这个决策权,所以只给一个粗粒度槽位,细活留给 PE/supplicant
  • USIG 复用 TWT 的 helperACTION_CATEGORY_USIG(802.11az)调的是 mgmt_get_twt_action_subtype()——USIG 的 action code 与 TWT 共用一套编号,QCOM 选择复用而非另写一个几乎相同的 helper,少一份维护面

这个「20 走 helper + 2 落常量 + 1 复用」的分布,正好是 §3.3 说的「订阅与裁决分离」在第二级映射上的投影:能静态区分到 action_code 的,第一层就分掉;区分不了需要看 OUI 的,留一个粗槽位给第二层裁决。default 分支丢给 MGMT_FRM_UNSPECIFIED 则兜住了未来标准新增 category 的「前向兼容」——新 category 出现时,老驱动不认识,直接丢弃而不是误判给某个错误 handler。一个例外值得单独记下:FT(category 6)虽然走了 mgmt_get_ft_action_subtype() 这个 helper,映射出的 4 个槽位(MGMT_ACTION_FT_REQUEST/RESPONSE/CONFIRM/ACK)却没有组件注册回调、PE 层也没有 category 6 case,FT Action 帧因此在 QCOM 驱动 RX 路径上实际无人消费(MTK 由 roaming_fsm 消费,双平台对比见 §2.3)。

Beacon/Probe Req/Probe Resp 的去向:以上 subtype→frm_type 映射中,Beacon/Probe Req/Probe Resp 的 frm_type 计算到这里就结束了——它们不会进入 lim_process_action_frame(),而是由 mgmt_txrx 回调表中的 Scan/MLME 组件通过注册 MGMT_BEACON/MGMT_PROBE_REQ/MGMT_PROBE_RESP 回调来消费。具体来说:Beacon 和 Probe Resp 进入 Scan 模块做 BSS 列表维护和信号强度跟踪,Probe Req 在 AP 模式下触发 Probe Resp 的构建与发送(与上一章 §2.1 的发送路径首尾衔接)。这条 Scan/MLME 消费链与 Action 帧的 PE/LIM category 分发是两条并行不悖的路径,具体处理细节留到后续章节展开。

3.3 两层分发如何处理同一个帧的多个消费者?

QCOM 的管理帧接收采用两层分发架构,这是理解 QCOM RX 路径的关键:

第一层 — mgmt_txrx 回调表

15 步分段小结(阅读前先建立结构预期):

阶段步骤核心操作失败后果
校验1-4buf/psoc 非空、帧类型、地址有效性丢弃帧
预处理5-8地址修复、指针偏移、加密帧处理帧体指针可能错误
分类9-10frm_type 计算、重复帧检测丢弃帧(UNSPECIFIED / 重复)
分发11-15查回调表、查 peer、clone buffer 并发N/A

下面按四个代码块逐步展开。先看校验阶段(步骤 1-4)——buf/psoc 非空、帧类型、地址有效性三道检查,任何一关不过就直接 qdf_nbuf_free(buf) 丢弃,后面阶段根本没有机会执行。

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
// QCOM: umac/cmn_services/mgmt_txrx/core/src/wlan_mgmt_txrx_main.c — 保留核心逻辑
QDF_STATUS wlan_mgmt_txrx_rx_frame_handler(
struct wlan_objmgr_psoc *psoc,
qdf_nbuf_t buf,
struct mgmt_rx_event_params *mgmt_rx_params)
{
struct ieee80211_frame *wh;
qdf_nbuf_t copy_buf;
struct wlan_objmgr_peer *peer = NULL;
uint8_t mgmt_type, mgmt_subtype;
uint8_t *mac_addr, *mpdu_data_ptr;
enum mgmt_frame_type frm_type;
struct mgmt_rx_handler *rx_handler;

// 1. 基础校验:buf/psoc 非空
if (!buf || !psoc) { ... return error; }

// 2. 提取帧头和长度
data = (uint8_t *)qdf_nbuf_data(buf);
wh = (struct ieee80211_frame *)data;
buflen = qdf_nbuf_len(buf);

// 3. 帧类型校验:只处理管理帧和控制帧
mgmt_type = (wh)->i_fc[0] & IEEE80211_FC0_TYPE_MASK;
mgmt_subtype = (wh)->i_fc[0] & IEEE80211_FC0_SUBTYPE_MASK;
if (mgmt_type != IEEE80211_FC0_TYPE_MGT &&
mgmt_type != IEEE80211_FC0_TYPE_CTL) {
qdf_nbuf_free(buf);
return QDF_STATUS_E_FAILURE;
}

// 4. 地址有效性:from_addr(addr2) 和 bssid(addr3) 至少一个合法
is_from_addr_valid = mgmt_rx_is_bssid_valid(wh->i_addr2);
is_bssid_valid = mgmt_rx_is_bssid_valid(wh->i_addr3);
if (!is_from_addr_valid && !is_bssid_valid) {
qdf_nbuf_free(buf);
return QDF_STATUS_E_FAILURE;
}

校验通过后,进入预处理阶段。前 4 步的校验确保了帧头和地址的合法性,接下来 4 步负责修正地址和计算帧体偏移——无论帧是 Beacon 还是加密的 Action 帧,都要保证 mpdu_data_ptr 指向帧体的正确起始位置。

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
/* ====== Steps 5-8: 地址修复、指针偏移、加密帧处理 ====== */

// 5. Beacon/Probe Resp 地址修复:from_addr 和 bssid 之一不合法时相互填充
if (mgmt_type == IEEE80211_FC0_TYPE_MGT &&
(mgmt_subtype == MGMT_SUBTYPE_BEACON ||
mgmt_subtype == MGMT_SUBTYPE_PROBE_RESP) &&
!(is_from_addr_valid && is_bssid_valid)) {
if (!is_from_addr_valid)
qdf_mem_copy(wh->i_addr2, wh->i_addr3, QDF_MAC_ADDR_SIZE);
else
qdf_mem_copy(wh->i_addr3, wh->i_addr2, QDF_MAC_ADDR_SIZE);
}

// 6. mpdu_data_ptr → 指向帧体起始(跳过 MAC Header)
mpdu_data_ptr = (uint8_t *)qdf_nbuf_data(buf) + sizeof(struct ieee80211_frame);

// 7. HT Control 字段偏移(Order bit 置位时 +4 字节)
if (wh->i_fc[1] & IEEE80211_FC1_ORDER)
mpdu_data_ptr += IEEE80211_HT_CTRL_LEN;

// 8. 受保护帧(WEP bit 置位且非组播/广播):IV/CCMP Header 偏移
if ((wh->i_fc[1] & IEEE80211_FC1_WEP) &&
!qdf_is_macaddr_group(wh->i_addr1) &&
!qdf_is_macaddr_broadcast(wh->i_addr1)) {
if (ivp && (ivp[WLAN_HDR_IV_LEN] & WLAN_HDR_EXT_IV_BIT))
mpdu_data_ptr += IEEE80211_CCMP_HEADERLEN; // CCMP (8 bytes IV)
else
mpdu_data_ptr += WLAN_HDR_EXT_IV_LEN; // WEP (4 bytes IV)
}

地址修复、指针偏移和加密帧头偏移完成后,mpdu_data_ptr 已经指向帧体(管理帧的 Frame Body 字段)的正确起始位置。接下来两步是关键决策点——根据帧体的内容计算出 frm_type,并检查是否为重复帧。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
/* ====== Steps 9-10: frm_type 计算 + 重复帧检测 ====== */

// 9. frm_type 计算 + 未识别帧丢弃
if (mgmt_type == IEEE80211_FC0_TYPE_MGT) {
frm_type = mgmt_txrx_get_frm_type(mgmt_subtype, mpdu_data_ptr);
if (frm_type == MGMT_FRM_UNSPECIFIED) {
qdf_nbuf_free(buf);
return QDF_STATUS_E_FAILURE;
}
} else {
frm_type = MGMT_CTRL_FRAME;
}

// 10. 重复帧检测(非 Beacon/Probe Req/Probe Resp 的管理帧)
if (mgmt_type == IEEE80211_FC0_TYPE_MGT &&
!(mgmt_subtype == MGMT_SUBTYPE_BEACON || ...) &&
mgmt_txrx_frame_is_duplicate(psoc, wh, frm_type)) {
qdf_nbuf_free(buf);
return QDF_STATUS_E_FAILURE;
}

frm_type 算出来后,进入分发阶段。前 10 步完成了帧的校验、预处理和类型计算,现在 handler 调用链开始工作:查回调表、查 peer、clone buffer 并发分发——每个已注册的 handler 都会拿到一份帧副本。

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
    /* ====== Steps 11-12: 查表、Beacon 限速 ====== */

// 11. 查回调表:mgmt_rx_comp_cb[frm_type] + MGMT_FRAME_TYPE_ALL 通配
qdf_spin_lock_bh(&mgmt_txrx_psoc_ctx->mgmt_txrx_psoc_ctx_lock);
rx_handler = mgmt_txrx_psoc_ctx->mgmt_rx_comp_cb[frm_type];
// ...复制 handler 链表(spinlock 保护下的快照)...

// MGMT_FRAME_TYPE_ALL:通配 handler,所有管理帧都会触发
rx_handler = mgmt_txrx_psoc_ctx->mgmt_rx_comp_cb[MGMT_FRAME_TYPE_ALL];
// ...追加到 handler 链表...

qdf_spin_unlock_bh(&mgmt_txrx_psoc_ctx->mgmt_txrx_psoc_ctx_lock);

// 12. Beacon 速率限制
if (mgmt_subtype == MGMT_SUBTYPE_BEACON &&
mgmt_rx_params->is_conn_ap.is_conn_ap_frm == 0)
wlan_mgmt_rx_beacon_rate_limit(psoc, mgmt_rx_params);

/* ====== Steps 13-15: 查 peer、遍历 handler 链表分发、释放资源 ====== */

// 13. 查 peer(先查 addr2,再查 addr1,支持广播帧 peer=NULL)
peer = wlan_objmgr_get_peer(psoc, mgmt_rx_params->pdev_id, wh->i_addr2, ...);
if (!peer && !qdf_is_macaddr_broadcast(wh->i_addr1))
peer = wlan_objmgr_get_peer(psoc, mgmt_rx_params->pdev_id, wh->i_addr1, ...);

// 14. 遍历 handler 链表,clone buffer 逐个分发
rx_handler = rx_handler_head;
while (rx_handler->next) {
copy_buf = qdf_nbuf_clone(buf); // 每个 handler 获得独立的 buffer 副本
if (copy_buf)
rx_handler->rx_cb(psoc, peer, copy_buf, mgmt_rx_params, frm_type);
rx_handler = rx_handler->next;
}
rx_handler->rx_cb(psoc, peer, buf, mgmt_rx_params, frm_type); // 最后一个用原始 buffer

// 15. 释放 peer 引用 + handler 链表内存
if (peer) wlan_objmgr_peer_release_ref(peer, ...);
// ...释放 handler 链表...
}

以上是对 wlan_mgmt_txrx_rx_frame_handler 的 15 步完整解读。它的核心职责可以归纳为三件事:校验(类型/地址/长度/重复帧)、分类(frm_type 计算)、分发(回调表 + clone buffer 并发)。frm_type 一旦被计算出来,后续的 handler 注册者(PE/LIM、P2P 组件、TDLS 组件等)只需要向 mgmt_rx_comp_cb[frm_type] 注册回调即可——这就是 mgmt_txrx 子系统作为 "中央调度器" 的价值。

第二层 — PE/LIM 层分发

对于非 P2P、非 TDLS 的管理帧(Auth、Assoc、Action 等),mgmt_txrx 回调表中注册的 pe_handle_mgmt_frame()lim_api.c:1254,注册为 MGMT_FRAME_TYPE_ALL 通配回调)先把帧重新封装成 CDS 报文,经 sys_bbt_process_message_core() 投递到 PE 消息队列,再由 lim_process_message_queue() 分发到 lim_process_action_frame()lim_process_action_frame.c)——在这里进行第二次 category 分发:本地处理的(QoS、HT、VHT、SA Query 等)在 PE 层消化;需要上报的(未识别 category、Vendor Specific)通过 lim_send_sme_mgmt_frame_ind()cfg80211_rx_mgmt() 上报。详细逻辑见第 2 章。

两层各司其职的分工,一句话就能概括:第一层回答「谁想要这帧」,第二层回答「这帧最终怎么办」。回调表是订阅机制——P2P 组件经 p2p_mgmt_rx_action_ops()wlan_p2p_roc.c)把 tgt_p2p_mgmt_frame_rx_cb 注册到 MGMT_ACTION_VENDOR_SPECIFIC 槽位,TDLS、SON 等组件同样按 frm_type 注册回调,新增一个消费者只需注册、不动核心分发表。

switch 是裁决机制——帧落到 PE/LIM 后,lim_process_action_frame() 的 switch 给出唯一权威结论(本地消化 / 上报 / 丢弃),穷尽且可审计。

正因为分工不同,同一个帧可以既被第一层的组件消费、又被第二层的 PE 裁决上报:GAS Initial Request 就是活例——第一层 SON 组件经 wlan_son_anqp_frame() 拦一份做 ANQP 采集,第二层 PE 的 switch 又把它 lim_send_sme_mgmt_frame_ind() 上报给 supplicant。

MTK 的单层分派把「订阅」和「裁决」合并进一个 switch,换来的是简单,代价是两者耦合:新增一个消费者(比如驱动内新增一个想监听 WNM 的模块)就得改 nicRxProcessActionFrame 的分派逻辑,而它是固件 ROM blob,驱动根本改不动。这就是 QCOM 宁可多一层抽象、也要把订阅与裁决拆开的深层原因。

3.4 对端发来的 Auth/Assoc 帧如何触发状态转移?

前面几节追踪了管理帧从固件到 PE/LIM 的驱动侧接收路径,但有一个问题还没回答:那些被上报的 Auth/Assoc 帧到达 supplicant 之后发生了什么? 这正好与上一篇(管理帧的发送 — 控制面的帧构建与下发)的 §2.3(Deauth 到达 supplicant 后的清理逻辑)形成首尾呼应。

当驱动通过 lim_send_sme_mgmt_frame_ind()kalIndicateRxMgmtFrame() 将帧上报后,它们最终都会经过同一个内核函数——cfg80211_rx_mgmt()

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// include/net/cfg80211.h — cfg80211_rx_mgmt() static inline 包装函数
static inline bool cfg80211_rx_mgmt(struct wireless_dev *wdev, int freq,
int sig_dbm, const u8 *buf, size_t len,
u32 flags)
{
struct cfg80211_rx_info info = {
.freq = MHZ_TO_KHZ(freq),
.sig_dbm = sig_dbm,
.buf = buf,
.len = len,
.flags = flags
};
return cfg80211_rx_mgmt_ext(wdev, &info);
}

注意cfg80211_rx_mgmt() 只是一个 static inline 包装,真正的帧上报逻辑在 cfg80211_rx_mgmt_ext()net/wireless/mlme.c)中:它遍历 wdev->mgmt_registrations 链表,按 frame_typematch_len 匹配注册的监听者,匹配成功后才通过 nl80211_send_mgmt() 上报。为什么要用 registration list? 因为多个用户态进程可能同时监听不同类型的帧(如 wpa_supplicant 监听 Auth/Assoc、P2P daemon 监听 P2P Action),内核需要通过 registration matching 将帧准确地推送给订阅了该帧类型的进程,而不是广播给所有进程。

在 nl80211 层,nl80211_send_mgmt() 构造 NL80211_CMD_FRAME 事件并通过 netlink socket 上报。wpa_supplicant 侧的 do_process_drv_event()src/drivers/driver_nl80211_event.c:3910)收到这个事件后,通过 switch(cmd) 分发 NL80211_CMD_FRAMEmlme_event()wpa_supplicant_event(),进入 supplicant 的事件处理中枢:

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
// wpa_supplicant/events.c — wpa_supplicant_event() Auth/Assoc/Disassoc/Deauth 事件分支
// 精简版:省略了测试选项检查(CONFIG_TESTING_OPTIONS)、日志输出和 FST 处理
void wpa_supplicant_event(void *ctx, enum wpa_event_type event,
union wpa_event_data *data)
{
struct wpa_supplicant *wpa_s = ctx;
switch (event) {
case EVENT_AUTH:
sme_event_auth(wpa_s, data);
// → 解析 Auth 响应帧中的 auth_alg、auth_transaction、status_code
// → SAE(Simultaneous Authentication of Equals,对等同时认证)多轮握手下一轮 / FT 序列下一帧 / OPEN 认证完成
// → 成功后触发 sme_associate() 发起关联
break;
case EVENT_ASSOC:
wpa_supplicant_event_assoc(wpa_s, data);
// → 解析 Assoc Resp 中的 status_code、AID、关联 IE
// → 成功后内部调用 wpa_sm_notify_assoc() 启动 4-Way Handshake
break;
case EVENT_DISASSOC:
wpas_event_disassoc(wpa_s, data ? &data->disassoc_info : NULL);
// → WPA_DRIVER_FLAGS_SME 时先 sme_event_disassoc() 清理驱动状态
// → 最后调 wpas_event_disconnect() 完成断开(详见上一篇 §2.3)
break;
case EVENT_DEAUTH:
wpas_event_deauth(wpa_s, data ? &data->deauth_info : NULL);
// → 先 wpa_reset_ft_completed() 清除 FT 状态
// → 直接调 wpas_event_disconnect(),不经过 sme_event_disassoc()
// → DEAUTH 和 DISASSOC 是两个独立事件路径,控制流不同
break;
// ...省略其他事件类型...
}
}

主要功能:

  • EVENT_AUTHsme_event_auth():解析 AP 返回的 Auth 响应帧,判断认证是否成功。SAE 模式下可能触发多轮 Commit/Confirm 握手;成功后自动调用 sme_associate() 发起关联——整个 Auth→Assoc 的 supplicant 侧衔接在这里完成闭环。为什么需要 sme_event_auth 做 wpa_state guard? 防止延迟到达的 Auth 帧干扰已进入 ASSOCIATING 等后续状态的状态机——如果当前状态不是 WPA_AUTHENTICATING,说明认证流程已经结束或超时,延迟帧必须被丢弃
  • EVENT_ASSOCwpa_supplicant_event_assoc():解析 Assoc Resp 中的 status_code 和 AID(Association ID)+ 关联 IE,成功后调用 wpa_sm_notify_assoc() 启动 WPA 状态机的 4-Way Handshake——管理帧的事到这里结束,EAPOL 密钥协商开始
  • EVENT_DISASSOCwpas_event_disassoc():当 WPA_DRIVER_FLAGS_SME 置位时先调用 sme_event_disassoc() 清理驱动侧的 authenticated 状态,最终调用 wpas_event_disconnect() 完成状态清理——与上一篇 §2.3 描述的三步清理逻辑一致
  • EVENT_DEAUTHwpas_event_deauth()不等于 DISASSOC——这条路径不经过 sme_event_disassoc(),而是先 wpa_reset_ft_completed() 清除 FT 状态,然后直接调用 wpas_event_disconnect() 完成断开。DEAUTH 和 DISASSOC 是两个独立的事件路径,这点在上一篇 §2.3 已有验证

Action 帧则完全不经过上面四条连接管理分支——它们以 EVENT_RX_MGMT 事件到达,在 wpa_supplicant_event()EVENT_RX_MGMT 分支里按 subtype 过滤掉 Probe Req(交给 P2P)之后,统一汇入 wpas_event_rx_mgmt_action()events.c:5565)做 category 级分发。这个函数不是 switch,而是一串 if (category == ...) 顺序判断——每个 category 命中即 return,跑完所有判断仍未命中的帧落到最后的 wpas_p2p_rx_action()p2p_supplicant.c:7827)兜底。分发顺序本身就是一条因果链,每一环都承接 §2.2 里 QCOM/MTK 驱动侧裁决的上游:

  • WMM(category 17)→ wmm_ac_rx_action()wmm_ac.c:732):ADDTS/DELTS 在 supplicant 侧的闭环。§2.1 说 QCOM/MTK 把 QoS/WMM 扣在驱动本地处理,但 supplicant 仍保留自己的 WMM AC 状态机,承担 WMM 参数协商的另一半
  • FT(category 6)→ ft_rx_action()events.c:5238):802.11r over-the-DS 快速切换的密钥上下文交换。注意它与 §2.3 说的 FT Auth 是两回事——FT Auth 是认证帧(看 Auth alg 字段),在 QCOM PE 层就被 lim_process_ft_auth_frame() 拦下;这里的 FT Action 是重关联阶段走 Action 通道的 FT Request/Response,两者共同拼出 FT 的完整流程
  • SA Query(category 8)→ sme_sa_query_rx()sme.c:3628):PMF 的 supplicant 侧闭环,与 §2.1 里 QCOM 驱动本地处理 SA Query 形成「驱动先答、supplicant 兜底」的双层防御
  • WNM(category 10)→ ieee802_11_rx_wnm_action()wnm_sta.c:1997):BTM Request/Response 的 supplicant 决策中枢。这正好接上 §2.2 里 QCOM 把 WNM 全量上报的原因——漫游决策(要不要切、切到哪个 BSS)必须在 supplicant 的 BSS 选择逻辑里做,驱动不该代劳;而 §2.4 里 MTK 却选择驱动本地处理 BTM,两条相反路线在此汇合
  • GAS(Public Action 4 / Protected Dual 9)→ gas_query_rx()gas_query.c:517):ANQP 查询收发状态机。§2.2 里 QCOM SON 模块先经 wlan_son_anqp_frame() 拦一份做采集再继续上报,那份「继续上报」的帧最终就落在这里
  • RRM(category 5)→ wpas_rrm_handle_radio_measurement_request()rrm.c:1415)、wpas_rrm_process_neighbor_rep()rrm.c:62)、wpas_rrm_handle_link_measurement_request()rrm.c:1453)三个独立入口,按 action field 细分
  • DPP(Public Action 里 Vendor Specific + DPP OUI type)→ wpas_dpp_rx_action()dpp_supplicant.c:4084):扫码配网的 supplicant 侧协议状态机,§2.2 里 QCOM 拿 dpp_oui 比对匹配后上报的帧就落在这里
  • 兜底 → wpas_p2p_rx_action():P2P 的 Public Action 子类型(GO Negotiation / Provision Discovery / Invitation)以及所有上面没接住的 category 都落在这里——它本身就是 supplicant 侧的「未识别帧兜底上报」镜像

注意 MBO 不在这条分发链里——它没有独立的 Action category,而是借道 WNM(BTM)和 Assoc 帧里的 MBO IE 生效,mbo_parse_rx_anqp_resp()mbo.c:662)只在 ANQP 响应里解析 MBO 属性。这个「MBO 无专属 Action category」的事实,正好解释了为什么 §2.2 的 QCOM 驱动分发表里也找不到 ACTION_CATEGORY_MBO

于是整条 Action 帧的裁决链被拉成三层:QCOM 的第一层(mgmt_txrx 回调表,按 frm_type 分拣)+ 第二层(PE/LIM 的 category switch,本地消化还是上报),加上 supplicant 这第三层(wpas_event_rx_mgmt_action(),上报来的帧由哪个协议子系统消费)。MTK 跳过第一层(单层分派),但 supplicant 这层两个平台共享——这是「驱动分平台、协议栈统一」的必然结果。

RX 路径全貌收束:现在你应该能把整条接收链路串起来了——固件上报(WMI/MBOX)→ wma_mgmt_rx_process / nicRxProcessActionFrame(驱动第一关)→ wlan_mgmt_txrx_rx_frame_handler / category 分派(QCOM 两层 / MTK 单层)→ cfg80211_rx_mgmt()(static inline 包装,内核上报入口)→ cfg80211_rx_mgmt_ext()(mgmt_registrations 链表遍历,frame_type 匹配后决定上报哪个用户态进程)→ nl80211 event(netlink broadcast)→ wpa_supplicant_event()。到了 supplicant 之后分两路:连接管理帧(Auth/Assoc/Disassoc/Deauth)走 sme_event_auth() / wpa_supplicant_event_assoc() / wpas_event_disassoc() / wpas_event_deauth() 四个事件分支;Action 帧走 EVENT_RX_MGMTwpas_event_rx_mgmt_action() 的 category 分发(WMM/FT/SA Query/WNM/GAS/RRM/DPP/P2P 兜底)。一条从对端空中信号到 supplicant 状态转移的完整链路,至此闭合。

3.5 MTK 为什么只需要一层分发?

MTK 的管理帧接收链路比 QCOM 短得多——没有 WMI 事件中间层,没有 mgmt_txrx 回调表,直接从固件 ROM 按 category 分派。

与 QCOM 的关键差异

  1. 无 WMI 层:MTK 不使用 WMI 事件机制上报管理帧——固件通过 NIC RX 中断直接把帧数据传递给驱动侧的 SW_RFB(Receive Frame Buffer)结构体,省去了 WMI 事件打包 / 解包的开销
  2. 无 mgmt_txrx 回调表:没有 frm_type 计算、没有 handler 链表遍历、没有 clone buffer 并发机制——category 分派是固件 ROM 直接完成的
  3. nicRxProcessActionFrame 是 ROM blob:这个函数不是驱动代码,驱动代码中只有它的声明(include/nic/nic_rx.h),实际实现在 WiFi 固件的 ROM 中。这意味着它的行为取决于固件版本,驱动无法修改它的分派逻辑
  4. 兜底策略相反:QCOM 不认识的 category 在 PE 层 default 分支静默丢弃;MTK 不认识的 category 通过 aisFuncValidateRxActionFramekalIndicateRxMgmtFrame 全量上报 supplicant

具体的 category→handler 分派链路(nicRxProcessActionFramep2pFuncValidateRxActionFrame / wnmWNMAction / aisFuncValidateRxActionFramekalIndicateRxMgmtFrame)已在 §2.4 完整描述,此处不再重复。上述 4 点架构差异解释了 MTK 为什么只需要单层分发:无 WMI 中间层意味着没有事件格式转换开销,无回调表意味着没有 frm_type 计算和 handler 遍历,ROM blob 意味着分派逻辑对驱动而言是黑盒,顺序分派意味着没有并发同步的复杂度——但也失去了并发处理多个帧的能力。

把这条链逐函数铺开,MTK 的「单层」其实也能拆成四个时序节点。第一节点是固件 ROM 的 nicRxProcessActionFrame()include/nic/nic_rx.h:1804,仅声明、实现在固件 blob)——它在固件侧完成 Action Frame Header 解析和 category 三选一分派,是整条链里驱动看不见的「黑盒前置」。第二节点由三个驱动侧 handler 之一接管:category 4 进 p2pFuncValidateRxActionFrame()mgmt/p2p_func.c:4448)做 P2P 子类型二级分发并推进 P2P FSM;category 10 进 wnmWNMAction()mgmt/wnm.c:85)按 action 细分 BTM/Timing Measure,开启 CFG_SUPPORT_802_11V_BTM_OFFLOAD 时本地应答、否则转兜底;其余 category 进 aisFuncValidateRxActionFrame()mgmt/ais_fsm.c:8394)。第三节点是 aisFuncValidateRxActionFrame 的 BSS 类型闸门——非 AIS 直接 return。第四节点 kalIndicateRxMgmtFrame()os/linux/gl_kal.c:7736)取 net_device、调 cfg80211_rx_mgmt() 出内核边界。对比 QCOM 的 15 步:MTK 把「frm_type 计算 + 回调表查表 + clone 分发」压缩成固件 ROM 里的一次 category switch,步数少了一个数量级,代价是每一帧只命中一个 handler。

「无回调表」的代价可以量化。QCOM 第一层回调表是 mgmt_rx_comp_cb[MGMT_MAX_FRAME_TYPE]wlan_mgmt_txrx_main_i.h:220,139 个 frm_type 槽位),每个槽位是一条 struct mgmt_rx_handler 链表而非单个函数指针——Scan、MLME、PE、P2P 等组件各自向同一槽位注册回调,分发时逐个 qdf_nbuf_clone() 复制 buffer 再调用(§3.3 步骤 14),同一个帧因此能被多个消费者同时看到。

MTK 恰好相反:单条 SW_RFB 顺序走完,nicRxProcessActionFrame 的 category switch 三选一(P2P / WNM / AIS 兜底),一帧只命中一个 handler,没有 clone、没有多消费者——需要共享时只能在上层再复制或串行排队,这就是「无回调表」的并发代价。

「三选一」的兜底分支值得单独拆开看容错语义。aisFuncValidateRxActionFrame()ais_fsm.c:8394)是所有不命中 P2P/WNM 的 Action 帧的最终落点,但它不是无条件上报——先 if (!IS_BSS_INDEX_AIS(...)) 校验帧所属 BSS index 必须是 AIS 网络类型,非 AIS(比如帧落在 P2P 接口的 BSS 上)直接 return 丢弃,只有 AIS 帧才经 kalIndicateRxMgmtFrame() 透传 cfg80211。这意味着 MTK 的「全量上报」其实有两道前置闸门:固件 ROM 按 category 三选一筛掉 P2P/WNM,驱动侧再按 BSS 类型筛掉非 AIS——两道都放行的才到 supplicant。

于是「未知 category」的错误处理路径在双平台呈现出镜像的两端:QCOM 对不认识的 category 在 mgmt_txrx 层(MGMT_FRM_UNSPECIFIED)或 PE 层(switch 的 default)静默丢弃,只打一行 pe_warn_rl 日志;MTK 则一路兜底上报 supplicant。QCOM 的选择是「驱动不猜」——802.11 标准还在演进,老驱动遇到新 category 时,猜错处理路径比丢弃更危险(可能误改链路状态或误触发状态机);代价是若新 category 恰是 supplicant 认识的,QCOM 会因提前丢弃而错过,必须升级驱动固件才能放行。

MTK 的选择是「上层兜底」——驱动只做粗筛,细判全交给 supplicant,前向兼容性更好(supplicant 更新快、懂的新 category 多);代价是驱动不认识的所有帧都涌向 supplicant,恶意对端可以构造海量怪帧打满 netlink 上报通道,supplicant 必须自己再做一轮合法性校验兜底。这个「丢在驱动」vs「漏给上层」的取舍,和 §2.4 里 WNM 的「上层决策 vs 驱动自治」是同一枚硬币的两面。

这种「一帧多消费者」与「一帧一消费者」的分野,在 Linux 内核和蜂窝协议栈里都有先例。netfilter 的 nf_register_net_hook() 允许多个钩子注册到同一个 hook 点、依次处理同一个 skb,是 QCOM 多消费者思想的雏形——但它比 QCOM 的槽位多了一维排序:5 个 hook 点(NF_INET_PRE_ROUTING/NF_INET_LOCAL_IN/NF_INET_FORWARD/NF_INET_LOCAL_OUT/NF_INET_POST_ROUTING)相当于 frm_type 槽位,nf_hook_ops.priority 字段(include/linux/netfilter.h:97)把同一 hook 点的钩子按 NF_IP_PRI_FIRSTINT_MIN)到 NF_IP_PRI_LASTINT_MAXinclude/uapi/linux/netfilter_ipv4.h:31/45)升序排列,priority 小的先执行,先后顺序显式可控。而 QCOM 的 struct mgmt_rx_handler 只有 next 指针,同一槽位的 handler 按注册顺序执行,先后不可控。

LTE/NR 的 MAC 层按 LCID(逻辑信道 ID)把一条 PDU 解复用给唯一的 RLC 实体——这是 MTK category 三选一的同构。

更深一层的差异在索引维度:netfilter 的 5 个 hook 点按包方向切分(入站 → 本地投递 → 转发 → 本地发出 → 出站),一个 skb 沿流向依次命中不同的 hook 点;QCOM 的 139 个 frm_type 槽位按帧类型切分,每个 subtype / action category 独占一个槽,一个帧只命中其中一个槽。同为「多消费者」调度表,一个以「这包往哪走」为轴,一个以「这帧是什么」为轴。

这种「多槽位、按类型索引」的分发表,在硬件层也有同构:PCIe 的 MSI-X 让一个设备函数最多分配 2048 个 vector,每个 vector 独立路由到不同 CPU core——一张「哪类中断 → 哪个处理队列」的表,与 mgmt_rx_comp_cb 的「哪类帧 → 哪个 handler 槽」是同一张表。差别在扇出方向:QCOM 命中槽位后靠 qdf_nbuf_clone() 把一帧复制成多份给多个消费者(一帧多消费者),MSI-X 命中 vector 后只触发绑定的那一个软中断(一中断一消费者)。

QCOM 的接收系统像机场的行李转盘分拣 + 登机口验票两层:第一层是行李转盘(mgmt_txrx 回调表),行李(帧)贴好 frm_type 标签后,转盘按标签自动路由——同一件行李还能克隆多份,同时送到 Scan、PE、P2P 多个领取口;第二层是登机口验票(PE/LIM category switch),看具体舱位(category),本地乘客在候机厅等,国际乘客送海关(cfg80211)出境。MTK 则像只有登机口验票的单层小机场——一张票(SW_RFB)一次只验一个旅客,固件 ROM 看 category 三选一,没有行李转盘的多份克隆,一次只放行一个。

QCOM vs MTK RX 对比

维度QCOMMTK
分发层数两层(mgmt_txrx + PE/LIM)单层(nicRx + FSM)
回调机制回调表 + handler 链表(多注册)直接 category 分派
P2P/TDLS 路径第一层就走专属 handler和其他帧同路径
未识别帧PE 层 default 静默丢弃AIS FSM 验证后上报
并发处理handler 链表 clone buffer 并发顺序分派

QCOM vs MTK RX 路径对比

以上追踪的都是驱动能 "看到" 的管理帧——Auth/Assoc/Action 帧从固件上报到驱动,经回调表或 category 分派,部分本地消化、部分上报 supplicant,整条软件栈都有对应的处理代码。但还有一类帧,它们同样属于管理帧或控制帧范畴,却在软件层完全不可见——不是因为代码被隐藏了,而是因为它们的设计目标要求响应时间在微秒级,软件根本来不及参与。下面就来看看这些 "隐身" 的帧。


4 那些你永远看不到的帧:RTS/CTS/ACK/PS-Poll 去哪了?

最后聊一类特殊的帧——你永远看不到它们的代码,因为它们根本不走驱动软件栈

帧类型处理者软件角色
RTS/CTS固件 MAC 层驱动通过 WMI/MBOX 设置 RTS threshold 参数,具体每个帧的 RTS/CTS 交换由固件自主决定
ACK固件 MAC 层设置 ACK timeout 参数后,固件在收到数据帧后自动回复 ACK,CPU 不参与
PS-Poll固件省电引擎驱动只配置 PS-Poll count 和间隔参数,帧的生成和发送全在固件

PS-Poll 帧格式:PS-Poll(Power Save Poll)是控制帧的一种(IEEE 802.11-2024 §9.3.1.5),用于 STA 从省电模式唤醒后向 AP 请求缓存帧。它的结构极其简洁——仅 16 字节:

字段长度说明
Frame Control2 字节Type=Control, Subtype=PS-Poll (1 0 1 0)
Duration/ID (AID)2 字节STA 的 Association ID(AID),两 MSB 置 1(非 BDT 变体)
RA (Receiver Address)6 字节AP 的 BSSID
TA (Transmitter Address)6 字节STA 的 MAC 地址

注意 PS-Poll 帧没有 Sequence Control 字段——它不是一个数据帧,不需要重传窗口管理。Duration/ID 字段的两 MSB 置 1,低 14 位携带 STA 的 Association ID(AID)。STA 收到 Beacon 后检查 TIM(Traffic Indication Map)位图中自己 AID 对应的 bit 是否置位,若置位则发送 PS-Poll;AP 收到 PS-Poll 后从 Duration/ID 字段提取 AID,据此查找该 STA 的缓存帧。整个 PS-Poll 在固件省电引擎中全自动完成:固件检测到 Beacon 中 TIM 位图对应 bit 置位→生成的 PS-Poll 帧→发送到 AP→接收缓存帧→返回省电模式,驱动层完全不参与。

ACK timeout 与 PS-Poll 的双平台配置对比:虽然这些帧本身不可见,但它们的时间参数省电策略仍然可以通过驱动配置来调整。

这些帧的共同特征是:硬实时——SIFS(Short Interframe Space)只有 10 微秒(802.11b DSSS)或 16 微秒(802.11a/g/n/ac/ax OFDM,取决于 PHY 类型而非频段),软件根本来不及反应。所以它们的设计哲学是 "参数配置 + 全自动执行"。

为什么 SIFS 决定了这些帧必须在硬件层处理? 以 ACK 帧为例:对端收到数据帧后,必须在 SIFS 时间(10us 或 16us) 内发出 ACK 帧。这个时间窗口极其苛刻——一次 CPU 中断响应就需要 5-10us,一次 DMA 传输也需要数个微秒。如果把 ACK 帧的生成交给软件(哪怕只是固件内的 CPU 进程),调度延迟就足以让它错过 SIFS 窗口。因此 802.11 协议从设计层面就要求这些帧由 MAC 硬件状态机 自主生成:硬件在收到完整数据帧后,自动在 SIFS 时间点触发 ACK 发送,CPU 全程不参与。

RTS/CTS 的交互也是同样的硬实时约束——从 CTS 帧结束到数据帧开始,间隔也是 SIFS,硬件必须自主完成。

RTS threshold 配置——软件层唯一的 "遥控按钮"。虽然 RTS/CTS 帧本身不可见,但驱动可以配置 RTS threshold 参数来控制固件的 RTS 行为:当数据帧长度超过该阈值时,固件在发送前自动发起 RTS/CTS 握手。以下是双平台如何设置这个参数:

1
2
3
4
5
6
7
8
9
10
11
// QCOM: core/hdd/src/wlan_hdd_cfg80211.c — set_wiphy_params
// cfg80211 通过 NL80211_ATTR_WIPHY_RTS_THRESHOLD 下发 → WIPHY_PARAM_RTS_THRESHOLD
if (changed & WIPHY_PARAM_RTS_THRESHOLD) {
u32 rts_threshold = (wiphy->rts_threshold == -1) ?
cfg_max(CFG_RTS_THRESHOLD) : // -1 = 禁用 RTS(设为最大值)
wiphy->rts_threshold;
if ((cfg_min(CFG_RTS_THRESHOLD) > rts_threshold) ||
(cfg_max(CFG_RTS_THRESHOLD) < rts_threshold))
return -EINVAL; // 范围校验
ucfg_mlme_set_rts_threshold(hdd_ctx->psoc, rts_threshold); // → WMI 下发固件
}
1
2
3
4
5
6
7
8
9
10
11
// MTK: os/linux/gl_p2p_cfg80211.c — mtk_p2p_cfg80211_set_wiphy_params
// MTK 的 cfg80211 RTS threshold 回调目前为存根(TODO),
// 实际配置走 WEXT ioctl 路径:gl_wext.c → kalIoctl → wlanoidSetRtsThreshold
if (changed & WIPHY_PARAM_RTS_THRESHOLD) {
/* TODO: */ // 当前实现为空,RTS 阈值
DBGLOG(P2P, TRACE, // 通过传统的 WEXT ioctl 接口设置
"The RETRY RTS threshold is changed.\n");
}
// WEXT 路径(实际工作路径):
// gl_wext.c → kalIoctl(prGlueInfo, wlanoidSetRtsThreshold, &u4RtsThresh, ...)
// → wlan_oid.c: *prRtsThreshold = prAdapter->rWlanInfo.eRtsThreshold(注:该 set 函数实际为读语义,疑似 MTK 实现瑕疵)

mac80211 侧:RTS threshold 在发送路径的实际生效点在 ieee80211_tx_h_rate_ctrl()net/mac80211/tx.c)——if (len > tx->local->hw.wiphy->rts_threshold) { txrc.rts = true; }。固件根据 use_rts 标志自动决定是否在发送数据帧前发起 RTS/CTS 交换。

PS-Poll 与省电参数的双平台配置:除了 RTS threshold,驱动还能通过以下参数影响固件的 PS-Poll 和省电行为:

1
2
3
// QCOM: PS-Poll 与省电相关 INI 参数(core/hdd/inc/wlan_hdd_cfg.h)
// qpower_max_ps_poll_count — 省电模式下最大 PS-Poll 发送次数,固件超过此计数仍未收到缓存帧则放弃(默认 10)
// gEnablePowerSaveOffload — 省电 offload 开关,交由固件自主管理(而非驱动轮询)

MTK 走同样的策略,但参数命名风格不同:

1
2
3
// MTK: PS-Poll 配置通过 power management 参数下发(mgmt/power.c)
// ucPsPollCount / ucPsPollInterval — 固件向 AP 轮询缓存帧的次数和间隔
// MTK 同样支持将 PS 管理完全 offload 给固件(fw_own_ps_ctrl)

双平台在 PS-Poll 管理上的策略一致:驱动只下发参数上限,具体执行全在固件。驱动不关心某个特定帧是第几次 PS-Poll、AP 有没有返回缓存帧——这些都是固件省电引擎的内部状态。

这些帧就像跑道的自动引导灯系统。塔台(驱动)不是每到一架飞机就过去手动开一次灯——它只负责设置开灯规则(什么时候亮、亮多久、亮多亮),具体的开关由跑道系统(固件 / 硬件)全自动执行。这样飞机滑行时(SIFS 时间内)灯能瞬间反应,不需要等塔台打电话通知。


5 双平台架构差异有多大?QCOM vs MTK 全景对比

维度QCOMMTK
帧校验位置固件 FCS + 驱动二次校验(PMF/长度/地址)固件 ROM FCS 单次校验
RX 架构两层分发(mgmt_txrx 回调表 + PE/LIM)单层分发(FSM category 分派)
Auth 处理PE 层直发,跳过 ROC/Policy Manager与其他帧同路径,经 MBOX
Action 分发第一层按 frm_type 查回调表,第二层 PE 按 category 二次分发AIS FSM 直接按 category 分派
通信机制WMI(控制面)MBOX 消息队列
并发Handler 链表 + clone buffer顺序分派
ROC(Remain on Channel,信道驻留)管理独立的 Policy Manager 审批MBOX 参数直传
未识别帧PE 层 default 静默丢弃AIS FSM 兜底全量上报

QCOM 的设计哲学:用复杂度换灵活性。双路径 + 两层分发让 QCOM 在 P2P、TDLS、多信道并发等场景下有更多优化空间,但代价是代码路径多、调试复杂。

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

回顾全链路,两个平台的 Action 帧接收分别由一对入口函数统领:QCOM 的 lim_process_action_frame()(PE 层 500 行 category 大全分发器)和 MTK 的 nicRxProcessActionFrame()(固件 ROM blob 入口,驱动侧按 category 直连 handler)。前者是所有 Action 帧的「中央调度塔」,后者是固件与驱动的「分工边界碑」——理解了这两个函数的定位,就理解了全章。

QCOM 的塔台是一个大型国际机场——值机 + 登机口两道关卡(两层分发)、VIP 通道(Auth 快速路径)。MTK 的塔台是一个支线航站楼——只设一道安检口(单层分派)、但给特大件开了货运电梯(大帧直通)。两种设计没有绝对的好坏,只有场景适配的区别。


6 总结:双平台接收架构对比

从对端空中信号到 supplicant 状态转移,管理帧接收路径的两条实现——QCOM 的两层分发与 MTK 的单层分派——至此全部走完。收束全章,可以归结为一张全链路回顾表和一张关键常量速查表。

全链路回顾表

环节QCOMMTK
固件上报WMI 事件(WMI_MGMT_RX_EVENTID)→ wma_mgmt_rx_process()NIC RX 中断 → ROM blob nicRxProcessActionFrame()
中央分发wlan_mgmt_txrx_rx_frame_handler():frm_type 计算 + 回调表 + clone 多消费者固件 ROM category 三选一:P2P / WNM / 兜底
二次分发lim_process_action_frame():category switch 四阶段(安检→本地→上报→丢弃)无(单层,无二次分发)
本地处理QoS/Spectrum/WMM/HT/VHT/SA Query/Block Ack(PE 层闭环)wnmWNMAction()(BTM offload)、p2pFuncValidateRxActionFrame()(P2P FSM)
未识别帧default 静默丢弃aisFuncValidateRxActionFrame() 兜底上报
上报通道lim_send_sme_mgmt_frame_ind() → SME → HDDkalIndicateRxMgmtFrame()
内核汇聚cfg80211_rx_mgmt()cfg80211_rx_mgmt_ext()nl80211_send_mgmt()同左
supplicant 闭环wpa_supplicant_event() 四分支(Auth/Assoc/Disassoc/Deauth)同左

关键常量速查表

常量定义位置含义
MGMT_MAX_FRAME_TYPE139wlan_mgmt_txrx_utils_api.h:866QCOM 回调表 frm_type 槽位数
MGMT_FRM_UNSPECIFIED-1wlan_mgmt_txrx_utils_api.h:726未识别帧类型,直接丢弃
SIFS10 / 16 usIEEE 802.11(b / a·g·n·ac·ax)帧间最小间隔,决定 ACK/RTS/CTS 必须硬件处理
PS-Poll 帧长度16 字节IEEE 802.11-2024 §9.3.1.5省电轮询帧固定长度
ACTION_CATEGORY_*0~127IEEE 802.11-2024 §9.4.1.11Action 帧 category 编码(20+ 种)
CFG_SUPPORT_802_11V_BTM_OFFLOAD编译选项MTK mgmt/wnm.c开启后 MTK 本地处理 BTM

两套架构,同一个目标:把对端发来的每一条管理帧,准确、及时地送到该去的地方——本地消化的在驱动闭环,需要决策的交 supplicant。从固件的第一声「有帧来了」,到 supplicant 的状态机向前推进一步,管理帧接收的控制面,至此闭合。


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

本文源码取自 QCOM qcacld-3.0qcacmn;MTK gen4m;wpa_supplicant 来自 w1.fi