LV06-03-chrdev-01-字符设备基础
本文主要是字符设备驱动——字符设备基础的相关笔记,若笔记中有错误或者不合适的地方,欢迎批评指正😃。
点击查看使用工具及版本
PC端开发环境 | Windows | Windows11 |
Ubuntu | Ubuntu20.04.2的64位版本 | |
VMware® Workstation 17 Pro | 17.6.0 build-24238078 | |
终端软件 | MobaXterm(Professional Edition v23.0 Build 5042 (license)) | |
Win32DiskImager | Win32DiskImager v1.0 | |
Linux开发板环境 | Linux开发板 | 正点原子 i.MX6ULL Linux 阿尔法开发板 |
uboot | NXP官方提供的uboot,使用的uboot版本为U-Boot 2019.04 | |
linux内核 | linux-4.19.71(NXP官方提供) |
点击查看本文参考资料
分类 | 网址 | 说明 |
官方网站 | https://www.arm.com/ | ARM官方网站,在这里我们可以找到Cotex-Mx以及ARMVx的一些文档 |
https://www.nxp.com.cn/ | NXP官方网站 | |
https://www.nxpic.org.cn/ | NXP 官方社区 | |
https://u-boot.readthedocs.io/en/latest/ | u-boot官网 | |
https://www.kernel.org/ | linux内核官网 |
点击查看相关文件下载
分类 | 网址 | 说明 |
NXP | https://github.com/nxp-imx | NXP imx开发资源GitHub组织,里边会有u-boot和linux内核的仓库 |
nxp-imx/linux-imx/releases/tag/v4.19.71 | NXP linux内核仓库tags中的v4.19.71 | |
nxp-imx/uboot-imx/releases/tag/rel_imx_4.19.35_1.1.0 | NXP u-boot仓库tags中的rel_imx_4.19.35_1.1.0 | |
I.MX6ULL | i.MX 6ULL Applications Processors for Industrial Products | I.MX6ULL 芯片手册(datasheet,可以在线查看) |
i.MX 6ULL Applications ProcessorReference Manual | I.MX6ULL 参考手册(下载后才能查看,需要登录NXP官网) | |
Source Code | https://elixir.bootlin.com/linux/latest/source | linux kernel源码 |
https://elixir.bootlin.com/u-boot/latest/source | uboot源码 |
一、字符设备驱动简介
1. linux设备分类
linux是文件型系统,所有硬件都会在对应的目录(/dev)下面用相应的文件表示。 在windows系统中,设备很好理解,像硬盘,磁盘指的是实实在在硬件。 而在文件系统的linux下面,都有对于文件与这些设备关联的,访问这些文件就可以访问实际硬件。 像访问文件那样去操作硬件设备,一切都会简单很多,不需要再调用以前com,prt等接口了。 直接读文件,写文件就可以向设备发送、接收数据。 按照读写存储数据方式,我们可以把设备分为以下几种:字符设备、块设备和网络设备。
字符设备:指应用程序按字节/字符来读写数据的设备。 这些设备节点通常为传真、虚拟终端和串口调制解调器、键盘之类设备提供流通信服务, 它通常不支持随机存取数据。字符设备在实现时,大多不使用缓存器。系统直接从设备读取/写入每一个字符。 例如,键盘这种设备提供的就是一个数据流,当你敲入“cnblogs”这个字 符串时, 键盘驱动程序会按照和输入完全相同的顺序返回这个由七个字符组成的数据流。它们是顺序的,先返回c,最后是s。
块设备:通常支持随机存取和寻址,并使用缓存器。 操作系统为输入输出分配了缓存以存储一块数据。当程序向设备发送了读取或者写入数据的请求时, 系统把数据中的每一个字符存储在适当的缓存中。当缓存被填满时,会采取适当的操作(把数据传走), 而后系统清空缓存。它与字符设备不同之处就是,是否支持随机存储。字符型是流形式,逐一存储。 典型的块设备有硬盘、SD卡、闪存等,应用程序可以寻址磁盘上的任何位置,并由此读取数据。 此外,数据的读写只能以块的倍数进行。
网络设备:是一种特殊设备,它并不存在于/dev下面,主要用于网络数据的收发。
Linux内核中处处体现面向对象的设计思想,为了统一形形色色的设备,Linux系统将设备分别抽象为struct cdev, struct block_device,struct net_devce三个对象,具体的设备都可以包含着三种对象从而继承和三种对象属性和操作, 并通过各自的对象添加到相应的驱动模型中,从而进行统一的管理和操作
字符设备驱动程序适合于大多数简单的硬件设备,而且比起块设备或网络驱动更加容易理解, 因此我们选择从字符设备开始,从最初的模仿,到慢慢熟悉。
2. 驱动程序的调用
我们先来简单的了解一下 Linux 下的应用程序是如何调用驱动程序的, Linux 应用程序对驱动程序的调用如下图:
在 Linux 中一切皆为文件,驱动加载成功以后会在“/dev”目录下生成一个相应的文件,应用程序通过对这个名为“/dev/xxx” (xxx 是具体的驱动文件名字)的文件进行相应的操作即可实现对硬件的操作。
比如现在有个叫做/dev/led 的驱动文件,此文件是 led 灯的驱动文件。应用程序使用 open 函数来打开文件/dev/led,使用完成以后使用 close 函数关闭/dev/led 这个文件。 open和 close 就是打开和关闭 led 驱动的函数,如果要点亮或关闭 led,那么就使用 write 函数来操作,也就是向此驱动写入数据,这个数据就是要关闭还是要打开 led 的控制参数。如果要获取led 灯的状态,就用 read 函数从驱动中读取相应的状态。
应用程序运行在用户空间,而 Linux 驱动属于内核的一部分,因此驱动运行于内核空间。当我们在用户空间想要实现对内核的操作,比如使用 open 函数打开/dev/led 这个驱动,因为用户空间不能直接对内核进行操作,因此必须使用一个叫做“系统调用”的方法来实现从用户空间“陷入” 到内核空间,这样才能实现对底层驱动的操作。 open、 close、 write 和 read 等这些函数是由 C 库提供的,在 Linux 系统中,系统调用作为 C 库的一部分。当我们调用 open 函数的时候流程
C 库如何通过系统调用“陷入” 到内核空间?这个有点复杂,可以参考这里理解一下:《01嵌入式开发/02IMX6ULL平台/LV03-应用开发/LV01-01-应用编程基本概念-01-基础知识.md》以及以下资料:
- Linux系统调用(syscall)原理 - Gityuan博客 | 袁辉辉的技术博客
- linux-insides-zh/SysCall/README.md at master · hust-open-atom-club/linux-insides-zh
这里就先不详细去说了。
3. 字符设备的抽象
Linux内核中将字符设备抽象成一个具体的数据结构(struct cdev),我们可以理解为字符设备对象, cdev记录了字符设备的相关信息(设备号、内核对象),字符设备的打开、读写、关闭等操作接口(file_operations), 在我们想要添加一个字符设备时,就是将这个对象注册到内核中,通过创建一个文件(设备节点)绑定对象的cdev, 当我们对这个文件进行读写操作时,就可以通过虚拟文件系统,在内核中找到这个对象及其操作接口,从而控制设备。
语言中没有面向对象语言的继承的语法,但是我们可以通过结构体的包含来实现继承,这种抽象提取了设备的共性, 为上层提供了统一接口,使得管理和操作设备变得很容易。
在硬件层,我们可以通过查看硬件的原理图、芯片的数据手册,确定底层需要配置的寄存器,这类似于裸机开发。 将对底层寄存器的配置,读写操作放在文件操作接口里面,也就是实现file_operations结构体。
其次在驱动层,我们将文件操作接口注册到内核,内核通过内部散列表来登记记录主次设备号。
在文件系统层,新建一个文件绑定该文件操作接口,应用程序通过操作指定文件的文件操作接口来设置底层寄存器。
实际上,在Linux上写驱动程序,都是做一些“填空题”。因为Linux给我们提供了一个基本的框架, 我们只需要按照这个框架来写驱动,内核就能很好的接收并且按我们所要求的那样工作。
二、相关概念及数据结构
在linux中,我们使用设备编号来表示设备,主设备号区分设备类别,次设备号标识具体的设备。 cdev结构体被内核用来记录设备号,而在使用设备时,我们通常会打开设备节点,通过设备节点的inode结构体、 file结构体最终找到file_operations结构体,并从file_operations结构体中得到操作设备的具体方法。
1. 设备号
1.1 什么是设备号?
对于字符的访问是通过文件系统的名称进行的,这些名称被称为特殊文件、设备文件,或者简单称为文件系统树的节点, Linux根目录下有/dev这个文件夹,专门用来存放设备中的驱动程序,我们可以使用ls -l 以列表的形式列出系统中的所有设备。 其中,每一行表示一个设备,每一行的第一个字符表示设备的类型。
如下图:’c’用来标识字符设备,’b’用来标识块设备。如 autofs 是一个字符设备c, 它的主设备号是10,次设备号是235; loop0 是一个块设备,它的主设备号是7,次设备号为0,同时可以看到loop0 - loop3共用一个主设备号,次设备号由0开始递增。
1 |
|
一般来说,主设备号指向设备的驱动程序,次设备号指向某个具体的设备。例如 I2C-0,I2C-1属于不同设备但是共用一套驱动程序。
1.2 设备编号的含义
在内核中,dev_t用来表示设备编号,dev_t是一个32位的数,其中,高12位表示主设备号,低20位表示次设备号。 也就是理论上主设备号取值范围:0-2^12,次设备号 0-2^20 。 实际上在内核源码中__register_chrdev_region(…)函数中,major被限定在0 - CHRDEV_MAJOR_MAX ,CHRDEV_MAJOR_MAX是一个宏,值是512。 dev_t定义在types.h - include/linux/types.h - dev_t:
1 | typedef u32 __kernel_dev_t; |
在kdev_t中,设备编号通过移位操作最终得到主/次设备号码,同样主/次设备号也可以通过位运算变成dev_t类型的设备编号。具体可以看这几个函数:kdev_t.h - include/linux/kdev_t.h
1 |
MAJOR和MINOR,可以根据设备的设备号来获取设备的主设备号和次设备号。宏定义MKDEV,用于将主设备号和次设备号合成一个设备号,主设备可以通过查阅内核源码的 devices.txt - Documentation/admin-guide/devices.txt 文件,而次设备号通常是从编号0开始。
1.3 cdev结构体
内核通过一个散列表(哈希表)来记录设备编号。 哈希表由数组和链表组成,吸收数组查找快,链表增删效率高,容易拓展等优点。
以主设备号为cdev_map编号,使用哈希函数f(major)=major%255来计算组数下标(使用哈希函数是为了链表节点尽量平均分布在各个数组元素中,提高查询效率); 主设备号冲突,则以次设备号为比较值来排序链表节点。 如下图所示,内核用struct cdev结构体来描述一个字符设备,并通过struct kobj_map类型的 散列表cdev_map来管理当前系统中的所有字符设备。
struct cdev结构体定义在 cdev.h - include/linux/cdev.h - struct cdev:
1 | struct cdev { |
- struct kobject kobj: 内嵌的内核对象,通过它将设备统一加入到“Linux设备驱动模型”中管理(如对象的引用计数、电源管理、热插拔、生命周期、与用户通信等)。
- struct module *owner: 字符设备驱动程序所在的内核模块对象的指针。
- const struct file_operations *ops: 文件操作,是字符设备驱动中非常重要的数据结构,在应用程序通过文件系统(VFS)呼叫到设备设备驱动程序中实现的文件操作类函数过程中,ops起着桥梁纽带作用,VFS与文件系统及设备文件之间的接口是file_operations结构体成员函数,这个结构体包含了对文件进行打开、关闭、读写、控制等一系列成员函数。
- struct list_head list: 用于将系统中的字符设备形成链表(这是个内核链表的一个链接因子,可以再内核很多结构体中看到这种结构的身影)。
- dev_t dev: 字符设备的设备号,有主设备和次设备号构成。
- unsigned int count: 属于同一主设备好的次设备号的个数,用于表示设备驱动程序控制的实际同类设备的数量。
2. 设备类
2.1 什么是设备类
备驱动模型中,还有一个 抽象概念 叫做类(CLass),准确来说,叫做设备类。所谓设备类,是指提供的用户接口相似的一类设备的集合,常见的设备类的有block、tty、input、usb等等。
举个例子,一些年龄相仿、需要获取的知识相似的人,聚在一起学习,就构成了一个班级(Class)。这个班级可以有自己的名称(如1307班),但如果离开构成它的学生(device),它就没有任何存在意义。另外,班级存在的最大意义是什么呢?是由老师讲授的每一个课程!因为老师只需要讲一遍,一个班的学生都可以听到。不然的话(例如每个学生都在家学习),就要为每人请一个老师,讲授一遍。而讲的内容,大多是一样的,这就是极大的浪费。
设备模型中的Class所提供的功能也一样了,例如一些相似的device(学生),需要向用户空间提供相似的接口(课程),如果每个设备的驱动都实现一遍的话,就会导致内核有大量的冗余代码,这就是极大的浪费。
设备类是一个设备的高层视图,它抽象出了底层的实现细节,从而允许用户空间使用设备所提供的功能,而不用关心设备是如何连接和工作的。设备类是用来抽象设备的共性而诞生的。类成员通常由上层代码所控制,而无需驱动的明确支持。但有些情况下驱动也需要直接处理类。
2.2 linux系统中的class
我们先看一下现有Linux系统中有关class的状况,我们来看一下这个/sys/class目录:
1 | ls /sys/class/ -alh |
我们这里以input这个目录为例来看一下。继续深入这个input目录:
1 | ls /sys/class/input/ -l |
我们看到里面有event0、event1、input0和input1等软链接,他们都链接到了哪里?这里的../../
是什么?event0这些软链接位于 /sys/class/input/
目录,往上推两级就是/sys/
,所以这里的../../
其实绝对路径就是/sys/
。我们看看这些软链接对应的目录都有什么:
1 | event0 |
发现input class也没做什么实实在在的事儿,它(input class)的功能,仅仅是:
(1)在/sys/class/
目录下,创建一个本class的目录(input)
(2)在本目录下,创建每一个属于该class的设备的符号链接,这样就可以在本class目录下,访问该设备的所有特性(即attribute)
如,把“
/sys/devices/soc0/soc/2000000.aips-bus/20cc000.snvs/20cc000.snvs:snvs-powerkey/input/input0/event0
”设备链接到”/sys/class/input/event0
”。
(3)device在sysfs的目录下,也会创建一个subsystem的符号链接,链接到本class的目录,如上图,这里的../../../../../../../../
是啥?我们知道这个subsystem的路径为/sys/devices/soc0/soc/2000000.aips-bus/20cc000.snvs/20cc000.snvs:snvs-powerkey/input/input0
所以从这里往上推8级就是/sys
所以这里其实subsystem是链接到了/sys/class/input/input0
。
2.3 struct class
struct class结构体定义在device.h - include/linux/device.h - struct class:
1 | struct class { |
2.3.1 name
name:设备类的名称,会在“/sys/class/
”目录下体现。(实际使用的是内部kobj包含的动态创建的名称。)
2.3.2 dev_kobj
dev_kobj是struct kobject类型,在device注册时,会在/sys/dev下创建名为自己设备号的软链接。但设备不知道自己属于块设备还是字符设备,所以会请示自己所属的class;class就是用dev_kobj记录本类设备应属于的哪种设备。
我们来看一下/sys/dev
:
1 | ls -alh /sys/dev |
发现里面有两个目录,其实这里就是不同的设备类型,block里面是块设备,char里面是字符设备:
2.3.3 总结
这里还是有一些概念没看懂,这里大概了解一下,后面遇到了会详细再去学习。
3. 设备节点
设备节点(设备文件):Linux中设备节点是通过“mknod”命令来创建的。一个设备节点其实就是一个文件, Linux中称为设备文件。有一点必要说明的是,在Linux中,所有的设备访问都是通过文件的方式, 一般的数据文件程序普通文件,设备节点称为设备文件。
设备节点被创建在/dev下,是连接内核与用户层的枢纽,就是设备是接到对应哪种接口的哪个ID 上。 相当于硬盘的inode一样的东西,记录了硬件设备的位置和信息在Linux中,所有设备都以文件的形式存放在/dev目录下, 都是通过文件的方式进行访问,设备节点是Linux内核对设备的抽象,一个设备节点就是一个文件。 应用程序通过一组标准化的调用执行访问设备,这些调用独立于任何特定的驱动程序。而驱动程序负责将这些标准调用映射到实际硬件的特有操作。
4. 数据结构
在驱动开发过程中,不可避免要涉及到三个重要的的内核数据结构分别包括文件操作方式(file_operations), 文件描述结构体(struct file)以及inode结构体,在我们开始阅读编写驱动程序的代码之前,有必要先了解这三个结构体。
4.1 struct file_operations
file_operations结构体定义在fs.h - include/linux/fs.h - struct file_operations:
1 | struct file_operations { |
在系统内部,I/O设备的存取操作通过特定的入口点来进行,而这组特定的入口点恰恰是由设备驱动程序提供的。 通常这组设备驱动程序接口是由结构file_operations结构体向系统说明的,它定义在ebf_buster_linux/include/linux/fs.h中。 传统上, 一个file_operation结构或者其一个指针称为 fops( 或者它的一些变体)。结构中的每个成员必须指向驱动中的函数, 这些函数实现一个特别的操作, 或者对于不支持的操作留置为NULL。当指定为NULL指针时内核的确切的行为是每个函数不同的。
- llseek: 用于修改文件的当前读写位置,并返回偏移后的位置。参数file传入了对应的文件指针,我们可以看到以上代码中所有的函数都有该形参,通常用于读取文件的信息,如文件类型、读写权限;参数loff_t指定偏移量的大小;参数int是用于指定新位置指定成从文件的某个位置进行偏移,SEEK_SET表示从文件起始处开始偏移;SEEK_CUR表示从当前位置开始偏移;SEEK_END表示从文件结尾开始偏移。
- read: 用于读取设备中的数据,并返回成功读取的字节数。该函数指针被设置为NULL时,会导致系统调用read函数报错,提示“非法参数”。该函数有三个参数:file类型指针变量,char __user *类型的数据缓冲区,__user用于修饰变量,表明该变量所在的地址空间是用户空间的。内核模块不能直接使用该数据,需要使用copy_to_user函数来进行操作。size_t类型变量指定读取的数据大小。
- write: 用于向设备写入数据,并返回成功写入的字节数,write函数的参数用法与read函数类似,不过在访问__user修饰的数据缓冲区,需要使用copy_from_user函数。
- unlocked_ioctl: 提供设备执行相关控制命令的实现方法,它对应于应用程序的fcntl函数以及ioctl函数。在 kernel 3.0 中已经完全删除了 struct file_operations 中的 ioctl 函数指针。
- open: 设备驱动第一个被执行的函数,一般用于硬件的初始化。如果该成员被设置为NULL,则表示这个设备的打开操作永远成功。
- release: 当file结构体被释放时,将会调用该函数。与open函数相反,该函数可以用于释放
4.2 struct file
内核中用file结构体来表示每个打开的文件,每打开一个文件,内核会创建一个结构体,并将对该文件上的操作函数传递给 该结构体的成员变量f_op,当文件所有实例被关闭后,内核会释放这个结构体。这个结构体定义在fs.h - include/linux/fs.h - struct file:
1 | struct file { |
- f_op:存放与文件操作相关的一系列函数指针,如open、read、wirte等函数。
- private_data:该指针变量只会用于设备驱动程序中,内核并不会对该成员进行操作。因此,在驱动程序中,通常用于指向描述设备的结构体。
4.3 struct inode
FS inode 包含文件访问权限、属主、组、大小、生成时间、访问时间、最后修改时间等信息。 它是Linux 管理文件系统的最基本单位,也是文件系统连接任何子目录、文件的桥梁。 内核使用inode结构体在内核内部表示一个文件。因此,它与表示一个已经打开的文件描述符的结构体(即file 文件结构)是不同的, 我们可以使用多个file文件结构表示同一个文件的多个文件描述符,但此时, 所有的这些file文件结构全部都必须只能指向一个inode结构体。 inode结构体包含了一大堆文件相关的信息,但是就针对驱动代码来说,我们只要关心其中的两个域即可(fs.h - include/linux/fs.h - struct inode)
1 | struct inode { |
- dev_t i_rdev: 表示设备文件的结点,这个域实际上包含了设备号。
- struct cdev *i_cdev: struct cdev是内核的一个内部结构,它是用来表示字符设备的,当inode结点指向一个字符设备文件时,此域为一个指向inode结构的指针。
参考资料: