当前位置: 首页 > news >正文

【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");
http://www.dtcms.com/a/393259.html

相关文章:

  • python、数据结构
  • 数字孪生能做什么?(续)
  • C++指针:高效编程的核心钥匙
  • WIN11操作系统安装PL2303TA USB转串口驱动问题
  • ​​[硬件电路-280]:两相步进电机的功能、四个接口信号与工作原理详解(电能转化为机械能)
  • conda换源
  • 博客系统UI自动化测试报告
  • 大语言模型 LLM 通过 Excel 知识库 增强日志分析,根因分析能力的技术方案(6):vLLM 为什么能够成为企业级推理事实上的标准?
  • Redis最佳实践——秒杀系统设计详解
  • 数字孪生能做什么?
  • 每天学习一个统计检验方法--协方差分析 (ANCOVA)(以噩梦障碍中的心跳诱发电位研究为例)
  • 2025年CSP-J初赛真题及答案解析
  • OpenHarmony电量与LED灯颜色定制开发
  • OpenHarmony 显示Display驱动全栈解析:DisplayLayer + Gralloc + Gfx 三位一体,打造高性能图形底座
  • 诊断中的一些复位跳转
  • Python爬虫实战:临近双节,构建携程网最新特价机票数据采集与推荐系统
  • 容器主机名设置在云服务器多容器环境的配置流程
  • UE5 socket通信
  • 如何用kimi写一个最小pdf查看软件
  • DTS和PTS
  • 【开题答辩实录分享】以《“平安行”驾校信息管理系统的设计与实现》为例进行答辩实录分享
  • Modbus RTU/TCP转EtherNet/IP网关配置:西门子PLC控制伦茨变频器
  • GEO完全指南:AI时代内容优化的新范式
  • 02-安装DRF框架
  • 浅谈矩阵在机器学习线性回归算法中的数学推导
  • Linux 系统编程中的Redis
  • 【OpenGL】绘制彩色立方体
  • 21.继承与混入
  • Python 开发!ImprovePdf 用算法提升PDF清晰度,免费开源工具
  • P1879 [USACO06NOV] Corn Fields G-提高+/省选-