跳到主要内容

12.4 节电模式 (Power Save Mode)

除了转发数据包,AP 还有一个重要的功能:充当那些「睡着」的客户端的保姆,帮它们缓存数据。

无线客户端大多是靠电池活命的——手机、笔记本、IoT 设备。为了省电,无线网卡不能一直开着接收机,它得时不时睡一会。但这就有个问题:当你睡觉的时候,别人发给你的微信怎么办?

在有线网络里,这个问题不存在,因为网线那头永远有电。但在无线世界里,这就是个大问题了。这一节,我们来看看在 Infrastructure 模式下,AP 和客户端是如何通过一套精巧的「推拉」机制来解决这个问题的。


进入节电模式

当客户端决定进入节电模式时,它不能悄悄睡过去——它得先跟 AP 打个招呼。

怎么做?通常是发一个 Null Data Packet(空数据包)。这名字听起来很绕,实际上它就是一个只有 802.11 头部、没有有效载荷的数据帧。这个包的真正目的不在于携带数据,而在于修改头部帧控制字段里的一个标志位:PM (Power Management) 位

只要把这个 PM 位置为 1,AP 就懂了:这哥们要睡觉了。

你可以把这理解为**「挂起请勿打扰」**的牌子。你把牌子挂在门口(发送 Null Packet),服务员(AP)经过看到后,就不会再敲门喊你接电话了。

但「挂牌子」这个比喻有一个地方是错的:现实中的服务员只是不敲门,但电话可能就直接挂断了。而在 802.11 里,AP 不仅不会打扰你,还会帮你把所有打进来的电话(数据包)都录音存起来(缓存)。

AP 收到 PM=1 的包后,会立即启用一套缓存机制。它在内核里(具体说是 mac80211 子系统)为每一个关联的站点维护了一个队列,叫做 ps_tx_buf

这个 ps_tx_buf 是个链表结构,专门用来存放发给这个站点的单播数据包。

但是,内存是有限的。mac80211 给每个站点的 ps_tx_buf 设定了上限:128 个包 (STA_MAX_TX_BUFFER)。

⚠️ 注意

这个限制是硬性的。如果客户端睡得太久,发包方又很猛,这 128 个槽位很快就会填满。

填满之后怎么办?AP 不会崩溃,它会开始「扔东西」。它遵循 FIFO (First In, First Out) 原则——把最老的包扔掉,给新包腾位置。这意味着,如果你的手机在省电模式下睡死过去了,醒来后你可能会发现最早的几条消息丢了,而且永远找不回来了。

除了单播包,还有一个更麻烦的家伙:组播/广播包

为什么麻烦?因为在 802.11 的定义里,广播包是要发给 BSS 里所有人的。只要有一个人在睡觉,这个广播包就不能发——因为发了他也收不到。

为了解决这个问题,AP 维护了一个全局的、大家共享的缓冲区:bc_buf

只要 BSS 里至少有一个站点处于睡眠状态,AP 就会把所有的组播和广播包扔进这个 bc_buf 里。这个缓冲区也有上限,也是 128 个包 (AP_MAX_BC_BUFFER)。

现在我们可以完善那个比喻了:AP 就像是一个前台。当你睡着(PM=1)时,前台会把找你的私人信件塞进你的私人信箱,而把那些群发给大家的传单先锁在抽屉里——只要屋里还有一个人在睡觉,传单就不发,等人齐了再发。


退出节电模式

客户端睡觉是为了省电,不是为了休克。它得醒来干活。怎么知道该醒来?通常靠一个定时器。

醒来第一件事做什么?听广播。

AP 会周期性地发送一种特殊的管理帧,叫做 Beacon (信标)。这个频率很高,通常是每秒 10 个 (100ms 间隔)。你可以把它想象成 AP 的心跳,或者整点报时。

Beacon 帧里不仅带着同步时间,还带着一个关键的信息元素,叫做 TIM (Traffic Indication Map,流量指示图)

这是一个非常聪明的位图设计。

还记得之前说的 AID 吗?每个关联的站点都有一个 1-2007 的 ID。TIM 就是一个包含 2008 个位的数组(对应 AID 0-2007)。如果 AP 发现你的 ps_tx_buf 里躺着包,它就会把 TIM 里属于你的那一位置 1

客户端醒来后,抓到一个 Beacon,就开始检查 TIM。

这个检查动作在内核里通过 ieee80211_check_tim() 方法实现(定义在 include/linux/ieee80211.h)。它不需要把整个 TIM 都读完,只需要算出自己的 AID 对应的位是不是 1。

  • 如果是 0:很好,没人找我,接着睡。
  • 如果是 1:醒醒,有活儿干了。

一旦发现自己的位被置 1 了,客户端就知道 AP 手里压着货。这时候,它需要主动去「提货」。

怎么提?通常是发 PS-Poll (Power Save Poll) 控制帧。

你可以把 PS-Poll 理解为你在前台拍桌子:「喂,我的信呢?」。

AP 收到 PS-Poll 后,就会从 ps_tx_buf 里把包掏出来发给你。这里有个微妙的地方:AP 会在发出来的每一个包的帧控制字段里,设置一个叫做 IEEE80211_FCTL_MOREDATA 的标志位

  • 如果这个位是 1:说明「还有货,接着 Poll」。
  • 如果这个位是 0:说明「这是最后一个了,拿完赶紧睡」。

客户端就是靠这个位来判断还要不要继续发 PS-Poll。一旦收到 MOREDATA=0 的包,它就知道存货空了,通常会再次进入睡眠(当然,标准没强制你必须睡,你可以醒着,但那样电池会挂得很快)。

回到那个「前台」比喻: TIM 就像是大堂里的滚动屏,上面显示着「101 号房有快递」。你醒了看一眼屏幕,发现有你的名字,就去柜台拍卡。 前台把第一件快递递给你,顺手说了一句「还有一件」。你拿了这件再拍卡,直到前台递给你最后一件时没说话,你就知道拿完了,可以回房接着睡了。


处理组播/广播缓冲区

单播包的逻辑比较简单:谁睡就给谁存。但组播/广播包的逻辑稍微复杂一点。

根据标准,组播/广播的 AID 被定义为 0

所以,当 AP 有广播包要发,且屋里有人在睡觉时,它就会把 TIM 的第 0 位(TIM[0])置为 1。但这还不够。

广播包很吵,对电量敏感。如果每次醒来都处理一遍广播包,客户端会很难受。而且,很多设备对广播包的兴趣没那么大。因此,802.11 引入了一个叫做 DTIM (Delivery TIM) 的概念。

DTIM 是一种特殊的 TIM,它不是每个 Beacon 都出现,而是每隔几个 Beacon 出现一次。这个间隔叫 DTIM Period,通常配置为 3 或 5(即每 300ms - 500ms 一次)。

只有在带有 DTIM 的那个 Beacon 之后,AP 才会把 bc_buf 里积压的广播/组播包全部倒出来。这意味着客户端可以忽略普通的 Beacon 带的 TIM[0],只在 DTIM 周期醒来处理广播包,从而极大节省电量。

在内核代码里,你需要调用 ieee80211_get_buffered_bc() 方法来从 bc_buf 里「抠」出这些包。

我们来看一张图,把这几个缓冲区的位置理清楚。

(此处为 Figure 12-3: AP 中缓冲数据包的示意图,展示 sta_info 链表、各自的 ps_tx_buf 以及共享的 bc_buf)

在 mac80211 的实现里,AP 被抽象为一个 ieee80211_if_ap 对象。这个对象里有一个成员叫 ps(是 ps_data 结构体的实例)。前面提到的那个大锅饭 bc_buf,就住在 ps 结构里。

接下来这张图,展示了客户端用 PS-Poll 疯狂「提货」的过程。

(此处为 Figure 12-4: 客户端发送 PSPOLL 包从 ap 的 ps_tx_buf 获取数据包的流程图)

注意看图里的数据流向:AP 发出的包,除了最后一个,全带着 MOREDATA 标志。这就是那个让客户端保持饥饿感的信号。一旦这个信号消失,交互就结束了。

(图里为了简洁没画 ACK 流程,但实际上每个数据包都需要 ACK 确认,这点在驱动实现时千万别忘了。)


⚠️ 节电 vs. 电源管理:别搞混了

这里有一个术语陷阱,很多新手(甚至老手)都会绕进去。

Power Management (电源管理)Power Save Mode (节电模式) 是两码事。

  • Power Save Mode:我们上面讲的这套。客户端睡几毫秒,AP 帮着缓存包。这是 802.11 协议层面的活儿,发生在正常运行期间。
  • Power Management:这是系统层面的活儿。比如你合上笔记本盖子(Suspend to RAM),或者点休眠(Hibernate)。这时候不仅是网卡断了,整个 CPU 都快停了。这由内核的 PM 框架处理,在 net/mac80211/pm.c 里,驱动里对应的是 resume/suspend 回调。

别把这两个搞混了。写驱动时,如果你试图在 suspend 回调里去处理 TIM 位图,那说明你走错房间了。


这一节我们其实只讲了一件事:信任

客户端信任 AP 会在它睡觉时帮它守住数据包;AP 信任客户端会在醒来时主动来取。为了维持这种信任,它们需要 TIM、PS-Poll、Null Data 这些看起来繁琐的握手信号。

下一节,我们将视角拉高,看看那个支撑起所有连接、扫描、认证的幕后英雄——MLME (MAC 层管理实体)。没有它,无线网络就是一盘散沙。