Linux内核与驱动学习记录-字符设备基础知识

By: Ailson Jack
Date: 2021.06.30
个人博客:http://www.only2fire.com/
本文在我博客的地址是:http://www.only2fire.com/archives/136.html,排版更好,便于学习,也可以去我博客逛逛,兴许有你想要的内容呢。

1.Linux设备分类

按照读写存储数据方式,我们可以把Linux设备分为以下几种:字符设备、块设备和网络设备。

字符设备: 指应用程序按字节/字符来读写数据的设备。它通常不支持随机存取数据。字符设备在实现时,大多不使用缓存器,系统直接从设备读取/写入每一个字符。

块设备: 通常支持随机存取和寻址,并使用缓存器。它与字符设备不同之处就是,是否支持随机存储。字符型是流形式,逐一存储。数据的读写只能以块的倍数进行。

网络设备: 是一种特殊设备,它并不存在于/dev 下面,主要用于网络数据的收发。

Linux 内核中处处体现面向对象的设计思想,为了统一形形色色的设备, Linux 系统将设备分别抽象为 struct cdev, struct block_device,struct net_devce 三个对象,具体的设备都可以包含着三种对象从而继承和三种对象属性和操作,并通过各自的对象添加到相应的驱动模型中,从而进行统一的管理和操作。

2.字符设备抽象

Linux 内核中将字符设备抽象成一个具体的数据结构 (struct cdev), 我们可以理解为字符设备对象,cdev 记录了字符设备的相关信息(设备号、内核对象),字符设备的打开、读写、关闭等操作接口(file_operations)。在我们想要添加一个字符设备时,就是将这个对象注册到内核中,通过创建一个文件(设备节点)绑定对象的cdev,当我们对这个文件进行读写操作时,就可以通过虚拟文件系统,在内核中找到这个对象及其操作接口,从而控制设备。

C 语言中没有面向对象语言的继承的语法,但是我们可以通过结构体的包含来实现继承,这种抽象提取了设备的共性,为上层提供了统一接口,使得管理和操作设备变得很容易。

在硬件层,我们可以通过查看硬件的原理图、芯片的数据手册,确定底层需要配置的寄存器,这类似于裸机开发。将对底层寄存器的配置,读写操作放在文件操作接口里面,也就是实现 file_operations 结构体。

其次在驱动层,我们将文件操作接口注册到内核,内核通过内部散列表来登记记录主次设备号。

在文件系统层,新建一个文件绑定该文件操作接口,应用程序通过操作指定文件的文件操作接口来设置底层寄存器。

3.相关概念及数据结构

在 linux 中,我们使用设备编号来表示设备,主设备号区分设备类别,次设备号标识具体的设备。字符设备的cdev 结构体记录了设备号,在使用设备时,我们通常会打开设备节点,通过设备节点的 inode 结构体、 file 结构体最终找到 file_operations 结构体,并从file_operations 结构体中得到操作设备的具体方法。

3.1.设备号

Linux对于设备的访问是通过文件系统的名称进行的,这些名称被称为特殊文件、设备文件,或者简单称为文件系统树的节点, Linux 根目录下有/dev 这个文件夹,专门用来存放设备中的驱动程序。通过执行命令:ls -l /dev,可以查看/dev目录下每个文件的详细信息,每一行的第一个字符表示设备的类型,'c’用来标识字符设备,'b’用来标识块设备。
Linux内核与驱动学习记录-字符设备基础知识_第1张图片
一般来说,主设备号指向设备的驱动程序,次设备号指向某个具体的设备。如上图,I2C-0,I2C-1 属于不同设备但是共用一套驱动程序。

在内核中,dev_t 用来表示设备编号,dev_t 是一个 32 位的数,其中,高 12 位表示主设备号,低 20 位表示次设备号。理论上主设备号取值范围:0~2^12-1,次设备号 0~2^20-1。实际上在内核源码中 __register_chrdev_region() 函数中,major 被限定在0~CHRDEV_MAJOR_MAX, CHRDEV_MAJOR_MAX 是一个宏,值是 512。

宏定义 MAJOR 和 MINOR,可以根据设备的设备号来获取设备的主设备号和次设备号。宏定义MKDEV,用于将主设备号和次设备号合成一个设备号,主设备号可以通过查阅内核源码的 Documentation/devices.txt 文件,而次设备号通常是从编号 0 开始。

内核通过一个散列表 (哈希表) 来记录设备编号。哈希表由数组和链表组成,吸收数组查找快,链表增删效率高,容易拓展等优点。

以主设备号为 cdev_map 编号,使用哈希函数 f(major)=major%255 来计算组数下标 (使用哈希函数是为了链表节点尽量平均分布在各个数组元素中,提高查询效率);主设备号冲突, 则以次设备号为比较值来排序链表节点。内核用 struct cdev 结构体来描述一个字符设备,并通过 struct kobj_map 类型的散列表cdev_map 来管理当前系统中的所有字符设备。

// 字符设备管理对象
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;

3.2.设备节点

设备节点(设备文件): Linux 中设备节点是通过“mknod”命令来创建的。一个设备节点其实就是一个文件,Linux 中称为设备文件。

设备节点被创建在/dev下,是连接内核与用户层的枢纽,就是设备是接到对应哪种接口的哪个 ID 上。设备节点是Linux内核对设备的抽象,一个设备节点就是一个文件。应用程序通过一组标准化的调用执行访问设备,这些调用独立于任何特定的驱动程序。而驱动程序负责将这些标准调用映射到实际硬件的特有操作。

3.3.数据结构

在驱动开发过程中,不可避免要涉及到三个重要的的内核数据结构分别包括文件操作方式(file_operations),文件描述结构体(struct file)以及inode 结构体。

file_operation 就是把系统调用和驱动程序关联起来的关键数据结构。

内核中用 file 结构体来表示每个打开的文件,每打开一个文件,内核会创建一个结构体,并将对该文件上的操作函数传递给该结构体的成员变量f_op,当文件所有实例被关闭后,内核会释放这个结构体。在file 结构体中,我们需要关心的数据成员有f_op和private_data:

const struct file_operations *f_op:存放与文件操作相关的一系列函数指针,如 open、 read、 wirte 等函数。

void *private_data:该指针变量只会用于设备驱动程序中,内核并不会对该成员进行操作。因此,在驱动程序中,通常用于指向描述设备的结构体。

inode 结构体是 Linux 管理文件系统的最基本单位,也是文件系统连接任何子目录、文件的桥梁。内核使用 inode 结构体在内核内部表示一个文件。因此,它与表示一个已经打开的文件描述符的结构体 (即 file 文件结构) 是不同的,我们可以使用多个 file 文件结构表示同一个文件的多个文件描述符,但此时,所有的这些 file 文件结构全部都必须只能指向一个 inode 结构体。在inode 结构体中,我们需要关心的数据成员有i_rdev和i_cdev:

dev_t i_rdev:表示设备文件的结点,这个域实际上包含了设备号。

struct cdev *i_cdev:struct cdev 是内核的一个内部结构,它是用来表示字符设备的,当 inode 结点指向一个字符设备文件时,此域为一个指向 inode 结构的指针。

欢迎关注博主的公众号呀:

Linux内核与驱动学习记录-字符设备基础知识_第2张图片
如果文中有什么问题欢迎指正,毕竟博主的水平有限。

如果这篇文章对你有帮助,记得点赞和关注博主就行了^_^。

排版更好的内容见我博客的地址:http://www.only2fire.com/archives/136.html

注:转载请注明出处,谢谢!^_^

你可能感兴趣的:(Linux内核与驱动,Linux,内核学习,驱动开发,嵌入式)