4. 字符设备驱动基础(二):cdev 结构体、设备号的动态分配、设备节点的自动创建
在上一章中,我们通过一个最简单的字符设备驱动了解了模块的加载与卸载。本章将深入字符设备驱动的核心数据结构 cdev,并解决驱动开发中的两个关键问题:设备号管理与设备节点自动创建。掌握这些内容后,你将能够编写出更规范、更实用的字符设备驱动。
本章核心目标:
- 理解
cdev结构体的组成与初始化流程。 - 掌握设备号的动态分配机制(
alloc_chrdev_region)。 - 学会使用 udev/mdev 实现设备节点的自动创建。
4.1 cdev 结构体:字符设备的内核抽象
在 Linux 内核中,每个字符设备都由一个 struct cdev 结构体表示。它是连接用户空间文件操作与内核驱动函数的桥梁。
4.1.1 cdev 结构体定义
cdev 结构体定义在 <linux/cdev.h> 中,其核心成员如下:
| 成员 | 类型 | 描述 |
|---|---|---|
kobj |
struct kobject |
内核对象基类,用于设备模型管理 |
owner |
struct module * |
指向所属模块的指针,通常设为 THIS_MODULE |
ops |
const struct file_operations * |
指向文件操作结构体,定义 open/read/write 等回调 |
list |
struct list_head |
用于将 cdev 挂入系统链表 |
dev |
dev_t |
设备号(主设备号 + 次设备号) |
count |
unsigned int |
该设备管理的次设备号数量 |
4.1.2 cdev 初始化与注册流程
使用 cdev 的标准流程分为三步:
- 分配 cdev 对象:使用
cdev_alloc()或静态定义。 - 初始化 cdev:调用
cdev_init()绑定file_operations。 - 注册 cdev:调用
cdev_add()将设备加入内核。
// 示例:cdev 初始化与注册
#include <linux/cdev.h>
#include <linux/fs.h>
static struct cdev my_cdev;
static dev_t dev_num;
static const struct file_operations my_fops = {
.owner = THIS_MODULE,
.open = my_open,
.read = my_read,
.write = my_write,
.release = my_release,
};
static int __init my_init(void)
{
int ret;
// 1. 初始化 cdev 结构体
cdev_init(&my_cdev, &my_fops);
my_cdev.owner = THIS_MODULE;
// 2. 注册 cdev(假设 dev_num 已分配)
ret = cdev_add(&my_cdev, dev_num, 1);
if (ret < 0) {
printk(KERN_ERR "Failed to add cdev\n");
return ret;
}
return 0;
}
static void __exit my_exit(void)
{
cdev_del(&my_cdev); // 注销 cdev
}
关键注意点:
cdev_add()必须在设备号分配成功后调用。cdev_del()必须在模块卸载时调用,否则设备节点仍可访问已释放的内存。cdev_init()会清空cdev结构体,因此owner赋值必须在cdev_init()之后。
4.1.3 cdev 生命周期 SVG 图
4.2 设备号的动态分配
设备号由主设备号(major)和次设备号(minor)组成,类型为 dev_t。传统方式使用静态分配(register_chrdev_region),但容易造成冲突。现代驱动推荐使用动态分配。
4.2.1 动态分配 API
| 函数 | 原型 | 说明 |
|---|---|---|
alloc_chrdev_region |
int alloc_chrdev_region(dev_t *dev, unsigned int baseminor, unsigned int count, const char *name) |
动态分配设备号,由内核自动选择主设备号 |
register_chrdev_region |
int register_chrdev_region(dev_t from, unsigned int count, const char *name) |
静态分配,需指定起始设备号(不推荐) |
unregister_chrdev_region |
void unregister_chrdev_region(dev_t from, unsigned int count) |
释放已分配的设备号 |
4.2.2 动态分配标准代码片段
// 动态分配设备号
static dev_t my_dev_num;
static int __init my_init(void)
{
int ret;
// 动态分配:起始次设备号 0,数量 1,设备名 "my_device"
ret = alloc_chrdev_region(&my_dev_num, 0, 1, "my_device");
if (ret < 0) {
printk(KERN_ERR "Failed to allocate device number\n");
return ret;
}
printk(KERN_INFO "Allocated device number: major=%d, minor=%d\n",
MAJOR(my_dev_num), MINOR(my_dev_num));
// 后续:cdev_init + cdev_add
return 0;
}
static void __exit my_exit(void)
{
unregister_chrdev_region(my_dev_num, 1); // 释放设备号
printk(KERN_INFO "Device number released\n");
}
最佳实践:
- 始终优先使用
alloc_chrdev_region而非静态分配。 - 使用
MAJOR()和MINOR()宏从dev_t中提取主/次设备号。 - 设备号释放必须在
cdev_del()之后,避免竞态。
4.2.3 设备号分配流程 SVG 图
4.3 设备节点的自动创建:udev / mdev
传统驱动需要手动使用 mknod 创建设备节点,效率低且易出错。现代 Linux 系统通过 udev(桌面/服务器)或 mdev(嵌入式 busybox)实现设备节点的自动创建。
4.3.1 自动创建机制原理
当驱动调用 cdev_add() 后,内核会通过 kobject_uevent 向用户空间发送 ADD 事件。udev/mdev 监听该事件,根据设备信息自动在 /dev 目录下创建节点。
驱动需要配合完成以下工作:
- 创建 设备类(class):使用
class_create()。 - 创建设备:使用
device_create(),指定类、设备号、设备名。 - 清理时销毁:使用
device_destroy()和class_destroy()。
4.3.2 完整代码示例
// 自动创建设备节点完整示例
#include <linux/device.h>
#include <linux/cdev.h>
#include <linux/fs.h>
#include <linux/module.h>
static struct class *my_class;
static struct device *my_device;
static struct cdev my_cdev;
static dev_t dev_num;
static const struct file_operations my_fops = {
.owner = THIS_MODULE,
.open = my_open,
.read = my_read,
.write = my_write,
.release = my_release,
};
static int __init my_init(void)
{
int ret;
// 1. 动态分配设备号
ret = alloc_chrdev_region(&dev_num, 0, 1, "my_device");
if (ret < 0) return ret;
// 2. 初始化并注册 cdev
cdev_init(&my_cdev, &my_fops);
my_cdev.owner = THIS_MODULE;
ret = cdev_add(&my_cdev, dev_num, 1);
if (ret < 0) {
unregister_chrdev_region(dev_num, 1);
return ret;
}
// 3. 创建设备类
my_class = class_create(THIS_MODULE, "my_class");
if (IS_ERR(my_class)) {
cdev_del(&my_cdev);
unregister_chrdev_region(dev_num, 1);
return PTR_ERR(my_class);
}
// 4. 创建设备节点(自动在 /dev 下生成 my_device0)
my_device = device_create(my_class, NULL, dev_num, NULL, "my_device%d", 0);
if (IS_ERR(my_device)) {
class_destroy(my_class);
cdev_del(&my_cdev);
unregister_chrdev_region(dev_num, 1);
return PTR_ERR(my_device);
}
printk(KERN_INFO "Device my_device0 created successfully\n");
return 0;
}
static void __exit my_exit(void)
{
device_destroy(my_class, dev_num); // 销毁设备
class_destroy(my_class); // 销毁类
cdev_del(&my_cdev); // 注销 cdev
unregister_chrdev_region(dev_num, 1); // 释放设备号
printk(KERN_INFO "Module exited\n");
}
module_init(my_init);
module_exit(my_exit);
MODULE_LICENSE("GPL");
4.3.3 udev 与 mdev 对比
| 特性 | udev | mdev |
|---|---|---|
| 适用场景 | 桌面/服务器 Linux | 嵌入式 Linux(busybox) |
| 依赖 | libudev、systemd | busybox mdev |
| 配置文件 | /etc/udev/rules.d/ | /etc/mdev.conf |
| 事件处理 | 异步、支持复杂规则 | 同步、规则简单 |
| 资源占用 | 较大 | 极小 |
| 自动创建节点 | 是 | 是 |
4.3.4 设备节点自动创建流程 SVG 图
4.4 本章小结
核心要点回顾:
- cdev 结构体是字符设备驱动的核心,通过
cdev_init+cdev_add注册,cdev_del注销。 - 设备号动态分配使用
alloc_chrdev_region,避免主设备号冲突。 - 设备节点自动创建依赖
class_create+device_create,由 udev/mdev 在用户空间完成。 - 驱动卸载时需按逆序释放资源:
device_destroy→class_destroy→cdev_del→unregister_chrdev_region。