Wi-Fi Direct 源码分析(09):从 P2P_CONNECT 到 GO Negotiation 完成

Wi-Fi Direct 源码分析(09):从 P2P_CONNECT 到 GO Negotiation 完成

摘要:从 CLI 显式执行 P2P_CONNECT 开始,追踪 Find 停止、可选 Provision Discovery、GO Intent、Action frame 与 GO Negotiation 完成回调。

文章目录

  • [Wi-Fi Direct 源码分析(09):从 P2P_CONNECT 到 GO Negotiation 完成](#Wi-Fi Direct 源码分析(09):从 P2P_CONNECT 到 GO Negotiation 完成)
    • [1. 先用三个终端把 P2P_CONNECT 完整跑通](#1. 先用三个终端把 P2P_CONNECT 完整跑通)
      • [1.1 Terminal A:启动 A 端 wpa_supplicant](#1.1 Terminal A:启动 A 端 wpa_supplicant)
      • [1.2 Terminal B:启动 B 端 wpa_supplicant](#1.2 Terminal B:启动 B 端 wpa_supplicant)
      • [1.3 Terminal C:双方先执行 P2P_FIND](#1.3 Terminal C:双方先执行 P2P_FIND)
      • [1.4 Terminal C:确认双方 peer table](#1.4 Terminal C:确认双方 peer table)
      • [1.5 Terminal C:B 先授权 A 使用 PBC](#1.5 Terminal C:B 先授权 A 使用 PBC)
      • [1.6 Terminal C:A 显式执行真正的 P2P_CONNECT](#1.6 Terminal C:A 显式执行真正的 P2P_CONNECT)
      • [1.7 本次实验的重要结果摘要](#1.7 本次实验的重要结果摘要)
      • [1.8 把三个终端的动作压缩成一张图](#1.8 把三个终端的动作压缩成一张图)
    • [2. P2P_CONNECT 是第二条独立的 control command](#2. P2P_CONNECT 是第二条独立的 control command)
    • [3. p2p_ctrl_connect() 如何进入 wpas_p2p_connect() 和 P2P Core](#3. p2p_ctrl_connect() 如何进入 wpas_p2p_connect() 和 P2P Core)
    • [4. p2p_connect():找回 peer、准备参数并停止 Find](#4. p2p_connect():找回 peer、准备参数并停止 Find)
      • [4.1 Find 是怎样被停止的](#4.1 Find 是怎样被停止的)
      • [4.2 正在 driver 中执行的 Scan 为什么不会硬取消](#4.2 正在 driver 中执行的 Scan 为什么不会硬取消)
    • [5. Provision Discovery 是什么,为什么是可选的](#5. Provision Discovery 是什么,为什么是可选的)
    • [6. Provision Discovery Response 为什么又回到 p2p_connect_send()](#6. Provision Discovery Response 为什么又回到 p2p_connect_send())
    • [7. GO Negotiation 的三帧主线与 Action frame 异步发送](#7. GO Negotiation 的三帧主线与 Action frame 异步发送)
      • [7.1 Request 发出去以后为什么不能直接"等待函数返回"](#7.1 Request 发出去以后为什么不能直接“等待函数返回”)
    • [8. GO Intent、tie breaker 与 p2p_go_det() 到底解决什么](#8. GO Intent、tie breaker 与 p2p_go_det() 到底解决什么)
      • [8.1 角色决定并不是 GO Negotiation 的全部](#8.1 角色决定并不是 GO Negotiation 的全部)
    • [9. Request、Response、Confirm 是怎样被接收和处理的](#9. Request、Response、Confirm 是怎样被接收和处理的)
      • [9.1 Responder:收到 Request](#9.1 Responder:收到 Request)
      • [9.2 Initiator:收到 Response](#9.2 Initiator:收到 Response)
      • [9.3 Confirm 接收函数 p2p_handle_go_neg_conf](#9.3 Confirm 接收函数 p2p_handle_go_neg_conf)
    • [10. TX status 与 Confirm 如何最终进入 p2p_go_complete()](#10. TX status 与 Confirm 如何最终进入 p2p_go_complete())
    • [11. classic P2P 与 hostap 2.12 的 P2P2 分支有什么区别](#11. classic P2P 与 hostap 2.12 的 P2P2 分支有什么区别)
    • [12. 本篇主线回看](#12. 本篇主线回看)
    • 关键源码索引
    • 资料来源
      • [S1 hostap 2.12 Git:P2P_CONNECT 与 GO Negotiation 实现](#[S1] hostap 2.12 Git:P2P_CONNECT 与 GO Negotiation 实现)
      • [S2 Wi-Fi Direct Specification v1.9](#[S2] Wi-Fi Direct Specification v1.9)
      • [S3 hostap 2.12 README-P2P](#[S3] hostap 2.12 README-P2P)
      • [S4 Wi-Fi Protected Setup / hostap WPS implementation](#[S4] Wi-Fi Protected Setup / hostap WPS implementation)
      • [S5 双 mac80211_hwsim 实验记录](#[S5] 双 mac80211_hwsim 实验记录)

第 08 篇已经把 Device Discovery 追到 P2P-DEVICE-FOUND,并确认 peer 已进入 P2P Core 的 p2p->devices。这一步只表示"已经发现对端", 不会自动进入 Group Formation 。

真正的连接阶段需要上层再次做一个明确动作:选择一个已经发现的 P2P Device Address,然后发送第二条 control command:

text 复制代码
P2P_CONNECT <peer_device_addr> ...

hostap 2.12 的 README-P2P 也把 p2p_connect 定义为"与已发现的 P2P peer 开始 Group Formation"的独立命令。S3

本文只追踪 classic/legacy P2P 新建 Group 的 GO Negotiation 主线,停止在 wpas_go_neg_completed() 收到协商结果的位置。WPS provisioning、关联、4-Way Handshake 和最终 P2P-GROUP-STARTED 不在本文展开。

源码基线统一为 hostap 2.12 release tag hostap_2_12。S1


1. 先用三个终端把 P2P_CONNECT 完整跑通

在进入 P2P_CONNECT 源码之前,先把本文后续要追踪的真实实验完整执行一遍。实验固定使用三个终端:Terminal A 只运行 A 的 wpa_supplicant,Terminal B 只运行 B 的 wpa_supplicant,Terminal C 专门使用 wpa_cli 发送 control command。这样 A/B 两侧的 -dd -t 日志不会与控制命令混在一起。

1.1 Terminal A:启动 A 端 wpa_supplicant

bash 复制代码
sudo wpa_supplicant-2.12 \
    -Dnl80211 \
    -i "$P2P_A" \
    -c /workspace/config/p2p-a.conf \
    -dd \
    -t

Terminal A 保持前台运行,用于观察 A 端 Device Discovery、GO Negotiation 以及后续 group interface 的日志。

1.2 Terminal B:启动 B 端 wpa_supplicant

bash 复制代码
sudo wpa_supplicant-2.12 \
    -Dnl80211 \
    -i "$P2P_B" \
    -c /workspace/config/p2p-b.conf \
    -dd \
    -t

Terminal B 同样保持前台运行。

1.3 Terminal C:双方先执行 P2P_FIND

先让 A 进入 Device Discovery:

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

本次实测返回:

text 复制代码
OK

再让 B 也进入 Device Discovery:

bash 复制代码
sudo wpa_cli-2.12 \
    -p /run/wpa_supplicant-p2p-b \
    -i "$P2P_B" \
    p2p_find

本次实测同样返回:

text 复制代码
OK

这里让两侧都执行 p2p_find 的原因是:后续不仅 A 要根据 peer table 找到 B,B 还要对已经发现的 A 执行 p2p_connect ... auth。OK 只表示 P2P_FIND control command 已被接受,不表示已经发现 peer。

1.4 Terminal C:确认双方 peer table

查看 A 当前发现到的 peer:

bash 复制代码
sudo wpa_cli-2.12 \
    -p /run/wpa_supplicant-p2p-a \
    -i "$P2P_A" \
    p2p_peers

本次实测得到:

text 复制代码
42:00:00:00:01:00

因此 B 的 P2P Device Address 为:

text 复制代码
42:00:00:00:01:00

再查看 B 的 peer table:

bash 复制代码
sudo wpa_cli-2.12 \
    -p /run/wpa_supplicant-p2p-b \
    -i "$P2P_B" \
    p2p_peers

本次实测得到:

text 复制代码
42:00:00:00:00:00

因此 A 的 P2P Device Address 为:

text 复制代码
42:00:00:00:00:00

p2p_peers 返回的是当前 P2P Core peer table 中的 Device Address。后续 P2P_CONNECT 使用的就是这里的 P2P Device Address ,不能把文章中的 <A_P2P_DEVICE_ADDR>、<B_P2P_DEVICE_ADDR> 占位符原样输入 shell;尖括号在 Bash 中还具有重定向语义。

1.5 Terminal C:B 先授权 A 使用 PBC

B 对 A 执行:

bash 复制代码
sudo wpa_cli-2.12 \
    -p /run/wpa_supplicant-p2p-b \
    -i "$P2P_B" \
    p2p_connect 42:00:00:00:00:00 pbc auth

本次实测返回:

text 复制代码
OK

这里的 auth 不是"B 主动开始 GO Negotiation"。它的含义是:B 为指定 peer 记录本轮允许使用的 WPS/PBC 授权参数,然后等待该 peer 主动发起 GO Negotiation。S3

因此这一步之后,B 是被授权的等待方;真正的主动发起动作还没有发生。

1.6 Terminal C:A 显式执行真正的 P2P_CONNECT

A 使用刚才从 p2p_peers 得到的 B Device Address:

bash 复制代码
sudo wpa_cli-2.12 \
    -p /run/wpa_supplicant-p2p-a \
    -i "$P2P_A" \
    p2p_connect 42:00:00:00:01:00 pbc

本次实测返回:

text 复制代码
OK

这条命令才是本文源码主线真正的起点。它会进入:

text 复制代码
P2P_CONNECT
    -> p2p_ctrl_connect()
    -> wpas_p2p_connect()
    -> p2p_connect()
    -> Provision Discovery(条件满足时)
    -> GO Negotiation

这里的 OK 仍然只表示 control command 已被接受,不表示 GO Negotiation、WPS 或 4-Way Handshake 已经同步完成。后续阶段通过 eloop、Action frame RX/TX status、driver event 等异步事件继续推进。

1.7 本次实验的重要结果摘要

本轮真实执行结果如下。S5

项目 实测结果
A P2P Device Address 42:00:00:00:00:00
B P2P Device Address 42:00:00:00:01:00
主动发起 P2P_CONNECT A
auth 等待方 B
最终 GO B
最终 Client A
GO Device Address 42:00:00:00:01:00
Group SSID DIRECT-fM
Operating Frequency 2412 MHz(2.4 GHz Channel 1)
A group interface p2p-wlan0-0,角色 client
B group interface p2p-wlan1-0,角色 GO

需要特别区分两件事:谁主动发送 P2P_CONNECT,并不等于谁最终成为 GO。 本次是 A 主动发起,但 GO Negotiation 的结果是 B 成为 GO、A 成为 Client。角色由后面要分析的 GO Intent、tie breaker 与协商参数共同决定,而不是由 initiator/responder 身份直接决定。S1S2

最终日志已经进入 P2P-GROUP-STARTED,说明整套实验确实成功建立了 Group;这里只把该结果作为实验闭环,不在本节提前展开 WPS、关联和 4-Way Handshake 的实现细节。S5

1.8 把三个终端的动作压缩成一张图

#mermaid-svg-dJkzijWnRbP9ZjXv{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-dJkzijWnRbP9ZjXv .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-dJkzijWnRbP9ZjXv .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-dJkzijWnRbP9ZjXv .error-icon{fill:#552222;}#mermaid-svg-dJkzijWnRbP9ZjXv .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-dJkzijWnRbP9ZjXv .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-dJkzijWnRbP9ZjXv .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-dJkzijWnRbP9ZjXv .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-dJkzijWnRbP9ZjXv .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-dJkzijWnRbP9ZjXv .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-dJkzijWnRbP9ZjXv .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-dJkzijWnRbP9ZjXv .marker{fill:#333333;stroke:#333333;}#mermaid-svg-dJkzijWnRbP9ZjXv .marker.cross{stroke:#333333;}#mermaid-svg-dJkzijWnRbP9ZjXv svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-dJkzijWnRbP9ZjXv p{margin:0;}#mermaid-svg-dJkzijWnRbP9ZjXv .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-dJkzijWnRbP9ZjXv .cluster-label text{fill:#333;}#mermaid-svg-dJkzijWnRbP9ZjXv .cluster-label span{color:#333;}#mermaid-svg-dJkzijWnRbP9ZjXv .cluster-label span p{background-color:transparent;}#mermaid-svg-dJkzijWnRbP9ZjXv .label text,#mermaid-svg-dJkzijWnRbP9ZjXv span{fill:#333;color:#333;}#mermaid-svg-dJkzijWnRbP9ZjXv .node rect,#mermaid-svg-dJkzijWnRbP9ZjXv .node circle,#mermaid-svg-dJkzijWnRbP9ZjXv .node ellipse,#mermaid-svg-dJkzijWnRbP9ZjXv .node polygon,#mermaid-svg-dJkzijWnRbP9ZjXv .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-dJkzijWnRbP9ZjXv .rough-node .label text,#mermaid-svg-dJkzijWnRbP9ZjXv .node .label text,#mermaid-svg-dJkzijWnRbP9ZjXv .image-shape .label,#mermaid-svg-dJkzijWnRbP9ZjXv .icon-shape .label{text-anchor:middle;}#mermaid-svg-dJkzijWnRbP9ZjXv .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-dJkzijWnRbP9ZjXv .rough-node .label,#mermaid-svg-dJkzijWnRbP9ZjXv .node .label,#mermaid-svg-dJkzijWnRbP9ZjXv .image-shape .label,#mermaid-svg-dJkzijWnRbP9ZjXv .icon-shape .label{text-align:center;}#mermaid-svg-dJkzijWnRbP9ZjXv .node.clickable{cursor:pointer;}#mermaid-svg-dJkzijWnRbP9ZjXv .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-dJkzijWnRbP9ZjXv .arrowheadPath{fill:#333333;}#mermaid-svg-dJkzijWnRbP9ZjXv .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-dJkzijWnRbP9ZjXv .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-dJkzijWnRbP9ZjXv .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-dJkzijWnRbP9ZjXv .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-dJkzijWnRbP9ZjXv .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-dJkzijWnRbP9ZjXv .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-dJkzijWnRbP9ZjXv .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-dJkzijWnRbP9ZjXv .cluster text{fill:#333;}#mermaid-svg-dJkzijWnRbP9ZjXv .cluster span{color:#333;}#mermaid-svg-dJkzijWnRbP9ZjXv 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-dJkzijWnRbP9ZjXv .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-dJkzijWnRbP9ZjXv rect.text{fill:none;stroke-width:0;}#mermaid-svg-dJkzijWnRbP9ZjXv .icon-shape,#mermaid-svg-dJkzijWnRbP9ZjXv .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-dJkzijWnRbP9ZjXv .icon-shape p,#mermaid-svg-dJkzijWnRbP9ZjXv .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-dJkzijWnRbP9ZjXv .icon-shape .label rect,#mermaid-svg-dJkzijWnRbP9ZjXv .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-dJkzijWnRbP9ZjXv .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-dJkzijWnRbP9ZjXv .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-dJkzijWnRbP9ZjXv :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} Terminal A: 启动 wpa_supplicant A
Terminal B: 启动 wpa_supplicant B
Terminal C: A p2p_find -> OK
Terminal C: B p2p_find -> OK
A p2p_peers -> 42:00:00:00:01:00
B p2p_peers -> 42:00:00:00:00:00
B: p2p_connect A pbc auth -> OK
A: p2p_connect B pbc -> OK
进入 P2P_CONNECT / GO Negotiation 源码主线
本次结果: B=GO, A=Client, 2412 MHz

因此实验链不是"启动后自动连接",而是明确的两阶段控制过程:先用 P2P_FIND 建立 peer 信息,再由上层显式发送 P2P_CONNECT。pbc 指的是 WPS 的 Push Button Configuration 方法,不存在独立的"PBC Negotiation"协议阶段;它是 Group Formation 选择的 WPS provisioning method,而 GO/Client 角色由 GO Negotiation 决定。S2S4


2. P2P_CONNECT 是第二条独立的 control command

第 02~05 篇已经建立了 control interface 的通用路径,所以这里不重新解释 UNIX control socket。wpa_supplicant_ctrl_iface_process() 收到文本命令后直接匹配:

c 复制代码
} else if (os_strncmp(buf, "P2P_CONNECT ", 12) == 0) {
        reply_len = p2p_ctrl_connect(wpa_s, buf + 12, reply,
                                     reply_size);
}

这说明 P2P_FIND 与 P2P_CONNECT 是两个独立命令:

text 复制代码
P2P_FIND
  目标:发现 peer

P2P_CONNECT
  目标:对一个已经发现的 peer 发起 Group Formation

p2p_ctrl_connect() 负责解析:

  • peer device address;
  • pbc / PIN / P2PS 等 provisioning method;
  • go_intent=<0..15>;
  • persistent;
  • freq=;
  • provdisc;
  • auth、join、auto、p2p2 等其它模式参数。S1S3

当前经典新建 Group 主线是:

text 复制代码
P2P_CONNECT <peer> pbc [go_intent=N] [provdisc]

如果使用 auth,control 层最终进入 p2p_authorize();如果没有 auth,则沿本文主线进入真正的连接发起路径。


3. p2p_ctrl_connect() 如何进入 wpas_p2p_connect() 和 P2P Core

p2p_ctrl_connect() 完成参数解析后,将结果交给 wpas_p2p_connect()。这一层仍属于 wpa_supplicant glue,负责把 control 参数、Group interface 需求、频率偏好、persistent 标志等整理成持续存在的 Group Formation 上下文。S1

经典主线可以压缩成:
#mermaid-svg-SBuGzCRP0iYlyT6R{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-SBuGzCRP0iYlyT6R .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-SBuGzCRP0iYlyT6R .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-SBuGzCRP0iYlyT6R .error-icon{fill:#552222;}#mermaid-svg-SBuGzCRP0iYlyT6R .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-SBuGzCRP0iYlyT6R .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-SBuGzCRP0iYlyT6R .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-SBuGzCRP0iYlyT6R .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-SBuGzCRP0iYlyT6R .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-SBuGzCRP0iYlyT6R .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-SBuGzCRP0iYlyT6R .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-SBuGzCRP0iYlyT6R .marker{fill:#333333;stroke:#333333;}#mermaid-svg-SBuGzCRP0iYlyT6R .marker.cross{stroke:#333333;}#mermaid-svg-SBuGzCRP0iYlyT6R svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-SBuGzCRP0iYlyT6R p{margin:0;}#mermaid-svg-SBuGzCRP0iYlyT6R .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-SBuGzCRP0iYlyT6R .cluster-label text{fill:#333;}#mermaid-svg-SBuGzCRP0iYlyT6R .cluster-label span{color:#333;}#mermaid-svg-SBuGzCRP0iYlyT6R .cluster-label span p{background-color:transparent;}#mermaid-svg-SBuGzCRP0iYlyT6R .label text,#mermaid-svg-SBuGzCRP0iYlyT6R span{fill:#333;color:#333;}#mermaid-svg-SBuGzCRP0iYlyT6R .node rect,#mermaid-svg-SBuGzCRP0iYlyT6R .node circle,#mermaid-svg-SBuGzCRP0iYlyT6R .node ellipse,#mermaid-svg-SBuGzCRP0iYlyT6R .node polygon,#mermaid-svg-SBuGzCRP0iYlyT6R .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-SBuGzCRP0iYlyT6R .rough-node .label text,#mermaid-svg-SBuGzCRP0iYlyT6R .node .label text,#mermaid-svg-SBuGzCRP0iYlyT6R .image-shape .label,#mermaid-svg-SBuGzCRP0iYlyT6R .icon-shape .label{text-anchor:middle;}#mermaid-svg-SBuGzCRP0iYlyT6R .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-SBuGzCRP0iYlyT6R .rough-node .label,#mermaid-svg-SBuGzCRP0iYlyT6R .node .label,#mermaid-svg-SBuGzCRP0iYlyT6R .image-shape .label,#mermaid-svg-SBuGzCRP0iYlyT6R .icon-shape .label{text-align:center;}#mermaid-svg-SBuGzCRP0iYlyT6R .node.clickable{cursor:pointer;}#mermaid-svg-SBuGzCRP0iYlyT6R .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-SBuGzCRP0iYlyT6R .arrowheadPath{fill:#333333;}#mermaid-svg-SBuGzCRP0iYlyT6R .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-SBuGzCRP0iYlyT6R .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-SBuGzCRP0iYlyT6R .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-SBuGzCRP0iYlyT6R .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-SBuGzCRP0iYlyT6R .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-SBuGzCRP0iYlyT6R .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-SBuGzCRP0iYlyT6R .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-SBuGzCRP0iYlyT6R .cluster text{fill:#333;}#mermaid-svg-SBuGzCRP0iYlyT6R .cluster span{color:#333;}#mermaid-svg-SBuGzCRP0iYlyT6R 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-SBuGzCRP0iYlyT6R .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-SBuGzCRP0iYlyT6R rect.text{fill:none;stroke-width:0;}#mermaid-svg-SBuGzCRP0iYlyT6R .icon-shape,#mermaid-svg-SBuGzCRP0iYlyT6R .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-SBuGzCRP0iYlyT6R .icon-shape p,#mermaid-svg-SBuGzCRP0iYlyT6R .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-SBuGzCRP0iYlyT6R .icon-shape .label rect,#mermaid-svg-SBuGzCRP0iYlyT6R .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-SBuGzCRP0iYlyT6R .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-SBuGzCRP0iYlyT6R .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-SBuGzCRP0iYlyT6R :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} p2p_ctrl_connect()
wpas_p2p_connect()
准备 WPS / GO Intent / frequency / Group context
wpas_p2p_start_go_neg()
P2P Core: p2p_connect()

真正与 struct p2p_device、P2P state、Public Action frame 和 GO Negotiation 状态机打交道的是 src/p2p/ 中的 p2p_connect()。


4. p2p_connect():找回 peer、准备参数并停止 Find

p2p_connect() 的第一步不是重新扫描,而是直接使用第 08 篇已经建立的 peer table:

c 复制代码
dev = p2p_get_device(p2p, peer_addr);
if (dev == NULL || (dev->flags & P2P_DEV_PROBE_REQ_ONLY)) {
        p2p_dbg(p2p, "Cannot connect to unknown P2P Device " MACSTR,
                MAC2STR(peer_addr));
        return -1;
}

这解释了为什么 Device Discovery 需要建立 struct p2p_device,而不能只打印 P2P-DEVICE-FOUND。P2P_CONNECT 会根据 peer address 重新取回完整的 peer 对象。S1

接下来 p2p_connect() 调用 p2p_prepare_channel(),准备本端允许用于 GO Negotiation 的 Channel List、preferred/forced operating channel 等,然后保存本轮组建参数:

c 复制代码
p2p_set_dev_persistent(dev, persistent_group);
p2p->go_intent = go_intent;
os_memcpy(p2p->intended_addr, own_interface_addr, ETH_ALEN);

如果启用 pd_before_go_neg,给 peer 设置 P2P_DEV_PD_BEFORE_GO_NEG;否则为本轮 GO Negotiation 分配 dialog token 和 tie breaker:

c 复制代码
if (pd_before_go_neg)
        dev->flags |= P2P_DEV_PD_BEFORE_GO_NEG;
else {
        dev->flags &= ~P2P_DEV_PD_BEFORE_GO_NEG;
        dev->dialog_token++;
        if (dev->dialog_token == 0)
                dev->dialog_token = 1;
        dev->tie_breaker = p2p->next_tie_breaker;
        p2p->next_tie_breaker = !p2p->next_tie_breaker;
}

4.1 Find 是怎样被停止的

如果此时仍处于第 08 篇的 Find/Search:

c 复制代码
if (p2p->state != P2P_IDLE)
        p2p_stop_find(p2p);

这里不是异常退出,也不是简单修改一个全局布尔变量。p2p_stop_find() 继续进入:

c 复制代码
void p2p_stop_find(struct p2p_data *p2p)
{
        p2p->pending_listen_freq = 0;
        p2p_stop_find_for_freq(p2p, 0);
}

p2p_stop_find_for_freq() 做的是一整套 Find 生命周期收尾:S1

text 复制代码
取消 p2p_find_timeout
    ↓
清除 P2P Core 当前 timeout
    ↓
若处于 P2P_SEARCH / P2P_SD_DURING_FIND,通知 find_stopped callback
    ↓
state -> P2P_IDLE
    ↓
释放本轮 requested device type
    ↓
清理 go_neg_peer / sd_peer / invite_peer 等 Find 上下文
    ↓
停止 Listen
    ↓
清除 send_action_in_progress

所以"停止 Find"的本质是:终止 Device Discovery 状态机及其 timeout/listen 上下文,把 P2P Core 重新切到可以开始 Group Formation 的状态。

4.2 正在 driver 中执行的 Scan 为什么不会硬取消

p2p_stop_find() 清理的是 P2P Find 状态机;如果已经有一轮 P2P scan 提交给 driver,并且 p2p->p2p_scan_running 仍为真,p2p_connect() 不会假装这次异步 scan 已经不存在,而是记录:

c 复制代码
if (p2p->p2p_scan_running) {
        p2p_dbg(p2p, "p2p_scan running - delay connect send");
        p2p->start_after_scan = P2P_AFTER_SCAN_CONNECT;
        os_memcpy(p2p->after_scan_peer, peer_addr, ETH_ALEN);
        return 0;
}

也就是:
#mermaid-svg-NA3ZeGsxtv90CSJS{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-NA3ZeGsxtv90CSJS .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-NA3ZeGsxtv90CSJS .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-NA3ZeGsxtv90CSJS .error-icon{fill:#552222;}#mermaid-svg-NA3ZeGsxtv90CSJS .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-NA3ZeGsxtv90CSJS .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-NA3ZeGsxtv90CSJS .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-NA3ZeGsxtv90CSJS .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-NA3ZeGsxtv90CSJS .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-NA3ZeGsxtv90CSJS .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-NA3ZeGsxtv90CSJS .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-NA3ZeGsxtv90CSJS .marker{fill:#333333;stroke:#333333;}#mermaid-svg-NA3ZeGsxtv90CSJS .marker.cross{stroke:#333333;}#mermaid-svg-NA3ZeGsxtv90CSJS svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-NA3ZeGsxtv90CSJS p{margin:0;}#mermaid-svg-NA3ZeGsxtv90CSJS .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-NA3ZeGsxtv90CSJS .cluster-label text{fill:#333;}#mermaid-svg-NA3ZeGsxtv90CSJS .cluster-label span{color:#333;}#mermaid-svg-NA3ZeGsxtv90CSJS .cluster-label span p{background-color:transparent;}#mermaid-svg-NA3ZeGsxtv90CSJS .label text,#mermaid-svg-NA3ZeGsxtv90CSJS span{fill:#333;color:#333;}#mermaid-svg-NA3ZeGsxtv90CSJS .node rect,#mermaid-svg-NA3ZeGsxtv90CSJS .node circle,#mermaid-svg-NA3ZeGsxtv90CSJS .node ellipse,#mermaid-svg-NA3ZeGsxtv90CSJS .node polygon,#mermaid-svg-NA3ZeGsxtv90CSJS .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-NA3ZeGsxtv90CSJS .rough-node .label text,#mermaid-svg-NA3ZeGsxtv90CSJS .node .label text,#mermaid-svg-NA3ZeGsxtv90CSJS .image-shape .label,#mermaid-svg-NA3ZeGsxtv90CSJS .icon-shape .label{text-anchor:middle;}#mermaid-svg-NA3ZeGsxtv90CSJS .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-NA3ZeGsxtv90CSJS .rough-node .label,#mermaid-svg-NA3ZeGsxtv90CSJS .node .label,#mermaid-svg-NA3ZeGsxtv90CSJS .image-shape .label,#mermaid-svg-NA3ZeGsxtv90CSJS .icon-shape .label{text-align:center;}#mermaid-svg-NA3ZeGsxtv90CSJS .node.clickable{cursor:pointer;}#mermaid-svg-NA3ZeGsxtv90CSJS .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-NA3ZeGsxtv90CSJS .arrowheadPath{fill:#333333;}#mermaid-svg-NA3ZeGsxtv90CSJS .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-NA3ZeGsxtv90CSJS .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-NA3ZeGsxtv90CSJS .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-NA3ZeGsxtv90CSJS .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-NA3ZeGsxtv90CSJS .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-NA3ZeGsxtv90CSJS .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-NA3ZeGsxtv90CSJS .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-NA3ZeGsxtv90CSJS .cluster text{fill:#333;}#mermaid-svg-NA3ZeGsxtv90CSJS .cluster span{color:#333;}#mermaid-svg-NA3ZeGsxtv90CSJS 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-NA3ZeGsxtv90CSJS .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-NA3ZeGsxtv90CSJS rect.text{fill:none;stroke-width:0;}#mermaid-svg-NA3ZeGsxtv90CSJS .icon-shape,#mermaid-svg-NA3ZeGsxtv90CSJS .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-NA3ZeGsxtv90CSJS .icon-shape p,#mermaid-svg-NA3ZeGsxtv90CSJS .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-NA3ZeGsxtv90CSJS .icon-shape .label rect,#mermaid-svg-NA3ZeGsxtv90CSJS .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-NA3ZeGsxtv90CSJS .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-NA3ZeGsxtv90CSJS .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-NA3ZeGsxtv90CSJS :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 否
是
P2P_CONNECT 到达
P2P scan 仍在 driver 中运行?
p2p_connect_send()
start_after_scan = P2P_AFTER_SCAN_CONNECT
保存 after_scan_peer
等待现有 scan completion

这里延续了第 07/08 篇建立的异步模型:已经交给内核/driver 的操作不会因为上层状态机切换就同步消失。


5. Provision Discovery 是什么,为什么是可选的

Provision Discovery(PD)是一段 P2P Public Action frame 交换,用来确认双方在后续 Group Formation 中准备采用什么 provisioning/WPS method,例如 PBC、Display PIN、Keypad PIN。S2

它解决的是:

text 复制代码
"双方准备用什么 provisioning method?"

而不是:

text 复制代码
"谁成为 GO?"

后者由 GO Negotiation 解决。

hostap 2.12 的 README-P2P 说明,provdisc 参数用于在 GO Negotiation 之前主动插入 Provision Discovery,主要作为与某些实现互操作时的兼容路径。因此它不是每次 P2P_CONNECT 的强制步骤。S3

当 P2P_DEV_PD_BEFORE_GO_NEG 存在时,第一次进入 p2p_connect_send():

c 复制代码
if (dev->flags & P2P_DEV_PD_BEFORE_GO_NEG) {
        u16 config_method;
        ...
        return p2p_prov_disc_req(p2p, dev->info.p2p_device_addr,
                                 NULL, config_method, 0, 0, 1);
}

PBC 在这里会映射到:

text 复制代码
WPS_CONFIG_PUSHBUTTON

随后发送 Provision Discovery Request,并从当前调用栈返回。


6. Provision Discovery Response 为什么又回到 p2p_connect_send()

PD 是异步空口交换。Request 发送后不会同步调用 Response handler;对端的 Provision Discovery Response 作为后续收到的 P2P Public Action frame 再进入 P2P Core。

接收侧不是直接跳进 p2p_process_prov_disc_resp()。Public Action frame 会先沿 P2P Core 的统一 RX 分发链进入对应 subtype handler:S1

text 复制代码
p2p_rx_action()
    ↓ category == WLAN_ACTION_PUBLIC
p2p_rx_action_public()
    ↓ vendor specific / P2P OUI
p2p_rx_p2p_action()
    ↓ subtype == P2P_PROV_DISC_RESP
p2p_handle_prov_disc_resp()
    ↓ parse P2P/WPS attributes
p2p_process_prov_disc_resp()

p2p_process_prov_disc_resp() 在 PD-before-GO-Neg 成功后做的关键动作是:S1

text 复制代码
清除 P2P_DEV_PD_BEFORE_GO_NEG
    ↓
结束当前 send-action 上下文
    ↓
再次调用 p2p_connect_send(p2p, dev)

第二次进入 p2p_connect_send() 时,PD 标志已经被清掉,因此不会形成死循环,而是继续进入 GO Negotiation Request 构造路径。
Peer Initiator Peer Initiator #mermaid-svg-vzgnXjgMol7H97XX{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-vzgnXjgMol7H97XX .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-vzgnXjgMol7H97XX .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-vzgnXjgMol7H97XX .error-icon{fill:#552222;}#mermaid-svg-vzgnXjgMol7H97XX .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-vzgnXjgMol7H97XX .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-vzgnXjgMol7H97XX .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-vzgnXjgMol7H97XX .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-vzgnXjgMol7H97XX .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-vzgnXjgMol7H97XX .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-vzgnXjgMol7H97XX .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-vzgnXjgMol7H97XX .marker{fill:#333333;stroke:#333333;}#mermaid-svg-vzgnXjgMol7H97XX .marker.cross{stroke:#333333;}#mermaid-svg-vzgnXjgMol7H97XX svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-vzgnXjgMol7H97XX p{margin:0;}#mermaid-svg-vzgnXjgMol7H97XX .actor{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-vzgnXjgMol7H97XX text.actor>tspan{fill:black;stroke:none;}#mermaid-svg-vzgnXjgMol7H97XX .actor-line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-vzgnXjgMol7H97XX .innerArc{stroke-width:1.5;stroke-dasharray:none;}#mermaid-svg-vzgnXjgMol7H97XX .messageLine0{stroke-width:1.5;stroke-dasharray:none;stroke:#333;}#mermaid-svg-vzgnXjgMol7H97XX .messageLine1{stroke-width:1.5;stroke-dasharray:2,2;stroke:#333;}#mermaid-svg-vzgnXjgMol7H97XX #arrowhead path{fill:#333;stroke:#333;}#mermaid-svg-vzgnXjgMol7H97XX .sequenceNumber{fill:white;}#mermaid-svg-vzgnXjgMol7H97XX #sequencenumber{fill:#333;}#mermaid-svg-vzgnXjgMol7H97XX #crosshead path{fill:#333;stroke:#333;}#mermaid-svg-vzgnXjgMol7H97XX .messageText{fill:#333;stroke:none;}#mermaid-svg-vzgnXjgMol7H97XX .labelBox{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-vzgnXjgMol7H97XX .labelText,#mermaid-svg-vzgnXjgMol7H97XX .labelText>tspan{fill:black;stroke:none;}#mermaid-svg-vzgnXjgMol7H97XX .loopText,#mermaid-svg-vzgnXjgMol7H97XX .loopText>tspan{fill:black;stroke:none;}#mermaid-svg-vzgnXjgMol7H97XX .loopLine{stroke-width:2px;stroke-dasharray:2,2;stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-vzgnXjgMol7H97XX .note{stroke:#aaaa33;fill:#fff5ad;}#mermaid-svg-vzgnXjgMol7H97XX .noteText,#mermaid-svg-vzgnXjgMol7H97XX .noteText>tspan{fill:black;stroke:none;}#mermaid-svg-vzgnXjgMol7H97XX .activation0{fill:#f4f4f4;stroke:#666;}#mermaid-svg-vzgnXjgMol7H97XX .activation1{fill:#f4f4f4;stroke:#666;}#mermaid-svg-vzgnXjgMol7H97XX .activation2{fill:#f4f4f4;stroke:#666;}#mermaid-svg-vzgnXjgMol7H97XX .actorPopupMenu{position:absolute;}#mermaid-svg-vzgnXjgMol7H97XX .actorPopupMenuPanel{position:absolute;fill:#ECECFF;box-shadow:0px 8px 16px 0px rgba(0,0,0,0.2);filter:drop-shadow(3px 5px 2px rgb(0 0 0 / 0.4));}#mermaid-svg-vzgnXjgMol7H97XX .actor-man line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-vzgnXjgMol7H97XX .actor-man circle,#mermaid-svg-vzgnXjgMol7H97XX line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;stroke-width:2px;}#mermaid-svg-vzgnXjgMol7H97XX :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} p2p_process_prov_disc_resp() clear P2P_DEV_PD_BEFORE_GO_NEG p2p_connect_send() 再次进入 Provision Discovery Request Provision Discovery Response GO Negotiation Request

因此"PD Response 回来后又回到 p2p_connect_send()"不是绕路,而是 hostap 对这条可选前置交换使用的明确异步续接点。


7. GO Negotiation 的三帧主线与 Action frame 异步发送

当不再需要 PD 时,p2p_connect_send() 构造 GO Negotiation Request:

c 复制代码
req = p2p_build_go_neg_req(p2p, dev);
...
p2p_set_state(p2p, P2P_CONNECT);
p2p->pending_action_state = P2P_PENDING_GO_NEG_REQUEST;
p2p->go_neg_peer = dev;
...
p2p_send_action(...);

经典 GO Negotiation 是三帧 P2P Public Action 交换:S2
Responder Initiator Responder Initiator #mermaid-svg-qzgOh59QnpkhTH0p{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-qzgOh59QnpkhTH0p .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-qzgOh59QnpkhTH0p .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-qzgOh59QnpkhTH0p .error-icon{fill:#552222;}#mermaid-svg-qzgOh59QnpkhTH0p .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-qzgOh59QnpkhTH0p .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-qzgOh59QnpkhTH0p .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-qzgOh59QnpkhTH0p .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-qzgOh59QnpkhTH0p .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-qzgOh59QnpkhTH0p .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-qzgOh59QnpkhTH0p .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-qzgOh59QnpkhTH0p .marker{fill:#333333;stroke:#333333;}#mermaid-svg-qzgOh59QnpkhTH0p .marker.cross{stroke:#333333;}#mermaid-svg-qzgOh59QnpkhTH0p svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-qzgOh59QnpkhTH0p p{margin:0;}#mermaid-svg-qzgOh59QnpkhTH0p .actor{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-qzgOh59QnpkhTH0p text.actor>tspan{fill:black;stroke:none;}#mermaid-svg-qzgOh59QnpkhTH0p .actor-line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-qzgOh59QnpkhTH0p .innerArc{stroke-width:1.5;stroke-dasharray:none;}#mermaid-svg-qzgOh59QnpkhTH0p .messageLine0{stroke-width:1.5;stroke-dasharray:none;stroke:#333;}#mermaid-svg-qzgOh59QnpkhTH0p .messageLine1{stroke-width:1.5;stroke-dasharray:2,2;stroke:#333;}#mermaid-svg-qzgOh59QnpkhTH0p #arrowhead path{fill:#333;stroke:#333;}#mermaid-svg-qzgOh59QnpkhTH0p .sequenceNumber{fill:white;}#mermaid-svg-qzgOh59QnpkhTH0p #sequencenumber{fill:#333;}#mermaid-svg-qzgOh59QnpkhTH0p #crosshead path{fill:#333;stroke:#333;}#mermaid-svg-qzgOh59QnpkhTH0p .messageText{fill:#333;stroke:none;}#mermaid-svg-qzgOh59QnpkhTH0p .labelBox{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-qzgOh59QnpkhTH0p .labelText,#mermaid-svg-qzgOh59QnpkhTH0p .labelText>tspan{fill:black;stroke:none;}#mermaid-svg-qzgOh59QnpkhTH0p .loopText,#mermaid-svg-qzgOh59QnpkhTH0p .loopText>tspan{fill:black;stroke:none;}#mermaid-svg-qzgOh59QnpkhTH0p .loopLine{stroke-width:2px;stroke-dasharray:2,2;stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-qzgOh59QnpkhTH0p .note{stroke:#aaaa33;fill:#fff5ad;}#mermaid-svg-qzgOh59QnpkhTH0p .noteText,#mermaid-svg-qzgOh59QnpkhTH0p .noteText>tspan{fill:black;stroke:none;}#mermaid-svg-qzgOh59QnpkhTH0p .activation0{fill:#f4f4f4;stroke:#666;}#mermaid-svg-qzgOh59QnpkhTH0p .activation1{fill:#f4f4f4;stroke:#666;}#mermaid-svg-qzgOh59QnpkhTH0p .activation2{fill:#f4f4f4;stroke:#666;}#mermaid-svg-qzgOh59QnpkhTH0p .actorPopupMenu{position:absolute;}#mermaid-svg-qzgOh59QnpkhTH0p .actorPopupMenuPanel{position:absolute;fill:#ECECFF;box-shadow:0px 8px 16px 0px rgba(0,0,0,0.2);filter:drop-shadow(3px 5px 2px rgb(0 0 0 / 0.4));}#mermaid-svg-qzgOh59QnpkhTH0p .actor-man line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-qzgOh59QnpkhTH0p .actor-man circle,#mermaid-svg-qzgOh59QnpkhTH0p line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;stroke-width:2px;}#mermaid-svg-qzgOh59QnpkhTH0p :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} GO Negotiation Request GO Negotiation Response GO Negotiation Confirm

7.1 Request 发出去以后为什么不能直接"等待函数返回"

p2p_send_action() 最终通过初始化时绑定的 cfg->send_action = wpas_send_action 回到 supplicant。wpas_send_action() 如果当前频率没有可复用的 radio work,会先创建 p2p-send-action work;work 真正启动后进入 wpas_send_action_cb(),再调用 offchannel_send_action() 把 Action frame 交给 driver。S1

发送完成或失败后,driver 上报 EVENT_TX_STATUS,supplicant 的 off-channel 层把 ACK/NO_ACK/FAILED 转换成 wpas_p2p_send_action_tx_status(),随后重新进入 P2P Core:

text 复制代码
p2p_send_action()
    ↓ cfg->send_action
wpas_send_action()
    ↓ radio_add_work("p2p-send-action")
wpas_send_action_cb()
    ↓
offchannel_send_action()
    ↓ driver TX
EVENT_TX_STATUS
    ↓
offchannel_send_action_tx_status()
    ↓
wpas_p2p_send_action_tx_status()
    ↓
p2p_send_action_cb()

p2p_send_action_cb() 根据 pending_action_state 分发:S1

text 复制代码
P2P_PENDING_GO_NEG_REQUEST
    -> p2p_go_neg_req_cb()

P2P_PENDING_GO_NEG_RESPONSE
    -> p2p_go_neg_resp_cb()

P2P_PENDING_GO_NEG_CONFIRM
    -> p2p_go_neg_conf_cb()

所以 Action frame 发送与"协议下一步"之间存在明确异步边界:
#mermaid-svg-XXACZE3Yid4MGe0r{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-XXACZE3Yid4MGe0r .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-XXACZE3Yid4MGe0r .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-XXACZE3Yid4MGe0r .error-icon{fill:#552222;}#mermaid-svg-XXACZE3Yid4MGe0r .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-XXACZE3Yid4MGe0r .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-XXACZE3Yid4MGe0r .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-XXACZE3Yid4MGe0r .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-XXACZE3Yid4MGe0r .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-XXACZE3Yid4MGe0r .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-XXACZE3Yid4MGe0r .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-XXACZE3Yid4MGe0r .marker{fill:#333333;stroke:#333333;}#mermaid-svg-XXACZE3Yid4MGe0r .marker.cross{stroke:#333333;}#mermaid-svg-XXACZE3Yid4MGe0r svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-XXACZE3Yid4MGe0r p{margin:0;}#mermaid-svg-XXACZE3Yid4MGe0r .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-XXACZE3Yid4MGe0r .cluster-label text{fill:#333;}#mermaid-svg-XXACZE3Yid4MGe0r .cluster-label span{color:#333;}#mermaid-svg-XXACZE3Yid4MGe0r .cluster-label span p{background-color:transparent;}#mermaid-svg-XXACZE3Yid4MGe0r .label text,#mermaid-svg-XXACZE3Yid4MGe0r span{fill:#333;color:#333;}#mermaid-svg-XXACZE3Yid4MGe0r .node rect,#mermaid-svg-XXACZE3Yid4MGe0r .node circle,#mermaid-svg-XXACZE3Yid4MGe0r .node ellipse,#mermaid-svg-XXACZE3Yid4MGe0r .node polygon,#mermaid-svg-XXACZE3Yid4MGe0r .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-XXACZE3Yid4MGe0r .rough-node .label text,#mermaid-svg-XXACZE3Yid4MGe0r .node .label text,#mermaid-svg-XXACZE3Yid4MGe0r .image-shape .label,#mermaid-svg-XXACZE3Yid4MGe0r .icon-shape .label{text-anchor:middle;}#mermaid-svg-XXACZE3Yid4MGe0r .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-XXACZE3Yid4MGe0r .rough-node .label,#mermaid-svg-XXACZE3Yid4MGe0r .node .label,#mermaid-svg-XXACZE3Yid4MGe0r .image-shape .label,#mermaid-svg-XXACZE3Yid4MGe0r .icon-shape .label{text-align:center;}#mermaid-svg-XXACZE3Yid4MGe0r .node.clickable{cursor:pointer;}#mermaid-svg-XXACZE3Yid4MGe0r .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-XXACZE3Yid4MGe0r .arrowheadPath{fill:#333333;}#mermaid-svg-XXACZE3Yid4MGe0r .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-XXACZE3Yid4MGe0r .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-XXACZE3Yid4MGe0r .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-XXACZE3Yid4MGe0r .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-XXACZE3Yid4MGe0r .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-XXACZE3Yid4MGe0r .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-XXACZE3Yid4MGe0r .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-XXACZE3Yid4MGe0r .cluster text{fill:#333;}#mermaid-svg-XXACZE3Yid4MGe0r .cluster span{color:#333;}#mermaid-svg-XXACZE3Yid4MGe0r 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-XXACZE3Yid4MGe0r .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-XXACZE3Yid4MGe0r rect.text{fill:none;stroke-width:0;}#mermaid-svg-XXACZE3Yid4MGe0r .icon-shape,#mermaid-svg-XXACZE3Yid4MGe0r .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-XXACZE3Yid4MGe0r .icon-shape p,#mermaid-svg-XXACZE3Yid4MGe0r .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-XXACZE3Yid4MGe0r .icon-shape .label rect,#mermaid-svg-XXACZE3Yid4MGe0r .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-XXACZE3Yid4MGe0r .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-XXACZE3Yid4MGe0r .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-XXACZE3Yid4MGe0r :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} P2P Core 构造 Action frame
p2p_send_action()
wpa_supplicant / driver
TX status
p2p_send_action_cb()
按 pending_action_state 继续状态机

这也是为什么只画 p2p_process_go_neg_req() -> Confirm 会漏掉真实运行机制。


8. GO Intent、tie breaker 与 p2p_go_det() 到底解决什么

GO Intent 的作用是表达"本设备希望成为 Group Owner 的倾向",范围是 0..15。它不是优先级越高就无条件成为 GO;真正结果由双方 Intent、tie breaker 以及后续参数合法性共同决定。S1S2

hostap 2.12 的角色决定函数非常直接:

c 复制代码
static int p2p_go_det(u8 own_intent, u8 peer_value)
{
        u8 peer_intent = peer_value >> 1;
        if (own_intent == peer_intent) {
                if (own_intent == P2P_MAX_GO_INTENT)
                        return -1;

                return (peer_value & 0x01) ? 0 : 1;
        }

        return own_intent > peer_intent;
}

其中 peer 的 GO Intent Attribute 把两类信息编码在一起:

text 复制代码
高位:peer GO Intent
最低位:tie breaker

因此规则是:

情况 结果
本端 Intent > 对端 本端成为 GO
本端 Intent < 对端 对端成为 GO
两端 Intent 相同且不是 15 tie breaker 决定唯一 GO
两端都是 15 不能得到唯一 GO,协商失败

go_intent=0 也不是"禁止成为 GO"。如果双方都是 0,仍会由 tie breaker 决出一个 GO。

8.1 角色决定并不是 GO Negotiation 的全部

p2p_process_go_neg_req() / p2p_process_go_neg_resp() 还会验证和处理:S1

  • Capability;
  • Listen Channel;
  • Operating Channel;
  • Channel List;
  • Intended P2P Interface Address;
  • Configuration Timeout;
  • WPS Device Password ID / provisioning method;
  • Group ID;
  • 双方共同可用信道。

例如收到 Response 后会明确检查:

text 复制代码
是否有 GO Intent
    ↓
p2p_go_det()
    ↓
是否有 Group ID(本端为 Client 时)
    ↓
是否有 Channel List
    ↓
p2p_peer_channels()
    ↓
是否存在 common channels
    ↓
WPS method 是否兼容
    ↓
必要时选择最终 operating channel

所以 GO Negotiation 的产物不是一个单独的 role_go,而是一整套后续 Group Formation 依赖的协商参数。


9. Request、Response、Confirm 是怎样被接收和处理的

收到 P2P Public Action frame 后,P2P Core 会根据 subtype 分发。GO Negotiation 三类消息分别落到 Request、Response、Confirm 处理路径。S1

9.1 Responder:收到 Request

Responder 进入:

text 复制代码
p2p_process_go_neg_req()

如果 peer 没有预授权 WPS method,经典路径会返回 P2P_SC_FAIL_INFO_CURRENTLY_UNAVAILABLE,同时调用 go_neg_req_rx callback 通知上层"有 peer 请求 GO Negotiation"。这正是实验里先用:

text 复制代码
p2p_connect <peer> pbc auth

进行预授权的原因之一。

peer 已授权后,p2p_process_go_neg_req() 会:

text 复制代码
解析 Request
    ↓
检查必需 P2P attributes
    ↓
p2p_go_det() 决定角色
    ↓
检查/求交 Channel List
    ↓
验证 WPS method
    ↓
构造 GO Negotiation Response
    ↓
设置 P2P_GO_NEG / WAIT_GO_NEG_CONFIRM 等状态

9.2 Initiator:收到 Response

Initiator 的核心处理函数是:

text 复制代码
p2p_process_go_neg_resp()

成功后构造 Confirm,再由:

text 复制代码
p2p_handle_go_neg_resp()
    ↓
p2p_send_action(... GO Negotiation Confirm ...)

把 Confirm 发送出去。

9.3 Confirm 接收函数 p2p_handle_go_neg_conf

它会检查:

  • 当前 go_neg_peer 是否匹配;
  • 是否确实在等待 Confirm;
  • dialog token;
  • Status;
  • Remote GO 时的 Group ID;
  • 其它协商结果。

然后继续完成 GO Negotiation。


10. TX status 与 Confirm 如何最终进入 p2p_go_complete()

发起端发送 Confirm 后,不能在当前调用栈里立即认为协议完成。pending_action_state 会被设成:

text 复制代码
P2P_PENDING_GO_NEG_CONFIRM

后续 Action frame TX status 回来后:

text 复制代码
p2p_send_action_cb()
    ↓
p2p_go_neg_conf_cb()

p2p_go_neg_conf_cb() 会处理 ACK、发送失败以及必要的 Confirm 重试。满足完成条件后,P2P Core 才进入 GO Negotiation 完成路径。S1

Responder 则在真正收到 Confirm 时通过:

text 复制代码
p2p_handle_go_neg_conf()

完成对应处理。

两端最后都会汇合到 GO Negotiation completion:

text 复制代码
整理协商结果
    ↓
struct p2p_go_neg_results
    ↓
P2P state -> P2P_PROVISIONING
    ↓
cfg->go_neg_completed(...)

P2P 初始化时,supplicant 已经绑定:

c 复制代码
p2p.go_neg_completed = wpas_go_neg_completed;

因此 P2P Core 的结果通过 callback 返回:
#mermaid-svg-PQmYyesCd25ZPaA5{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-PQmYyesCd25ZPaA5 .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-PQmYyesCd25ZPaA5 .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-PQmYyesCd25ZPaA5 .error-icon{fill:#552222;}#mermaid-svg-PQmYyesCd25ZPaA5 .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-PQmYyesCd25ZPaA5 .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-PQmYyesCd25ZPaA5 .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-PQmYyesCd25ZPaA5 .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-PQmYyesCd25ZPaA5 .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-PQmYyesCd25ZPaA5 .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-PQmYyesCd25ZPaA5 .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-PQmYyesCd25ZPaA5 .marker{fill:#333333;stroke:#333333;}#mermaid-svg-PQmYyesCd25ZPaA5 .marker.cross{stroke:#333333;}#mermaid-svg-PQmYyesCd25ZPaA5 svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-PQmYyesCd25ZPaA5 p{margin:0;}#mermaid-svg-PQmYyesCd25ZPaA5 .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-PQmYyesCd25ZPaA5 .cluster-label text{fill:#333;}#mermaid-svg-PQmYyesCd25ZPaA5 .cluster-label span{color:#333;}#mermaid-svg-PQmYyesCd25ZPaA5 .cluster-label span p{background-color:transparent;}#mermaid-svg-PQmYyesCd25ZPaA5 .label text,#mermaid-svg-PQmYyesCd25ZPaA5 span{fill:#333;color:#333;}#mermaid-svg-PQmYyesCd25ZPaA5 .node rect,#mermaid-svg-PQmYyesCd25ZPaA5 .node circle,#mermaid-svg-PQmYyesCd25ZPaA5 .node ellipse,#mermaid-svg-PQmYyesCd25ZPaA5 .node polygon,#mermaid-svg-PQmYyesCd25ZPaA5 .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-PQmYyesCd25ZPaA5 .rough-node .label text,#mermaid-svg-PQmYyesCd25ZPaA5 .node .label text,#mermaid-svg-PQmYyesCd25ZPaA5 .image-shape .label,#mermaid-svg-PQmYyesCd25ZPaA5 .icon-shape .label{text-anchor:middle;}#mermaid-svg-PQmYyesCd25ZPaA5 .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-PQmYyesCd25ZPaA5 .rough-node .label,#mermaid-svg-PQmYyesCd25ZPaA5 .node .label,#mermaid-svg-PQmYyesCd25ZPaA5 .image-shape .label,#mermaid-svg-PQmYyesCd25ZPaA5 .icon-shape .label{text-align:center;}#mermaid-svg-PQmYyesCd25ZPaA5 .node.clickable{cursor:pointer;}#mermaid-svg-PQmYyesCd25ZPaA5 .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-PQmYyesCd25ZPaA5 .arrowheadPath{fill:#333333;}#mermaid-svg-PQmYyesCd25ZPaA5 .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-PQmYyesCd25ZPaA5 .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-PQmYyesCd25ZPaA5 .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-PQmYyesCd25ZPaA5 .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-PQmYyesCd25ZPaA5 .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-PQmYyesCd25ZPaA5 .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-PQmYyesCd25ZPaA5 .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-PQmYyesCd25ZPaA5 .cluster text{fill:#333;}#mermaid-svg-PQmYyesCd25ZPaA5 .cluster span{color:#333;}#mermaid-svg-PQmYyesCd25ZPaA5 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-PQmYyesCd25ZPaA5 .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-PQmYyesCd25ZPaA5 rect.text{fill:none;stroke-width:0;}#mermaid-svg-PQmYyesCd25ZPaA5 .icon-shape,#mermaid-svg-PQmYyesCd25ZPaA5 .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-PQmYyesCd25ZPaA5 .icon-shape p,#mermaid-svg-PQmYyesCd25ZPaA5 .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-PQmYyesCd25ZPaA5 .icon-shape .label rect,#mermaid-svg-PQmYyesCd25ZPaA5 .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-PQmYyesCd25ZPaA5 .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-PQmYyesCd25ZPaA5 .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-PQmYyesCd25ZPaA5 :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} P2P Core GO Negotiation 完成
p2p_go_neg_results
cfg->go_neg_completed
wpas_go_neg_completed()
P2P-GO-NEG-SUCCESS

wpas_go_neg_completed() 能拿到的关键结果包括:

text 复制代码
role_go
peer_device_addr
peer_interface_addr
freq
SSID / Group ID
WPS method
persistent flag
P2P2 related result

到这里才完成本文的核心问题:本轮 GO Negotiation 已经产生角色和 Group Formation 所需的协商结果。

P2P-GO-NEG-SUCCESS 仍不等于 P2P-GROUP-STARTED。此时还没有完成后续 WPS provisioning 与最终安全数据连接。


11. classic P2P 与 hostap 2.12 的 P2P2 分支有什么区别

hostap 2.12 在多个 P2P 数据结构和流程中增加了 p2p2 标志,并出现 P2P2 IE、PASN、PMK/PMKID、SAE 等新路径。S1

这里需要避免把源码里的一个 p2p2 布尔值简单等同于"所有 Wi-Fi Direct Release 2 功能"。更准确的说法是:本文的 classic 路径使用传统 P2P GO Negotiation + WPS provisioning;hostap 2.12 的 P2P2 路径则引入了新的 P2P2/PASN 安全与 provisioning 机制。

对比维度 classic/legacy P2P hostap P2P2 路径
本文 CLI 主线 p2p_connect <peer> pbc p2p_connect ... p2p2 ... 等扩展参数
GO Negotiation 经典 P2P Public Action 交换 P2P2 相关信息可结合 PASN 等新机制
provisioning WPS PBC/PIN 是核心路径 可使用 P2P2 pairing / PASN 派生安全材料
Group security setup 后续通过 WPS credential 再进入 WPA/RSN 可携带/派生 PMK、PMKID、SAE/PASN 相关结果
代码复杂度 依赖 classic P2P + WPS 主线,组件边界较少 需要额外理解 PASN、P2P2 IE、pairing/security result
工程优势 与只实现 classic P2P/WPS 的 peer 对接时路径直接;hostap 中长期存在,调试资料多 能使用 hostap 2.12 新增的 P2P2/PASN 安全能力,并把部分安全材料放到新的 pairing 流程中形成
工程限制 provisioning 依赖 WPS PBC/PIN,后续还需要正常 WPA/RSN 连接 依赖双方都支持对应 P2P2/PASN 能力及构建配置,代码路径和互操作前提更多
更适合的学习/测试场景 建立 Wi-Fi Direct 基础心智模型、验证传统设备互通 专门验证 P2P2/PASN、新 pairing/security 能力时单独展开

这些"优势/限制"是从 hostap 2.12 的实现依赖和控制流得出的工程比较,不应理解成协议层面对所有设备的绝对优劣。当前实验与源码学习主线继续使用 classic PBC 路径,这样可以把 Device Discovery、GO Negotiation、WPS 和 WPA 4-Way Handshake 的边界逐层看清。P2P2 分支保留为版本差异,不混入本文主调用链。


12. 本篇主线回看

本篇现在可以压缩成下面这条完整运行链:
#mermaid-svg-l9neeMNwEV3V5EXP{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-l9neeMNwEV3V5EXP .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-l9neeMNwEV3V5EXP .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-l9neeMNwEV3V5EXP .error-icon{fill:#552222;}#mermaid-svg-l9neeMNwEV3V5EXP .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-l9neeMNwEV3V5EXP .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-l9neeMNwEV3V5EXP .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-l9neeMNwEV3V5EXP .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-l9neeMNwEV3V5EXP .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-l9neeMNwEV3V5EXP .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-l9neeMNwEV3V5EXP .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-l9neeMNwEV3V5EXP .marker{fill:#333333;stroke:#333333;}#mermaid-svg-l9neeMNwEV3V5EXP .marker.cross{stroke:#333333;}#mermaid-svg-l9neeMNwEV3V5EXP svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-l9neeMNwEV3V5EXP p{margin:0;}#mermaid-svg-l9neeMNwEV3V5EXP .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-l9neeMNwEV3V5EXP .cluster-label text{fill:#333;}#mermaid-svg-l9neeMNwEV3V5EXP .cluster-label span{color:#333;}#mermaid-svg-l9neeMNwEV3V5EXP .cluster-label span p{background-color:transparent;}#mermaid-svg-l9neeMNwEV3V5EXP .label text,#mermaid-svg-l9neeMNwEV3V5EXP span{fill:#333;color:#333;}#mermaid-svg-l9neeMNwEV3V5EXP .node rect,#mermaid-svg-l9neeMNwEV3V5EXP .node circle,#mermaid-svg-l9neeMNwEV3V5EXP .node ellipse,#mermaid-svg-l9neeMNwEV3V5EXP .node polygon,#mermaid-svg-l9neeMNwEV3V5EXP .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-l9neeMNwEV3V5EXP .rough-node .label text,#mermaid-svg-l9neeMNwEV3V5EXP .node .label text,#mermaid-svg-l9neeMNwEV3V5EXP .image-shape .label,#mermaid-svg-l9neeMNwEV3V5EXP .icon-shape .label{text-anchor:middle;}#mermaid-svg-l9neeMNwEV3V5EXP .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-l9neeMNwEV3V5EXP .rough-node .label,#mermaid-svg-l9neeMNwEV3V5EXP .node .label,#mermaid-svg-l9neeMNwEV3V5EXP .image-shape .label,#mermaid-svg-l9neeMNwEV3V5EXP .icon-shape .label{text-align:center;}#mermaid-svg-l9neeMNwEV3V5EXP .node.clickable{cursor:pointer;}#mermaid-svg-l9neeMNwEV3V5EXP .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-l9neeMNwEV3V5EXP .arrowheadPath{fill:#333333;}#mermaid-svg-l9neeMNwEV3V5EXP .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-l9neeMNwEV3V5EXP .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-l9neeMNwEV3V5EXP .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-l9neeMNwEV3V5EXP .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-l9neeMNwEV3V5EXP .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-l9neeMNwEV3V5EXP .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-l9neeMNwEV3V5EXP .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-l9neeMNwEV3V5EXP .cluster text{fill:#333;}#mermaid-svg-l9neeMNwEV3V5EXP .cluster span{color:#333;}#mermaid-svg-l9neeMNwEV3V5EXP 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-l9neeMNwEV3V5EXP .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-l9neeMNwEV3V5EXP rect.text{fill:none;stroke-width:0;}#mermaid-svg-l9neeMNwEV3V5EXP .icon-shape,#mermaid-svg-l9neeMNwEV3V5EXP .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-l9neeMNwEV3V5EXP .icon-shape p,#mermaid-svg-l9neeMNwEV3V5EXP .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-l9neeMNwEV3V5EXP .icon-shape .label rect,#mermaid-svg-l9neeMNwEV3V5EXP .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-l9neeMNwEV3V5EXP .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-l9neeMNwEV3V5EXP .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-l9neeMNwEV3V5EXP :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 是
否
P2P-DEVICE-FOUND
人/上层选择 peer
可选: peer 端 p2p_connect ... auth
发起端 p2p_connect pbc
p2p_ctrl_connect()
wpas_p2p_connect()
p2p_connect()
停止 Find / 等待 in-flight scan 收尾
PD-before-GO-Neg?
Provision Discovery Request/Response
p2p_connect_send() 再进入
GO Negotiation Request
GO Negotiation Response
GO Negotiation Confirm
TX status / Confirm RX completion
p2p_go_neg_results
wpas_go_neg_completed()
P2P-GO-NEG-SUCCESS

这个终点故意停在 GO Negotiation 结果。WPS provisioning、GO/AP 启动、Client association、Credential、4-Way Handshake 与 P2P-GROUP-STARTED 属于另一条完整状态机,不在本文展开。


关键源码索引

符号 本文位置 hostap 2.12
P2P_CONNECT command [CLI 与 command](#符号 本文位置 hostap 2.12 P2P_CONNECT command CLI 与 command ctrl_iface.c wpas_p2p_connect() supplicant glue p2p_supplicant.c p2p_connect() Core connect p2p.c p2p_stop_find() 停止 Find p2p.c p2p_connect_send() PD / GO Neg 发送 p2p_go_neg.c p2p_process_prov_disc_resp() PD Response p2p_pd.c p2p_go_det() GO Intent p2p_go_neg.c p2p_process_go_neg_req() Request p2p_go_neg.c p2p_process_go_neg_resp() Response p2p_go_neg.c p2p_handle_go_neg_conf() Confirm p2p_go_neg.c p2p_send_action_cb() TX status p2p.c wpas_go_neg_completed() GO Negotiation completion p2p_supplicant.c) ctrl_iface.c
wpas_p2p_connect() [supplicant glue](#符号 本文位置 hostap 2.12 P2P_CONNECT command CLI 与 command ctrl_iface.c wpas_p2p_connect() supplicant glue p2p_supplicant.c p2p_connect() Core connect p2p.c p2p_stop_find() 停止 Find p2p.c p2p_connect_send() PD / GO Neg 发送 p2p_go_neg.c p2p_process_prov_disc_resp() PD Response p2p_pd.c p2p_go_det() GO Intent p2p_go_neg.c p2p_process_go_neg_req() Request p2p_go_neg.c p2p_process_go_neg_resp() Response p2p_go_neg.c p2p_handle_go_neg_conf() Confirm p2p_go_neg.c p2p_send_action_cb() TX status p2p.c wpas_go_neg_completed() GO Negotiation completion p2p_supplicant.c) p2p_supplicant.c
p2p_connect() [Core connect](#符号 本文位置 hostap 2.12 P2P_CONNECT command CLI 与 command ctrl_iface.c wpas_p2p_connect() supplicant glue p2p_supplicant.c p2p_connect() Core connect p2p.c p2p_stop_find() 停止 Find p2p.c p2p_connect_send() PD / GO Neg 发送 p2p_go_neg.c p2p_process_prov_disc_resp() PD Response p2p_pd.c p2p_go_det() GO Intent p2p_go_neg.c p2p_process_go_neg_req() Request p2p_go_neg.c p2p_process_go_neg_resp() Response p2p_go_neg.c p2p_handle_go_neg_conf() Confirm p2p_go_neg.c p2p_send_action_cb() TX status p2p.c wpas_go_neg_completed() GO Negotiation completion p2p_supplicant.c) p2p.c
p2p_stop_find() [停止 Find](#符号 本文位置 hostap 2.12 P2P_CONNECT command CLI 与 command ctrl_iface.c wpas_p2p_connect() supplicant glue p2p_supplicant.c p2p_connect() Core connect p2p.c p2p_stop_find() 停止 Find p2p.c p2p_connect_send() PD / GO Neg 发送 p2p_go_neg.c p2p_process_prov_disc_resp() PD Response p2p_pd.c p2p_go_det() GO Intent p2p_go_neg.c p2p_process_go_neg_req() Request p2p_go_neg.c p2p_process_go_neg_resp() Response p2p_go_neg.c p2p_handle_go_neg_conf() Confirm p2p_go_neg.c p2p_send_action_cb() TX status p2p.c wpas_go_neg_completed() GO Negotiation completion p2p_supplicant.c) p2p.c
p2p_connect_send() [PD / GO Neg 发送](#符号 本文位置 hostap 2.12 P2P_CONNECT command CLI 与 command ctrl_iface.c wpas_p2p_connect() supplicant glue p2p_supplicant.c p2p_connect() Core connect p2p.c p2p_stop_find() 停止 Find p2p.c p2p_connect_send() PD / GO Neg 发送 p2p_go_neg.c p2p_process_prov_disc_resp() PD Response p2p_pd.c p2p_go_det() GO Intent p2p_go_neg.c p2p_process_go_neg_req() Request p2p_go_neg.c p2p_process_go_neg_resp() Response p2p_go_neg.c p2p_handle_go_neg_conf() Confirm p2p_go_neg.c p2p_send_action_cb() TX status p2p.c wpas_go_neg_completed() GO Negotiation completion p2p_supplicant.c) p2p_go_neg.c
p2p_process_prov_disc_resp() [PD Response](#符号 本文位置 hostap 2.12 P2P_CONNECT command CLI 与 command ctrl_iface.c wpas_p2p_connect() supplicant glue p2p_supplicant.c p2p_connect() Core connect p2p.c p2p_stop_find() 停止 Find p2p.c p2p_connect_send() PD / GO Neg 发送 p2p_go_neg.c p2p_process_prov_disc_resp() PD Response p2p_pd.c p2p_go_det() GO Intent p2p_go_neg.c p2p_process_go_neg_req() Request p2p_go_neg.c p2p_process_go_neg_resp() Response p2p_go_neg.c p2p_handle_go_neg_conf() Confirm p2p_go_neg.c p2p_send_action_cb() TX status p2p.c wpas_go_neg_completed() GO Negotiation completion p2p_supplicant.c) p2p_pd.c
p2p_go_det() [GO Intent](#符号 本文位置 hostap 2.12 P2P_CONNECT command CLI 与 command ctrl_iface.c wpas_p2p_connect() supplicant glue p2p_supplicant.c p2p_connect() Core connect p2p.c p2p_stop_find() 停止 Find p2p.c p2p_connect_send() PD / GO Neg 发送 p2p_go_neg.c p2p_process_prov_disc_resp() PD Response p2p_pd.c p2p_go_det() GO Intent p2p_go_neg.c p2p_process_go_neg_req() Request p2p_go_neg.c p2p_process_go_neg_resp() Response p2p_go_neg.c p2p_handle_go_neg_conf() Confirm p2p_go_neg.c p2p_send_action_cb() TX status p2p.c wpas_go_neg_completed() GO Negotiation completion p2p_supplicant.c) p2p_go_neg.c
p2p_process_go_neg_req() [Request](#符号 本文位置 hostap 2.12 P2P_CONNECT command CLI 与 command ctrl_iface.c wpas_p2p_connect() supplicant glue p2p_supplicant.c p2p_connect() Core connect p2p.c p2p_stop_find() 停止 Find p2p.c p2p_connect_send() PD / GO Neg 发送 p2p_go_neg.c p2p_process_prov_disc_resp() PD Response p2p_pd.c p2p_go_det() GO Intent p2p_go_neg.c p2p_process_go_neg_req() Request p2p_go_neg.c p2p_process_go_neg_resp() Response p2p_go_neg.c p2p_handle_go_neg_conf() Confirm p2p_go_neg.c p2p_send_action_cb() TX status p2p.c wpas_go_neg_completed() GO Negotiation completion p2p_supplicant.c) p2p_go_neg.c
p2p_process_go_neg_resp() [Response](#符号 本文位置 hostap 2.12 P2P_CONNECT command CLI 与 command ctrl_iface.c wpas_p2p_connect() supplicant glue p2p_supplicant.c p2p_connect() Core connect p2p.c p2p_stop_find() 停止 Find p2p.c p2p_connect_send() PD / GO Neg 发送 p2p_go_neg.c p2p_process_prov_disc_resp() PD Response p2p_pd.c p2p_go_det() GO Intent p2p_go_neg.c p2p_process_go_neg_req() Request p2p_go_neg.c p2p_process_go_neg_resp() Response p2p_go_neg.c p2p_handle_go_neg_conf() Confirm p2p_go_neg.c p2p_send_action_cb() TX status p2p.c wpas_go_neg_completed() GO Negotiation completion p2p_supplicant.c) p2p_go_neg.c
p2p_handle_go_neg_conf() [Confirm](#符号 本文位置 hostap 2.12 P2P_CONNECT command CLI 与 command ctrl_iface.c wpas_p2p_connect() supplicant glue p2p_supplicant.c p2p_connect() Core connect p2p.c p2p_stop_find() 停止 Find p2p.c p2p_connect_send() PD / GO Neg 发送 p2p_go_neg.c p2p_process_prov_disc_resp() PD Response p2p_pd.c p2p_go_det() GO Intent p2p_go_neg.c p2p_process_go_neg_req() Request p2p_go_neg.c p2p_process_go_neg_resp() Response p2p_go_neg.c p2p_handle_go_neg_conf() Confirm p2p_go_neg.c p2p_send_action_cb() TX status p2p.c wpas_go_neg_completed() GO Negotiation completion p2p_supplicant.c) p2p_go_neg.c
p2p_send_action_cb() [TX status](#符号 本文位置 hostap 2.12 P2P_CONNECT command CLI 与 command ctrl_iface.c wpas_p2p_connect() supplicant glue p2p_supplicant.c p2p_connect() Core connect p2p.c p2p_stop_find() 停止 Find p2p.c p2p_connect_send() PD / GO Neg 发送 p2p_go_neg.c p2p_process_prov_disc_resp() PD Response p2p_pd.c p2p_go_det() GO Intent p2p_go_neg.c p2p_process_go_neg_req() Request p2p_go_neg.c p2p_process_go_neg_resp() Response p2p_go_neg.c p2p_handle_go_neg_conf() Confirm p2p_go_neg.c p2p_send_action_cb() TX status p2p.c wpas_go_neg_completed() GO Negotiation completion p2p_supplicant.c) p2p.c
wpas_go_neg_completed() [GO Negotiation completion](#符号 本文位置 hostap 2.12 P2P_CONNECT command CLI 与 command ctrl_iface.c wpas_p2p_connect() supplicant glue p2p_supplicant.c p2p_connect() Core connect p2p.c p2p_stop_find() 停止 Find p2p.c p2p_connect_send() PD / GO Neg 发送 p2p_go_neg.c p2p_process_prov_disc_resp() PD Response p2p_pd.c p2p_go_det() GO Intent p2p_go_neg.c p2p_process_go_neg_req() Request p2p_go_neg.c p2p_process_go_neg_resp() Response p2p_go_neg.c p2p_handle_go_neg_conf() Confirm p2p_go_neg.c p2p_send_action_cb() TX status p2p.c wpas_go_neg_completed() GO Negotiation completion p2p_supplicant.c) p2p_supplicant.c

资料来源

S1 hostap 2.12 Git:P2P_CONNECT 与 GO Negotiation 实现

S2 Wi-Fi Direct Specification v1.9

  • 版本:v1.9
  • URL/文档:Wi-Fi Direct Specification v1.9
  • 使用位置:第 1、5、7、8 节
  • 支撑内容:Provision Discovery、GO Negotiation、GO Intent/tie breaker、Group Formation 阶段语义。

S3 hostap 2.12 README-P2P

  • 来源:wpa_supplicant/README-P2P
  • 使用位置:第 1、2、5 节
  • 支撑内容:p2p_connect 命令语义、auth、provdisc、go_intent 等 CLI 参数。

S4 Wi-Fi Protected Setup / hostap WPS implementation

S5 双 mac80211_hwsim 实验记录

  • 版本:hostap 2.12 hostap_2_12,Linux mac80211_hwsim 双 P2P Device 实验,2026-09-29
  • 定位:本文第 1 节所列 A/B/C 三终端命令及对应 -dd -t 运行日志
  • 使用位置:第 1 节
  • 支撑内容:双方 p2p_peers 得到的 Device Address、p2p_connect ... auth 与主动 p2p_connect 的实际执行结果,以及本轮最终角色 B=GO、A=Client、SSID=DIRECT-fM、频率 2412 MHz、group interface 名称。
相关推荐
忆挽篱笙歌1 小时前
linux开发工具
linux·运维·ubuntu
吴声子夜歌1 小时前
Nginx应用与运维——Nginx Web服务应用实战(伪流媒体服务器的搭建)
运维·前端·nginx
打工仔折腾 AI1 小时前
多台服务器日志分散难查?用Promtail+Loki+Grafana搭建集中检索平台
服务器·人工智能·后端·python·性能优化·django·grafana
Lsetea1 小时前
OpenSSL verify报certificate signature failure:error 7与证书签名排查
运维·https·ssl证书·openssl·证书链
吴声子夜歌2 小时前
Nginx应用与运维——Nginx Web服务应用实战(HTTP增强协议服务器的搭建)
运维·前端·nginx
JWASX2 小时前
Java 转 go 学习 - map
学习·golang
小白的码BUG之路2 小时前
Docker -- 构建ruoyi-auth镜像
运维·docker·容器
我叫洋洋4 小时前
涂鸦 T5+ hashcat 自动化破解 WPA2:从单缺 M2 到全自动值守的踩坑实录 破解wifi 密码
运维·单片机·自动化
꯭自꯭闭꯭10 小时前
达梦SQL优化相关
linux·运维·数据库·sql