Mac80211 实现细节
现在,让我们把目光从空中的协议交互收回内核。 上一节我们看着数据包怎么变换身份,怎么在省电模式下玩捉迷藏。 但你可能一直有个疑问:这些是谁在管?代码到底长什么样?
Linux 的无线世界并不是杂乱无章的。它有一套标准的「施工规范」叫 mac80211。
无论是 Intel 的 iwlwifi,还是 Marvell 的 lbtf,只要想进 Linux 的无线门,就得按 mac80211 的规矩办。
这一节,我们要掀开引擎盖,看看里面的活塞和连杆。
如果不去摸透 ieee80211_hw 和 sta_info 这几块核心骨头,你对无线栈的理解就永远是隔着一层玻璃看花。
12.6 核心骨架:从 ieee80211_hw 到驱动回调
mac80211 的 API 复杂到令人发指,里面全是让人眼晕的细节。
我不打算把这几十兆的代码全搬出来(那也没意义),但我会给你一张最精确的寻宝图,让你知道哪里是入口,哪里是陷阱。
一切始于一个叫 struct ieee80211_hw 的结构体。
你可以把它理解成 「硬件设备的身份证」。
内核用这个结构体来告诉无线栈:「嘿,这块网卡长什么样,它有哪些绝活。」
但这里有一个关键的设计细节:ieee80211_hw 里面有一个 priv 指针。
这可不是个普通的指针,它是驱动开发者的私房钱。
这个 priv 指针被定义为 void *,意味着内核根本不关心也不看里面存的是什么。
只有驱动自己知道,这通常指向一个更大的、驱动自己定义的私有结构体——比如 Intel 的驱动里叫 iwl_priv,Marvell 的叫 lbtf_private。
这种设计把「通用框架」和「私有实现」切得干干净净。
要开始干活,驱动得先做几件事:
- 申领空间:调用
ieee80211_alloc_hw(size_t priv_data_len, const struct ieee80211_ops *ops)。 这一步不仅分配了ieee80211_hw,还顺带分配了你要求的priv_data_len大小的私有空间,并把两者绑在一起。 - 注册设备:拿着分配好的
hw,调用ieee80211_register_hw(struct ieee80211_hw *hw)。 这一步就像是给设备上户口,从此它就在内核里存在了。 - 接收数据:当硬件收到包时,驱动通常会调用
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,那恭喜,这包是废的。
接下来是一系列流水线作业:
-
监听模式处理:
ieee80211_rx_monitor()会出场。 如果你开了 Monitor Mode(抓包用的),它会先帮你剥掉 FCS(校验位),顺便处理一下可能存在的radiotap头部。 注意:不是所有网卡都支持 Monitor Mode。 -
802.11n 的重排序:如果你用的是 HT(802.11n),乱序的包需要排队。 这时
ieee80211_rx_reorder_ampdu()会被叫来把乱了次序的积木排好。 -
真正的处理:
__ieee80211_rx_handle_packet()最终会调用ieee80211_invoke_rx_handlers()。
这里有一个非常精妙的设计,叫 「责任链模式」。 内核有一串处理器,每个处理器都看一眼这个包:
- 如果它说「我不处理,下一个」,就返回
RX_CONTINUE。 - 如果它说「这包我吞了」,就返回
RX_QUEUED。 - 如果它说「这包是垃圾」,就返回
RX_DROP_MONITOR或RX_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 很像,也是先准备,再过流水线。
-
准备工作:
__ieee80211_tx_prepare()。 这里会做各种预检查,比如,如果你发的是一个长度小于 10 字节的包,内核会觉得你在开玩笑,直接dev_kfree_skb(skb)丢掉。 -
发送处理器链:
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 下面挖宝。
这里有几个特别好用的条目:
-
total_ps_buffered: 这就是 AP 的「暂存仓」。 它显示了 AP 为那些睡着的站点(PS 模式)缓存了多少个包(包括单播和组播)。 这是ieee80211_tx_h_unicast_ps_buf和ieee80211_tx_h_multicast_ps_buf两个人一起加出来的数。 -
statistics目录: 这里面全是战绩。frame_duplicate_count:收到了多少个重复的包(有人在重传)。transmitted_frame_count:成功发出了多少个包。retry_count:重试了多少次。如果这个数字飙升,说明你的信号质量很差,或者干扰很大。fragmentation_threshold:当前的分片阈值。
-
netdev:wlan0目录(假设你的接口叫 wlan0): 这里能看到具体的接口信息。aid:关联 ID。assoc_tries:试了多少次才连上。bssid:你连的路由器地址。
-
rc/name: 这里写着当前用的速率控制算法名字。 选对算法比选对路由器还重要。
12.6.7 无线模式:网卡的身份卡
最后,网卡不是只有一种形态。
根据你想干什么,你可以把它切成不同的模式。
这取决于硬件支不支持(你可以查 wiphy 的 interface_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)和聚合技术是怎么把无线网变成光纤的。
那个世界里的逻辑,又完全是另一套了。