3. 字符设备驱动框架:设备号注册与注销、file_operations结构体、cdev结构体、设备节点的创建(手动与自动)
字符设备是Linux驱动开发中最基础的设备类型,其核心框架围绕设备号、cdev结构体和file_operations操作集展开。本章将系统讲解字符设备驱动的完整骨架:从设备号的申请与释放,到内核对象的注册与注销,再到用户空间设备节点的创建方式。
3.1 字符设备驱动整体架构
字符设备驱动的用户态与内核态交互遵循“文件操作”抽象模型。应用程序通过open()、read()、write()等系统调用进入内核,VFS层根据设备号找到对应的cdev,再回调驱动注册的file_operations函数指针。
3.2 设备号:主设备号与次设备号
设备号是内核标识字符/块设备的唯一键值,由主设备号和次设备号两部分组成,类型为dev_t(32位无符号整数)。
| 组成部分 | 位宽 | 作用 | 示例 |
|---|---|---|---|
| 主设备号 (major) | 高12位 | 标识设备驱动程序 | 8 (SD/MMC块设备) |
| 次设备号 (minor) | 低20位 | 标识同驱动下的不同设备实例 | 0, 1, 2 (分区) |
内核宏操作:使用
MAJOR(dev_t) 提取主设备号,MINOR(dev_t) 提取次设备号,MKDEV(major, minor) 合成 dev_t。
3.2.1 静态申请 vs 动态分配
设备号注册有两种策略,推荐使用动态分配以避免冲突:
| 方式 | 函数 | 优点 | 缺点 |
|---|---|---|---|
| 静态申请 | register_chrdev_region() |
设备号固定,适合已知标准设备 | 容易与已有驱动冲突 |
| 动态分配 | alloc_chrdev_region() |
内核自动分配未使用的主设备号 | 设备号不固定,需通过 /proc/devices 查看 |
动态分配代码模板:
dev_t dev_num;
int ret;
ret = alloc_chrdev_region(&dev_num, 0, 1, "my_device");
if (ret < 0) {
pr_err("Failed to allocate device number\n");
return ret;
}
pr_info("Allocated major=%d minor=%d\n",
MAJOR(dev_num), MINOR(dev_num));
3.2.2 设备号注销
在驱动卸载时,必须释放已注册的设备号,否则下次加载会失败:
void unregister_chrdev_region(dev_t from, unsigned count);
常见错误:卸载驱动时忘记调用
unregister_chrdev_region(),导致 /proc/devices 残留僵尸设备号,下次 insmod 返回 -EBUSY。
3.3 file_operations 结构体
file_operations 是字符设备驱动的“函数跳转表”,每个成员对应一个系统调用。驱动只需实现需要的回调,未实现的成员置为 NULL,内核会返回 -EINVAL。
| 成员 | 系统调用 | 典型实现 |
|---|---|---|
open |
open() |
递增使用计数、初始化硬件 |
release |
close() |
递减使用计数、释放资源 |
read |
read() |
从硬件读取数据到用户缓冲区 |
write |
write() |
从用户缓冲区写入硬件 |
unlocked_ioctl |
ioctl() |
设备控制命令处理 |
llseek |
lseek() |
修改文件读写位置 |
标准初始化方式(推荐使用宏):
static struct file_operations my_fops = {
.owner = THIS_MODULE,
.open = my_open,
.release = my_release,
.read = my_read,
.write = my_write,
.unlocked_ioctl = my_ioctl,
.llseek = my_llseek,
};
3.4 cdev 结构体:内核设备对象
cdev 是内核中表示字符设备的内部对象,必须在使用前分配并初始化,然后添加到内核设备链表中。
3.4.1 cdev 生命周期
3.4.2 标准初始化代码
struct cdev my_cdev;
dev_t dev_num;
/* 1. 分配设备号 */
alloc_chrdev_region(&dev_num, 0, 1, "my_device");
/* 2. 初始化 cdev */
cdev_init(&my_cdev, &my_fops);
my_cdev.owner = THIS_MODULE;
/* 3. 注册 cdev */
int ret = cdev_add(&my_cdev, dev_num, 1);
if (ret) {
unregister_chrdev_region(dev_num, 1);
return ret;
}
3.4.3 卸载时清理
cdev_del(&my_cdev);
unregister_chrdev_region(dev_num, 1);
顺序不可颠倒:必须先
cdev_del() 再 unregister_chrdev_region()。如果先注销设备号,cdev 仍在内核链表中,后续访问会导致内核崩溃。
3.5 设备节点的创建
设备节点是用户空间访问驱动的“文件入口”,位于 /dev 目录下。创建方式分为手动和自动两种。
3.5.1 手动创建:mknod
适用于开发调试阶段,需要已知主次设备号:
# 查看已注册的设备号
cat /proc/devices
# 手动创建设备节点
sudo mknod /dev/my_device c 240 0
sudo chmod 666 /dev/my_device
| 参数 | 含义 | 示例值 |
|---|---|---|
c |
字符设备类型 | c |
240 |
主设备号 | 从 /proc/devices 获取 |
0 |
次设备号 | 0 ~ 255 |
3.5.2 自动创建:udev + class
生产环境必须使用自动创建,依赖内核的 device class 机制和用户空间的 udev 守护进程。
3.5.3 自动创建代码实现
static struct class *my_class;
/* 在 probe 或 init 中 */
my_class = class_create(THIS_MODULE, "my_device_class");
if (IS_ERR(my_class)) {
unregister_chrdev_region(dev_num, 1);
return PTR_ERR(my_class);
}
device_create(my_class, NULL, dev_num, NULL, "my_device%d", 0);
/* 在 remove 或 exit 中 */
device_destroy(my_class, dev_num);
class_destroy(my_class);
调试技巧:使用
udevadm monitor --kernel --property 可以实时查看内核发送的 uevent,确认自动创建流程是否正常。
3.6 完整驱动框架模板
将以上所有要素整合,形成字符设备驱动的标准骨架:
#include <linux/module.h>
#include <linux/fs.h>
#include <linux/cdev.h>
#include <linux/device.h>
static dev_t dev_num;
static struct cdev my_cdev;
static struct class *my_class;
static int my_open(struct inode *inode, struct file *filp) {
pr_info("Device opened\n");
return 0;
}
static int my_release(struct inode *inode, struct file *filp) {
pr_info("Device closed\n");
return 0;
}
static ssize_t my_read(struct file *filp, char __user *buf,
size_t len, loff_t *off) {
pr_info("Read %zu bytes\n", len);
return 0;
}
static ssize_t my_write(struct file *filp, const char __user *buf,
size_t len, loff_t *off) {
pr_info("Write %zu bytes\n", len);
return len;
}
static struct file_operations my_fops = {
.owner = THIS_MODULE,
.open = my_open,
.release = my_release,
.read = my_read,
.write = my_write,
};
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) {
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);
}
device_create(my_class, NULL, dev_num, NULL, "my_device");
pr_info("my_device loaded: major=%d minor=%d\n",
MAJOR(dev_num), MINOR(dev_num));
return 0;
}
static void __exit my_exit(void) {
device_destroy(my_class, dev_num);
class_destroy(my_class);
cdev_del(&my_cdev);
unregister_chrdev_region(dev_num, 1);
pr_info("my_device unloaded\n");
}
module_init(my_init);
module_exit(my_exit);
MODULE_LICENSE("GPL");
MODULE_AUTHOR("Driver Expert");
MODULE_DESCRIPTION("Character device driver template");
3.7 常见错误与排查
| 错误现象 | 可能原因 | 解决方案 |
|---|---|---|
| insmod 返回 -EBUSY | 设备号已被占用 | 使用动态分配或卸载冲突驱动 |
| open /dev/my_device 返回 -ENOENT | 设备节点未创建或主设备号不匹配 | 检查 /proc/devices,重新 mknod |
| open 返回 -ENODEV | cdev_add 未调用或设备号错误 | 检查 cdev_add 返回值,确认 dev_t 正确 |
| rmmod 后 /dev 节点仍存在 | 未调用 device_destroy | 手动 rm /dev/my_device,修复 exit 函数 |
核心原则总结:
- 设备号注册 ↔ 注销:
alloc_chrdev_region↔unregister_chrdev_region - cdev 注册 ↔ 注销:
cdev_add↔cdev_del - 设备节点创建 ↔ 销毁:
device_create↔device_destroy - 卸载顺序:
device_destroy→class_destroy→cdev_del→unregister_chrdev_region