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。理解这一点,就能明白为什么驱动代码里几乎看不到“主动调用设备”,而是“注册并等待匹配”。

Linux内核架构与驱动所处位置 用户空间应用 open / read / write / ioctl / mmap 系统调用接口 (syscall) VFS · 网络协议栈 · 内存管理 · 进程调度 设备模型与总线抽象 platform · I2C · SPI · PCI · USB · device / driver / bus 匹配 具体硬件驱动 (drivers/) 字符设备 · 块设备 · 网络设备 · 平台驱动 · 中断处理 体系结构相关代码 (arch/) ARM / ARM64 / x86 · 启动 · 中断控制器 · 时钟 · 内存映射

二、模块机制:驱动的加载与卸载

内核模块(.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按键驱动,完整路径如下:

  1. 确认内核版本与源码路径,执行make defconfig并开启CONFIG_MODULES、CONFIG_GPIOLIB、CONFIG_INPUT。
  2. 在drivers/input/keyboard/下新建驱动文件,使用platform_driver注册,通过设备树匹配compatible。
  3. 编写Makefile与Kconfig,将驱动加入Kbuild体系。
  4. 编译模块,insmod加载,用evtest验证按键事件。
  5. 若probe未触发,依次检查:设备树节点是否存在、compatible是否匹配、GPIO是否被占用、时钟/中断是否配置。

常见误区有三:一是把驱动写成“主动轮询设备”,忽略内核的设备模型与中断机制;二是修改内核源码后未重新编译对应模块,导致符号版本不匹配;三是裁剪配置时只关注驱动本身,忽略其依赖的总线与子系统。复盘时应把“配置—编译—加载—probe—功能验证”作为固定检查链,任何一环缺失都会表现为“驱动不工作”,而根因往往不在驱动代码本身。

内核基础不是背诵目录,而是建立“配置决定能力、模块决定形态、总线决定匹配、目录决定位置”的工程直觉。掌握这条链路,后续字符设备、平台驱动、设备树与中断处理才能顺利展开。