4. 设备号管理:静态分配与动态分配设备号

在 Linux 字符设备驱动中,设备号是内核与用户空间识别设备的唯一令牌。设备号由主设备号(major)和次设备号(minor)组成,内核通过 dev_t 类型(32 位)管理。本章深入剖析设备号的静态分配与动态分配机制,以及核心 API alloc_chrdev_region 与 register_chrdev_region 的实战用法。

4.1 设备号结构

设备号在内核中定义为 dev_t,高 12 位为主设备号,低 20 位为次设备号。主设备号标识设备驱动类型,次设备号标识同一驱动下的不同设备实例。

字段 位宽 范围 说明
主设备号(major) 12 位 0 ~ 4095 标识驱动类型,由内核统一管理
次设备号(minor) 20 位 0 ~ 1048575 标识同一驱动下的具体设备实例
提示:内核源码中通过 MAJOR(dev) 和 MINOR(dev) 宏从 dev_t 中提取主、次设备号,使用 MKDEV(major, minor) 宏组合设备号。

4.2 静态分配 vs 动态分配

设备号分配策略分为静态分配和动态分配两种,各有适用场景。下表对比了两种策略的核心差异:

对比维度 静态分配 动态分配
分配方式 驱动开发者手动指定主设备号 内核自动分配未使用的主设备号
核心 API register_chrdev_region alloc_chrdev_region
主设备号来源 从 /proc/devices 或内核文档中预知 由内核动态生成,驱动无需预知
设备节点创建 需手动 mknod 或提前约定主设备号 可通过 udev 自动创建设备节点
冲突风险 高,多个驱动可能使用相同主设备号 低,内核保证唯一性
推荐场景 嵌入式平台、固定硬件、内核内置驱动 通用驱动、可热插拔设备、模块化驱动
注意:静态分配的主设备号需要向 Linux 内核社区申请并登记在 Documentation/admin-guide/devices.txt 中,否则可能与其他驱动冲突。动态分配是当前驱动开发的主流实践。

4.3 核心 API 详解

4.3.1 register_chrdev_region — 静态分配

函数原型:

int register_chrdev_region(dev_t from, unsigned count, const char *name);
  • from:起始设备号,通常使用 MKDEV(major, minor) 构造。
  • count:连续分配的次设备号数量。
  • name:设备名称,显示在 /proc/devices 中。
  • 返回值:成功返回 0,失败返回负的错误码。

使用示例:

#include <linux/fs.h>
#include <linux/device.h>

#define MY_MAJOR 240
#define MY_MINOR 0
#define DEVICE_NAME "my_static_dev"

static dev_t dev_num;

static int __init my_init(void)
{
    dev_num = MKDEV(MY_MAJOR, MY_MINOR);
    int ret = register_chrdev_region(dev_num, 1, DEVICE_NAME);
    if (ret < 0) {
        pr_err("Failed to register device number: %d\n", ret);
        return ret;
    }
    pr_info("Static device registered: major=%d, minor=%d\n",
            MAJOR(dev_num), MINOR(dev_num));
    return 0;
}

static void __exit my_exit(void)
{
    unregister_chrdev_region(dev_num, 1);
    pr_info("Device number unregistered\n");
}

module_init(my_init);
module_exit(my_exit);

4.3.2 alloc_chrdev_region — 动态分配

函数原型:

int alloc_chrdev_region(dev_t *dev, unsigned baseminor, unsigned count, const char *name);
  • dev:输出参数,返回分配到的起始设备号。
  • baseminor:起始次设备号,通常为 0。
  • count:连续分配的次设备号数量。
  • name:设备名称。
  • 返回值:成功返回 0,失败返回负的错误码。

使用示例:

#include <linux/fs.h>
#include <linux/device.h>

#define DEVICE_NAME "my_dynamic_dev"

static dev_t dev_num;

static int __init my_init(void)
{
    int ret = alloc_chrdev_region(&dev_num, 0, 1, DEVICE_NAME);
    if (ret < 0) {
        pr_err("Failed to allocate device number: %d\n", ret);
        return ret;
    }
    pr_info("Dynamic device allocated: major=%d, minor=%d\n",
            MAJOR(dev_num), MINOR(dev_num));
    return 0;
}

static void __exit my_exit(void)
{
    unregister_chrdev_region(dev_num, 1);
    pr_info("Device number released\n");
}

module_init(my_init);
module_exit(my_exit);
关键区别:register_chrdev_region 需要驱动预先知道主设备号,而 alloc_chrdev_region 由内核动态分配,驱动只需提供次设备号起始值和数量。动态分配是现代驱动开发的首选方式。

4.4 设备号分配流程对比图

下图展示了静态分配与动态分配在内核中的完整流程:

设备号分配流程对比图 设备号分配流程对比:静态 vs 动态 静态分配 register_chrdev_region 驱动指定主设备号(如 240) MKDEV(major, minor) 构造 dev_t 内核检查主设备号是否已被占用 成功:注册到内核设备号表 手动 mknod 或 udev 规则创建设备节点 动态分配 alloc_chrdev_region 驱动不指定主设备号,仅提供次设备号起始值 内核扫描空闲主设备号区间 分配未使用的主设备号 + 指定次设备号 成功:通过 dev 指针返回设备号 udev 自动创建设备节点(推荐) 动态分配无需预知主设备号,内核自动管理,避免冲突,配合 udev 实现设备节点自动创建

4.5 设备号释放

无论使用哪种分配方式,在驱动卸载时都必须释放设备号,使用统一的 API:

void unregister_chrdev_region(dev_t from, unsigned count);
  • from:与分配时相同的起始设备号。
  • count:与分配时相同的次设备号数量。
重要:设备号释放必须在 cdev_del 之后、class_destroy 之前调用,否则可能导致内核访问已释放的资源。典型顺序为:cdev_del → device_destroy → class_destroy → unregister_chrdev_region。

4.6 实战选择建议

场景 推荐策略 原因
嵌入式固定硬件平台 静态分配 主设备号固定,便于脚本和工具链集成
通用 Linux 驱动模块 动态分配 避免冲突,支持热插拔,udev 自动管理
内核主线驱动 静态分配(需申请) 社区要求固定主设备号,便于用户空间兼容
教学实验驱动 动态分配 简化流程,聚焦驱动核心逻辑
最佳实践:在驱动开发初期使用 alloc_chrdev_region 动态分配,通过 dmesg 查看分配到的设备号,配合 udev 规则自动创建设备节点。待驱动稳定后,如需提交内核主线,再申请静态主设备号。

4.7 常见错误与调试

  • EINVAL:register_chrdev_region 指定的主设备号超出范围(0~4095)或次设备号超出范围。
  • EBUSY:静态分配时指定的主设备号已被其他驱动占用。
  • ENOMEM:内核内存不足,无法分配设备号表项。
  • 调试方法:使用 cat /proc/devices 查看当前已注册的设备号列表,确认分配是否成功。