2. Linux内核基础
做Linux驱动开发,第一步不是写驱动,而是理解驱动所依附的“宿主”——Linux内核。内核为驱动提供了设备模型、内存管理、并发控制、中断框架和总线抽象,驱动工程师只有先看清内核的整体架构与构建方式,才能判断自己的代码应该放在哪里、以什么形式加载、依赖哪些配置项。本节围绕内核架构、模块机制、编译系统、源码目录与配置裁剪五个方面展开,形成一条从“认识内核”到“构建内核”的完整链路。
一、内核架构:分层与子系统
Linux内核是宏内核(Monolithic Kernel),但通过模块化和子系统划分获得了接近微内核的灵活性。从驱动视角看,内核可理解为四层结构:最上层是系统调用接口,向用户空间暴露open/read/write/ioctl等入口;其下是VFS、网络协议栈、内存管理、进程调度等核心子系统;再往下是设备模型与总线抽象(platform、I2C、SPI、PCI、USB等);最底层是具体硬件驱动与体系结构相关代码(arch/)。
驱动工程师主要工作在“设备模型+总线+具体驱动”这一层。内核通过struct device、struct device_driver、struct bus_type三者建立匹配关系,驱动注册后由总线负责probe。理解这一点,就能明白为什么驱动代码里几乎看不到“主动调用设备”,而是“注册并等待匹配”。
二、模块机制:驱动的加载与卸载
内核模块(.ko)是驱动最常见的存在形式。它允许驱动在运行时动态插入内核,无需重新编译整个内核。模块机制的核心是module_init()与module_exit()两个宏,分别指定加载和卸载时执行的函数。
#include <linux/module.h>
#include <linux/init.h>
static int __init mydrv_init(void)
{
pr_info("mydrv: loaded\n");
return 0;
}
static void __exit mydrv_exit(void)
{
pr_info("mydrv: unloaded\n");
}
module_init(mydrv_init);
module_exit(mydrv_exit);
MODULE_LICENSE("GPL");
MODULE_AUTHOR("driver-dev");
MODULE_DESCRIPTION("Demo driver module");
常用操作命令如下:
insmod mydrv.ko # 加载模块
lsmod # 查看已加载模块
modinfo mydrv.ko # 查看模块信息
rmmod mydrv # 卸载模块
dmesg | tail # 查看内核日志
MODULE_LICENSE("GPL"),否则内核会标记为“污染”,部分GPL-only符号将无法使用,这是驱动开发中最常见的低级错误之一。三、编译系统:Kbuild与Makefile
Linux内核使用Kbuild编译系统,驱动既可以编译进内核(built-in),也可以编译为模块(.ko)。外部驱动通常通过make -C $(KDIR) M=$(PWD) modules的方式调用内核构建系统。
# 外部模块 Makefile 示例
obj-m += mydrv.o
mydrv-objs := main.o irq.o
KDIR := /lib/modules/$(shell uname -r)/build
PWD := $(shell pwd)
all:
$(MAKE) -C $(KDIR) M=$(PWD) modules
clean:
$(MAKE) -C $(KDIR) M=$(PWD) clean
Kbuild的关键变量包括:obj-m表示编译为模块,obj-y表示编译进内核,mydrv-objs用于将多个源文件链接为一个模块。交叉编译时需指定ARCH和CROSS_COMPILE。
四、源码目录与配置裁剪
内核源码目录结构是驱动工程师的“地图”。下表列出主要目录及其职责:
| 目录 | 职责 | 驱动相关度 |
|---|---|---|
| arch/ | 体系结构相关代码,如arm、arm64、x86 | 高(启动、中断、时钟) |
| drivers/ | 各类设备驱动,按总线/功能分类 | 极高(主战场) |
| include/ | 内核头文件,含linux/、asm/等 | 高(接口定义) |
| kernel/ | 进程调度、中断、时间、同步等核心 | 中(依赖其API) |
| mm/ | 内存管理,页分配、slab、vmalloc | 中(内存接口) |
| fs/ | 文件系统与VFS | 中(字符设备接口) |
| net/ | 网络协议栈 | 中(网络驱动) |
| scripts/ | Kbuild脚本、配置工具 | 低(构建辅助) |
配置裁剪通过make menuconfig完成,生成.config文件。每个配置项对应一个CONFIG_XXX宏,驱动代码中常用#ifdef CONFIG_XXX做条件编译。裁剪的目标是:去掉不需要的子系统与驱动,减小内核体积、缩短启动时间、降低攻击面。
make ARCH=arm64 CROSS_COMPILE=aarch64-linux-gnu- defconfig
make ARCH=arm64 CROSS_COMPILE=aarch64-linux-gnu- menuconfig
make ARCH=arm64 CROSS_COMPILE=aarch64-linux-gnu- -j8 Image modules dtbs
CONFIG_MODULES,则所有.ko都无法加载;若误关驱动依赖的总线或子系统(如I2C、GPIO),对应驱动即使编译成功也无法probe。裁剪后务必用dmesg确认关键子系统初始化正常。五、案例与复盘:一个平台驱动的落地路径
假设要为一块ARM64开发板写一个GPIO按键驱动,完整路径如下:
- 确认内核版本与源码路径,执行
make defconfig并开启CONFIG_MODULES、CONFIG_GPIOLIB、CONFIG_INPUT。 - 在
drivers/input/keyboard/下新建驱动文件,使用platform_driver注册,通过设备树匹配compatible。 - 编写Makefile与Kconfig,将驱动加入Kbuild体系。
- 编译模块,
insmod加载,用evtest验证按键事件。 - 若probe未触发,依次检查:设备树节点是否存在、compatible是否匹配、GPIO是否被占用、时钟/中断是否配置。
常见误区有三:一是把驱动写成“主动轮询设备”,忽略内核的设备模型与中断机制;二是修改内核源码后未重新编译对应模块,导致符号版本不匹配;三是裁剪配置时只关注驱动本身,忽略其依赖的总线与子系统。复盘时应把“配置—编译—加载—probe—功能验证”作为固定检查链,任何一环缺失都会表现为“驱动不工作”,而根因往往不在驱动代码本身。