3、Linux内核驱动基础:字符设备驱动框架、模块加载与卸载、file_operations结构体
好,咱们今天聊聊字符设备驱动。这是Linux驱动开发里最基础、也是最核心的一块。说白了,你只要搞懂了字符设备驱动,后面再去看块设备、网络设备,思路都会清晰很多。
我个人习惯,学任何新东西,先看它长什么样,再拆开看里面有什么。字符设备驱动也不例外。我们先搭一个最简单的框架出来。
3.1 字符设备驱动的基本框架
字符设备,顾名思义,就是按字节流来读写数据的设备。键盘、鼠标、串口,这些都是典型的字符设备。驱动的作用,就是让用户空间的程序能通过标准的文件操作——open、read、write——来和硬件打交道。
一个字符设备驱动,核心就三件事:
- 注册设备号:告诉内核,我这个驱动管哪个设备。
- 实现操作函数:告诉内核,用户读写时该干什么。
- 注册设备:把设备和操作函数绑定起来。
嗯,这里要注意,设备号分主设备号和次设备号。主设备号标识驱动,次设备号标识具体的设备实例。我在项目中遇到过,两个驱动抢同一个主设备号,结果系统直接崩溃。所以设备号分配一定要小心。
3.2 模块加载与卸载
Linux驱动可以编译进内核,也可以编译成模块动态加载。我建议初学者先从模块开始,调试起来方便,不用每次改代码都重新编译整个内核。
模块的入口和出口,就两个宏:
#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/fs.h>
// 模块加载函数
static int __init my_driver_init(void)
{
printk(KERN_INFO "Hello, my driver loaded!\n");
// 这里注册设备号、注册设备
return 0;
}
// 模块卸载函数
static void __exit my_driver_exit(void)
{
printk(KERN_INFO "Goodbye, my driver unloaded!\n");
// 这里注销设备、释放资源
}
module_init(my_driver_init);
module_exit(my_driver_exit);
MODULE_LICENSE("GPL");
MODULE_AUTHOR("Your Name");
MODULE_DESCRIPTION("A simple char device driver");
你想想看,这个结构是不是很清晰?加载时调用init,卸载时调用exit。我曾经犯过一个低级错误——在init里申请了内存,exit里忘了释放。结果模块卸载后,内存泄漏了。嗯,从那以后我每次写exit,都会对照init,确保成对释放。
3.3 file_operations结构体
这个结构体,是字符设备驱动的灵魂。它定义了用户空间程序能对设备做哪些操作。说白了,它就是驱动和用户态之间的协议。
我们来看一个典型的file_operations定义:
static struct file_operations my_fops = {
.owner = THIS_MODULE,
.open = my_open,
.release = my_release,
.read = my_read,
.write = my_write,
.llseek = my_llseek,
};
每个成员都是一个函数指针。你实现了哪个,用户就能用哪个。比如你只实现了open和read,那用户就只能打开和读取,不能写。
我刚开始做驱动时,总觉得这些函数要写得很复杂。其实不然。很多设备只需要简单的读写操作。举个例子,一个温度传感器驱动,read函数就是从硬件寄存器里读温度值,然后拷贝到用户空间。就这么简单。
下面是一个简单的实现示例:
static int my_open(struct inode *inode, struct file *file)
{
printk(KERN_INFO "Device opened\n");
return 0;
}
static int my_release(struct inode *inode, struct file *file)
{
printk(KERN_INFO "Device closed\n");
return 0;
}
static ssize_t my_read(struct file *file, char __user *buf,
size_t len, loff_t *offset)
{
char data[] = "Hello from kernel!";
size_t datalen = strlen(data);
if (*offset >= datalen)
return 0; // 已经读到末尾了
if (len > datalen - *offset)
len = datalen - *offset;
if (copy_to_user(buf, data + *offset, len))
return -EFAULT;
*offset += len;
return len;
}
static ssize_t my_write(struct file *file, const char __user *buf,
size_t len, loff_t *offset)
{
char kbuf[100];
if (len > sizeof(kbuf) - 1)
len = sizeof(kbuf) - 1;
if (copy_from_user(kbuf, buf, len))
return -EFAULT;
kbuf[len] = '\0';
printk(KERN_INFO "Received from user: %s\n", kbuf);
return len;
}
3.4 完整的注册流程
有了模块框架和file_operations,接下来就是注册了。流程如下:
- 分配设备号(动态或静态)
- 初始化cdev结构体
- 将cdev添加到内核
- 创建设备节点(或者用udev自动创建)
代码示例:
#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 __init my_driver_init(void)
{
int ret;
// 1. 动态分配设备号
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. 初始化cdev
cdev_init(&my_cdev, &my_fops);
my_cdev.owner = THIS_MODULE;
// 3. 添加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;
}
// 4. 创建类,用于自动创建设备节点
my_class = class_create(THIS_MODULE, "my_class");
if (IS_ERR(my_class)) {
printk(KERN_ERR "Failed to create class\n");
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");
printk(KERN_INFO "My driver loaded successfully\n");
return 0;
}
static void __exit my_driver_exit(void)
{
device_destroy(my_class, dev_num);
class_destroy(my_class);
cdev_del(&my_cdev);
unregister_chrdev_region(dev_num, 1);
printk(KERN_INFO "My driver unloaded\n");
}
3.5 避坑指南
最后,分享几个我踩过的坑:
- 设备节点权限:默认创建的设备节点可能只有root能访问。记得在udev规则里设置权限,或者用chmod改一下。
- 并发访问:多个进程同时读写同一个设备,不加锁的话数据会乱。我早期一个项目,两个线程同时写,结果数据交叉错乱,查了两天才发现是没加互斥锁。
- 内存泄漏:每次open时分配的内存,一定要在release里释放。否则驱动跑几天,系统内存就被吃光了。
- 错误处理:每一步都要检查返回值。alloc_chrdev_region失败、cdev_add失败,都要回滚之前的所有操作。否则系统会留下半残的状态。
好了,字符设备驱动的基础框架就这些。你把这个框架练熟了,后面学什么驱动都顺手。下一章我们聊聊如何用ioctl来控制设备,那才是真正让驱动“活”起来的关键。