2. 字符设备驱动基础:设备号概念与分配

在 Linux 字符设备驱动中,设备号是内核与用户空间识别设备的核心标识。每个字符设备在系统中都拥有唯一的设备号,内核通过它来查找对应的驱动程序并完成文件操作。本章将系统讲解设备号的构成、分配与释放机制,为后续实现中断处理和 ioctl 接口打下基础。

2.1 设备号概念

设备号是一个 dev_t 类型的 32 位无符号整数,由两部分组成:

  • 主设备号(Major Number):高 12 位,用于标识设备对应的驱动程序。内核通过主设备号找到驱动注册的 file_operations 结构体。
  • 次设备号(Minor Number):低 20 位,由驱动程序内部使用,通常用于区分同一驱动管理的多个物理或逻辑设备实例。
关键理解: 主设备号是“驱动 ID”,次设备号是“设备实例 ID”。同一个主设备号下的不同次设备号共享同一套驱动操作函数。
设备号 dev_t 结构(32 位) 主设备号(Major) 高 12 位(bit 20–31) 次设备号(Minor) 低 20 位(bit 0–19) MAJOR(dev) 提取主设备号 | MINOR(dev) 提取次设备号 | MKDEV(major, minor) 合成 dev_t

2.2 主设备号与次设备号的职责划分

属性 主设备号 次设备号
位数 12 位(0–4095) 20 位(0–1048575)
内核用途 查找驱动注册的 file_operations 驱动内部区分设备实例
分配方式 静态指定 或 动态分配 由驱动开发者自行规划
典型示例 89(I2C 设备) 0–3(同一 I2C 总线上的 4 个从设备)
用户空间可见 ls -l /dev 第一列数字 ls -l /dev 第二列数字
实践提示: 在 /dev 目录下执行 ls -l,你会看到类似 crw-rw---- 1 root root 10, 235 的输出,其中 10 是主设备号,235 是次设备号。

2.3 设备号分配与释放 API

Linux 内核提供两组核心函数用于设备号的注册与注销。驱动开发者必须在模块加载时分配设备号,在卸载时释放。

2.3.1 静态分配

当驱动使用已知的、未占用的主设备号时,可以静态指定:

#include <linux/fs.h>

int register_chrdev_region(dev_t first, unsigned int count, const char *name);
// first: 起始设备号(主设备号 << 20 | 起始次设备号)
// count: 连续分配的次设备号数量
// name: 设备名称(显示在 /proc/devices)
// 返回值:0 成功,负值失败

2.3.2 动态分配

推荐使用动态分配,避免主设备号冲突:

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

2.3.3 释放设备号

无论使用哪种分配方式,释放接口相同:

void unregister_chrdev_region(dev_t first, unsigned int count);
// first: 之前分配的起始设备号
// count: 与分配时相同的数量
常见错误: 释放时 count 参数必须与分配时一致,否则会导致设备号泄漏或错误释放其他驱动的设备号。建议在模块卸载函数中立即调用释放。
设备号分配与释放流程 模块加载入口 已知主设备号? 是 register_chrdev_region 否 alloc_chrdev_region cdev_add + class_create 模块卸载 → unregister_chrdev_region 动态分配优先使用 alloc_chrdev_region,避免主设备号冲突

2.4 查看已分配设备号

系统运行时,可以通过以下方式查看当前已注册的设备号:

  • /proc/devices:显示所有已注册的主设备号及其对应的驱动名称。
  • /sys/dev/char/:按主设备号/次设备号组织的 sysfs 目录。
  • ls -l /dev:显示设备文件对应的设备号。
调试技巧: 在驱动开发中,如果 insmod 失败并提示 Device or resource busy,通常是因为主设备号已被占用。此时应改用 alloc_chrdev_region 动态分配。

2.5 完整示例:设备号分配与释放

以下代码展示了一个字符设备驱动模块中设备号管理的标准模式:

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

#define DEVICE_NAME "my_char_dev"
#define MINOR_COUNT 4

static dev_t dev_num;

static int __init my_init(void)
{
    int ret;

    /* 动态分配 4 个连续设备号,起始次设备号为 0 */
    ret = alloc_chrdev_region(&dev_num, 0, MINOR_COUNT, DEVICE_NAME);
    if (ret < 0) {
        pr_err("Failed to allocate device numbers\n");
        return ret;
    }

    pr_info("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, MINOR_COUNT);
    pr_info("Device numbers released\n");
}

module_init(my_init);
module_exit(my_exit);

MODULE_LICENSE("GPL");
MODULE_AUTHOR("Driver Expert");
MODULE_DESCRIPTION("Device number allocation example");

2.6 本章小结

知识点 核心要点
设备号结构 dev_t 高 12 位主设备号 + 低 20 位次设备号
主设备号作用 内核查找驱动 file_operations 的索引
次设备号作用 驱动内部区分多个设备实例
静态分配 register_chrdev_region(),需已知空闲主设备号
动态分配 alloc_chrdev_region(),推荐使用,避免冲突
释放接口 unregister_chrdev_region(),参数必须与分配一致