3. 内核版本演进概览

Linux 设备模型并不是一次性设计完成的静态框架,而是随着内核版本迭代不断重构、抽象和规范化的结果。从 2.6 系列到 6.x,设备模型经历了从“能跑起来”到“可维护、可扩展、可热插拔”的演进。理解这条演进主线,是跨版本适配工程的第一课:只有知道哪些接口在哪个版本被引入、废弃或语义改变,才能在面对不同内核时做出正确的条件编译与兼容层设计。

一、核心原理:设备模型演进的三条主线

设备模型的核心目标始终是:用统一的数据结构描述设备、驱动和总线之间的关系,并让电源管理、热插拔、sysfs 暴露等机制能够自动工作。围绕这个目标,2.6 到 6.x 的变更可以归纳为三条主线。

  • 总线与设备注册接口的规范化:早期驱动常直接操作 struct device 和 struct bus_type,后来逐步收敛到 device_register()、device_unregister()、bus_register() 等标准入口,并强化了引用计数与生命周期管理。
  • 设备树与 ACPI 的统一抽象:2.6 后期引入设备树(Device Tree),3.x 开始大量使用 of_* 接口,4.x 之后 fwnode 抽象逐渐统一了 DT、ACPI 和其他固件描述方式,驱动不再需要为每种固件写两套代码。
  • 电源管理与运行时 PM 的深化:从 suspend/resume 到 dev_pm_ops,再到 runtime PM 和 genpd 域,设备模型承担了越来越多的电源状态管理职责,接口也随之变化。
适配视角:设备模型的变更往往不是孤立 API 替换,而是“数据结构 + 注册流程 + 生命周期 + 固件描述”四者联动。只改函数名而不理解语义变化,是跨版本适配中最常见的错误。

二、版本演进关键节点与 API 变化

下面这张图展示了从 2.6 到 6.x 设备模型关键变更的时间轴与影响范围,便于在适配时快速定位版本边界。

Linux 设备模型版本演进与适配影响 时间 2.6 基础设备模型 kobject / kset bus / device / driver sysfs 初步成型 class_device 仍存在 3.x DT 与统一设备 of_* 接口普及 class_device 移除 devm_* 资源管理 platform_driver 规范 4.x fwnode 与 PM 深化 fwnode 抽象统一 dev_pm_ops 强化 runtime PM 普及 genpd 电源域 5.x 现代化与清理 remove 返回 void probe 新签名 旧 API 批量废弃 设备链接完善 6.x 持续演进 API 收敛 兼容层减少 驱动现代化 适配影响范围 驱动注册/注销 probe/remove 签名 固件描述 DT / ACPI / fwnode 电源管理 PM ops / runtime PM sysfs / 属性 attribute 接口 适配策略:条件编译 + 兼容宏 + 抽象封装层,优先使用 devm_* 与 fwnode 接口。

从图中可以看出,2.6 到 3.x 是“结构定型期”,4.x 是“抽象统一期”,5.x 到 6.x 是“清理与现代化期”。适配工作在不同阶段面临的痛点差异很大。

三、常见适配痛点与影响范围

版本区间 关键变更 常见痛点 影响范围
2.6 → 3.x class_device 移除,of_* 接口引入 旧驱动仍使用 class_device_create,编译失败 字符设备、平台驱动、sysfs 属性
3.x → 4.x fwnode 抽象、dev_pm_ops 强化 DT 与 ACPI 双路径维护,PM 回调签名不一致 电源管理、总线驱动、设备探测
4.x → 5.x remove 返回 void,probe 新签名 返回值处理逻辑失效,错误路径遗漏 所有实现 platform_driver 的驱动
5.x → 6.x 旧 API 批量废弃,兼容层收缩 依赖已删除接口的第三方模块无法编译 外部模块、BSP 驱动、老平台代码
特别注意:remove 返回 void 的变更看似简单,但它意味着驱动不能再通过返回值阻止设备移除。如果旧代码在 remove 中返回错误码并期望框架重试,迁移后必须改为在 probe 阶段完成资源校验,否则会出现“移除后无法恢复”的隐性故障。

四、案例场景:一个平台驱动的跨版本迁移

假设有一个 2.6.32 时代的平台驱动,使用 class_device 创建 sysfs 节点,并在 remove 中返回 int。迁移到 5.10 时,至少需要处理以下变化。

/* 旧代码(2.6.32 风格) */
static int my_remove(struct platform_device *pdev)
{
    class_device_destroy(my_class, dev);
    return 0;
}

/* 新代码(5.x 风格) */
static void my_remove(struct platform_device *pdev)
{
    /* 使用 devm_* 后通常无需手动释放 */
}

/* 兼容宏示例 */
#if LINUX_VERSION_CODE < KERNEL_VERSION(5,0,0)
static int my_remove(struct platform_device *pdev)
#else
static void my_remove(struct platform_device *pdev)
#endif
{
    ...
#if LINUX_VERSION_CODE < KERNEL_VERSION(5,0,0)
    return 0;
#endif
}

这个案例说明,跨版本适配不是简单替换函数名,而是要在编译期识别版本边界,并在运行期保证语义一致。对于大规模 BSP 代码,建议先建立“版本—接口”映射表,再逐模块迁移。

实用建议:优先使用 devm_* 系列接口和 fwnode 抽象,它们能显著减少版本差异带来的资源管理问题。对于必须兼容老版本的内核模块,可以用 LINUX_VERSION_CODE 配合自定义兼容头文件,把差异集中到一处。

五、误区与复盘

在跨版本适配中,最常见的误区有三个。第一,认为“编译通过就等于适配完成”,忽略了运行时 PM 和热插拔路径的语义变化。第二,直接复制新版本驱动代码到老内核,导致使用了尚未引入的 API。第三,过度依赖条件编译,使代码可读性急剧下降,最终难以维护。

更稳妥的做法是:先梳理设备模型相关接口的版本矩阵,再为每个模块建立最小兼容层,最后通过真实硬件或 QEMU 进行 probe/remove/suspend/resume 全路径验证。只有把版本差异显式化、集中化,才能让设备模型适配从“救火”变成“工程”。