WiFi 打开(三)Supplicant 启动
驱动加载完成、芯片就绪后,wpa_supplicant 是怎么启动的?eloop 事件循环是怎么工作的?它的核心数据结构、状态机、AIDL 接口层又是怎么设计的?
上篇回顾:在 Framework 与 HAL 篇中,我们追踪了从用户点击开关到 HAL 就绪的完整链路——ActiveModeWarden 调度、WifiController 状态机、HalDeviceManager 启动、厂商私有 HAL 加载。本篇继续往下走:HAL 就绪后,Supplicant 怎么启动?
¶ 本章导读
Supplicant 是餐厅的前台接待——就位后通过对讲机(eloop 事件循环)随时响应厨房(内核)和客人(Java Framework)的消息。接待台上的工作手册记录了所有接口、网络、密钥的状态。
你将学到
- wpa_supplicant 的五大核心数据结构:
wpa_global、wpa_supplicant、wpa_radio、wpa_radio_work、wpa_ssid - 状态机:10 个状态的枚举定义、
wpa_supplicant_set_state()的 5 阶段执行 - 事件驱动架构:
wpa_supplicant_event()的分发逻辑(switch 30+ 事件类型) - AIDL 三层接口:ISupplicant / ISupplicantStaIface / ISupplicantStaNetwork
- 四大设计模式:虚函数表、观察者、Work Queue、状态保存恢复
- Supplicant 启动的完整时间线(每一步的典型耗时和变量分析)
代码说明:本文代码来自真实源码,有精简(去掉 log 语句和非核心错误处理),关键路径保留完整调用链。精简处标注 // ...省略...。
系列导航:本篇聚焦 Supplicant 启动与内部架构。上篇聚焦驱动加载与 SSR 崩溃恢复。
全篇架构图:wpa_supplicant 软件分层(从 Java Framework 到内核驱动) —— 自上而下五层:Java Framework(SupplicantStaIfaceHalAidlImpl / WifiMonitor)→ binder/AIDL → wpa_supplicant(eloop +
wpa_supplicant_event()分发)→ nl80211 netlink → 内核 cfg80211 与驱动。本章逐层展开时,随时可以回到这张图定位当前所处的那一层。
¶1 核心数据结构:Supplicant 的「五脏六腑」
在看启动流程之前,必须先认识 Supplicant 的核心数据结构。不理解这些结构体就直接看启动代码,就像不看地图就走进一座大楼——你会迷路。
¶1.1 struct wpa_global —— 全局单例,所有接口共享
wpa_global 是整个 Supplicant 进程的根对象。全局只有一个——wpa_supplicant_init() 分配它,wpa_supplicant_deinit() 释放它。所有接口(wlan0 / p2p0 等)共享同一个 wpa_global。
1 | // external_wpa_supplicant_8/wpa_supplicant/wpa_supplicant_i.h:283-324 |
- ifaces 是单链表:通过
wpa_supplicant.next链接。遍历所有接口只需for (wpa_s = global->ifaces; wpa_s; wpa_s = wpa_s->next)。 - P2P 字段占了近一半:P2P 的复杂状态(正在建组、邀请中、GO 等待客户端等)全部存储在
wpa_global级别,因为 P2P 操作跨越多个接口。 - drv_priv 是数组:
drv_count是已初始化的驱动 wrapper 数量(如 nl80211、wext)。每个驱动的私有数据通过global->drv_priv[i]访问。
¶1.2 struct wpa_supplicant —— 每接口状态,Supplicant 的灵魂
这是整个代码库中最大的结构体——约 600 个字段,分布在 15 个功能分组中。以下从源码中提取核心字段并按功能分组展示:
1 | // external_wpa_supplicant_8/wpa_supplicant/wpa_supplicant_i.h:697-1150+ |
这 170 行字段不必逐行背诵——扫读时抓住三条主线即可:身份与层级(global / radio / parent 挂在哪个链表上)、当前连接(bssid / current_ssid / assoc_freq 连到了哪个 AP)、安全与扫描状态(wpa / eapol / scan_* 走到了哪一步)。下面三条 bullet 挑出其中最值得记住的设计点。
- 字段数惊人,但有规律:所有
struct wpa_supplicant字段都围绕一个核心问题——「这个 WiFi 接口现在处于什么状态?」。状态信息是最密集的:当前连接的 BSSID、SSID、频率、密钥、加密套件、扫描结果缓存…… - 按功能域分群:
bssid/current_ssid/assoc_freq是一组(当前连接);pairwise_cipher/group_cipher/key_mgmt/wpa_proto是一组(安全协商结果);scan_*字段是一组(扫描状态);driver/drv_priv/drv_flags是一组(驱动接口)。 - 为什么不是子结构体? 源码中除了 SME(
wpa_s->sme)和 MLO links 外,其他字段都平铺在wpa_supplicant中。这是一种历史遗留的设计选择——Supplicant 从 2003 年起持续演进,字段随时间累积。重构为子结构体需要改动数百处引用,风险远大于收益。
¶1.3 struct wpa_radio + struct wpa_radio_work —— 射频的「任务队列」
1 | // external_wpa_supplicant_8/wpa_supplicant/wpa_supplicant_i.h:334-370 |
- radio 和 interface 是 1:N 关系:一个物理 WiFi 芯片对应一个
wpa_radio,多个虚拟接口(wlan0 → STA、p2p0 → P2P)共享同一个 radio。这保证了射频操作的互斥——你不会同时在两个接口上做 offchannel 操作。 - work 队列是「任务调度器」:当一个接口需要独占射频(如扫描、连接),它通过
radio_add_work()向队列提交一个 work item。如果有并发能力(WPA_DRIVER_FLAGS_OFFCHANNEL_SIMULTANEOUS),最多允许MAX_ACTIVE_WORKS(2)个 work 同时运行。
¶1.4 struct wpa_ssid —— 网络配置,Supplicant 的「常客档案」
wpa_ssid 存储一个已保存网络的完整配置。每个 wpa_supplicant 接口通过 wpa_s->conf->ssid 链表管理多个 wpa_ssid(每个对应一个用户保存过的 WiFi 网络)。核心字段包括:
1 | // external_wpa_supplicant_8/wpa_supplicant/config_ssid.h(精简) |
- 一对多关系:一个接口可以保存多个网络(如家里 WiFi + 公司 WiFi + 咖啡店 WiFi),每个对应一个
wpa_ssid节点。wpa_ssid.next形成单链表。 - 密码存储:Android 上
passphrase由 WiFiService 通过 AIDL 传入(Keystore 解密后),Supplicant 内部通过 PBKDF2 派生 PMK 存入psk字段。 disabled标志:用户在 Settings 中关闭某个已保存网络时,对应的wpa_ssid.disabled被置为 1,Supplicant 跳过该网络。disabled_for_connect是 WPS 连接期间被临时禁用的网络,WPS 连接成功或失败后自动恢复。
¶1.5 数据结构关系速查
| 结构体 | 数量 | 拥有者 | 核心职责 |
|---|---|---|---|
wpa_global | 1 个 / 进程 | 进程唯一 | 全局参数、EAP 方法注册、P2P 全局状态、接口链表 |
wpa_supplicant | 1 个 / 接口 | global->ifaces 链表 | 接口的状态机、连接信息、扫描缓存、密钥、EAPOL |
wpa_radio | 1 个 / 物理芯片 | wpa_s->radio 指向 | 射频互斥、work 队列、并发控制 |
wpa_radio_work | N 个 / radio | radio->work 链表 | 射频操作的序列化(扫描/连接/P2P 发现) |
wpa_ssid | N 个 / 接口 | wpa_s->conf->ssid 链表 | 已保存网络的配置(SSID/密码/安全模式等) |
用餐厅的比喻来总结这五层结构:wpa_global 是营业执照,整个餐厅只有一张;wpa_radio 是厨房,一个物理空间所有服务员共用;wpa_supplicant 是每个服务员的工作站,每个接口一个;wpa_radio_work 是厨房任务单——扫描是「出去看看外面有什么客人」,连接是「招待这位客人就座」;wpa_ssid 是常客档案,记录每个回头客的口味偏好。
¶ 二、wpa_supplicant 怎么启动的?eloop + AIDL 的细节
启动机制:
main()是怎么被调起来的?(rc 文件 + HIDL lazy HAL + init 服务启动)已在上一篇《Framework 与 HAL》第九章详细说明。本章聚焦main()之后的内部初始化。
¶2.1 main() —— 四步启动
1 | // external_wpa_supplicant_8/wpa_supplicant/main.c |
前两步是基础设施搭建。os_program_init() 是平台相关的——在 Android 上它执行权限降级(切换到 wifi 用户,保留 CAP_NET_ADMIN 和 CAP_NET_RAW 两个能力用于操作 netlink socket),然后初始化随机数种子供后续 EAP 密钥协商使用。wpa_supplicant_init() 创建全局根对象 wpa_global,内部调用 eloop_init() 创建 epoll fd 并注册所有 EAP 方法(PEAP、TLS、TTLS、SIM、AKA 等数十种)。
1 | // ===== 第三步:为每个接口调用 add_iface ===== |
后两步启动接口并进入事件循环。wpa_supplicant_add_iface() 是每个 WiFi 接口的完整初始化入口(详见 2.3 节)。wpa_supplicant_run() 调用 eloop_run() 进入无限循环——进程从此不再主动返回,所有操作都是被动响应事件(内核 netlink 消息、binder 请求、timer 超时)。清理代码只有在进程终止时才执行到。
异常路径则是一刀切:三步任一步失败(os_program_init() 非 0、wpa_supplicant_init() 返回 NULL、wpa_supplicant_add_iface() 返回 NULL)都 return -1 退出,由 init 按 restart 策略重新拉起进程。
主要功能:
os_program_init()是平台相关的初始化。在 Android 上,它执行权限降级和安全设置:通过setgroups()设置附属组(wifi/inet/keystore/log),通过setgid()/setuid()将进程切换到wifi用户身份,通过prctl(PR_SET_KEEPCAPS)+capset()保留CAP_NET_ADMIN和CAP_NET_RAW两项能力(操作 netlink socket 和管理网络接口所必需),最后通过srandom()初始化随机数种子供后续 EAP 密钥协商使用。wpa_supplicant_init()创建struct wpa_global——这是整个 Supplicant 进程的全局根对象。内部调用eloop_init()创建 epoll fd,调用eap_register_methods()注册所有 EAP 方法(PEAP、TLS、TTLS、SIM、AKA 等数十种),注册全局 ctrl interface(wpa_cli 的全局级控制通道)。wpa_supplicant_add_iface()是每个 WiFi 接口的初始化入口。后面详细展开。wpa_supplicant_run()调用eloop_run()进入无限循环。进程从此不再主动返回——所有操作都是被动响应事件(内核 netlink 消息、binder 请求、timer 超时)。
把这四步串起来,Supplicant 的启动是一条「逐级递进」的链路:OS 层先降权、全局层再搭底座、接口层做最重的初始化、最后进入事件循环。下表给出每一步的典型耗时与影响变量——耗时是量级估计(无逐设备实测),变量是决定这一步快慢的关键因素。
| 步骤 | 典型耗时 | 影响变量 | 关键动作 |
|---|---|---|---|
os_program_init() | <1 ms | 无(固定 syscall 序列) | 降权到 wifi 用户、保留 CAP_NET_ADMIN/CAP_NET_RAW、初始化随机种子 |
wpa_supplicant_init() | 1~5 ms | EAP 方法注册数(数十种,基本固定)、/dev/urandom 读取延迟 | 分配 wpa_global、eloop_init() 建 epoll fd、eap_register_methods() 注册 EAP 方法 |
wpa_supplicant_add_iface() | 10~50 ms | 驱动能力探测的 netlink 往返次数、已保存网络数 | 开 nl80211 socket、查硬件特性/能力、初始化 WPA/EAPOL 状态机、WPS/DPP/P2P |
wpa_supplicant_run() | 无限(阻塞) | daemonize(决定 AIDL 注册走同步还是 lazy HAL 路径,见 2.5 节) | 进入 eloop_run(),此后只被动响应事件 |
时间线的关键洞察是:启动耗时由第三步 add_iface 主导。前三步加起来通常只有几十毫秒,而 wpa_supplicant_add_iface() 独占其中大头——它要打开 nl80211 socket,再通过 wpa_drv_get_hw_feature_data()、wpa_drv_get_capa() 向内核 / 驱动发起多次能力探测,每一次都是用户态↔内核态的 netlink 往返。相比之下,os_program_init() 和 wpa_supplicant_init() 只是本地 syscall 和内存分配,几乎可以忽略。daemonize 则决定 AIDL 注册落在启动链路的哪个位置:daemonize=true 时 wpas_aidl_init() 在进入 eloop_run() 前同步完成;Android 默认 daemonize=false 走 lazy HAL 路径,wpas_aidl_init() 反而更早——在 wpa_supplicant_init() 内同步完成,真正被推迟到 ServiceManager 首次触发的是进程启动本身(详见 2.5 节)。
¶2.2 wpa_supplicant_init() 内部:初始化了什么?
1 | // wpa_supplicant/wpa_supplicant.c |
步骤 1-5 搭建了 Supplicant 的两个核心支柱:EAP 方法表和对讲机底座(eloop)。eap_register_methods() 注册所有 EAP 方法——每种方法提供 struct eap_method(包含 init/process/getKey 等函数指针),eap_register_methods() 将它们组织成方法查找表。eloop_init() 创建 epoll fd——这是 Supplicant 整个事件驱动架构的基石。
IO 多路复用:Supplicant 需要同时监听多个 I/O 源(binder fd、nl80211 fd、EAPOL fd、ctrl_iface fd、timer fd),但它是单线程的。如果用阻塞 read () 逐一等待,一个 fd 没数据时整个线程就会卡住,其他 fd 的事件也处理不了。IO 多路复用(Linux 上是 epoll)解决了这个问题——它让一个线程同时 "盯着" 所有 fd,任何一个有数据就绪时 epoll_wait () 立即返回,告诉你是哪些 fd 触发了事件。这就是为什么 Supplicant 能用单线程处理所有 I/O:epoll 帮它实现了 "多路监听、单线程处理"。epoll 之前的 select/poll 也能做同样的事,但 epoll 在 fd 数量多时性能更好(O (1) 事件通知 vs O (n) 扫描)。
1 | // 6. 初始化随机数生成器 |
步骤 6-11 激活外围功能。random_init() 从 /dev/urandom 获取高质量随机数——这对 EAP 密钥协商至关重要。wpa_supplicant_global_ctrl_iface_init() 创建全局级 Unix domain socket,它接受 wpa_cli 的连接,支持 INTERFACE_ADD、INTERFACE_REMOVE、SAVE_CONFIG 等全局级命令。步骤 11 的 eloop_register_timeout() 注册了 Supplicant 的第一个 timer——一个周期性清理任务,定期释放过期的 BSS 缓存、检查配置文件更新、清理过期 PMKSA 缓存。
¶2.3 wpa_supplicant_init_iface() —— 一个接口的完整初始化
这是 Supplicant 中最长、最复杂的函数之一(约 700 行),它把一个 WiFi 接口从「空白状态」初始化为「可工作状态」:
1 | // wpa_supplicant/wpa_supplicant.c |
第一阶段小结:步骤 1-4 完成的是「地基」工作。wpa_config_read() 读取网络配置——在 Android 上这不是静态文件,而是由 WiFiService 通过 AIDL 动态写入(密码由 Keystore 解密后传入)。步骤 4 的 wpas_init_driver() 是整个初始化的关键转折点:它选择 nl80211 驱动 wrapper,打开与内核 cfg80211 的 netlink socket,就像前台接待插上了对讲机的电源线——从此刻起,Supplicant 可以向内核发送 NL80211_CMD_* 命令并接收事件。
1 | // ===== 第二阶段:能力探测与 WPA 初始化(步骤 5-8) ===== |
第二阶段小结:驱动连通后,步骤 5-8 探测硬件能力并初始化安全核心。wpa_supplicant_init_wpa() 建立 WPA/WPA2/WPA3 的状态机框架——这是后续四次握手和组密钥握手的执行引擎。两个 get_* 调用向驱动查询硬件能力:最多同时扫描几个 SSID、offchannel 最长能待多久——这些值直接影响后续扫描策略。wpa_supplicant_driver_init() 完成最后的驱动配置:获取 MAC 地址、初始化 L2 packet socket(用于收发 EAPOL 帧)、清除所有密钥残留、启用已保存的网络配置。
1 | // ===== 第三阶段:高级功能与对外接口(步骤 9-12) ===== |
第三阶段小结:地基和能力都确认后,步骤 9-12 激活高级功能并建立对外接口——接待台的工作手册填写完毕,对讲机频道全部开通。WPS 和 DPP 提供零配置配网能力(WPS 用 PIN/PBC,DPP 用 QR 码),EAPOL 状态机进入就绪状态。wpa_supplicant_ctrl_iface_init() 创建 per-interface 的 Unix domain socket(路径如 /data/vendor/wifi/wpa/sockets/wlan0),wpa_cli 通过它发送 SCAN、LIST_NETWORKS、STATUS 等命令。GAS query 为 Hotspot 2.0 的 ANQP 查询做准备,最后 P2P 初始化使接口具备 Wi-Fi Direct 能力。
¶2.4 eloop 事件循环 —— 为什么 wpa_supplicant 不需要多线程?
wpa_supplicant 的核心架构设计哲学是:单进程、单线程、事件驱动。所有 I/O 操作都通过 eloop 统一调度:
1 | // src/utils/eloop.c:163-192 |
eloop_init() 的核心操作只有两步:创建 epoll fd(epoll_create1(0)),初始化三个 socket table(readers / writers / exceptions)。它不做任何 fd 注册——那由各个模块在初始化时调用 eloop_register_read_sock() 完成。可以这样理解:eloop 是一个对讲机底座,eloop_init() 给底座通了电,但各个频道(fd)要由使用方自己插上去。
1 | // src/utils/eloop.c:1071-1253(行号对应原始含条件编译的源码,精简为 epoll 路径后仅作参考) |
等待阶段的精髓在于 epoll_wait 的 timeout_ms 参数。eloop 不是死循环空转——它通过计算最近一个 timer 的到期时间,让 epoll_wait 精确阻塞到那个时刻。如果没有任何 fd 注册也没有 timer,while 循环条件不满足,循环自然退出。这个设计意味着进程在空闲时几乎不消耗 CPU:epoll_wait 会让进程进入内核的 TASK_INTERRUPTIBLE 睡眠状态,直到有事件到来才被唤醒。
1 | // 5. 清除 changed 标志 + 处理待决信号 |
分发阶段处理两类唤醒源:timer 到期(步骤 6)和 I/O 事件就绪(步骤 9)。值得注意的细节是步骤 8——如果在 timer callback 中修改了 socket 表(比如注册了一个新的 fd),eloop 会 continue 重试整个循环,而不是继续分发旧的事件列表,避免了在已被修改的数据结构上操作。步骤 7 的 continue 则跳过了 I/O 分发:如果 epoll_wait 只是因 timeout 返回(没有任何 fd 就绪),处理完 timer 就重新进入等待,无需遍历 socket table。
图 2.1:eloop 事件循环架构 —— epoll fd 居中调度,四周连接的 fd(nl80211 / ctrl / EAPOL / binder)各自注册对应的 callback,右侧 timer 链表按到期时间排序。
主要功能:
- eloop 的本质是一个「事件分发器」:注册 fd + callback,当 fd 可读/可写/异常时调用 callback。加上 timer 机制(到期后调用 callback)。所有异步操作都转换为「注册一个 callback 等待某个 fd 变为可读」。
- 注册到 eloop 的 fd 包括:
- nl80211 netlink socket:来自内核 cfg80211 的事件(扫描结果到达、关联状态变更、断开通知等)。对应的 callback 是
wpa_driver_nl80211_event_receive()。 - per-interface ctrl socket:来自
wpa_cli的命令。对应的 callback 是wpa_supplicant_ctrl_iface_receive()。 - EAPOL L2 packet socket:来自 AP 的 EAPOL 帧(四次握手的关键帧)。对应的 callback 是
wpa_supplicant_rx_eapol()。 - AIDL binder fd:来自 Java Framework 的 binder 请求。对应的 callback 是
wpas_aidl_sock_handler()。 - DHCP packet socket:DHCP 响应包(如果 Supplicant 负责 DHCP)。
- nl80211 netlink socket:来自内核 cfg80211 的事件(扫描结果到达、关联状态变更、断开通知等)。对应的 callback 是
- 单线程模型避免了锁竞争,代码简单可靠。代价是每个 callback 必须快速返回、不能执行阻塞 I/O。如果有耗时操作(如完整的 TLS 握手涉及多次往返),需要将其拆分为多个异步状态并通过 eloop 的 timeout 机制驱动状态机。
¶2.5 AIDL 注册:Supplicant 怎么和 Java Framework 对接?
在 wpa_supplicant_run() 中(实际在进入 eloop 循环之前),如果编译了 CONFIG_AIDL,会检查 params.daemonize 标志。当 daemonize=true 时(后台守护进程模式),同步调用 wpas_aidl_init() 完成 AIDL 注册。在 Android 上,wpa_supplicant 由 init 启动时 daemonize 通常为 false,AIDL 注册走的是 lazy HAL 路径——由 ServiceManager.waitForDeclaredService() 在 Supplicant 外部触发。无论哪条路径,最终都会调用 wpas_aidl_init():
1 | // wpa_supplicant/aidl/vendor/aidl.cpp |
两条路径的时序差异值得展开:daemonize=true(经典守护进程)时,wpa_supplicant_run() 先调 wpa_supplicant_daemon() fork 出后台子进程,再同步调用 wpas_aidl_init()(binder fd 须在 fork 后的子进程内创建,避免父子共享同一 fd 的所有权,见 notify.c:95 注释)——binder fd 注册进 eloop、AIDL 服务写入 ServiceManager 全部落定后才进入 eloop_run(),此时接口已 add_iface 完成,Java 一连上即可用。daemonize=false(Android 默认)则相反:wpas_aidl_init() 反而执行得更早——在 wpa_supplicant_init() 第 8 步 wpas_notify_supplicant_initialized() 内同步完成,早于任何接口的 add_iface。真正被「推迟」的是进程启动本身:lazy HAL 下 init 不主动拉起 wpa_supplicant,等 Framework 端 ServiceManager.waitForDeclaredService() 首次触发才启动进程,进程一起来 AIDL 注册随即落定。这条初始化链的错误传播是闭环的:wpas_aidl_init() 返回 NULL → wpas_notify_supplicant_initialized() 返回 -1 → wpa_supplicant_init() 返回 NULL → main() return -1,进程退出后由 init 按 restart 策略重新拉起。
Java 端如何连接:
1 | // packages_modules_Wifi/service/java/com/android/server/wifi/SupplicantStaIfaceHalAidlImpl.java |
主要功能:
- binder fd 注册到 eloop 是关键设计决策。这意味着 AIDL 请求和其他 I/O 事件共享同一个事件循环。好处是无需额外线程处理 binder,坏处是如果某个 AIDL 处理函数执行时间过长,会阻塞其他所有 I/O(包括 nl80211 事件和 EAPOL 包)。
addStaInterface("wlan0")的调用链穿过了整个 binder 栈:Java → Binder driver(ioctl)→ native libbinder →ABinderProcess_handlePolledCommands()→Supplicant::addStaInterface()→wpa_supplicant_add_iface()。它执行的是和main()中对-i wlan0完全相同的初始化逻辑。- 除了 vendor AIDL 服务外,还有一个 mainline AIDL 服务(
wifi_mainline_supplicant),通过mainline_aidl_init()注册。Mainline 服务提供 NAN USD(WiFi Aware 服务发现)接口的动态管理能力。mainline 服务以/apex/com.android.wifi/bin/wpa_supplicant_mainline独立进程运行,与 vendor 服务并存、各自向 ServiceManager 注册。
¶3 状态机详解:前台的「状态指示牌」
如果说 eloop 是前台的对讲机总机——负责把厨房(内核)和客人(Java Framework)的每条消息转接进来,那么状态机就是前台墙上的状态指示牌:Supplicant 的 10 个状态决定了它能做什么、不能做什么,以及下一步应该做什么。
¶3.1 enum wpa_states —— 10 个状态,一个完整的连接生命周期
枚举按连接自然流程从 0 递增到 9,扫读时抓顺序即懂进展。
1 | // external_wpa_supplicant_8/src/common/defs.h:248-353 |
枚举值设计意图:
注意:枚举值没有显式赋值,C 语言编译器自动给它们赋值 0, 1, 2, ..., 9。这个顺序不是随意的——它对应了 STA 从断开到完成连接的自然流程。这个顺序让代码中大量使用 > 和 < 比较来判断「进展」:
if (state > WPA_SCANNING)→ 「已经过了扫描阶段」→ 停止自动扫描if (state < WPA_ASSOCIATED)→ 「还没有完成关联」→ 停止后台扫描if (old_state >= WPA_ASSOCIATED && wpa_s->wpa_state < WPA_ASSOCIATED)→ 「从已关联掉到了未关联」→ 通知 WMM AC 断开
注意:
WPA_AUTHENTICATING(值 4)只在 SME 模式下使用(驱动设置了WPA_DRIVER_FLAGS_SME)。非 SME 模式下,状态会直接从SCANNING(3)跳到ASSOCIATING(5),中间不会经过AUTHENTICATING。因此> WPA_SCANNING在非 SME 模式下等价于「已经开始关联或更后面」。
¶3.2 wpa_supplicant_set_state() —— 状态切换的「仪式」
状态切换不是简单的赋值。wpa_supplicant_set_state() 是一个约 180 行的函数,它执行了 5 个阶段的操作:
1 | // external_wpa_supplicant_8/wpa_supplicant/wpa_supplicant.c:1032-1209 |
阶段 1 和阶段 2 都在状态赋值之前执行——它们根据「将要变成什么状态」做清理和记录工作,比如记录漫游耗时、清理 connect work、发送 WPA_EVENT_CONNECTED 事件通知上层。这些操作需要用到旧状态信息(如 wpa_s->new_connection 标志、roam_start 时间戳),一旦状态被覆盖就丢失了上下文。
1 | // ===== 阶段 3:赋值 ===== |
阶段 3 到阶段 5 是赋值之后的逻辑。核心行只有一句 wpa_s->wpa_state = state,但它前后的 if-else 逻辑构成了完整的「状态切换仪式」。阶段 4 的决策全部依赖新状态值——比如 state > WPA_SCANNING 判断是否已经过了扫描阶段,这个比较只有在新值生效后才有意义。阶段 5 的多通道通知确保 Java Framework、P2P 模块、D-Bus 等所有观察者同步得知状态变化。bigtk 验证(wpas_verify_ssid_beacon_prot)紧跟在 wpas_notify_auth_changed() 之后,同样仅在状态确实变化时触发。
五个阶段的总结:
| 阶段 | 做什么 | 为什么 |
|---|---|---|
| 1. 准备阶段 | 处理新状态立即需要的清理 / 记录(漫游时间、normal_scans) | 这些操作需要旧状态信息,必须在赋值前完成 |
| 2. 新连接副作用 | 发 WPA_EVENT_CONNECTED 事件、设置 operstate、清除失败计数 | 连接成功 / 断开的副作用,影响上层 Framework |
| 3. 赋值 | wpa_s->wpa_state = state | 核心操作,最简单但也最危险——后面所有代码都依赖这个值 |
| 4. 赋值后副作用 | 启动 / 停止扫描、重置 BTM、WMM AC 通知 | 这些决策需要知道新状态是什么 |
| 5. 状态变更通知 | wpas_notify_state_changed() → AIDL callback → Java Framework | 只有状态真的变了才通知,避免无意义事件 |
用餐厅的比喻来说,wpa_supplicant_set_state() 就是更新「当前状态指示牌」——但更新不只是翻个牌子,前后还有五步「仪式」要走。
¶3.3 状态转换速查表
| 触发事件 | 原状态 | 新状态 | 代码位置 |
|---|---|---|---|
| 禁用接口(rfkill/Airplane mode) | 任意 | WPA_INTERFACE_DISABLED | wpa_supplicant_disable_network() |
| 启用接口 | WPA_INTERFACE_DISABLED | WPA_DISCONNECTED | wpa_supplicant_enable_network() |
| 没有启用的网络 | WPA_DISCONNECTED | WPA_INACTIVE | wpa_supplicant_set_state() |
| 开始扫描 | WPA_DISCONNECTED / INACTIVE | WPA_SCANNING | wpa_supplicant_scan() |
| 认证开始(SME 模式) | WPA_SCANNING | WPA_AUTHENTICATING | sme_authenticate() |
| 关联开始 | WPA_AUTHENTICATING (SME) / SCANNING | WPA_ASSOCIATING | sme_associate() / wpa_supplicant_associate() |
| 关联成功(EVENT_ASSOC) | WPA_ASSOCIATING | WPA_ASSOCIATED | wpa_supplicant_event() |
| 收到 EAPOL-Key msg 1/4 | WPA_ASSOCIATED | WPA_4WAY_HANDSHAKE | wpa_sm_notify_eapol_rx() |
| 四次握手完成(msg 4/4 发出) | WPA_4WAY_HANDSHAKE | WPA_GROUP_HANDSHAKE | wpa_supplicant_key_neg_complete() |
| 组密钥握手完成 / 直接完成 | WPA_GROUP_HANDSHAKE | WPA_COMPLETED | wpa_supplicant_key_neg_complete() |
| 断开连接(EVENT_DISASSOC/DEAUTH) | 任意 | WPA_DISCONNECTED | wpa_supplicant_event() |
| 扫描完成但无匹配网络 | WPA_SCANNING | WPA_DISCONNECTED / INACTIVE(恢复 scan_prev_wpa_state) | scan_only_handler() |
图 3.1:wpa_supplicant 状态机层级(10 状态的连接生命周期) —— 主链(绿)沿枚举值 0→9 推进:DISCONNECTED → SCANNING → AUTHENTICATING → ASSOCIATING → ASSOCIATED → 4WAY_HANDSHAKE → GROUP_HANDSHAKE → COMPLETED;旁路(橙虚线)是 WPA_INTERFACE_DISABLED(禁用)与 WPA_INACTIVE(无网络)两条分支。上面的速查表逐行对应这条主链上的每一次跃迁。
关键设计洞察:状态是按数值递增的(DISCONNECTED=0, ..., COMPLETED=9)。这意味着
>比较等价于「进展更多」。源码中大量使用if (state > WPA_SCANNING)来判断「是否已经过了扫描阶段」,这种设计让代码非常简洁。
¶4 事件驱动架构:对讲机的「总机」
Supplicant 设计了分层事件处理架构:
1 | 内核 nl80211 事件 |
¶4.1 wpa_supplicant_event() —— 事件分发总入口
1 | // external_wpa_supplicant_8/wpa_supplicant/events.c:6274-6445 |
前四个 case(AUTH / ASSOC / DISASSOC / DEAUTH)构成了连接状态变更的核心路径。EVENT_ASSOC 是其中最重要的——它不只是记录关联成功,还会检查驱动是否已在内部完成 SME(authorized 标志),如果是则直接跳过 EAPOL 阶段推进到授权完成。驱动内建 SME 的动机是把认证 / 关联状态机 offload 到固件,省掉 host↔driver 的往返,也降低 host 功耗。EVENT_DISASSOC 和 EVENT_DEAUTH 分别处理两种断开方式:Disassociation 通常由 AP 主动发起(如负载均衡),Deauthentication 则可能由 AP 或 STA 任一方发起。
1 | case EVENT_SCAN_STARTED: |
扫描事件组是 Work Queue 模式的关键触发点。EVENT_SCAN_STARTED 区分「自己发起的扫描」和「外部程序触发的扫描」,内部扫描会记录 scan_start_time 用于后续计算扫描耗时。EVENT_SCAN_RESULTS 处理最重——解析扫描结果并更新 BSS 缓存,处理完毕后调用 radio_work_check_next() 自动启动队列中的下一个射频任务(如排队中的连接请求)。
1 | case EVENT_EAPOL_RX: |
主要功能:
wpa_supplicant_event()的门卫机制:接口禁用时(WPA_INTERFACE_DISABLED),只允许 4 种事件通过——EVENT_INTERFACE_ENABLED(恢复)、EVENT_INTERFACE_STATUS(状态查询)、EVENT_SCAN_RESULTS(清理扫描状态)、EVENT_SCHED_SCAN_STOPPED(停止调度扫描)。其他所有事件都被忽略。- 关联成功后的
authorized检查:如果驱动在关联完成时就报告了 authorized(这说明驱动内部完成了 SME),则 Supplicant 直接跳过 EAPOL 阶段,进入认证完成处理。 - 扫描结果的
radio_work_check_next():扫描完成后,Supplicant 检查 radio 的 work 队列是否有下一个任务(如连接请求在扫描期间被提交)。这是 Work Queue 模式的核心——扫描只是队列中的一个任务,完成后自动启动下一个。
¶4.2 事件流全景
eloop 就像前台的对讲机总机——binder fd、nl80211 fd、EAPOL fd 是三个频道,任何一个响起就转给对应部门(AIDL 处理 / 事件分发 / 安全模块)。关键是:总机一次只处理一条消息,处理完才接下一条——这正对应半双工对讲机的特性:按着说话时收不到其他频道,说完松开才轮到别人。
¶5 AIDL 接口层:三级服务
Supplicant 的 AIDL 接口分了三个层级,对应不同的抽象层次。这就像餐厅的三级服务:前台经理(ISupplicant)、服务员(ISupplicantStaIface)、菜单(ISupplicantStaNetwork)。
¶5.1 ISupplicant —— 进程级接口(前台经理)
1 | // hardware_interfaces/wifi/supplicant/aidl/android/hardware/wifi/supplicant/ISupplicant.aidl |
关键方法详解:
@PropagateAllowBlocking注解:标注了此注解的方法允许在 binder 线程中执行阻塞操作(如文件 I/O、驱动初始化)。默认情况下 binder 调用不允许阻塞,但 Supplicant 的接口初始化涉及内核交互,可能耗时较长,因此需要此注解。addStaInterface、getStaInterface、addP2pInterface、getP2pInterface四个方法都标注了此注解。
addStaInterface(String ifName):这是 Framework 创建 WiFi 接口的入口。传入 "wlan0",Supplicant 内部调用wpa_supplicant_add_iface()——和main()中对-i wlan0执行完全相同的初始化逻辑。返回ISupplicantStaIface对象供后续操作。removeInterface(IfaceInfo):移除接口时传入类型 + 名称。Supplicant 内部调用wpa_supplicant_remove_iface(),释放对应的struct wpa_supplicant和所有关联资源(扫描缓存、网络配置、密钥材料等)。terminate():oneway 方法(不需要等待返回)。Framework 调用它通知 Supplicant 准备退出。在 lazy HAL 协议下,这可以触发进程退出。
¶5.2 ISupplicantStaIface —— STA 接口级接口(服务员)
1 | // hardware_interfaces/wifi/supplicant/aidl/android/hardware/wifi/supplicant/ISupplicantStaIface.aidl |
关键方法详解:
addNetwork():创建一个新的ISupplicantStaNetwork对象,对应 Supplicant 内部的struct wpa_ssid。返回的 network 对象初始为空——Framework 需要后续调用setSsid()、setKeyMgmt()等方法填充配置。disconnect()vsreassociate()vsreconnect():三个方法语义不同。disconnect()是主动断开,进入WPA_DISCONNECTED;reassociate()是强制对当前 AP 重新执行关联(用于刷新密钥);reconnect()只在已断开时重新连接(如果已连接则返回错误)。registerCallback(ISupplicantStaIfaceCallback):Framework 通过它注册回调对象,Supplicant 状态变化时通过 binder 反向调用 Java 端的onStateChanged()等方法。这是观察者模式的典型实现。
¶5.3 ISupplicantStaNetwork —— 网络配置接口(菜单)
这张菜单列出网络的全部配置项,读时抓 setSsid / setKeyMgmt / select 三行即可。
1 | // hardware_interfaces/wifi/supplicant/aidl/android/hardware/wifi/supplicant/ISupplicantStaNetwork.aidl |
关键方法详解:
select():触发 Supplicant 使用此网络配置发起连接。它内部调用wpa_supplicant_select_network(),设置wpa_s->next_ssid并触发扫描或直接连接。setPskPassphrase(String)vssetPsk(byte[]):前者接受 8-63 字符的 ASCII 密码短语(Supplicant 内部通过 PBKDF2 派生 PMK),后者接受 32 字节的原始 PSK(跳过派生直接使用)。- EAP 配置是网络配置中最复杂的部分:涉及证书路径、私钥、多种 EAP 方法的选择。
¶5.4 事件回调机制 —— 从 C 到 Java 的状态通知
1 | // hardware_interfaces/wifi/supplicant/aidl/android/hardware/wifi/supplicant/ |
回调触发链路(以状态变化为例):
1 | wpa_s->wpa_state 变化 |
- 双通道并行通知:
wpas_notify_state_changed()同时走两条路径——ctrl socket(wpa_cli可见,用于调试)和 AIDL callback(Framework 消费)。两条路径在wpas_notify_state_changed()内部是串行调用的,但从外部看它们是同一事件的两个通知目标。 - oneway 修饰:所有 AIDL 回调方法都声明为
oneway,Supplicant 发送回调后不等待 Java 端的处理结果,避免被慢速的 Framework 处理阻塞。
¶6 设计模式:前台的「SOP 流程」
Supplicant 代码库中反复出现四种设计模式。理解它们就等于掌握了代码的组织逻辑。
¶6.1 虚函数表模式 —— wpa_driver_ops
Supplicant 支持多种驱动后端(nl80211、wext、wired 等),通过虚函数表实现多态。读这段先抓 name 与 init 两行,其余是同构函数指针:
1 | // external_wpa_supplicant_8/src/drivers/driver.h:3097-5353 |
实际使用:
1 | // Supplicant 核心代码通过 wpa_drv_* 宏调用,而不是直接调用 driver->* |
- 60 个函数指针不是都要实现:
wpa_drv_*宏在调用前检查函数指针是否为 NULL。不支持的操作用return -1优雅降级。 - 驱动选择在初始化时确定:
wpa_supplicant_set_driver(wpa_s, "nl80211")遍历wpa_drivers[]数组,找到 name 匹配的wpa_driver_ops,赋值给wpa_s->driver。 - 为什么不用 C++ 虚函数? wpa_supplicant 是纯 C 项目(历史原因:2003 年开始开发时 C++ 在嵌入式系统上不普及)。手动虚函数表是 C 语言实现多态的标准做法。
¶6.2 观察者模式 —— 状态变化通知链
Supplicant 的状态变化通过多通道通知出去:
1 | wpa_supplicant_set_state() |
- 多观察者、多通道:一个状态变化同时通知 ctrl_iface(调试)、AIDL callback(Framework)、P2P(内部模块)、D-Bus(如果启用)。
- 通知只在状态确实变化时发生:
wpa_supplicant_set_state()阶段 5 有if (wpa_s->wpa_state != old_state)守卫,避免重复设置同一状态时产生垃圾通知。
¶6.3 Work Queue 模式 —— radio_add_work() 序列化射频操作
WiFi 芯片只有一个射频前端,但可能有多个操作竞争使用它(扫描、连接、P2P 发现……)。Work Queue 模式解决了这个问题:
1 | // external_wpa_supplicant_8/wpa_supplicant/wpa_supplicant.c:7131-7178 |
radio_add_work() 是 Work Queue 的「入队」操作——创建 work item,按优先级(next=1 插头部、next=0 插尾部)插入射频队列。入队后如果队列为空或驱动支持并发 offchannel 且未达到 MAX_ACTIVE_WORKS 上限,立即触发执行。type 字段("scan" / "connect" / "p2p-scan")不仅是日志标签,还影响频段计算逻辑——扫描类 work 需要从外部传入的扫描参数中解析频段列表。
1 | // wpa_supplicant.c:7188-7197 |
radio_work_done() 是 Work Queue 的「出队」操作——释放 work item,然后自动调用 radio_work_check_next() 启动队列中的下一个任务。if (started) 守卫很关键:如果 work 在开始执行前就被取消了(radio_remove_works()),started 为 0,此时不需要触发下一个任务。这保证了射频操作的串行链式推进——每个 work 完成后自动启动下一个。
Work Queue 的关键特性:
- 高优先级插队:
next=1的 work 插入链表头部(如用户手动触发扫描),next=0插入尾部(如自动扫描)。 - 并发度控制:
MAX_ACTIVE_WORKS = 2。支持OFFCHANNEL_SIMULTANEOUS的驱动可以同时执行最多 2 个 work。 - 自动链式推进:
radio_work_done()调用radio_work_check_next(),自动启动下一个 work。这保证了射频操作串行执行、不会冲突。
¶6.4 状态保存 / 恢复模式 —— scan_prev_wpa_state
扫描是一个特殊的操作——它需要临时改变接口的状态,但不能丢失扫描前的状态信息:
1 | // wpa_supplicant/scan.c:1157-1160 |
- 为什么需要保存 / 恢复? 扫描可能在多种状态下被触发——可能在
WPA_DISCONNECTED时自动扫描寻找 AP,也可能在WPA_COMPLETED时后台扫描做 roaming 准备。扫描完成后必须恢复到原来的状态。 - 保存是无条件的,状态转换是有条件的:
scan_prev_wpa_state始终保存当前状态(即使已经是 SCANNING 也会覆盖),但只有在WPA_DISCONNECTED或WPA_INACTIVE时才会真正转入WPA_SCANNING。恢复时也有守卫——只有当前状态确实是WPA_SCANNING才恢复,避免在扫描期间发生了其他状态转换(如关联)时错误回退。 - 恢复不是简单赋值:
wpa_supplicant_set_state()会再次执行 5 阶段操作,包括状态变更通知。这意味着 Framework 会看到COMPLETED → SCANNING → COMPLETED的状态序列,这是正确的行为。 - 主要恢复点在
scan_only_handler()(scan.c:3262),而非wpa_supplicant_event_scan_results()。scan_only_handler()是扫描 radio work 的回调,扫描完成时由radio_work_done()触发。
用餐厅的比喻收个尾:scan_prev_wpa_state 就像前台接待去门口「看看外面有什么客人」(扫描)之前,先在状态指示牌上临时翻到「扫描中」;看完回来再照着记下的旧状态把牌子翻回原位——而不是永远停在「扫描中」忘了复位。
¶7 总结:Supplicant 架构全景
¶7.1 eloop 单线程模型:为什么不用多线程?
wpa_supplicant 的核心架构设计是单进程、单线程、事件驱动。这个设计贯穿了整个 Supplicant 的生命周期——从 main() 调用 eloop_run() 开始,进程就进入了一个无限循环,所有操作(接收内核 netlink 事件、处理 binder 请求、驱动 EAPOL 状态机)都是在这个循环中被动触发的回调。
单线程模型的核心优势是:避免了所有锁竞争。如果 Supplicant 使用多线程——比如一个线程处理 nl80211 事件、另一个线程处理 binder 请求、第三个线程处理 EAPOL 定时器——那么 struct wpa_supplicant 中的数百个字段(wpa_state、key_mgmt、ptk、gtk 等)都需要加锁保护。
以 Supplicant 的状态复杂度,锁的粒度和顺序将极难正确设计——死锁和竞态条件的风险远大于单线程模型的性能损失。
代价是每个 eloop callback 必须快速返回。如果有耗时操作(如完整的 EAP-TLS 握手涉及多次往返),需要将其拆分为多个异步状态,通过 eloop 的 timeout 机制驱动状态机推进。这增加了代码复杂度,但换来了确定性——你知道在任何一个时间点,只有一个 callback 在运行,没有任何字段被并发修改。
¶7.2 五大核心设计要素
| 设计要素 | 核心价值 | 关键文件 |
|---|---|---|
| 数据结构 | wpa_global(全局根对象)→ wpa_supplicant(每接口状态)→ wpa_radio(射频互斥)→ wpa_radio_work(任务队列)→ wpa_ssid(网络配置) | wpa_supplicant_i.h |
| 状态机 | 10 个状态的数值递增设计,wpa_supplicant_set_state() 的 5 阶段执行 | wpa_supplicant.c, defs.h |
| eloop | 单线程事件驱动,epoll 统一调度所有 I/O + timer | eloop.c |
| AIDL | 三层接口:ISupplicant(进程级)→ ISupplicantStaIface(接口级)→ ISupplicantStaNetwork(网络级) | ISupplicant.aidl, ISupplicantStaIface.aidl, ISupplicantStaNetwork.aidl |
| 事件处理 | wpa_supplicant_event() 分发 30+ 事件到对应的 SME/EAPOL/Scan 处理器 | events.c |
把五件套放回餐厅视角:数据结构是接待台的工作手册(记录每个接口和网络的状态),状态机是墙上的状态指示牌(决定下一步能做什么),eloop 是前台的对讲机总机(转接所有消息),AIDL 三级服务是面对客人的窗口(前台经理/服务员/菜单),事件处理则是传菜通道(把每条消息送到对应的后厨部门)。
¶7.3 从这篇文章开始,后续将怎么展开?
Supplicant 启动是餐厅「前台接待就位」的一步——它确保芯片就绪后上层能正常使用 WiFi。前台就位之后,后续各章依次展开上层的建筑:
- 扫描:芯片就绪后第一个动作,如何发现周围的 AP?QCOM 的 scan engine 如何调度主动扫描和被动扫描?MTK 如何在三个频段之间并行扫描?
- 关联:怎么选择 AP、完成 802.11 的认证和关联?安全模式(Open/WPA2/WPA3)如何影响关联流程?
- 四次握手与 EAPOL:WPA2 的 PSK 模式和 WPA3 的 SAE 模式的密钥协商细节。wpa_supplicant 的 EAPOL 状态机如何驱动四次握手?
- IP 获取与数据通路:DHCP 流程、ARP 表填充、路由表配置,数据包如何从芯片 → HIF → 网络栈 → socket。
每一章都会继续沿着 QCOM 和 MTK 两条路径对比走读。
¶7.4 Supplicant 端点速查表
全文各环节的入口函数散落在上面几节,下面把它们收进一张速查表——从启动到事件分发、从 AIDL 注册到射频任务队列,每个环节的入口函数、核心机制、关键文件一页扫完。
| 环节 | 入口函数 | 核心机制 | 关键文件 |
|---|---|---|---|
| Supplicant 启动 | main() → eloop_run() | epoll 事件循环(单线程) | external_wpa_supplicant_8/wpa_supplicant/main.c |
| 全局初始化 | wpa_supplicant_init() | EAP 方法注册 + eloop_init + ctrl iface | wpa_supplicant/wpa_supplicant.c |
| 接口初始化 | wpa_supplicant_add_iface() | nl80211 socket + WPA 状态机 + EAPOL socket | wpa_supplicant/wpa_supplicant.c |
| 状态切换 | wpa_supplicant_set_state() | 5 阶段执行 + 多通道通知 | wpa_supplicant/wpa_supplicant.c:1032 |
| 事件分发 | wpa_supplicant_event() | switch 30+ 事件类型 | wpa_supplicant/events.c:6274 |
| AIDL 注册 | wpas_aidl_init() | binder fd → eloop + ServiceManager 注册 | wpa_supplicant/aidl/vendor/aidl.cpp |
| Java 端连接 | SupplicantStaIfaceHalAidlImpl.startDaemon() | ISupplicant.Stub.asInterface() + DeathRecipient | packages_modules_Wifi/service/java/com/android/server/wifi/SupplicantStaIfaceHalAidlImpl.java |
| 射频任务队列 | radio_add_work() + radio_work_done() | 双向链表队列 + 并发度控制 | wpa_supplicant/wpa_supplicant.c:7131 |
| 回调通知 | wpas_notify_state_changed() | AIDL oneway 跨进程回调 | wpa_supplicant/notify.c |
下一章:芯片就绪后,第一个动作永远是扫描。我们会看到 QCOM 的
wlan_scanengine 如何调度主动扫描(Probe Request)和被动扫描(Beacon 监听),MTK 的scan_module如何利用 offload 能力在 2.4G/5G/6G 三个频段并行扫描,以及 wpa_supplicant 的wpa_supplicant_scan()如何把一次 scan request 从 JavaWifiManager.startScan()一路传到芯片寄存器。
到这里,「STA 打开」系列完结。从用户点击 WiFi 开关,到 Framework 调度、HAL 桥接、驱动加载、固件握手、Supplicant 前台就位——整条链路已经完整走通,餐厅正式开张迎客。接下来,我们将进入 WiFi 最基础的操作:扫描。
本文涉及的规范章节:IEEE 802.11-2020 §5.1 (STA architecture), §4.3 (WLAN components), §11.3 (STA authentication and association);Wi-Fi Alliance WPA3 Specification v3.5 §2 (WPA3-Personal / SAE)。源码路径索引见文内各代码块首行注释。
源码出处:external/wpa_supplicant_8、AIDL 定义 hardware_interfaces/wifi/supplicant/aidl/。