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

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:指向文件操作的指针,包括open、read、write等操作接口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、数据结构之间关系

上图绘制是对字符设备驱动程序的数据结构以及
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_init和module_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的对象与字符设备进行绑定,其作用是来处理该字符设备的open、read、write等操作。
我们要做的就是去实现我们需要的函数接口,如:
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接口字符设备驱动整体框架
希望对大家有所帮助。

一文秒懂|Linux字符设备驱动的更多相关文章
- 深入理解Linux字符设备驱动
文章从上层应用访问字符设备驱动开始,一步步地深入分析Linux字符设备的软件层次.组成框架和交互.如何编写驱动.设备文件的创建和mdev原理,对Linux字符设备驱动有全面的讲解.本文整合之前发表的& ...
- Linux字符设备驱动结构(一)--cdev结构体、设备号相关知识机械【转】
本文转载自:http://blog.csdn.net/zqixiao_09/article/details/50839042 一.字符设备基础知识 1.设备驱动分类 linux系统将设备分为3类:字符 ...
- Smart210学习记录----beep linux字符设备驱动
今天搞定了beep linux字符设备驱动,心里还是很开心的,哈哈...但在完成的过程中却遇到了一个非常棘手的问题,花费了我大量的时间,,,, 还是把问题描述一下吧,好像这个问题很普遍的,网上许多解决 ...
- Linux字符设备驱动实现
Linux字符设备驱动实现 要求 编写一个字符设备驱动,并利用对字符设备的同步操作,设计实现一个聊天程序.可以有一个读,一个写进程共享该字符设备,进行聊天:也可以由多个读和多个写进程共享该字符设备,进 ...
- Linux字符设备驱动基本结构
1.Linux字符设备驱动的基本结构 Linux系统下具有三种设备,分别是字符设备.块设备和网络设备,Linux下的字符设备是指只能一个字节一个字节读写的设备,不能随机读取设备内存中某一数据,读取数据 ...
- (57)Linux驱动开发之三Linux字符设备驱动
1.一般情况下,对每一种设备驱动都会定义一个软件模块,这个工程模块包含.h和.c文件,前者定义该设备驱动的数据结构并声明外部函数,后者进行设备驱动的具体实现. 2.典型的无操作系统下的逻辑开发程序是: ...
- Linux字符设备驱动框架
字符设备是Linux三大设备之一(另外两种是块设备,网络设备),字符设备就是字节流形式通讯的I/O设备,绝大部分设备都是字符设备,常见的字符设备包括鼠标.键盘.显示器.串口等等,当我们执行ls -l ...
- Linux 字符设备驱动模型
一.使用字符设备驱动程序 1. 编译/安装驱动 在Linux系统中,驱动程序通常采用内核模块的程序结构来进行编码.因此,编译/安装一个驱动程序,其实质就是编译/安装一个内核模块 2. 创建设备文件 通 ...
- linux字符设备驱动学习笔记(一):简单的字符设备驱动
最近在鼓捣lnux字符设备驱动,在网上搜集的各种关于linux设备驱动的代码和注释,要么是针对2.4的,要么是错误百出,根本就不能运行成功,真希望大家在发博客的时候能认真核对下代码的正确性,特别是要把 ...
- Linux字符设备驱动
一.字符设备基础 字符设备 二.字符设备驱动与用户空间访问该设备的程序三者之间的关系 三.字符设备模型 1.Linux内核中,使用 struct cdev 来描述一个字符设备 动态申请(构造)cdev ...
随机推荐
- 你一定要知道的iterator和generator
generator是一种特殊的iterator,generator可以替代iterator实现,使代码更为简洁 什么是iterator iterator叫做迭代器,是用来帮助某个数据结构进行遍历的对象 ...
- 使用MediatR实现CQRS
CQRS和中介者模式 MediatR库主要是为了帮助开发者快速实现两种软件架构模式:CQRS和Mediator.这两种架构模式看上去似乎差不多,但还是有很多区别的. CQRS CQRS是Command ...
- 《SQLi-Labs》02. Less 6~10
@ 目录 索引 Less-6 题解 原理 Less-7 题解 Less-8 题解 Less-9 题解 原理 Less-10 题解 sqli.开启新坑. 索引 Less-6:布尔盲注,字符型[" ...
- C++算法之旅、05 基础篇 | 第二章 数据结构
常用代码模板2--数据结构 - AcWing 笔试用数组模拟而不是结构体 使用结构体指针,new Node() 非常慢,创建10万个节点就超时了,做笔试题不会用这种方式(优化是提前初始化好数组,但这样 ...
- MySQL数据库触发器讲解 [创建/删除/查询/select into]
刚学习实例完mysql触发器, 前来分享学习经验. 菜鸟装逼, 老鸟勿喷 先来认识一下有关触发器的一些关键词. 在使用触发器时, 这些关键词将被用到, 请记下它们的模样和用途(意思) cre ...
- 【matplotlib基础】--样式表
Matplotlib库 由于诞生的比较早,所以其默认的显示样式很难符合现在的审美,这也是它经常为人诟病的地方. 不过,经过版本更迭之后,现在 Matplotlib 已经内置了很多样式表,通过使用不同的 ...
- 模块化打包工具-初识Webpack
1. 为什么需要模块化打包工具 在上一篇文章中提到的ES Module可以帮助开发者更好地组织代码,完成js文件的模块化,基本解决了模块化的问题,但是实际开发中仅仅完成js文件的模块化是不够的,尤其是 ...
- 低代码平台如何借助Nginx实现网关服务
摘要:本文由葡萄城技术团队于博客园原创并首发.转载请注明出处:葡萄城官网,葡萄城为开发者提供专业的开发工具.解决方案和服务,赋能开发者. 前言 在典型的系统部署架构中,应用服务器是一种软件或硬件系统, ...
- 其它——paramiko模块的使用
文章目录 paramiko 一 介绍 二 通过用户名密码方式远程执行命令 三 通过用户名密码方式上传下载文件 四 通过公钥私钥远程执行命令 五 通过公钥私钥远程上传下载文件 六 通过私钥字符串远程连接 ...
- 文心一言 VS 讯飞星火 VS chatgpt (109)-- 算法导论10.1 7题
七.用go语言,说明如何用两个队列实现一个栈,并分析相关栈操作的运行时间. 文心一言: 在Go语言中,我们可以用两个队列(queue1和queue2)实现一个栈.以下是实现代码: package ma ...