1. 深入解析wpa_supplicant-2.10的WiFi扫描机制
在Linux无线网络开发中,理解用户空间工具与内核驱动的交互过程至关重要。本文将以wpa_supplicant-2.10与RTL8188驱动的扫描流程为例,详细剖析从用户触发扫描到硬件执行的完整调用链。不同于简单的API说明,我们将重点关注各层间的数据传递和转换机制,以及实际开发中可能遇到的坑点。
2. 用户空间:wpa_supplicant的扫描请求构建
2.1 扫描参数初始化
当用户在wpa_cli或前端界面发起扫描请求时,wpa_supplicant首先会构建一个struct wpa_driver_scan_params结构体。这个结构体包含了扫描所需的所有参数:
c复制struct wpa_driver_scan_params {
const u8 *ssids[MAX_SCAN_SSIDS]; // 指定扫描的SSID列表
size_t ssid_lens[MAX_SCAN_SSIDS]; // 各SSID的长度
int num_ssids; // SSID数量
const u8 *extra_ies; // 额外信息元素
size_t extra_ies_len; // 信息元素长度
u16 freqs[MAX_SCAN_FREQS]; // 指定扫描的信道频率
int num_freqs; // 信道数量
unsigned int passive_scan:1; // 被动扫描标志
unsigned int only_new_results:1; // 仅返回新结果
// ... 其他标志位
};
关键细节:如果num_ssids为0且num_freqs为0,表示执行全信道全SSID扫描。这在开发调试时经常使用,但生产环境建议指定参数以减少扫描耗时。
2.2 Netlink消息构造
wpa_supplicant通过nl80211_scan_common()函数将扫描参数转换为Netlink消息。这个过程有几个技术要点:
- 消息类型:使用NL80211_CMD_TRIGGER_SCAN命令
- 属性嵌套:扫描参数通过NL80211_ATTR_SCAN_FREQUENCIES等属性嵌套传递
- 内存管理:Netlink消息使用sk_buff结构,需要特别注意内存对齐和释放
c复制static struct nl_msg *nl80211_scan_common(struct i802_bss *bss,
enum nl80211_commands cmd,
struct wpa_driver_scan_params *params)
{
struct nl_msg *msg;
msg = nlmsg_alloc(); // 分配Netlink消息缓冲区
// ... 构造消息头
if (params->freqs) {
nla_put(msg, NL80211_ATTR_SCAN_FREQUENCIES,
params->num_freqs * sizeof(u32), params->freqs);
}
// ... 添加其他属性
return msg;
}
2.3 消息发送与同步
通过send_and_recv_msgs()发送消息时,wpa_supplicant会等待内核确认:
c复制ret = send_and_recv_msgs(drv, msg, NULL, NULL, NULL, NULL);
实际开发中发现:如果内核处理超时(默认5秒),这个调用会返回-ETIMEDOUT。在嵌入式设备上,可能需要根据硬件性能调整这个超时值。
3. 内核空间:nl80211与cfg80211的处理流程
3.1 nl80211的消息分发
内核的nl80211模块通过genl_small_ops注册了命令处理回调:
c复制static const struct genl_small_ops nl80211_small_ops[] = {
{
.cmd = NL80211_CMD_TRIGGER_SCAN,
.doit = nl80211_trigger_scan,
.flags = GENL_UNS_ADMIN_PERM,
.internal_flags = NL80211_FLAG_NEED_WDEV_UP |
NL80211_FLAG_NEED_RTNL,
},
// ... 其他命令
};
nl80211_trigger_scan()函数的关键处理步骤:
- 获取对应的无线设备(
struct wiphy和struct wireless_dev) - 验证权限和接口状态
- 解析Netlink属性并转换为
cfg80211_scan_request - 调用cfg80211层的扫描接口
3.2 扫描请求的转换过程
用户空间的扫描参数在内核中被转换为struct cfg80211_scan_request:
c复制struct cfg80211_scan_request {
struct cfg80211_ssid *ssids; // SSID列表
int n_ssids; // SSID数量
const u32 *freqs; // 频率列表
int n_channels; // 信道数量
bool passive; // 被动扫描标志
// ... 其他字段
};
转换过程中的注意事项:
- 内存分配使用kmalloc,需要确保在错误路径上正确释放
- SSID和频率列表需要深拷贝,不能直接引用用户空间指针
- 需要检查硬件支持的信道范围
3.3 cfg80211的通用处理
cfg80211_scan()函数是无线子系统的核心枢纽,它负责:
- 合并全局扫描策略(如监管限制)
- 处理扫描抑制(避免频繁扫描)
- 调用具体驱动的扫描实现
c复制int cfg80211_scan(struct cfg80211_registered_device *rdev,
struct cfg80211_scan_request *request)
{
// ... 策略检查
rdev->scan_req = request;
err = rdev->ops->scan(&rdev->wiphy, request);
// ... 错误处理
}
性能提示:cfg80211会维护一个扫描结果缓存。开发时可以通过
iw dev wlan0 scan flush命令强制刷新缓存。
4. 驱动层实现:RTL8188的扫描处理
4.1 驱动操作集注册
RTL8188驱动通过rtw_cfg80211_ops注册了自己的回调函数:
c复制static struct cfg80211_ops rtw_cfg80211_ops = {
.scan = cfg80211_rtw_scan,
// ... 其他操作
};
这种属于fullmac驱动架构,直接处理cfg80211请求而不依赖mac80211中间层。
4.2 扫描命令下发流程
cfg80211_rtw_scan()的内部处理流程:
- 参数转换:将cfg80211请求转换为驱动私有结构体
- 构造扫描命令:通过
rtw_init_sitesurvey_parm()准备命令参数 - 命令排队:通过
rtw_enqueue_cmd()将扫描命令加入队列 - 超时设置:
mlme_set_scan_to_timer()设置扫描超时(通常10-15秒)
c复制static int cfg80211_rtw_scan(struct wiphy *wiphy,
struct cfg80211_scan_request *request)
{
struct adapter *padapter = wiphy_to_adapter(wiphy);
struct mlme_priv *pmlmepriv = &padapter->mlmepriv;
// 转换扫描参数
rtw_init_sitesurvey_parm(padapter, request);
// 下发扫描命令
rtw_sitesurvey_cmd(padapter, pmlmepriv);
return 0;
}
4.3 硬件交互细节
RTL8188的实际扫描过程涉及以下硬件操作:
- 设置射频参数(信道、带宽等)
- 发送探测请求(主动扫描)或监听信标(被动扫描)
- 接收并解析探测响应/信标帧
- 将扫描结果上报给内核
调试技巧:可以通过
iw event命令实时监控扫描状态变化,或使用tcpdump -i wlan0 -n抓取空口报文验证扫描行为。
5. 扫描结果上报流程
5.1 驱动结果收集
RTL8188驱动通过中断处理接收扫描结果后:
- 调用
rtw_surveydone_event_callback()处理完成事件 - 通过
rtw_indicate_scan_done()通知上层 - 最终调用
cfg80211_scan_done()将结果提交给内核
c复制void rtw_indicate_scan_done(struct adapter *padapter, bool aborted)
{
struct mlme_priv *pmlmepriv = &padapter->mlmepriv;
// 调用cfg80211完成回调
if (pmlmepriv->scan_req) {
struct cfg80211_scan_info info = {
.aborted = aborted
};
cfg80211_scan_done(pmlmepriv->scan_req, &info);
pmlmepriv->scan_req = NULL;
}
}
5.2 内核结果处理
cfg80211接收到扫描结果后:
- 验证结果的完整性和时效性
- 更新无线设备的BSS列表
- 通过nl80211通知用户空间
5.3 用户空间接收
wpa_supplicant通过Netlink监听SCAN_RESULTS事件:
c复制static void nl80211_scan_done(struct wpa_driver_nl80211_data *drv,
struct nl_msg *msg)
{
// 处理扫描结果
wpa_supplicant_event(drv->ctx, EVENT_SCAN_RESULTS, NULL);
}
常见问题:如果扫描结果未及时更新,可能是内核到用户空间的通知丢失。可以尝试手动发送
SIGHUP信号给wpa_supplicant强制刷新。
6. 不同驱动架构的对比
6.1 Fullmac vs mac80211
| 特性 | Fullmac驱动 (如RTL8188) | mac80211驱动 (如ath9k) |
|---|---|---|
| 扫描实现位置 | 驱动直接实现 | 通过mac80211中间层 |
| 典型回调路径 | rdev->ops->scan | ieee80211_scan -> drv_scan |
| 硬件控制粒度 | 驱动完全控制 | mac80211处理大部分通用逻辑 |
| 开发复杂度 | 较高 | 相对较低 |
| 适用场景 | 专有硬件 | 标准802.11硬件 |
6.2 主动扫描与被动扫描
-
主动扫描:驱动发送Probe Request帧,适用于快速发现网络
- 优点:扫描速度快,结果准确
- 缺点:可能被某些网络过滤
-
被动扫描:仅监听Beacon帧,适用于隐蔽扫描
- 优点:不会被探测过滤
- 缺点:扫描时间长(需等待每个信道的Beacon间隔)
在驱动中通常通过设置scan_req->passive标志来控制扫描类型。
7. 实际开发中的经验与技巧
7.1 性能优化建议
-
信道驻留时间:调整
struct cfg80211_scan_request中的duration_mandatory和duration字段,平衡扫描速度和结果完整性。实测发现,大多数环境下20-40ms的信道驻留时间即可获得可靠结果。 -
并行扫描:现代硬件通常支持多信道并行扫描。可以通过
NL80211_FEATURE_SCHED_SCAN特性检查硬件支持情况。 -
结果缓存:合理利用cfg80211的BSS缓存机制,避免重复扫描相同信道。
7.2 常见问题排查
-
扫描无结果:
- 检查
iw list确认硬件支持的目标频段 - 使用
iw event监控扫描状态变化 - 通过
dmesg查看内核日志中的错误信息
- 检查
-
扫描耗时过长:
- 确认是否指定了扫描信道(全信道扫描可能耗时数秒)
- 检查驱动是否实现了并行扫描
- 调整被动扫描的超时时间
-
扫描崩溃或死锁:
- 确保在扫描过程中不执行其他并发操作(如连接、断开)
- 检查驱动中的RTNL锁和扫描锁的使用
7.3 调试工具推荐
-
iw工具集:
iw dev wlan0 scan:手动触发扫描iw event:实时监控无线事件iw list:查看硬件能力
-
tcpdump/wireshark:
- 抓取空口报文分析扫描行为
- 过滤条件:
wlan.fc.type_subtype == 0x08(信标帧)或0x05(探测响应)
-
内核跟踪:
- ftrace跟踪
cfg80211_*和驱动相关函数 - 动态打印:
echo 'module cfg80211 +p' > /sys/kernel/debug/dynamic_debug/control
- ftrace跟踪
在开发RTL8188驱动时,我发现其扫描超时默认设置为10秒,这在信道拥堵环境下可能导致用户体验不佳。通过修改mlme_set_scan_to_timer()中的超时值,可以显著改善扫描响应速度,但需要平衡扫描完整性和速度。另一个实用技巧是在驱动中实现扫描结果过滤,提前丢弃信号强度低于阈值的AP,减少用户空间处理开销。