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

【☀Linux驱动开发笔记☀】新字符设备驱动开发_02

新字符设备驱动

新字符设备驱动原理

首先是动态分配设备号dev_t

旧的驱动设备存在的问题

  1. 设备号是静态分配的,不能动态分配
  2. 设备号是固定的,不能动态改变
  3. 设备号是全局的,不能重复
  4. 设备号是静态的,不能动态添加或删除
  5. 而且十分浪费,比如说现在设led主设备号为200那么次设备号的区间就被浪费了。所以一个led只能有一个主设备号一个次设备号
    因此现在都是动态分配设备号
int major;//主设备号
int minor;//次设备号
dev_t devid;//设备号if(major == 0)
{alloc_chrdev_region(&devid,0,1,"test");//申请设备号//param:1.指向 dev_t 的指针,2.起始设备号这里从 0 开始,3.设备号数量,4.设备名major = MAJOR(devid);//获取主设备号minor = MINOR(devid);//获取次设备号
}else{devid = MKDEV(major,0);//大部分次设备号都是0号register_chrdev_region(devid,1,"test");//注册设备号
}

如果想要注销设备号

unregister_chrdev_region(devid,1);//注销设备号
//param:1.设备号,2.设备号数量

第二步是字符设备结构struct cdev

那么linux中的字符设备结构struct cdev是这样的

struct cdev{struct kobject kobj;//kobject是linux中的对象模型,这里用来表示字符设备struct module *owner;//模块指针const struct file_operations *ops;//文件操作结构体指针struct list_head list;//链表头dev_t dev;//设备号unsigned int count;//设备号数量
}

这个结构体当中我们要着重关注ops和dev这两个成员
ops是文件操作结构体指针,这里我们要实现自己的文件操作函数
dev是设备号,这里我们要注册自己的设备号,就是我们第一步在做的内容
首先定义一个cdev结构体变量

struct cdev test_cdev;

第三步是初始化cdev结构体

  1. 先定义一个struct file_operations
struct file_operations test_fops = {.owner = THIS_MODULE,.read = test_read,.write = test_write,.open = test_open,.release = test_release,
};
//param:1.模块指针,2.读取函数指针,3.写入函数指针,4.打开函数指针,5.关闭函数指针
  1. dev_init 函数对test_cdev结构体进行初始化重要的成员ops
test_cdev.owner = THIS_MODULE;//模块指针
cdev_init(&test_cdev,&test_fops);//初始化cdev结构体
//param:1.指向 cdev 结构体的指针,2.指向 file_operations 结构体的指针
  1. dev_add 函数对test_cdev结构体进行初始化重要的成员dev
test_cdev.dev = devid;//设备号
cdev_add(&test_cdev,devid,1);//添加cdev结构体到系统
//param:1.指向 cdev 结构体的指针,2.设备号,3.设备号数量

如果要卸载驱动就用dev_del函数

cdev_del(&test_cdev);//删除cdev结构体
//param:1.指向 cdev 结构体的指针

以上三步做完后就是配置好了字符设备的结构体,下一步就是自动创建设备节点

自动创建设备节点

mdev机制

udev是一个用户程序,linux通过udev来实现设备文件的创建与删除,udev可以检测系统中的硬件设备状态,当有新的硬件设备插入时,udev会自动创建对应的设备文件,当硬件设备移除时,udev会自动删除对应的设备文件。比说如当用modprobe命令加载模块的时候就会自动在**/dev目录**下创建对应的设备文件,当用modprobe -r命令卸载模块的时候就会自动删除对应的设备文件。
就是用于实现热拔插的一个用户程序
因此class_create/device_create 让 udev 自动在 /dev 下创建设备节点
为什么要创建这样一个设备文件
因为linux是一个多用户多任务的操作系统,每个用户都有自己的权限,而设备文件是用户访问硬件的接口,所以每个用户都需要有自己的设备文件,而不是所有用户都共享一个设备文件。

  • 不需要为每个用户单独创建设备文件。设备文件通常是系统级的一个节点,所有用户共享同一个 /dev/xxx 。
  • 多用户访问的权限控制依靠设备文件的属主/属组和权限位,或通过 udev 规则统一管理,而不是为每个用户建一份。

class/device 指内核设备模型中的两个对象: struct class 和 struct device 。它们把你的设备以标准化方式呈现在 /sys (sysfs)中,并向用户空间发送热插拔事件(uevent)。
用户空间的 udev 监听这些事件,根据设备信息和规则自动在 /dev 下创建设备节点、设置权限、建立符号链接、做持久命名等。
具体过程
创建类: class_create(THIS_MODULE, “test”) → 在 /sys/class/test/ 出现一个类目录。
创建设备: device_create(test_class, NULL, devid, NULL, “test”) → 在 /sys/class/test/test/ 出现设备对象,携带 devid (主/次设备号),并触发 uevent 。

第一步创建和删除类

这里的类是什么意思
它是对一类设备的逻辑分组,出现在 /sys/class/<类名>/ 下,便于用户态(如 udev )发现和管理设备。
与字符设备的 cdev 配合使用: cdev 让内核知道你有一个字符设备; class/device 让用户态看到它并创建 /dev/xxx 节点。!!!

自动船舰设备结点的工作是在驱动程序的入口函数中完成的,要在cdev_add函数后添加相关代码

struct class * test_class = class_create(THIS_MODULE,"test");//创建类
//param:1.模块指针,2.类名

如果要删除类就用class_destroy函数

class_destroy(test_class);//删除类
//param:1.类指针

第二步创建设备节点

要放在驱动入口函数static int __init led_init(void)中

struct device *device = device_create(test_class,NULL,devid,NULL,"test");//创建设备节点
//param:1.类指针,2.父设备指针,3.设备号,4.额外数据,5.设备名

如果要删除设备节点就用device_destroy函数

device_destroy(test_class,devid);//删除设备节点
//param:1.类指针,2.设备号

第三步设置文件私有数据

每一个设备都有一些属性比如主设备号(dev_t),类(class)、设备(device)、开关状态(state)等等,在编写驱动的时候你可以将这些属性全部写成变量的形式,我们最好将这些变量做成一个结构体。在编写驱动open函数的时候将设备结构体作为私有数据添加到设备文件中

/* 设备结构体 */struct test_dev{dev_t devid; /* 设备号 */struct cdev cdev; /* cdev */struct class *class; /* 类 */struct device *device; /* 设备 */int major; /* 主设备号 */int minor; /* 次设备号 */struct mutex lock;//可以添加一个互斥锁做并发处理char data[100];//内核缓冲区与有效数据长度size_t data_len;};struct test_dev testdev
//open函数
static int test_open(struct inode *inode,struct file *filp)
{filp->private_data = &testdev;//将设备结构体添加到设备文件中return 0;
}

open 是字符设备被用户态调用 open(“/dev/xxx”) 时,VFS 进入驱动的入口函数。
它是“每次打开”独有的,不同进程或多次 open 会得到不同的 filp ,这就支持了并发和多实例,可承载不同状态。
在open函数设置好私有数据以后,在write、read、close函数中就可以通过filp->private_data来获取设备结构体

总结一下上面两个大步骤

cdev_add(&cdev, devid, count) 是关键步骤:把 devid 映射到你的 file_operations ,也就是说,把设备号( dev_t ,含主/次设备号)注册到内核的字符设备表,并绑定到你的回调集合 struct file_operations。没有它打开节点也不会进入你的驱动。

device_create(test_class, NULL, devid, NULL, “test”) 把设备对象注册到 /sys/class/test/ 并携带设备号,触发节点创建与事件。有 devtmpfs/udev 时,这个设备对象会触发在 /dev 下生成节点(通常名为你传的 “test” 或被 udev 规则重命名),该节点的设备号也会是 249:0 。
在这里插入图片描述

这里可以看到在/sys/class/chrdevbase/目录下有一个chrdevbase目录,这个目录就是我们创建的类,在这个目录下有一个chrdevbase设备对象,这个设备对象就是我们创建的设备节点,它的设备号就是249:0(主设备号|次设备号)
然后在/dev目录下也有一个chrdevbase设备节点,这个节点的设备号也就是249:0。所以说这个设备节点就是我们创建的设备文件,用户可以通过这个设备文件来访问我们的设备。这个是udev看到sys里的类和设备对象,然后根据类和设备对象创建出设备节点。

访问路径

  • 用户打开 /dev/test → 读取节点的设备号 → 通过主设备号定位 cdev → 进入 fops->open/read/write/… , filp->private_data 拿到设备上下文。

对共享数据做一个并发的保护-添加一个互斥锁

在驱动的入口函数添加一个互斥锁的初始化

mutex_init(&testdev.lock);//初始化互斥锁

接下来就是对设备操作函数的编写

先是向设备read函数

/** @description		: 从设备读取数据 * @param - filp 	: 要打开的设备文件(文件描述符)* @param - buf 	: 返回给用户空间的数据缓冲区* @param - cnt 	: 要读取的数据长度* @param - offt 	: 相对于文件首地址的偏移* @return 			: 读取的字节数,如果为负值,表示读取失败*/
/** 读设备:根据用户请求长度与当前有效数据长度计算实际拷贝字节数* 注意:copy_to_user 返回的是“未拷贝字节数”,非 0 表示发生错误(-EFAULT)* 规范做法是返回实际成功拷贝的字节数,便于用户态依据返回值处理*/
static ssize_t chrdevbase_read(struct file *filp, char __user *buf, size_t cnt, loff_t *offt)
{struct chrdevbase_dev *dev = filp->private_data;size_t to_copy;int not_copied;mutex_lock(&dev->lock);//加锁保护共享数据to_copy = cnt;if (to_copy > dev->data_len)//如果用户请求长度大于有效数据长度to_copy = dev->data_len;//实际拷贝字节数为有效数据长度not_copied = copy_to_user(buf, dev->data, to_copy);//将数据从内核缓冲区拷贝到用户空间mutex_unlock(&dev->lock);//解锁保护共享数据if (not_copied)return -EFAULT;return to_copy;
}

介绍一下copy_to_user这个函数
copy_to_user函数用于将数据从内核缓冲区拷贝到用户空间,它的原型为

unsigned long copy_to_user(void __user *to, const void *from, unsigned long n);
//param:1.用户空间地址,2.内核空间地址,3.要拷贝的字节数
//return:返回未拷贝的字节数

向设备写write函数

/** @description		: 向设备写数据 * @param - filp 	: 设备文件,表示打开的文件描述符* @param - buf 	: 要写给设备写入的数据* @param - cnt 	: 要写入的数据长度* @param - offt 	: 相对于文件首地址的偏移* @return 			: 写入的字节数,如果为负值,表示写入失败*/
/** 写设备:限定拷贝长度避免溢出,并补 '\0' 便于打印* 注意:copy_from_user 返回“未拷贝字节数”,非 0 表示失败(-EFAULT)* 返回成功拷贝的字节数,便于用户态确认写入规模*/
static ssize_t chrdevbase_write(struct file *filp, const char __user *buf, size_t cnt, loff_t *offt)
{struct chrdevbase_dev *dev = filp->private_data;size_t to_copy;int not_copied;mutex_lock(&dev->lock);to_copy = cnt;if (to_copy > sizeof(dev->data) - 1)//如果用户请求长度大于内核缓冲区长度to_copy = sizeof(dev->data) - 1;//实际拷贝字节数为内核缓冲区长度减1not_copied = copy_from_user(dev->data, buf, to_copy);if (!not_copied) {dev->data[to_copy] = '\0';dev->data_len = to_copy;}mutex_unlock(&dev->lock);if (not_copied)return -EFAULT;printk("kernel recevdata:%s\r\n", dev->data);return to_copy;
}

介绍一下copy_from_user这个函数
copy_from_user函数用于将数据从用户空间拷贝到内核缓冲区,它的原型为

unsigned long copy_from_user(void *to, const void __user *from, unsigned long n);
//param:1.内核空间地址,2.用户空间地址,3.要拷贝的字节数
//return:返回未拷贝的字节数

最后是对驱动程序编写对应的应用测试程序

#include "stdio.h"
#include "unistd.h"//包含标准输入输出函数的头文件
#include "sys/types.h"//包含系统数据类型的头文件
#include "sys/stat.h"//包含文件状态结构体的头文件
#include "fcntl.h"//包含文件操作函数的头文件
#include "stdlib.h"
#include "string.h"
static char usrdata[] = {"usr data!"};/** @description		: main主程序* @param - argc 	: argv数组元素个数* @param - argv 	: 具体参数* @return 			: 0 成功;其他 失败*/
int main(int argc, char *argv[])
{int fd, retvalue;//文件描述符,返回值char *filename;//设备文件名char readbuf[100], writebuf[100];if(argc != 3){printf("Error Usage!\r\n");return -1;}filename = argv[1];//获取设备文件名/* 打开驱动文件 */fd  = open(filename, O_RDWR);//打开设备文件,读写模式if(fd < 0){printf("Can't open file %s\r\n", filename);return -1;}if(atoi(argv[2]) == 1){ /* 从驱动文件读取数据:按返回长度打印 */retvalue = read(fd, readbuf, 50);//从设备文件读取数据,最多读取50字节if(retvalue < 0){printf("read file %s failed!\r\n", filename);}else{if(retvalue > 0 && retvalue < (int)sizeof(readbuf)){readbuf[retvalue] = '\0';}printf("read(%d) data:%s\r\n", retvalue, readbuf);}}if(atoi(argv[2]) == 2){/* 向设备驱动写数据:按返回长度确认写入规模 */memcpy(writebuf, usrdata, sizeof(usrdata));retvalue = write(fd, writebuf, 50);//向设备文件写入数据,最多写入50字节if(retvalue < 0){printf("write file %s failed!\r\n", filename);}else{printf("write(%d) done\r\n", retvalue);}}/* 关闭设备 */retvalue = close(fd);//关闭设备文件if(retvalue < 0){printf("Can't close file %s\r\n", filename);return -1;}return 0;
}
http://www.dtcms.com/a/610521.html

相关文章:

  • Java-173 Neo4j + Spring Boot 实战:从 Driver 到 Repository 的整合与踩坑
  • 阳光保险网站wordpress phpwind
  • Android内核进阶之获取DMA地址snd_pcm_sgbuf_get_addr:用法实例(九十一)
  • 隔离地过孔要放哪里,才能最有效减少高速信号过孔串扰?
  • 鸿蒙应用开发从入门到实战(五):ArkUI概述
  • 广东大唐建设网站网站开发名片怎么做
  • 图片展示类网站wordpress模板在线编辑
  • 大模型面试题:请讲一下生成式语言模型的工作机理
  • OpenWebui 富文本提示词 远程命令注入漏洞 | CVE-2025-64495 复现研究
  • 黑马Python+AI大模型开发课程笔记(个人记录、仅供参考)
  • 安全的响应式网站建设半月报网站建设商务代表工作总结
  • 现在1做啥网站流量大上海网站制作网站制作公司
  • 如何做彩票网站域名查询入口
  • 学习react第四天
  • 宜宾百度网站建设武锡网站建设生购房政策
  • 领域驱动设计(DDD)与微服务架构的集成
  • windows中程序端口被占用解决步骤
  • DBeaver常用配置
  • 【ZeroRange WebRTC】Amazon Kinesis Video Streams WebRTC Control Plane API 深度解析
  • 网站域名续费多少钱珠海市企业网络推广
  • 电力系统暂态信号多尺度时频分析与卷积循环神经网络驱动的故障快速识别技术
  • 贵州建设公司网站868868域名查询
  • 建立网站链接结构的基本方式是模拟创建一个公司
  • 5-基于C5G 开发板的FPGA 串口通信设计 (FT232R, Altera UART IP和Nios II系统串口收发命令)
  • 手机视频网站怎么做宁夏石嘴山市城乡建设局提意见网站
  • 基于LLM 的 RAG 应用开发实战
  • 服务端开发案例(不定期更新)
  • 济宁网站建设培训班怎么提高网站加载速度慢
  • 简写单词
  • c2c模式的网站微网站在哪制作的