一文秒懂|Linux字符设备驱动

img
我的圈子: 高级工程师聚集地
我是董哥,高级嵌入式软件开发工程师,从事嵌入式Linux驱动开发和系统开发,曾就职于世界500强公司!
创作理念:专注分享高质量嵌入式文章,让大家读有所得!
img

文章目录

  • 1、前言
  • 2、关键数据结构
    • 2.1 cdev
    • 2.2 file_operations
    • 2.3 dev_t
  • 3、数据结构之间关系
  • 4、字符设备驱动整体架构
    • 4.1 加载与卸载函数
    • 4.2 设备号管理
      • 4.2.1 设备号的概念
      • 4.2.2 设备号的分配
      • 4.2.3 设备号的注销
      • 4.2.4 设备号的获取
      • 4.2.4 通用代码实现
    • 4.3 字符设备的管理
      • 4.3.1、字符设备初始化
      • 4.3.2、字符设备注册
      • 4.3.3、字符设备注销
    • 4.4 文件操作接口的实现
  • 5、总结

image-20231123091238538

1、前言

众所周知,Linux内核主要包括三种驱动模型,字符设备驱动,块设备驱动以及网络设备驱动。

其中,Linux字符设备驱动,可以说是Linux驱动开发中最常见的一种驱动模型。

我们该系列文章,主要为了帮助大家快速入门Linux驱动开发,该篇主要来了解一些字符设备驱动的框架和机制。

系列文章基于Kernel 4.19

 

2、关键数据结构

2.1 cdev

struct cdev {
    struct kobject kobj;
    struct module *owner;
    const struct file_operations *ops;
    struct list_head list;
    dev_t dev;
    unsigned int count;
} __randomize_layout;

结构体名称cdev

文件位置include/linux/cdev.h

主要作用cdev可以理解为char device,用来抽象一个字符设备。

核心成员及含义

  • kobj:表示一个内核对象。
  • owner:指向该模块的指针
  • ops:指向文件操作的指针,包括openreadwrite等操作接口
  • list:用于将该设备加入到内核模块链表中
  • dev:设备号,由主设备号和次设备号构成
  • count:表示有多少个同类型设备,也间接表示设备号的范围
  • __randomize_layout:一个编译器指令,用于随机化结构体的布局,以增加安全性。

 

2.2 file_operations

struct file_operations {
    struct module *owner;
    loff_t (*llseek) (struct file *, loff_t, int);
    ssize_t (*read) (struct file *, char __user *, size_t, loff_t *);
    ssize_t (*write) (struct file *, const char __user *, size_t, loff_t *);
    ssize_t (*read_iter) (struct kiocb *, struct iov_iter *);
    ssize_t (*write_iter) (struct kiocb *, struct iov_iter *);
    int (*iterate) (struct file *, struct dir_context *);
    int (*iterate_shared) (struct file *, struct dir_context *);
    __poll_t (*poll) (struct file *, struct poll_table_struct *);
    long (*unlocked_ioctl) (struct file *, unsigned int, unsigned long);
    long (*compat_ioctl) (struct file *, unsigned int, unsigned long);
    int (*mmap) (struct file *, struct vm_area_struct *);
    unsigned long mmap_supported_flags;
    int (*open) (struct inode *, struct file *);
    int (*flush) (struct file *, fl_owner_t id);
    int (*release) (struct inode *, struct file *);
    int (*fsync) (struct file *, loff_t, loff_t, int datasync);
    int (*fasync) (int, struct file *, int);
    int (*lock) (struct file *, int, struct file_lock *);
    ssize_t (*sendpage) (struct file *, struct page *, int, size_t, loff_t *, int);
    unsigned long (*get_unmapped_area)(struct file *, unsigned long, unsigned long, unsigned long, unsigned long);
    int (*check_flags)(int);
    int (*flock) (struct file *, int, struct file_lock *);
    ssize_t (*splice_write)(struct pipe_inode_info *, struct file *, loff_t *, size_t, unsigned int);
    ssize_t (*splice_read)(struct file *, loff_t *, struct pipe_inode_info *, size_t, unsigned int);
    int (*setlease)(struct file *, long, struct file_lock **, void **);
    long (*fallocate)(struct file *file, int mode, loff_t offset,
              loff_t len);
    void (*show_fdinfo)(struct seq_file *m, struct file *f);
#ifndef CONFIG_MMU
    unsigned (*mmap_capabilities)(struct file *);
#endif
    ssize_t (*copy_file_range)(struct file *, loff_t, struct file *,
            loff_t, size_t, unsigned int);
    int (*clone_file_range)(struct file *, loff_t, struct file *, loff_t,
            u64);
    int (*dedupe_file_range)(struct file *, loff_t, struct file *, loff_t,
            u64);
    int (*fadvise)(struct file *, loff_t, loff_t, int);
} __randomize_layout;

结构体名称file_operations

文件位置include/linux/fs.h

主要作用:正如其名,主要用来描述文件操作的各种接口,Linux一切接文件的思想,内核想要操作哪个文件,都需要通过这些接口来实现。

核心成员及含义

  • open:打开文件的函数
  • read:读取文件的函数。
  • write:写入文件的函数。
  • release:关闭文件的函数。
  • flush:刷新文件的函数,通常在关闭文件时调用。
  • llseek:改变文件读写指针位置的函数。
  • fsync:将文件数据同步写入磁盘的函数。
  • poll:询问文件是否可被非阻塞读写

 

2.3 dev_t

typedef u32 __kernel_dev_t;

typedef __kernel_dev_t		dev_t;

类型名称dev_t

文件位置include/linux/types.h

主要作用:表示字符设备对应的设备号,其中包括主设备号和次设备号。

 

3、数据结构之间关系

image-20231123085448145

上图绘制是对字符设备驱动程序的数据结构以及API的关系图,

有需要原始文件,可在公~号【嵌入式艺术】获取。

 

4、字符设备驱动整体架构

4.1 加载与卸载函数

驱动首先实现的就是加载和卸载函数,也是驱动程序的入口函数。

我们一般这么定义驱动的加载卸载函数:

static int __init xxx_init(void)
{

}

static void __exit xxx_exit(void)
{
    
}

module_init(xxx_init);
module_exit(xxx_exit);

这段代码就是实现一个通用驱动的加载与卸载,关于module_initmodule_exit的实现机制,可以查看之前总结文章。

 

4.2 设备号管理

4.2.1 设备号的概念

每一类字符设备都有一个唯一的设备号,其中设备号又分为主设备号和次设备号,那么这两个分别作用是什么呢?

  • 主设备号:用于标识设备的类型,
  • 次设备号:用于区分同类型的不同设备

简单来说,主设备号用于区分是IIC设备还是SPI设备,而次设备号用于区分IIC设备下,具体哪一个设备,是MPU6050还是EEPROM

 

4.2.2 设备号的分配

了解了设备号的概念,Linux中设备号有那么多,那么我们该如何去使用正确的设备号呢?

设备号的分配方式有两种,一种是动态分配,另一种是静态分配,也可以理解为一种是内核自动分配,一种是手动分配。

静态分配函数

int register_chrdev_region(dev_t from, unsigned count, const char *name);
  • from:表示已知的一个设备号
  • count:表示连续设备编号的个数,(同类型的设备有多少个)
  • name:表示设备或者驱动的名称

函数作用:以from设备号开始,连续分配count个同类型的设备号

 

动态分配函数

int alloc_chrdev_region(dev_t *dev, unsigned baseminor, unsigned count, const char *name);
  • dev:设备号的指针,用于存放分配的设备号的值
  • baseminor:次设备号开始分配的起始值
  • count:表示连续设备编号的个数,(同类型的设备有多少个)
  • name:表示设备或者驱动的名称

函数作用:从baseminor次设备号开始,连续分配count个同类型的设备号,并自动分配一个主设备号,将主、次组成的设备号信息赋值给*dev

 

这两个函数最大的区别在于

  • register_chrdev_region:调用前,已预先定义好了主设备号和次设备号,调用该接口后,会将自定义的设备号登记加入子系统中,方便系统追踪系统设备号的使用情况。
  • alloc_chrdev_region:调用前,未定义主设备号和次设备号;调用后,主设备号以0来表示,以自动分配,并且将自动分配的设备号,同样加入到子系统中,方便系统追踪系统设备号的使用情况。

 

这两个函数的共同点在于

系统维护了一个数组列表,用来登记所有的已使用的设备号信息,这两个接口归根到底也是将其设备号信息,登记到系统维护的设备号列表中,以免后续冲突使用。

Linux中,我们可以通过cat /proc/devices命令,查看所有i登记的设备号列表。

 

后面有时间,我们可以详细聊设备号的自动分配机制,管理机制。

 

4.2.3 设备号的注销

设备号作为一种系统资源,当所对应的设备卸载时,当然也要将其所占用的设备号归还给系统,无论时静态分配,还是动态分配,最终都是调用下面函数来注销的。

void unregister_chrdev_region(dev_t from, unsigned count);
  • from:表示已知的一个设备号
  • count:表示连续设备编号的个数,(同类型的设备有多少个)

函数作用:要注销from主设备号下的连续count个设备

 

4.2.4 设备号的获取

设备号的管理很简单,在关键数据结构中,我们看到设备号的类型是dev_t,也就是u32类型表示的一个数值。

其中主设备号和次设备号的分界线,由MINORBITS宏定义指定:

#define MINORBITS	20

也就是主设备号占用高12bit,次设备号占用低20bit

并且,内核还提供了相关API接口,来获取主设备号和次设备号,以及生成设备号的接口,如下:

#define MINORMASK	((1U << MINORBITS) - 1)

#define MAJOR(dev)	((unsigned int) ((dev) >> MINORBITS))
#define MINOR(dev)	((unsigned int) ((dev) & MINORMASK))
#define MKDEV(ma,mi)	(((ma) << MINORBITS) | (mi))

以上,通过移位操作,来实现主次设备号的获取。

 

4.2.4 通用代码实现

#define CUSTOM_DEVICE_NUM 0
#define DEVICE_NUM 1
#device DEVICE_NAME "XXXXXX"
static dev_t global_custom_major = CUSTOM_DEVICE_NUM;

static int __init xxx_init(void)
{
    dev_t custom_device_number= MKDEV(global_custom_major, 0);	//	custom device number
    /* device number register*/
    if (global_custom_major) {
        ret = register_chrdev_region(custom_device_number, DEVICE_NUM, DEVICE_NAME);
    } else {
        ret = alloc_chrdev_region(&custom_device_number, 0, DEVICE_NUM, DEVICE_NAME);
        global_custom_major = MAJOR(custom_device_number);
    }
}

static void __exit xxx_exit(void)
{
    unregister_chrdev_region(MKDEV(global_mem_major, 0), DEVICE_NUM);
}

module_init(xxx_init);
module_exit(xxx_exit);

该函数实现了设备号的分配,如果主设备号为0,则采用动态配分的方式,否则采用静态分配的方式。

 

更多干货可见:高级工程师聚集地,助力大家更上一层楼!

 

4.3 字符设备的管理

了解完设备号的管理之后,我们来看下字符设备是如何管理的。

4.3.1、字符设备初始化

void cdev_init(struct cdev *cdev, const struct file_operations *fops);
  • cdev:一个字符设备对象,也就是我们创建好的字符设备
  • fops:该字符设备的文件处理接口

函数作用:初始化一个字符设备,并且将所对应的文件处理指针与字符设备绑定起来。

 

4.3.2、字符设备注册

int cdev_add(struct cdev *p, dev_t dev, unsigned count);
  • p:一个字符设备指针,只想待添加的字符设备对象
  • dev:该字符设备所负责的第一个设备编号
  • count:该类型设备的个数

函数作用:添加一个字符设备驱动到Linux系统中。

 

4.3.3、字符设备注销

void cdev_del(struct cdev *p);
  • p:指向字符设备对象的指针

函数作用:从系统中移除该字符设备驱动

 

4.4 文件操作接口的实现

因为在Linux中,一切皆文件的思想,所以每一个字符设备,也都有一个文件节点来对应。

我们在初始化字符设备的时候,会将struct file_operations的对象与字符设备进行绑定,其作用是来处理该字符设备的openreadwrite等操作。

我们要做的就是去实现我们需要的函数接口,如:

static const struct file_operations global_mem_fops = {
    .owner = THIS_MODULE,
    .llseek = global_mem_llseek,
    .read = global_mem_read,
    .write = global_mem_write,
    .unlocked_ioctl = global_mem_ioctl,
    .open = global_mem_open,
    .release = global_mem_release,
};

至此,我们一个基本的字符设备驱动程序的框架就基本了然于胸了

 

5、总结

本篇文章,旨在通俗易懂的讲解:

  • 字符设备驱动相关数据结构
  • 数据结构关系图
  • 核心API接口
  • 字符设备驱动整体框架

希望对大家有所帮助。

img
欢迎关注 公号&星球【嵌入式艺术】,董哥原创!

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

『C++成长记』构造函数和析构函数

&#x1f525;博客主页&#xff1a;小王又困了 &#x1f4da;系列专栏&#xff1a;C &#x1f31f;人之为学&#xff0c;不日近则日退 ❤️感谢大家点赞&#x1f44d;收藏⭐评论✍️ 目录 一、类的六个个默认成员函数 &#x1f4d2;1.1认识默认成员函数 二、构造函数 …

TA-Lib学习研究笔记(二)——Overlap Studies下

TA-Lib学习研究笔记&#xff08;二&#xff09;——Overlap Studies下 &#xff08;11&#xff09;SAR - Parabolic SAR 抛物线指标 函数名&#xff1a;SAR 名称&#xff1a; 抛物线指标 简介&#xff1a;抛物线转向也称停损点转向&#xff0c;是利用抛物线方式&#xff0c;随…

机器学习-回归问题(Regression)

前言 与KNN分类任务预测的输出为离散型不同. 在机器学习中&#xff0c;回归任务是用于预测连续数值型变量的任务。回归任务在很多领域都有着广泛的应用. 简单的线性回归 在一个回归问题中&#xff0c;很显然模型选择和好坏会直接关系到将来预测结果的接近程度.即寻找到一条直…

苹果输入法怎么换行?3个换行技巧,速速掌握!

在日常打字的时候&#xff0c;我们经常会遇到需要换行的情况。比如&#xff0c;在聊天、写作、编辑文档等场景下&#xff0c;当一行文字输入完成后&#xff0c;我们通常需要将光标移动到下一行再继续输入文字。那么这时候就需要我们进行换行操作。 然而&#xff0c;很多用户对…

Web安全漏洞分析-XSS(上)

随着互联网的迅猛发展&#xff0c;Web应用的普及程度也愈发广泛。然而&#xff0c;随之而来的是各种安全威胁的不断涌现&#xff0c;其中最为常见而危险的之一就是跨站脚本攻击&#xff08;Cross-Site Scripting&#xff0c;简称XSS&#xff09;。XSS攻击一直以来都是Web安全领…

HMM(Hidden Markov Model)详解——语音信号处理学习(三)(选修一)

参考文献&#xff1a; Speech Recognition (Option) - HMM哔哩哔哩bilibili 2020 年 3月 新番 李宏毅 人类语言处理 独家笔记 HMM - 6 - 知乎 (zhihu.com) 隐马尔可夫&#xff08;HMM)的解码问题维特比算法 - 知乎 (zhihu.com) 本次省略所有引用论文 目录 一、介绍 二、建模单…

MATLAB中imbothat函数用法

目录 语法 说明 示例 使用底帽和顶帽滤波增强对比度 imbothat函数的功能是对图像进行底帽滤波。 语法 J imbothat(I,SE) J imbothat(I,nhood) 说明 J imbothat(I,SE) 使用结构元素 SE 对灰度或二值图像 I 执行形态学底帽滤波。底帽滤波计算图像的形态学闭运算&#…

从零开始,轻松打造个人化Chrome浏览器插件

说在前面 在我日常开发以及娱乐生活中&#xff0c;浏览器是我使用频率较高的一个应用&#xff0c;当我大学拥有第一部电脑开始&#xff0c;之后不论电脑换成什么&#xff0c;以及使用的是什么系统&#xff0c;我的首选浏览器都是Chrome&#xff0c;不仅仅是因为其速度快&#x…

Redis中的缓存穿透、雪崩、击穿(详细)

目录 一、概念 1. 缓存穿透&#xff08;Cache Penetration&#xff09; 解决方案&#xff1a; 2. 缓存雪崩&#xff08;Cache Avalanche&#xff09; 解决方案&#xff1a; 3. 缓存击穿&#xff08;Cache Breakdown&#xff09; 解决方案&#xff1a; 二、三者出现的根本原…

dbeaver连接amabri-hbase

目录 尝试过程 解决之道 总结 尝试过程 注意此章节为记录试错过程&#xff0c;无需跟随操作&#xff0c;仅作试错记录。真正操作方法请看“解决之道”章节 环境ambari安装的hbase2.1.6 使用apche phoenix默认驱动配置 备注&#xff1a;Apache Phoenix 是一个开源的、基于…

Amazon CodeWhisperer 使用体验

文章作者&#xff1a;STRIVE Amazon CodeWhisperer 是最新的代码生成工具&#xff0c;支持多种编程语言&#xff0c;如 java,js,Python 等&#xff0c;能减少开发人员手敲代码时间&#xff0c;提升工作效率。PS:本人是一名 CodeWhisperer 业余爱好者 亚马逊云科技开发者社区为开…

文件中找TopK问题

目录 1.解题思路2.创建一个文件并在文件中写入数据3.为什么要建立小堆而不建立大堆&#xff1f;4.如何在现有的数据中建立适合的大堆&#xff1f;5.代码实现 1.解题思路 TopK问题即是在众多数据中找出前K大的值&#xff0c;则可以根据堆的性质来实现&#xff0c;但在使用堆之前…

R语言gWQS包在加权分位数和回归模型的应用

在流行病学研究中&#xff0c;相较于单一因素的暴露&#xff0c;多因素同时暴露的情况更为常见。传统模型在评价多因素联合暴露时存在数据维度高、多重共线性等问题. WQS 回归模型的基本原理是通过分位数间距及加权的方法&#xff0c;将多种研究因素的效应综合成为一个指数&…

华为云cce负载配置时间同步

华为云cce将负载配置好之后&#xff0c;发现里面的时间与真实时间不同步&#xff0c;差了12小时&#xff0c;怎么办&#xff1f; 这时候就需要配置时间同步了。 华为云cce里面通过配置数据存储的路径来解决这个问题的&#xff0c;配置后&#xff0c;需要重启负载。 新建负载…

python实验3 石头剪刀布游戏

实验3&#xff1a;石头剪刀布游戏 一、实验目的二、知识要点图三、实验1. 石头剪刀布2. 实现大侠个人信息 一、实验目的 了解3类基本组合数据类型。理解列表概念并掌握Python中列表的使用。理解字典概念并掌握Python中字典的使用。运用jieba库进行中文分词并进行文本词频统计。…

JSON.stringify方法详解 后端接受JSON数据格式

1、方法定义&#xff1a;JSON.stringify(value, replacer, space) 参数说明&#xff1a; value&#xff1a;js对象 replacer&#xff1a;替换对象&#xff0c;可以是一个方法、对象或数组&#xff0c;将value按照替换规则展示。 space&#xff1a;填充参数&#xff0c;可以是数…

Protocol handler start failed

背景 上一次启动项目还好好的&#xff0c;关闭项目重新打开时&#xff0c;报错了&#xff01; 报错提示 英文&#xff1a;Protocol handler start failed 翻译&#xff1a;协议处理程序启动失败 原因 端口被其他程用了&#xff0c;导致端口冲突。 解决方案 打开任务管理…

深入理解Java中的String、StringBuilder和StringBuffer(每天一个技术点,第一天)

大家好&#xff0c;我是你们的博主每天一个技术点。今天&#xff0c;我们将探讨Java中的一个重要主题&#xff1a;String、StringBuilder和StringBuffer。这些类在Java编程中无处不在&#xff0c;但它们之间的区别和用法可能并不是所有人都清楚。所以&#xff0c;让我们深入了解…

10分钟的时间,带你彻底搞懂JavaScript数据类型转换

前言 &#x1f4eb; 大家好&#xff0c;我是南木元元&#xff0c;热衷分享有趣实用的文章&#xff0c;希望大家多多支持&#xff0c;一起进步&#xff01; &#x1f345; 个人主页&#xff1a;南木元元 目录 JS数据类型 3种转换类型 ToBoolean ToString ToNumber 对象转原…

Flutter加固原理及加密处理

​ 引言 为了保护Flutter应用免受潜在的漏洞和攻击威胁&#xff0c;加固是必不可少的措施之一。Flutter加固原理主要包括代码混淆、数据加密、安全存储、反调试与反分析、动态加载和安全通信等多个方面。通过综合运用这些措施&#xff0c;可以提高Flutter应用的安全性&#xf…
最新文章