驱动开发硬核特训 · Day 16:字符设备驱动模型与实战注册流程
🎥 视频教程请关注 B 站:“嵌入式 Jerry”
一、为什么要学习字符设备驱动?
在 Linux 驱动开发中,字符设备(Character Device)驱动 是最基础也是最常见的一类驱动类型。很多设备(如 LED、按键、串口、传感器)都可以以字符设备方式呈现,并通过 read()
、write()
、ioctl()
与用户空间通信。
理解字符设备驱动,不仅是掌握 Linux 驱动开发的起点,也为日后深入 platform、总线设备驱动、misc、input 等模型打下坚实基础。
二、字符设备驱动的核心模型
字符设备驱动的注册模型由内核 include/linux/cdev.h
文件中的结构与接口决定:
2.1 struct cdev
是关键结构
struct cdev {struct kobject kobj;struct module *owner;const struct file_operations *ops;...
};
它是内核维护的代表“一个字符设备”的核心结构体。
2.2 file_operations
是接口描述
这是用户空间调用 open/read/write/ioctl 等系统调用时,最终调用的驱动函数集。
static const struct file_operations my_fops = {.owner = THIS_MODULE,.open = my_open,.read = my_read,.write = my_write,.release = my_release,
};
2.3 主设备号 & 次设备号
- 主设备号用于标识驱动程序
- 次设备号用于区分同一类驱动下的不同设备实例
三、字符设备注册的五个步骤
学习字符设备注册的流程,可以总结为五步:
步骤 1:分配设备号
dev_t devno;
alloc_chrdev_region(&devno, 0, 1, "mychardev");
devno
是设备号(类型为dev_t
),用MAJOR(devno)
和MINOR(devno)
分别提取主次设备号。alloc_chrdev_region()
会自动分配主设备号。
步骤 2:初始化 cdev 结构
struct cdev my_cdev;
cdev_init(&my_cdev, &my_fops);
my_cdev.owner = THIS_MODULE;
步骤 3:添加 cdev 到系统中
cdev_add(&my_cdev, devno, 1);
这一步使得字符设备被内核正式管理。
步骤 4:创建设备节点
配合 udev
或手动创建设备节点:
mknod /dev/mydev c <主设备号> <次设备号>
更推荐在内核中通过 class_create
+ device_create
来自动创建节点。
步骤 5:卸载时释放
device_destroy(my_class, devno);
class_destroy(my_class);
cdev_del(&my_cdev);
unregister_chrdev_region(devno, 1);
四、完整实战代码:LED 字符设备驱动
我们用一个模拟的 LED 驱动为例,实现一个字符设备:
4.1 驱动核心代码
#include <linux/init.h>
#include <linux/module.h>
#include <linux/fs.h>
#include <linux/cdev.h>
#include <linux/device.h>
#include <linux/uaccess.h>#define DEV_NAME "myled"static dev_t devno;
static struct cdev my_cdev;
static struct class *my_class;static int led_value = 0;static ssize_t led_read(struct file *filp, char __user *buf, size_t len, loff_t *off)
{return copy_to_user(buf, &led_value, sizeof(led_value)) ? -EFAULT : sizeof(led_value);
}static ssize_t led_write(struct file *filp, const char __user *buf, size_t len, loff_t *off)
{return copy_from_user(&led_value, buf, sizeof(led_value)) ? -EFAULT : sizeof(led_value);
}static int led_open(struct inode *inode, struct file *file)
{pr_info("LED device opened\n");return 0;
}static int led_release(struct inode *inode, struct file *file)
{pr_info("LED device closed\n");return 0;
}static struct file_operations led_fops = {.owner = THIS_MODULE,.read = led_read,.write = led_write,.open = led_open,.release = led_release,
};static int __init led_init(void)
{alloc_chrdev_region(&devno, 0, 1, DEV_NAME);cdev_init(&my_cdev, &led_fops);cdev_add(&my_cdev, devno, 1);my_class = class_create(THIS_MODULE, "myled_class");device_create(my_class, NULL, devno, NULL, DEV_NAME);pr_info("LED driver loaded: major=%d\n", MAJOR(devno));return 0;
}static void __exit led_exit(void)
{device_destroy(my_class, devno);class_destroy(my_class);cdev_del(&my_cdev);unregister_chrdev_region(devno, 1);pr_info("LED driver unloaded\n");
}module_init(led_init);
module_exit(led_exit);MODULE_LICENSE("GPL");
MODULE_AUTHOR("Jerry");
MODULE_DESCRIPTION("Simple LED Char Device Driver");
五、测试方法与验证
编译并加载
make
insmod myled.ko
查看设备节点
ls /dev/myled
读写测试
echo 1 > /dev/myled
cat /dev/myled
六、文件系统中的呈现:/proc 与 /sys
字符设备驱动在加载后,会自动出现在以下路径中:
路径 | 含义 |
---|---|
/proc/devices | 显示当前注册的主设备号及驱动名 |
/sys/class/myled_class/myled/ | 设备节点属性 |
/dev/myled | 用户空间访问入口 |
七、理论总结:字符设备模型的特点
- 与平台设备无绑定关系,适合用于最简设备建模。
- 所有用户空间 I/O 都通过
file_operations
映射函数实现。 - 可作为更复杂模型(如 misc、input、platform)的基础。
八、常见问题答疑
Q1:字符设备必须创建设备节点吗?
A:必须。否则无法从用户空间访问。可使用 mknod
或 device_create
自动生成。
Q2:file_operations 中哪些函数必须实现?
A:至少实现 open
、release
、read
和 write
。也可以只实现 ioctl
,视业务而定。
Q3:主设备号可以固定吗?
A:可以使用 register_chrdev()
指定固定主设备号,但建议使用动态分配。
九、结语与展望
字符设备模型虽是最基础的驱动类型,但它揭示了驱动与用户空间之间的交互机制,是理解 Linux 驱动架构的入口。后续我们将继续向上探索:
- misc 模型
- platform 总线匹配机制
- device tree 配合 platform 驱动
敬请期待《驱动开发硬核特训 · Day 17》!
📺 视频教程请关注 B 站:“嵌入式 Jerry”
如有问题,欢迎评论区留言交流,也欢迎点赞收藏,让我们一起成为 Linux 驱动高手!