Linux网络驱动开发从入门到DMA数据收发 - 第1节

1.1 Linux 内核架构概览

Linux 内核是一个宏内核(Monolithic Kernel),但吸收了微内核的模块化设计思想。理解内核架构是驱动开发的基石,它决定了驱动程序如何与内核其他部分交互、如何管理内存、如何调度任务以及如何访问硬件。

核心要点: 驱动开发者最常打交道的三个子系统是进程管理(系统调用、调度)、内存管理(页分配、DMA 缓冲区)和设备驱动模型(总线、设备、驱动绑定)。

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),可以直接访问物理内存和硬件寄存器。用户空间程序通过系统调用陷入内核,由内核代表用户执行特权操作。这种隔离机制保障了系统安全,但也意味着驱动中的任何错误(空指针、越界访问)都可能导致整个系统崩溃。

Linux 内核架构分层与驱动位置 用户空间 (User Space) 应用程序 / C 库 / 系统调用接口 系统调用层 (syscall) 内核空间 (Kernel Space) 进程管理 内存管理 VFS 网络协议栈 设备驱动模型 总线 / 设备 / 驱动 / 类 硬件抽象层 (HAL) 中断控制器 / DMA 引擎 / 时钟 / GPIO 硬件层 (Hardware) 以太网 PHY / MAC / DMA 控制器 / 物理内存 ⬅ 网络驱动驻留层
开发启示: 网络驱动主要工作在“设备驱动模型”和“硬件抽象层”。驱动通过 struct net_device 向上注册到网络协议栈,通过 struct platform_device 或 PCI 接口向下绑定硬件。理解这个分层位置,有助于在调试时快速定位问题属于协议栈层还是硬件适配层。

1.2 内核模块概念

Linux 内核模块(Kernel Module)是一种可以在运行时动态加载到内核或从内核卸载的代码片段。它允许在不重新编译整个内核、不重启系统的情况下扩展内核功能。绝大多数设备驱动都是以模块形式发布的。

1.2.1 模块的生命周期

一个内核模块从编写到运行经历以下阶段:

  1. 编写:使用 C 语言编写,包含模块入口/出口函数。
  2. 编译:借助内核源码树中的 Makefile 和 Kbuild 系统编译为 .ko 文件。
  3. 加载:通过 insmod 或 modprobe 将模块插入内核空间。
  4. 初始化:内核调用模块的 init_module 函数(或宏 module_init 指定的函数)。
  5. 运行:模块注册设备、创建文件节点、注册中断等,开始提供服务。
  6. 卸载:通过 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");
内核模块生命周期与加载流程 编写 .c 源码 编译为 .ko insmod 加载 module_init 模块运行中 (Scheduled) rmmod 卸载

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 中获取源码。

准备内核源码树步骤:

  1. 下载内核源码:wget https://cdn.kernel.org/pub/linux/kernel/v6.x/linux-6.6.tar.xz
  2. 解压:tar -xf linux-6.6.tar.xz
  3. 配置(至少执行一次 defconfig):make ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf- defconfig
  4. 编译内核(可选,但建议编译一次以生成 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 兼容
交叉编译环境架构 宿主机 (x86_64) 交叉编译链 (arm-linux-gnueabihf-gcc) 内核源码树 (Linux 6.6) 输出: hello_netdev.ko scp / nfs / usb 目标板 (ARM Cortex-A) Linux 内核 (与源码树同版本) insmod hello_netdev.ko dmesg 查看打印输出
效率技巧: 在宿主机上搭建 NFS 或 TFTP 服务,将编译好的 .ko 文件直接挂载到目标板文件系统,可以免去反复拷贝的步骤。配合 make -C $(KDIR) M=$(PWD) modules 的增量编译,每次修改后只需几秒即可在目标板上加载测试。