第2章:Linux字符设备驱动基础
字符设备驱动,说白了就是Linux世界里最简单、最基础的一类驱动。你想想看,键盘、鼠标、串口,这些设备都是按字节流来读写数据的,它们都属于字符设备。我刚开始学驱动开发时,就是从字符设备入手的。这一章,咱们就把字符设备驱动的几个核心概念彻底搞明白。
2.1 设备号:驱动的身份证
每个字符设备在Linux内核里都有一个唯一的编号,这就是设备号。设备号由两部分组成:主设备号和次设备号。
- 主设备号:标识设备对应的驱动程序。比如,所有串口设备可能共用同一个主设备号。
- 次设备号:标识同一个驱动管理下的不同设备实例。比如,你电脑上有两个串口,主设备号相同,次设备号不同。
在Linux内核中,设备号用dev_t类型表示,它是一个32位的无符号整数。其中,高12位是主设备号,低20位是次设备号。嗯,这里要注意,主设备号的范围是0到4095,次设备号的范围是0到1048575。
设备号操作宏
#include <linux/types.h>
MAJOR(dev_t dev); // 从设备号中提取主设备号
MINOR(dev_t dev); // 从设备号中提取次设备号
MKDEV(int major, int minor); // 将主次设备号组合成dev_t
我个人习惯在驱动初始化时,先用MKDEV生成设备号,然后再注册。这样代码看起来更清晰。
2.2 设备号的分配方式
设备号有两种分配方式:静态分配和动态分配。
| 分配方式 | 方法 | 优点 | 缺点 |
|---|---|---|---|
| 静态分配 | 手动指定主设备号 | 设备号固定,方便用户空间创建设备节点 | 可能与其他驱动冲突 |
| 动态分配 | 由内核自动分配 | 避免冲突,推荐使用 | 设备号不固定,需要查看/proc/devices |
我在项目中遇到过一个问题:两个驱动模块都静态分配了同一个主设备号,结果加载第二个模块时直接报错。从那以后,我基本都用动态分配了。动态分配的函数是alloc_chrdev_region,它会自动给你找一个没被占用的主设备号。
动态分配示例
dev_t dev_num;
int ret;
ret = alloc_chrdev_region(&dev_num, 0, 1, "my_device");
if (ret < 0) {
printk(KERN_ERR "Failed to allocate device number\n");
return ret;
}
printk(KERN_INFO "Allocated major=%d, minor=%d\n",
MAJOR(dev_num), MINOR(dev_num));
2.3 file_operations结构体:驱动的接口表
字符设备驱动和用户程序之间的桥梁,就是file_operations结构体。它里面定义了一堆函数指针,比如open、read、write、release等等。用户程序调用open()系统调用时,内核就会找到对应的驱动,然后调用你注册的open函数。
说白了,这个结构体就是驱动对外提供的服务清单。你实现了哪些函数,用户程序就能用哪些功能。
典型的file_operations定义
struct file_operations my_fops = {
.owner = THIS_MODULE,
.open = my_open,
.release = my_release,
.read = my_read,
.write = my_write,
.llseek = my_llseek,
};
这里有个细节要注意:.owner一定要赋值为THIS_MODULE,它用来防止模块在使用过程中被卸载。我曾经有一次忘了写这一行,结果模块在读写时被rmmod了,内核直接崩溃。嗯,血的教训。
2.4 cdev结构体:字符设备的抽象
在Linux内核里,每个字符设备都用cdev结构体来表示。你需要先初始化它,然后把它注册到内核里。
初始化cdev的步骤很简单:
- 用
cdev_alloc或静态定义的方式分配cdev结构体。 - 用
cdev_init初始化它,并关联file_operations。 - 用
cdev_add把它添加到内核中,同时指定设备号。
cdev注册示例
struct cdev my_cdev;
dev_t dev_num;
// 1. 初始化cdev
cdev_init(&my_cdev, &my_fops);
my_cdev.owner = THIS_MODULE;
// 2. 注册cdev
ret = cdev_add(&my_cdev, dev_num, 1);
if (ret < 0) {
printk(KERN_ERR "Failed to add cdev\n");
unregister_chrdev_region(dev_num, 1);
return ret;
}
注册成功后,用户程序就可以通过设备节点来访问你的驱动了。设备节点通常用mknod命令创建,比如:
mknod /dev/my_device c 250 0
这里的c表示字符设备,250是主设备号,0是次设备号。
2.5 cdev的注销
有注册就有注销。当模块卸载时,一定要把cdev从内核中移除,并释放设备号。否则,下次加载模块时可能会出问题。
注销顺序很重要
void cleanup_module(void)
{
// 1. 删除cdev
cdev_del(&my_cdev);
// 2. 释放设备号
unregister_chrdev_region(dev_num, 1);
printk(KERN_INFO "Module unloaded\n");
}
注意:一定要先cdev_del,再unregister_chrdev_region。顺序反了的话,设备号虽然释放了,但cdev还在内核里,可能会引起野指针问题。
2.6 模块加载与卸载
Linux驱动是以模块的形式存在的。模块的加载和卸载,对应两个宏:module_init和module_exit。
module_init(my_init):指定模块加载时调用的函数。module_exit(my_exit):指定模块卸载时调用的函数。
完整的模块框架大概是这样的:
完整的字符设备驱动模板
#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 int __init my_init(void)
{
int ret;
// 1. 动态分配设备号
ret = alloc_chrdev_region(&dev_num, 0, 1, "my_device");
if (ret < 0) {
printk(KERN_ERR "alloc_chrdev_region failed\n");
return ret;
}
printk(KERN_INFO "Major: %d, Minor: %d\n",
MAJOR(dev_num), MINOR(dev_num));
// 2. 初始化并注册cdev
cdev_init(&my_cdev, &my_fops);
my_cdev.owner = THIS_MODULE;
ret = cdev_add(&my_cdev, dev_num, 1);
if (ret < 0) {
printk(KERN_ERR "cdev_add failed\n");
unregister_chrdev_region(dev_num, 1);
return ret;
}
printk(KERN_INFO "Module loaded successfully\n");
return 0;
}
static void __exit my_exit(void)
{
cdev_del(&my_cdev);
unregister_chrdev_region(dev_num, 1);
printk(KERN_INFO "Module unloaded\n");
}
module_init(my_init);
module_exit(my_exit);
MODULE_LICENSE("GPL");
MODULE_AUTHOR("Your Name");
MODULE_DESCRIPTION("A simple character device driver");
这里有个小技巧:__init和__exit这两个宏,告诉内核这些函数只在初始化或卸载时使用。初始化完成后,__init函数占用的内存可以被回收。我在做嵌入式设备时,内存很紧张,这个特性帮了不少忙。
2.7 避坑指南
最后,分享几个我踩过的坑:
- 设备号冲突:静态分配时一定要查
/proc/devices,看看哪些主设备号已经被用了。我建议直接用动态分配,省心。 - cdev_add失败:如果
cdev_add返回错误,一定要记得释放之前分配的设备号。否则设备号就泄漏了。 - 模块卸载顺序:先
cdev_del,再unregister_chrdev_region。这个顺序我强调过,但每次写代码时还是容易搞反。 - file_operations的owner:别忘了赋值
.owner = THIS_MODULE。我曾经因为这个导致模块在读写时被卸载,内核直接Oops。
好了,这一章的内容就到这里。字符设备驱动的基础,说白了就是设备号、file_operations、cdev这三个东西。把它们搞明白了,后面的内容就好办了。下一章,咱们聊聊设备节点的自动创建,也就是udev和class机制。