Linux驱动开发笔记(一)字符驱动

文章目录

  • 前言
  • 一、字符设备驱动程序框架
  • 二、基本原理
    • 1. 设备号的申请与归还
    • 2. 保存file_operations接口
    • 3. 设备节点的创建和销毁
    • 4. 创建文件设备
      • 4.1 mknod
      • 4.2 init_special_incode( )函数
    • 5. 查找file_operation接口
    • 函数速查表
  • 三、程序编写
    • 1. 模块初始化及关闭
    • 2. 文件操作方式的实现


前言

  本文将通过字符驱动正式展开Linux驱动开发的学习。

一、字符设备驱动程序框架

  字符设备驱动是一种Linux驱动,用于支持以字符为单位进行I/O操作的设备,如串口、终端等。字符设备驱动的设计原理主要包括定义一个结构体,该结构体内部定义了一些设备的打开、关闭、读、写、控制函数;在结构体外分别实现这些函数;并向内核中注册或删除驱动模块。
  字符设备驱动框架主要涉及Linux软件系统的层次关系,包括应用程序与底层驱动程序之间的交互过程。我们从下面的思维导图来解读内核源码。
在这里插入图片描述

二、基本原理

1. 设备号的申请与归还

  Linux中提供了两种字符定义方式:第一种方式,就是我们常见的变量定义;第二种方式,是内核提供的动态分配方式,调用该函数之 后,会返回一个struct cdev类型的指针,用于描述字符设备,笔者这里习惯性使用第一种。

//第一种方式
static struct cdev chrdev;
//第二种方式
struct cdev *cdev_alloc(void);

  register_chrdev_region函数用于静态地为一个字符设备申请一个或多个设备编号。函数原型如下所示:

//保存新设备号到哈希表中防止冲突
int register_chrdev_region(dev_t from, unsigned count, const char *name)
  • 参数

    • from:dev_t类型的变量,用于指定字符设备的起始设备号,如果要注册的设备号已经被其他的设备注册了,那么就会导致注册失败。
    • count:指定要申请的设备号个数,count的值不可以太大,否则会与下一个主设备号重叠。
    • name:用于指定该设备的名称,我们可以在/proc/devices中看到该设备。
  • 返回值: 返回0表示申请成功,失败则返回错误码

  使用register_chrdev_region函数时,都需要去查阅内核源码的Documentation/devices.txt文件, 这就十分不方便。因此,内核又为我们提供了一种能够动态分配设备编号的方式:alloc_chrdev_region。调用该函数,内核会自动分配给我们一个尚未使用的主设备号。 我们可以通过命令“cat /proc/devices”查询内核分配的主设备号。

int alloc_chrdev_region(dev_t *dev, unsigned baseminor, unsigned count, const char *name)
  • 参数:

    • dev:指向dev_t类型数据的指针变量,用于存放分配到的设备编号的起始值;

    • baseminor:次设备号的起始值,通常情况下,设置为0;

    • count、name:同register_chrdev_region类型,用于指定需要分配的设备编号的个数以及设备的名称。

  • 返回值: 返回0表示申请成功,失败则返回错误码

  内核还提供了register_chrdev函数用于分配设备号。该函数是一个内联函数,它不仅支持静态申请设备号,也支持动态申请设备号,并将主设备号返回,函数原型如下所示。

static inline int register_chrdev(unsigned int major, const char *name,
const struct file_operations *fops)
{return __register_chrdev(major, 0, 256, name, fops);
}
  • 参数:

    • major:用于指定要申请的字符设备的主设备号,等价于register_chrdev_region函数,当设置为0时,内核会自动分配一个未使用的主设备号。
    • name:用于指定字符设备的名称
    • fops:用于操作该设备的函数接口指针。
  • 返回值: 主设备号

  使用register函数申请的设备号,则应该使用unregister_chrdev函数进行注销。我们从以上代码中可以看到,使用register_chrdev函数向内核申请设备号,同一类字符设备(即主设备号相同),会在内核中申请了256个,通常情况下,我们不需要用到这么多个设备,这就造成了极大的资源浪费。

//使用register函数申请的设备号,则应该使用unregister_chrdev函数进行注销。
static inline void unregister_chrdev(unsigned int major, const char *name)
{
__unregister_chrdev(major, 0, 256, name);
}
  • 参数:

    • major:指定需要释放的字符设备的主设备号,一般使用register_chrdev函数的返回值作为实参。

    • name:执行需要释放的字符设备的名称。

  • 返回值: 无

  当我们删除字符设备时候,我们需要把分配的设备编号交还给内核,对于使用register_chrdev_region函数以及alloc_chrdev_region函数分配得到的设备编号,可以使用unregister_chrdev_region函数实现该功能。

void unregister_chrdev_region(dev_t from, unsigned count)
  • 参数:

    • from:指定需要注销的字符设备的设备编号起始值,我们一般将定义的dev_t变量作为实参。

    • count:指定需要注销的字符设备编号的个数,该值应与申请函数的count值相等,通常采用宏定义进行管理。

  • 返回值: 无

2. 保存file_operations接口

  在完成第一步的设备号申请之后,我们要开始着手考虑如何利用file_operations这个结构体中来编写读写函数。那如何将该结构体与我们的字符设备结构体相关联呢?内核提供了cdev_init函数,来实现这个过程(cdev结构体在上节已经介绍过了)。

void cdev_init(struct cdev *cdev, const struct file_operations *fops)
{//将cdev中的所有值清零memset(cdev, 0, sizeof *cdev);//用于初始化这个 list_head 成员,使其成为一个空的链表头。INIT_LIST_HEAD(&cdev->list);//用于初始化一个已经分配好的 struct kobject 结构体,并将其与指定的类型、父对象以及名称关联起来。kobject_init(&cdev->kobj, &ktype_cdev_default);cdev->ops = fops;
}
  • 参数:
    • cdev:struct cdev类型的指针变量,指向需要关联的字符设备结构体;
    • fops:file_operations类型的结构体指针变量,一般将实现操作该设备的结构体file_operations结构体作为实参。
  • 返回值: 无

  cdev_add函数用于向内核的cdev_map散列表添加一个新的字符设备,如下所示:

int cdev_add(struct cdev *p, dev_t dev, unsigned count)
{int error;//赋值字符设备的设备号p->dev = dev;//赋值设备驱动程序控制的实际同类设备的数量p->count = count;error = kobj_map(cdev_map, dev, count, NULL,exact_match, exact_lock, p);if (error)return error;kobject_get(p->kobj.parent);return 0;
}
  • 参数:
    • p:struct cdev类型的指针,用于指定需要添加的字符设备;
    • dev:dev_t类型变量,用于指定设备的起始编号;
    • count:指定注册多少个设备。
  • 返回值: 错误码

  cdev_del函数用于从Linux内核系统中移除一个字符设备。当调用这个函数时,输入参数所代表的字符设备将从系统中注销,使其不可用。从系统中删除cdev,cdev设备将无法再打开,但任何已经打开的cdev将保持不变, 即使在cdev_del返回后,它们的FOP仍然可以调用。

//从内核中移除该字符设备
void cdev_del(struct cdev *p)
  • 参数(p): struct cdev类型的指针,用于指定需要删除的字符设备

  • 返回值: 无

  需要注意的是,cdev_del函数并不负责释放cdev结构体本身所占用的内存。如果cdev结构体是通过动态内存分配(如使用cdev_alloc函数)创建的,那么在调用cdev_del之后,还需要手动释放这块内存,以避免内存泄漏。

3. 设备节点的创建和销毁

  device_create是 Linux 内核中的一个函数,用于在内核中创建一个新的设备对象。这个函数是设备驱动开发中非常重要的一部分,它允许你将设备和其对应的类关联起来,并为设备提供一组属性和操作。

struct device *device_create(struct class *class, struct device *parent, dev_t devt, void *drvdata, const char *fmt, ...) 
  • 参数
    • class: 指向一个 class 结构体的指针,表示这个设备所属的类
    • parent: 指向父设备的指针,如果设备没有父设备,可以设置为 NULL
    • dev_t devt: 设备的设备号
    • drvdata: 要进行回调的数据
    • fmt: 指定设备的名称
  • 返回值:成功:新创建的 device 结构体的指针;失败:ERR_PTR( )

  device_destroy 函数的主要功能是销毁指定的设备对象,并将其从内核的设备模型中移除。当设备不再需要时(例如,驱动被卸载或设备被移除),应该调用这个函数来清理相关的资源。

void device_destroy(struct class *class, dev_t devt)
  • 参数
    • class:指向注册此设备的struct类的指针;
    • devt:以前注册的设备的开发;
  • 返回值: 无
  • 注意事项
    • 顺序性:通常,你应该在调用 device_destroy 之前,确保没有任何线程正在使用或引用这个设备对象。如果还有其他线程正在使用设备,销毁操作可能会导致未定义的行为或崩溃。
    • 引用计数:内核中的设备对象通常使用引用计数来管理其生命周期。在调用 device_destroy 之前,确保没有其他地方持有对该设备对象的引用是很重要的。否则,设备对象可能不会被正确销毁。
    • 错误处理:虽然 device_destroy 函数本身没有返回值来表示操作是否成功,但在调用它之前,你应该检查传入的参数是否有效,以确保不会传递无效的设备类或设备号。
    • 同步:如果你的驱动在多个线程或中断上下文中操作设备对象,你需要确保对 device_destroy 的调用是同步的,以避免竞态条件。

  注:除了使用代码创建设备节点,还可以使用mknod命令创建设备节点。

4. 创建文件设备

4.1 mknod

  mknod命令在Linux系统中用于创建特殊文件,包括字符设备文件和块设备文件。这些特殊文件为设备提供了接口,使得用户空间程序能够与内核空间的设备进行交互。mknod命令的语法为:

mknod [- 选项(可不选)] [文件名称] [类型] [主设备号] [次设备号]
  • 选项
    • Z:设置安全的上下文。
    • m:设置权限信息。
    • help:显示帮助信息。
    • version:显示版本信息。
      参数包括:
  • 文件名:要创建的设备文件名。
  • 类型:指定要创建的设备文件类型
    • c、u代表(无缓冲区)字符设备文件
    • b代表(有缓冲区)块设备文件
    • p代表FIFO型特殊文件
  • 主设备号:指定设备文件的主设备号,用于区分不同种类的设备。
  • 次设备号:指定设备文件的次设备号,用于区分同一类型的多个设备。

4.2 init_special_incode( )函数

  当我们使用上述命令,创建了一个字符设备文件时,实际上就是创建了一个设备节点inode结构体, 并且将该设备的设备编号记录在成员i_rdev,将成员f_op指针指向了字符设备通用的def_chr_fops结构体。

void init_special_inode(struct inode *inode, umode_t mode, dev_t rdev)
{inode->i_mode = mode;//是否是字符类型if (S_ISCHR(mode)) {inode->i_fop = &def_chr_fops;inode->i_rdev = rdev;} //是否是块类型else if (S_ISBLK(mode)) {inode->i_fop = &def_blk_fops;inode->i_rdev = rdev;} //是否是FIFO类型else if (S_ISFIFO(mode))inode->i_fop = &pipefifo_fops;else if (S_ISSOCK(mode));	/* leave it no_open_fops */elseprintk(KERN_DEBUG "init_special_inode: bogus i_mode (%o) for inode %s:%lu\n", mode, inode->i_sb->s_id,inode->i_ino);
}
EXPORT_SYMBOL(init_special_inode);

5. 查找file_operation接口

  用户空间使用open()系统调用函数打开一个字符设备时(int fd = open(“dev/xxx”, O_RDWR))大致有以下过程:

  1. 在虚拟文件系统VFS中的查找对应与字符设备对应 struct inode节点
  2. 遍历散列表cdev_map,根据inod节点中的 cdev_t设备号找到cdev对象
  3. 创建struct file对象(系统采用一个数组来管理一个进程中的多个被打开的设备,每个文件秒速符作为数组下标标识了一个设备对象)
  4. 初始化struct file对象,将 struct file对象中的 file_operations成员指向 struct cdev对象中的 file_operations成员(file->fops = cdev->fops)
  5. 回调file->fops->open函数
      下图中列出了open函数执行的大致过程:
    在这里插入图片描述

函数速查表

  看到这么多函数,笔者已经头晕了,以下是一个函数表,大家可以比对一下:

函数功能备注
register_chrdev_region分配设备号静态申请
alloc_chrdev_region分配未使用的设备号register_chrdev_region上位替代
unregister_chrdev_region注销设备号和register_chrdev_region函数及alloc_chrdev_region函数搭配使用
register_chrdev用于分配设备号函数是一个内联函数,支持静态/动态,并将主设备号返回,等价于register_chrdev_region函数
unregister_chrdev注销设备号注销register_chrdev函数申请的设备号
cdev_init关联file_operations
cdev_add添加一个新的字符设备
cdev_del移除一个字符设备
device_create创建一个新的设备对象
device_destroy销毁指定的设备对象

三、程序编写

  具体流程如下:

  • 内核模块入口获得相关寄存器并初始化
  • 构造file_operations接口并注册到内核
  • 创建设备文件,绑定file_operations接口
  • 应用程序获得文件句柄后,使用库函数提供的write或ioctl函数发出控制命令。

1. 模块初始化及关闭

#define DEV_NAME "EmbedCharDev"
#define DEV_CNT (1)
#define BUFF_SIZE 128
//定义字符设备的设备号
static dev_t devno;
//定义字符设备结构体chr_dev
static struct cdev chr_dev;
static int __init chrdev_init(void)
{int ret = 0;printk("chrdev init\n");//第一步//采用动态分配的方式,获取设备编号,次设备号为0,//设备名称为EmbedCharDev,可通过命令cat /proc/devices查看//DEV_CNT为1,当前只申请一个设备编号ret = alloc_chrdev_region(&devno, 0, DEV_CNT, DEV_NAME);if (ret < 0) {printk("fail to alloc devno\n");//当获取失败时,直接返回对应的错误码goto alloc_err;}//第二步//关联字符设备结构体cdev与文件操作结构体file_operationscdev_init(&chr_dev, &chr_dev_fops);//第三步//添加设备至cdev_map散列表中ret = cdev_add(&chr_dev, devno, DEV_CNT);if (ret < 0) {printk("fail to add cdev\n");//当添加设备失败的话,需要将申请的设备号注销掉goto add_err;}return 0;add_err://添加设备失败时,需要注销设备号unregister_chrdev_region(devno, DEV_CNT);alloc_err:return ret;}static void __exit chrdev_exit(void)
{printk("chrdev exit\n");unregister_chrdev_region(devno, DEV_CNT);cdev_del(&chr_dev);
}module_init(chrdev_init);
module_exit(chrdev_exit);

2. 文件操作方式的实现

  首先我们先来编写自己的open函数和release函数,考虑到字符设备确实是一种虚拟设备,所以不需要确切的回应,这里选择仅打印。

static int chr_dev_open(struct inode *inode, struct file *filp)
{printk("\nopen\n");return 0;
}static int chr_dev_release(struct inode *inode, struct file *filp)
{printk("\nrelease\n");return 0;
}

  下面,我们开始实现字符设备最重要的部分,即利用文件操作方式结构体file_operations实现write\read函数。当我们的应用程序调用write\read函数,实际上就是调用我们的chr_dev_write\chr_dev_read函数,代码如下(示例):

static ssize_t chr_dev_write(struct file *filp, const char __user * buf, size_t count, loff_t *ppos)
{unsigned long p = *ppos;int ret;int tmp = count ;if (p > BUFF_SIZE)return 0;if (tmp > BUFF_SIZE - p)tmp = BUFF_SIZE - p;ret = copy_from_user(vbuf, buf, tmp);*ppos += tmp;return tmp;
}static ssize_t chr_dev_read(struct file *filp, char __user * buf, size_t count, loff_t *ppos)
{unsigned long p = *ppos;int ret;int tmp = count ;if (p >= BUFF_SIZE)return 0;if (tmp > BUFF_SIZE - p)tmp = BUFF_SIZE - p;ret = copy_to_user(buf, vbuf+p, tmp);*ppos +=tmp;return tmp;
}

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/diannao/738.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

Pytorch 学习路程

目录 下载Pytorch 入门尝试 几种常见的Tensor Scalar Vector Matrix AutoGrad机制 线性回归尝试 使用hub模块 Pytorch是重要的人工智能深度学习框架。既然已经点进来&#xff0c;我们就详细的介绍一下啥是Pytorch PyTorch 希望将其代替 Numpy 来利用 GPUs 的威力&…

Python --- 在python中安装NumPy,SciPy,Matplotlib以及scikit-learn(Windows平台)

在python中安装NumPy&#xff0c;SciPy&#xff0c;Matplotlib以及scikit-learn(Windows平台) 本文是针对(像我一样的)python新用户所写的&#xff0c;刚刚在电脑上装好python之后&#xff0c;所需的一些常见/常用的python第三方库/软件包的快速安装指引。包括了这些常用安装包…

(mac)性能监控平台搭建JMeter+Grafana+Influxdb

【实现原理】 通过influxdb数据库存储jmeter的结果&#xff0c;再通过grafana采集influxdb数据库数据&#xff0c;完成监控平台展示 一、时间序列数据InfluxDB 1.InfluxDB下载安装 官网下载 https://portal.influxdata.com/downloads/ 官网最新版&#xff1a; &#xff0…

测试用例的编写评审

1、什么叫软件测试用例 什么是测试用例 测试用例(TestCase) 是为项目需求而编制的一组测试输入、执行条件 以及预期结果&#xff0c;以便测试某个程序是否满足客户需求。–测试依据 可以总结为:每一个测试点的数据设计和步骤设计。–测试用例 2、测试用例的重要性(了解) 2.1…

x-cmd mod | x whisper - 使用 whisper.cpp 进行本地 AI 语音识别

介绍 Whisper 模块通过 whisper.cpp 帮助用户快速将音频转换为文字。 INFO: whisper.cpp 是一个用 C/C 编写的轻量级智能语音识别库&#xff0c;是基于 OpenAI 的 Whisper 模型的移植版本&#xff0c;旨在通过深度学习模型实现音频转文字功能。 由于 whisper.cpp 目前只支持 1…

记录一下flume中因为taildir_position.json因位置不对导致数据无法从kafka被采到hdfs上的问题

【背景说明】 我需要用flume将kafka上的数据采集到hdfs上&#xff0c;发现数据怎么到不了hdfs。 【问题排查】 1.kafka上已有相应的数据 2.我的flume配置文档&#xff08;没问题&#xff09;&#xff0c; 3.时间拦截器&#xff08;没问题&#xff09;&#xff0c; 4.JSONObje…

《运营之光》3.0 读书笔记

&#x1f604;作者简介&#xff1a; 小曾同学.com,一个致力于测试开发的博主⛽️&#xff0c;主要职责&#xff1a;测试开发、CI/CD 如果文章知识点有错误的地方&#xff0c;还请大家指正&#xff0c;让我们一起学习&#xff0c;一起进步。 &#x1f60a; 座右铭&#xff1a;不…

HarmonyOS开发案例:【首选项】

介绍 本篇Codelab是基于HarmonyOS的首选项能力实现的一个简单示例。实现如下功能&#xff1a; 创建首选项数据文件。将用户输入的水果名称和数量&#xff0c;写入到首选项数据库。读取首选项数据库中的数据。删除首选项数据文件。 最终效果图如下&#xff1a; 相关概念 [首…

OpenHarmony鸿蒙南向开发案例:【智能门铃】

样例简介 智能门铃通过监控来访者信息&#xff0c;告诉主人门外是否有人按铃、有陌生人靠近或者无人状态。主人可以在数字管家中远程接收消息&#xff0c;并根据需要进行远程取消报警和一键开锁。同时&#xff0c;也可以通过室内屏幕获取门外状态。室内屏幕显示界面使用DevEco…

SQL增加主键约束的条件

结论 常见认为设为主键的条件为&#xff1a; 值唯一不含空值 具体实施中会出现各种问题 添加主键约束的条件细则&#xff1a; 值唯一数据中不含空值在定义时需要not null约束&#xff08;使用check约束不行&#xff09; 验证实验 接下来我做了关于这个细则的验证实验&am…

【MATLAB源码-第193期】基于matlab的网络覆盖率NOA优化算法仿真对比VFINOA,VFPSO,VFNGO,VFWOA等算法。

操作环境&#xff1a; MATLAB 2022a 1、算法描述 NOA&#xff08;Network Optimization Algorithm&#xff0c;网络优化算法&#xff09;是一个针对网络覆盖率优化的算法&#xff0c;它主要通过优化网络中节点的分布和配置来提高网络的整体覆盖性能。网络覆盖率是衡量一个无…

【学习】软件压力测试对软件产品的作用

在信息化高速发展的今天&#xff0c;软件产品已经成为各行各业不可或缺的一部分。然而&#xff0c;随着软件功能的日益复杂和用户需求的不断增长&#xff0c;软件产品的稳定性和可靠性问题也愈发凸显。在这样的背景下&#xff0c;软件压力测试作为软件质量保障的重要手段之一&a…

【项目亮点】大厂中分布式事务的最佳实践 问题产生->难点与权衡(偏爱Saga)->解决方案

【项目亮点】大厂中分布式事务的最佳实践 问题产生->难点与权衡->解决方案->底层实现->应用案例 不断有同学问我大厂中实践分布式事务的问题,这里从分布式事务的产生,到强弱一致性与性能的权衡,再到最终落地的解决方案,再到实际的代码实现,再到我工作中实际使用SA…

【C语言__动态内存管理__复习篇6】

目录 前言 一、动态内存管理 二、动态内存函数 2.1 malloc 2.2 free 2.3 calloc 2.4 realloc 三、动态内存常见的6个使用错误 3.1 接收malloc/calloc返回的参数后未及时检查是否为NULL 3.2 越界访问动态内存空间 3.3 对非动态开辟的内存使用free释放 3.4 使用free只释放了…

AI时代,我要如何学习,才能跟上步伐

在21世纪这个被数据驱动的时代&#xff0c;人工智能&#xff08;AI&#xff09;已经渗透到我们生活的方方面面。无论是智能手机中的语音助手、在线客服的聊天机器人&#xff0c;还是自动驾驶汽车&#xff0c;AI的应用都在告诉我们一个信息&#xff1a;未来已来。因此&#xff0…

1.微服务介绍

完整的微服务架构图 注册中心 配置中心 服务集群 服务网关 分布式缓存 分布式搜索 数据库集群 消息队列 分布式日志服务 系统监控链路追踪 Jenkins docker k8s 技术栈 微服务治理&#xff1a; 注册发现、远程调用、负载均衡、配置管理、网关路由、系统保护、流量…

企业单位IPTV数字电视直播与点播系统-中国卫通怀来地球站IPTV数字电视直播与点播系统应用浅析

企业单位IPTV数字电视直播与点播系统-中国卫通怀来地球站IPTV数字电视直播与点播系统应用浅析 由北京海特伟业科技有限公司任洪卓发布于2024年4月19日 一、运营商光猫接入企业/单位IPTV数字电视直播与点播系统建设概述 中国卫通怀来地球站&#xff0c;位于怀来县土木镇&#xf…

小球反弹(蓝桥杯)

文章目录 小球反弹【问题描述】答案&#xff1a;1100325199.77解题思路模拟 小球反弹 【问题描述】 有一长方形&#xff0c;长为 343720 单位长度&#xff0c;宽为 233333 单位长度。在其内部左上角顶点有一小球&#xff08;无视其体积&#xff09;&#xff0c;其初速度如图所…

CentOS 7静默安装Oracle 11g(记一次最小化CentOS 7安装Oracle 11g的经历)

# [pdf在线免费转word文档](https://orcc.online/pdf) https://orcc.online/pdf 1.最小化安装CentOS 7后首先设置一下固定IP 可以先查询一下自己的网卡设备的名称&#xff0c;是ens33&#xff0c;所以网卡配置文件名称就是ifcfg-ens33&#xff08;前面的ifcfg-不用管&#xf…

HCIP-Datacom-ARST必选题库_01_ACL【7道题】

一、单选 1.下面是一台路由器的部分配置,关于该配置描述正确的是&#xff1a; 源地址为1.1.1.1的数据包匹配第一条ACL语句rule 0,匹配规则为允许 源地址为1.1.1.3的数据包匹配第三条ACL语句rule 2,匹配规则为拒绝 源地址为1.1.1.4的数据包匹配第四条ACL语句rule 3,匹配规则为允…