做Linux WiFi设备驱动开发,有点像是在硬件和操作系统中间架一座桥。内核的无线子系统只是把通用的规则定好,真正的WiFi芯片有各自私有寄存器、固件、基带校准表、射频前端,驱动的工作就是把这些差异消化掉,让上层802.11协议栈感觉不到芯片换了。这篇文章写给两类人:一类是嵌入式工程师,产品里用了某个WiFi模组,厂商SDK又老又封闭,想自己动手在内核里“扒”一套干净驱动;另一类是刚入门的Linux内核学习者,对字符驱动已经很熟,想往网络子系统方向再走一步。我会从架构选型、关键机制、设备树配置、最小可复现驱动代码、常见排障这几个角度,把我这几年实际踩过的坑和验证过的方法完整讲一遍。
1. 理解Linux WiFi驱动开发的核心思路:从选型开始
1.1 先搞清楚FullMAC与SoftMAC,再决定怎么写驱动
很多人一上来就翻芯片手册,这是误区。第一步应该想清楚:你的WiFi芯片到底是FullMAC还是SoftMAC。
FullMAC设备,比如很多USB WiFi网卡,MAC层的管理功能(Beacon管理、ACK重传、扫描逻辑、省电调度)全部由芯片固件完成,驱动的主要工作就是配置寄存器、搬运数据和维护固件状态。它的优点是驱动简单,但很多私有逻辑被固件锁死,调试时很难看到内部状态。
SoftMAC设备,比如大量SDIO接口的WiFi模组、部分PCIe网卡,内核的mac80211层负责MAC管理,芯片只提供物理层收发和基础控制。驱动要处理Beacon接收、扫描请求、连接状态切换等事件,代码量明显更多,但灵活性和可控性也更强。
这两者的驱动接口差异很大。FullMAC驱动主要面向cfg80211_ops,需要自己把连接管理、扫描结果等翻译成内核可理解的事件;SoftMAC驱动则面向ieee80211_ops,mac80211会替你处理协议栈的大部分工作,你只需要把硬件能力告诉它,并实现帧收发和硬件配置函数。
我在实际选型时的判断依据很简单:看芯片厂商的开源力度。像RTL8189、ESP8089这类模组,厂商SDK基本是SoftMAC思路;部分高端模组提供FullMAC固件。性质不能搞错。如果你把FullMAC芯片当SoftMAC写,你会发现mac80211需要的各种硬件事件根本报不上来;反过来把SoftMAC芯片当FullMAC简化写,连接会经常异常掉线。
1.2 开发环境的搭建:源码、工具链、目标板三件事
环境不平衡是开发期的最大杀手。我见过太多人卡在内核源码版本不一致上。内核版本必须和目标板上跑的系统一致,最好是同一个git仓库检出的。WiFi驱动比字符驱动更依赖内核内部API,job不同版本之间结构体定义、回调函数签名都可能变,稍微差几个版本就会出现大量编译错误,而这些错误跟你的代码逻辑毫无关系。
工具链优先用板卡厂商或芯片厂商提供的交叉编译器。不要迷信系统自带的gcc。某些WiFi芯片固件加载工具对编译选项敏感,换了工具链后固件校验不通过、DMA对齐异常这些怪问题都会冒出来。交叉编译器版本和内核编译器版本也需要保持同一套,否则模块加载时会出现“Unknown symbol”之类的诡异报错。
目标板准备方面,除了开发板本身,强烈建议再准备:
- 一台可手动指定信道、关闭自动信道选择的路由器或AP;
- 一台可以跑WireShark的电脑,用来抓802.11管理帧;
- 一根质量可靠的USB转串口线,WiFi驱动的串口日志比网口日志可靠太多,因为网口本身可能就是被测设备。
开发板的供电问题也要重视,WiFi射频发射瞬间电流峰值不低,供电不足会导致驱动尝试上报错误的中断状态,表现为随机死机或固件加载失败,这类问题在普通字符驱动开发中几乎不会遇到,所以特别容易忽略。
1.3 设计驱动的“翻译”思路:从硬件手册到内核框架
一旦确定走SoftMAC路线,驱动设计其实就是一个“翻译”过程。芯片手册里描述的能力,要翻译成内核可以识别的能力集合;芯片寄存器操作,要翻译成具体总线读写;芯片中断,要翻译成内核中断回调。
我习惯先画一张“功能映射表”。内核希望知道:支持2.4GHz还是5GHz,支持哪个速率集,最大支持多少个SSID扫描,是否支持AP模式,是否支持硬件加密解密。这些信息从芯片手册的datasheet里就能找到,之后填充到wiphy的属性和硬件能力标志里。
总线类型决定probe函数的写法,这一步也不能选错。PCIe接口的WiFi芯片走标准PCI枚举流程,驱动基于pci_driver结构实现probe;SDIO接口的WiFi芯片属于MMC子系统的一部分,需要先由mmc核心完成SDIO设备枚举,再匹配你的驱动;USB接口则是另一个完全不同的模型。我刚开始做SDIO WiFi时,一直误以为驱动probe失败是驱动本身问题,排查了半天才发现是设备树里中断脚没配置,mmc子系统的设备枚举阶段就卡住了。
这种“翻译”思路的价值在于,它能帮你把问题拆开来看。看到“iw dev wlan0 scan”没结果,先判断问题是在链路层还是在上层——是芯片没收到射频数据,还是驱动没把扫描结果提交给cfg80211。方向对了,排查速度能快好几倍。
2. 驱动与内核的协作机制:你需要掌握的关键细节
2.1 WiFi驱动在内核网络子系统中的位置
要真正写好Linux WiFi驱动,光会写代码不够,得先理解数据从用户空间到射频天线之间经过哪些环节。
用户空间的网络配置工具是wpa_supplicant或iw,它们通过netlink与内核的nl80211模块通信。nl80211向下对接cfg80211核心,cfg80211负责策略控制和状态机管理。如果是SoftMAC架构,cfg80211再往下是mac80211,由它实现802.11协议栈中的大部分MAC功能。驱动中的ieee80211_ops回调,就是mac80211直接调用的“硬件操作接口”。
这里有个新手很容易搞混的地方:驱动不是直接跟wpa_supplicant通信的。也就是说,当你看到wpa_supplicant发起连接时,驱动并不会收到一个“connect”命令,而是收到mac80211下发的一组配置操作,比如设置信道、设置BSSID、启动硬件队列等。实际管理帧的收发由mac80211组织,驱动只需要在正确时机把数据传到正确地方。
理解这个分层还有一个实际好处:排查问题时,每一步都有对应的观测点。用户空间工具报错,先查nl80211;nl80211正常但扫描状态没变化,去查cfg80211;cfg80211状态正常但射频没有实际动作,问题就出在驱动和固件。逐层切分,比漫无目的地加printk高效得多。
2.2 关键数据结构与注册流程:顺序不能乱
先写一个最小框架时,最重要的三个元素是:ieee80211_hw、ieee80211_ops、私有数据。ieee80211_hw是内核视角下的一块“虚拟WiFi硬件”,是mac80211与驱动沟通的核心对象。它本身不存业务数据,真正存储使用的是hw->priv,这段内存由驱动申请,大小在ieee80211_alloc_hw时指定。
注册顺序上,我的建议永远是一个固定模板:
- 调用ieee80211_alloc_hw分配硬件对象和私有数据内存;
- 填充hw->priv里的业务字段;
- 设置hw->flags、hw->wiphy等能力信息;
- 通过SET_IEEE80211_DEV将硬件对象与struct device关联;
- 设置永久MAC地址;
- 最后调用ieee80211_register_hw把这块硬件真正注册到内核无线子系统。
第4步特别容易被忽略。没有正确关联struct device,后续某些事件上报、电源管理等路径会找不到设备上下文,出现空指针或警告。第6步一旦执行,内核就开始向用户空间暴露无线设备,之后的某些配置就不能再随意改了,否则会出现运行时状态和你预期不一致的诡异结果。
操作函数的实现上,SoftMAC驱动至少要实现start、stop、config、add_interface、remove_interface、tx这几个回调。config负责信道、功率等基础参数配置;add_interface和remove_interface处理接口创建和销毁;tx负责把mac80211交下来的数据帧真正送进芯片。很多驱动还会实现bss_info_changed回调,用来接收连接状态变化,这个回调里做固件设置,通常能覆盖80%的连接问题。
2.3 设备树与WiFi设备的匹配机制
如果你的WiFi芯片走SDIO或部分MMC接口,设备树配置正确与否会直接决定驱动能不能被加载。
一个典型的SDIO WiFi设备树节点长这样:
&mmc1 { status = "okay"; vmmc-supply = <&wifi_pwr_reg>; bus-width = <4>; non-removable; wifi@1 { compatible = "vendor,sdio-wifi"; reg = <1>; interrupts-extended = <&gpio 37 IRQ_TYPE_LEVEL_LOW>; reset-gpios = <&gpio 38 GPIO_ACTIVE_LOW>; }; };SDIO设备的地址不像I2C那样静态分配,而是在枚举阶段动态分配,所以reg的值可能因控制器而异。很多人的误区是照抄别人设备树里的reg,换了主控之后设备完全找不到。建议先用mmc子系统提供的调试手段查看实际枚举结果,再回头确认reg值对不对。
interrupts-extended直接关联SDIO设备的中断引脚,这个必须和硬件实际接线一致。如果SDIO clk和cmd能枚举成功但没有中断,WiFi能识别但无法上报事件,表现为扫描卡住、连接无响应。reset-gpios则控制芯片复位时序,有些芯片上电后需要先拉低复位引脚、再拉高并延迟一段时间,这个时序如果不对,固件加载会失败或加载后芯片不响应。
设备树只是“匹配机制”,不是“配置全部”。也就是说,设备树告诉内核“这里有个WiFi芯片,用这个驱动”,但芯片内部的射频校准参数、固件路径、天线配置等,通常还是要靠驱动代码和固件文件协同完成。设备树写错,驱动根本没机会跑;设备树正确但固件版本不对,问题又会表现为另一个样。排查时把这两个阶段分开看,能少走很多弯路。
3. 从零开始写一个可复现的WiFi驱动:实操过程记录
3.1 最小驱动代码骨架:从注册到注销
这里给出一个基于SDIO接口的SoftMAC驱动骨架,略去了硬件寄存器读写细节,重点展示驱动与mac80211的衔接逻辑。实际产品代码中,probe函数里还要做固件加载、私有数据初始化、中断申请、DMA缓冲区初始化等,但下面这个骨架是理解原理的最小可执行框架。
#include <linux/module.h> #include <linux/kernel.h> #include <linux/mmc/sdio_func.h> #include <linux/mmc/sdio_ids.h> #include <net/mac80211.h> #include <net/cfg80211.h> struct my_wifi_priv { struct ieee80211_hw *hw; struct sdio_func *func; u8 mac_addr[ETH_ALEN]; /* 其他业务字段,例如固件状态、射频校准参数 */ }; static int my_wifi_start(struct ieee80211_hw *hw) { struct my_wifi_priv *priv = hw->priv; /* 启动硬件:上电、加载固件、启用中断 */ dev_info(&priv->func->dev, "wifi hardware started\n"); return 0; } static void my_wifi_stop(struct ieee80211_hw *hw) { struct my_wifi_priv *priv = hw->priv; /* 停止硬件:关闭中断、进入低功耗模式 */ dev_info(&priv->func->dev, "wifi hardware stopped\n"); } static int my_wifi_config(struct ieee80211_hw *hw, u32 changed) { struct my_wifi_priv *priv = hw->priv; /* mac80211要求驱动调整信道、带宽、功率时触发 */ if (changed & IEEE80211_CONF_CHANGE_CHANNEL) { /* 读取 hw->conf.chandef 并写入芯片寄存器 */ } return 0; } static int my_wifi_add_interface(struct ieee80211_hw *hw, struct ieee80211_vif *vif) { switch (vif->type) { case NL80211_IFTYPE_STATION: /* 无线客户端模式,最常用 */ break; case NL80211_IFTYPE_AP: /* 软AP模式,需要额外配置Beacon等 */ break; default: return -EOPNOTSUPP; } return 0; } static void my_wifi_remove_interface(struct ieee80211_hw *hw, struct ieee80211_vif *vif) { /* 释放接口对应的硬件资源 */ } static void my_wifi_tx(struct ieee80211_hw *hw, struct ieee80211_tx_control *control, struct sk_buff *skb) { struct my_wifi_priv *priv = hw->priv; /* 把skb中的数据搬到芯片发送FIFO,或者构造成DMA描述符 */ /* 发送完成后必须调用 ieee80211_tx_status(hw, skb) 或者 dev_kfree_skb_any(skb) */ dev_kfree_skb_any(skb); } static const struct ieee80211_ops my_wifi_ops = { .start = my_wifi_start, .stop = my_wifi_stop, .config = my_wifi_config, .add_interface = my_wifi_add_interface, .remove_interface = my_wifi_remove_interface, .tx = my_wifi_tx, }; static int my_wifi_probe(struct sdio_func *func, const struct sdio_device_id *id) { struct ieee80211_hw *hw; struct my_wifi_priv *priv; int ret; /* 第一步:分配硬件对象和私有数据 */ hw = ieee80211_alloc_hw(sizeof(struct my_wifi_priv), &my_wifi_ops); if (!hw) return -ENOMEM; priv = hw->priv; priv->hw = hw; priv->func = func; priv->mac_addr[0] = 0x00; priv->mac_addr[1] = 0x11; priv->mac_addr[2] = 0x22; priv->mac_addr[3] = 0x33; priv->mac_addr[4] = 0x44; priv->mac_addr[5] = 0x55; /* 第二步:设置硬件能力 */ hw->flags |= IEEE80211_HW_SIGNAL_UNSPEC; hw->wiphy->interface_modes = BIT(NL80211_IFTYPE_STATION) | BIT(NL80211_IFTYPE_AP); /* 第三步:关联设备并设置地址 */ SET_IEEE80211_DEV(hw, &func->dev); SET_IEEE80211_PERM_ADDR(hw, priv->mac_addr); /* 第四步:注册硬件 */ ret = ieee80211_register_hw(hw); if (ret) goto err_free_hw; sdio_set_drvdata(func, hw); return 0; err_free_hw: ieee80211_free_hw(hw); return ret; } static void my_wifi_remove(struct sdio_func *func) { struct ieee80211_hw *hw = sdio_get_drvdata(func); ieee80211_unregister_hw(hw); ieee80211_free_hw(hw); } static const struct sdio_device_id my_wifi_sdio_ids[] = { { SDIO_DEVICE(0x6666, 0x0001) }, {} }; MODULE_DEVICE_TABLE(sdio, my_wifi_sdio_ids); static struct sdio_driver my_wifi_driver = { .name = "my_wifi", .probe = my_wifi_probe, .remove = my_wifi_remove, .id_table = my_wifi_sdio_ids, }; module_sdio_driver(my_wifi_driver); MODULE_LICENSE("GPL"); MODULE_DESCRIPTION("Minimal Linux WiFi driver skeleton");注意SDIO设备ID里的厂商号和产品号,必须填成目标芯片实际响应的值。很多SDIO WiFi芯片在枚举阶段先用厂商号0x6666这种测试值占位,等固件加载后才切换成真正的厂商号。这种情况下驱动匹配时就得额外处理,否则probe虽然能跑,但后续的访问会失败。
驱动注销流程的顺序也有讲究。ieee80211_unregister_hw会把设备从系统中移除,之后再释放硬件对象,这个顺序如果反过来,内核会访问已经被释放的内存。SDIO驱动的remove逻辑还需要把中断释放干净,防止卸载后中断回调还在执行。
3.2 设备树配置的关键点:不仅仅是把节点写对
设备树节点写完之后,光看不报错不代表工作正常。我做过一批板子,启动日志里SDIO设备能枚举,但驱动的probe始终不执行,检查发现设备树里的status字段写成了"disabled"。这类低级错误最浪费时间,建议统一流程:先看设备是否被内核正确枚举,再看驱动是否匹配,最后才怀疑probe里的代码逻辑。
WiFi设备树有一个容易被忽视的点:电源域。很多WiFi芯片需要独立的数字电源和模拟电源,至少需要一路带负载能力的稳压器给射频PA供电。设备树里的vmmc-supply或者单独给芯片供电的regulator,如果在某个启动阶段没有稳定建立,芯片可能只完成部分初始化,表现为能识别但无法发射信号。
我习惯在设备树里也加上pd-capable、sdio-wifi这类与驱动无关、但便于识别是WiFi节点的属性名。真正做配置时,还会加一个wakeup-source,表示WiFi芯片在系统休眠时可以作为唤醒源。这些属性不影响SDIO枚举,但会影响后续电源管理逻辑。
&mmc2 { status = "okay"; bus-width = <4>; non-removable; cap-sdio-irq; keep-power-in-suspend; wifi@1 { compatible = "vendor,softmac-wifi"; reg = <1>; interrupts-extended = <&gpio 29 IRQ_TYPE_LEVEL_LOW>; reset-gpios = <&gpio 30 GPIO_ACTIVE_LOW>; wakeup-source; }; };cap-sdio-irq表示SDIO控制器支持SDIO中断模式,这个属性对大多数SDIO WiFi来说是必须的,没有它驱动申请的中断请求可能无法正常触发。keep-power-in-suspend则用于休眠场景,如果WiFi芯片需要始终保持供电,这个属性不能省。reset-gpios对应芯片的复位引脚,某些参考设计中由PMIC控制复位,这种情况下就不需要在设备树里写了。
3.3 编译、加载与验证的全流程:从insmod到ping通
代码和设备树准备齐全后,编译是第一个关卡。WiFi驱动通常编译成内核模块,Makefile里需要告诉kbuild这个模块由哪些源文件组成:
obj-m += my_wifi.o my_wifi-objs := src/main.o src/fw.o src/rx.o src/tx.o KERNEL_DIR ?= /lib/modules/$(shell uname -r)/build PWD := $(shell pwd) all: $(MAKE) -C $(KERNEL_DIR) M=$(PWD) modules clean: $(MAKE) -C $(KERNEL_DIR) M=$(PWD) clean交叉编译时,把KERNEL_DIR指向目标板对应内核源码目录,再用ARCH和CROSS_COMPILE参数指定目标架构和工具链,注意模块编译用到的头文件必须与目标板上运行的内核完全一致。
加载模块之后,验证分四步走:
# 第一步:确认驱动绑定了设备 dmesg | grep my_wifi # 第二步:确认无线设备已经注册到内核 iw dev # 第三步:查看硬件能力是否被正确识别 iw phy phy0 info # 第四步:用wpa_supplicant发起连接 wpa_supplicant -D nl80211 -i wlan0 -c /etc/wpa_supplicant.conf -B dhclient wlan0如果第四步能拿到IP并且ping通,说明驱动的基本链路是通的。这一步通不过的话,回到第2.1节讲的分层模型,逐层检查。有一个我经常用的小技巧:先把wpa_supplicant日志开到最大调试级别:
wpa_supplicant -D nl80211 -i wlan0 -c /etc/wpa_supplicant.conf -dd这样可以看到底层事件上报的细节,比如驱动有没有正确上报Beacon信息、认证和关联响应是哪个阶段失败的。日志里的关键字比任何猜测都直观。
4. 开发中常见的坑与排查技巧
4.1 常见问题速查表
以下是我在多个WiFi驱动项目中遇到的高频问题。与其反复试错,不如先整理成表,逐个对照。
| 现象 | 可能原因 | 处理办法 |
|---|---|---|
| probe不执行 | 设备树status字段错误、SDIO ID不匹配 | 检查设备树节点、用dmesg查看mmc枚举日志 |
| 模块加载成功但iw dev无设备 | ieee80211_register_hw失败 | 检查wiphy注册返回值、确认固件已加载 |
| 扫描无结果 | 天线未连接、信道被锁定、扫描能力未声明 | 检查天线馈线、用iw set freq手动指定信道测试 |
| 无法连接或频繁掉线 | 电源管理导致射频休眠、MAC地址冲突 | 关闭PS模式、设置不同MAC地址再测 |
| AP模式启动失败 | interface_modes未声明AP、Bridge需要4addr支持 | 检查wiphy接口模式、配置4addr |
| 数据速率极低 | DMA描述符配置错误、SDIO时钟过低 | 检查总线时钟、DMA对齐和缓冲区分配 |
| 固件加载失败 | 固件路径错误、固件版本不匹配 | 检查/lib/firmware目录、校验固件校验和 |
| 系统重启后WiFi失效 | 设备树复位时序错误、电源域未保持 | 检查复位GPIO时序、电源管理状态 |
有一种很隐蔽的情况是扫描偶尔成功、偶尔失败。这种问题多半是中断竞争或者DMA缓冲没有及时回收,属于随机性问题,比稳定失败难排查得多。我会先用stress工具做单线程高频率扫描,看是否复现,再逐步关闭中断和DMA的并发路径,定位到具体函数。
4.2 内核调试开关:把mac80211的内部日志打开
WiFi驱动调试的终极大杀器,是内核自带的mac80211和cfg80211调试开关。配置内核时打开CONFIG_MAC80211_DEBUGS、CONFIG_CFG80211_DEBUGFS、CONFIG_MAC80211_DEBUGFS,重新编译烧写后,会在/sys/kernel/debugfs/ieee80211/目录下看到明细项目。
必要的时候,可以用ftrace跟踪关键函数:
# 挂载tracefs mount -t tracefs nodev /sys/kernel/tracing # 查看可跟踪的mac80211函数 cat /sys/kernel/tracing/available_filter_functions | grep mac80211 # 开启跟踪 echo function_graph > /sys/kernel/tracing/current_tracer echo 'ieee80211_*' > /sys/kernel/tracing/set_ftrace_filter cat /sys/kernel/tracing/trace通过ftrace能看到mac80211在事件处理流程中调用驱动的哪个函数,也能看到驱动回调返回后的状态变化。当驱动本身已经能工作,但性能和稳定性有问题时,这种主动追踪比被动插打印要有用得多。
还有一个容易忽略的点:iw工具的版本。新版iw使用新的nl80211命令集,老内核可能不支持部分命令,表现是“iw dev wlan0 scan”返回不支持。这常常被误判为驱动问题。我建议在目标板上单独交叉编译一版与内核匹配的iw,不要图省事直接拷贝开发机的可执行文件。
5. 一些值得长期坚持的工程经验
5.1 把“先复现、后修复”当成铁律
WiFi驱动问题常常有很强的环境依赖,同样一台板子,换个信道、换个AP、换个电源适配器,问题可能就消失了。这种条件下,如果一上来就改代码,很容易改错方向。我养成的习惯是:任何Bug都先记录完整复现环境,包括AP型号、信道、距离、供电方式、内核版本、驱动commit号、复现概率,然后尝试在同一环境里重复触发。能在10分钟内稳定复现一次的问题,往往1小时之内就能找到根因;那些“偶尔出现”的问题,大多数都是电源、DMA缓冲或者并发竞争方面的根源。
5.2 从小到大迭代:不要让第一次就追求完美
新手最容易犯的错,是想一次写出一个包含所有功能点的完整WiFi驱动。这个领域和字符设备驱动不一样,协议状态机多、并发路径多、硬件时序严格,一步到位几乎不可能。我建议把一个完整目标拆成五个里程碑:
- 模块能加载并且probe成功;
- iw phy能看到设备、iw dev能看到接口;
- 能扫描到AP,信号强度绝对值是否正确暂时不重要;
- 用wpa_supplicant能连上开放网络或测试AP;
- 连上加密网络、跑通iperf吞吐测试。
每个里程碑验证完成后再进入下一个。前面任何一步失败,都不要往后走。这套顺序帮我避免过很多“链路全通但细节全错”的困境。
5.3 数据缓冲和并发访问要提前设计
驱动开发早期,硬件通信还没理顺,数据收发路径和中断上下文里的并发问题往往被掩盖。等到吞吐量一上来,突然出现随机丢包、oops、内核panic,再回头改缓冲设计就非常痛苦。我现在写驱动时,会把接收DMA缓冲区、发送队列锁、中断标志位这些内容当作框架的一部分先起草,而不是等基础功能通了再补。提前设计并发的坑,比事后排查节省的时间至少是十倍。
6. 后续还能往哪些方向深入
驱动跑通只是一个开始。实际产品里,WiFi驱动往往还牵扯到电源管理、射频校准、天线匹配、吞吐量调优、蓝牙共存等一整套问题。电源管理一项就够研究很久,比如WoWLAN(Wake on Wireless LAN)可以让系统在睡眠状态下通过WiFi唤醒,这要求驱动在休眠路径里正确保存固件状态、在唤醒路径里恢复射频上下文。蓝牙与WiFi共用天线的模组,驱动力还要处理PTA(Packet Traffic Arbitration)协同,否则蓝牙耳机和WiFi同时使用时会互相干扰。
如果目标是往无线子系统深入,建议多读mac80211源码里的几个核心文件,比如tx.c、rx.c、mlme.c,它们对应着WiFi驱动最常碰触的三条主线。读的时候不要从头到尾,而是挑一个具体的操作流程,比如“站点连接AP时内核如何处理关联请求”,顺着一条调用链走一遍,理解会比零散看代码深刻得多。这种把源码当“工具书”而不是“教科书”的方式,是我这几年学习内核驱动最推荐的方法。