3. 字符设备驱动框架:file_operations结构体、主次设备号、cdev注册与注销、设备节点创建
字符设备是Linux驱动开发中最基础、最常用的设备类型,其核心框架围绕设备号管理、cdev对象、file_operations操作集和设备节点四个要素展开。本章从零搭建字符设备驱动的骨架,理清用户空间open/read/write与内核驱动回调之间的完整链路。
核心要点:字符设备驱动的本质是“文件操作”的虚拟化——通过VFS层将用户对设备文件的系统调用,映射到驱动开发者注册的
file_operations回调函数。
3.1 字符设备驱动四要素
| 要素 | 数据结构/类型 | 作用 |
|---|---|---|
| 设备号 | dev_t(主设备号+次设备号) |
内核唯一标识设备类型与实例 |
| cdev对象 | struct cdev |
向内核注册字符设备的核心结构体 |
| 操作函数集 | struct file_operations |
定义设备支持的文件操作回调 |
| 设备节点 | /dev/xxx(文件系统入口) |
用户空间访问设备的文件接口 |
理解关键:四要素缺一不可。设备号是“身份证”,cdev是“登记表”,file_operations是“技能清单”,设备节点是“门牌号”。
3.2 主次设备号:设备的身份证
设备号由主设备号(major)和次设备号(minor)组成,类型为dev_t(32位无符号整数,高12位为主设备号,低20位为次设备号)。
- 主设备号:标识设备驱动类型(如硬盘、串口、GPIO)。
- 次设备号:标识同一驱动下的不同实例(如第1个串口、第2个串口)。
设备号操作宏
#include <linux/types.h> // dev_t
#include <linux/kdev_t.h> // MAJOR/MINOR/MKDEV
dev_t devno = MKDEV(250, 0); // 构造设备号:主=250,次=0
int major = MAJOR(devno); // 提取主设备号 → 250
int minor = MINOR(devno); // 提取次设备号 → 0
设备号分配方式
| 方式 | 函数 | 适用场景 |
|---|---|---|
| 静态分配 | register_chrdev_region() |
已知主设备号(避免冲突) |
| 动态分配 | alloc_chrdev_region() |
推荐方式,由内核自动分配未使用的主设备号 |
// 动态分配示例
dev_t devno;
int major, ret;
ret = alloc_chrdev_region(&devno, 0, 1, "my_char_dev");
if (ret < 0) {
printk(KERN_ERR "alloc_chrdev_region failed\n");
return ret;
}
major = MAJOR(devno);
printk("Allocated major number: %d\n", major);
避坑提示:静态分配前请检查
/proc/devices确认主设备号未被占用。动态分配是生产环境的首选,避免设备号冲突。
3.3 file_operations结构体:驱动与VFS的契约
struct file_operations定义了用户空间系统调用与内核驱动回调的映射关系。驱动只需实现需要的回调,未实现的回调置为NULL,内核将返回-ENOTTY或默认行为。
常用成员
| 成员 | 原型 | 说明 |
|---|---|---|
open |
int (*open)(struct inode *, struct file *) |
打开设备时调用,初始化硬件或分配资源 |
release |
int (*release)(struct inode *, struct file *) |
关闭设备时调用,释放资源 |
read |
ssize_t (*read)(struct file *, char __user *, size_t, loff_t *) |
从设备读取数据到用户空间 |
write |
ssize_t (*write)(struct file *, const char __user *, size_t, loff_t *) |
从用户空间写入数据到设备 |
unlocked_ioctl |
long (*unlocked_ioctl)(struct file *, unsigned int, unsigned long) |
设备控制命令(无大内核锁) |
llseek |
loff_t (*llseek)(struct file *, loff_t, int) |
修改文件读写位置 |
典型实现模板
static int my_open(struct inode *inode, struct file *filp) {
printk("Device opened\n");
return 0;
}
static int my_release(struct inode *inode, struct file *filp) {
printk("Device released\n");
return 0;
}
static ssize_t my_read(struct file *filp, char __user *buf,
size_t count, loff_t *f_pos) {
// 从内核空间拷贝数据到用户空间
// copy_to_user(buf, kernel_buf, count);
return count;
}
static ssize_t my_write(struct file *filp, const char __user *buf,
size_t count, loff_t *f_pos) {
// 从用户空间拷贝数据到内核空间
// copy_from_user(kernel_buf, buf, count);
return count;
}
static struct file_operations my_fops = {
.owner = THIS_MODULE,
.open = my_open,
.release = my_release,
.read = my_read,
.write = my_write,
};
关键约束:用户空间指针
__user不能直接解引用,必须使用copy_to_user/copy_from_user进行安全拷贝,否则会导致内核崩溃或安全漏洞。
3.4 cdev注册与注销:驱动入住内核
struct cdev是内核管理字符设备的核心对象,驱动必须完成cdev的初始化、添加和最终移除。
cdev操作流程
- 分配cdev对象:静态定义或
cdev_alloc()动态分配。 - 初始化cdev:
cdev_init(&cdev, &fops)绑定操作函数集。 - 添加cdev到内核:
cdev_add(&cdev, devno, count)使设备生效。 - 注销cdev:
cdev_del(&cdev)从内核移除。
#include <linux/cdev.h>
static struct cdev my_cdev;
static int __init my_init(void) {
dev_t devno;
int ret;
// 1. 分配设备号
ret = alloc_chrdev_region(&devno, 0, 1, "my_dev");
if (ret) return ret;
// 2. 初始化cdev
cdev_init(&my_cdev, &my_fops);
my_cdev.owner = THIS_MODULE;
// 3. 添加cdev到内核
ret = cdev_add(&my_cdev, devno, 1);
if (ret) {
unregister_chrdev_region(devno, 1);
return ret;
}
printk("Character device registered, major=%d\n", MAJOR(devno));
return 0;
}
static void __exit my_exit(void) {
dev_t devno = my_cdev.dev; // 从cdev中获取设备号
// 4. 注销cdev
cdev_del(&my_cdev);
// 5. 释放设备号
unregister_chrdev_region(devno, 1);
printk("Character device unregistered\n");
}
module_init(my_init);
module_exit(my_exit);
顺序不可颠倒:
cdev_del必须在unregister_chrdev_region之前调用,否则设备号释放后cdev仍可能被访问,导致内核崩溃。
3.5 设备节点创建:用户空间的入口
设备节点是/dev目录下的特殊文件,用户通过它访问设备。创建方式有两种:
| 方式 | 命令/机制 | 特点 |
|---|---|---|
| 手动创建 | mknod /dev/mydev c 250 0 |
调试阶段使用,不自动管理 |
| 自动创建 | udev/mdev + class_create/device_create | 生产环境推荐,热插拔自动生成 |
自动创建设备节点(推荐)
#include <linux/device.h>
static struct class *my_class;
static struct device *my_device;
static int __init my_init(void) {
// ... 设备号分配、cdev_add ...
// 创建设备类
my_class = class_create(THIS_MODULE, "my_class");
if (IS_ERR(my_class)) {
// 错误处理
}
// 创建设备节点 /dev/my_dev
my_device = device_create(my_class, NULL, devno, NULL, "my_dev");
if (IS_ERR(my_device)) {
class_destroy(my_class);
// 错误处理
}
return 0;
}
static void __exit my_exit(void) {
// 先销毁设备节点
device_destroy(my_class, devno);
// 再销毁设备类
class_destroy(my_class);
cdev_del(&my_cdev);
unregister_chrdev_region(devno, 1);
}
自动创建原理:
device_create会在sysfs中生成uevent,用户空间的udev监听到事件后,根据规则在/dev下创建对应的设备节点。
3.6 完整框架SVG架构图
3.7 完整驱动模板(汇总)
#include <linux/module.h>
#include <linux/fs.h>
#include <linux/cdev.h>
#include <linux/device.h>
#include <linux/uaccess.h>
#define DEVICE_NAME "my_demo"
#define CLASS_NAME "demo_class"
static int major;
static struct class *demo_class;
static struct device *demo_device;
static struct cdev demo_cdev;
static int demo_open(struct inode *inode, struct file *filp) {
printk("demo: open\n");
return 0;
}
static int demo_release(struct inode *inode, struct file *filp) {
printk("demo: release\n");
return 0;
}
static ssize_t demo_read(struct file *filp, char __user *buf,
size_t count, loff_t *f_pos) {
char kernel_buf[] = "Hello from kernel!\n";
size_t len = strlen(kernel_buf);
if (count < len) len = count;
if (copy_to_user(buf, kernel_buf, len))
return -EFAULT;
return len;
}
static ssize_t demo_write(struct file *filp, const char __user *buf,
size_t count, loff_t *f_pos) {
char kernel_buf[128];
if (count > sizeof(kernel_buf))
count = sizeof(kernel_buf);
if (copy_from_user(kernel_buf, buf, count))
return -EFAULT;
printk("demo: received %zu bytes\n", count);
return count;
}
static struct file_operations demo_fops = {
.owner = THIS_MODULE,
.open = demo_open,
.release = demo_release,
.read = demo_read,
.write = demo_write,
};
static int __init demo_init(void) {
dev_t devno;
int ret;
ret = alloc_chrdev_region(&devno, 0, 1, DEVICE_NAME);
if (ret) return ret;
major = MAJOR(devno);
cdev_init(&demo_cdev, &demo_fops);
demo_cdev.owner = THIS_MODULE;
ret = cdev_add(&demo_cdev, devno, 1);
if (ret) {
unregister_chrdev_region(devno, 1);
return ret;
}
demo_class = class_create(THIS_MODULE, CLASS_NAME);
if (IS_ERR(demo_class)) {
cdev_del(&demo_cdev);
unregister_chrdev_region(devno, 1);
return PTR_ERR(demo_class);
}
demo_device = device_create(demo_class, NULL, devno, NULL, DEVICE_NAME);
if (IS_ERR(demo_device)) {
class_destroy(demo_class);
cdev_del(&demo_cdev);
unregister_chrdev_region(devno, 1);
return PTR_ERR(demo_device);
}
printk("demo: loaded, major=%d\n", major);
return 0;
}
static void __exit demo_exit(void) {
dev_t devno = MKDEV(major, 0);
device_destroy(demo_class, devno);
class_destroy(demo_class);
cdev_del(&demo_cdev);
unregister_chrdev_region(devno, 1);
printk("demo: unloaded\n");
}
module_init(demo_init);
module_exit(demo_exit);
MODULE_LICENSE("GPL");
MODULE_AUTHOR("Linux Driver Expert");
MODULE_DESCRIPTION("Character device demo");
测试命令:
sudo insmod demo.ko — 加载驱动,自动创建/dev/my_democat /dev/my_demo — 读取设备,触发demo_readecho "test" > /dev/my_demo — 写入设备,触发demo_writesudo rmmod demo — 卸载驱动,自动删除设备节点
3.8 常见问题与避坑
| 问题 | 原因 | 解决方案 |
|---|---|---|
insmod成功但/dev无节点 |
未调用device_create或udev未运行 |
检查驱动中是否调用device_create;嵌入式环境需手动mknod |
open返回-ENODEV |
cdev_add失败或设备号冲突 | 检查cdev_add返回值,确认主设备号未被占用 |
read/write返回-EFAULT |
用户空间指针拷贝错误 | 确认使用copy_to_user/copy_from_user,检查缓冲区大小 |
| rmmod时系统崩溃 | 设备正在被使用或cdev_del顺序错误 | 确保先cdev_del再unregister_chrdev_region;检查是否有进程占用 |
调试利器:使用
cat /proc/devices查看已注册的主设备号;使用ls -l /dev/my_demo确认设备节点的主次设备号与驱动一致。