Linux内核驱动开发实战50例 - 第1节

1.1 开发环境全景概览

Linux 内核驱动开发的第一步是搭建一个稳定、可复现的编译与调试环境。本章将引导你完成从工具链安装到内核镜像生成的全流程,并深入理解内核模块的编译框架。

环境速查表:以下为推荐的最低配置与软件版本,实际使用时请根据发行版调整。
组件 推荐版本 / 工具 说明
主机操作系统 Ubuntu 22.04 LTS / Debian 12 包管理完善,社区支持好
内核源码 Linux 6.6 LTS 长期支持版,稳定性高
编译器 GCC 12.2+ 需支持 C11 及内联汇编
构建工具 make 4.3+ / LLVM 16+ 可选 clang 编译
调试器 GDB 13.1 + QEMU 8.0 用于内核级调试

1.1.1 工具链安装

使用以下命令一次性安装内核编译所需的全部基础包:

sudo apt update
sudo apt install -y \
    build-essential \
    libncurses-dev \
    flex bison \
    libssl-dev \
    libelf-dev \
    bc \
    dwarves \
    git \
    qemu-system-x86 \
    gdb-multiarch
提示:如果使用 clang 编译,额外安装 clang-16 lld-16,并在 make 时添加 LLVM=1 参数。

1.2 内核源码获取与版本管理

推荐从 kernel.org 获取主线源码,或使用发行版打补丁后的源码树。本课程统一使用 Linus Torvalds 的 torvalds/linux 仓库。

# 克隆主线仓库(浅克隆,节省带宽)
git clone --depth=1 --branch v6.6 https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git
cd linux
注意:不要使用 sudo 执行 git clone,否则后续文件权限混乱会导致编译失败。

1.2.1 源码目录结构

理解内核源码的顶层目录是驱动开发的基础:

目录 内容 驱动开发者关注点
arch/ 体系结构相关代码(x86, arm64, riscv) 板级支持、DTS、启动汇编
drivers/ 所有设备驱动 你的代码将放在这里
include/ 内核头文件 驱动 API 声明
kernel/ 核心调度、进程管理 通常不修改
Documentation/ 内核文档(RST 格式) 必读参考
内核源码顶层目录与驱动开发关系 内核源码顶层目录与驱动开发关系 Linux Kernel 源码根 arch/ drivers/ include/ Documentation/ 驱动开发者核心路径: 在 drivers/ 下编写模块 → 使用 include/ 中的 API → 参考 Documentation/ 规范 → 通过 arch/ 适配硬件

1.3 内核配置与编译

内核配置是编译前最关键的一步。错误的配置会导致驱动无法加载或系统崩溃。本课程采用 defconfig 为基础,再手动开启驱动开发所需的选项。

1.3.1 配置方法对比

命令 交互方式 适用场景
make defconfig 自动生成默认配置 快速开始,通用平台
make menuconfig 终端图形界面 手动开关选项,最常用
make nconfig 终端菜单界面 比 menuconfig 更高效
make olddefconfig 非交互,使用现有 .config 升级内核版本时迁移配置
# 生成 x86_64 默认配置
make defconfig

# 进入菜单配置界面,开启以下选项:
#   CONFIG_MODULES=y          —— 支持模块
#   CONFIG_MODVERSIONS=y      —— 模块版本检查
#   CONFIG_DEBUG_INFO=y       —— 调试符号
make menuconfig
关键配置项:驱动开发必须确保 CONFIG_MODULES=y 和 CONFIG_MODULE_UNLOAD=y,否则无法动态加载/卸载模块。

1.3.2 编译内核与模块

# 使用全部 CPU 核心编译内核(-j 参数)
make -j$(nproc)

# 单独编译内核模块
make modules -j$(nproc)

# 安装模块到 /lib/modules/$(uname -r)/
sudo make modules_install

# 安装内核镜像和 System.map
sudo make install
常见错误:如果遇到 No rule to make target 'debian/certs/signing_key.pem',执行 make scripts 生成签名工具,或在 menuconfig 中关闭 CONFIG_MODULE_SIG。
内核编译与模块安装流程 内核编译与模块安装流程 make defconfig make menuconfig make -j$(nproc) sudo make modules_install sudo make install 最终生成:arch/x86/boot/bzImage(内核镜像) + /lib/modules/$(uname -r)/(模块目录)

1.4 内核模块编译框架

内核模块的编译不依赖传统的 gcc 直接调用,而是通过内核构建系统(Kbuild)完成。Kbuild 会读取源码目录下的 Kbuild 或 Makefile,自动处理依赖、交叉编译和版本检查。

1.4.1 最小模块 Makefile 模板

# Makefile —— 单文件模块
obj-m := hello.o

# 如果模块由多个源文件组成:
# hello-objs := main.o helper.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
关键变量:obj-m 表示编译为可加载模块,obj-y 表示编译进内核。驱动开发阶段始终使用 obj-m。

1.4.2 模块入口与退出函数

// hello.c
#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/init.h>

static int __init hello_init(void)
{
    pr_info("Hello, Linux Kernel Module!\n");
    return 0;
}

static void __exit hello_exit(void)
{
    pr_info("Goodbye, Kernel Module!\n");
}

module_init(hello_init);
module_exit(hello_exit);

MODULE_LICENSE("GPL");
MODULE_AUTHOR("Your Name");
MODULE_DESCRIPTION("A simple Hello World module");

1.4.3 编译与测试

# 编译模块
make

# 加载模块(需要 root 权限)
sudo insmod hello.ko

# 查看内核日志
dmesg | tail -5
# 输出: [  xxx] Hello, Linux Kernel Module!

# 卸载模块
sudo rmmod hello

# 再次查看日志
dmesg | tail -5
# 输出: [  xxx] Goodbye, Kernel Module!
调试技巧:如果 insmod 失败,使用 dmesg 查看具体错误。常见原因包括:modprobe: ERROR: could not insert 'hello': Exec format error —— 内核版本不匹配,或 CONFIG_MODVERSIONS 未开启。
内核模块编译框架 内核模块编译框架 用户 Makefile hello.c 源码 Kbuild 构建系统 内核源码树 hello.ko 模块 Module.symvers 编译流程: 1. make -C /lib/modules/$(uname -r)/build M=$(pwd) modules 2. Kbuild 读取 obj-m,调用内核编译规则生成 .o → .ko 3. 同时生成 Module.symvers 记录导出的符号

1.5 本章小结与实战要点

  • 环境搭建:优先使用 Ubuntu/Debian,安装 build-essential 及内核编译依赖。
  • 源码管理:使用 git clone --depth=1 获取主线内核,避免下载完整历史。
  • 配置编译:make defconfig 后通过 menuconfig 开启 CONFIG_MODULES 和 CONFIG_DEBUG_INFO。
  • 模块框架:牢记 obj-m 与 module_init/module_exit 的配对使用。
  • 调试手段:dmesg 是驱动开发者的第一调试工具,学会过滤关键词 dmesg | grep -i hello。
课后练习:编写一个模块,在 init 时打印当前 jiffies 值,在 exit 时打印模块运行时长(使用 jiffies_to_msecs 转换)。编译加载验证后,将代码和 Makefile 保存为 ch1_timer 目录。