2、Linux WiFi 子系统架构:mac80211、cfg80211、nl80211 框架解析

做 WiFi 驱动开发,第一件事就是搞清楚 Linux 内核里那套复杂的 WiFi 子系统。说实话,我刚入行那会儿也被这三个名字绕晕过——mac80211、cfg80211、nl80211,长得像三胞胎,干的活却完全不一样。

今天我就带你把这套架构拆开来看。你想想看,一个无线网卡要正常工作,从用户态到硬件,中间要经过多少层?用户想连哪个 AP、用什么加密方式、要不要扫描……这些请求怎么一步步传到硬件?反过来,硬件收到数据包、检测到信号变弱,又怎么通知上层?

嗯,这套机制就是 Linux WiFi 子系统的核心。我们一个一个来说。

2.1 整体架构:三层两接口

先看一张总图,把三个模块的位置和关系搞清楚。

Linux WiFi 子系统架构图 Linux WiFi 子系统架构 用户空间 iw / wpa_supplicant / hostapd nl80211 (netlink 协议) cfg80211 (配置管理层) 扫描 / 连接 / 监管 / 加密协商 mac80211 (软件 MAC 层) TX/RX 路径 / 速率控制 / 电源管理 驱动层 (Driver) WiFi 硬件 (FullMAC / SoftMAC) 关键接口 ① nl80211 → cfg80211 ② cfg80211_ops ③ ieee80211_ops ④ skb 数据路径

从这张图你能看到,整个架构分三层:

  • 用户空间:iw、wpa_supplicant、hostapd 这些工具通过 netlink 和内核通信。
  • 内核空间:nl80211 是协议通道,cfg80211 管配置策略,mac80211 管数据收发。
  • 硬件层:分 FullMAC 和 SoftMAC 两种,驱动写法完全不同。

我个人习惯把这三层比喻成「前台接待、项目经理、技术工人」——nl80211 负责传话,cfg80211 负责决策,mac80211 负责干活。这样记起来就顺了。

2.2 nl80211:用户态与内核的「传话筒」

nl80211 本质上是一个 netlink 协议族。它定义了一组命令和属性,用户态工具通过发送 netlink 消息来触发内核操作。

举个例子,当你在终端敲 iw dev wlan0 scan 时,发生了什么?

// 用户态 iw 工具发送的消息(简化)
struct nl_msg *msg = nlmsg_alloc();
genlmsg_put(msg, 0, 0, nl80211_family, 0, 0, NL80211_CMD_TRIGGER_SCAN, 0);
nla_put_u32(msg, NL80211_ATTR_IFINDEX, ifindex);
nla_put_nested(msg, NL80211_ATTR_SCAN_SSIDS, ssids);
nl_send_auto(sock, msg);

内核收到后,nl80211 的 handler 会解析命令,然后调用 cfg80211 的扫描接口。整个过程是异步的——扫描结果通过事件回调再通知用户态。

调试小技巧:用 iw event 可以实时查看 nl80211 事件。我曾经靠这个命令抓到一个扫描超时的 bug——用户态发了扫描请求,但内核没收到,最后发现是 netlink socket 缓冲区溢出了。

2.3 cfg80211:策略与配置的「大脑」

cfg80211 是 WiFi 子系统的配置管理层。它不直接操作硬件,而是负责:

  • 扫描请求的管理和结果缓存
  • 连接/断连的状态机
  • 监管域(regulatory domain)控制
  • 加密参数的协商
  • 虚拟接口(vif)的创建和删除

驱动需要注册一个 cfg80211_ops 结构体,里面全是函数指针。比如:

static const struct cfg80211_ops my_cfg80211_ops = {
    .scan = my_scan,
    .connect = my_connect,
    .disconnect = my_disconnect,
    .add_virtual_intf = my_add_vif,
    .del_virtual_intf = my_del_vif,
    .set_channel = my_set_channel,
    .start_ap = my_start_ap,
    // ... 还有几十个回调
};

这里有个坑,我必须要说。很多新手写驱动时,只实现了 scan 和 connect,结果发现 iw dev wlan0 set channel 报错——因为没注册 set_channel。cfg80211 的检查很严格,缺一个回调,对应的功能就直接不可用。

注意:cfg80211_ops 的回调是在进程上下文中调用的,不能在里面做长时间的阻塞操作。我曾经在 scan 回调里直接调了硬件扫描并等待完成,结果把整个 WiFi 线程卡死了。正确的做法是:提交扫描请求,立即返回,扫描完成后通过 cfg80211_scan_done 通知上层。

2.4 mac80211:数据收发的「心脏」

mac80211 是软件 MAC 层的实现。对于 SoftMAC 类型的网卡(比如大多数 USB WiFi 和 SDIO WiFi),mac80211 负责:

  • 802.11 帧的封装和解析
  • TX 路径:从上层拿到 skb,加上 802.11 头,交给驱动发送
  • RX 路径:驱动上报原始帧,mac80211 解析后递交给上层
  • 速率控制算法(如 minstrel_ht)
  • 电源管理(PS-Poll、U-APSD)
  • 重传和序列号管理

驱动需要注册 ieee80211_ops:

static const struct ieee80211_ops my_ieee80211_ops = {
    .tx = my_tx,
    .start = my_start,
    .stop = my_stop,
    .config = my_config,
    .configure_filter = my_configure_filter,
    .set_key = my_set_key,
    .sta_state = my_sta_state,
    .ampdu_action = my_ampdu_action,
};

这里最核心的是 tx 回调。驱动拿到 skb 后,要把它发到硬件。但注意,skb 里的数据已经是完整的 802.11 帧了,驱动不需要再封装头。

static void my_tx(struct ieee80211_hw *hw, struct ieee80211_tx_control *control,
                  struct sk_buff *skb) {
    // skb->data 指向完整的 802.11 帧
    // 驱动只需要把数据写入硬件 TX FIFO
    write_tx_desc(hw, skb->data, skb->len);
    dev_kfree_skb(skb);
}
FullMAC vs SoftMAC 的选择:
特性 FullMAC SoftMAC
MAC 层实现 硬件固件完成 mac80211 软件完成
驱动复杂度 低(只需 cfg80211_ops) 高(需 cfg80211_ops + ieee80211_ops)
灵活性 低,依赖固件 高,可定制协议行为
典型芯片 Broadcom BCM43xx Atheros ath9k、MediaTek mt76

2.5 驱动与内核的交互接口总结

把上面三个模块串起来,驱动开发中最重要的几个接口就是:

  1. 注册接口:ieee80211_alloc_hw + ieee80211_register_hw,把驱动注册到 mac80211 子系统。
  2. 配置回调:cfg80211_ops 处理用户态配置请求。
  3. 数据回调:ieee80211_ops 处理数据收发。
  4. 上报接口:驱动通过 ieee80211_rx 上报收到的帧,通过 cfg80211_scan_done 上报扫描结果。
  5. 控制接口:ieee80211_wake_queues / ieee80211_stop_queues 控制 TX 流控。

我记得第一次写 SoftMAC 驱动时,最头疼的就是搞清楚什么时候该调 ieee80211_rx,什么时候该调 ieee80211_rx_irqsafe。后来翻代码才发现:前者不能在中断上下文调,后者可以。就这么一个细节,让我 debug 了整整两天。

我的建议:刚开始写驱动时,先找一个现成的驱动(比如 ath9k 或 mt76)对照着看。把它的 cfg80211_ops 和 ieee80211_ops 抄下来,一行一行理解每个回调是干什么的。比你自己从头看文档快得多。

2.6 数据流全景:从扫描到连接

最后,我用一个完整的扫描→连接流程,把三个模块串起来给你看:

  1. 用户态 iw dev wlan0 scan → 通过 nl80211 发送 NL80211_CMD_TRIGGER_SCAN
  2. 内核 nl80211 handler 解析命令 → 调用 cfg80211 的 rdev_scan
  3. cfg80211 检查权限、缓存策略 → 调用驱动的 scan 回调
  4. 驱动向硬件下发扫描命令 → 硬件开始扫描信道
  5. 硬件收到 beacon/probe response → 驱动通过 ieee80211_rx 上报 mac80211
  6. mac80211 解析帧 → 调用 cfg80211_inform_bss 更新 BSS 列表
  7. 扫描完成 → 驱动调 cfg80211_scan_done → nl80211 发事件给用户态
  8. 用户态选择 AP → 发 NL80211_CMD_CONNECT → 重复类似流程

你看,一个简单的扫描操作,就串起了 nl80211、cfg80211、mac80211 三个模块。每个模块各司其职,缺一不可。

嗯,这套架构设计得其实挺巧妙的。把策略和数据分离,让驱动开发者只需要关注硬件相关的部分,上层逻辑由内核统一管理。但代价就是——你得先花时间把这套框架摸透,不然写出来的驱动要么功能不全,要么性能拉胯。