Wi-Fi Direct教程 02:从 wpa_cli main() 到 P2P_FIND——用源码注释追踪 CLI 控制命令发送
摘要:沿 wpa_supplicant 2.12 的 wpa_cli 实际执行路径,用连续源码片段和行内注释说明参数解析、eloop、UNIX datagram control socket、命令分发以及 P2P_FIND 的发送与同步回复。
@[toc]
上一篇已经完成双 mac80211_hwsim radio、双 wpa_supplicant 2.12 实例和独立 control interface。本篇只追踪下面这条命令在 wpa_cli 进程内部如何执行:
1 | sudo wpa_cli-2.12 \ |
目标是回答:
1 | Shell 输入 p2p_find |
本篇停止在:
1 | src/common/wpa_ctrl.c:wpa_ctrl_request() |
本文只分析 wpa_cli 进程内部的 CLI 解析、control socket client 和同步 request/reply 边界;server 进程内部不在本文展开。
源码基线固定为 wpa_supplicant 2.12;本文源码结论以同版本 wpa_cli.c、wpa_ctrl.c、eloop.c 为主要证据。S1
1 | wpa_supplicant 2.12 |
本文主要看:
| 文件 | 关注点 |
|---|---|
wpa_supplicant/wpa_cli.c |
main()、wpa_cli_open_connection()、wpa_request()、wpa_cli_commands[]、wpa_cli_cmd_p2p_find() |
src/common/wpa_ctrl.c |
wpa_ctrl_open2()、wpa_ctrl_request()、wpa_ctrl_attach()、wpa_ctrl_detach() |
src/utils/os_unix.c |
os_program_init()、ANDROID、WPA_TRACE |
src/utils/eloop.c |
eloop_init() 以及 select/poll/epoll/kqueue backend |
wpa_supplicant/Makefile、defconfig |
control interface 与 eloop 的编译期选择 |
1. 从 main() 看起:Shell 参数怎样变成 wpa_cli 内部状态
当前 Shell 命令进入进程以后,可以先把参数理解为:
1 | argc = 6 |
argc 不包含最后的 NULL。
1.1 main() 入口先调用 os_program_init()
wpa_cli.c 中与当前主线直接相关的入口片段就是:
1 | /* [解读] |
随后进入 src/utils/os_unix.c:os_program_init()。当前 Ubuntu 构建没有定义 ANDROID,因此预处理之后,和本次执行路径直接相关的函数可以完整地读成:
1 | int os_program_init(void) |
原始源码在随机种子初始化之前还包着一个 #ifdef ANDROID 平台分支。阅读它时可以直接按下面的预处理结构理解:
1 |
|
这里要把 ANDROID 理解成“平台编译开关”:
- Ubuntu/Linux 普通构建:没有定义,Android 专用代码在预处理阶段就消失;
- Android 构建:会包含 Android 用户组、UID/GID、capability 等平台适配;
- 它不是“当前是不是手机”的运行时判断,也不是 Wi-Fi Direct 协议本身的一部分。
Android 分支里出现 PR_SET_KEEPCAPS、setuid()、setgid()、CAP_NET_ADMIN、CAP_NET_RAW,核心目的不是实现 P2P,而是让进程降低普通 root 权限后仍保留完成网络管理所需的有限 capability。
1.2 WPA_TRACE 是什么
os_unix.c、eloop.c 中还会看到由 WPA_TRACE 包住的调试代码。阅读结构可以理解为:
1 |
|
WPA_TRACE 同样是开发调试用的编译期开关。它主要用于追踪:
- 内存分配;
- 对象/注册关系;
- 错误使用位置;
- backtrace;
- 退出时的内存泄漏线索。
它不是:
1 | Wi-Fi 抓包开关 |
因此本篇看到 WPA_TRACE 时,只需要知道:它服务于 wpa_supplicant 自身的开发调试,不改变当前 p2p_find 正常控制路径的业务语义。
当前实验没有专门开启它,所以后续源码阅读不沿这条分支深入。
2. getopt():-p、-i 和 p2p_find 为什么不是同一类参数
main() 接下来进入 option 解析。当前路径的核心代码可以放在一起看:
1 | for (;;) { |
当前值是:
1 | argc = 6 |
所以:
1 | argc == optind |
最终:
1 | interactive = 0 |
这说明当前运行模式是:
1 | 一次性执行 p2p_find |
而不是进入:
1 | wpa_cli> |
交互提示符。
参数解析过程可以压缩成:
1 | flowchart LR |
关键点只有一个:
p2p_find不属于getopt()的 option。getopt()处理完-p/-i后,通过optind把真正的 CLI 子命令留给后面的命令分发逻辑。
3. main() 为什么调用了 eloop_init(),但这次回复等待却不是 epoll/kqueue
解析完参数以后,main() 在打开 control connection 前还有一段初始化:
1 | /* [解读] |
3.1 CONFIG_ELOOP_EPOLL / KQUEUE 到底控制什么
src/utils/eloop.c 会按编译配置选择事件等待 backend:
| 配置 | 常见平台 | eloop 主循环使用的等待机制 | 本篇需要掌握到什么程度 |
|---|---|---|---|
| 都未定义 | Linux/BSD 都可 | select() |
默认 fallback,知道即可 |
CONFIG_ELOOP_POLL |
POSIX | poll() |
可选 backend |
CONFIG_ELOOP_EPOLL |
Linux | epoll_create1() / epoll_wait() |
Linux 大量 fd 场景更适合的事件通知机制 |
CONFIG_ELOOP_KQUEUE |
BSD/macOS | kqueue() / kevent() |
BSD 系平台事件通知机制 |
源码逻辑本质上是:
1 |
也就是说:没有明确选择 poll/epoll/kqueue 时,普通 UNIX eloop 默认退回 select()。
但本篇最容易混淆的是:
eloopbackend 的选择,不等于所有地方的 socket 等待都会自动改成该 backend。
当前一次性命令,例如:
1 | sudo wpa_cli-2.12 -p /run/wpa_supplicant-p2p-a -i wlan0 p2p_find |
不会进入交互模式的长期 eloop_run()。后面 P2P_FIND 的同步 request/reply 等待由 wpa_ctrl_request() 自己直接调用 select() 完成。
因此即使某个构建启用了:
1 | CONFIG_ELOOP_EPOLL=y |
也不能推导成:
1 | wpa_ctrl_request() 等 OK 时使用 epoll_wait() |
这是两段不同代码。
4. main() 进入 non-interactive 分支:现在才打开 control connection
当前:
1 | interactive = 0 |
因此 main() 进入一次性命令路径。
把两个关键调用放在同一个连续阅读单元里看。下面是按当前实际条件裁剪后的执行路径阅读版:删掉 action_file、daemonize 和连接失败分支,只保留本次成功执行必经的语句;因此它用于理解运行顺序,不冒充完整 main() 原文。
1 | /* [解读] |
原函数如果 wpa_cli_open_connection() 返回负值,会在这里报错并直接结束,不会进入后面的 wpa_request();当前主线只跟踪连接成功的实际执行路径。
当前实参等价于:
1 | wpa_cli_open_connection("wlan0", 0); |
其中:
1 | &argv[5] -> "p2p_find" |
所以主线现在分成两个连续问题:
wpa_cli_open_connection("wlan0", 0)怎样建立 control transport;- 连接成功以后,
wpa_request()怎样把p2p_find分发成P2P_FIND。
先看第一个。
5. wpa_cli_open_connection():先决定 control backend,再拼 server 地址
函数入口:
1 | static int wpa_cli_open_connection(const char *ifname, int attach) |
当前参数:
1 | ifname = "wlan0" |
下面把当前 Linux/UNIX 路径集中在一个代码块里看:
1 | static int wpa_cli_open_connection(const char *ifname, int attach) |
5.1 access() 是干什么的
这里出现两次:
1 | access(ctrl_iface_dir, F_OK) |
access(path, mode) 用来检查调用进程按指定模式访问路径是否允许。本文只用到了:
| mode | 含义 |
|---|---|
F_OK |
路径是否存在 |
R_OK |
是否可读 |
W_OK |
是否可写 |
X_OK |
是否可执行/可搜索 |
当前代码使用 F_OK,因此不要把它理解成:
1 | 这个目录一定可读可写 |
它只是在问:
1 | 这个路径存在吗? |
在当前 Ubuntu 命令里:
1 | ctrl_iface_dir = /run/wpa_supplicant-p2p-a |
最终拼成:
1 | /run/wpa_supplicant-p2p-a/wlan0 |
这就是 wpa_cli 要联系的 wpa_supplicant control endpoint。
5.2 CONFIG_CTRL_IFACE_UNIX / UDP / NAMED_PIPE 是编译期 transport 选择
当前构建最终使用 UNIX Domain Socket。它和下面这些是同一层的 transport backend 选择:
| backend | 典型承载 | 适用环境 |
|---|---|---|
unix |
UNIX Domain Socket | Linux/BSD 常规本机 control interface |
udp / udp6 |
localhost UDP | 特殊环境/测试 |
named_pipe |
Windows Named Pipe | Windows |
udp-remote / udp6-remote |
可远端访问 UDP | 上游配置明确偏向测试用途 |
要注意:
1 | P2P_FIND / STATUS / PING / ATTACH |
是 control interface 的命令语义;
1 | UNIX / UDP / Named Pipe |
是承载这些命令的 transport。
两者不是同一层。
6. wpa_ctrl_open2():真正创建一个 UNIX datagram client
现在进入:
1 | src/common/wpa_ctrl.c |
函数:
1 | struct wpa_ctrl * wpa_ctrl_open2(const char *ctrl_path, |
当前参数:
1 | ctrl_path = "/run/wpa_supplicant-p2p-a/wlan0" |
与其拆成十几个两三行的小片段,不如直接把当前 UNIX 路径按执行顺序放在一起看:
1 | struct wpa_ctrl * wpa_ctrl_open2(const char *ctrl_path, |
执行完以后,最重要的状态是:
1 | ctrl->s |
两条地址不要混:
| 地址 | 属于谁 | 作用 |
|---|---|---|
/tmp/wpa_ctrl_<pid>-<counter> |
wpa_cli |
server 回复时需要知道 client 在哪里 |
/run/wpa_supplicant-p2p-a/wlan0 |
wpa_supplicant |
client command 的目标 endpoint |
6.1 为什么这里用 SOCK_DGRAM,而不是 SOCK_STREAM
对于 UNIX Domain Socket,本文最值得比较的是下面三种类型。SOCK_DGRAM、connect() 与 pathname socket 的系统调用语义可对照 Linux man-pages。S2
| socket type | 是否连接导向 | 是否保留消息边界 | server 是否通常需要 listen()/accept() |
当前 control command 是否适合 |
|---|---|---|---|---|
SOCK_DGRAM |
否;但可以 connect() 固定默认 peer |
是 | 否 | 很适合“一条命令 = 一条 message” |
SOCK_STREAM |
是 | 否,只有连续 byte stream | 是 | 必须额外解决 framing/拆包问题 |
SOCK_SEQPACKET |
是 | 是 | 是 | 也能保留消息边界,但现有 ctrl_iface 并未按它设计 |
当前 control interface 的文本天然是一条一条的:
1 | PING |
SOCK_DGRAM 能保留 datagram/message 边界,因此收到一条 datagram 时就能按一条 command 处理。
如果改成 SOCK_STREAM:
1 | 不能只改 socket(PF_UNIX, SOCK_DGRAM, 0) |
因为服务端还要同步改成:
1 | bind() |
并设计:
1 | 一条命令到哪里结束? |
这就是 stream framing 问题。
6.2 SOCK_DGRAM 的 connect() 为什么不是 TCP connect()
这一句:
1 | connect(ctrl->s, |
不能按 TCP 三次握手理解。
当前是:
1 | AF_UNIX + SOCK_DGRAM |
这里 connect() 的核心作用是给 datagram socket 绑定一个默认通信 peer。这样后续可以直接使用已经固定 peer 的接口:
1 | send(ctrl->s, buf, len, 0); |
而不用每次都显式携带目标地址,例如:
1 | sendto(ctrl->s, |
所以更准确的心智模型是:
1 | socket() |
7. 返回 main():control connection 建好后才处理 p2p_find
wpa_cli_open_connection() 返回成功后,main() 执行:
1 | ret = wpa_request(ctrl_conn, |
当前等价于:
1 | wpa_request(ctrl_conn, 1, &argv[5]); |
所以进入 wpa_request() 后,它看到的是:
1 | argc = 1 |
不再是原始 Shell 的完整 argv[]。
7.1 wpa_cli_commands[] 是静态命令分发表
这里不再把整个 wpa_request() 用占位符拼成一个“伪完整函数”,而是保留两个真正承担当前主线语义的连续源码片段,并直接把解释写进代码旁边。
先看查表。下面保留原函数与当前输入直接相关的匹配逻辑,并在代码内标出变量怎样变化:
1 | /* [解读] |
命中的 handler 本身很短,可以完整看完:
1 | static int wpa_cli_cmd_p2p_find(struct wpa_ctrl *ctrl, |
这里必须建立一个非常明确的概念边界:
1 | p2p_find |
随后 wpa_cli_cmd() 会调用 write_cmd() 把命令和参数拼到 buffer 中。
当前没有参数,因此最终发送文本就是:
1 | P2P_FIND |
如果以后输入:
1 | p2p_find 10 type=social |
才会继续把参数拼入 control command。
当前调用继续进入:
1 | wpa_cli_cmd() |
8. wpa_ctrl_request():P2P_FIND 在这里真正跨进程发送
_wpa_ctrl_command() 最终把:
1 | cmd = "P2P_FIND" |
交给:
1 | int wpa_ctrl_request(struct wpa_ctrl *ctrl, |
这一函数包含 UDP cookie、non-blocking 重试、超时计算等多个分支。当前 binary 使用 UNIX backend,所以先把“UNIX 成功路径”按一个连续逻辑单元读完;UDP 专用分支在编译阶段不存在。
1 | /* [解读] |
这时跨进程边界已经非常明确:
1 | wpa_cli client socket |
server 处理完成以后再把 reply 发回 client 地址。
wpa_ctrl_request() 收到正式 reply 后返回,最终 _wpa_ctrl_command() 打印 buffer,于是 Shell 会看到:
1 | OK |
或者:
1 | FAIL |
这里的 OK 只表示:
control interface 接收并成功受理了当前
P2P_FINDrequest。
它不表示:
1 | 已经发现 peer |
那些不属于本篇的同步 control request/reply 主线。
9. wpa_supplicant control interface 的应用层文本命令协议
ATTACH、DETACH、PING、STATUS、P2P_FIND等属于 wpa_supplicant control interface 的应用层文本命令协议。S3
它们不是:
1 | IEEE 802.11 management frame |
9.1 ATTACH 的源码语义
wpa_ctrl.c 中:
1 | static int wpa_ctrl_attach_helper(struct wpa_ctrl *ctrl, int attach) |
因此:
1 | wpa_ctrl_open2() |
ATTACH 不是“建立 socket”。
9.2 为什么 interactive 模式通常有 ctrl_conn 和 mon_conn
前面 wpa_cli_open_connection() 已经看到:
1 | ctrl_conn = wpa_ctrl_open2(cfile, client_socket_dir); |
interactive 模式中通常把职责分开:
| connection | 主要职责 |
|---|---|
ctrl_conn |
普通 command request/reply |
mon_conn |
ATTACH 后持续接收 unsolicited event |
当前一次性命令,例如:
1 | sudo wpa_cli-2.12 -p /run/wpa_supplicant-p2p-a -i wlan0 p2p_find |
调用:
1 | wpa_cli_open_connection(ctrl_ifname, 0) |
所以:
1 | ctrl_conn = 有 |
1 | sudo wpa_cli-2.12 \ |
它直接来自当前 binary 对应的 wpa_cli_commands[],因此不会出现“查到别的版本命令表”的问题。
如果只查 P2P,则优先看当前源码目录中的:
1 | wpa_supplicant/README-P2P |
10. control interface 和真正的 P2P 网络通信不要混成一层
这一层关系只需要保留抽象技术边界,不引入具体业务角色。
当前文章讨论的是:
1 | 同一台 Linux 主机内 |
这属于:
1 | 本机 control plane / IPC |
而 Wi-Fi Direct Group 真正建立以后,另一层才是:
1 | P2P Device A |
所以:
1 | /run/wpa_supplicant-p2p-a/wlan0 |
不是远端 P2P peer 的网络地址。
UNIX Domain Socket 本身也不能跨机器。
这一区分后面非常重要:
1 | ctrl_iface 的 UDP backend |
也不能自动等价为:
1 | P2P Group 建成以后,两台设备之间应该使用 UDP 业务通信 |
前者是 wpa_supplicant 的控制接口承载方式;后者是 P2P Group 形成后的 IP 数据面协议选择。两层应分别分析。
11. 把整条 CLI 路径重新串起来
到这里,wpa_cli 侧已经形成闭环:
1 | flowchart TD |
现在再看四个关键边界:
| 边界 | 正确理解 |
|---|---|
getopt() -> optind |
-p/-i 是 wpa_cli option;p2p_find 是留下来的 CLI 子命令 |
wpa_ctrl_open2() |
只是在创建 control transport,还没有执行 P2P Discovery |
p2p_find -> P2P_FIND |
CLI command name 转换成 control interface 文本 command |
wpa_ctrl_request() |
CLI 侧真正跨进程:发送文本 command、等待同步 reply |
还可以再把容易混淆的编译期开关放到一张表中:
| 名称 | 控制什么 | 当前 Ubuntu p2p_find 主线是否需要深入 |
|---|---|---|
ANDROID |
Android 平台权限与 socket 适配代码 | 否,只需认识 |
WPA_TRACE |
内存/注册关系/backtrace 等开发调试追踪 | 否 |
CONFIG_CTRL_IFACE_* |
control interface transport backend | 是,当前为 UNIX |
CONFIG_ELOOP_EPOLL |
eloop 主循环使用 epoll | 认识即可 |
CONFIG_ELOOP_KQUEUE |
eloop 主循环使用 kqueue | 认识即可 |
CONFIG_ELOOP_POLL |
eloop 主循环使用 poll | 认识即可 |
| 无上述 eloop backend | eloop 默认 select | 认识即可 |
最后再强调一次本篇最重要的执行事实:
1 | 一次性 `wpa_cli [options] p2p_find` |
虽然前面调用了:
1 | eloop_init() |
但 P2P_FIND 同步 reply 的等待点仍然是:
1 | wpa_ctrl_request() |
不是 eloop_run()。
12. 本篇停止点
本篇只完成 client 侧:
1 | Shell |
本篇到 wpa_ctrl_request() 收到 OK/FAIL 为止;这已经完整回答了 wpa_cli p2p_find 如何把文本命令发送到 control interface 并同步等待 reply。
关键源码索引
| 关键对象 / 符号 | 本文位置 | Git 源码 |
|---|---|---|
main() |
CLI 入口 | hostap 2.12 |
wpa_cli_open_connection() |
control client 打开 | hostap 2.12 |
wpa_ctrl_open2() |
AF_UNIX client | hostap 2.12 |
wpa_cli_cmd_p2p_find() |
p2p_find 命令分发 | hostap 2.12 |
wpa_ctrl_request() |
send/select/recv | hostap 2.12 |
资料来源
[S1] hostap 2.12 Git:wpa_cli 与 control client
- 版本:
hostap_2_12/831364bf02710ad09c2f27d3efa92abeeb5634c0 - 文件:wpa_cli.c、wpa_ctrl.c、eloop.c、os_unix.c
- 使用位置:第 1~8、11~12 节
- 支撑内容:CLI 参数解析、命令分发表、
p2p_find -> P2P_FIND、control client socket 与wpa_ctrl_request()同步 request/reply。
[S2] Linux man-pages:UNIX domain socket 与 socket API
- URL/文档:unix(7)、connect(2)、select(2)
- 使用位置:第 5~8 节
- 支撑内容:AF_UNIX pathname socket、
SOCK_DGRAM、datagramconnect()与select()readiness 语义。
[S3] wpa_supplicant control interface 设计文档
- 来源:hostap Git: doc/ctrl_iface.doxygen
- URL/文档:w1.fi control interface
- 使用位置:第 9~10 节
- 支撑内容:control interface 是本地管理接口,
ATTACH用于事件监视,与空口协议帧不同层。










