第二章:Linux内核基础——内核模块机制、设备驱动模型与模块加载流程

各位同学,欢迎来到第二章。这一章,我们聊聊Linux内核的基础设施。说白了,就是搞清楚驱动是怎么“住进”内核的,以及内核怎么跟硬件打交道。

我刚开始接触GPU驱动适配时,也踩过不少坑。比如模块加载顺序不对,系统直接panic。又比如PCIe设备枚举失败,查了半天发现是驱动没注册对。嗯,这些经验我会在这一章里分享给你。

2.1 内核模块机制:驱动就是“插件”

Linux内核是宏内核,但支持模块化。模块(Module)就是一段可以在运行时动态加载到内核的代码。GPU驱动通常以模块形式存在。

为什么不用直接编译进内核?我个人习惯是:能用模块就用模块。原因很简单——调试方便。你改一行代码,重新编译模块,insmod一下就行。要是编进内核,你得重启机器。

核心概念:模块就是内核的“插件”。它有自己的生命周期:加载、初始化、运行、卸载。

2.1.1 模块的基本结构

一个最简单的内核模块长这样:

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

static int __init my_driver_init(void)
{
    printk(KERN_INFO "GPU driver loaded\n");
    return 0;
}

static void __exit my_driver_exit(void)
{
    printk(KERN_INFO "GPU driver unloaded\n");
}

module_init(my_driver_init);
module_exit(my_driver_exit);

MODULE_LICENSE("GPL");
MODULE_AUTHOR("Your Name");
MODULE_DESCRIPTION("A simple GPU driver module");

这里要注意几个点:

  • __init 和 __exit 宏:告诉内核这些函数只在初始化/退出时使用,之后可以释放内存。
  • module_init 和 module_exit:注册入口和出口函数。
  • MODULE_LICENSE:必须声明,否则内核会报“tainted”警告。

我的经验:我曾经在项目里忘了加MODULE_LICENSE,结果加载模块时内核直接拒绝。查了半天才发现是许可证问题。嗯,这种低级错误,犯一次就记住了。

2.1.2 模块参数与依赖

模块可以接受参数。比如你想指定GPU的BAR地址:

static int gpu_bar_addr = 0x10000000;
module_param(gpu_bar_addr, int, 0644);
MODULE_PARM_DESC(gpu_bar_addr, "GPU BAR base address");

加载时这样传参:

insmod gpu_driver.ko gpu_bar_addr=0x20000000

模块之间也有依赖关系。比如你的GPU驱动依赖一个PCIe核心模块。用 modprobe 会自动处理依赖,而 insmod 不会。我建议你养成用 modprobe 的习惯。

2.2 设备驱动模型:总线、设备、驱动

Linux内核的设备驱动模型,说白了就是“媒人”机制。它有三个核心角色:

  • 总线(Bus):负责匹配设备和驱动。
  • 设备(Device):硬件实体,挂在总线上。
  • 驱动(Driver):软件实体,负责控制设备。

你想想看,当系统启动时,内核会枚举所有PCIe设备。每个设备都有一个vendor ID和device ID。驱动会声明自己支持哪些ID。总线负责牵线搭桥——找到匹配的,就调用驱动的probe函数。

核心流程:总线枚举设备 → 驱动注册 → 总线匹配 → 调用probe → 设备初始化完成

2.2.1 PCIe设备驱动模型

GPU通常挂在PCIe总线上。PCIe驱动的核心结构是 pci_driver:

static struct pci_device_id gpu_pci_ids[] = {
    { PCI_DEVICE(0x10DE, 0x1E84) },  // NVIDIA GPU示例
    { 0, }
};
MODULE_DEVICE_TABLE(pci, gpu_pci_ids);

static int gpu_probe(struct pci_dev *dev, const struct pci_device_id *id)
{
    int ret;
    printk(KERN_INFO "GPU found: vendor=%04x device=%04x\n",
           id->vendor, id->device);

    // 启用设备
    ret = pci_enable_device(dev);
    if (ret) {
        printk(KERN_ERR "Failed to enable PCI device\n");
        return ret;
    }

    // 请求BAR空间
    ret = pci_request_regions(dev, "gpu_driver");
    if (ret) {
        printk(KERN_ERR "Failed to request regions\n");
        pci_disable_device(dev);
        return ret;
    }

    // 设置DMA掩码
    ret = dma_set_mask(&dev->dev, DMA_BIT_MASK(64));
    if (ret) {
        ret = dma_set_mask(&dev->dev, DMA_BIT_MASK(32));
        if (ret) {
            printk(KERN_ERR "No suitable DMA mask\n");
            pci_release_regions(dev);
            pci_disable_device(dev);
            return ret;
        }
    }

    return 0;
}

static void gpu_remove(struct pci_dev *dev)
{
    pci_release_regions(dev);
    pci_disable_device(dev);
    printk(KERN_INFO "GPU removed\n");
}

static struct pci_driver gpu_driver = {
    .name     = "gpu_driver",
    .id_table = gpu_pci_ids,
    .probe    = gpu_probe,
    .remove   = gpu_remove,
};

module_pci_driver(gpu_driver);

避坑指南:我曾经在probe函数里忘了调用pci_enable_device,结果读写BAR空间时系统直接死机。记住:启用设备是第一步,否则PCI配置空间不可用。

2.2.2 Platform设备驱动模型

有些GPU(比如嵌入式GPU)不挂在PCIe上,而是作为Platform设备存在。Platform设备是内核中“虚拟总线”上的设备,常用于SoC内部集成外设。

static struct platform_driver gpu_platform_driver = {
    .probe  = gpu_platform_probe,
    .remove = gpu_platform_remove,
    .driver = {
        .name = "gpu_platform",
        .of_match_table = gpu_of_match,
    },
};

module_platform_driver(gpu_platform_driver);

Platform驱动通常配合设备树(Device Tree)使用。设备树里描述硬件资源,驱动通过 of_ 系列API读取。

2.3 内核编译与模块加载流程

这部分我建议你亲手操作一遍。光看文档是记不住的。

2.3.1 编译内核模块

一个标准的Makefile:

obj-m += gpu_driver.o

all:
    make -C /lib/modules/$(shell uname -r)/build M=$(PWD) modules

clean:
    make -C /lib/modules/$(shell uname -r)/build M=$(PWD) clean

执行 make 后,会生成 gpu_driver.ko 文件。这就是内核模块。

我的习惯:编译时加上 V=1 参数,可以看到详细的编译命令。调试时很有用。

2.3.2 模块加载与卸载

常用命令:

命令 作用 说明
insmod gpu_driver.ko 加载模块 不处理依赖,需要指定路径
modprobe gpu_driver 加载模块 自动处理依赖,从标准路径加载
rmmod gpu_driver 卸载模块 需要模块未被使用
lsmod 列出已加载模块 查看模块状态
modinfo gpu_driver.ko 查看模块信息 显示作者、描述、参数等

加载流程是这样的:

  1. 用户执行 insmod 或 modprobe
  2. 内核调用 sys_init_module 系统调用
  3. 内核将模块ELF文件加载到内核空间
  4. 解析模块的符号表和依赖
  5. 调用模块的 init_module 函数(即你写的 module_init 注册的函数)
  6. 模块进入运行状态

避坑指南:我曾经在调试时反复insmod/rmmod,结果有一次rmmod后立刻insmod,系统报“module already exists”。原因是内核有引用计数,rmmod后需要等一会儿。用 lsmod | grep gpu 确认模块确实已卸载再重新加载。

2.3.3 查看内核日志

模块的 printk 输出会进入内核日志。用 dmesg 查看:

dmesg | tail -20

也可以实时监控:

tail -f /var/log/kern.log

或者用 journalctl(systemd系统):

journalctl -kf

2.4 知识体系总览

下面这张图,是我为你梳理的本章知识结构。建议你保存下来,复习时对照着看。

Linux内核基础:驱动适配知识体系 内核模块机制 模块结构:init/exit 模块参数:module_param 模块依赖:modprobe 设备驱动模型 总线(Bus):匹配机制 PCIe驱动:pci_driver Platform驱动:设备树 编译与加载流程 Makefile编写 insmod/modprobe dmesg日志查看 模块加载核心流程 用户执行insmod sys_init_module 加载到内核空间 解析符号表 调用init_module 模块进入运行态 注:PCIe驱动在probe中完成设备初始化,Platform驱动类似

2.5 本章小结

这一章我们聊了三件事:

  • 内核模块机制:驱动就是模块,有固定的结构和生命周期。
  • 设备驱动模型:总线、设备、驱动三者配合,PCIe和Platform是两种常见场景。
  • 编译与加载流程:从Makefile到insmod,再到dmesg看日志,每一步都有讲究。

嗯,这些内容看起来基础,但GPU驱动适配的很多坑都埋在这里。比如PCIe BAR空间没正确映射,比如模块加载顺序导致符号找不到。下一章我们会深入PCIe配置空间和BAR映射,到时候你会回来感谢这一章的铺垫。

一句话总结:驱动适配,内核基础是地基。地基不稳,上层再漂亮的代码也是空中楼阁。

可可精品资料库