2. Linux字符设备驱动基础:cdev结构体、设备号注册、file_operations结构体、模块加载卸载模板
字符设备驱动是Linux驱动开发中最基础、最常用的驱动类型。本章将系统讲解字符设备驱动的核心数据结构、设备号管理机制、操作函数集以及标准的模块加载/卸载模板,为后续实现ioctl命令集奠定坚实的底层基础。
2.1 字符设备驱动核心数据结构
Linux内核使用 struct cdev 结构体来描述一个字符设备。每个字符设备在内核中对应一个cdev实例,该结构体包含了设备的主次设备号、操作函数集指针以及设备列表链接信息。
| 成员 | 类型 | 说明 |
|---|---|---|
kobj |
struct kobject |
内核对象基类,用于设备生命周期管理 |
owner |
struct module * |
指向所属模块的指针,通常设为 THIS_MODULE |
ops |
const struct file_operations * |
设备操作函数集指针,驱动核心接口 |
list |
struct list_head |
用于将cdev链接到系统设备链表 |
dev |
dev_t |
设备号(主设备号+次设备号) |
count |
unsigned int |
关联的次设备号数量 |
关键理解: cdev结构体是字符设备在内核中的"身份证",驱动开发者需要先分配并初始化cdev,然后将其注册到内核,用户空间才能通过设备文件访问该设备。
2.2 设备号注册与分配
设备号由主设备号(major)和次设备号(minor)组成,类型为 dev_t(32位无符号整数)。主设备号标识设备类型对应的驱动,次设备号标识同一驱动管理的不同设备实例。
2.2.1 设备号操作宏
| 宏/函数 | 功能 |
|---|---|
MAJOR(dev_t dev) |
从dev_t中提取主设备号 |
MINOR(dev_t dev) |
从dev_t中提取次设备号 |
MKDEV(int major, int minor) |
由主次设备号组合成dev_t |
register_chrdev_region() |
静态注册指定范围的设备号 |
alloc_chrdev_region() |
动态分配设备号(推荐方式) |
unregister_chrdev_region() |
注销已注册的设备号 |
2.2.2 设备号分配策略对比
| 策略 | 优点 | 缺点 | 适用场景 |
|---|---|---|---|
| 静态注册 | 设备号固定,便于用户空间预知 | 可能与其他驱动冲突,需申请官方编号 | 广泛使用的标准设备 |
| 动态分配 | 避免冲突,自动分配空闲设备号 | 设备号不固定,需通过/proc/devices查看 | 大多数自定义驱动 |
实践建议: 在开发阶段始终使用动态分配(
alloc_chrdev_region),避免与系统中已有驱动产生设备号冲突。产品定型后可根据需要切换为静态注册。
2.3 file_operations结构体详解
struct file_operations 是字符设备驱动的核心接口表,定义了用户空间通过系统调用操作设备文件时内核调用的驱动函数。每个成员对应一个系统调用或VFS操作。
struct file_operations {
struct module *owner;
loff_t (*llseek) (struct file *, loff_t, int);
ssize_t (*read) (struct file *, char __user *, size_t, loff_t *);
ssize_t (*write) (struct file *, const char __user *, size_t, loff_t *);
__poll_t (*poll) (struct file *, struct poll_table_struct *);
long (*unlocked_ioctl) (struct file *, unsigned int, unsigned long);
long (*compat_ioctl) (struct file *, unsigned int, unsigned long);
int (*mmap) (struct file *, struct vm_area_struct *);
int (*open) (struct inode *, struct file *);
int (*release) (struct inode *, struct file *);
int (*flush) (struct file *, fl_owner_t id);
int (*fsync) (struct file *, loff_t, loff_t, int);
/* ... 其他可选成员 */
};
| 成员 | 系统调用 | 驱动实现职责 |
|---|---|---|
open |
open() |
设备初始化、权限检查、私有数据设置 |
release |
close() |
资源释放、设备关闭清理 |
read |
read() |
从设备读取数据到用户空间 |
write |
write() |
从用户空间写入数据到设备 |
unlocked_ioctl |
ioctl() |
设备控制命令处理(ioctl核心) |
compat_ioctl |
ioctl() (32位兼容) |
64位内核处理32位应用程序ioctl |
llseek |
lseek() |
修改文件读写位置 |
mmap |
mmap() |
设备内存映射到用户空间 |
ioctl相关重点: 在2.6.36之后的内核中,
ioctl 成员已被移除,驱动必须实现 unlocked_ioctl。对于需要支持32位应用程序的64位驱动,还必须实现 compat_ioctl。
2.4 字符设备驱动完整框架SVG
2.5 模块加载/卸载标准模板
以下是一个完整的字符设备驱动模板,集成了cdev初始化、设备号动态分配、file_operations实现以及模块加载/卸载函数。
#include <linux/module.h>
#include <linux/fs.h>
#include <linux/cdev.h>
#include <linux/slab.h>
#include <linux/uaccess.h>
#define DEVICE_NAME "my_char_dev"
#define CLASS_NAME "my_class"
static int major;
static struct class *my_class = NULL;
static struct device *my_device = NULL;
/* 设备结构体:封装cdev和私有数据 */
struct my_device_data {
struct cdev cdev;
char buffer[256];
int data_len;
};
static struct my_device_data *dev_data;
/* open 实现 */
static int my_open(struct inode *inode, struct file *file)
{
struct my_device_data *data;
data = container_of(inode->i_cdev, struct my_device_data, cdev);
file->private_data = data;
return 0;
}
/* release 实现 */
static int my_release(struct inode *inode, struct file *file)
{
return 0;
}
/* read 实现 */
static ssize_t my_read(struct file *file, char __user *buf,
size_t count, loff_t *ppos)
{
struct my_device_data *data = file->private_data;
size_t available = data->data_len - *ppos;
size_t to_read = min(count, available);
if (to_read == 0)
return 0;
if (copy_to_user(buf, data->buffer + *ppos, to_read))
return -EFAULT;
*ppos += to_read;
return to_read;
}
/* write 实现 */
static ssize_t my_write(struct file *file, const char __user *buf,
size_t count, loff_t *ppos)
{
struct my_device_data *data = file->private_data;
size_t to_write = min(count, sizeof(data->buffer));
if (copy_from_user(data->buffer, buf, to_write))
return -EFAULT;
data->data_len = to_write;
*ppos = 0;
return to_write;
}
/* unlocked_ioctl 实现(后续章节重点展开) */
static long my_ioctl(struct file *file, unsigned int cmd,
unsigned long arg)
{
/* ioctl命令处理将在后续章节详细实现 */
return -ENOTTY;
}
/* file_operations 结构体实例化 */
static const struct file_operations my_fops = {
.owner = THIS_MODULE,
.open = my_open,
.release = my_release,
.read = my_read,
.write = my_write,
.unlocked_ioctl = my_ioctl,
};
/* 模块初始化函数 */
static int __init my_driver_init(void)
{
dev_t dev_num;
int ret;
/* 1. 动态分配设备号 */
ret = alloc_chrdev_region(&dev_num, 0, 1, DEVICE_NAME);
if (ret < 0) {
pr_err("Failed to allocate device number\n");
return ret;
}
major = MAJOR(dev_num);
pr_info("Allocated major number: %d\n", major);
/* 2. 分配设备数据结构 */
dev_data = kzalloc(sizeof(struct my_device_data), GFP_KERNEL);
if (!dev_data) {
unregister_chrdev_region(dev_num, 1);
return -ENOMEM;
}
/* 3. 初始化cdev */
cdev_init(&dev_data->cdev, &my_fops);
dev_data->cdev.owner = THIS_MODULE;
/* 4. 添加cdev到内核 */
ret = cdev_add(&dev_data->cdev, dev_num, 1);
if (ret < 0) {
pr_err("Failed to add cdev\n");
kfree(dev_data);
unregister_chrdev_region(dev_num, 1);
return ret;
}
/* 5. 创建设备类和设备节点(自动生成/dev/下设备文件) */
my_class = class_create(THIS_MODULE, CLASS_NAME);
if (IS_ERR(my_class)) {
cdev_del(&dev_data->cdev);
kfree(dev_data);
unregister_chrdev_region(dev_num, 1);
return PTR_ERR(my_class);
}
my_device = device_create(my_class, NULL, dev_num,
NULL, DEVICE_NAME);
if (IS_ERR(my_device)) {
class_destroy(my_class);
cdev_del(&dev_data->cdev);
kfree(dev_data);
unregister_chrdev_region(dev_num, 1);
return PTR_ERR(my_device);
}
pr_info("Character device driver loaded successfully\n");
return 0;
}
/* 模块退出函数 */
static void __exit my_driver_exit(void)
{
dev_t dev_num = MKDEV(major, 0);
/* 按注册逆序释放资源 */
device_destroy(my_class, dev_num);
class_destroy(my_class);
cdev_del(&dev_data->cdev);
kfree(dev_data);
unregister_chrdev_region(dev_num, 1);
pr_info("Character device driver unloaded\n");
}
module_init(my_driver_init);
module_exit(my_driver_exit);
MODULE_LICENSE("GPL");
MODULE_AUTHOR("Linux Driver Expert");
MODULE_DESCRIPTION("Character device driver template with ioctl support");
MODULE_VERSION("1.0");
2.6 模块加载/卸载流程SVG
2.7 关键避坑指南
- cdev_add 失败处理: 必须在cdev_add之后才能认为设备已注册成功,之前的任何失败都需要清理已分配的设备号。
- container_of 宏: 在open函数中通过
container_of(inode->i_cdev, struct my_device_data, cdev)获取设备私有数据结构,这是字符设备驱动的标准做法。 - copy_to_user / copy_from_user: 内核空间与用户空间数据交换必须使用这两个函数,禁止直接解引用用户空间指针。
- 模块计数:
file_operations的owner字段必须设为THIS_MODULE,防止设备正在使用时模块被卸载。 - 设备节点自动创建: 使用
class_create+device_create组合可以在/dev/下自动生成设备文件,无需手动mknod。
模板使用说明: 上述模板是字符设备驱动的"骨架",后续章节将在此基础上填充ioctl命令处理逻辑、添加锁机制、实现阻塞I/O等高级特性。建议读者将此模板作为所有字符设备驱动的起点。
2.8 本章小结
| 知识点 | 核心要点 |
|---|---|
| cdev结构体 | 字符设备内核描述符,包含设备号、操作函数集、所有者 |
| 设备号管理 | 动态分配优先,使用alloc_chrdev_region避免冲突 |
| file_operations | 驱动与VFS的接口表,unlocked_ioctl替代了旧版ioctl |
| 模块模板 | 加载按序分配资源,卸载逆序释放,错误处理必须完整 |
| 设备节点 | class_create + device_create 实现自动创建设备文件 |