⚠️ 关键认知:Android Framework 的常规扫描(Settings 10s 扫描、App 调用 startScan、WifiConnectivityManager 周期性扫描)不经过 wpa_supplicant——它们走的是 wificond → nl80211 → 驱动的直通路径(详见 05 上)。那 wpa_supplicant 的扫描引擎是在什么时候用的?答案是:supplicant 自己内部触发的扫描——已连接状态下的 bgscan(后台扫描更优 AP 供漫游)、P2P Device Discovery、WPS 配网时的扫描。这些扫描不和 Framework 的扫描共享任何代码路径,它们是 supplicant 的内部事务。
上篇回顾:在 05(上)中,我们追踪了 Android Framework 的扫描请求从 App 到内核的完整链路——wificond 直通 nl80211,不经 supplicant。本篇聚焦另一条独立路径:supplicant 自己触发的扫描。
本章导读
本章比喻:如果 Framework 的 wificond 扫描是雷达指挥中心发起的例行探测任务(定时扫、用户要求扫),那么 wpa_supplicant 的扫描引擎就是雷达的自动维护程序——它在后台悄悄地扫描周围环境,自己决定何时扫、扫什么,目的是保持连接质量和发现 P2P 设备。最精妙的设计是 radio work 队列——就像雷达的任务调度器:同一台雷达不能同时做两件事,所有射频操作(扫描、连接、P2P)必须排队。
你将学到:
•supplicant 内部扫描的触发场景:连接前扫描(无缓存 BSS 时先扫再连)、bgscan(漫游候选)、P2P Discovery、WPS 配网•
wpa_supplicant_scan()的参数构建过程——SSID 列表、频率列表、IE 构建、MAC 随机化•radio work 的队列机制——为什么扫描要排队?单信道 vs 多信道互斥规则•扫描结果的完整分发链——EVENT_SCAN_RESULTS→ BSS 更新•BSS 缓存的生命周期管理——添加、更新、scan_miss_count过期淘汰、使用中保护
数据来源:
•external/wpa_supplicant_8[1](eloop / scan / bss / events)
代码说明:本文代码来自 AOSP external/wpa_supplicant_8 真实源码,有精简(去掉
wpa_dbglog 语句),关键路径完整保留。
系列导航:本篇是扫描三部曲的中篇,聚焦 wpa_supplicant 内部的扫描引擎(bgscan/P2P/漫游场景)。上篇讲 Framework → wificond → nl80211 的直通路径,下篇讲驱动执行 + PNO + WiFi 6E/7。
一、supplicant 什么时候会自己扫描?
一句话总结:Android Framework 的常规扫描走 wificond,不经过 supplicant。supplicant 的扫描引擎在四种场景下触发——bgscan(漫游)、P2P Discovery、WPS 配网、以及连接前扫描(没有缓存 BSS 时先扫再连)。
1.1 Framework 扫描 vs supplicant 扫描 — 两条独立路径
路径 A:Framework 扫描(05 上) 路径 B:supplicant 内部扫描(本篇)App.startScan() supplicant 内部事件触发→ WifiScanningServiceImpl → wpa_supplicant_req_scan()→ WifiNl80211Manager → wpa_supplicant_scan()→ wificond ScannerImpl::scanRequest() → wpa_supplicant_trigger_scan()→ ScanUtils::Scan() → radio_add_work("scan", wpas_trigger_scan_cb)→ NL80211_CMD_TRIGGER_SCAN → wpa_drv_scan()→ 内核 → 驱动 → 固件 → driver_nl80211_scan()→ NL80211_CMD_TRIGGER_SCAN→ 内核 → 驱动 → 固件
两条路径互不感知——Framework 不知道 supplicant 在扫什么,supplicant 不知道 Framework 什么时候扫。
1.2 supplicant 内部扫描的四种场景
| 连接前扫描 | wpa_supplicant_select_network() | scan_res_valid_for_connect | |
| bgscan | bgscan="simple:30:-45:300" | ||
| P2P Device Discovery | p2p_find | ||
| WPS 配网扫描 |
连接前扫描的完整流程——这是最容易被忽略的场景:
wpa_supplicant_select_network() // wpa_supplicant.c:5152├── wpa_supplicant_fast_associate() // 检查缓存是否可用│ ├── last_scan_res_used == 0? → 无缓存,return -1│ ├── scan_res_valid_for_connect 超时? → 缓存过期,return -1│ ├── crossed_6ghz_dom? → 跨越 6GHz 域,return -1(需重新扫描)│ └── wpas_select_network_from_last_scan() // 有缓存 → 直接选网连接│└── fast_associate 失败(无缓存/过期/跨 6GHz 域):(注:connect_without_scan 仅在 MESH/AP 模式下设置,普通 STA 连接不会跳过扫描)→ wpa_supplicant_req_scan() // 调度扫描→ wpa_supplicant_scan() // 本篇的核心函数→ 驱动层扫描→ EVENT_SCAN_RESULTS→ wpas_select_network_from_last_scan()→ wpa_supplicant_connect()→ wpa_supplicant_associate()
也就是说,当用户在 Settings 中点击连接一个 WiFi 时,如果 supplicant 的 BSS 缓存中没有这个 AP 的信息(或缓存已过期),它会先扫描再连接——这也是 supplicant 扫描引擎的典型使用场景。connect_without_scan 标志仅在 MESH/AP 模式下被设置(wpa_supplicant.c);P2P GO 的 connect_without_scan 在 p2p_supplicant.c 中单独设置——普通 STA 连接不会跳过扫描。
Tips:bgscan 是否还在用?Android 11+ 默认配置下,connected mode 的后台扫描由 WifiConnectivityManager 通过 wificond 触发(Framework 路径)。bgscan 模块主要用于遗留配置和某些 OEM 定制。本篇的引擎机制(eloop 调度、radio work、BSS 缓存)无论触发源是什么,底层逻辑完全相同。
1.3 各种扫描最终都汇聚到 eloop 定时器
下图展示了 supplicant 扫描引擎的完整流程——从 eloop 定时器触发到 BSS 缓存更新:

// external_wpa_supplicant_8/wpa_supplicant/scan.cvoid wpa_supplicant_req_scan(struct wpa_supplicant *wpa_s, int sec, int usec){int res;if (wpa_s->p2p_mgmt) { // P2P management 接口不执行 STA 扫描return;}res = eloop_deplete_timeout(sec, usec, wpa_supplicant_scan, wpa_s,NULL);if (res == 1) {wpa_dbg(wpa_s, MSG_DEBUG, "Rescheduling scan request: %d.%06d sec",sec, usec);} else if (res == 0) {wpa_dbg(wpa_s, MSG_DEBUG, "Ignore new scan request for %d.%06d sec ""since an earlier request is scheduled to trigger sooner",sec, usec);} else {wpa_dbg(wpa_s, MSG_DEBUG, "Setting scan request: %d.%06d sec",sec, usec);eloop_register_timeout(sec, usec, wpa_supplicant_scan, wpa_s, NULL);}}
主要功能:
•eloop_deplete_timeout() 检查是否已有 wpa_supplicant_scan 的定时器在排队——如果有且触发时间更早,则忽略新请求(res == 0);如果新请求更早,则替换(res == 1)•这是一种去抖动机制——短时间内多次调用 wpa_supplicant_req_scan() 不会导致多次扫描,只有最早触发的那个会生效•eloop_register_timeout() 注册的到期回调就是 wpa_supplicant_scan()——eloop 事件循环在 select() 超时到期后自动调用它
1.4 eloop 定时器机制——Supplicant 的「心跳」
eloop_register_timeout() 是 Supplicant 中最核心的调度原语。它把一个超时事件插入 eloop 的有序双向链表(按到期时间升序排列):
// external_wpa_supplicant_8/src/utils/eloop.cint eloop_register_timeout(unsigned int secs, unsigned int usecs,eloop_timeout_handler handler,void *eloop_data, void *user_data){struct eloop_timeout *timeout, *tmp;timeout = os_zalloc(sizeof(*timeout));if (timeout == NULL)return -1;// ...os_get_reltime() 获取当前时间 + 计算绝对到期时间...// 注:源码有两个 goto overflow 检查点(sec 加法溢出和 usec 进位后溢出),// overflow 标签会 os_free(timeout) 并返回 0(注册失败但不崩溃),此处省略timeout->time.sec += secs;timeout->time.usec += usecs;while (timeout->time.usec >= 1000000) {timeout->time.sec++;timeout->time.usec -= 1000000;}timeout->handler = handler;timeout->eloop_data = eloop_data;timeout->user_data = user_data; // eloop_deplete_timeout 按 handler+eloop_data+user_data 三元组匹配/* 按到期时间升序插入双向链表 */dl_list_for_each(tmp, &eloop.timeout, struct eloop_timeout, list) {if (os_reltime_before(&timeout->time, &tmp->time)) {dl_list_add(tmp->list.prev, &timeout->list);return 0;}}dl_list_add_tail(&eloop.timeout, &timeout->list);return 0;}
主要功能:
•链表按到期时间升序排列——eloop_run() 中的 select() 只取链表头的时间作为超时值•这意味着 eloop 的 select() 调用总是自动选择最近到期的超时——不需要手动管理多个定时器•扫描的回调 wpa_supplicant_scan 就是通过这个机制被调度的
二、wpa_supplicant_scan() — 参数构建的三个列表
一句话总结:这是 Supplicant 扫描的主入口,它不直接执行扫描,而是构建好所有参数后通过 radio work 排队。核心是三个列表的构建:SSID 列表(找谁)、频率列表(在哪找)、IE 列表(带什么附加信息)。
2.1 前置检查——这些情况下不扫描
wpa_supplicant_scan() 在构建参数之前先走一遍提前退出检查。以下是源码中实际存在的退出路径(按逻辑分组,非严格代码顺序):
wpa_s->wpa_state == WPA_INTERFACE_DISABLED | ||
wpa_s->disconnected && scan_req == NORMAL_SCAN_REQ | WPA_DISCONNECTED 后直接返回 | |
wpa_s->scanning | wpa_supplicant_req_scan(wpa_s, 1, 0) | |
connect_without_scan | wpa_supplicant_associate() | |
scan_req == NORMAL_SCAN_REQ | wpa_supplicant_set_state(wpa_s, WPA_INACTIVE) 后返回 | |
ap_scan = 0 | ap_scan 为 0,后续通过 ap_scan == 0 检查退出(跳过参数构建和扫描下发) | |
ap_scan == 0 | ||
wpas_p2p_in_progress(wpa_s) | wpa_supplicant_req_scan(wpa_s, 5, 0) | |
wpa_s->pno || wpa_s->pno_sched_pending) | wpa_supplicant_req_scan(wpa_s, 0, 100000) |
关键设计:拒绝不丢弃——多数情况通过 wpa_supplicant_req_scan() 重新调度。另外 wpa_supplicant_trigger_scan() 中还有一个独立检查——如果已有 scan_work 在排队则拒绝(radio work 串行化保证同时间只有一个扫描 work)。
2.2 SSID 列表构建——找谁?
扫描参数的 SSID 列表决定了 Probe Request 中携带的 SSID。构建优先级如下:
手动扫描: 用 scan_req 中的 SSID 列表↓ (如果没有手动请求)P2P 场景: 特定 SSID + BSSID(如 WPS provisioning)↓ (如果不是 P2P)通用场景: 遍历 wpa_s->conf->ssid 链表└── 加入 scan_ssid=1 且未禁用的网络 SSID└── max_ssids==1 时交替 wildcard 和具体 SSID
源码中 SSID 链表遍历的实际代码(wpa_supplicant_scan() 内,第 1213-1290 行区域):
// scan.c: wpa_supplicant_scan() — SSID 列表构建的核心逻辑/* 找到上次扫描的断点,从这里继续往下遍历 */ssid = wpa_s->conf->ssid;if (wpa_s->prev_scan_ssid != WILDCARD_SSID_SCAN) {while (ssid) {if (ssid == wpa_s->prev_scan_ssid) {ssid = ssid->next; // 从上次断点的下一个开始break;}ssid = ssid->next;}}while (ssid && params.num_ssids < max_ssids) {if (!wpas_network_disabled(wpa_s, ssid) && ssid->scan_ssid) {// 只有 scan_ssid=1 且未禁用的网络才加入扫描列表os_memcpy(params.ssids[params.num_ssids].ssid,ssid->ssid, ssid->ssid_len);params.ssids[params.num_ssids].ssid_len = ssid->ssid_len;params.num_ssids++;}ssid = ssid->next;}
主要功能:
•断点续扫:prev_scan_ssid 记录上次扫描到哪个 SSID,本次从下一个开始——这样不会每次都从链表头扫,节省时间•只扫需要主动探测的网络:scan_ssid=1 的才加入(隐藏网络),普通广播 SSID 的网络用 wildcard 一次扫出•max_ssids 限制:受 wpa_s->max_scan_ssids(驱动能力上限)和 WPAS_MAX_SCAN_SSIDS(supplicant 硬上限 16)双重约束
为什么 max_ssids==1 时要交替? 如果驱动只支持一次扫描携带一个 SSID(如某些老驱动),Supplicant 会交错发送 wildcard SSID(探测所有 AP)和具体 SSID(探测隐藏网络)——这样既不会错过一般 AP,也能发现隐藏网络。
2.3 频率列表构建——在哪扫?
频率(信道)的构建是 wpa_supplicant_scan() 中最精细的部分。以下 7 级是通用场景的优先级;P2P/WPS 场景下 wpa_supplicant_optimize_freqs() 会在所有级别之前预设频率(详见 2.2 节 P2P 场景)。优先级从高到低:
scan.c:1377-1409) | ||
manual_scan_freqs | wpa_s->last_scan_req == MANUAL_SCAN_REQ && wpa_s->manual_scan_freqs | |
select_network_scan_freqs | wpa_s->select_network_scan_freqs | |
next_scan_freqs | ||
setband_scan_freqs | wpa_setband_scan_freqs() | |
initial_freq_listfreq_list | INITIAL_SCAN_REQinitial_freq_list,否则用 freq_list | |
scan_cur_freq | wpa_s->conf->scan_cur_freq | |
params.freqs,驱动扫全部支持的信道 |
// scan.c: wpa_supplicant_scan() — 频率来源的实际代码if (wpa_s->last_scan_req == MANUAL_SCAN_REQ && params.freqs == NULL &&wpa_s->manual_scan_freqs) {params.freqs = wpa_s->manual_scan_freqs; // ① 手动指定的频率wpa_s->manual_scan_freqs = NULL; // 用完即清,防止下次复用}if (params.freqs == NULL && wpa_s->select_network_scan_freqs) {params.freqs = wpa_s->select_network_scan_freqs; // ② 当前网络配置的频率wpa_s->select_network_scan_freqs = NULL;}if (params.freqs == NULL && wpa_s->next_scan_freqs) {params.freqs = wpa_s->next_scan_freqs; // ③ BTM 漫游候选频率}wpa_s->next_scan_freqs = NULL;wpa_setband_scan_freqs(wpa_s, ¶ms); // ④ 频段限制// ⑤ 启动扫描时用 initial_freq_list,后续用 freq_listif (wpa_s->last_scan_req == INITIAL_SCAN_REQ &&wpa_s->conf->initial_freq_list && !params.freqs) {int_array_concat(¶ms.freqs, wpa_s->conf->initial_freq_list);} else if (wpa_s->conf->freq_list && !params.freqs) {int_array_concat(¶ms.freqs, wpa_s->conf->freq_list);}// 如果以上全部为空 → params.freqs = NULL → 驱动扫描全部支持的信道
主要功能:
•链式 fallthrough:每个 if 都检查 params.freqs == NULL——一旦前面设置了频率就不再覆盖•用完即清:manual_scan_freqs、select_network_scan_freqs 在赋值后置 NULL——防止下次扫描误复用•默认不设 = 全扫:如果所有来源都为空,params.freqs 保持 NULL,驱动默认扫全部支持的信道
2.4 IE 构建——带什么附加信息?
wpa_supplicant_extra_ies() 函数构建 Probe Request 中携带的额外信息元素(Information Element):
| MLD probe | ||
| Extended Capabilities | ||
| Interworking | ||
| WPS | ||
| P2P | ||
| MBO/OCE | ||
| Vendor Specific |
上表列出主要 IE 类别,完整列表还包括 Hotspot 2.0(
wpas_hs20_add_indication)、FST(wpa_s->fst_ies)、Mesh(wpa_supplicant_mesh_add_scan_ie)等按编译配置和运行状态条件添加的 IE。
这些 IE 决定了 AP 回复的 Probe Response 中会包含哪些能力信息——可以理解为 STA 在 Probe Request 中说的「我会这些,请告诉我你对应支持什么」。
2.5 MAC 地址随机化
扫描时的 MAC 随机化策略在 wpas_trigger_scan_cb() 中生效:
if ((wpa_s->mac_addr_rand_enable & MAC_ADDR_RAND_SCAN) &&wpa_s->wpa_state <= WPA_SCANNING)wpa_setup_mac_addr_rand_params(params, wpa_s->mac_addr_scan);
关键判断:wpa_state <= WPA_SCANNING——已连接状态下扫描不使用随机 MAC。这是因为已连接时的扫描通常是漫游场景(寻找同 ESS 的其他 AP),使用随机 MAC 会让 AP 不认识你,漫游就无法加速(无法复用 PMKSA 缓存)。
三、radio work — 为什么扫描要排队?
一句话总结:同一台射频同一时间只能干一件事。radio work 队列是 Supplicant 内部所有射频操作(扫描、连接、P2P 发现、offchannel 操作)的串行化机制——它确保不会有两个操作同时抢占射频。
回到雷达比喻——同一台雷达天线上,你不能同时执行两套扫描任务(两套的信道列表、dwell time、增益参数都不同)。radio work 队列就是来排队使用射频资源的。

3.1 数据结构
// external_wpa_supplicant_8/wpa_supplicant/wpa_supplicant_i.hstruct wpa_radio {char name[16]; // 射频名称(驱动提供)unsigned int num_active_works; // 当前激活的 work 数量struct dl_list ifaces; // 共享此射频的接口链表struct dl_list work; // work 队列(双向链表)struct wpa_supplicant *external_scan_req_interface; // 外部扫描请求接口(协调 Framework 扫描)};struct wpa_radio_work {struct dl_list list; // 链表节点unsigned int freq; // 操作频率(0 = 多信道,非 0 = 单信道)const char *type; // 类型标签:"scan"/"p2p-scan"/"connect"等struct wpa_supplicant *wpa_s;void (*cb)(struct wpa_radio_work *work, int deinit); // 回调函数void *ctx; // 回调的上下文(如 wpa_driver_scan_params)unsigned int started:1; // 是否已开始执行struct os_reltime time; // 入队时间unsigned int bands; // 涉及的频段};
关键字段:
•freq = 0 表示多信道操作(如扫描),不锁定特定信道——只要驱动不支持 OFFCHANNEL_SIMULTANEOUS,它就和所有其他 work 互斥•freq != 0 表示单信道操作(如 P2P listen 在某信道上),只阻塞同信道的其他 work•started 标志区分「排队中」和「执行中」——radio_work_done() 只在 started = 1 时触发下一个 work
3.2 radio_add_work() — 加入队列
// external_wpa_supplicant_8/wpa_supplicant/wpa_supplicant.cint radio_add_work(struct wpa_supplicant *wpa_s, unsigned int freq,const char *type, int next,void (*cb)(struct wpa_radio_work *work, int deinit),void *ctx){struct wpa_radio *radio = wpa_s->radio;struct wpa_radio_work *work;int was_empty;work = os_zalloc(sizeof(*work));work->freq = freq;work->type = type;work->wpa_s = wpa_s;work->cb = cb;work->ctx = ctx;if (freq)work->bands = wpas_freq_to_band(freq);else if (os_strcmp(type, "scan") == 0 || os_strcmp(type, "p2p-scan") == 0)work->bands = wpas_get_bands(wpa_s,((struct wpa_driver_scan_params *)ctx)->freqs);elsework->bands = wpas_get_bands(wpa_s, NULL);was_empty = dl_list_empty(&wpa_s->radio->work);if (next)dl_list_add(&wpa_s->radio->work, &work->list); // 插入队头elsedl_list_add_tail(&wpa_s->radio->work, &work->list); // 插入队尾if (was_empty) {/* 队列之前是空的——立即调度 */radio_work_check_next(wpa_s);} else if ((wpa_s->drv_flags & WPA_DRIVER_FLAGS_OFFCHANNEL_SIMULTANEOUS)&& radio->num_active_works < MAX_ACTIVE_WORKS) {/* 驱动支持并发 offchannel——尝试立即调度 */radio_work_check_next(wpa_s);}return 0;}
主要功能:
•next 参数控制优先级:next=1 插入队头(更高优先级),next=0 插入队尾•频段自动计算:扫描类型从 wpa_driver_scan_params 的 freqs 中推断频段,非扫描类型从全局配置推断•并发策略:队列为空时立即调度;如果驱动支持 OFFCHANNEL_SIMULTANEOUS 且激活数未满,即使有正在执行的 work 也尝试并发
3.3 单信道 vs 多信道的互斥规则
radio work 的互斥逻辑在 radio_work_check_next() 中实现:
OFFCHANNEL_SIMULTANEOUS) | ||
设计要点:扫描是 freq=0 的 work——它需要独占射频。这就是为什么扫描过程中不能同时做 P2P 发现或发起连接——它们的 work 都在队列里等着扫描完成。
3.4 radio_work_done() — 完成并触发下一个
// external_wpa_supplicant_8/wpa_supplicant/wpa_supplicant.cvoid radio_work_done(struct wpa_radio_work *work){struct wpa_supplicant *wpa_s = work->wpa_s;unsigned int started = work->started;radio_work_free(work); // 从队列中移除并释放内存if (started)radio_work_check_next(wpa_s); // 只有已启动的 work 完成时才触发下一个}
关键行为:radio_work_done() 只在 started=1 时触发 radio_work_check_next()——如果一个 work 还在排队就被取消(如 radio_remove_works() 调用 cb(work, 1) → deinit),它不会触发下一个 work。这防止了「取消的 work 意外唤醒下一个」的问题。
3.5 radio_remove_works() — 取消扫描
// external_wpa_supplicant_8/wpa_supplicant/wpa_supplicant.cvoid radio_remove_works(struct wpa_supplicant *wpa_s,const char *type, int remove_all){struct wpa_radio_work *work, *tmp;struct wpa_radio *radio = wpa_s->radio;dl_list_for_each_safe(work, tmp, &radio->work, struct wpa_radio_work, list) {if (type && os_strcmp(type, work->type) != 0)continue;if (!remove_all && work->wpa_s != wpa_s)continue;work->cb(work, 1); // deinit=1 → 通知回调:这个 work 被取消了radio_work_free(work);}radio_work_check_next(wpa_s); // 取消后可能释放了射频,检查是否有下一个可用}
主要功能:
•按 type 过滤(如 "scan" 只取消扫描类型的 work)•remove_all=1 时跨接口取消(如禁用 radio 时清空所有排队操作)•回调传 deinit=1 让 work 的 callback 做清理(如释放 wpa_driver_scan_params)
四、wpas_trigger_scan_cb() — 实际下发扫描到驱动
回到雷达比喻:参数构建完成,就像雷达确定了扫描范围和模式。radio work 队列是任务调度器——现在轮到扫描任务执行了。
wpas_trigger_scan_cb()就是雷达操作员按下发射按钮的那一刻。
一句话总结:当 radio work 被调度到执行时,
wpas_trigger_scan_cb()被调用——它设置 MAC 随机化、调用wpa_drv_scan()下发扫描、处理失败重试。
// external_wpa_supplicant_8/wpa_supplicant/scan.c. 教学简化版——省略 log/wpa_msg 语句static void wpas_trigger_scan_cb(struct wpa_radio_work *work, int deinit){struct wpa_supplicant *wpa_s = work->wpa_s;struct wpa_driver_scan_params *params = work->ctx;int ret;if (deinit) {// 清理路径:radio work 被取消时走这里if (!work->started) {wpa_scan_free_params(params);return;}wpa_supplicant_notify_scanning(wpa_s, 0);wpas_notify_scan_done(wpa_s, 0);wpa_s->scan_work = NULL;return;}// MAC 随机化:仅在 wpa_state <= WPA_SCANNING 时启用if ((wpa_s->mac_addr_rand_enable & MAC_ADDR_RAND_SCAN) &&wpa_s->wpa_state <= WPA_SCANNING)wpa_setup_mac_addr_rand_params(params, wpa_s->mac_addr_scan);// 随机 MAC 分配失败 → 中止扫描(关键异常路径)if (wpas_update_random_addr_disassoc(wpa_s) < 0) {wpa_scan_free_params(params);radio_work_done(work);return;}if (wpa_s->clear_driver_scan_cache) {params->only_new_results = 1;}wpa_supplicant_notify_scanning(wpa_s, 1);ret = wpa_drv_scan(wpa_s, params);if (ret == 0)wpa_s->curr_scan_cookie = params->scan_cookie; // vendor scan cookie(QCA 平台用)wpa_scan_free_params(params);work->ctx = NULL;if (ret) {// 失败路径:检查是否需要重试int retry = wpa_s->last_scan_req != MANUAL_SCAN_REQ &&!wpa_s->beacon_rep_data.token;if (wpa_s->disconnected)retry = 0; // 已断连不重试if (ret == -EOPNOTSUPP)retry = 0; // 驱动不支持不重试// ...省略 wpa_msg SCAN_FAILED、状态恢复...wpa_supplicant_notify_scanning(wpa_s, 0);wpas_notify_scan_done(wpa_s, 0);radio_work_done(work);if (retry) {wpa_s->scan_req = wpa_s->last_scan_req;wpa_supplicant_req_scan(wpa_s, 1, 0); // 1 秒后重试} else if (wpa_s->scan_res_handler) {wpa_s->scan_res_handler = NULL; // 清理回调处理器}return;}// 成功路径:记录扫描状态(注意 scan_work 在函数末尾,不在 if(ret==0) 内)os_get_reltime(&wpa_s->scan_trigger_time);wpa_s->scan_runs++;wpa_s->normal_scans++;wpa_s->own_scan_requested = 1;wpa_s->clear_driver_scan_cache = 0;wpa_s->scan_work = work; // 追踪当前激活的扫描——再有请求来会被拒绝}
主要功能:
•两条路径:deinit=1 是清理路径(work 被取消),deinit=0 是正常执行路径•MAC 随机化在这里生效:在调用 wpa_drv_scan() 之前设置随机 MAC 参数•失败重试策略:非手动扫描 + 非 EOPNOTSUPP + 非断连状态时,1 秒后自动重试;不重试时清理 scan_res_handler•成功后记录 scan_work:scan_work = work 在函数最末尾(不在 if (ret == 0) 内)——只有成功下发扫描时才执行到这里(失败路径已提前 return),确保 scan_work 一定被正确设置
wpa_drv_scan() 是驱动接口的虚函数调用,对于 Linux 平台最终落到 driver_nl80211_scan()——它构建 NL80211_CMD_TRIGGER_SCAN 并发送到内核。这是上篇第五章中 wificond 层也调用的同一个内核命令——wificond 和 Supplicant 都可以发扫描命令,取决于 Android 版本和 HAL 层配置。
五、扫描结果怎么回来?—— EVENT_SCAN_RESULTS 的分发链
一句话总结:驱动完成扫描后通过 nl80211 multicast 通知 Supplicant,事件经过
wpa_supplicant_event()分发到_wpa_supplicant_event_scan_results(),然后走一条结果处理链:获取扫描结果 → 更新 BSS 缓存 → 判断是否做网络选择。
5.1 事件分发
驱动完成扫描后内核发送 NL80211_CMD_NEW_SCAN_RESULTS,Supplicant 的事件回调被触发:
内核 nl80211 multicast → do_process_drv_event()→ send_scan_event()→ wpa_supplicant_event(EVENT_SCAN_RESULTS, data)→ _wpa_supplicant_event_scan_results(wpa_s, data, own_request, update_only)
_wpa_supplicant_event_scan_results() 的处理优先级链(以下列出主要步骤,完整链路有 20+ 个判断节点):
1.自定义 scan_res_handler(如果有注册)→ 最高优先级,直接消费结果并返回2.AP 模式检查 —— AP/Mesh 模式下提前返回,不做 STA 网络选择3.external_scan 检查 —— 外部请求的扫描结果(如通过 ctrl_iface 触发)提前返回4.OBSS 扫描处理(802.11ax 重叠 BSS 扫描)5.Beacon Report 处理(802.11k 无线测量)6.ML link probe 处理(WiFi 7 多链路探测)7.ap_scan==2 + WPS 搜索中检查8.autoscan(自动扫描模块)9.断连检查(已断连 → 跳过网络选择)10.bgscan(后台扫描模块)11.WPS AP 信息更新12.WNM 处理(无线网络管理)13.认证中状态检查14.6GHz Short SSID match 检测15.WPS PBC Partner Link 检查(PBC 激活且配网扫描未完成 → 直接返回)16.6GHz 触发扫描(Short SSID 匹配后补扫 6GHz)17.radio_work_done() —— 释放射频工作队列槽位,触发下一个排队的 work18.最终落到wpas_select_network_from_last_scan()
5.2 获取扫描结果
在更新 BSS 缓存之前,先要从驱动/内核拉取原始扫描结果:
wpa_supplicant_get_scan_results()→ wpa_drv_get_scan_results()→ driver_nl80211_get_scan_results()→ NL80211_CMD_GET_SCAN(NLM_F_DUMP)→ 内核返回 BSS 列表
内核返回的每个 BSS 都是一个 wpa_scan_res 结构——包含 BSSID、频率、信号强度、TSF 时间戳、IE 数据。Supplicant 逐一提取后进入 BSS 缓存更新流程。
5.3 扫描超时保护
扫描超时不是 Supplicant 设置的——是驱动层在 wpa_driver_nl80211_scan() 中注册的:
// driver_nl80211_scan.c — wpa_driver_nl80211_scan() 中超时注册逻辑timeout = drv->uses_6ghz ? 20 : 10; // ① 先判 6GHz:是则 20sif (drv->uses_s1g)timeout += 5; // ② S1G 额外加 5sif (drv->scan_complete_events) {timeout = 30; // ③ completion events 最后无条件覆盖为 30s}eloop_cancel_timeout(wpa_driver_nl80211_scan_timeout, drv, bss->ctx);eloop_register_timeout(timeout, 0, wpa_driver_nl80211_scan_timeout,drv, bss->ctx);
超时处理函数会尝试 NL80211_CMD_ABORT_SCAN 中止扫描,然后仍去获取已完成的那部分结果——已经扫到的 AP 结果不会因超时而丢失。
六、BSS 缓存管理——Supplicant 的「雷达回波记忆」
一句话总结:Supplicant 维护了一个 BSS 缓存链表——每次扫描结果到达时,新 AP 被加入缓存,已知 AP 被更新信号和 IE,未再出现的 AP 按
scan_miss_count计数淘汰。

6.1 为什么需要 BSS 缓存?
设想你连着一个 AP,信号在 -55 dBm 到 -72 dBm 之间波动。如果没有 BSS 缓存,每次扫描结果到来时你只看当次的数据——你无法判断「这个 AP 的信号在变弱,可能需要漫游了」。BSS 缓存维护了跨多次扫描的 BSS 状态——信号变化趋势、IE 变更(比如 AP 升级了固件)、消失的 AP。
6.2 wpa_bss_update_scan_res() — 逐条更新
每一条扫描结果都会经过 wpa_bss_update_scan_res() 处理:
// external_wpa_supplicant_8/wpa_supplicant/bss.cvoid wpa_bss_update_scan_res(struct wpa_supplicant *wpa_s,struct wpa_scan_res *res,struct os_reltime *fetch_time){const u8 *ssid;struct wpa_bss *bss;// 陈旧结果过滤if (wpa_s->conf->ignore_old_scan_res) {// 如果 BSS 的时间戳早于 scan_trigger_time,跳过// ...}ssid = wpa_scan_get_ie(res, WLAN_EID_SSID);if (ssid == NULL || ssid[1] > SSID_MAX_LEN)return; // 无有效 SSID,跳过// P2P 接口的特殊过滤(跳过无 P2P IE 的结果)// ...// 在缓存中查找已有 entry(按 BSSID + SSID)bss = wpa_bss_get(wpa_s, res->bssid, ssid + 2, ssid[1]);if (bss == NULL)bss = wpa_bss_add(wpa_s, ssid + 2, ssid[1], res, fetch_time);elsebss = wpa_bss_update(wpa_s, bss, res, fetch_time);// 加入 last_scan_res 数组(动态扩容)if (wpa_s->last_scan_res_used >= wpa_s->last_scan_res_size) {// ...扩容(初始 32,翻倍增长)...}wpa_s->last_scan_res[wpa_s->last_scan_res_used++] = bss;}
主要功能:
•BSSID + SSID 联合查重:同一个 AP 的多个 SSID(Multi-BSSID 场景)会创建独立的缓存条目•wpa_bss_add() 做的事:分配 wpa_bss 结构 → 复制 IE 数据 → 解析 RSN/HT/VHT/HE/EHT/MLO IE → 加入链表末尾•wpa_bss_update() 做的事:比较 IE 变化 → 更新信号强度和频率 → 重置 scan_miss_count = 0 → 移到链表末尾•链表按最后更新时间排序——最新扫描到的 BSS 在链表尾部
6.3 BSS 过期淘汰机制
每次扫描结束后,wpa_bss_update_end() 遍历整个 BSS 链表,对未在本轮扫描中出现的 BSS 递增 scan_miss_count:
// external_wpa_supplicant_8/wpa_supplicant/bss.cvoid wpa_bss_update_end(struct wpa_supplicant *wpa_s, struct scan_info *info,int new_scan){struct wpa_bss *bss, *n;if ((info && info->aborted) || !new_scan)return; // 扫描被中止或不完整,不淘汰dl_list_for_each_safe(bss, n, &wpa_s->bss, struct wpa_bss, list) {if (wpa_bss_in_use(wpa_s, bss))continue; // 正在使用的 BSS 不淘汰if (!wpa_bss_included_in_scan(bss, info))continue; // 🔑 BSS 的频率或 SSID 不在本轮扫描范围内——不淘汰if (bss->last_update_idx < wpa_s->bss_update_idx)bss->scan_miss_count++;if (bss->scan_miss_count >=wpa_s->conf->bss_expiration_scan_count) {wpa_bss_remove(wpa_s, bss, "no match in scan");}}}
淘汰规则表:
wpa_bss_included_in_scan() | ||
scan_miss_count >= bss_expiration_scan_count | ||
wpa_bss_in_use() | current_bss(已连接)→ ml_connect_probe_bss(MLO 链路探测)→ wnm_target_bss(WNM 漫游目标,在 #ifdef CONFIG_WNM 下)→ SSID guard(不同 SSID 则跳过后续检查)→ bssid/pending_bssid 比较 → MLO link BSS。注意:SSID guard 是 early-return——若 BSS 的 SSID 与 current_bss 不同,直接返回 0(不在使用中),后续的 BSSID/MLO 检查不会执行。这防止不同网络中恰好 BSSID 相同的 BSS 被误保护 | |
aborted) |
6.4 BSS 生命周期总结
新 AP 首次出现:→ wpa_bss_add() → scan_miss_count = 0 → 加入链表末尾已知 AP 再次出现:→ wpa_bss_update() → 更新信号/IE → scan_miss_count = 0 → 移到链表末尾已知 AP 本轮未出现:→ scan_miss_count++scan_miss_count 超过阈值:→ wpa_bss_remove() → 从链表移除 → 释放内存特殊保护:current_bss / wnm_target_bss → 不淘汰(即使 scan_miss_count 很高)
七、扫描结果怎么变成连接决策?——网络选择收口
一句话总结:扫描的目的是连接。
wpas_select_network_from_last_scan()遍历所有已启用网络配置,用 BSS 缓存中的最佳匹配来决定是否发起连接。
这一节只做收口——网络选择的完整逻辑(WifiNetworkSelector 评分算法、自动连接策略)是 06 章连接的核心内容。这里只展示扫描结果怎么流向连接决策:
// 概念示意图——以下是逻辑简化版,非真实源码行,真实实现在 wpa_supplicant_pick_network() 中// 实际调用链: _wpa_supplicant_event_scan_results() → wpa_supplicant_pick_network()// → wpa_supplicant_select_bss() → wpa_supplicant_connect()//// 逻辑要点(本节仅做收口,06 章展开):// 1. 遍历所有已启用网络配置(按 priority 分层,不是简单链表遍历)// 2. wpa_supplicant_select_bss() 在 BSS 缓存中匹配多要素(SSID/key_mgmt/加密方式/频率,详见 06 章)// 3. wpa_scan_result_compar() 排序:WPA/WPA2 支持 > Privacy > SAE 优先(有条件)> est_throughput > 频段偏好 > SNR// 4. 找到最佳 BSS → wpa_supplicant_connect() → 进入连接流程
关键点:
•wpa_supplicant_select_bss() 匹配多要素:SSID、key_mgmt(WPA2/WPA3)、加密方式(pairwise_cipher 如 CCMP/GCMP)、频率在当前信道列表中——具体匹配逻辑委托给 wpa_scan_res_match()(详见 06 章连接篇)•wpa_scan_result_compar() 的排序优先级(从高到低):① WPA/WPA2 支持有 RSN IE 的 AP 优先于无 RSN IE 的;② Privacy(加密能力)优先于 Open;③ SAE 认证的 AP 在 SNR 足够好时(≥ GREAT_SNR 或不低于对方)优先于 PSK——低 SNR 时不保证 WPA3 优先;④ SNR 差异 < 7dB 时比 est_throughput(6GHz 网络无条件触发 est_throughput 比较,因为 LPI/VLP 功率规则导致 SNR 不可直接跨频段比较);⑤ 频段偏好(6GHz > 5GHz > 2.4GHz,SNR 差异 < 5dB 时生效);⑥ 最终 fallback 到 SNR 全值比较•找到最佳 BSS 后立即发起连接——supplicant 的 bgscan 会主动漫游,不需要 Framework 的指令
八、总结
8.1 Supplicant 内部扫描的完整时间线
1. 外部请求到达(Framework / eloop 定时器)→ wpa_supplicant_req_scan(sec, usec)→ eloop_register_timeout → 排队2. eloop 超时到期→ wpa_supplicant_scan()├── 前置检查(10+ 种不扫描的情况)├── 构建 SSID 列表├── 构建频率列表(7 级优先级)├── 构建额外 IE└── wpa_supplicant_trigger_scan()→ radio_add_work("scan", wpas_trigger_scan_cb)3. radio work 被调度执行→ wpas_trigger_scan_cb()├── MAC 随机化设置├── wpa_drv_scan() → driver_nl80211_scan()│ → NL80211_CMD_TRIGGER_SCAN → 内核└── eloop_register_timeout(timeout, scan_timeout) // 10s/20s/30s,见 §5.34. 内核报告 NL80211_CMD_NEW_SCAN_RESULTS→ wpa_supplicant_event(EVENT_SCAN_RESULTS)├── 取消 10 秒超时├── wpa_drv_get_scan_results()├── wpa_bss_update_scan_res() × N次│ └── wpa_bss_add() / wpa_bss_update()├── wpa_bss_update_end()│ └── 清理过期 BSS├── radio_work_done()└── wpas_select_network_from_last_scan()└── 如果找到匹配 → wpa_supplicant_associate()
8.2 关键模块职责
| eloop 调度 | wpa_supplicant_req_scan() | |
| 参数构建 | wpa_supplicant_scan() | |
| 射频排队 | radio_add_work()radio_work_done() | |
| 扫描下发 | wpas_trigger_scan_cb() | wpa_drv_scan() + 失败重试 |
| 事件分发 | _wpa_supplicant_event_scan_results() | |
| 缓存管理 | wpa_bss_update_scan_res()wpa_bss_update_end() | |
| 网络选择 | wpas_select_network_from_last_scan() |
8.3 关键设计决策
•异步调度而非直接调用:扫描通过 eloop 定时器触发,允许去抖动和优先级管理•radio work 串行化:不是 Supplicant 故意慢,而是射频物理上的单工性质决定的——同一时间只能在一个信道上操作•BSS 缓存跨扫描持久化:scan_miss_count 机制让缓存自动淘汰消失的 AP,同时保护正在使用的 BSS•扫描失败有重试:非手动扫描失败后自动 1 秒重试——用户不需要感知底层的偶发失败
回到雷达比喻:整个 supplicant 扫描引擎就像雷达的自动维护程序——自己决定何时扫、扫什么、结果怎么处理,而 Framework 只负责告诉它"我需要看什么"。radio work 是任务调度器,BSS 缓存是雷达回波记忆,eloop 是心跳时钟。三者协作,让这台雷达在无人值守时也能保持对环境的感知。
References
[1] external/wpa_supplicant_8: https://android.googlesource.com/platform/external/wpa_supplicant_8/
夜雨聆风