【Linux驱动】Linux字符设备框架
【Linux驱动】Linux字符设备框架
文章目录
- 【Linux驱动】Linux字符设备框架
- 一、字符设备基础概念
- 二、Linux 字符设备框架核心结构
- (1) 设备号:驱动与设备文件的 “绑定钥匙”
- (2)struct file_operations:文件操作与硬件操作的 “桥梁”
- (3)struct cdev:字符设备的 “抽象代表”
- 三、字符设备驱动的完整实现流程
在 Linux 驱动开发中,字符设备(Character Device)是最常见的一类设备类型。它们通过字符流的方式和用户空间进行数据交互,比如串口、I2C、SPI、GPIO 控制接口等等。本文将带大家梳理 Linux 字符设备框架的整体流程。
一、字符设备基础概念
Linux 内核把设备分为三大类:
字符设备(Character Device)
按字节(字符流)顺序读写,例如串口 /dev/ttyS0。
块设备(Block Device)
按块读写,支持随机访问,例如磁盘 /dev/sda。
网络设备(Network Device)
通过套接字通信,例如 eth0。
在 Linux 系统中,“一切皆文件” 是核心设计理念,字符设备也不例外 —— 它会被抽象成一个设备文件,应用层通过 open()、read()、write()、close() 等标准文件 API 与驱动层交互。
而字符设备的本质,是指数据传输以 “字符流” 形式进行,且通常不具备缓存能力的设备。比如:
字符终端(/dev/tty)串口设备(/dev/ttyS*)简单外设(LED、按键、蜂鸣器)
这类设备的驱动开发,都围绕 Linux 内核提供的 “字符设备框架” 展开,核心是实现 “文件操作与硬件操作的映射”。
二、Linux 字符设备框架核心结构
Linux 内核为字符设备驱动提供了一套标准化的结构体和函数接口,开发者无需从零构建驱动逻辑,只需基于框架填充硬件相关的实现。核心结构主要有 3 个:
struct cdev
struct file_operations
设备号
(1) 设备号:驱动与设备文件的 “绑定钥匙”
设备号是字符设备的唯一标识,内核通过设备号区分不同的字符设备,实现 “应用层调用设备文件时,能找到对应驱动”。
设备号由两部分组成,共 32 位:
主设备号(12 位):标识驱动类型(同一类设备共用一个主设备号,如串口驱动主设备号为 4);
次设备号(20 位):标识同一驱动下的不同设备(如/dev/ttyS0和/dev/ttyS1,主设备号都是 4, 次设备号分别为 0 和 1)。
设备号的关键操作函数:
int alloc_chrdev_region(dev_t *dev, unsigned int firstminor, \unsigned int count, const char *name);
1.动态申请设备号(推荐,避免主设备号冲突):
dev: 输出参数,存储申请到的设备号;
firstminor: 起始次设备号(通常设为 0);
count: 申请的设备数量;
name: 设备名称(会显示在/proc/devices中)
2.静态注册设备号(需提前确认主设备号未被占用):
int register_chrdev_region(dev_t dev, unsigned int count, const char *name);
dev: 手动指定的设备号(通过MKDEV(主设备号, 次设备号)生成)。
3.释放设备号(驱动卸载时必须调用):
void unregister_chrdev_region(dev_t dev, unsigned int count);
(2)struct file_operations:文件操作与硬件操作的 “桥梁”
struct file_operations是字符设备驱动的 “核心回调函数集合”,它定义了应用层调用read()、write()等 API 时,内核会执行的驱动层函数。
开发者的核心工作之一,就是实现该结构体中与硬件相关的回调函数。
常用回调函数解析:
struct file_operations {// 打开设备(如初始化硬件、申请资源)int (*open)(struct inode *inode, struct file *file);// 关闭设备(如释放资源、恢复硬件默认状态)int (*release)(struct inode *inode, struct file *file);// 读设备(从硬件读取数据到应用层)ssize_t (*read)(struct file *file, char __user *buf, size_t count, loff_t *pos);// 写设备(从应用层发送数据到硬件)ssize_t (*write)(struct file *file, const char __user *buf, size_t count, loff_t *pos);// 控制设备(如设置硬件参数,对应应用层ioctl())long (*unlocked_ioctl)(struct file *file, unsigned int cmd, unsigned long arg);// 设备文件对应的私有数据(通常用于存储设备状态、硬件地址等)const struct file_operations *owner;
};
关键注意点:
__user修饰符:表示buf是应用层地址,驱动层不能直接访问,必须通过copy_from_user()(读)或copy_to_user()(写)函数进行数据拷贝,避免内核空间与用户空间地址越界;
owner:通常设为THIS_MODULE,表示该结构体属于当前内核模块,防止模块在被使用时被卸载。
(3)struct cdev:字符设备的 “抽象代表”
struct cdev是内核用于描述字符设备的结构体,它将 “设备号” “file_operations” “设备私有数据” 关联在一起,是字符设备在 kernel 中的 “身份证”。
struct cdev 的核心操作:
1.初始化 cdev:将file_operations与cdev绑定;
void cdev_init(struct cdev *cdev, const struct file_operations *fops);
2.添加 cdev 到内核:将 cdev 与设备号关联,并注册到内核字符设备链表中(此时内核才能识别该设备);
int cdev_add(struct cdev *cdev, dev_t dev, unsigned int count);
3.从内核移除 cdev(驱动卸载时调用):
void cdev_del(struct cdev *cdev);
三、字符设备驱动的完整实现流程
基于上述核心结构,字符设备驱动的开发流程可总结为 “5 步走”,我们结合简化代码示例(以 LED 驱动为例)来讲解。
步骤 1:定义驱动核心数据结构
通常会自定义一个结构体,存储设备的私有数据(如设备号、cdev、硬件寄存器地址等):
// 自定义设备结构体(存储设备私有数据)
struct led_dev {dev_t dev_num; // 设备号struct cdev cdev; // cdev结构体struct class *class; // 设备类(用于自动创建设备文件)struct device *dev; // 设备(用于自动创建设备文件)void __iomem *reg_base; // LED寄存器映射地址(硬件相关)
};// 定义全局设备结构体(实际开发中可动态分配)
struct led_dev led_device;
步骤 2:实现 file_operations 回调函数
以open()、write()、release()为例(硬件逻辑需根据实际芯片手册调整):
// 打开设备:映射LED寄存器地址
static int led_open(struct inode *inode, struct file *file) {// 将自定义设备结构体挂载到file->private_data,方便后续回调函数访问file->private_data = &led_device;// 映射LED寄存器物理地址到内核虚拟地址(假设物理地址为0x12340000)led_device.reg_base = ioremap(0x12340000, 4); // 4字节地址空间if (led_device.reg_base == NULL) {return -ENOMEM;}return 0;
}// 写设备:控制LED亮灭(应用层传入1亮,0灭)
static ssize_t led_write(struct file *file, const char __user *buf, size_t count, loff_t *pos) {struct led_dev *dev = file->private_data;char val;// 从应用层拷贝数据到内核层if (copy_from_user(&val, buf, 1) != 0) {return -EFAULT;}// 操作硬件寄存器(假设bit0控制LED:1亮,0灭)if (val == 1) {writel(readl(dev->reg_base) | (1 << 0), dev->reg_base); // 置1} else {writel(readl(dev->reg_base) & ~(1 << 0), dev->reg_base); // 清0}return count; // 返回实际写入的字节数
}// 关闭设备:取消寄存器映射
static int led_release(struct inode *inode, struct file *file) {struct led_dev *dev = file->private_data;iounmap(dev->reg_base); // 取消地址映射return 0;
}// 初始化file_operations结构体
static const struct file_operations led_fops = {.owner = THIS_MODULE,.open = led_open,.write = led_write,.release = led_release,
};
步骤 3:驱动入口函数(insmod 时执行)
完成设备号申请、cdev 初始化与注册、设备文件自动创建(需借助class机制):
static int __init led_driver_init(void) {int ret;// 1. 动态申请设备号ret = alloc_chrdev_region(&led_device.dev_num, 0, 1, "led_dev");if (ret < 0) {printk(KERN_ERR "alloc_chrdev_region failed!\n");return ret;}// 2. 初始化cdev并绑定file_operationscdev_init(&led_device.cdev, &led_fops);led_device.cdev.owner = THIS_MODULE;// 3. 将cdev添加到内核ret = cdev_add(&led_device.cdev, led_device.dev_num, 1);if (ret < 0) {printk(KERN_ERR "cdev_add failed!\n");goto err_unregister; // 出错时释放已申请的设备号}// 4. 创建设备类(会在/sys/class下生成led_class目录)led_device.class = class_create(THIS_MODULE, "led_class");if (IS_ERR(led_device.class)) {ret = PTR_ERR(led_device.class);printk(KERN_ERR "class_create failed!\n");goto err_cdev_del; // 出错时删除cdev}// 5. 创建设备文件(会在/dev下生成led_dev设备文件)led_device.dev = device_create(led_device.class, NULL, led_device.dev_num, NULL, "led_dev");if (IS_ERR(led_device.dev)) {ret = PTR_ERR(led_device.dev);printk(KERN_ERR "device_create failed!\n");goto err_class_destroy; // 出错时销毁设备类}printk(KERN_INFO "led driver init success!\n");return 0;// 错误处理(逆向释放资源)
err_class_destroy:class_destroy(led_device.class);
err_cdev_del:cdev_del(&led_device.cdev);
err_unregister:unregister_chrdev_region(led_device.dev_num, 1);return ret;
}
步骤 4:驱动出口函数(rmmod 时执行)
释放所有申请的资源(设备文件、设备类、cdev、设备号):
static void __exit led_driver_exit(void) {// 1. 销毁设备文件device_destroy(led_device.class, led_device.dev_num);// 2. 销毁设备类class_destroy(led_device.class);// 3. 删除cdevcdev_del(&led_device.cdev);// 4. 释放设备号unregister_chrdev_region(led_device.dev_num, 1);printk(KERN_INFO "led driver exit success!\n");
}// 注册入口和出口函数
module_init(led_driver_init);
module_exit(led_driver_exit);// 模块许可证(必须添加,否则内核会报“tainted”)
MODULE_LICENSE("GPL");
MODULE_DESCRIPTION("Simple LED Character Device Driver");
MODULE_AUTHOR("CSDN_LinuxDriver");
步骤 5:编译与测试
1.编写 Makefile(需指定内核源码路径):
obj-m += led_driver.o
KERNELDIR ?= /lib/modules/$(shell uname -r)/build
PWD := $(shell pwd)default:$(MAKE) -C $(KERNELDIR) M=$(PWD) modules
clean:$(MAKE) -C $(KERNELDIR) M=$(PWD) clean
2.编译驱动:执行make生成led_driver.ko模块文件。
3. 加载驱动:sudo insmod led_driver.ko,通过ls /dev/led_dev可看到设备文件,cat /proc/devices可看到设备名称和主设备号。
4.应用层测试:编写简单的 C 程序或直接用echo控制 LED:
# 点亮LED(向/dev/led_dev写入1)
echo 1 > /dev/led_dev
# 熄灭LED(向/dev/led_dev写入0)
echo 0 > /dev/led_dev
完整代码:
#include <linux/module.h> // 内核模块基础头文件(module_init/exit等)
#include <linux/fs.h> // 文件操作相关头文件(struct file_operations等)
#include <linux/cdev.h> // 字符设备相关头文件(struct cdev等)
#include <linux/device.h> // 设备类/设备创建头文件(class_create等)
#include <linux/io.h> // 地址映射头文件(ioremap/iounmap等)
#include <linux/uaccess.h> // 用户空间数据拷贝头文件(copy_from_user等)// -------------------------- 1. 自定义设备结构体 --------------------------
// 存储设备私有数据:设备号、cdev、设备类、寄存器映射地址等
struct led_dev {dev_t dev_num; // 设备号(主+次设备号)struct cdev cdev; // 字符设备核心结构体struct class *class; // 设备类(用于自动创建设备文件)struct device *dev; // 具体设备实例void __iomem *reg_base; // LED寄存器物理地址映射后的虚拟地址
};// 定义全局设备结构体(实际开发中可动态分配,此处简化为全局变量)
struct led_dev led_device;// -------------------------- 2. file_operations回调函数实现 --------------------------
// 打开设备:初始化硬件(地址映射)、绑定私有数据
static int led_open(struct inode *inode, struct file *file)
{// 将自定义设备结构体挂载到file->private_data,供后续回调函数访问file->private_data = &led_device;// 映射LED控制寄存器的物理地址到内核虚拟地址// 注意:实际硬件需替换为手册中的真实物理地址(此处示例为0x12340000)led_device.reg_base = ioremap(0x12340000, 4); // 4字节地址空间(单个控制寄存器)if (led_device.reg_base == NULL) {printk(KERN_ERR "led_open: ioremap failed!\n");return -ENOMEM; // 内存分配失败错误码}printk(KERN_INFO "led_open: device opened successfully\n");return 0;
}// 写设备:接收应用层数据,控制LED亮灭
static ssize_t led_write(struct file *file, const char __user *buf, size_t count, loff_t *pos)
{struct led_dev *dev = file->private_data; // 从file中获取设备私有数据char val; // 存储从用户空间拷贝的数据(1=亮,0=灭)// 从用户空间拷贝1字节数据到内核空间(用户空间地址不可直接访问)if (copy_from_user(&val, buf, 1) != 0) {printk(KERN_ERR "led_write: copy_from_user failed!\n");return -EFAULT; // 地址错误错误码}// 操作硬件寄存器:控制LED亮灭(假设寄存器bit0为LED控制位)if (val == '1') { // 应用层传入字符'1'(如echo "1" > /dev/led_dev)writel(readl(dev->reg_base) | (1 << 0), dev->reg_base); // 置bit0为1(点亮)printk(KERN_INFO "led_write: LED turned ON\n");} else if (val == '0') { // 应用层传入字符'0'writel(readl(dev->reg_base) & ~(1 << 0), dev->reg_base); // 清bit0为0(熄灭)printk(KERN_INFO "led_write: LED turned OFF\n");} else {printk(KERN_WARNING "led_write: invalid value (only '0'/'1' allowed)\n");return -EINVAL; // 无效参数错误码}return 1; // 返回实际写入的字节数(此处固定为1字节)
}// 关闭设备:释放资源(取消地址映射)
static int led_release(struct inode *inode, struct file *file)
{struct led_dev *dev = file->private_data;// 取消寄存器地址映射(与ioremap成对调用)iounmap(dev->reg_base);printk(KERN_INFO "led_release: device closed successfully\n");return 0;
}// 初始化file_operations结构体:绑定回调函数
static const struct file_operations led_fops = {.owner = THIS_MODULE, // 标识驱动所属模块,防止模块被意外卸载.open = led_open, // 绑定打开设备的回调函数.write = led_write, // 绑定写设备的回调函数.release = led_release, // 绑定关闭设备的回调函数
};// -------------------------- 3. 驱动入口函数(insmod时执行) --------------------------
static int __init led_driver_init(void)
{int ret = 0; // 存储函数返回值,用于错误处理// 步骤1:动态申请设备号(避免主设备号冲突,推荐使用)ret = alloc_chrdev_region(&led_device.dev_num, 0, 1, "led_dev");if (ret < 0) {printk(KERN_ERR "led_init: alloc_chrdev_region failed (ret=%d)\n", ret);goto err_alloc; // 出错跳转到资源释放逻辑}printk(KERN_INFO "led_init: allocated dev_num - major=%d, minor=%d\n",MAJOR(led_device.dev_num), MINOR(led_device.dev_num));// 步骤2:初始化cdev结构体,并绑定file_operationscdev_init(&led_device.cdev, &led_fops);led_device.cdev.owner = THIS_MODULE; // 设置cdev所属模块// 步骤3:将cdev添加到内核(内核自此识别该字符设备)ret = cdev_add(&led_device.cdev, led_device.dev_num, 1);if (ret < 0) {printk(KERN_ERR "led_init: cdev_add failed (ret=%d)\n", ret);goto err_cdev_add; // 出错跳转到释放设备号}// 步骤4:创建设备类(在/sys/class下生成"led_class"目录)led_device.class = class_create(THIS_MODULE, "led_class");if (IS_ERR(led_device.class)) { // 检查class_create是否成功(返回错误指针需转换)ret = PTR_ERR(led_device.class);printk(KERN_ERR "led_init: class_create failed (ret=%d)\n", ret);goto err_class_create; // 出错跳转到删除cdev}// 步骤5:创建设备文件(在/dev下生成"led_dev"设备文件,无需手动mknod)led_device.dev = device_create(led_device.class, NULL, led_device.dev_num, NULL, "led_dev");if (IS_ERR(led_device.dev)) {ret = PTR_ERR(led_device.dev);printk(KERN_ERR "led_init: device_create failed (ret=%d)\n", ret);goto err_device_create; // 出错跳转到销毁设备类}printk(KERN_INFO "led_init: driver initialized successfully\n");return 0; // 初始化成功// -------------------------- 错误处理(逆向释放已申请资源) --------------------------
err_device_create:class_destroy(led_device.class); // 销毁已创建的设备类
err_class_create:cdev_del(&led_device.cdev); // 从内核删除cdev
err_cdev_add:unregister_chrdev_region(led_device.dev_num, 1); // 释放已申请的设备号
err_alloc:printk(KERN_ERR "led_init: driver initialization failed\n");return ret; // 返回错误码
}// -------------------------- 4. 驱动出口函数(rmmod时执行) --------------------------
static void __exit led_driver_exit(void)
{// 逆向释放所有资源(与初始化顺序相反)device_destroy(led_device.class, led_device.dev_num); // 销毁设备文件class_destroy(led_device.class); // 销毁设备类cdev_del(&led_device.cdev); // 删除cdevunregister_chrdev_region(led_device.dev_num, 1); // 释放设备号printk(KERN_INFO "led_exit: driver unloaded successfully\n");
}// -------------------------- 5. 模块注册与许可证声明 --------------------------
module_init(led_driver_init); // 注册驱动入口函数
module_exit(led_driver_exit); // 注册驱动出口函数// 声明模块许可证(必须为GPL兼容协议,否则内核报"tainted"警告)
MODULE_LICENSE("GPL");
// 模块描述信息(modinfo命令可查看)
MODULE_DESCRIPTION("Simple LED Character Device Driver (Merged Version)");
// 模块作者信息
MODULE_AUTHOR("CSDN_LinuxDriver");
// 模块版本信息
MODULE_VERSION("V1.0");