Linux网络驱动开发从入门到DMA数据收发 - 第1节
1.1 Linux 内核架构概览
Linux 内核是一个宏内核(Monolithic Kernel),但吸收了微内核的模块化设计思想。理解内核架构是驱动开发的基石,它决定了驱动程序如何与内核其他部分交互、如何管理内存、如何调度任务以及如何访问硬件。
1.1.1 内核子系统分层
内核按功能划分为若干子系统,各子系统之间通过函数调用和数据结构进行通信。下表列出了与网络驱动开发最相关的核心子系统及其职责:
| 子系统 | 核心职责 | 驱动开发者关注点 |
|---|---|---|
| 进程管理 | 创建/销毁进程、调度、信号处理 | 中断上下文、工作队列、等待队列 |
| 内存管理 | 虚拟地址映射、物理页分配、SLAB 分配器 | kmalloc、dma_alloc_coherent、IOMMU 映射 |
| 虚拟文件系统 (VFS) | 统一文件操作接口(open/read/write/ioctl) | file_operations 结构体、字符设备注册 |
| 网络协议栈 | 套接字、TCP/IP、Netfilter 过滤 | sk_buff 操作、NAPI 轮询、网络设备注册 |
| 设备驱动模型 | 总线、设备、驱动、类之间的标准绑定 | platform_driver、device tree 匹配、DMA 引擎 |
| 中断管理 | IRQ 注册、中断上下半部、中断亲和性 | request_irq、tasklet、 threaded IRQ |
1.1.2 内核空间与用户空间
驱动运行在内核空间,拥有最高特权级(Ring 0),可以直接访问物理内存和硬件寄存器。用户空间程序通过系统调用陷入内核,由内核代表用户执行特权操作。这种隔离机制保障了系统安全,但也意味着驱动中的任何错误(空指针、越界访问)都可能导致整个系统崩溃。
struct net_device 向上注册到网络协议栈,通过 struct platform_device 或 PCI 接口向下绑定硬件。理解这个分层位置,有助于在调试时快速定位问题属于协议栈层还是硬件适配层。
1.2 内核模块概念
Linux 内核模块(Kernel Module)是一种可以在运行时动态加载到内核或从内核卸载的代码片段。它允许在不重新编译整个内核、不重启系统的情况下扩展内核功能。绝大多数设备驱动都是以模块形式发布的。
1.2.1 模块的生命周期
一个内核模块从编写到运行经历以下阶段:
- 编写:使用 C 语言编写,包含模块入口/出口函数。
- 编译:借助内核源码树中的 Makefile 和 Kbuild 系统编译为
.ko文件。 - 加载:通过
insmod或modprobe将模块插入内核空间。 - 初始化:内核调用模块的
init_module函数(或宏module_init指定的函数)。 - 运行:模块注册设备、创建文件节点、注册中断等,开始提供服务。
- 卸载:通过
rmmod卸载,内核调用cleanup_module函数释放资源。
1.2.2 最小内核模块示例
以下代码展示了一个最简内核模块的结构,它不操作任何硬件,仅用于验证编译环境和加载流程:
// hello_netdev.c — 最小网络驱动骨架模块
#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/netdevice.h>
static int __init hello_netdev_init(void)
{
printk(KERN_INFO "Hello NetDev: 模块加载成功\n");
/* 在这里可以注册 net_device、申请 DMA 缓冲区等 */
return 0;
}
static void __exit hello_netdev_exit(void)
{
printk(KERN_INFO "Hello NetDev: 模块卸载成功\n");
/* 在这里释放 net_device、归还 DMA 缓冲区等 */
}
module_init(hello_netdev_init);
module_exit(hello_netdev_exit);
MODULE_LICENSE("GPL");
MODULE_AUTHOR("Linux NetDev Course");
MODULE_DESCRIPTION("最小网络驱动骨架模块 — 第1章示例");
register_netdev 注册的网络设备,导致卸载后 /proc/net/dev 中仍残留僵尸设备。务必在 cleanup_module 中调用 unregister_netdev 和 free_netdev。
1.2.3 模块参数与符号导出
模块可以通过 module_param 宏接收加载时传入的参数,方便调试时动态调整行为。驱动中的关键函数可以通过 EXPORT_SYMBOL 导出,供其他模块或内核核心调用。
| 宏 / 函数 | 用途 | 示例 |
|---|---|---|
module_param(name, type, perm) |
声明模块加载参数 | module_param(debug, int, 0644); |
EXPORT_SYMBOL(func) |
导出符号供其他模块使用 | EXPORT_SYMBOL(my_netdev_alloc); |
MODULE_DESCRIPTION() |
模块描述信息 | MODULE_DESCRIPTION("Gigabit Ethernet Driver"); |
MODULE_LICENSE() |
声明许可证(影响符号可用性) | MODULE_LICENSE("GPL"); |
1.3 驱动开发环境搭建
嵌入式 Linux 驱动开发通常采用交叉编译模式:在性能强大的宿主机(x86)上编写和编译代码,生成的目标文件在目标板(ARM/RISC-V)上运行。搭建正确的开发环境是避免“编译通过、运行崩溃”的第一步。
1.3.1 交叉编译链
交叉编译链(Cross Toolchain)包含针对目标架构的编译器、链接器、库和头文件。对于网络驱动开发,最常用的工具链是 arm-linux-gnueabihf- 系列(ARM 32位)或 aarch64-linux-gnu- 系列(ARM 64位)。
安装示例(Ubuntu 22.04):
# 安装 ARM 32位交叉编译链
sudo apt-get install gcc-arm-linux-gnueabihf
# 安装 ARM 64位交叉编译链
sudo apt-get install gcc-aarch64-linux-gnu
# 验证安装
arm-linux-gnueabihf-gcc --version
aarch64-linux-gnu-gcc --version
1.3.2 内核源码树
编译内核模块必须依赖完整的内核源码树(Kernel Source Tree),仅安装 linux-headers 包是不够的——驱动开发需要访问头文件、Kbuild 系统和架构相关代码。推荐从内核官网或芯片厂商 BSP 中获取源码。
准备内核源码树步骤:
- 下载内核源码:
wget https://cdn.kernel.org/pub/linux/kernel/v6.x/linux-6.6.tar.xz - 解压:
tar -xf linux-6.6.tar.xz - 配置(至少执行一次
defconfig):make ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf- defconfig - 编译内核(可选,但建议编译一次以生成
Module.symvers):make -j4 ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf- zImage
Makefile 是整个构建系统的入口;scripts/ 目录包含 Kbuild 辅助脚本;include/linux/ 包含驱动开发最常引用的头文件(如 netdevice.h、skbuff.h、dma-mapping.h)。
1.3.3 模块编译 Makefile 模板
以下 Makefile 适用于编译上述 hello_netdev.c 模块,支持宿主机本地编译和交叉编译两种模式:
# Makefile — 网络驱动模块编译模板
# 使用方式:
# 本地编译: make
# 交叉编译(ARM): make ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf-
# 内核源码树路径(根据实际环境修改)
KDIR ?= /lib/modules/$(shell uname -r)/build
# 交叉编译时请覆盖 KDIR,例如:
# make ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf- KDIR=/path/to/linux-6.6
obj-m := hello_netdev.o
all:
$(MAKE) -C $(KDIR) M=$(PWD) modules
clean:
$(MAKE) -C $(KDIR) M=$(PWD) clean
install:
$(MAKE) -C $(KDIR) M=$(PWD) modules_install
1.3.4 环境验证清单
在开始编写网络驱动之前,请逐项确认以下环境要素:
| 检查项 | 验证命令 / 方法 | 预期结果 |
|---|---|---|
| 交叉编译器可用 | arm-linux-gnueabihf-gcc --version |
显示版本号,无“command not found” |
| 内核源码树存在 | ls $(KDIR)/include/linux/netdevice.h |
文件存在且可读 |
| Kbuild 配置已生成 | ls $(KDIR)/.config |
配置文件存在(非空) |
| 模块编译测试 | make ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf- |
生成 hello_netdev.ko,无报错 |
| 目标板内核版本匹配 | uname -r(在目标板上执行) |
与源码树版本一致或 vermagic 兼容 |
.ko 文件直接挂载到目标板文件系统,可以免去反复拷贝的步骤。配合 make -C $(KDIR) M=$(PWD) modules 的增量编译,每次修改后只需几秒即可在目标板上加载测试。