Wi-Fi Direct教程 02:从 wpa_cli main() 到 P2P_FIND——用源码注释追踪 CLI 控制命令发送

Wi-Fi Direct教程 02:从 wpa_cli main() 到 P2P_FIND------用源码注释追踪 CLI 控制命令发送

摘要:沿 wpa_supplicant 2.12 的 wpa_cli 实际执行路径,用连续源码片段和行内注释说明参数解析、eloop、UNIX datagram control socket、命令分发以及 P2P_FIND 的发送与同步回复。

文章目录

  • [Wi-Fi Direct教程 02:从 wpa_cli main() 到 P2P_FIND------用源码注释追踪 CLI 控制命令发送](#Wi-Fi Direct教程 02:从 wpa_cli main() 到 P2P_FIND——用源码注释追踪 CLI 控制命令发送)
    • [1. 从 main() 看起:Shell 参数怎样变成 wpa_cli 内部状态](#1. 从 main() 看起:Shell 参数怎样变成 wpa_cli 内部状态)
      • [1.1 main() 入口先调用 os_program_init()](#1.1 main() 入口先调用 os_program_init())
      • [1.2 WPA_TRACE 是什么](#1.2 WPA_TRACE 是什么)
    • [2. getopt():-p、-i 和 p2p_find 为什么不是同一类参数](#2. getopt():-p、-i 和 p2p_find 为什么不是同一类参数)
    • [3. main() 为什么调用了 eloop_init(),但这次回复等待却不是 epoll/kqueue](#3. main() 为什么调用了 eloop_init(),但这次回复等待却不是 epoll/kqueue)
      • [3.1 CONFIG_ELOOP_EPOLL / KQUEUE 到底控制什么](#3.1 CONFIG_ELOOP_EPOLL / KQUEUE 到底控制什么)
    • [4. main() 进入 non-interactive 分支:现在才打开 control connection](#4. main() 进入 non-interactive 分支:现在才打开 control connection)
    • [5. wpa_cli_open_connection():先决定 control backend,再拼 server 地址](#5. wpa_cli_open_connection():先决定 control backend,再拼 server 地址)
      • [5.1 access() 是干什么的](#5.1 access() 是干什么的)
      • [5.2 CONFIG_CTRL_IFACE_UNIX / UDP / NAMED_PIPE 是编译期 transport 选择](#5.2 CONFIG_CTRL_IFACE_UNIX / UDP / NAMED_PIPE 是编译期 transport 选择)
    • [6. wpa_ctrl_open2():真正创建一个 UNIX datagram client](#6. wpa_ctrl_open2():真正创建一个 UNIX datagram client)
      • [6.1 为什么这里用 SOCK_DGRAM,而不是 SOCK_STREAM](#6.1 为什么这里用 SOCK_DGRAM,而不是 SOCK_STREAM)
      • [6.2 SOCK_DGRAM 的 connect() 为什么不是 TCP connect()](#6.2 SOCK_DGRAM 的 connect() 为什么不是 TCP connect())
    • [7. 返回 main():control connection 建好后才处理 p2p_find](#7. 返回 main():control connection 建好后才处理 p2p_find)
      • [7.1 wpa_cli_commands\[\] 是静态命令分发表](#7.1 wpa_cli_commands[] 是静态命令分发表)
    • [8. wpa_ctrl_request():P2P_FIND 在这里真正跨进程发送](#8. wpa_ctrl_request():P2P_FIND 在这里真正跨进程发送)
    • [9. wpa_supplicant control interface 的应用层文本命令协议](#9. wpa_supplicant control interface 的应用层文本命令协议)
      • [9.1 ATTACH 的源码语义](#9.1 ATTACH 的源码语义)
      • [9.2 为什么 interactive 模式通常有 ctrl_conn 和 mon_conn](#9.2 为什么 interactive 模式通常有 ctrl_conn 和 mon_conn)
    • [10. control interface 和真正的 P2P 网络通信不要混成一层](#10. control interface 和真正的 P2P 网络通信不要混成一层)
    • [11. 把整条 CLI 路径重新串起来](#11. 把整条 CLI 路径重新串起来)
    • [12. 本篇停止点](#12. 本篇停止点)
    • 关键源码索引
    • 资料来源
      • [S1 hostap 2.12 Git:wpa_cli 与 control client](#[S1] hostap 2.12 Git:wpa_cli 与 control client)
      • [S2 Linux man-pages:UNIX domain socket 与 socket API](#[S2] Linux man-pages:UNIX domain socket 与 socket API)
      • [S3 wpa_supplicant control interface 设计文档](#[S3] wpa_supplicant control interface 设计文档)

上一篇已经完成双 mac80211_hwsim radio、双 wpa_supplicant 2.12 实例和独立 control interface。本篇只追踪下面这条命令在 wpa_cli 进程内部 如何执行:

bash 复制代码
sudo wpa_cli-2.12 \
    -p /run/wpa_supplicant-p2p-a \
    -i wlan0 \
    p2p_find

目标是回答:

text 复制代码
Shell 输入 p2p_find
    ↓
wpa_cli 如何解析 -p / -i / p2p_find
    ↓
如何建立本地 control socket
    ↓
p2p_find 如何变成 P2P_FIND
    ↓
P2P_FIND 如何 send() 给 wpa_supplicant
    ↓
为什么最后能收到 OK / FAIL

本篇停止在:

text 复制代码
src/common/wpa_ctrl.c:wpa_ctrl_request()
    -> send()
    -> select()
    -> recv()

本文只分析 wpa_cli 进程内部的 CLI 解析、control socket client 和同步 request/reply 边界;server 进程内部不在本文展开。

源码基线固定为 wpa_supplicant 2.12;本文源码结论以同版本 wpa_cli.c、wpa_ctrl.c、eloop.c 为主要证据。S1

text 复制代码
wpa_supplicant 2.12
/opt/wifi-direct/src/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 命令进入进程以后,可以先把参数理解为:

text 复制代码
argc = 6

argv[0] = "wpa_cli-2.12"
argv[1] = "-p"
argv[2] = "/run/wpa_supplicant-p2p-a"
argv[3] = "-i"
argv[4] = "wlan0"
argv[5] = "p2p_find"
argv[6] = NULL

argc 不包含最后的 NULL。

1.1 main() 入口先调用 os_program_init()

wpa_cli.c 中与当前主线直接相关的入口片段就是:

c 复制代码
/* [解读]
 * main() 先做 OS 相关初始化。
 * 返回非 0 时直接结束;成功后才继续参数解析和 control connection 初始化。
 */
if (os_program_init())
    return -1;

随后进入 src/utils/os_unix.c:os_program_init()。当前 Ubuntu 构建没有定义 ANDROID,因此预处理之后,和本次执行路径直接相关的函数可以完整地读成:

c 复制代码
int os_program_init(void)
{
    unsigned int seed;

    /* [解读]
     * 从 OS 获取随机数据,用它初始化 libc random() 的 seed。
     * 这是当前 Ubuntu 路径真实执行的主体。
     */
    if (os_get_random((unsigned char *) &seed, sizeof(seed)) == 0)
        srandom(seed);

    return 0;
}

原始源码在随机种子初始化之前还包着一个 #ifdef ANDROID 平台分支。阅读它时可以直接按下面的预处理结构理解:

c 复制代码
#ifdef ANDROID
    /* [解读]
     * 仅 Android 构建保留:处理 Android 用户组、UID/GID 与 capability。
     * 普通 Ubuntu 构建在预处理阶段会把整个分支删除。
     */
#endif /* ANDROID */

这里要把 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 包住的调试代码。阅读结构可以理解为:

c 复制代码
#ifdef WPA_TRACE
    /* [解读]
     * 只有编译时启用 WPA_TRACE,开发调试追踪代码才会进入 binary。
     * 当前正常 p2p_find 控制路径不依赖它。
     */
#endif /* WPA_TRACE */

WPA_TRACE 同样是开发调试用的编译期开关。它主要用于追踪:

  • 内存分配;
  • 对象/注册关系;
  • 错误使用位置;
  • backtrace;
  • 退出时的内存泄漏线索。

它不是:

text 复制代码
Wi-Fi 抓包开关
P2P 状态机日志开关
802.11 trace 开关

因此本篇看到 WPA_TRACE 时,只需要知道:它服务于 wpa_supplicant 自身的开发调试,不改变当前 p2p_find 正常控制路径的业务语义。

当前实验没有专门开启它,所以后续源码阅读不沿这条分支深入。


2. getopt():-p、-i 和 p2p_find 为什么不是同一类参数

main() 接下来进入 option 解析。当前路径的核心代码可以放在一起看:

c 复制代码
for (;;) {
    /* [解读]
     * getopt() 只解析以 '-' 开头的 wpa_cli option。
     * "a:Bg:G:hi:p:P:rs:v" 中带 ':' 的 option 后面需要参数。
     */
    c = getopt(argc, argv, "a:Bg:G:hi:p:P:rs:v");
    if (c < 0)
        break;

    switch (c) {
    case 'i':
        /* [解读]
         * -i wlan0
         *     -> optarg == "wlan0"
         *     -> ctrl_ifname 保存接口名。
         */
        os_free(ctrl_ifname);
        ctrl_ifname = os_strdup(optarg);
        break;

    case 'p':
        /* [解读]
         * -p /run/wpa_supplicant-p2p-a
         *     -> ctrl_iface_dir 指向 control socket 所在目录。
         */
        ctrl_iface_dir = optarg;
        break;

    /* [解读] 当前命令没有使用的其他 option 分支此处不展开。 */
    }
}

/* [解读]
 * getopt() 结束以后,optind 指向第一个没有被 option parser 消费的参数。
 * 当前就是 argv[5] = "p2p_find"。
 */
interactive = (argc == optind) && (action_file == NULL);

当前值是:

text 复制代码
argc   = 6
optind = 5
argv[optind] = "p2p_find"

所以:

text 复制代码
argc == optind
6 == 5
false

最终:

text 复制代码
interactive = 0

这说明当前运行模式是:

text 复制代码
一次性执行 p2p_find
执行完成后退出

而不是进入:

text 复制代码
wpa_cli>

交互提示符。

参数解析过程可以压缩成:
#mermaid-svg-LMG6ZFXlPQPkQgs1{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-LMG6ZFXlPQPkQgs1 .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-LMG6ZFXlPQPkQgs1 .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-LMG6ZFXlPQPkQgs1 .error-icon{fill:#552222;}#mermaid-svg-LMG6ZFXlPQPkQgs1 .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-LMG6ZFXlPQPkQgs1 .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-LMG6ZFXlPQPkQgs1 .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-LMG6ZFXlPQPkQgs1 .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-LMG6ZFXlPQPkQgs1 .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-LMG6ZFXlPQPkQgs1 .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-LMG6ZFXlPQPkQgs1 .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-LMG6ZFXlPQPkQgs1 .marker{fill:#333333;stroke:#333333;}#mermaid-svg-LMG6ZFXlPQPkQgs1 .marker.cross{stroke:#333333;}#mermaid-svg-LMG6ZFXlPQPkQgs1 svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-LMG6ZFXlPQPkQgs1 p{margin:0;}#mermaid-svg-LMG6ZFXlPQPkQgs1 .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-LMG6ZFXlPQPkQgs1 .cluster-label text{fill:#333;}#mermaid-svg-LMG6ZFXlPQPkQgs1 .cluster-label span{color:#333;}#mermaid-svg-LMG6ZFXlPQPkQgs1 .cluster-label span p{background-color:transparent;}#mermaid-svg-LMG6ZFXlPQPkQgs1 .label text,#mermaid-svg-LMG6ZFXlPQPkQgs1 span{fill:#333;color:#333;}#mermaid-svg-LMG6ZFXlPQPkQgs1 .node rect,#mermaid-svg-LMG6ZFXlPQPkQgs1 .node circle,#mermaid-svg-LMG6ZFXlPQPkQgs1 .node ellipse,#mermaid-svg-LMG6ZFXlPQPkQgs1 .node polygon,#mermaid-svg-LMG6ZFXlPQPkQgs1 .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-LMG6ZFXlPQPkQgs1 .rough-node .label text,#mermaid-svg-LMG6ZFXlPQPkQgs1 .node .label text,#mermaid-svg-LMG6ZFXlPQPkQgs1 .image-shape .label,#mermaid-svg-LMG6ZFXlPQPkQgs1 .icon-shape .label{text-anchor:middle;}#mermaid-svg-LMG6ZFXlPQPkQgs1 .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-LMG6ZFXlPQPkQgs1 .rough-node .label,#mermaid-svg-LMG6ZFXlPQPkQgs1 .node .label,#mermaid-svg-LMG6ZFXlPQPkQgs1 .image-shape .label,#mermaid-svg-LMG6ZFXlPQPkQgs1 .icon-shape .label{text-align:center;}#mermaid-svg-LMG6ZFXlPQPkQgs1 .node.clickable{cursor:pointer;}#mermaid-svg-LMG6ZFXlPQPkQgs1 .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-LMG6ZFXlPQPkQgs1 .arrowheadPath{fill:#333333;}#mermaid-svg-LMG6ZFXlPQPkQgs1 .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-LMG6ZFXlPQPkQgs1 .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-LMG6ZFXlPQPkQgs1 .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-LMG6ZFXlPQPkQgs1 .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-LMG6ZFXlPQPkQgs1 .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-LMG6ZFXlPQPkQgs1 .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-LMG6ZFXlPQPkQgs1 .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-LMG6ZFXlPQPkQgs1 .cluster text{fill:#333;}#mermaid-svg-LMG6ZFXlPQPkQgs1 .cluster span{color:#333;}#mermaid-svg-LMG6ZFXlPQPkQgs1 div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-LMG6ZFXlPQPkQgs1 .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-LMG6ZFXlPQPkQgs1 rect.text{fill:none;stroke-width:0;}#mermaid-svg-LMG6ZFXlPQPkQgs1 .icon-shape,#mermaid-svg-LMG6ZFXlPQPkQgs1 .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-LMG6ZFXlPQPkQgs1 .icon-shape p,#mermaid-svg-LMG6ZFXlPQPkQgs1 .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-LMG6ZFXlPQPkQgs1 .icon-shape .label rect,#mermaid-svg-LMG6ZFXlPQPkQgs1 .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-LMG6ZFXlPQPkQgs1 .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-LMG6ZFXlPQPkQgs1 .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-LMG6ZFXlPQPkQgs1 :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} argv: -p DIR -i wlan0 p2p_find
getopt(): 处理 -p
ctrl_iface_dir = DIR
getopt(): 处理 -i
ctrl_ifname = wlan0
optind = 5
argv5 = p2p_find
interactive = false

关键点只有一个:

p2p_find 不属于 getopt() 的 option。getopt() 处理完 -p/-i 后,通过 optind 把真正的 CLI 子命令留给后面的命令分发逻辑。


3. main() 为什么调用了 eloop_init(),但这次回复等待却不是 epoll/kqueue

解析完参数以后,main() 在打开 control connection 前还有一段初始化:

c 复制代码
/* [解读]
 * 初始化 wpa_cli 自己的 event-loop 全局数据。
 */
if (eloop_init())
    return -1;

/* [解读]
 * 只有使用 -g/global control interface 时才进入。
 * 当前没有 -g,所以跳过。
 */
if (global && wpa_cli_open_global_ctrl() < 0)
    return -1;

/* [解读] 注册进程终止信号处理。 */
eloop_register_signal_terminate(wpa_cli_terminate, NULL);

/* [解读]
 * 当前已经通过 -i wlan0 指定 ctrl_ifname,
 * 所以不会自动扫描默认接口。
 */
if (!ctrl_ifname && !global)
    ctrl_ifname = wpa_cli_get_default_ifname();

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 系平台事件通知机制

源码逻辑本质上是:

c 复制代码
#if !defined(CONFIG_ELOOP_POLL) && \
    !defined(CONFIG_ELOOP_EPOLL) && \
    !defined(CONFIG_ELOOP_KQUEUE)
#define CONFIG_ELOOP_SELECT
#endif

也就是说:没有明确选择 poll/epoll/kqueue 时,普通 UNIX eloop 默认退回 select()。

但本篇最容易混淆的是:

eloop backend 的选择,不等于所有地方的 socket 等待都会自动改成该 backend。

当前一次性命令,例如:

bash 复制代码
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() 完成。

因此即使某个构建启用了:

text 复制代码
CONFIG_ELOOP_EPOLL=y

也不能推导成:

text 复制代码
wpa_ctrl_request() 等 OK 时使用 epoll_wait()

这是两段不同代码。


4. main() 进入 non-interactive 分支:现在才打开 control connection

当前:

text 复制代码
interactive = 0

因此 main() 进入一次性命令路径。

把两个关键调用放在同一个连续阅读单元里看。下面是按当前实际条件裁剪后的执行路径阅读版 :删掉 action_file、daemonize 和连接失败分支,只保留本次成功执行必经的语句;因此它用于理解运行顺序,不冒充完整 main() 原文。

c 复制代码
/* [解读]
 * 当前:global == NULL,ctrl_ifname == "wlan0"。
 * main() 原代码会检查返回值;本次连接成功,所以继续往下执行。
 * attach=0 表示这里只需要普通 command request/reply connection。
 */
wpa_cli_open_connection(ctrl_ifname, 0);

/* [解读]
 * control connection 建立成功以后,
 * 才把 getopt() 留下来的 argv[optind] 交给命令分发。
 */
ret = wpa_request(ctrl_conn,
                  argc - optind,
                  &argv[optind]);

原函数如果 wpa_cli_open_connection() 返回负值,会在这里报错并直接结束,不会进入后面的 wpa_request();当前主线只跟踪连接成功的实际执行路径。

当前实参等价于:

c 复制代码
wpa_cli_open_connection("wlan0", 0);

wpa_request(ctrl_conn,
            1,
            &argv[5]);

其中:

text 复制代码
&argv[5] -> "p2p_find"

所以主线现在分成两个连续问题:

  1. wpa_cli_open_connection("wlan0", 0) 怎样建立 control transport;
  2. 连接成功以后,wpa_request() 怎样把 p2p_find 分发成 P2P_FIND。

先看第一个。


5. wpa_cli_open_connection():先决定 control backend,再拼 server 地址

函数入口:

c 复制代码
static int wpa_cli_open_connection(const char *ifname, int attach)

当前参数:

text 复制代码
ifname = "wlan0"
attach = 0
interactive = 0

下面把当前 Linux/UNIX 路径集中在一个代码块里看:

c 复制代码
static int wpa_cli_open_connection(const char *ifname, int attach)
{
#if defined(CONFIG_CTRL_IFACE_UDP) || \
    defined(CONFIG_CTRL_IFACE_NAMED_PIPE)

    /* [解读]
     * 如果 binary 编译成 UDP 或 Windows Named Pipe backend,
     * 会走这里。
     * 当前 Ubuntu binary 不是这条路径。
     */
    ctrl_conn = wpa_ctrl_open(ifname);
    if (ctrl_conn == NULL)
        return -1;

    if (attach && interactive)
        mon_conn = wpa_ctrl_open(ifname);
    else
        mon_conn = NULL;

#else
    char *cfile = NULL;
    int flen, res;

    if (ifname == NULL)
        return -1;

#ifdef ANDROID
    /* [解读]
     * Android 特殊兼容路径。
     * access(path, F_OK) 这里只检查 ctrl_iface_dir 是否存在。
     * 若普通目录不存在,就先把 ifname 直接作为 cfile,
     * 后续 Android socket namespace 代码可以按平台规则解释它。
     * 当前 Ubuntu 构建没有 ANDROID,因此整段不存在。
     */
    if (access(ctrl_iface_dir, F_OK) < 0) {
        cfile = os_strdup(ifname);
        if (cfile == NULL)
            return -1;
    }
#endif /* ANDROID */

    /* [解读]
     * 如果用户通过 -s 指定 client_socket_dir,
     * 先确认这个目录存在。
     * 当前没有 -s,因此 client_socket_dir 为空,直接跳过。
     */
    if (client_socket_dir && client_socket_dir[0] &&
        access(client_socket_dir, F_OK) < 0) {
        perror(client_socket_dir);
        os_free(cfile);
        return -1;
    }

    /* [解读]
     * 当前 cfile 仍然是 NULL,进入普通 UNIX filesystem socket 路径。
     * 把 ctrl_iface_dir 和 ifname 拼成 server control socket 地址。
     */
    if (cfile == NULL) {
        flen = os_strlen(ctrl_iface_dir) + os_strlen(ifname) + 2;
        cfile = os_malloc(flen);
        if (cfile == NULL)
            return -1;

        res = os_snprintf(cfile, flen, "%s/%s",
                          ctrl_iface_dir, ifname);
        if (os_snprintf_error(flen, res)) {
            os_free(cfile);
            return -1;
        }
    }

    /* [解读]
     * 当前得到:
     * cfile = "/run/wpa_supplicant-p2p-a/wlan0"
     *
     * wpa_ctrl_open2() 才是真正创建 UNIX datagram client 的地方。
     */
    ctrl_conn = wpa_ctrl_open2(cfile, client_socket_dir);
    if (ctrl_conn == NULL) {
        os_free(cfile);
        return -1;
    }

    /* [解读]
     * 只有 attach != 0 且 interactive != 0 时才额外创建 mon_conn。
     * 当前 attach=0,所以 mon_conn=NULL。
     */
    if (attach && interactive)
        mon_conn = wpa_ctrl_open2(cfile, client_socket_dir);
    else
        mon_conn = NULL;

    os_free(cfile);
#endif

    /* [解读]
     * 当前 mon_conn == NULL,因此不会发送 ATTACH。
     * interactive 模式才会用专门的 monitor connection 订阅异步事件。
     */
    if (mon_conn) {
        if (wpa_ctrl_attach(mon_conn) == 0) {
            wpa_cli_attached = 1;
            if (interactive)
                eloop_register_read_sock(
                    wpa_ctrl_get_fd(mon_conn),
                    wpa_cli_mon_receive,
                    NULL, NULL);
        } else {
            wpa_cli_close_connection();
            return -1;
        }
    }

    return 0;
}

5.1 access() 是干什么的

这里出现两次:

c 复制代码
access(ctrl_iface_dir, F_OK)
access(client_socket_dir, F_OK)

access(path, mode) 用来检查调用进程按指定模式访问路径是否允许。本文只用到了:

mode 含义
F_OK 路径是否存在
R_OK 是否可读
W_OK 是否可写
X_OK 是否可执行/可搜索

当前代码使用 F_OK,因此不要把它理解成:

text 复制代码
这个目录一定可读可写

它只是在问:

text 复制代码
这个路径存在吗?

在当前 Ubuntu 命令里:

text 复制代码
ctrl_iface_dir = /run/wpa_supplicant-p2p-a
ifname          = wlan0

最终拼成:

text 复制代码
/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 上游配置明确偏向测试用途

要注意:

text 复制代码
P2P_FIND / STATUS / PING / ATTACH

是 control interface 的命令语义;

text 复制代码
UNIX / UDP / Named Pipe

是承载这些命令的 transport。

两者不是同一层。

6. wpa_ctrl_open2():真正创建一个 UNIX datagram client

现在进入:

text 复制代码
src/common/wpa_ctrl.c

函数:

c 复制代码
struct wpa_ctrl * wpa_ctrl_open2(const char *ctrl_path,
                                 const char *cli_path)

当前参数:

text 复制代码
ctrl_path = "/run/wpa_supplicant-p2p-a/wlan0"
cli_path  = NULL

与其拆成十几个两三行的小片段,不如直接把当前 UNIX 路径按执行顺序放在一起看:

c 复制代码
struct wpa_ctrl * wpa_ctrl_open2(const char *ctrl_path,
                                 const char *cli_path)
{
    struct wpa_ctrl *ctrl;
    static int counter = 0;
    int ret;
    int tries = 0;
    int flags;

    /* [解读] 没有 server endpoint,无法建立 control connection。 */
    if (ctrl_path == NULL)
        return NULL;

    /* [解读]
     * 为这条 client control connection 分配上下文。
     * UNIX backend 中最重要的成员是:
     *   ctrl->s      socket fd
     *   ctrl->local  client 自己的地址
     *   ctrl->dest   wpa_supplicant 的地址
     */
    ctrl = os_zalloc(sizeof(*ctrl));
    if (ctrl == NULL)
        return NULL;

    /* [解读]
     * 创建 AF_UNIX/PF_UNIX 的 SOCK_DGRAM socket。
     * 注意:这里只是创建内核 socket object,还没有本地地址,也没有 peer。
     */
    ctrl->s = socket(PF_UNIX, SOCK_DGRAM, 0);
    if (ctrl->s < 0) {
        os_free(ctrl);
        return NULL;
    }

    /* [解读] local = wpa_cli 自己的 client address。 */
    ctrl->local.sun_family = AF_UNIX;
    counter++;

try_again:
    /* [解读]
     * 如果 -s 指定了绝对 client socket 目录,使用它;
     * 否则使用默认 CONFIG_CTRL_IFACE_CLIENT_DIR(通常 /tmp)。
     *
     * 典型结果:
     * /tmp/wpa_ctrl_<pid>-<counter>
     */
    if (cli_path && cli_path[0] == '/') {
        ret = os_snprintf(ctrl->local.sun_path,
                          sizeof(ctrl->local.sun_path),
                          "%s/" CONFIG_CTRL_IFACE_CLIENT_PREFIX "%d-%d",
                          cli_path, (int) getpid(), counter);
    } else {
        ret = os_snprintf(ctrl->local.sun_path,
                          sizeof(ctrl->local.sun_path),
                          CONFIG_CTRL_IFACE_CLIENT_DIR "/"
                          CONFIG_CTRL_IFACE_CLIENT_PREFIX "%d-%d",
                          (int) getpid(), counter);
    }

    if (ret < 0 ||
        (size_t) ret >= sizeof(ctrl->local.sun_path)) {
        close(ctrl->s);
        os_free(ctrl);
        return NULL;
    }

    tries++;

    /* [解读]
     * bind() 后 client 才真正拥有一个可被回信的本地 UNIX socket 地址。
     */
    if (bind(ctrl->s,
             (struct sockaddr *) &ctrl->local,
             sizeof(ctrl->local)) < 0) {
        if (errno == EADDRINUSE && tries < 2) {
            /* [解读]
             * 可能是异常退出遗留的同名 client socket 文件。
             * 删除后重试一次。
             */
            unlink(ctrl->local.sun_path);
            goto try_again;
        }

        close(ctrl->s);
        os_free(ctrl);
        return NULL;
    }

#ifdef ANDROID
    /* [解读]
     * Android 还会在这里处理 reserved/abstract socket namespace、
     * socket ownership 等平台差异。
     * 当前 Ubuntu binary 在预处理阶段删除这一整段。
     */
#endif /* ANDROID */

    /* [解读]
     * dest = wpa_supplicant server control socket。
     */
    ctrl->dest.sun_family = AF_UNIX;
    os_strlcpy(ctrl->dest.sun_path,
               ctrl_path,
               sizeof(ctrl->dest.sun_path));

    /* [解读]
     * 对 SOCK_DGRAM 调用 connect():固定默认 peer。
     * 这里没有 TCP 三次握手,也没有 listen()/accept()。
     */
    if (connect(ctrl->s,
                (struct sockaddr *) &ctrl->dest,
                sizeof(ctrl->dest)) < 0) {
        close(ctrl->s);
        unlink(ctrl->local.sun_path);
        os_free(ctrl);
        return NULL;
    }

    /* [解读]
     * 改成 non-blocking。
     * 后续 send()/recv() 不能简单假设永远立即成功,
     * wpa_ctrl_request() 会处理 EAGAIN/EWOULDBLOCK 等情况。
     */
    flags = fcntl(ctrl->s, F_GETFL);
    if (flags >= 0) {
        flags |= O_NONBLOCK;
        fcntl(ctrl->s, F_SETFL, flags);
    }

    return ctrl;
}

执行完以后,最重要的状态是:

text 复制代码
ctrl->s
    = 一个 AF_UNIX + SOCK_DGRAM fd

ctrl->local.sun_path
    = /tmp/wpa_ctrl_<pid>-<counter>

ctrl->dest.sun_path
    = /run/wpa_supplicant-p2p-a/wlan0

两条地址不要混:

地址 属于谁 作用
/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 的文本天然是一条一条的:

text 复制代码
PING
STATUS
P2P_FIND
ATTACH
DETACH

SOCK_DGRAM 能保留 datagram/message 边界,因此收到一条 datagram 时就能按一条 command 处理。

如果改成 SOCK_STREAM:

text 复制代码
不能只改 socket(PF_UNIX, SOCK_DGRAM, 0)
                    ↓
                SOCK_STREAM

因为服务端还要同步改成:

text 复制代码
bind()
listen()
accept()
read()/write()

并设计:

text 复制代码
一条命令到哪里结束?
两条命令粘在一次 read() 里怎么办?
一条命令被拆成多次 read() 怎么办?

这就是 stream framing 问题。

6.2 SOCK_DGRAM 的 connect() 为什么不是 TCP connect()

这一句:

c 复制代码
connect(ctrl->s,
        (struct sockaddr *) &ctrl->dest,
        sizeof(ctrl->dest));

不能按 TCP 三次握手理解。

当前是:

text 复制代码
AF_UNIX + SOCK_DGRAM

这里 connect() 的核心作用是给 datagram socket 绑定一个默认通信 peer。这样后续可以直接使用已经固定 peer 的接口:

c 复制代码
send(ctrl->s, buf, len, 0);
recv(ctrl->s, reply, reply_len, 0);

而不用每次都显式携带目标地址,例如:

c 复制代码
sendto(ctrl->s,
       buf,
       len,
       0,
       (struct sockaddr *) &ctrl->dest,
       sizeof(ctrl->dest));

所以更准确的心智模型是:

text 复制代码
socket()
    -> 创建 endpoint

bind()
    -> 给 client 自己一个 local address

connect()
    -> 固定默认 peer = wpa_supplicant control endpoint

7. 返回 main():control connection 建好后才处理 p2p_find

wpa_cli_open_connection() 返回成功后,main() 执行:

c 复制代码
ret = wpa_request(ctrl_conn,
                  argc - optind,
                  &argv[optind]);

当前等价于:

c 复制代码
wpa_request(ctrl_conn, 1, &argv[5]);

所以进入 wpa_request() 后,它看到的是:

text 复制代码
argc = 1
argv[0] = "p2p_find"

不再是原始 Shell 的完整 argv[]。

7.1 wpa_cli_commands\[\] 是静态命令分发表

这里不再把整个 wpa_request() 用占位符拼成一个"伪完整函数",而是保留两个真正承担当前主线语义的连续源码片段,并直接把解释写进代码旁边。

先看查表。下面保留原函数与当前输入直接相关的匹配逻辑,并在代码内标出变量怎样变化:

c 复制代码
/* [解读]
 * 当前进入 wpa_request() 时:
 *   argc    = 1
 *   argv[0] = "p2p_find"
 */
count = 0;
cmd = wpa_cli_commands;

while (cmd->cmd) {
    if (os_strncasecmp(cmd->cmd,
                       argv[0],
                       os_strlen(argv[0])) == 0) {
        /* [解读] 先记住当前候选表项。 */
        match = cmd;

        /* [解读]
         * 当前 argv[0] 恰好就是完整字符串 "p2p_find",
         * 因此这里得到 exact match,直接结束查表。
         */
        if (os_strcasecmp(cmd->cmd, argv[0]) == 0) {
            count = 1;
            break;
        }

        /* [解读] 只有"前缀匹配但不是精确匹配"时才继续累计候选。 */
        count++;
    }

    cmd++;
}

/* [解读]
 * 当前 count == 1,因此不会进入 ambiguous/unknown command 分支。
 * 唯一表项通过函数指针进入 wpa_cli_cmd_p2p_find()。
 */
ret = match->handler(ctrl, argc - 1, &argv[1]);

命中的 handler 本身很短,可以完整看完:

c 复制代码
static int wpa_cli_cmd_p2p_find(struct wpa_ctrl *ctrl,
                                int argc,
                                char *argv[])
{
    /* [解读]
     * 这里完成 CLI command name -> control protocol command 的转换:
     *
     *     p2p_find
     *         ↓
     *     P2P_FIND
     */
    return wpa_cli_cmd(ctrl, "P2P_FIND", 0, argc, argv);
}

这里必须建立一个非常明确的概念边界:

text 复制代码
p2p_find
    = wpa_cli 提供给人的 CLI command name

P2P_FIND
    = 发送给 wpa_supplicant control interface 的文本 command

随后 wpa_cli_cmd() 会调用 write_cmd() 把命令和参数拼到 buffer 中。

当前没有参数,因此最终发送文本就是:

text 复制代码
P2P_FIND

如果以后输入:

text 复制代码
p2p_find 10 type=social

才会继续把参数拼入 control command。

当前调用继续进入:

text 复制代码
wpa_cli_cmd()
    -> wpa_ctrl_command()
    -> _wpa_ctrl_command()
    -> wpa_ctrl_request()

8. wpa_ctrl_request():P2P_FIND 在这里真正跨进程发送

_wpa_ctrl_command() 最终把:

text 复制代码
cmd = "P2P_FIND"

交给:

c 复制代码
int wpa_ctrl_request(struct wpa_ctrl *ctrl,
                     const char *cmd,
                     size_t cmd_len,
                     char *reply,
                     size_t *reply_len,
                     void (*msg_cb)(char *msg, size_t len))

这一函数包含 UDP cookie、non-blocking 重试、超时计算等多个分支。当前 binary 使用 UNIX backend,所以先把"UNIX 成功路径"按一个连续逻辑单元读完;UDP 专用分支在编译阶段不存在。

c 复制代码
/* [解读]
 * UNIX backend 不需要拼 UDP cookie,因此实际发送内容就是 cmd:
 *   _cmd     = cmd
 *   _cmd_len = cmd_len
 * 当前 cmd = "P2P_FIND"。
 */
_cmd = cmd;
_cmd_len = cmd_len;

/* [解读]
 * 原函数实际把 send() 放在 non-blocking 错误处理/有限重试逻辑里。
 * 本次成功执行时,真正跨进程发送动作就是这一句:
 */
send(ctrl->s, _cmd, _cmd_len, 0);

/* [解读]
 * 若 send() 返回 EAGAIN / EBUSY / EWOULDBLOCK,原函数会有限重试;
 * 其他发送错误会直接结束 request。发送成功后进入下面的同步收包循环。
 */
for (;;) {
    /* [解读] 每一轮都重新设置最长约 10 s 的等待时间。 */
    tv.tv_sec = 10;
    tv.tv_usec = 0;

    FD_ZERO(&rfds);
    FD_SET(ctrl->s, &rfds);

    /* [解读]
     * 同步等待 control socket 可读。
     * 这里明确调用 select();不是 eloop_run(),也不会因为
     * CONFIG_ELOOP_EPOLL/KQUEUE 而自动替换成 epoll_wait()/kevent()。
     */
    res = select(ctrl->s + 1, &rfds, NULL, NULL, &tv);

    /* [解读] 被信号中断时继续等待。 */
    if (res < 0 && errno == EINTR)
        continue;
    if (res < 0)
        return res;

    if (FD_ISSET(ctrl->s, &rfds)) {
        /* [解读] socket 可读后,一次 recv() 取一条返回 datagram。 */
        res = recv(ctrl->s, reply, *reply_len, 0);
        if (res < 0)
            return res;

        /* [解读]
         * '<' 开头或 "IFNAME=" 开头的数据属于 unsolicited message。
         * 有 msg_cb 时先交给 callback,再继续等待本次 request 的正式 reply。
         */
        if ((res > 0 && reply[0] == '<') ||
            (res > 6 && strncmp(reply, "IFNAME=", 7) == 0)) {
            if (msg_cb)
                msg_cb(reply, res);
            continue;
        }

        /* [解读] 走到这里才是当前 request 的同步 reply。 */
        *reply_len = res;
        break;
    }

    /* [解读] select() 超时,没有等到本次 request 的 reply。 */
    return -2;
}

这时跨进程边界已经非常明确:

text 复制代码
wpa_cli client socket
/tmp/wpa_ctrl_<pid>-1
        │
        │ send("P2P_FIND")
        ▼
wpa_supplicant control socket
/run/wpa_supplicant-p2p-a/wlan0

server 处理完成以后再把 reply 发回 client 地址。

wpa_ctrl_request() 收到正式 reply 后返回,最终 _wpa_ctrl_command() 打印 buffer,于是 Shell 会看到:

text 复制代码
OK

或者:

text 复制代码
FAIL

这里的 OK 只表示:

control interface 接收并成功受理了当前 P2P_FIND request。

它不表示:

text 复制代码
已经发现 peer
已经完成后续 P2P 协议阶段
已经建立 P2P Group

那些不属于本篇的同步 control request/reply 主线。


9. wpa_supplicant control interface 的应用层文本命令协议

ATTACH、DETACH、PING、STATUS、P2P_FIND 等属于 wpa_supplicant control interface 的应用层文本命令协议 。S3

它们不是:

text 复制代码
IEEE 802.11 management frame
Wi-Fi Direct 空口 Action Frame
P2P peer 之间直接交换的无线报文

9.1 ATTACH 的源码语义

wpa_ctrl.c 中:

c 复制代码
static int wpa_ctrl_attach_helper(struct wpa_ctrl *ctrl, int attach)
{
    char buf[10];
    int ret;
    size_t len = 10;

    /* [解读]
     * attach=1 -> 发送 "ATTACH"
     * attach=0 -> 发送 "DETACH"
     *
     * 它仍然只是通过已经存在的 control connection
     * 发送一条文本 command。
     */
    ret = wpa_ctrl_request(ctrl,
                           attach ? "ATTACH" : "DETACH",
                           6,
                           buf,
                           &len,
                           NULL);

    if (ret < 0)
        return ret;

    if (len == 3 && os_memcmp(buf, "OK\n", 3) == 0)
        return 0;

    return -1;
}

int wpa_ctrl_attach(struct wpa_ctrl *ctrl)
{
    return wpa_ctrl_attach_helper(ctrl, 1);
}

int wpa_ctrl_detach(struct wpa_ctrl *ctrl)
{
    return wpa_ctrl_attach_helper(ctrl, 0);
}

因此:

text 复制代码
wpa_ctrl_open2()
    = 建立 transport endpoint

ATTACH
    = 在 transport 已经存在以后发送的一条协议命令
    = 请求把当前 client 注册成 event monitor

DETACH
    = 取消 monitor registration

ATTACH 不是"建立 socket"。

9.2 为什么 interactive 模式通常有 ctrl_conn 和 mon_conn

前面 wpa_cli_open_connection() 已经看到:

c 复制代码
ctrl_conn = wpa_ctrl_open2(cfile, client_socket_dir);

if (attach && interactive)
    mon_conn = wpa_ctrl_open2(cfile, client_socket_dir);
else
    mon_conn = NULL;

interactive 模式中通常把职责分开:

connection 主要职责
ctrl_conn 普通 command request/reply
mon_conn ATTACH 后持续接收 unsolicited event

当前一次性命令,例如:

bash 复制代码
sudo wpa_cli-2.12 -p /run/wpa_supplicant-p2p-a -i wlan0 p2p_find

调用:

c 复制代码
wpa_cli_open_connection(ctrl_ifname, 0)

所以:

text 复制代码
ctrl_conn = 有
mon_conn  = NULL
ATTACH    = 不因为这次 p2p_find 自动执行

bash 复制代码
sudo wpa_cli-2.12 \
    -p /run/wpa_supplicant-p2p-a \
    -i wlan0 \
    help

它直接来自当前 binary 对应的 wpa_cli_commands[],因此不会出现"查到别的版本命令表"的问题。

如果只查 P2P,则优先看当前源码目录中的:

text 复制代码
wpa_supplicant/README-P2P

10. control interface 和真正的 P2P 网络通信不要混成一层

这一层关系只需要保留抽象技术边界,不引入具体业务角色。

当前文章讨论的是:

text 复制代码
同一台 Linux 主机内

wpa_cli
  -> AF_UNIX + SOCK_DGRAM
  -> wpa_supplicant control interface

这属于:

text 复制代码
本机 control plane / IPC

而 Wi-Fi Direct Group 真正建立以后,另一层才是:

text 复制代码
P2P Device A
    -> 802.11 P2P link
    -> IP
    -> TCP/UDP/其他上层协议
    -> P2P Device B

所以:

text 复制代码
/run/wpa_supplicant-p2p-a/wlan0

不是远端 P2P peer 的网络地址。

UNIX Domain Socket 本身也不能跨机器。

这一区分后面非常重要:

text 复制代码
ctrl_iface 的 UDP backend

也不能自动等价为:

text 复制代码
P2P Group 建成以后,两台设备之间应该使用 UDP 业务通信

前者是 wpa_supplicant 的控制接口承载方式;后者是 P2P Group 形成后的 IP 数据面协议选择。两层应分别分析。


11. 把整条 CLI 路径重新串起来

到这里,wpa_cli 侧已经形成闭环:
#mermaid-svg-PQGlXwSFtSTF3WBE{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-PQGlXwSFtSTF3WBE .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-PQGlXwSFtSTF3WBE .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-PQGlXwSFtSTF3WBE .error-icon{fill:#552222;}#mermaid-svg-PQGlXwSFtSTF3WBE .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-PQGlXwSFtSTF3WBE .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-PQGlXwSFtSTF3WBE .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-PQGlXwSFtSTF3WBE .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-PQGlXwSFtSTF3WBE .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-PQGlXwSFtSTF3WBE .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-PQGlXwSFtSTF3WBE .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-PQGlXwSFtSTF3WBE .marker{fill:#333333;stroke:#333333;}#mermaid-svg-PQGlXwSFtSTF3WBE .marker.cross{stroke:#333333;}#mermaid-svg-PQGlXwSFtSTF3WBE svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-PQGlXwSFtSTF3WBE p{margin:0;}#mermaid-svg-PQGlXwSFtSTF3WBE .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-PQGlXwSFtSTF3WBE .cluster-label text{fill:#333;}#mermaid-svg-PQGlXwSFtSTF3WBE .cluster-label span{color:#333;}#mermaid-svg-PQGlXwSFtSTF3WBE .cluster-label span p{background-color:transparent;}#mermaid-svg-PQGlXwSFtSTF3WBE .label text,#mermaid-svg-PQGlXwSFtSTF3WBE span{fill:#333;color:#333;}#mermaid-svg-PQGlXwSFtSTF3WBE .node rect,#mermaid-svg-PQGlXwSFtSTF3WBE .node circle,#mermaid-svg-PQGlXwSFtSTF3WBE .node ellipse,#mermaid-svg-PQGlXwSFtSTF3WBE .node polygon,#mermaid-svg-PQGlXwSFtSTF3WBE .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-PQGlXwSFtSTF3WBE .rough-node .label text,#mermaid-svg-PQGlXwSFtSTF3WBE .node .label text,#mermaid-svg-PQGlXwSFtSTF3WBE .image-shape .label,#mermaid-svg-PQGlXwSFtSTF3WBE .icon-shape .label{text-anchor:middle;}#mermaid-svg-PQGlXwSFtSTF3WBE .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-PQGlXwSFtSTF3WBE .rough-node .label,#mermaid-svg-PQGlXwSFtSTF3WBE .node .label,#mermaid-svg-PQGlXwSFtSTF3WBE .image-shape .label,#mermaid-svg-PQGlXwSFtSTF3WBE .icon-shape .label{text-align:center;}#mermaid-svg-PQGlXwSFtSTF3WBE .node.clickable{cursor:pointer;}#mermaid-svg-PQGlXwSFtSTF3WBE .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-PQGlXwSFtSTF3WBE .arrowheadPath{fill:#333333;}#mermaid-svg-PQGlXwSFtSTF3WBE .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-PQGlXwSFtSTF3WBE .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-PQGlXwSFtSTF3WBE .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-PQGlXwSFtSTF3WBE .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-PQGlXwSFtSTF3WBE .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-PQGlXwSFtSTF3WBE .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-PQGlXwSFtSTF3WBE .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-PQGlXwSFtSTF3WBE .cluster text{fill:#333;}#mermaid-svg-PQGlXwSFtSTF3WBE .cluster span{color:#333;}#mermaid-svg-PQGlXwSFtSTF3WBE div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-PQGlXwSFtSTF3WBE .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-PQGlXwSFtSTF3WBE rect.text{fill:none;stroke-width:0;}#mermaid-svg-PQGlXwSFtSTF3WBE .icon-shape,#mermaid-svg-PQGlXwSFtSTF3WBE .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-PQGlXwSFtSTF3WBE .icon-shape p,#mermaid-svg-PQGlXwSFtSTF3WBE .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-PQGlXwSFtSTF3WBE .icon-shape .label rect,#mermaid-svg-PQGlXwSFtSTF3WBE .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-PQGlXwSFtSTF3WBE .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-PQGlXwSFtSTF3WBE .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-PQGlXwSFtSTF3WBE :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} Shell: wpa_cli -p DIR -i wlan0 p2p_find
main(): os_program_init()
getopt(): DIR / wlan0 / optind
interactive = false
eloop_init()
wpa_cli_open_connection(wlan0, 0)
wpa_ctrl_open2(): socket -> bind -> connect -> nonblock
wpa_request(): 查 wpa_cli_commands\[\]
wpa_cli_cmd_p2p_find(): p2p_find -> P2P_FIND
wpa_ctrl_request(): send(P2P_FIND)
select() 等待 ctrl socket 可读
recv(): OK / FAIL

现在再看四个关键边界:

边界 正确理解
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 认识即可

最后再强调一次本篇最重要的执行事实:

text 复制代码
一次性 `wpa_cli [options] p2p_find`

虽然前面调用了:

text 复制代码
eloop_init()

但 P2P_FIND 同步 reply 的等待点仍然是:

text 复制代码
wpa_ctrl_request()
    -> select()
    -> recv()

不是 eloop_run()。


12. 本篇停止点

本篇只完成 client 侧:

text 复制代码
Shell
  -> main()
  -> os_program_init()
  -> getopt()/optind
  -> eloop_init()
  -> wpa_cli_open_connection()
  -> wpa_ctrl_open2()
  -> AF_UNIX/SOCK_DGRAM client
  -> wpa_request()
  -> wpa_cli_commands[]
  -> wpa_cli_cmd_p2p_find()
  -> P2P_FIND
  -> wpa_ctrl_request()
  -> send()
  -> select()
  -> recv()
  -> OK/FAIL

本篇到 wpa_ctrl_request() 收到 OK/FAIL 为止;这已经完整回答了 wpa_cli p2p_find 如何把文本命令发送到 control interface 并同步等待 reply。

关键源码索引

关键对象 / 符号 本文位置 Git 源码
main() [CLI 入口](#关键对象 / 符号 本文位置 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) hostap 2.12
wpa_cli_open_connection() [control client 打开](#关键对象 / 符号 本文位置 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) hostap 2.12
wpa_ctrl_open2() [AF_UNIX client](#关键对象 / 符号 本文位置 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) hostap 2.12
wpa_cli_cmd_p2p_find() [p2p_find 命令分发](#关键对象 / 符号 本文位置 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) hostap 2.12
wpa_ctrl_request() [send/select/recv](#关键对象 / 符号 本文位置 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) hostap 2.12

资料来源

S1 hostap 2.12 Git:wpa_cli 与 control client

S2 Linux man-pages:UNIX domain socket 与 socket API

  • URL/文档:unix(7)、connect(2)、select(2)
  • 使用位置:第 5~8 节
  • 支撑内容:AF_UNIX pathname socket、SOCK_DGRAM、datagram connect() 与 select() readiness 语义。

S3 wpa_supplicant control interface 设计文档

相关推荐
P1Browser1 小时前
换了 IP 还是被关联:WebRTC 泄露本地 IP 的三种触发路径
网络协议·tcp/ip·webrtc
DD云验证1 小时前
DD云验证,免费网络验证系统
服务器·python·易语言·网络验证·卡密验证
skywalk81631 小时前
Deepseek harness的4种模式 配置文件内容
linux·人工智能·ubuntu·deepseek·harness
HRTOS2 小时前
HRTOS Shell继续完善:优化printf缓冲区与字符串越界保护,国庆后随新版本更新
运维·单片机·嵌入式硬件·算法·51单片机
智能RPA2 小时前
农业与矿业行业智能体自动化平台对比评测(计量与巡检场景)
运维·人工智能·python·自动化·agent·rpa
沫璃染墨2 小时前
《从零入门Linux系统篇(五十一):线程篇·四——pthread线程库详解:从线程创建到终止与分离》
linux·运维·服务器·开发语言·c++·系统架构·线程
被摘下的星星2 小时前
HTTP 状态码 和 网络端口号
网络·网络协议·http
云边云科技_云网融合2 小时前
生物医疗跨院区影像与诊疗数据传输:高带宽与合规双重目标下的网络搭建指南
网络·科技
墨家前任巨子2 小时前
定时关机脚本
运维