跳到主要内容

Mac80211 实现细节

现在,让我们把目光从空中的协议交互收回内核。 上一节我们看着数据包怎么变换身份,怎么在省电模式下玩捉迷藏。 但你可能一直有个疑问:这些是谁在管?代码到底长什么样?

Linux 的无线世界并不是杂乱无章的。它有一套标准的「施工规范」叫 mac80211。 无论是 Intel 的 iwlwifi,还是 Marvell 的 lbtf,只要想进 Linux 的无线门,就得按 mac80211 的规矩办。 这一节,我们要掀开引擎盖,看看里面的活塞和连杆。 如果不去摸透 ieee80211_hwsta_info 这几块核心骨头,你对无线栈的理解就永远是隔着一层玻璃看花。


12.6 核心骨架:从 ieee80211_hw 到驱动回调

mac80211 的 API 复杂到令人发指,里面全是让人眼晕的细节。 我不打算把这几十兆的代码全搬出来(那也没意义),但我会给你一张最精确的寻宝图,让你知道哪里是入口,哪里是陷阱。

一切始于一个叫 struct ieee80211_hw 的结构体。 你可以把它理解成 「硬件设备的身份证」

内核用这个结构体来告诉无线栈:「嘿,这块网卡长什么样,它有哪些绝活。」 但这里有一个关键的设计细节:ieee80211_hw 里面有一个 priv 指针。 这可不是个普通的指针,它是驱动开发者的私房钱。 这个 priv 指针被定义为 void *,意味着内核根本不关心也不看里面存的是什么。 只有驱动自己知道,这通常指向一个更大的、驱动自己定义的私有结构体——比如 Intel 的驱动里叫 iwl_priv,Marvell 的叫 lbtf_private。 这种设计把「通用框架」和「私有实现」切得干干净净。

要开始干活,驱动得先做几件事:

  1. 申领空间:调用 ieee80211_alloc_hw(size_t priv_data_len, const struct ieee80211_ops *ops)。 这一步不仅分配了 ieee80211_hw,还顺带分配了你要求的 priv_data_len 大小的私有空间,并把两者绑在一起。
  2. 注册设备:拿着分配好的 hw,调用 ieee80211_register_hw(struct ieee80211_hw *hw)。 这一步就像是给设备上户口,从此它就在内核里存在了。
  3. 接收数据:当硬件收到包时,驱动通常会调用 ieee80211_rx_irqsafe()(在 net/mac80211/rx.c 里实现)。 为什么叫 irqsafe?因为这是在中断上下文里调用的,不能睡觉。

那张你在文档里见过的架构图(Figure 12-5),其实画的就是这层关系: 驱动层和 mac80211 层之间,就是靠 ieee80211_ops 这组回调函数握手连接的。


12.6.1 驱动的操作集

刚才提到的 ieee80211_ops,就是驱动写给内核的一份「承诺书」。 这也是一个结构体,里面全是函数指针。不是每个都要填,但有几个是命根子。

让我们看几个最核心的回调:

  • tx():这是发工资的部门。每次内核要发包,都会调这个函数。 通常情况下它得返回 NETDEV_TX_OK,除非出了岔子。
  • start()stop():这对兄弟负责电源开关。 start() 用来激活硬件,打开接收帧的开关;stop() 则是关机,通常会把硬件彻底断电。
  • add_interface()remove_interface(): 这听起来很抽象,实际上很简单:当你 ifconfig wlan0 up 的时候,这个回调就会被触发。 内核在问驱动:「嘿,我要把虚拟网络设备绑上来了,你硬件那边准备好接客了吗?」 remove_interface 则是反向操作。
  • config():这是调参旋钮。 比如你要切换信道(从 CH6 切到 CH11),内核就会调这个函数通知硬件去改频率。
  • configure_filter():这是看门狗的设置。 它告诉硬件:「我只想要这几种类型的包,其他的别吵我。」

12.6.2 站点管理:sta_info 结构

除了硬件本身,无线世界里最重要的是什么?是 「人」( Stations,站点)。 无论是连上来的手机,还是隔壁的路由器,在内核里都是一个 struct sta_info(定义在 net/mac80211/sta_info.h)。

这个结构体内容非常丰富,简直就是一个站点的黑匣子:

  • 统计计数器:收了多少包,发了多少包,丢了多少包。
  • 标志位:它是不是在省电模式?它有没有授权?
  • ps_tx_buf:还记得上一节说的省电缓冲吗?就是这个数组,用来存给睡着了站点的单播包。
  • debugfs 条目:方便我们在用户空间窥探它的状态。

内核怎么管理成百上千个这样的站点? 用哈希表(sta_hash)和链表(sta_list)。 当你要找一个站点时,通常会用到这三个工具:

  • sta_info_insert(struct sta_info *sta):把一个新站点加入表格。
  • sta_info_destroy_addr(...):踢人。通过 MAC 地址把站点干掉(内部会调用 __sta_info_destroy)。
  • sta_info_get(...):查户口。给定一个 MAC 地址(通常是 BSSID),把这个站点的 sta_info 结构体捞出来。

12.6.3 RX Path:当数据包飞进来

接收路径是整个栈里最繁忙的地方。 主角是 ieee80211_rx() 函数(在 net/mac80211/rx.c)。

当驱动把一个 SKB 递上来时,它不是空手来的。它在 SKB 的控制缓冲区里塞了一张小纸条,叫 ieee80211_rx_status。 你可以用 IEEE80211_SKB_RXCB() 这个宏把这张纸条抽出来看。 上面写着什么?写着这包收过来的质量怎么样:FCS 校验有没有挂?信号强度多少? 如果 flag 字段里带上了 RX_FLAG_FAILED_FCS_CRC,那恭喜,这包是废的。

接下来是一系列流水线作业:

  1. 监听模式处理ieee80211_rx_monitor() 会出场。 如果你开了 Monitor Mode(抓包用的),它会先帮你剥掉 FCS(校验位),顺便处理一下可能存在的 radiotap 头部。 注意:不是所有网卡都支持 Monitor Mode。

  2. 802.11n 的重排序:如果你用的是 HT(802.11n),乱序的包需要排队。 这时 ieee80211_rx_reorder_ampdu() 会被叫来把乱了次序的积木排好。

  3. 真正的处理__ieee80211_rx_handle_packet() 最终会调用 ieee80211_invoke_rx_handlers()

这里有一个非常精妙的设计,叫 「责任链模式」。 内核有一串处理器,每个处理器都看一眼这个包:

  • 如果它说「我不处理,下一个」,就返回 RX_CONTINUE
  • 如果它说「这包我吞了」,就返回 RX_QUEUED
  • 如果它说「这包是垃圾」,就返回 RX_DROP_MONITORRX_DROP_UNUSABLE

看一段真实的代码(net/mac80211/rx.c):

static ieee80211_rx_result
ieee80211_rx_h_mgmt_check(struct ieee80211_rx_data *rx)
{
struct ieee80211_mgmt *mgmt = (struct ieee80211_mgmt *) rx->skb->data;
struct ieee80211_rx_status *status = IEEE80211_SKB_RXCB(rx->skb);

. . .
if (rx->skb->len < 24)
return RX_DROP_MONITOR;

if (!ieee80211_is_mgmt(mgmt->frame_control))
return RX_DROP_MONITOR;
. . .
}

这代码在干嘛? 它在检查:这包既然说是管理帧,长度连 24 字节都不够(802.11 头部的最小长度),或者是假的不是管理帧,直接 RX_DROP_MONITOR——扔进垃圾桶。 如果是 PS-Poll 包,但收包的不是 AP 角色,那也是 RX_DROP_UNUSABLE。 这种层层过滤的设计,保证了内核不会被非法包把 CPU 跑满。


12.6.4 TX Path:当数据包飞出去

发送路径的主角是 ieee80211_tx()(在 net/mac80211/tx.c)。 它的逻辑和 RX 很像,也是先准备,再过流水线。

  1. 准备工作__ieee80211_tx_prepare()。 这里会做各种预检查,比如,如果你发的是一个长度小于 10 字节的包,内核会觉得你在开玩笑,直接 dev_kfree_skb(skb) 丢掉。

  2. 发送处理器链invoke_tx_handlers()。 和 RX 一样,这也是用宏 CALL_TXH 串起来的一排处理函数。

    • TX_CONTINUE:没我的事,走下一个。
    • TX_QUEUED:这包被我接管了(比如进了缓冲区)。
    • TX_DROP:这包有坑,发不得。

如果这一路绿灯,最后会调用 __ieee80211_tx() 把数据包真正推向硬件驱动的 tx() 回调。


12.6.5 分片:当大象装进冰箱

现在的无线很快,但在以前,为了保证可靠性,大象也是要切碎了的。 这就是 分片

802.11 规定,每个站点都有一个分片阈值。 如果单播包的大小超过了这个阈值,就必须切成小片。 每切一片,都要单独确认一次(ACK)。 你可以在命令行里用 iwconfig 看到这个阈值,或者改掉它:

iwconfig wlan0 frag 512

这就把阈值设成了 512 字节。 阈值越小,碰撞的概率越低,但开销越大。 在发送端,切肉的刀是 ieee80211_tx_h_fragment() 方法。 在接收端,拼肉的神器是 ieee80211_rx_h_defragment() 方法。

但这里有个巨大的坑: 分片和聚合(802.11n 用来提速的技术)是水火不容的。 既然现在的 802.11n/ac 速度这么快,空中时间很短,分片这玩意儿现在已经很少见到了。 如果你在现代的高速率网络里开启分片,可能会发现网络反而变慢了,甚至因为不支持聚合而导致吞吐量暴跌。


12.6.6 debugfs:内核的黑匣子

内核怕你搞不懂它在想什么,所以留了个后门叫 debugfs。 这是一个虚拟文件系统,专门用来往外吐调试信息。

首先你得把它挂载上来:

mount -t debugfs none_debugs /sys/kernel/debug

⚠️ 注意 你的内核编译时必须选上 CONFIG_DEBUG_FS,否则这一步会报错,你也看不到下面的任何内容。

假设你的物理设备是 phy0,你就可以去 /sys/kernel/debug/ieee80211/phy0 下面挖宝。

这里有几个特别好用的条目:

  1. total_ps_buffered: 这就是 AP 的「暂存仓」。 它显示了 AP 为那些睡着的站点(PS 模式)缓存了多少个包(包括单播和组播)。 这是 ieee80211_tx_h_unicast_ps_bufieee80211_tx_h_multicast_ps_buf 两个人一起加出来的数。

  2. statistics 目录: 这里面全是战绩。

    • frame_duplicate_count:收到了多少个重复的包(有人在重传)。
    • transmitted_frame_count:成功发出了多少个包。
    • retry_count:重试了多少次。如果这个数字飙升,说明你的信号质量很差,或者干扰很大。
    • fragmentation_threshold:当前的分片阈值。
  3. netdev:wlan0 目录(假设你的接口叫 wlan0): 这里能看到具体的接口信息。

    • aid:关联 ID。
    • assoc_tries:试了多少次才连上。
    • bssid:你连的路由器地址。
  4. rc/name: 这里写着当前用的速率控制算法名字。 选对算法比选对路由器还重要。


12.6.7 无线模式:网卡的身份卡

最后,网卡不是只有一种形态。 根据你想干什么,你可以把它切成不同的模式。 这取决于硬件支不支持(你可以查 wiphyinterface_modes 字段,或者去 linuxwireless.org 查你的驱动列表)。

  • AP 模式 (NL80211_IFTYPE_AP): 网卡变成路由器。它维护一个站点列表,网络名字(SSID)由它定义。
  • Managed 模式 (NL80211_IFTYPE_STATION): 网卡变成客户端。这就是你手机连 WiFi 的模式。
  • Monitor 模式 (NL80211_IFTYPE_MONITOR): 嗅探者模式。所有经过空中的包,不管是不是发给它的,统统收下。 这是抓包神器(比如 airodump-ng)的依赖。 在这个模式下发包叫 注入,数据包会被打上 IEEE80211_TX_CTL_INJECTED 的特殊标记。
  • Ad Hoc 模式 (NL80211_IFTYPE_ADHOC): 点对点,没有 AP,大家平等。
  • WDS 模式 (NL80211_IFTYPE_WDS): 无线分发系统,用来做桥接的。
  • Mesh 模式 (NL80211_IFTYPE_MESH_POINT): 网状网,我们下一节会专门讲这个。

至此,我们已经看完了 mac80211 的骨架和肌肉。 但现在的无线世界不仅要通,还要快。 下一节,我们要进入 802.11n 的高速公路,看看多天线(MIMO)和聚合技术是怎么把无线网变成光纤的。 那个世界里的逻辑,又完全是另一套了。