linux驱动-字符设备
目录一、概念核心特点典型应用场景与块设备的区别二、代码实现1、设备号1.1 组成主设备号 次设备号1.2 设备号两种分配方式静态分配、动态分配1.2.1 静态分配手动指定固定主设备号1.2.2 动态分配推荐内核自动分配空闲主号2、stuct cdev 字符设备核心结构体2.1 结构体原型内核源码简化版2.2 配套核心函数固定配对流程2.2.1 cdev_init2.2.2 cdev_add2.2.3. cdev_del2.3 标准使用流程3、创建设备容器函数作用4、创建单个设备实例触发自动生成 /dev 节点函数作用只调用 device_create 不先 class_create三、完整实现一、概念字符设备是Linux系统中一种以字符为单位进行数据传输的设备类型与块设备以固定大小的数据块为单位相对。字符设备通常用于需要逐字节或非结构化数据流传输的场景例如键盘、鼠标、串口、终端等。核心特点按字节访问数据以字符流形式传输不支持随机访问如直接跳转到指定位置。无缓存机制数据通常直接传输不经过系统缓冲区少数例外可通过设置实现。实时性高适用于对延迟敏感的设备如传感器或交互式输入设备。典型应用场景输入设备键盘、鼠标、触摸屏。输出设备串口终端、打印机。虚拟设备/dev/null、/dev/random等特殊文件。与块设备的区别特性字符设备块设备数据传输单位字节字符流固定大小的数据块如512B访问方式顺序访问支持随机访问缓存机制通常无缓存通常带缓存典型设备串口、终端硬盘、SSD二、代码实现1、设备号1.1 组成主设备号 次设备号内核中每个字符设备唯一标识 设备号 dev_tdev_t 是 32 位无符号整数高 12 位主设备号 major低 20 位次设备号 minor宏操作内核代码MAJOR(dev_t dev); // 提取主设备号 unsigned int MINOR(dev_t dev); // 提取次设备号 unsigned int MKDEV(maj, min); // 拼接主次生成dev_t dev_t1.2 设备号两种分配方式静态分配、动态分配1.2.1 静态分配手动指定固定主设备号核心api// 拼接主次号 dev_t MKDEV(unsigned int maj, unsigned int min); int register_chrdev_region(dev_t from, unsigned count, const char *name);参数说明from起始设备号用 MKDEV 拼接好count连续占用多少个次设备name驱动名存到 /proc/devices使用步骤自己选一个未被占用的主设备号查看/proc/devicesMKDEV(指定主号, 起始次号)生成 dev_t调用 register_chrdev_region 注册#define MY_MAJ 200 dev_t devno MKDEV(MY_MAJ, 0); // 占用次设备0共1个设备 register_chrdev_region(devno, 1, static_dev);释放接口void unregister_chrdev_region(dev_t from, unsigned count);优缺点优点设备号固定不用 mknod 每次换号缺点容易和系统已有驱动主设备号冲突加载失败嵌入式不推荐。1.2.2 动态分配推荐内核自动分配空闲主号核心 apiint alloc_chrdev_region(dev_t *dev, unsigned baseminor, unsigned count, const char *name);参数说明dev输出参数内核回填分配好的起始 dev_tbaseminor起始次设备号一般填 0count连续次设备数量name驱动名称。返回值成功 0失败负错误码。使用步骤dev_t devno; // 自动分配主设备次设备从0开始1个设备 int ret alloc_chrdev_region(devno, 0, 1, auto_dev); if(ret 0) return ret; // 提取打印主次号 printk(major:%d minor:%d, MAJOR(devno), MINOR(devno));释放同样用unregister_chrdev_region(devno, 1);优缺点优点不会冲突不用手动查空闲主设备号通用驱动首选缺点每次开机加载主设备号可能变化搭配device_create自动生成 /dev 节点可规避该问题。2、stuct cdev 字符设备核心结构体2.1 结构体原型内核源码简化版struct cdev { struct kobject kobj; // 内核对象sysfs 驱动管理 const struct file_operations *ops; // 绑定读写open/read/write接口 struct module *owner; // 所属模块 THIS_MODULE dev_t dev; // 该设备对应的完整设备号 unsigned int count; // 占用次设备数量 };2.2 配套核心函数固定配对流程2.2.1 cdev_initvoid cdev_init(struct cdev *cdev, const struct file_operations *fops);功能只做结构体初始化把file_operations函数集绑定到cdev-ops2.2.2 cdev_addint cdev_add(struct cdev *p, dev_t dev, unsigned count);功能把 cdev 注册进内核系统能识别该字符设备返回值成功返回 0失败负数错误码2.2.3. cdev_delvoid cdev_del(struct cdev *p);功能从内核注销 cdev2.3 标准使用流程// 1. 定义全局cdev对象 struct cdev mycdev; dev_t devno; // 初始化文件操作集 struct file_operations my_fops { .open dev_open, .read dev_read, .write dev_write, .release dev_release, }; static int __init drv_init(void) { // 分配设备号 alloc_chrdev_region(devno, 0, 1, mydev); // 2. 初始化cdev绑定fops cdev_init(mycdev, my_fops); mycdev.owner THIS_MODULE; // 标记所属模块防卸载崩溃 // 3. 注册cdev到内核 cdev_add(mycdev, devno, 1); return 0; } static void __exit drv_exit(void) { // 注销cdev cdev_del(mycdev); // 释放设备号 unregister_chrdev_region(devno, 1); }3、创建设备容器函数作用在/sys/class/下创建一个分类文件夹用来归类同一类型的设备生成struct class结构体是device_create必须依赖的父容器向内核设备模型注册设备分类为后续自动生成/dev节点做前置准备。版本区分// Linux 5.x / 6.2及更早 struct class *cls class_create(THIS_MODULE, my_class); // Linux 6.3 struct class *cls class_create(my_class);执行后生成目录/sys/class/my_class/销毁配对class_destroy(cls);4、创建单个设备实例触发自动生成 /dev 节点必须依赖 class_create 返回的 class 指针才能调用。函数作用在上面创建好的 class 分类下新建一个具体设备内核发送 uevent 事件给用户空间udev/mdevudev 收到消息后自动执行类似mknod /dev/xxx c 主号 次号生成设备文件在/sys/class/my_class/下生成对应设备的属性目录存放设备信息。struct device *dev device_create(cls, NULL, devno, NULL, mydev);执行后两处产物/dev/mydev应用程序操作的设备节点/sys/class/my_class/mydev/设备 sysfs 目录销毁配对device_destroy(cls, devno);只调用 device_create 不先 class_create编译直接报错缺少struct class参数无法运行。三、完整实现char_demo.c#include linux/init.h #include linux/module.h #include linux/cdev.h #include linux/fs.h #include linux/device.h #include linux/uaccess.h // 1. 自定义参数 #define DEV_MAJOR 230 #define DEV_MINOR 0 #define DEV_COUNT 1 #define DEV_NAME mychar // 2. 全局字符设备结构体 static struct cdev char_dev; static struct class *dev_class; static char buf[128] char device test data; // read用户层read()触发把内核数据拷贝到用户空间 static ssize_t char_read(struct file *file, char __user *ubuf, size_t size, loff_t *off) { int ret; // 如果偏移超过缓冲区总长度返回0cat读到0就停止循环 if (*off sizeof(buf)) return 0; // 计算本次能读多少剩余字节 和 用户传入size 取小值 size_t read_len min(size, sizeof(buf) - *off); ret copy_to_user(ubuf, buf *off, read_len); if (ret ! 0) return -EFAULT; // 关键更新文件偏移光标向后移动 *off read_len; return read_len; } // write用户层write()触发用户数据拷贝进内核 static ssize_t char_write(struct file *file, const char __user *ubuf, size_t size, loff_t *off) { int ret; ret copy_from_user(buf, ubuf, size); if(ret ! 0) return -EFAULT; return size; } static int char_open(struct inode *inode, struct file *file) { printk(KERN_INFO char dev open\n); return 0; } static int char_release(struct inode *inode, struct file *file) { printk(KERN_INFO char dev close\n); return 0; } // 绑定操作函数 static struct file_operations char_fops { .owner THIS_MODULE, .open char_open, .read char_read, .write char_write, .release char_release, }; // 模块加载入口 static int __init char_dev_init(void) { dev_t devno MKDEV(DEV_MAJOR, DEV_MINOR); int ret; // 1. 注册设备号 ret register_chrdev_region(devno, DEV_COUNT, DEV_NAME); if(ret 0){ printk(register dev fail\n); return ret; } // 2. 初始化cdev绑定操作集 cdev_init(char_dev, char_fops); // 3. 添加cdev到内核 ret cdev_add(char_dev, devno, DEV_COUNT); if(ret 0){ unregister_chrdev_region(devno, DEV_COUNT); return ret; } // 4. 创建/class 新版Linux6.x class_create 只传名字 dev_class class_create(char_class); if (IS_ERR(dev_class)) { ret PTR_ERR(dev_class); cdev_del(char_dev); unregister_chrdev_region(devno, DEV_COUNT); return ret; } // 5. 生成/dev设备节点 device_create(dev_class, NULL, devno, NULL, DEV_NAME); printk(char device init ok /dev/%s\n, DEV_NAME); return 0; } // 模块卸载入口 static void __exit char_dev_exit(void) { dev_t devno MKDEV(DEV_MAJOR, DEV_MINOR); // 销毁设备节点、类 device_destroy(dev_class, devno); class_destroy(dev_class); // 删除cdev、注销设备号 cdev_del(char_dev); unregister_chrdev_region(devno, DEV_COUNT); printk(char device exit\n); } module_init(char_dev_init); module_exit(char_dev_exit); MODULE_LICENSE(GPL);Makefileobj-m char_demo.o KERNELDIR ? /lib/modules/$(shell uname -r)/build PWD : $(shell pwd) all: $(MAKE) -C $(KERNELDIR) M$(PWD) modules clean: $(MAKE) -C $(KERNELDIR) M$(PWD) cleaninsmod char_demo.koroot1:/sys/class/char_class/mychar# lsdev power subsystem ueventroot1:/sys/class/char_class/mychar# cat ueventMAJOR230 主设备号MINOR0 次设备号DEVNAMEmychar 设备名root1:/dev# xxd mychar00000000: 6368 6172 2064 6576 6963 6520 7465 7374 char device test00000010: 2064 6174 6100 0000 0000 0000 0000 0000 data...........00000020: 0000 0000 0000 0000 0000 0000 0000 0000 ................00000030: 0000 0000 0000 0000 0000 0000 0000 0000 ................00000040: 0000 0000 0000 0000 0000 0000 0000 0000 ................00000050: 0000 0000 0000 0000 0000 0000 0000 0000 ................00000060: 0000 0000 0000 0000 0000 0000 0000 0000 ................00000070: 0000 0000 0000 0000 0000 0000 0000 0000 ................root1:/dev#

相关新闻