第一次碰 WiFi 驱动的人,几乎都会在同一个地方卡住:lspci能看到设备、dmesg里有枚举日志、/sys/bus/mmc/devices里节点也在,但ifconfig -a死活不出现wlan0。更难受的是,翻遍网上的 Linux 驱动开发资料,字符设备驱动、I2C 设备驱动、平台设备驱动都讲得很清楚,一到 WiFi 就好像断层了——因为 WiFi 驱动不是"一类设备驱动",它是总线驱动 + 网络设备 + 无线协议栈适配层三件事叠在一起,缺任何一层都跑不起来。
这篇内容就是我把自己折腾 Linux WiFi 设备驱动开发的路径整理出来的一次记录,面向的是已经写过最简单的字符设备驱动、看得懂file_operations和 probe/remove 那套模型,但还没摸过 mac80211 的开发者。核心想解决三件事:搞清楚内核里"WiFi 驱动"到底指哪一部分代码;把从总线枚举到能扫描、能连接这条链路拆开看;以及把那些官方文档里几乎不会写、但实践里必踩的坑提前摆出来。全文按"概念边界 → 子系统骨架 → 环境搭建 → 注册时序 → 监管域与信道 → 排查链路 → 设备树与电源 → 性能调优"的顺序推进,你可以按需跳读。
1. 先搞清楚"WiFi驱动"在内核里到底指哪一部分代码
1.1 一个 net_device 背后站着四个不同的东西
很多人第一次看wlan0会以为它和eth0差不多,都是网卡驱动注册出来的。这个理解只对了一半。Linux 里一个无线接口确实最终表现为一个struct net_device,但支撑它的代码分成四块,各自归不同的子系统管:
| 层面 | 归谁管 | 你写不写 |
|---|---|---|
| 总线枚举与资源分配 | PCIe / USB / SDIO / SPI 子系统 | 不写,但要会读日志 |
| 硬件具体操作(寄存器、DMA、固件) | 你写的驱动模块 | 必须写 |
| 无线协议栈适配(管理帧、扫描、密钥) | mac80211 / cfg80211 | 二选一,看硬件能力 |
| 用户态配置与连接 | wpa_supplicant、iw、NetworkManager | 不写,但要会调 |
换句话说,一个完整的"WiFi 驱动开发"任务里,真正需要你从零写的只有第二层和第三层的一部分。第一层内核已经做完了,第四层有成熟工具。新手最容易犯的错是把第四层的事也揽过来,比如自己写扫描逻辑、自己在驱动里解析 SSID 列表——这些在 mac80211 体系下都是框架做掉的事情。
1.2 为什么内核里没有"WiFi驱动"这个目录
打开drivers/net/wireless/,你会看到的是按厂商分的目录:intel/、ath/、broadcom/、realtek/、mediatek/,而不是一个统一的wifi/。这个组织方式本身就说明了问题:无线驱动是按芯片家族划分的,因为不同芯片的固件接口、队列模型、射频校准方式完全不同,没有抽象的余地。
真正起到"统一"作用的是net/wireless/和net/mac80211/这两个目录。前者就是 cfg80211,后者就是 mac80211。你写的驱动模块本质上是在这两个框架提供的回调表里填空。所以我通常建议:读源码先从include/net/cfg80211.h和include/net/mac80211.h的头文件注释入手,而不是先去看某个具体厂商驱动的几千行代码——后者会让你淹没在硬件细节里,看不到骨架。
1.3 一个反直觉的结论:先别急着看自己的芯片
我带过几个刚入门的同学,他们的第一反应都是"我要开发某某型号的 WiFi 驱动,那我先把厂商给的 SDK 看一遍"。结果往往是看了一周,连哪些函数是必须实现的、调用顺序是什么都说不清。
更有效的路径是反过来的:先拿一个内核里已经支持的、结构干净的驱动当模板,比如drivers/net/wireless/ath/ath9k/或者drivers/net/wireless/mediatek/mt7601u/,把它的probe、ieee80211_ops、cfg80211_ops这三处抄出来对照头文件看一遍,建立起"我的驱动需要提供哪些能力"的清单,再回去翻芯片手册。顺序对了,效率差好几倍。
2. cfg80211与mac80211:一上一下的两条控制路径
2.1 分层不是摆设,选错层等于白干
先把关系说清楚。cfg80211 是配置面:它是 nl80211 netlink 接口的实现者,负责处理用户态发来的"扫描一下""连到这个 BSS""设置监管域"这类请求,向上暴露的是一个叫wiphy的对象。mac80211 是数据面调度器 + 软件 MAC:它处理管理帧的收发与解析、块确认聚合、报文重排、队列调度、密钥管理等一大堆协议细节,向下调用你实现的ieee80211_ops。
这两者的关系不是并列的,而是 mac80211 向 cfg80211 注册自己,你的驱动再向 mac80211 注册硬件。调用链大概是:
iw dev wlan0 scan -> netlink -> cfg80211 (net/wireless/) -> rdev_scan 回调 -> mac80211 (net/mac80211/) -> ieee80211_ops.hw_scan 或 sw_scan_start(你的代码)看清楚这条链,你就知道为什么调试的时候既要开 cfg80211 的 tracepoint,又要开 mac80211 的 tracepoint——问题可能卡在任何一段。
2.2 fullmac 还是 softmac,这个决定影响后面所有工作
这是开发初期最关键的选型判断,判断错了后面全部推倒重来:
| 类型 | 硬件做什么 | 你要实现什么 | 典型场景 |
|---|---|---|---|
| fullmac | 硬件自己完成 802.11 MAC,包括扫描、关联、加密 | cfg80211_ops | 部分 SDIO/USB 低成本模块 |
| softmac | 硬件只做射频和基带,MAC 交给主机 | ieee80211_ops | PCIe 高性能网卡、嵌入式常见方案 |
判断方法很直接:看芯片手册里有没有"自主扫描""自主关联"这类描述,或者看固件接口是不是一个"命令 + 事件"的邮箱模型。如果固件把完整的 MAC 状态机都包了,那就是 fullmac 路线,你只需要实现cfg80211_ops里的scan、connect、disconnect、add_key等回调,工作量小,但灵活性差,很多统计信息拿不到。反之 softmac 路线你需要实现ieee80211_ops,工作量翻倍,但能控制到每一个重传、每一个聚合窗口。
2.3 ieee80211_ops 里哪些回调是真必须的
struct ieee80211_ops成员很多,第一次看容易慌。按我的经验,跑通最基本的"扫描 + 连接 + 收发"需要先实现这些:
static const struct ieee80211_ops my_hw_ops = { .tx = my_tx, /* 发数据帧,入口 */ .start = my_start, /* 启动硬件,配置队列 */ .stop = my_stop, .add_interface = my_add_interface, /* 创建 vif,分配硬件上下文 */ .remove_interface = my_remove_interface, .config = my_config, /* 信道、类型、功率等 */ .configure_filter = my_configure_filter,/* 决定哪些帧上报主机 */ .bss_info_changed = my_bss_info_changed,/* BSSID、速率集、beacon 丢失阈值 */ .sta_add = my_sta_add, /* 对端站信息 */ .sta_remove = my_sta_remove, .set_key = my_set_key, /* 密钥下发到硬件 */ .ampdu_action = my_ampdu_action, /* 聚合开关 */ .sw_scan_start = my_sw_scan_start, /* 没有硬件扫描能力时用软件扫描 */ .sw_scan_complete = my_sw_scan_complete, .get_tsf = my_get_tsf, /* 时间同步,很多上层依赖它 */ };这里面configure_filter和bss_info_changed是新手最容易忽略的两个。前者决定你能否收到 probe request、beacon 这类管理帧;后者是连接状态变化的唯一通知点,很多"连上了但收不到包"的问题就出在这里没处理BSS_CHANGED_BSSID。.get_tsf如果没有,某些依赖时间戳的功能会直接报错,不是可选项。
3. 搭建一个能反复验证的开发环境
3.1 内核源码与配置,别用发行版自带的头文件凑合
开发无线驱动必须要有和目标内核版本完全一致的内核源码树,而且要能编译出模块。发行版的linux-headers包只包含部分头文件,net/mac80211这类内部头是不会给你的。所以标准做法是:
# 拿到与目标板一致的源码和配置 git clone --depth 1 -b v6.1 <内核源码地址> linux-6.1 cd linux-6.1 cp /path/to/board/.config .config make ARCH=arm64 CROSS_COMPILE=aarch64-linux-gnu- olddefconfig make ARCH=arm64 CROSS_COMPILE=aarch64-linux-gnu- modules_preparemodules_prepare这一步很多人省掉,结果编译自己模块时提示找不到Module.symvers,或者更隐蔽地出现"符号能编过但加载时 unknown symbol"。原因是modules_prepare会生成模块版本校验所需的信息,跳过它会让你在insmod时才踩坑。
驱动模块的 Makefile 用最朴素的写法就够了:
obj-m += my_wifi.o my_wifi-y := main.o txrx.o fw.o debugfs.o KDIR ?= /lib/modules/$(shell uname -r)/build PWD := $(shell pwd) all: $(MAKE) -C $(KDIR) M=$(PWD) modules clean: $(MAKE) -C $(KDIR) M=$(PWD) clean3.2 调试环境:把 debugfs 和 trace 提前打开
无线部分的调试信息基本都在 debugfs 里,但很多发行版默认不挂载。开发前先确认:
mount -t debugfs none /sys/kernel/debug ls /sys/kernel/debug/ieee80211/同时内核配置里这几个选项要打开,不然后面排查会毫无抓手:
CONFIG_MAC80211_DEBUGFS=yCONFIG_MAC80211_MESH=y(做 mesh 才需要)CONFIG_CFG80211_DEBUGFS=yCONFIG_DYNAMIC_DEBUG=yCONFIG_FUNCTION_TRACER=y与CONFIG_FTRACE=y
3.3 一个容易被忽略的准备:接口命名策略
新内核默认启用了可预测接口命名,WiFi 接口可能叫wlp3s0或者带 MAC 后缀的名字,做脚本化测试时很烦。开发阶段我一般临时改回传统命名,避免脚本到处写死:
# 内核启动参数加 net.ifnames=0这不是技术难点,但能省下大量"为什么脚本找不到 wlan0"的时间。
4. 从 probe 到 register:让设备先"活过来"的完整时序
4.1 三种总线入口的差别,决定了 probe 的第一段怎么写
| 总线 | 资源获取方式 | 中断方式 | 数据搬运 | 常见坑 |
|---|---|---|---|---|
| PCIe | pci_iomap映射 BAR | MSI/MSI-X | 描述符环 + DMA | BAR 映射失败多为固件未加载或电源域未开 |
| USB | usb_set_intfdata | 中断 urb | bulk in/out + urb 池 | urb 提交失败通常是端点类型判断错 |
| SDIO | sdio_enable_func | 数据线中断或外部 GPIO | 块传输 | 3.3V/1.8V 电压切换时序错误导致初始化随机失败 |
| SPI | spi_setup | 外部 GPIO | 单向半双工 | 吞吐低,不适合高带宽,但成本极低 |
以 SDIO 为例,probe 的骨架大概长这样,每一步的顺序都不建议改:
static int my_probe(struct sdio_func *func, const struct sdio_device_id *id) { int ret; struct my_priv *priv; sdio_claim_host(func); ret = sdio_enable_func(func); if (ret) goto err_release; ret = sdio_set_block_size(func, 512); if (ret) goto err_disable; sdio_release_host(func); /* 1) 时钟、电源、复位 GPIO:先让芯片能响应寄存器读 */ ret = my_hw_power_on(func); if (ret) goto err_disable; /* 2) 下载固件并等待就绪事件 */ ret = my_fw_download(func); if (ret) goto err_power_off; /* 3) 申请中断:必须在硬件 ready 之后,否则可能收不到首次中断 */ ret = my_request_irq(func); if (ret) goto err_power_off; /* 4) 注册到 mac80211:这一句之后 wlan0 就会出现 */ ret = ieee80211_register_hw(priv->hw); if (ret) goto err_free_irq; return 0; /* ... 错误路径逐层回滚 ... */ }4.2 固件加载的时机与方式,差一步症状完全不同
固件加载用request_firmware,文件放在/lib/firmware/下。常见的两种写法区别很大:
- 同步加载:在 probe 里直接
request_firmware。简单,但 probe 会阻塞几百毫秒,启动阶段可能触发超时告警。 - 异步加载:probe 里用
request_firmware_nowait,加载完成后再继续初始化。启动快,但代码复杂度上升,而且必须处理好"加载完成前用户态就试图 up 接口"的竞态。
我踩过的一个坑值得单独说:某次固件下载用了异步方式,结果ieee80211_register_hw在固件回执之前就执行了,返回成功、wlan0也出现了,但第一次扫描永远超时。dmesg里没有任何错误,只是一个孤零零的scan request failed。后来加了事件序列打印才发现是硬件还在复位状态。结论是:注册框架的时机必须严格在硬件就绪之后,顺序不对不会报错,只会表现为各种超时。
4.3 中断、DMA 与收发队列的初始配置
my_start被调用时,硬件才真正开始工作。这里要做三件事:初始化 DMA 描述符环、配置收发队列、把硬件中断使能打开。关于队列,mac80211 会通过ieee80211_wake_queue/ieee80211_stop_queue管理流控,硬件一般提供 4~8 个队列对应不同的 AC(语音、视频、尽力而为、背景)。如果你的硬件队列数少于 mac80211 期望的数量,需要在add_interface里调整vif->hw_queue映射,否则会出现某个优先级的报文全部发不出去。
另一个细节是ieee80211_stop_queue之后的恢复。写驱动时非常容易漏掉"发送完成中断里检查队列是否需要 wake"这一步,症状是跑一段大流量后吞吐突然掉到零,iw dev wlan0 station dump却显示一切正常。这类问题不查队列状态是看不出来的。
5. 监管域、信道表与扫描请求:被内核"拦住"的常见原因
5.1 监管域不是可选项,配错连扫描都发不出去
很多新手第一次看到regulatory_hint和wiphy->regulatory_flags会直接跳过,觉得和自己的硬件初始化没关系。实际上一旦监管域没设置好,cfg80211 会把一部分信道标记为 disabled,扫描请求直接返回-EINVAL,或者扫描结果里看不到任何 AP。
驱动的责任是:把芯片支持的频段和信道如实填进wiphy->bands[NL80211_BAND_2GHZ]和bands[NL80211_BAND_5GHZ],并在拿到固件里携带的国家码信息后调用regulatory_hint。填信道表时几个字段必须给对:
struct ieee80211_channel ch = { .band = NL80211_BAND_2GHZ, .hw_value = 3, .center_freq = 2412, .max_power = 20, /* 单位 dBm,不是 mW */ .flags = IEEE80211_CHAN_NO_IR, /* 某些信道不允许主动发送 */ };max_power的单位错误是最常见的低级失误,写成 dBm 才对,写成 mW 会导致功率超标或者弱到连不上。IEEE80211_CHAN_NO_IR这类标志位漏掉,会让设备在不该主动发包的信道上发探测请求,属于合规问题。
5.2 一次扫描请求在内核里的完整旅程
搞清这条链,排查扫描问题就不会乱:
- 用户态
iw dev wlan0 scan通过 netlink 发出NL80211_CMD_TRIGGER_SCAN。 - cfg80211 检查当前接口状态、监管域、是否有正在进行的扫描。
- 有硬件扫描能力时调用
rdev_scan,最终进到你的hw_scan;没有则走sw_scan_start。 - 你的驱动把信道列表配置给硬件,逐个信道发起探测。
- 硬件把收到的探测响应通过接收路径上报,你在中断里把它们封装成 skb 交给 mac80211。
- mac80211 解析出 BSS 信息,通过 cfg80211 缓存到
scan_request。 - 扫描完成,
NL80211_CMD_NEW_SCAN_RESULTS返回给用户态。
链路上的每一环都可能断。链路上没有硬件扫描能力却实现了hw_scan,会让框架认为你可以扫全信道,结果永远等不到完成事件——这个坑我见过不止一次,正确做法是能力不具备时老老实实只实现sw_scan_start/sw_scan_complete。
5.3 连接建立过程中驱动该做什么
连接流程里,大部分协议细节由 mac80211 处理,驱动要做的是"把状态告诉硬件、把密钥交给硬件"。具体几个点:
bss_info_changed里收到BSS_CHANGED_BSSID时,把 BSSID 写进硬件过滤寄存器,否则硬件的地址过滤会把该 BSS 的帧全丢掉。- 收到
BSS_CHANGED_ASSOC时更新关联状态标志,并在未关联时清空对端站信息。 sta_add/sta_remove对应硬件里加解密表项的增删,漏掉sta_remove会导致硬件表项泄漏,长时间跑下来会出现"新站加不进去"。- 四次握手阶段,用户态的 wpa_supplicant 完成 EAPOL 交换后,把协商出的密钥通过 nl80211 下发,内核侧最终调用你的
set_key。如果有硬件加密卸载,在这里写密钥表;没有的话保持IEEE80211_KEY_FLAG_*标志指示由软件加解密即可。
这里有一个经验:加密相关的 bug 往往表现为"能连上但 ping 不通"或"能 ping 通但传大文件就断"。区别在于单播和组播走的表项不同,如果只配了单播密钥忘了组播密钥,症状就是你 ping 网关(单播)好得很,一旦有广播流量就崩。
6. 问题排查链路:从 dmesg 到 tracepoint 的六步定位法
6.1 先分层看日志,别一上来就翻代码
无线驱动的报错信息经常很含糊,我的习惯是按固定顺序过一遍,绝大多数问题在前三步就能定位:
- 总线层:
lspci -vv、lsusb -t、dmesg | grep -i mmc,确认设备被枚举、资源分配成功。 - 驱动 probe 层:
dmesg里看你自己的打印,确认 probe 是否被调用、返回什么值。 - 框架注册层:
cat /sys/kernel/debug/ieee80211/有没有对应phyN目录,iw dev能不能列出接口。 - 协议层:
iw dev wlan0 scan能不能扫到东西、dmesg里有没有 cfg80211 的告警。 - 数据层:
ip -s link show wlan0、cat /proc/net/dev看重传、错误计数。 - 硬件层:这时候才去读寄存器、看频谱。
跳过前三步直接去查协议,是新手最常见的时间黑洞。
6.2 用 tracepoint 看到函数之间的真实调用顺序
printk打点的问题是它不告诉你"谁调用了谁、耗时多少"。mac80211 和 cfg80211 自带了一批 tracepoint,把它打开能省下大量时间:
cd /sys/kernel/debug/tracing echo 1 > events/mac80211/enable echo 1 > events/cfg80211/enable cat trace_pipe输出里能看到drv_tx、drv_return_int、rdev_scan这类事件,函数名、参数、调用时序一目了然。如果还想看耗时,可以切到 function_graph:
echo function_graph > current_tracer echo 'my_*' > set_ftrace_filter cat trace_pipe这个组合特别适合查"某个回调被调用了几十次"或者"某个耗时操作在中断上下文里做了很久"这类问题。
6.3 我把踩过的坑列成一张对照表
下面这些是我在实际调试中反复遇到的,症状和根因往往不直观:
| 症状 | 可能根因 | 验证方式 |
|---|---|---|
| probe 返回 -ENOENT | 固件文件缺失或路径不对 | dmesg搜 firmware,检查/lib/firmware/ |
| wlan0 不出现 | ieee80211_register_hw失败或未调用 | 在注册前后各加一条打印 |
| 扫描永远超时 | 注册时机早于硬件就绪;信道表为空 | 打印固件事件序列 |
| 能连上但 ping 不通 | 组播密钥未配置;接收过滤把帧丢了 | 查configure_filter的入参位掩码 |
| 大流量下突然静默 | 队列 stop 后没 wake | iw dev wlan0 station dump看队列状态 |
| 休眠唤醒后中断失效 | 中断唤醒源未配置或寄存器未恢复 | cat /proc/interrupts看计数是否增长 |
| 随机初始化失败 | 时钟/电压切换时序不对 | 加大上电延时后复测 |
6.4 一个真实的排查过程
印象最深的一次是某模块在高温环境下连续跑两小时后丢包率飙升。dmesg 干净,iw station dump的 RSSI 正常,看起来毫无头绪。按六步法走到第五步时,/proc/net/dev的tx dropped一直在涨,于是打开events/mac80211的 trace,发现drv_tx被调用的频率远高于硬件实际发出量。
顺着查下去,问题是收发描述符环的大小只有 32,而 mac80211 在高负载时会在一个调度周期里塞进上百个 skb,环满之后驱动返回NETDEV_TX_BUSY,但上层没有正确处理重试,导致报文被静默丢弃。把描述符环扩到 128,并把NETDEV_TX_BUSY的处理改成停队列 + 发送完成后 wake,问题消失。这个案例说明,很多"无线不稳定"的锅最后都落在队列和流控上,而不是射频。
7. 设备树与电源管理:嵌入式平台最容易翻车的两处配置
7.1 设备树里那几行看起来无害的配置
在 ARM 平台上,WiFi 模块能不能稳定工作,一半取决于设备树写得对不对。以 SDIO 接口的模块为例,一个能用的节点大概是这样:
&mmc1 { status = "okay"; bus-width = <4>; non-removable; keep-power-in-suspend; cap-power-off-card; vmmc-supply = <&wifi_vmmc>; vqmmc-supply = <&wifi_vqmmc>; mmc-pwrseq = <&wifi_pwrseq>; }; &wifi_pwrseq { compatible = "mmc-pwrseq-simple"; reset-gpios = <&gpio3 12 GPIO_ACTIVE_LOW>; post-power-on-delay-ms = <200>; };几个关键点:
non-removable必须加。不加会被当成可插拔卡,控制器会周期性重新探测,表现为运行中偶发掉线。cap-power-off-card影响休眠时的断电策略,配上keep-power-in-suspend才能保证唤醒后不断连。post-power-on-delay-ms是给芯片上电稳定留的时间。这个值太小是"冷启动随机失败"的头号原因,加大到 200ms 往往直接解决。vqmmc-supply对应 IO 电压。如果芯片支持 1.8V 高速模式但 regulator 只给了 3.3V,会表现为协商失败或者速率上不去。
7.2 休眠唤醒:中断为什么在 resume 之后就没了
带电池的设备必然要做低功耗。WiFi 这边的核心是两点:把中断配置成唤醒源,以及在suspend/resume回调里完整保存和恢复寄存器状态。
static int my_suspend(struct device *dev) { struct my_priv *priv = dev_get_drvdata(dev); if (!device_may_wakeup(dev)) my_hw_enter_low_power(priv); else enable_irq_wake(priv->irq); return 0; }一个容易忽略的细节是:如果硬件在休眠时断电,唤醒后固件需要重新下载。有些方案为了唤醒快,选择让芯片保持供电只关射频,这就必须在设备树里配keep-power-in-suspend,否则控制器断电后芯片状态全丢,唤醒回来就是一块砖。这个取舍没有标准答案,取决于唤醒延迟要求:要求秒连就用保持供电,要求极致省电就重新初始化。
7.3 唤醒源的验证方法
配置完之后不要只看"能不能唤醒",要确认是 WiFi 中断唤醒的:
cat /sys/power/wakeup_count cat /proc/interrupts | grep my_wifi休眠前后对比中断计数,如果唤醒后计数增长了,说明确实是 WiFi 侧的中断把系统叫醒的;如果计数没变但系统也醒了,那是别的设备唤醒的,WiFi 的唤醒配置其实是失效的,只是被掩盖了。
8. 吞吐与稳定性调优:队列、聚合与中断的三个观察点
8.1 先把收发的瓶颈位置量出来
调优之前不要猜。我一般用这条路子定位瓶颈:
# 打流并观察 iperf3 -c <对端地址> -t 60 -P 4 # 同时看队列与错误计数 watch -n1 'cat /proc/net/dev | grep wlan0' # 看中断分布是否集中在某一个核 watch -n1 'cat /proc/interrupts | grep my_wifi'如果tx dropped持续增长,瓶颈在发送队列或者 DMA 环;如果重传计数高但丢包不多,瓶颈可能在射频环境或者速率自适应;如果只有一个 CPU 核的中断计数在涨、softirq占用很高,那就是中断聚合没做好。三种情况对应的优化方向完全不同。
8.2 聚合参数不是越大越好
AMPDU 聚合和 A-MSDU 聚合是提升吞吐最直接的手段,但参数设错会反过来伤害延迟和小包性能:
| 参数 | 偏大 | 偏小 | 建议 |
|---|---|---|---|
| AMPDU 长度 | 吞吐高,重传代价大,延迟抖动 | 吞吐上不去 | 大文件传输为主时用大值 |
| AMPDU 数量 | 缓冲区压力大,内存吃紧 | 流水线不足 | 结合描述符环大小定 |
| A-MSDU | 头开销小,但单包出错整块重传 | 开销大 | 小包多时谨慎开启 |
| 中断聚合阈值 | CPU 占用低,延迟升高 | 延迟低,CPU 占用高 | 交互场景调小,吞吐场景调大 |
我的经验是先在实验室把参数拉到极端值,分别测吞吐和延迟,画出两条曲线,再根据实际业务决定落点。拍脑袋填一个中间值,往往两头都不讨好。
8.3 长时间跑流的观察点
稳定性问题和性能问题经常混在一起。跑长时间压力测试时,除了吞吐,我固定观察四个量:
- 重传率:
iw dev wlan0 station dump里的tx retries与tx packets比值,持续高于 20% 说明链路质量有问题或者速率自适应没生效。 - 描述符环水位:驱动里打个周期性日志看环的使用峰值,长期贴满就是环太小。
- 中断合并命中率:硬件一般有统计寄存器,命中率低说明聚合阈值设得不合适。
- 内存水位:无线驱动频繁分配 skb,长时间跑下来如果
slabtop里skbuff_head_cache一直不回落,检查是不是有 skb 泄漏,通常出现在错误路径里漏了dev_kfree_skb。
最后分享一个小技巧:调试阶段在驱动的收包路径上加一个可开关的计数器,统计每个队列每小时处理了多少包、丢弃了多少包,比临时加打印有用得多,也不会打乱中断时序。这套计数器后来成了我所有无线驱动模块的标配,很多时候问题都是它先报警的。
至于接下来还能往哪走,我个人的路线是先把一个 fullmac 风格的简单模块跑通,把注册、扫描、连接这条链完全走顺,再去啃 softmac 下的聚合和速率自适应。前者大概一两周能出成果,后者往往要几个月才能调稳,别想着一步到位。