朋友说家里刚换了千兆宽带,结果手机连Wi-Fi测速最多跑两三百兆,问我是不是运营商偷工减料。我第一反应不是吐槽运营商,而是问他家里同时连着多少设备。他一数,手机、平板、电视、智能音箱、摄像头加一起十来个。我说这大概率不是宽带的问题,是Wi-Fi自己在跟自己打架。这就要说到Wi-Fi的分布式协调功能(DCF),以及藏在它里面的伪随机退避计时器。
Wi-Fi这个DCF,全称Distributed Coordination Function,翻译过来就是“分布式协调功能”。它解决的问题特别朴素:屋里这么多设备共用一条无线信道,同一时刻只能有一个设备在说话,否则信号叠在一起全变噪音。问题是,谁来说、什么时候说、说完了怎么让给别人?DCF就是这套规矩。而这套规矩里最核心的“排队叫号系统”,就是伪随机退避计时器。理解了它,你就能明白为什么设备一多Wi-Fi就卡,为什么某些延迟高的场景下体验会急剧下降,也才能明白怎么样从路由器设置和抓包数据里找出真正的问题。
这篇文章我会从DCF的整体设计讲起,把伪随机退避的原理、参数、演算过程拆开,再带你看一遍抓包和代码模拟的实际操作,最后聊聊Ubuntu找不到Wi-Fi、Wi-Fi Direct投屏卡顿这些真实场景里跟退避机制有关的排查经验。适合做无线网络、嵌入式通信、路由器开发的朋友读,也适合家里设备多、想搞清楚Wi-Fi原理的普通用户。
1. 先搞懂DCF在整张Wi-Fi“饭桌”上管什么事
1.1 为什么无线网络不能“边发边听”
有线以太网解决冲突的思路是CSMA/CD,载波侦听多址访问加冲突检测,核心是“边发边听”。网线是双向的,网卡发信号的同时还能检测到线缆上的电压变化,一旦发现自己的帧跟别人的帧叠在一起,立刻停止发送,然后双方各自随机等一会儿再重试。
Wi-Fi没法这么干。无线网卡在发射数据的时候,天线发出的信号功率很强,这个信号会直接从发射链路泄漏到接收链路,把远处传来的微弱信号彻底淹没。换句话说,一个设备正在发射的时候,是听不见别人说话的。所以Wi-Fi不能“边说边听”,只能换个思路:在开口之前先把耳朵竖起来听一会儿,确定没人说话再开口。这就成了CSMA/CA,载波侦听多址访问加冲突避免。CD变成了CA,从“撞了再补救”变成了“尽量别撞”。
可“听一听再开口”也有麻烦。无线信道是开放的,谁都听得到,而且所有设备地位平等,没有一个总调度员在那里发号施令。你说听完没声音就开口,那我也听完没声音就开口,结果两个人同时开口,还是撞了。这时就需要一套额外的规则来减少“同时开口”的概率,这套规则就是DCF。DCF的聪明之处在于,它不试图消灭碰撞,因为无线环境里碰撞根本消灭不了,它只要求所有设备在用信道之前,先从一个随机数里抽一个等待时间,各自等长短不同的时间再说话。等的时间短的人先开口,其他人听到信道忙了,就继续等。这样一来,多人同时开口的概率被压到足够低。
1.2 一帧数据从排队到发出去的完整流程
DCF的完整流程可以拆成“听、等、退避、发、确认”五个动作。设备要发数据帧时,先监听信道,如果信道空闲,并且空闲时间已经持续了一个DIFS(分布式协调功能帧间间隔),理论上它就可以发送了。但802.11标准规定,发数据帧之前必须额外执行一个退避过程,哪怕信道一直是空闲的也要退避。这一步很多初学者会忽略,但它恰恰是DCF公平性的关键:如果没有这个强制退避,那么上一个发送完刚歇下来的站点会在DIFS后立刻抢到信道,一直霸占不放手,别的站点永远没机会。
退避过程具体是这么走的:站点从0到当前竞争窗口(CW)之间随机抽一个整数,再乘以一个物理层的Slot Time,得到退避计时器的初值。然后在每个空闲时隙里,退避计数减1;一旦信道被其他站点占用,退避计时器就冻结,等信道重新空闲并稳定了一个DIFS之后再继续倒数。当计数值减到0,站点才真正开始发数据。接收方如果顺利解出帧,会在SIFS之后回复一个ACK。发送方收到ACK,就知道这帧成功了,下一次发新帧时把竞争窗口重置回最小值。如果没收到ACK,发送方默认刚才撞车了,于是把竞争窗口翻倍,重新抽退避值,再走一遍“听、等、退避、发”的流程。
这套流程里,帧间间隔是不同帧类型之间优先级的分水岭。SIFS最短,留给ACK、CTS这种必须马上响应的控制帧;PIFS比SIFS长一点,用在AP主动接管信道的场景;DIFS更长,用来区分普通数据帧之间的等待级别。具体的数值随物理层不同而不同,常见局域网配置下,802.11a/g/n/ac的Slot Time是9微秒,11b则是20微秒,SIFS和DIFS也随物理层各有差别。这些数值看着不起眼,但它们共同决定了这套分布式协调机制的响应速度和公平性。
| 帧间间隔 | 用途 | 典型关系 |
|---|---|---|
| SIFS | ACK、CTS等高优先级帧 | 最短间隔 |
| PIFS | 点协调功能使用 | SIFS + 1个Slot Time |
| DIFS | 普通DCF数据帧发送前 | SIFS + 2个Slot Time |
| EIFS | 收到错误帧后的等待 | 比DIFS更长 |
2. 伪随机退避计时器,到底“伪”在哪里
2.1 一个时隙一个时隙地数数:退避计时器的工作方式
退避计时器本质上就是一个倒数计时器,但它有两个特点特别重要:随机和冻结。
先说随机。站点要发送数据帧时,先从当前竞争窗口里按均匀分布抽一个整数。这里说的均匀分布,意思是窗口里每个整数被抽中的概率理论上相同。假设CW等于15,站点就在0到15之间抽一个数,抽到0到15的概率都是十六分之一。之所以要求均匀,是为了避免退避时间短的站点总占便宜,退避时间长的站点总吃亏。如果随机数生成器有偏,某些站点的平均退避时间偏短,长期下来信道就会被它们垄断,这违背DCF的公平设计。
再说冻结。退避计时器并不是发条手表上好了就一直走,它只在信道空闲时递减。只要信道上有其他站点的信号,计时器就停在原地,这叫“退避冻结”。冻结机制的实际意义非常大:它让所有正在退避的站点保持相对位置不变,等信道一空,大家继续同步倒数。设想两个站点正在退避,一个还剩3个时隙,一个还剩7个时隙,这时有第三方的长帧占用了信道,如果不冻结,计时器还在走,等信道空了,那个本来还剩3个时隙的站点可能已经变成0了,原本的公平秩序就被打乱了。冻结保证了“先到先得”的相对排序在信道忙期间不会失真。
当退避计时器数到0,站点会立刻通过物理层的CCA(空闲信道评估)再确认一次信道状态,确认空闲后就开始发包。这里还有一个细节:如果退避计时器到0的那一刻,CCA检测发现信道竟然是忙的,站点不能硬发,它会先等一下,等到信道空闲并满足DIFS后再发送。这个细节用来处理某些站点还没来得及更新NAV(网络分配向量)的情况,是虚拟载波侦听机制里比较容易被忽略的一环。
2.2 竞争窗口指数翻倍,背后的数学逻辑
伪随机退避的“伪随机”只是手法,真正的灵魂是竞争窗口的动态变化。802.11采用的是二进制指数退避算法,核心逻辑很简单:每一次传输失败,竞争窗口就翻倍,直到达到上限;每一次传输成功,竞争窗口立刻重置为最小值。
CW的典型值在不同物理层里是有差异的。802.11b的CWmin是31,CWmax是1023;802.11a/g/n/ac/ax的CWmin是15,CWmax是1023。计算公式是:
CW_new = min(2 * (CW_old + 1) - 1, CWmax)
第一次失败后,CW从15变成31,第二次失败变成63,接着是127、255、511,直到撞到1023的天花板。这个递进公式可能看上去有点绕,其实等价于把窗口的“上边界”先加1,翻倍,再减1。因为窗口大小永远是“2的n次方减1”这种形式,所以只能通过这种方式从15跳到31,从31跳到63。
为什么要指数翻倍而不是线性增加?原因是网络负载越重,碰撞越频繁,这时必须让所有设备都“退得更远”,把重新取随机数的时间范围拉大,以减少下一轮同时发送的概率。这就像会议室里大家同时开口说话,第一次撞车后大家约定数1秒再开口,结果又撞了,那就数2秒,再撞就数4秒,直到收敛。指数增长能快速拉开退避值之间的差距,让站点在几轮重试内迅速分散开。如果只做线性退避,比如每次只加1,那么在大量设备同时竞争时,退避时间范围会增长得很慢,碰撞率居高不下,整个网络会一直处于“撞车-重传-再撞车”的泥潭里。
系统还有一个重传上限来兜底。短帧的重试上限默认是7次,长帧是4次,超过上限就直接丢包,不再继续无休止地重传。这个设计防止了单个坏帧耗尽信道资源。每次重试的CW值都不同,所以实际退避时间不是固定值,而是随着重试轮次逐渐变大。以下是一张第一轮到第五轮重试时CW取值范围的表,方便直观感受:
| 重试轮次 | 重传次数 | CWmin | CWmax | 可能退避时隙数 |
|---|---|---|---|---|
| 第1次发送 | 0 | 0 | 15 | 0到15 |
| 第1次重传 | 1 | 0 | 31 | 0到31 |
| 第2次重传 | 2 | 0 | 63 | 0到63 |
| 第3次重传 | 3 | 0 | 127 | 0到127 |
| 第4次重传 | 4 | 0 | 255 | 0到255 |
| 第5次重传 | 5 | 0 | 511 | 0到511 |
注意CWmin这一列写成0,是因为竞争窗口指的范围包含0,站点可以在0到这个最大值之间等概率抽取。抽取到0的概率始终存在,也就是说极端情况下某次重传可能完全不退避。概率虽小但不是零,这也是DCF允许的。
2.3 “伪随机”为什么够用,又是从哪来的
标题里特意点出的“伪随机”三个字,很多人第一次看到会觉得奇怪,随机就随机,怎么还“伪”?其实这里的伪随机不是说算法骗人,而是指它本质上不是真正的物理随机。
802.11标准并没有规定网卡必须用真随机数发生器,它只要求退避值在统计上服从均匀分布。实际工程里,网卡通常用伪随机数生成器(PRNG)来产生退避值,比如线性反馈移位寄存器,或者更简单的时间/计数器混合算法。伪随机数由确定性的种子启动,只要种子定了,后续序列就定了。但各个设备的种子来源不同,启停时刻不同,时钟漂移不同,所以在真实环境里,两个设备同时产生相同退避序列的概率极低。伪随机数有三个优点:不需要额外的物理熵源硬件,生成速度快,逻辑可复现,这对芯片设计非常友好。有些高端芯片确实会集成真随机数发生器,但从协议角度看,伪随机数已经足够支撑DCF的统计需求。
关键在于,DCF关心的从来不是“数本身不可预测”,而是“多个站点抽到同一个数的概率足够低”。只要每个人都从同一个窗口里均匀抽取,设备数量不多时,碰撞概率就可以控制在可接受范围。而且伪随机数的分布特性完全可以通过测试验证,硬件实现时可以反复仿真,不需要依赖玄学。题目标题点出“伪随机”,想表达的实际含义是:IEEE 802.11用一套工程上易实现的随机数方案,支撑起了分布式的公平接入机制。越是理解这一点,越能明白DCF不是玄学,而是严谨的统计学设计。
3. 把退避过程“看”出来:抓包和蒙特卡洛模拟
3.1 抓无线包时先看什么
讲完原理,肯定有人想问:道理我都懂,可我自己怎么验证?最直接的办法是抓包。抓Wi-Fi包不像抓有线包那么容易,因为普通网卡只接收发给自己的帧,要想看到整个信道的争用过程,需要让网卡进入监视模式。
在Linux环境下,如果网卡和驱动支持,可以用下面这组命令把无线网卡切到monitor模式:
sudo ip link set wlan0 down sudo iw dev wlan0 set type monitor sudo ip link set wlan0 up sudo wireshark切换成功之后,Wireshark就能看到周围所有Wi-Fi帧。不是所有网卡都支持这个功能,很多USB网卡和内置网卡驱动不支持monitor模式,遇到这种情况不用死磕,换一片支持monitor的USB网卡就行。还有更省事的办法,不少商用AP自带无线抓包功能,可以在管理后台直接抓取本AP覆盖范围内的空口报文。
抓包之后,重点看两个东西。一个是重传标志位。802.11帧头里有一个Retry位,置1表示这个帧是重传帧。在Wireshark里可以用过滤表达式单独筛出来:
wlan.fc.retry == 1如果统计数据里重传帧的比例超过5%,基本可以判断信道竞争很严重,DCF退避机制正在频繁介入,网络延迟和吞吐都会受影响。另一个值得看的是NAV时间,在Wireshark里对应wlan.duration字段,它反映了站点声明自己要占用信道多长时间。如果周围有很多站点都在设置很长的NAV,说明信道被长时间预约,你的设备很多时候只能退避等待。
实测时要注意一个坑:如果只是自己家里一台手机一台电脑,重传率通常很低,看不出什么问题。最好在多个设备同时开视频会议、播在线视频的时刻抓包,这时候信道上数据帧密集,退避和重传现象会明显得多。抓包环境尽量避开微波炉、蓝牙音箱等干扰源,否则数据里混入太多物理层噪声,反而看不清DCF自身的规律。
3.2 Python模拟:碰撞率和平均退避时间的直观结果
抓包只能看现象,想定量理解“CW翻倍到底有多大作用”,可以用一段简单的蒙特卡洛模拟来算。我写了一个很精简的Python脚本,模拟多个站点同时退避的场景:每个站点从0到CW之间均匀抽一个整数,如果抽到的最小值不止一个站点,就认为这两个站点会在同一时隙发数据,产生碰撞;同时统计最小值的平均值,这个平均值可以代表平均退避时隙数。
import random def simulate(nodes, cw, trials=50000): coll = 0 backoff_sum = 0 for _ in range(trials): vals = [random.randint(0, cw) for _ in range(nodes)] mn = min(vals) if vals.count(mn) > 1: coll += 1 backoff_sum += mn return coll / trials, backoff_sum / trials for nodes in [2, 5, 10, 20]: for cw in [15, 31, 63, 127]: p, avg = simulate(nodes, cw) print(f"N={nodes:2d} CW={cw:3d} 碰撞率={p:6.2%} 平均退避时隙={avg:7.2f}")这个模型做了简化,它假设所有站点在同一时刻开始退避,没有考虑信道冻结、传输时间和ACK时间,但用来观察竞争窗口和站点数量对碰撞概率的影响足够了。跑出来大概是这样的趋势:
| 站点数 | CW=15 | CW=31 | CW=63 | CW=127 |
|---|---|---|---|---|
| 2个站点 | 约6% | 约3% | 约1.5% | 约0.8% |
| 5个站点 | 约17% | 约9% | 约5% | 约2.5% |
| 10个站点 | 约26% | 约14% | 约8% | 约4% |
| 20个站点 | 约33% | 约19% | 约10% | 约5% |
两个结论非常明显。第一,站点数越多,碰撞率越高;第二,CW越大,碰撞率下降,但同时平均退避时隙也变大。这就是DCF的本质代价:用时间换稳定。CW设为15时,20个站点的碰撞率高达三成以上,如果信道再忙一点,重传风暴几乎是必然的;CW翻到127后碰撞率降到5%左右,网络稳定了,但每个站点平均要等几十个时隙才能开口说话。
3.3 从模拟结果反推真实的退避开销
平均退避时隙数看起来只是数字,换算成时间才更能体感。以802.11g/n常用的Slot Time 9微秒计算,平均退避20个时隙就是180微秒。如果传输一个1500字节的数据帧,在54Mbps速率下大约需要222微秒,在300Mbps下大约40微秒。当一个站点平均要退避180微秒才能发一个耗时40微秒的帧时,信道利用率已经很低了。
这就是为什么多设备环境下,吞吐量下降并不是线性衰减。站点数从2涨到10,碰撞率从6%变成17%到26%,加上碰撞后的指数退避,实际可用信道时间被大幅吞噬。更麻烦的是,碰撞后CW会翻倍,退避时间进一步拉长,所以设备越多,每个设备分到的有效传输时间越少,还越不稳定。
我建议你跑完这段模拟后,把CW换成15、31、63、127、255、511、1023,再画一条碰撞率曲线出来,会看到一条典型的“指数退避救场曲线”。很多人在路由器后台调“Beacon间隔”“RTS阈值”之类的参数时很迷茫,其实只要理解了退避开销,你就知道这类参数该在什么场景下动,什么场景下根本不用动。
4. 实战中的两个经典坑:Ubuntu没有Wi-Fi与投屏卡顿
4.1 Ubuntu找不到Wi-Fi,先别急着装驱动
“Ubuntu系统没有Wi-Fi”是很多Linux用户都会撞上的问题,而且这个问题的锅,有时候根本不在无线网卡驱动,而是出在系统服务和射频开关上。我自己的排查顺序是这样的。
第一步,先确认无线网卡有没有被系统认到。执行:
lspci -nnk | grep -iA3 network如果是USB无线网卡,就用lsusb看。有输出说明硬件被PCI/USB总线识别了,问题可能出在驱动加载或上层服务;完全没输出,硬件就没认到,这种情况常见于太新的网卡搭配太旧的内核,优先考虑升级内核或安装backport驱动。
第二步,看驱动状态。执行:
lshw -C network输出里的driver字段如果为空,说明没有驱动绑定这个设备。常见的做法是先安装linux-firmware,再检查dkms状态:
sudo apt install linux-firmware如果网卡芯片太新,官方源里没有合适固件,就只能找芯片厂商提供的dkms源码编译。编译过程不复杂,但一定注意每次升级内核后要重新触发dkms模块构建,否则一升级内核Wi-Fi又消失。
第三步,检查射频开关。这一步经常被忽略,但实际遇到频率不低。执行:
rfkill list如果看到某个设备的状态是blocked: yes,说明无线被软开关或硬开关禁用了。软禁用执行sudo rfkill unblock wifi就能解开;硬禁用则要看笔记本上有没有实体飞行模式开关或Fn组合键。很多笔记本在Windows下能联网,一到Ubuntu就找不到Wi-Fi,就是因为系统默认把某个射频开关锁住了。
第四步,确认NetworkManager服务状态。执行:
systemctl status NetworkManager如果服务是死的,执行sudo systemctl restart NetworkManager试试。这套流程走完,绝大多数“没有Wi-Fi”的问题都能定位到具体环节。一旦无线网卡正常工作,接下来它参与信道竞争的方式就跟路由器上的DCF机制密切相关了。
4.2 Wi-Fi Direct投屏卡顿,问题常常出在竞争和休眠
Wi-Fi Direct技术,很多人是通过无线投屏第一次接触到的。手机和平板投到电视、显示器时,设备之间直接建立P2P连接,不需要经过路由器,这在Wi-Fi术语里叫Wi-Fi P2P。P2P连接里的Group Owner(群主)设备扮演类似AP的角色,另一个设备作为客户端加入。很多人以为P2P直连就是“点对点专线”,不会被干扰,实际上完全不是这样。Wi-Fi Direct仍然运行在2.4GHz或5GHz频段,仍然使用CSMA/CA机制,仍然要走DCF的退避计时器。也就是说,投屏的两个设备只要在同一个信道附近活动,一样要跟其他Wi-Fi网络抢信道。
无线投屏卡顿还有一个隐性原因,就是Wi-Fi Direct的省电机制。投屏这种场景,手机屏幕常亮、持续传输视频流,按理说不需要省电,但P2P标准里定义了群主和客户端之间的休眠协商机制,比如客户端可以通过Notice of Absence向群主申请休眠窗口。一旦协商出的休眠窗口不合理,视频帧就会在发送端排队,表现为偶发的画面停滞和音频断续。这种延迟问题叠加DCF本身的随机退避,体感上就是“投屏一会儿流畅一会儿卡”。
如果遇到投屏卡顿,我的排查方向是:先看两个设备是否都支持5GHz频段,优先把投屏连接固定在5GHz;再看周围2.4GHz信道占用程度,因为我实测过很多投屏器默认跑在2.4GHz,而2.4GHz在居民楼里基本是“菜市场”,蓝牙、无线鼠标、邻居Wi-Fi全挤在一起;最后检查投屏器或电视的省电选项,有“游戏模式”或“低延迟模式”就打开,很多设备默认的省电策略会强制插入休眠窗口,对低延迟应用非常不友好。
4.3 多设备同处一室,如何减少竞争损耗
回到文章开头那个朋友家的场景,十来台设备挤在同一个路由器下面,即使都连着5GHz,DCF的竞争成本也是一笔实实在在的开销。设备数一多,碰撞率上升,重传变多,退避时间变长,整体延迟自然恶化。这时候最有效的措施不是换更贵的网线,而是减少同频竞争。
优先把支持Wi-Fi 6的设备尽量都连到Wi-Fi 6路由器上,因为802.11ax引入了OFDMA和MU-MIMO,AP可以主动分配资源,不用所有设备都靠随机退避抢信道。如果路由器后台能单独启用Wi-Fi 6模式,且家里所有设备都兼容,可以关掉旧协议的兼容模式,避免Wi-Fi 4老设备拖慢整个信道的退避节奏。尤其是802.11b设备,它的Slot Time长达20微秒,比11g/n/ac的9微秒慢一倍还多,一个老设备存在就会逼迫AP为了兼容而拉长整个BSS的退避时间,所有设备一起遭殃。
智能家居设备能分到2.4GHz的尽量留在2.4GHz,把5GHz频段留给手机、电脑、电视这类对带宽和延迟敏感的设备。这样相当于物理上隔离了两个竞争域,比任何路由器参数优化都直接。如果家里面积大、房间多,一个AP扛不住,建议用有线Mesh回程或者多AP组网,而不是指望一个大功率路由器单挑全屋。多AP各自调度各自的信道,减少了单个BSS内竞争设备的数量,这才是解决“人多嘴杂”的根本思路。
5. 为什么我至今还在研究DCF,以及下一步想折腾的事
DCF这套机制从1997年802.11标准诞生起就存在,到现在802.11be都提上日程了,它还是所有Wi-Fi设备的“默认用语”。就算Wi-Fi 6的OFDMA能通过AP集中调度减少竞争,一旦网络里混入不支持802.11ax的旧设备,或者某些突发流量需要在没有AP参与的情况下传输,DCF仍然是最后的兜底机制。所以搞懂DCF,不是学历史知识,而是掌握理解Wi-Fi性能问题的根基。
我自己常用的工作流是:遇到Wi-Fi变慢,先别急着猜干扰,先抓包看重传率,再看路由器后台的接入设备数和信道占用,最后才是调参数。重传率一高,十有八九是DCF竞争在恶化,这时与其去调什么“Wi-Fi功率”“频道带宽”,不如先把设备分流、把频段分开。这些经验从哪来?就是从一次次抓包、一次次模拟和一次次被卡顿折磨后累计出来的。
下一步我想折腾的方向,是把802.11ax里OFDMA调度和DCF退避机制共存的边界条件测得更清楚,比如在混合速率场景下,哪些情况下OFDMA能完全压住竞争,哪些情况下退避还是会冒出尖峰延迟。这个方向很有意思,也可能让量化“Wi-Fi 6到底快了多少”这个问题多一个更扎实的维度。如果你也在做无线网络调试或者协议分析,欢迎从今天的退避模拟脚本开始,把站点数改成你家里的真实设备数,看看你的环境里碰撞率是多少,再去抓包验证一下。很多“路由器玄学”,其实都是可以用数字解释清楚的。