Android驱动的系统笔记1


注意:本网站所有内容为自己学习期间记录的笔记,基本都是原作者那边摘抄过来的,不作商业用途也不插播广告。(如有侵权,请联系我删除,15909440083)

北京迅为电子

同款开发板购买链接

【第16期的 最新驱动资料(文档+例程)】
链接:https://pan.baidu.com/s/1Qf5d5_e2u_VklJWJK9wzNQ 提取码:n4q6

第一篇 驱动基础

第1 章 前言

1.2 基础准备

首先,不要脱离硬件。就好比用仿真软件学习51 单片机是永远掌握不了单片机的精髓的。所以有一块硬件开发板是学习驱动的前提。

第二,有了开发板之后,要掌握开发板的基本操作。如开发板的启动,烧写镜像等操作。

第三,能够成功编译开发板的系统源码。在驱动的学习过程中,是避免不了的要对内核的某些功能进行使能、修改设备树添加对应的设备,这些操作都需要进行源码编译。

第四,掌握C 语言。驱动程序是由C 语言编写的,而且内核源码中绝大多数的代码也都是由C 语言编写的,在学习驱动的过程中,或多或少的要对源码进行阅读。所以C 语言基础是学习驱动的必要条件之一。

第五,掌握Linux 环境搭建以及shell 命令的使用。

第六,驱动最后必然要落实到相应的硬件上,所以肯定要对底层电路有所了解,以驱动LED 灯为例,必然要了解其控制电路,找到相应的控制引脚,要能读懂简单的硬件原理图。

第2 章你好!内核源码

本章我们来认识Linux 内核源码

2.1 初识内核源码
Linux 内核源码的官方网站为https://www.kernel.org/,可以在该网站下载最新的Linux 内核源码。进入该网站之后如下图(图2-1)所示:

图2-1

从上图(图1)可以看到多个版本的内核分支,分别为主线版本(mainline)、稳定版本(stable)和长期支持版本(longterm)。以上各个支线和主线是由linus torvalds(Linux 之父)所领导。半导体厂商和一些内核爱好者会在官网下载相应版本的内核源码,对该源码进行打补丁等操作。以此让官网的内核源码可以在半导体厂家设计的主控(CPU)上跑起来,所以在开发和学习的过程中,我们并不会直接去Linux 内核官网下去下载源码,而是使用半导体厂家提供的源码包。

但是不论是Linux 官网的内核源码还是半导体厂家提供的内核源码不影响我们来看它的庐山真面目!讯为下载了Linux 官方网站的4.19.262 分支源码,下载好的源码存放在“iTOP-RK3568开发板【底板V1.7 版本】\03_【iTOP-RK3568 开发板】指南教程\02_Linux 驱动配套资料\01_Linux内核官方源码”如下图(图2-2)所示:(我自己下了一个最新的!)

image-20240809145649679

将Linux 内核官方源码拷贝到虚拟机ubuntu 上如下图(图2-3)所示:

image-20240809145817648

使用以下命令对内核官方源码进行解压,解压完成如下图(图2-4)所示:

sudo tar -vxf linux-4.19.262.tar.xz

image-20240809145855618

解压完成后我们会看到非常多的文件夹,这些文件夹放的就是Linux 内核源码,在下一小节中作者来介绍Linux 内核源码的结构和每个目录的作用。

2.2 内核源码结构

目录的内容如下表(表2-6)所示:

目录 内容
arch 存放不同平台体系相关代码
block 存放块设备相关代码
crypto 存放加密、压缩、CRC 校验等算法相关代码
Documentation 存放相关说明文档,很多实用文档,包括驱动编写等
drivers 存放Linux 内核设备驱动程序源码。该目录包含众多驱动,目录按照设备类别进行分类,如char、block 、input、i2c、spi、pci、usb 等。
firmware 存放处理器相关的一些特殊固件
fs 存放虚拟文件系统代码
include 存放内核所需、与平台无关的头文件
init Linux 系统启动初始化相关的代码
ipc 存放进程间通信代码
kernel Linux 内核的核心代码,包含了进程调度子系统,以及和进程调度相关的模块。
lib 库文件代码, 实现需要在内核中使用的库函数,例如CRC、FIFO、list、MD5等。
mm 实现存放内存管理代码
net 存放网络相关代码
samples 存放提供的一些内核编程范例
scripts 存放一些脚本文件
security 存放系统安全性相关代码
sound 存放声音、声卡相关驱动
tools 一些常用工具,如性能剖析、自测试等
usr 用于生成initramfs 的代码。
virt 提供虚拟机技术(KVM 等)的支持

2.3 编译内核源码

本小节使用的内核源码是半导体厂家提供的内核源码,是我们学习和开发要使用的内核源码。在进行驱动学习之前需要将此内核源码编译成功。
内核源码存放路径为“iTOP-RK3568 开发板【底板V1.7 版本】\03_【iTOP-RK3568 开发板】指南教程\02_Linux 驱动配套资料\02_Linux_SDK 源码”,将对应目录下的内核源码拷贝到虚拟机ubuntu 目录下,如下图(图2-7)所示:

image-20240809150920027

注:编译环境使用的是迅为搭建好的编译环境,迅为的环境经过测试在不进行修改的前提下,可以直接将内核源码编译通过。(我自己放到自己的目录下没有编译成功!)

使用以下命令对内核源码的进行解压,解压完成如下图(图2-8)所示:

tar -vxf linux_sdk.tar.gz

使用“cd linux_sdk”命令进入内核源码目录,如下图(图2-9)所示:

image-20240809151231581

使用命令“./build.sh kernel”进行内核源码的编译,编译过程如下图(图2-10)所示:

image-20240809151250417

编译时间和电脑虚拟机配置相关,编译完成如下图(图2-11)所示:

image-20240809152115505

不知道为啥,反正我的没编译成功。

image-20240809152112335

通过对内核源码官网的探索,内核源码的目录结构讲解以及内核源码的编译。我相信大家对Linux 内核源码应该有了一个初步的认识了。下一章我们来学习第一个驱动helloworld。

第3 章 helloworld 驱动实验

在学习C 语言或者其他语言的时候,我们通常是打印一句“helloworld”来开启编程世界的大门。学习驱动程序编程亦可以如此,使用helloworld 作为我们的第一个驱动程序。接下来开始编写第一个驱动程序—helloworld。

3.1 驱动编写

本小节来编写一个最简单的驱动——helloworld 驱动。helloworld.c 如下(图3-1)所示代码:

#include <linux/module.h>
#include <linux/kernel.h>
static int __init helloworld_init(void){ //驱动入口函数
    printk(KERN_EMERG "helloworld_init\r\n");       //注意:内核打印用printk 而不是printf
    return 0;
}

static void __exit helloworld_exit(void){            //驱动出口函数
    printk(KERN_EMERG "helloworld_exit\r\n");
}

module_init(helloworld_init);               //注册入口函数
module_exit(helloworld_exit);               //注册出口函数

/* 
 * 设置模块的许可协议。
 * 设置模块的作者信息。
 * 设置模块的描述信息。
 */
MODULE_LICENSE("Dual BSD/GPL");		//或者用这个 MODULE_LICENSE("GPL v2");
MODULE_AUTHOR("xiaowen");
MODULE_DESCRIPTION("A simple Hello world module");

看似非常简单的helloworld 驱动代码,却五脏俱全。一个简单的helloworld 驱动包含驱动的基本框架。我们继续往下看。

3.2 驱动的基本框架

Linux 驱动的基本框架主要由模块加载函数,模块卸载函数,模块许可证声明,模块参数,模块导出符号,模块作者信息等几部分组成,其中模块参数,模块导出符号,模块作者信息是可选的部分,也就是可要可不要。剩余部分是必须有的。我们来看一下这几个部分的作用:

  • 1 模块加载函数
    当使用加载驱动模块时,内核会执行模块加载函数,完成模块加载函数中的初始化工作。
  • 2 模块卸载函数
    当卸载某模块时,内核会执行模块卸载函数,完成模块卸载函数中的退出工作。
  • 3 模块许可证声明
    许可证声明描述了内核模块的许可权限,如果不声明模块许可,模块在加载的时候,会收到“内核被污染(kernel tainted)”的警告。可接受的内核模块声明许可包括“GPL”“GPL v2”。
  • 4 模块参数(可选择)
    模块参数是模块被加载的时候可以传递给它的值。
  • 5 模块导出符号(可选择)
    内核模块可以导出的符号,如果导出,其他模块可以使用本模块中的变量或函数。
  • 6 模块作者信息等说明(可选择)
    上一小节我们说,helloworld 驱动麻雀虽小五脏俱全,我们来分析helloworld 驱动。通过helloworld 代码再来看驱动框架。

(1)模块加载函数:

static int __init helloworld_init(void){ //驱动入口函数
    printk(KERN_EMERG "helloworld_init\r\n");
    return 0;
}
module_init(helloworld_init); //注册入口函数

(2)模块卸载函数:

static void __exit helloworld_exit(void){ //驱动出口函数
	printk(KERN_EMERG "helloworld_exit\r\n");
}
module_exit(helloworld_exit); //注册出口函数

(3)模块许可证声明

MODULE_LICENSE("GPL v2"); //同意GPL 开源协议

(4)模块作者信息

MODULE_AUTHOR("topeet"); //作者信息

(5)头文件

#include <linux/module.h> //模块加载函数和卸载函数需要的头文件
#include <linux/kernel.h>

通过上面的分析,helloworld 驱动是不是包含了驱动框架的所有必要的部分呢。因此helloworld 驱动我们可以看作是驱动代码的模板。任何一个驱动代码都用它作为基础来编写实现。同学们要将他记忆下来!

第4 章内核模块实验

在上一章节我们编写了最简单的helloworld 驱动程序。有了驱动程序以后,要如何编译并使用驱动呢。编译驱动有俩种方法,分别是将驱动编译成内核和将驱动编译成内核模块。我们先来学习如何将驱动编译成内核模块、(我没用买板子,我还是用的系统的gcc,如果大家有板子一定要用这个交叉编译器啊)

4.1 设置交叉编译器

1 下载网盘资料下的交叉编译器,网盘路径为:“iTOP-3568 开发板\03_【iTOP-RK3568 开发板】指南教程\02_Linux 驱动配套资料\03_交叉编译器”,将下载的交叉编译器拷贝到Ubuntu的/usr/local 目录下,如下图(图4-1)所示:

image-20240809153437994

2 输入以下命令,解压交叉编译编译器压缩包,解压完毕会生成“gcc-linaro-6.3.1-2017.05-x86_64_aarch64-linux-gnu”文件夹,这是实验需要的交叉编译工具,如下图(图4-2)所示:

tar -vxf gcc-linaro-6.3.1-2017.05-x86_64_aarch64-linux-gnu.tar.gz

image-20240809153503380

3 在终端输入“sudo vi /etc/profile”命令,在文件最后输入以下命令修改环境变量。

export PATH=$PATH:/usr/local/gcc-linaro-6.3.1-2017.05-x86_64_aarch64-linux-gnu/bin

image-20240809153736924

4 保存退出,在终端输入“reboot”命令重新启动Ubuntu 系统,使交叉编译环境生效。Ubuntu 系统重新启动之后,登录到系统后,打开终端,输入命令“aarch64-linux-gnu-gcc -v”,如果终端有如下图(图4-4)所示的打印信息,说明交叉编译环境搭建成功。如果没有出现如下图(图4-4)的打印信息,需要检查上一步骤是否设置正确。

image-20240809153808211

4.2 编写Makefile

编译驱动程序还需要使用Makefile 文件。我们为helloworld.c 编写一个简单的Makefile,Makefile 文件和源文件helloworld.c 位于同一级目录,代码如下(图4-5)所示:

export ARCH=arm64								#设置ARCH 变量为arm64
export CROSS_COMPILE=aarch64-linux-gnu-			#交叉编译器前缀
obj-m := helloworld.o						# helloworld.c 对应.o 文件的名称。名称要保持一致。
KDIR :=/home/topeet/Linux/linux_sdk/kernel    #这里是你的内核目录                                                                                                                            
PWD ?= $(shell pwd)
all:
	make -C $(KDIR) M=$(PWD) modules    #make操作
clean:
	make -C $(KDIR) M=$(PWD) clean    #make clean操作
第1 行设置ARCH 变量为arm64
第2 行设置交叉编译器前缀为aarch64-linux-gnu-
第3 行obj-m += <文件>:将指定的文件(需要是以.o 结尾)设为编译时以模块形式编译
第4 行是设备树内核的源码路径,请大家根据实际内核路径进行修改。
第5 行是获取当前目录的变量
第7 行是编译make 操作,会进入内核源码的路径,然后把当前路径下的代码编译成模块。
第9 行是清除编译文件

没有正开发板的就用下面的编译吧,我用的比较习惯,记得要改最后的obj-m:=hello.o

# Makefile
$(warning KERNELRELEASE=$(KERNELRELEASE))		# 使用$(warning)函数输出变量KERNELRELEASE的值

ifeq ($(KERNELRELEASE),)	# 判断条件,如果KERNELRELEASE为空,则执行以下代码块

# 定义变量KERNELDIR,如果未定义,则默认值为/lib/modules/当前内核版本/build
# 使用$(shell)函数获取当前内核版本
KERNELDIR ?= /lib/modules/$(shell uname -r)/build
# 使用$(shell)函数获取当前目录路径
PWD := $(shell pwd)

# 定义目标modules,通过$(MAKE)命令在$(KERNELDIR)目录下编译当前目录下的模块
modules:
	$(MAKE) -C $(KERNELDIR) M=$(PWD) modules
# 清理编译过程中生成的临时文件和目标文件
	rm -rfv *.o *~ core .depend .*.cmd *.mod.c *.mod .tmp_versions Module* modules*

# 定义目标modules_install,通过$(MAKE)命令在$(KERNELDIR)目录下安装当前目录下的模块
modules_install:
	$(MAKE) -C $(KERNELDIR) M=$(PWD) modules_install

# 定义目标clean,用于清理编译过程中生成的临时文件和目标文件
clean:
	rm -rfv *.o *~ core .depend .*.cmd *.ko *.mod.c *.mod .tmp_versions Module* modules*

# 定义伪目标.PHONY,用于标识以下目标为伪目标,即使文件系统中存在同名文件,也不会被认为是该目标的产物
.PHONY: modules modules_install clean

# 条件语句之外的代码块,用于定义obj-m变量,指向当前目录下的hello.o模块文件
else
	obj-m:=hello.o

endif

编写完成如下图(图4-6)所示:

image-20240809155321556

4.3 编译模块

有了Makefile 以后,输入“make”命令就可以编译helloworld 驱动模块,如下图(图4-7)所示:

image-20240809155341418

编译完生成helloworld.ko 目标文件就是我们需要的内核模块。内核模块是以ko 为后缀名,如下图(图4-8)所示:

image-20240809155355795

输入“make clean”命令清除编译文件,如下图(图4-9)所示:

image-20240809155432698

4.4 模块加载与卸载

有了内核模块以后,我们要如何使用呢?编译驱动有俩种方式,那Linux 驱动的运行方式也肯定有俩种。一种就是将驱动编译进内核,这样Linux 系统启动后会自动运行程序。第二种就是将驱动编译成模块,在Linux 系统启动以后使“insmod”命令加载驱动模块。在上个小节中编译了驱动模块helloworld.ko , 在RK3568 开发板上通过“ insmod helloworld.ko”命令可以加载驱动,在加载驱动模块的时候会执行驱动入口的函数,也就是helloworld 程序中的helloworld_init 函数, 所以可以看到打印出来的字符串信息“helloworld_init”。如下图(图4-10)所示:

image-20240809155548171

如果要卸载helloworld 内核模块,可以通过“rmmod helloworld”命令来卸载驱动模块,同理在卸载驱动模块的时候会执行驱动出口的函数,所以可以看到驱动出口函数打印出来的字符串信息“helloworld_exit”,如下图(图4-11)所示:

image-20240809155617210

加载驱动模块也可以使用modprobe 命令,它比insmod 命令更强大,modprobe 命令在加载驱动模块的时候,会同时加载该模块依赖的其他模块。比如helloworld.ko 依赖before.ko,使用insmod 加载的时候,就必须先加载before.ko,然后在加载helloworld.ko 才可以加载成功从。但是使用modprobe 加载的时候,他会自动分析模块的依赖关系,然后将所有的依赖的模块都加载到内核当中。比较“聪明”。

同样,在卸载驱动模块的时候,如果模块存在依赖关系,如果使用insmod 命令,需要手动卸载依赖的内核模块,但是使用modprobe 命令可以自动卸载驱动模块所依赖的其他模块。所以,如果驱动模块是以“modprobe helloworld.ko”命令加载的,卸载的时候使用“modprobe -r helloworld.ko”命令卸载。

但是使用modprobe 卸载存在一个问题,如果所依赖的模块被其他模块所使用,比如刚才例子中的before.ko 还被其他的模块使用,这时候就不能使用modprobe 卸载。所以还是推荐使用rmmod 命令来卸载。

4.5 查看模块信息

在驱动模块加载之后,使用“modinfo helloworld.ko”命令可以获得模块的信息,包括模块作者,模块说明,模块支持的参数等等。
lsmod 命令可以列出已经载入Linux 内核模块,在helloworld 驱动加载之后,查看内核中加载的模块,如下(图4-12)所示:

image-20240809155844334

第5 章 驱动模块传参实验

经过前两章实验的实战操作,我们已经完成最简单的helloworld 驱动实验和模块驱动实验,加载模块可以使用“insmod”函数,使用“insmod”函数进行模块加载时也能进行参数的传递。运用得当可以极大提升内核测试速度。本节就来学习一下如何进行驱动模块的传参。

5.1 驱动模块传参简介

驱动模块传参是一种可以随时向内核模块传递、修改参数的方法。例如可以传递串口驱动的波特率、数据位数、校验位、停止位等参数,进行功能的设置,以此节省编译模块的时间,大大提高调试速度。

Linux 内核提供了module_param(name, type, perm)module_param_array(name, type, nump, perm)宏和module_param_string(name, string, len, perm)宏,分别进行基本类型、数组和字符串参数的传递。它们定义在“内核源码/include/linux/moduleparam.h”文件中(在module.h 文件中已经对export.h 进行引用,所以不需要单独引用moduleparam.h 文件),详细定义如下(图5-1)所示:

#define module_param(name, type, perm) module_param_named(name, name, type, perm)

#define module_param_array(name, type, nump, perm) module_param_array_named(name, name, type, nump, perm)

#define module_param_string(name, string, len, perm) \
	static const struct kparam_string __param_string_##name \
    	= { len, string }; \
    __module_param_call(MODULE_PARAM_PREFIX, name, \
        &param_ops_string, \
        .str = &__param_string_##name, perm, -1, 0);\
    __MODULE_PARM_TYPE(name, "string")

以上宏定义中的module_param ()函数可以用来进行基本类型参数的传递,传入的三个参数定义如下:

  • name:模块参数的名称
  • type: 模块参数的数据类型
  • perm: 模块参数的访问权限

参数type 可以取以下任意一种情况:

bool : 布尔型
inbool : 布尔反值
charp: 字符指针(相当于char *,不超过1024 字节的字符串)
short: 短整型
ushort : 无符号短整型
int : 整型
uint : 无符号整型
long : 长整型
ulong: 无符号长整型。

参数perm 表示该参数在sysfs 文件系统中所对应的文件节点的属性,其权限定义在“内核源码/include/linux/stat.h”文件中。可以用宏定义和数字法两种方式来表示。详细宏定义如下(图5-2)所示:

#define S_IRUSR 00400 /*文件所有者可读*/
#define S_IWUSR 00200 /*文件所有者可写*/
#define S_IXUSR 00100 /*文件所有者可执行*/
#define S_IRGRP 00040 /*与文件所有者同组的用户可读*/
#define S_IWGRP 00020 /*与文件所有者同组的用户可写*/
#define S_IXGRP 00010 /*与文件所有者同组的用户可执行*/
#define S_IROTH 00004 /*与文件所有者不同组的用户可读*/
#define S_IWOTH 00002 /*与文件所有者不同组的用户可写*/
#define S_IXOTH 00001 /*与文件所有者不同组的用户可可执行*/

如果要传递数组类型参数可以使用module_param_array ()函数,相较于module_param ()函数多了n_para 参数,用来表示传递参数个数;n_para 参数值会根据输入的参数个数而改变,n_para的最终值为传递的数组元素个数。

最后是module_param_string(name, string, len, perm)函数,用来传递字符串类型的变量,四个参数的定义如下所示:

  • name:外部传入的参数名,即加载模块时的传入值
  • string:内部的变量名,即程序内定义的参数名
  • len:以string 命名的buffer 大小(可以小于buffer 的大小,但是没有意义)
  • perm:模块参数的访问权限

至此,关于驱动模块传参所使用的函数就讲解完成了,在下一小节中将编写驱动模块传参函数代码。

5.2 实验程序的编写

本实验对应的网盘路径为:iTOP-RK3568 开发板【底板V1.7 版本】\03_【iTOP-RK3568开发板】指南教程\02_Linux 驱动配套资料\04_Linux 驱动例程\02。

本章实验将编写Linux 下的驱动传参实例代码,通过“insmod”命令进行参数的传递,并将相应的参数打印到串口终端上。

编写完成的parameter.c 代码如下(图5-3)所示

#include <linux/init.h>
#include <linux/module.h>
static int number;              //定义int 类型变量number
static char *name;              //定义char 类型变量name
static int para[8];             //定义int 类型的数组
static char str1[10];           //定义char 类型字符串str1
static int n_para;              //定义int 类型的用来记录module_param_array 函数传递数组元素个数的变量n_para
module_param(number, int, S_IRUGO);//传递int 类型的参数number,S_IRUGO 表示权限为可读
module_param(name, charp, S_IRUGO);//传递char 类型变量name
module_param_array(para , int , &n_para , S_IRUGO);//传递int 类型的数组变量para
module_param_string(str, str1 ,sizeof(str1), S_IRUGO);//传递字符串类型的变量str1
static int __init parameter_init(void)//驱动入口函数
{
    static int i;
    printk(KERN_EMERG "%d\n",number);
    printk(KERN_EMERG "%s\n",name);
    for(i = 0; i < n_para; i++)
    {
        printk(KERN_EMERG "para[%d] : %d \n", i, para[i]);
    }
    printk(KERN_EMERG "%s\n",str1);
    return 0;
}
static void __exit parameter_exit(void)//驱动出口函数
{
    printk(KERN_EMERG "parameter_exit\n");
}
module_init(parameter_init);//注册入口函数
module_exit(parameter_exit);//注册出口函数
MODULE_LICENSE("GPL v2");//同意GPL 开源协议
MODULE_AUTHOR("topeet"); //作者信息

以上代码将传递int 类型参数number、char 类型参数name、int 类型的数组para 和char类型字符串str1,并在驱动入口函数中,对各个参数进行打印。在下一小节会进行驱动加载测试。

5.3 运行测试

5.3.1 编译驱动程序

在上一小节中的parameter.c 代码同一目录下创建Makefile 文件,Makefile 文件内容如下(图5-4)所示:

$(warning KERNELRELEASE=$(KERNELRELEASE))

ifeq ($(KERNELRELEASE),)

KERNELDIR ?= /lib/modules/$(shell uname -r)/build
PWD := $(shell pwd)

modules:
	$(MAKE) -C $(KERNELDIR) M=$(PWD) modules
	rm -rfv *.o *~ core .depend .*.cmd *.mod.c *.mod .tmp_versions Module* modules*

modules_install:
	$(MAKE) -C $(KERNELDIR) M=$(PWD) modules_install

clean:
	rm -rfv *.o *~ core .depend .*.cmd *.ko *.mod.c *.mod .tmp_versions Module* modules*

.PHONY: modules modules_install clean

else
	obj-m:=parameter.o

endif

对于Makefile 的内容注释已在上图添加,保存退出之后,来到存放parameter.c 和Makefile 文件目录下,然后使用命令“make”进行驱动的编译,编译完生成parameter.ko 目标文件,至此我们的驱动模块就编译成功了,下面进行驱动的运行测试。

5.3.2 运行测试

在上一章节中已经学习了使用insmod 命令加载模块,而驱动模块传参的命令格式为

insmod 对应的模块参数

parameter.ko 驱动加载可以传递3 个参数,分别为int 类型的参数number,char 类型的参数name 和int 数组类型的参数para。使用以下命令进行驱动的加载,加载完成之后的打印信息如下图(图5-8)所示:

insmod parameter.ko number=100 name="topeet" para=0,1,2,3,4,5,6,7 str="itop"

image-20240809161725269

可以看到传递的参数都分别打印了出来。最后可以输入以下命令进行驱动的卸载,如下图(图5-9)所示:

rmmod parameter.ko

image-20240809161814051

第6 章内核模块符号导出实验

在上一小节中,给大家讲解了驱动模块传参实验,使用insmod 命令加载驱动时可以进行参数的传递,但是每一个内核模块之间是相互独立的,那模块间的符号传递要怎样进行呢,让我们带着疑问来进行本章节的学习吧!

6.1 内核模块符号导出简介

驱动程序编译生成的ko 文件是相互独立的,即模块之间变量或者函数在正常情况下无法进行互相访问。而一些复杂的驱动模块需要分层进行设计,这时候就需要用到内核模块符号导出。

内核符号导出指的是在内核模块中导出相应的函数和变量,在加载模块时被记录在公共内核符号表中,以供其他模块调用。符号导出所使用的宏为EXPORT_SYMBOL(sym)EXPORT_SYMBOL_GPL(sym)。它们定义在“内核源码/include/linux/export.h”文件中(在module.h 文件中已经对export.h 进行引用,所以不需要单独引用export.h 文件),详细定义如下(图6-1)所示:

#define EXPORT_SYMBOL(sym) __EXPORT_SYMBOL(sym, "")
#define EXPORT_SYMBOL_GPL(sym) __EXPORT_SYMBOL(sym, "_gpl")

EXPORT_SYMBOL(sym)EXPORT_SYMBOL_GPL(sym)两个宏使用方法相同,而EXPORT_SYMBOL_GPL(sym)导出的模块只能被GPL 许可的模块使用,所以绝大多数的情况都使用EXPORT_SYMBOL(sym)进行符号导出。sym 为函数的唯一参数,表示要导出的函数或变量名称。

至此,关于内核模块符号导出函数就讲解完成了,在下一小节中将会编写两个驱动代码来进行内核模块符号导出实验。

6.2 实验程序的编写

本实验对应的网盘路径为:iTOP-RK3568 开发板【底板V1.7 版本】\03_【iTOP-RK3568 开发板】指南教程\02_Linux 驱动配套资料\04_Linux 驱动例程\03。

本章实验将编写Linux 下的内核模块符号导出实例代码,总共有两个驱动程序,第一个驱动文件名为mathmodule.c,用来定义参数num 和函数add(a,b),第二个驱动文件名为hello.c,会引用mathmodule.c 驱动程序中的参数num 和数学函数add(a,b),并将相应的参数值和函数返回值打印到串口终端上。

编写完成的mathmodule.c 代码如下(图6-2)所示

#include <linux/init.h>
#include <linux/module.h>
int num = 10;//定义参数num
EXPORT_SYMBOL(num);//导出参数num

int add(int a, int b)//定义数学函数add(),用来实现加法
{
    return a + b;
}
EXPORT_SYMBOL(add);//导出数学函数add()

static int __init math_init(void)//驱动入口函数
{
    printk("math_moudle init\n");
    return 0;
}

static void __exit math_exit(void)//驱动出口函数
{
    printk("math_module exit\n");
}

module_init(math_init);//注册入口函数
module_exit(math_exit);//注册出口函数

MODULE_LICENSE("GPL");//同意GPL开源协议
MODULE_AUTHOR("topeet");//作者信息

以上代码定义了一个int 类型的num 变量和add()数学函数,并使用EXPORT_SYMBOL 宏进行导出。
编写完成的hello.c 代码如下(图6-3)所示:

#include <linux/init.h>
#include <linux/module.h>
extern int num;//导入int类型变量num
extern int add(int a, int b);//导入函数add
static int __init hello_init(void)//驱动入口函数
{
    static int sum;
    printk("num = %d\n", num);//打印num值
    sum = add(3, 4);//使用add函数进行3+4的运算                                                                                                                                                               
    printk("sum = %d\n", sum);//打印add函数的运算值
    return 0;
}

static void __exit hello_exit(void)//驱动出口函数
{
    printk("Goodbye hello module\n");
}

module_init(hello_init);//注册入口函数
module_exit(hello_exit);//注册出口函数

MODULE_LICENSE("GPL");//同意GPL开源协议
MODULE_AUTHOR("topeet");//作者信息

程序导入了int 类型的变量num 和add()函数,并在驱动入口函数中打印相应了num 的参数值并对add()函数进行了调用。

至此两个驱动代码就编写完成了,代码较为简单,实现了内核模块符号的导出和导出符号的使用,具体的驱动加载运行测试会在下个小节进行。

6.3 运行测试

6.3.1 编译驱动程序

在mathmodule.c 和hello.c 的同一目录下创建Makefile 文件,Makefile 文件内容如下(图6-4)所示:

export ARCH=arm64#设置平台架构
export CROSS_COMPILE=aarch64-linux-gnu-#交叉编译器前缀
obj-m := mathmodule.o
obj-m += hello.o
KDIR :=/home/topeet/Linux/linux_sdk/kernel    #这里是你的内核目录                                                                                                                            
PWD ?= $(shell pwd)
all:
	make -C $(KDIR) M=$(PWD) modules    #make操作
clean:
	make -C $(KDIR) M=$(PWD) clean    #make clean操作

对于Makefile 的内容注释已在上图进行添加,这里要注意的是在hello.c 代码中使用了mathmodule.c 所导出的符号,所以mathmodule.c 要在hello.c 之前进行编译,即第3 行和第4行顺序不能交换。保存退出之后,来到相应的文件目录下

(没有开发板的用这个验证)

# Makefile
$(warning KERNELRELEASE=$(KERNELRELEASE))	

ifeq ($(KERNELRELEASE),)	# 判断条件,如果KERNELRELEASE为空,则执行以下代码块

KERNELDIR ?= /lib/modules/$(shell uname -r)/build
PWD := $(shell pwd)

modules:
	$(MAKE) -C $(KERNELDIR) M=$(PWD) modules
	rm -rfv *.o *~ core .depend .*.cmd *.mod.c *.mod .tmp_versions Module* modules*

modules_install:
	$(MAKE) -C $(KERNELDIR) M=$(PWD) modules_install

clean:
	rm -rfv *.o *~ core .depend .*.cmd *.ko *.mod.c *.mod .tmp_versions Module* modules*

.PHONY: modules modules_install clean

else
	obj-m:=mathmodule.o
	obj-m+=hello.o				# 这个一定是+= ,不能写成:=
endif

然后使用命令“make”进行驱动的编译,:编译完后会生成hello.ko 和mathmodule.ko 目标文件,至此我们的驱动模块就编译成功了,下面进行驱动的加载运行测试。

6.3.2 运行测试

这里要注意的是,由于hello.ko 依赖于mathmodule.ko,所以mathmodule.ko 需要先加载,分别使用以下命令进行模块的加载(加载顺序不能变),如下(图6-8)所示:

insmod mathmodule.ko
insmod hello.ko

image-20240809163215167

可以看到hello.ko 驱动加载的时候,mathmodule.ko 模块中定义的num 参数值和调用sum()函数的后正确的返回值都被打印了出来。至此内核模块符号导出实验就完成了。

最后可以输入以下命令进行驱动的卸载,如下图(图6-9)所示:

rmmod hello.ko
rmmod mathmodule.ko

image-20240809163326808

注意:由于hello.ko 文件使用了mathmodule.ko 导出的符号,所以要先卸载hello.ko,卸载完成之后再卸载mathmodule.ko

第二篇 字符设备基础

第7 章 menuconfig 图形化配置实验

Linux 内核可以通过输入“make menuconfig”来打开图形化配置界面,menuconfig 是一套图形化的配置工具,本章节来学习使用menuconfig 配置内核。

7.1 图形化界面的操作

menuconfig 图形化的配置工具需要ncurses 库支持。ncurses 库提供了一系列的API 函数供调用者生成基于文本的图形界面,因此在使用menuconfig 图形化配置界面之前需要先在Ubuntu 中安装ncurses 库,命令如下:

sudo apt-get install build-essential
sudo apt-get install libncurses5-dev

图形化配置界面主要有以下四种,在这四种方式中,最推荐的是make menuconfig,它不依赖于QT 或GTK+,且非常直观。

make config 		#(基于文本的最为传统的配置界面,不推荐使用)
make menuconfig 	#(基于文本菜单的配置界面)
make xconfig 		#(要求QT 被安装)
make gconfig 		#(要求GTK+ 被安装)

如何打开menuconfig 图形化配置界面呢?
以RK3568 为例,在内核源码目录下输入以下命令,打开图形化配置界面。

export ARCH=arm64
make rockchip_linux_defconfig
make menuconfig

image-20240812092209563

打开后界面如下所(图7-2)示:

image-20240812092234128

打开menuconfig 图形化配置界面以后,可以使用以下方式进行操作,如下表(表7-3)所示:

image-20240812092350198

7.2 Kconfig 语法简介

上一小节我们打开的图形化配置界面是如何生成的呢?图形化配置界面中的每一个界面都会对应一个Kconfig 文件。所以图形化配置界面的每一级菜单是由Kconfig 文件来决定的。
图形化配置界面有很多菜单。所以就会有很多Kconfig 文件,这也就是为什么我们会在内核源码的每个子目录下,都会看到Kconfig 文件的原因,那掌握Kconfig 文件相关的知识是不是就非常重要呢。
所以这一小节我们来看下如何编写Kconfig 文件来生成图形化配置界面,也就是Kconfig 文件的语法是什么。

  1. Mainmenu

mainmenu 顾名思义就是主菜单,也就是我们输入完“make menuconfig”以后默认打开的界面,mainmenu 用来设置主菜单的标题,如下所示:

mainmenu "Linux/$(ARCH) $(KERNELVERSION) Kernel Configuration"

此行代码是设置菜单的名字为“Linux/$(ARCH) $(KERNELVERSION) Kernel Configuration”。如下图(图7-4)所示,ARCH 变量是通过“export ARCH=arm64”设置的,内核版本KERNELVERSION为4.19.232。

image-20240812092504252

2.source

source 用于读取另一个Kconfig 文件,比如“source “init/Kconfig””就是读取init 目录下的Kconfig 文件。

3.menu/endmenu

menu/endmenu 条目用于生成菜单,如下(图7-5)所示,生成了Watchdog Timer Support的菜单。

menu "Watchdog Timer Support"

config HW_WATCHDOG
bool

config WDT
bool "Enable driver model for watchdog timer drivers"
depends on DM
help
	Enable driver model for watchdog timer. At the moment the API.
......
endmenu

menu 之后的字符串是菜单名,“menu”是菜单开始的标志,“endmenu”是菜单结束的标志,这俩个是成对出现的。“menu”和“endmenu”之间有很多config 条目。在kernel 目录下输入make menuconfig,如下图(图7-6)所示,可以看到上述代码描述的”Watchdog Timer Support”菜单。

image-20240812092707251

进入“ Watchdog Timer Support —> ”可以看到很多config 定义的条目,如下(图7-7)所示:

image-20240812092717679

4.if/endif
if/endif 语句是一个条件判断,定义了一个if 结构,Kconfig 中代码如下(图7-8)所示:

menu "Hardware Drivers Config"
    menuconfig BSP_USING_CAN
        bool "Enable CAN"
        default n
        select RT_USING_CAN
            if BSP_USING_CAN
            config BSP_USING_CAN1
            bool "Enable CAN1"
            default n
            endif
endmenu

当没有选中”Enable CAN” 选项时,下面通过if 判断的Enable CAN1 选项并不会显示出来。当上一级菜单选中”Enable CAN” 时,Enable CAN1 选项才会显示。

5.choice/endchooice

choice 条目将多个类似的配置选项组合到一起,供用户选择,用户选择是从“choice”开始,从“endchoice”结束,“choice”和“endchoice”之间有很多的config 条目,这些config 条目是提供用户选择的,如下(图7-9)所示:

choice
    bool "Parade TrueTouch Gen5 MultiTouch Protocol"
    depends on TOUCHSCREEN_CYPRESS_CYTTSP5
    default TOUCHSCREEN_CYPRESS_CYTTSP5_MT_B
    help
        This option controls which MultiTouch protocol will be used to
        report the touch events.
config TOUCHSCREEN_CYPRESS_CYTTSP5_MT_A
    bool "Protocol A"
    help
        Select to enable MultiTouch touch reporting using protocol A
        on Parade TrueTouch(tm) Standard Product Generation4 touchscreen
        controller.
        
config TOUCHSCREEN_CYPRESS_CYTTSP5_MT_B
    bool "Protocol B"
    help
        Select to enable MultiTouch touch reporting using protocol B
        on Parade TrueTouch(tm) Standard Product Generation4 touchscreen
        controller.
endchoice

我们在内核目录下输入make menuconfig 可以看到,如下(图7-10)所示,“Parade TrueTouch Gen5 MultiTouch Protocol”是choice 选项名称,“Protocol B”是Kconfig 里面默认选择的。“–>”代表此菜单能进入,需要键盘操作进入。

image-20240812092955653

进入“Parade TrueTouch Gen5 MultiTouch Protocol”后,可以看到多选项提供给用户进行选择,如下(图7-11)所示:

image-20240812093006104

6.comment
comment 语句出现在界面的第一行,用于定义一些提示信息。

comment "Compiler: $(CC_VERSION_TEXT)"

以上代码的配置界面如下(图7-12)所示:

image-20240812093031255

7.config
使用关键字config 来定义一个新的选项,如下(图7-13)所示

config helloworld
bool “hello world support”
default y
help
hello world

如上所示,使用config 关键字定义了一个“helloworld”选项,每个选项都必须指定类型,类型包括bool,tristate,string,hex,int。最常见的是bool,tristate,string 这三个。

  • bool 类型取值只有“y”和“n”
  • tristate 类型的变量取值有3 种:“y”,“n”,“m”
  • string 类型取值为字符串
  • hex 类型取值为十六进制的数据
  • int 类型取值为十进制的数据
  • help 表示帮助信息,当我们在图形化界面按下h 按键,弹出来的就是help 的内容。

8.depends on
Kconfig 中depends on 关键字用来指定依赖关系,当依赖的选项被选中时,当前的配置选项的信息才会在菜单中显示出来,才能操作该选项的内容。举例来说,如下所示,选项A 依赖选项B,只有当选项B 被选中时,选项A 才可以被选中。

config A
depends on B

9.select
Kconfig 中select 关键字用来表示反向依赖关系,当指定当前选项被选中时,此时select 后面的选项也会被自动选中。举个例子来说,如下所示,在选项A 被选中的情况下,选项B 自动被选中。

config A
select on B

10.menuconfig
menuconfig 可以认为是config 中的升级版。menuconfig 也是一个正常的配置项,通过自己的配置值来决定另外一组配置项是否作为子菜单的形式显示出来并供用户配置。代码如下(图7-14)所示。

menuconfig NETDEVICES
default y if UML
depends on NET
bool "Network device support"
---help---
    if NETDEVICES
    config MII
    tristate
    config NET_CORE
    default y
bool "Network core driver support"
---help---
    You can say N here if you do not intend to use any of the
    networking core drivers (i.e. VLAN, bridging, bonding, etc.)

以上代码中通过menuconfig 配置了一个bool 类型的配置项,在图形化配置界面中显示(图7-15)如下:

image-20240812093405740

当我们选中”Network device support”配置项时,其子菜单被显示出来,如下图(图7-16)所示:

image-20240812093415956

7.3 .config 配置文件介绍

我们在图形化配置界面配置好了以后,会得到一个.config 配置文件。在编译内核的时候会根据这个.config 文件来编译内核。这样是不是就实现了通过图像化界面的配置来配置内核呀。用通俗的话来说,Kconfig 就是饭店的菜单,.config 就是客人点完的菜。然后厨师会根据客人点的菜,也就是.config 来做菜,对应的操作就是编译内核。
那.config 是如何产生的呢?对应上面的例子就是要有服务员给我们点菜呀。
当我们使用make menuconfig 的时候,会通过mconf 程序去解析Kconfig 文件,然后生成对应的配置文件.config。所以这个mconf 就是服务员。
mconf 程序源码在内核源码scripts/kconfig 目录下,如下图所示,这里不对Kconfig 文件的解析流程进行分析,感兴趣的同学可以自行分析下mconf 的源码。

image-20240812093511361

有了.config 配置文件以后,内核就可以根据这个配置文件来编译内核,比如控制某些驱动编译进内核,或者控制某些驱动不编译内核。那他是怎么实现的呢?
.config 会通过syncconfig 目标将.config 作为输入然后输出需要文件,这里我们重点更关注auto.conf 和autoconf.h。如下图(图7-19)所示:

image-20240812093530292

在auto.conf 文件中,存放的是配置信息。如下图(图7-20)所示:

image-20240812093543848

在内核源码的顶层Makefile 中会包含auto.conf 文件,以此引用其中的变量来控制Makefile的动作,如哪些驱动编译,哪些驱动不编译。如:

auto.conf 文件中

include include/config/auto.conf
CONFIG _A=y

顶层Makefile 中包含auto.conf 文件

ifeq ($(dot-config),1)
include include/config/auto.conf
Endif

内核源码下drivers/A/Makefile 引用这个变量

obj-$(CONFIG _A) +=A.o

注:obj-y 就是编译进内核,obj-m 就是编译成ko 文件。
在autoconf.h 中,是C 语言代码。用来配合编译时的条件选择。如下图(图7-21)所示:

image-20240812093655830

7.4 defconfig 配置文件

defconfig 文件和.config 文件都是linux 内核的配置文件,defconfig 文件在内核源码的arch/$(ARCH)/configs 目录下,是Linux 系统默认的配置文件。比如说瑞芯微平台Linux 源码默认的配置文件为:kernel/arch/arm64/configs/rockchip_linux_defconfig

.config 文件位于Linux 内核源码的顶层目录下,编译Linux 内核时会使用.config 文件里面的配置来编译内核镜像。
如果.config 文件存在,make menuconfig 界面的默认配置也就是当前.config 文件的配置,如果修改了图形化配置界面的设置并保存,那么.config 文件会被更新。

如果.config 文件不存在,使用命令“make XXX_defconfig”命令会根据arch/$(ARCH)/configs目录下的XXX_defconfig 自动生成.config。make menuconfig 界面的默认配置则为defconfig 文件中的默认配置,比如说瑞芯微平台Linux 内核源码目录下输入“make rockchip_linux_defconfig”会自动生成.config 文件。那么此时rockchip_linux_defconfig 的配置项和.config 的配置项是相同的。

7.5 自定义菜单实验

有了上面的理论基础后,我们就可以自己在图形化配置界面中来自定义一个菜单,要定义一个菜单,根据我们前面的分析,是不是就要从Kconfig 文件入手呀。因为图形化配置界面是根据Kconfig 文件来生成的!
1 在kernel 目录下创建一个topeet 的文件夹,如下(图7-22)所示:

image-20240812093832690

2 打开kernel 下的Kconfig 文件,在里面加入以下代码:

source "topeet/Kconfig"

添加完成后如下(图7-23)所示:

image-20240812093852174

3 然后进入到topeet 文件夹,在此文件夹下创建一个Kconfig 文件,创建完成如下(图7-24)所示:

image-20240812093904979

4 打开创建好的Kconfig 文件,写入以下(图7-25)内容:

menu "test menu"
    config TEST_CONFIG
    bool "test"
    default y
    help
        just test
        comment "just test"
endmenu

在上面的代码中,我们在主菜单中添加了一个名为test menu 的子菜单,然后在这个子菜单里面我们添加了一个名为TEST_CONFIG 的配置项,这个配置项变量类型为bool,默认配置为Y,帮助信息为just test,注释为just test。添加完成如下图(图7-26)所示:

image-20240812093947114

5 添加完成以后,打开图形化配置界面,如下图(图7-27)所示:

image-20240812094012606

6 子菜单中的配置项,默认为y,注释信息为just test。

image-20240812094023596

7 在此界面输入?,显示帮助信息为just test,如下(图7-29)所示:

image-20240812094035478

8 保存退出后,打开内核源码目录下的.config 文件,如下图(图7-30)所示:

image-20240812094048281

9 可以在这个.config 文件中找到添加的TEST_CONFIG ( 注意, 我们需要在make menuconfig 中保存才可以看到,否则是看不到我们添加的这个选项的),这样在编译内核的时候就可以根据这个配置信息来执行对应的操作了,就是我们下一章节要给大家讲的把驱动编译进内核,如下图所示:

image-20240812094104457

第8 章驱动模块编译进内核实验

通过上一章的学习,我们学会了使用menuconfig 图形化配置工具,以及了解了menuconfig相关的文件:Kconfig .config XXXdefconfig。本章节学习将helloworld 驱动编译进内核。

输入“cd drivers/char”进入到drivers/char 目录下,然后输入“mkdir hello”建立hello文件夹,并输入“cd hello”进入hello 文件夹,如下(图8-1)所示:

image-20240812094139507

然后将第三章编写的hello.c 文件拷贝到hello 文件夹内。如下(图8-2)所示:

image-20240812094149918

输入“touch Kconfig”命令创建Kconfig 文件,Kconfig 文件内容如下所示:

config HELLO
tristate "hello world"
help
hello hello

然后“touch Makefile”命令创建Makefile 文件,Makefile 文件内容如下所示:

obj-$(CONFIG_HELLO)+=helloworld.o

接下来修改上一级目录的Kconfig 文件和Makefile 文件,也就是driver/char 目录。Makefile添加如下图(图8-3)所示内容。

obj-y += hello/

image-20240812094240968

Kconfig 添加如下图(图8-4)所示内容:

source "drivers/char/hello/Kconfig"

image-20240812094306868

最后打开menuconfig 图形化配置工具,在配置界面选择helloworld 驱动。把驱动编译进Linux 内核,用* 来表示,所以配置选项改为*。如果想要将驱动编译为模块,则用M 来表示,配置选项改为M。这里我们选择成*

Device Drivers ─>
    Character devices --->
        <*> hello world 或者<M> hello world

image-20240812094409843

然后将光标移动到save,保存配置,如下图(图8-6)所示:

image-20240812094419951

保存到.config 文件,如下(图8-7)所示:

image-20240812094430377

退出配置界面,然后输入以下命令便可以编译源码了。

make savedefconfig
cp defconfig arch/arm64/configs/rockchip_linux_defconfig
cd ../
./build.sh kernel

image-20240812094451505

编译成功之后,进入到drivers/char/hello 目录下,可以看到会生成对应的.o 文件。就说明已经成功将驱动编译进内核。

image-20240812103050086

将编译好的内核镜像烧写到开发板上后,在开发板系统启动的时候也可以成功看到加载helloworld 驱动,如下图(图8-10)所示:

image-20240812103105611

如果在图形化配置界面中选择的M,也就是编译成驱动模块,则生成helloworld.ko 文件如下图(图8-11)所示:

image-20240812103118578

第9 章申请字符设备号实验

经过前面章节的学习,相信大家已经对驱动模块的基本框架、驱动模块传参等知识有了自己的认识,本章节开始就要进入字符设备的世界了。字符设备是指在I/O 传输过程中以字符为单位进行传输的设备,可以使用与普通文件相同的文件操作命令(打开、关闭、读、写等)对字符设备进行操作,是Linux 驱动中最基本的一类设备驱动,例如最常见的LED、按键、IIC、SPI,LCD 等都属于字符设备的范畴。要想对字符设备进行操作,需要通过设备号来对相应的设备进行查找,在本章节将对设备号相关知识进行讲解。

9.1 申请驱动设备号

9.1.1 设备号申请

在Linux 系统中每一个设备都有相应的设备号,通过该设备号查找对应的设备,从而进行之后的文件操作。设备号有主设备号与次设备号之分,主设备号用来表示一个特定的驱动,次设备号用来管理下面的设备。

在Linux 驱动中可以使用以下两种方法进行设备号的申请:
1.通过register_chrdev_region(dev_t from, unsigned count, const char *name)函数进行静态申请设备号。

2.通过alloc_chrdev_region(dev_t *dev, unsigned baseminor, unsigned count,const char* name)函数进行动态申请设备号。
两个函数在“内核源码/include/linux/fs.h”文件中引用(在编写驱动程序的时候要加入该文件的引用),如下(图9-1)所示:

extern int alloc_chrdev_region(dev_t *, unsigned, unsigned, const char *);
extern int register_chrdev_region(dev_t, unsigned, const char *);

静态申请设备号:

函数原型:
    register_chrdev_region(dev_t from, unsigned count, const char *name)
函数作用:
    静态申请设备号,对指定好的设备号进行申请。
参数含义:
    from: 自定义的dev_t 类型设备号
    count: 申请设备的数量
    name: 申请的设备名称
函数返回值:申请成功返回0,申请失败返回负数

动态申请设备号:

函数原型:
    alloc_chrdev_region(dev_t *dev, unsigned baseminor, unsigned count,const char *name)
函数作用:
    动态申请设备号,内核会自动分配一个未使用的设备号,相较于静态申请设备号,动态申请会避免注册设备号相同引发冲突的问题。
参数含义:
    dev *: 会将申请完成的设备号保存在dev 变量中
    baseminor: 次设备号可申请的最小值
    count: 申请设备的数量
    name: 申请的设备名称
函数返回值:申请成功返回0,申请失败返回负数

对于申请设备号所用到的函数就讲解完成了,会在之后的测试小节对两个函数进行实际运用。

9.1.2 设备号类型

申请的设备号类型为dev_t ,在“内核源码/include/linux/types.h” 文件中定义如下(图9-2)所示:

typedef u32 __kernel_dev_t;
....
typedef __kernel_dev_t dev_t;

dev_t 为u32 类型,而u32 定义在文件“内核源码/include/uapi/asm-generic/int-ll64.h”文件中,定义如下(图9-3):

typedef unsigned int __u32;

__u32unsigned int 类型,所以dev_t 是一个无符号的32 位整形类型。其中高12 位表示主设备号,低20 位表示次设备号。在“内核源码/include/linux/kdev_t.h”中提供了设备号相关的宏定义,如下(图9-4)所示:

#define MINORBITS 20 /*次设备号位数*/
#define MINORMASK ((1U << MINORBITS) - 1) /*次设备号掩码*/
#define MAJOR(dev) ((unsigned int) ((dev) >> MINORBITS))/*dev 右移20 位得到主设备号*/
#define MINOR(dev) ((unsigned int) ((dev) & MINORMASK)) /*与次设备掩码与,得到次设备号*/
#define MKDEV(ma,mi) (((ma) << MINORBITS) | (mi))/*MKDEV 宏将主设备号(ma)左移20 位,然后与次设备号(mi)相与,得到设备号*/

在稍后的实验中不论是静态申请设备号还是动态申请设备号都会用到上述宏,例如在静态申请设备号时需要将指定的主设备号和从设备号通过MKDEV(ma,mi)宏进行设备号的转换,在动态申请设备号时可以用MAJOR(dev) 和MINOR(dev)宏将动态申请的设备号转化为主设备号和从设备号。
至此,关于设备号相关的知识就结束了,在下一小节中将对申请设备号实验代码进行编写。

9.2 实验程序的编写

本实验对应的网盘路径为:iTOP-RK3568 开发板【底板V1.7 版本】\03_【iTOP-RK3568 开发板】指南教程\02_Linux 驱动配套资料\04_Linux 驱动例程\04。
本章节实验将编写Linux 下申请字符设备号实例代码,如果在进行驱动模块加载时传入了major 主设备号,则通过静态的方式进行设备号的申请,如果不传入任何参数进行驱动模块加载,则通过动态的方式进行设备号申请。
编写完成的dev_t.c 代码如下(图9-5)所示

#include <linux/kernel.h>
#include <linux/init.h>
#include <linux/module.h>
#include <linux/fs.h>
#include <linux/kdev_t.h>
#include <linux/cdev.h>

static int major;//定义静态加载方式时的主设备号参数major
static int minor;//定义静态加载方式时的次设备号参数minor
module_param(major,int,S_IRUGO);//通过驱动模块传参的方式传递主设备号参数major
module_param(minor,int,S_IRUGO);//通过驱动模块传参的方式传递次设备号参数minor
static dev_t dev_num;//定义dev_t类型(32位大小)的变量dev_num

static int __init dev_t_init(void)//驱动入口函数
{
	int ret;//定义int类型的变量ret,用来判断函数返回值
	/*以主设备号进行条件判断,即如果通过驱动传入了major参数则条件成立,进入以下分支*/
	if(major){
    	dev_num = MKDEV(major,minor);//通过MKDEV函数将驱动传参的主设备号和次设备号转换成dev_t类型的设备号
    	printk("major is %d\n",major);
    	printk("minor is %d\n",minor);
    	ret = register_chrdev_region(dev_num,1,"chrdev_name");//通过静态方式进行设备号注册
        if(ret < 0){
            printk("register_chrdev_region is error\n");
        }
        printk("register_chrdev_region is ok\n");
    }
	/*如果没有通过驱动传入major参数,则条件成立,进入以下分支*/
    else{
        ret = alloc_chrdev_region(&dev_num,0,1,"chrdev_num");//通过动态方式进行设备号注册
        if(ret < 0){
            printk("alloc_chrdev_region is error\n");
        }                                                                                                                                              
        printk("alloc_chrdev_region is ok\n");
        major=MAJOR(dev_num);//通过MAJOR()函数进行主设备号获取
        minor=MINOR(dev_num);//通过MINOR()函数进行次设备号获取
        printk("major is %d\n",major);
        printk("minor is %d\n",minor);
    }
    return 0;
}

static void __exit dev_t_exit(void)//驱动出口函数
{
    unregister_chrdev_region(dev_num,1);//注销字符驱动
    printk("module exit \n");
}

module_init(dev_t_init);//注册入口函数
module_exit(dev_t_exit);//注册出口函数
MODULE_LICENSE("GPL v2");//同意GPL开源协议
MODULE_AUTHOR("topeet");  //作者信息

以上代码通过对传入参数的判断,从而进行设备号申请方式的选择,会在下一小节进行相应的驱动加载测试。

9.3 运行测试

9.3.1 编译驱动程序

在上一小节中的dev_t.c 代码同一目录下创建Makefile 文件,Makefile 文件内容如下(图9-6)所示:

export ARCH=arm64	#设置平台架构
export CROSS_COMPILE=aarch64-linux-gnu-	#交叉编译器前缀
obj-m += dev_t.o	 #此处要和你的驱动源文件同名
KDIR :=/home/topeet/Linux/linux_sdk/kernel #这里是你的内核目录
PWD ?= $(shell pwd)
all:
make -C $(KDIR) M=$(PWD) modules #make 操作
clean:
make -C $(KDIR) M=$(PWD) clean #make clean 操作

对于Makefile 的内容注释已在上图添加,保存退出之后,来到存放dev_t.c 和Makefile 文件目录下,然后使用命令“make”进行驱动的编译,编译完生成dev_t.ko 目标文件至此我们的驱动模块就编译成功了,下面对驱动进行加载测试。

9.3.2 运行测试

开发板上电启动之后,使用以下命令加载dev_t.ko 驱动,加载完成之后的打印信息如下图图(9-10)所示:

insmod dev_t.ko major=200 minor=0

image-20240812104528538

可以看到传入的主设备号和次设备号都被打印了出来,“register_chrdev_region is ok”也被成功打印了证明设备注册成功了,然后使用以下命令进行注册设备号的查看,如下图(图9-11)所示:

cat /proc/devices

可以看到主设备号200 的设备名为chrdev_name,和驱动程序中设置的相同,证明我们的设备号注册成功了,然后使用以下命令进行驱动的卸载,如下图(图9-12)所示:

rmmod dev_t.ko

image-20240812104624263

下面进行动态申请设备号实验,使用以下命令进行驱动模块的加载,如下图(图9-13)所示:

insmod dev_t.ko

image-20240812104644361

可以看到动态申请设备号成功了,主设备号为236,次设备号为0,然后使用以下命令进行注册设备号的查看,如下

cat /proc/devices

image-20240812104722637

可以看到主设备号236 的设备名为chrdev_name,和驱动程序中设置的相同,证明我们的设备号注册成功了,最后可以输入以下命令对驱动进行卸载,卸载完成如下图(图9-15)所示:

rmmod dev_t.ko

image-20240812104740342

第10 章注册字符设备实验

在上一小节中已经对设备号的相关知识进行了讲解,并成功申请到了设备号,那在Linux系统中,设备号是怎样与字符设备进行关联的呢?字符设备又是怎样注册的呢?带着疑问,让我们开始本章节的学习吧。

10.1 注册字符设备

注册字符设备可以分为两个步骤:

  1. 字符设备初始化
  2. 字符设备的添加

在本小节将对上述两个步骤所用到的函数和结构体进行讲解。

10.1.1 字符设备初始化

字符设备初始化所用到的函数为cdev_init(...),在对该函数讲解之前,首先对cdev 结构体进行介绍。
Linux 内核中将字符设备抽象成一个具体的数据结构(struct cdev), 我们可以理解为字符设备对象,cdev 记录了字符设备号、内核对象、文件操作file_operations 结构体(设备的打开、读写、关闭等操作接口)等信息,struct cdev 结构体定义在“内核源码/include/linux/cdev.h”文件中(在编写驱动程序的时候要加入该文件的引用),如下所示:

struct cdev {
    struct kobject kobj; //内嵌的内核对象.
    struct module *owner; //该字符设备所在的内核模块的对象指针.
    const struct file_operations *ops; //该结构描述了字符设备所能实现的方法,是极为关键的一个结构体.
    struct list_head list; //用来将已经向内核注册的所有字符设备形成链表.
    dev_t dev; //字符设备的设备号,由主设备号和次设备号构成.
    unsigned int count; //隶属于同一主设备号的次设备号的个数.
};

关于该结构体参数的注释在上图已经添加,设备初始化所用到的函数为cdev_init(),该函数同样在“内核源码/include/linux/cdev.h”文件中所引用如下所示:

void cdev_init(struct cdev *, const struct file_operations *);

该函数的详细内容在“内核源码/include/fs/char_dev.c”文件中定义,如下所示:

void cdev_init(struct cdev *cdev, const struct file_operations *fops)
{
    memset(cdev, 0, sizeof *cdev);//将整个结构体清零;
    INIT_LIST_HEAD(&cdev->list);//初始化list 成员使其指向自身;
    kobject_init(&cdev->kobj, &ktype_cdev_default);//初始化kobj 成员;
    cdev->ops = fops;//初始化ops 成员,建立cdev 和file_operations 之间的连接
}
函数作用:
    初始化传入的cdev 类型的结构体,并与自定义的file_operations * 类型的结构体进行链
接。
参数含义:
    cdev: 要传入的cdev 类型结构体,为要初始化的字符设备。
    fops:要传入的file_operations * 类型结构体,关于file_operations 结构体的相关的知识会在下一章节进行讲解。
函数返回值:无返回值。

10.1.2 字符设备的注册

字符设备的注册:
字符设备添加所用到的函数为cdev_add(),该函数在“内核源码/include/linux/cdev.h”文件中所引用,如下(图10-4)所示:

int cdev_add(struct cdev *, dev_t, unsigned);

函数原型:
	int cdev_add(struct cdev *p, dev_t dev, unsigned count)
函数作用:
    该函数向内核注册一个struct cdev 结构体
参数含义:
    (1)第一个参数为要添加的struct cdev 类型的结构体
    (2)第二个参数为申请的字符设备号
    (3)第三个参数为和该设备关联的设备编号的数量。
    这两个参数直接赋值给struct cdev 的dev 成员和count 成员。
函数返回值:添加成功返回0,添加失败返回负数。

字符设备的注销:
字符设备删除所用到的函数为cdev_del(),该函数同样在“内核源码/include/linux/cdev.h”文件中所引用,如下(图10-5)所示:

void cdev_del(struct cdev *);

函数原型:
    void cdev_del(struct cdev *p)
函数作用:
    该函数会向内核删除一个struct cdev 类型结构体
参数含义:
    该函数只有一个参数,为要删除的struct cdev 类型的结构体
函数返回值:无返回值

至此,关于注册字符设备实验所用到的函数就讲解完成了,在下一小节中将编写注册字符设备代码。

10.2 实验程序的编写

本实验对应的网盘路径为:iTOP-RK3568 开发板【底板V1.7 版本】\03_【iTOP-RK3568 开发板】指南教程\02_Linux 驱动配套资料\04_Linux 驱动例程\05
本实验采用动态申请设备号的方式进行设备号的申请,然后对设备进行注册,并将申请到的主设备号和次设备号以及设备注册情况打印到终端上。
编写完成的cdev.c 代码如下(图10-6)所示

#include <linux/init.h>
#include <linux/module.h>
#include <linux/fs.h>
#include <linux/kdev_t.h>
#include <linux/cdev.h>

static dev_t dev_num;//定义dev_t类型(32位大小)的变量dev_num,用来存放设备号
static struct cdev cdev_test;//定义cdev结构体类型的变量cdev_test
static struct file_operations cdev_test_ops = {
	.owner=THIS_MODULE,//将owner字段指向本模块,可以避免在模块的操作正在被使用时卸载该模块
};//定义file_operations结构体类型的变量cdev_test_ops

static int __init module_cdev_init(void)//驱动入口函数
{
    int ret;//定义int类型变量ret,进行函数返回值判断
    int major,minor;//定义int类型的主设备号major和次设备号minor
    ret = alloc_chrdev_region(&dev_num,0,1,"chrdev_name");//自动获取设备号,设备名为chrdev_name
    if (ret < 0){
        printk("alloc_chrdev_region is error\n");
    }
    printk("alloc_register_region is ok\n");
    major = MAJOR(dev_num);//使用MAJOR()函数获取主设备号
    minor = MINOR(dev_num);//使用MINOR()函数获取次设备号
    printk("major is %d\n",major);
    printk("minor is %d\n",minor);          	
    cdev_init(&cdev_test,&cdev_test_ops);//使用cdev_init()函数初始化cdev_test结构体,并链接到cdev_test_ops结构体
	cdev_test.owner = THIS_MODULE;//将owner字段指向本模块,可以避免在模块的操作正在被使用时卸载该模块
    ret = cdev_add(&cdev_test,dev_num,1);//使用cdev_add()函数进行字符设备的添加
    if(ret < 0 ){
        printk("cdev_add is error\n");
    }
    printk("cdev_add is ok\n");
    return 0;
}

static void __exit module_cdev_exit(void)//驱动出口函数
{
    cdev_del(&cdev_test);//使用cdev_del()函数进行字符设备的删除
    unregister_chrdev_region(dev_num,1);//释放字符驱动设备号 
    printk("module exit \n");
}

module_init(module_cdev_init);//注册入口函数
module_exit(module_cdev_exit);//注册出口函数
MODULE_LICENSE("GPL v2");//同意GPL开源协议
MODULE_AUTHOR("topeet");  //作者信息  

相较于上一章节实验,本章节的代码去掉了静态申请设备号部分代码,并在申请设备号完成之后注册了相应的字符设备,并在驱动出口函数中添加了相应的字符设备删除代码(相关代码已加粗)。
需要注意的是,字符设备的注册要放在申请字符设备号之后,字符设备的删除要放在释放字符驱动设备号之前。

10.3 运行测试

10.3.1 编译驱动程序

在上一小节中的cdev.c 代码同一目录下创建Makefile 文件,Makefile 文件内容如下(图10-7)所示:还是用之前的Makefile,改一下名字就好了。

然后使用命令“make”进行驱动的编译,编译完会生成cdev.ko 目标文件。至此我们的驱动模块就编译成功了,下面进行驱动的运行测试。

10.3.2 运行测试
开发板启动之后,使用以下命令进行驱动模块的加载,如下图(图10-11)所示:

insmod cdev.ko

image-20240813110200417

可以看到动态申请设备号成功了,主设备号为236,次设备号为0,然后使用以下命令进行注册设备号的查看,如下图所示:

cat /proc/devices

image-20240813110315411

可以看到主设备号236 的设备名为chrdev_name,和驱动程序中设置的设备名称相同,证明字符设备注册成功了,最后可以使用以下命令对驱动进行卸载,卸载完成如下所示:

rmmod cdev.ko

image-20240813110348431

第11 章创建设备节点实验

在上两个章节的学习中,我们已经成功的申请了设备号并且注册了相应的字符设备。系统通过设备号对设备进行查找,而字符设备注册到内核之后,并不能直接进行设备文件操作命令(打开、关闭、读、写等),需要相应的设备文件作为桥梁以此来进行设备的访问,在本章节将对如何创建设备节点进行学习。

11.1 创建设备节点

在Linux 操作系统中一切皆文件,设备访问也是通过文件的方式来进行的,对于用来进行设备访问的文件称之为设备节点,设备节点被创建在/dev 目录下,将内核中注册的设备与用户层进行链接,这样应用程序才能对设备进行访问。

根据设备节点的创建方式不同,分为了手动创建设备节点和自动创建设备节点,下面对两种设备节点创建方式进行介绍。

11.1.1 手动创建设备节点

使用mknod 命令手动创建设备节点,mknod 命令格式为:

mknod NAME TYPE MAJOR MINOR

参数含义:
    NAME: 要创建的节点名称
    TYPE: b 表示块设备,c 表示字符设备,p 表示管道
    MAJOR:要链接设备的主设备号
    MINOR: 要链接设备的从设备号

例如使用以下命令创建一个名为device_test 的字符设备节点,链接设备的主设备号和从设备号分别为236 和0:

mknod /dev/device_test c 236 0

11.1.2 自动创建设备节点

设备文件的自动创建是利用udev(mdev)机制来实现,多数情况下采用自动创建设备节点的方式。udev(mdev)可以检测系统中硬件设备状态,可以根据系统中硬件设备状态来创建或者删除设备文件。在驱动中首先使用class_create(…)函数对class 进行创建,这个类存放于/sys/class/ 目录下,之后使用device_create(…)函数创建相应的设备,在进行模块加载时,用户空间中的udev 会自动响应device_create()函数,寻找对应的类从而创建设备节点。

下面对于自动创建节点中所用到的函数进行解释说明:

class_create(…)函数
该函数在“内核源码/include/linux/device.h”文件中所引用(由于上一小节中引用的cdev.h文件已包含device.h,所以不需要再重复引用),如下所示:

#define class_create(owner, name) \
({ \
static struct lock_class_key __key; \
__class_create(owner, name, &__key); \
})

函数作用:
    用于动态创建设备的逻辑类,并完成部分字段的初始化,然后将其添加进Linux 内核系统。
参数含义:
    owner:struct module 结构体类型的指针,指向函数即将创建的这个struct class 的模块。一般赋值为THIS_MODULE。
    name:char 类型的指针,代表即将创建的struct class 变量的名字。
返回值:struct class * 类型的结构体。

class_destroy(...)函数
该函数在“内核源码/include/linux/device.h”文件中所引用,如下(图11-2)所示:

extern void class_destroy(struct class *cls);

函数作用:
    用于删除设备的逻辑类,即从Linux 内核系统中删除设备的逻辑类。
参数含义:
    owner:struct module 结构体类型的指针,指向函数即将创建的这个struct class 的模块。一般赋值为THIS_MODULE。
    name:char 类型的指针,代表即将创建的struct class 变量的名字。
返回值:无

**device_create(...)函数**
该函数在“内核源码/include/linux/device.h”文件中所引用,如下(图11-3)所示:

struct device *device_create(struct class *cls, struct device *parent, dev_t devt, 
                             void *drvdata, const char *fmt, ...);

函数作用:
    用来在class 类中下创建一个设备属性文件,udev 会自动识别从而进行设备节点的创建。
参数含义:
    cls:指定所要创建的设备所从属的类。
    parent:指定该设备的父设备,如果没有就指定为NULL。
    devt:指定创建设备的设备号。
    drvdata:被添加到该设备回调的数据,没有则指定为NULL。
    fmt:添加到系统的设备节点名称。
返回值:struct device * 类型结构体

device_destroy(...)函数
在“内核源码/include/linux/device.h”文件中所引用,如下所示:

extern void device_destroy(struct class *cls, dev_t devt);

函数作用:
    用来删除class 类中的设备属性文件,udev 会自动识别从而进行设备节点的删除。
参数含义:
    cls:指定所要创建的设备所从属的类。
    devt:指定创建设备的设备号。
返回值:无

至此,关于自动创建节点相关的函数就介绍完成了,会在下一小节中对于设备节点的自动创建进行相应实验程序的编写。

11.2 实验程序的编写

本实验对应的网盘路径为:iTOP-RK3568 开发板【底板V1.7 版本】\03_【iTOP-RK3568 开发板】指南教程\02_Linux 驱动配套资料\04_Linux 驱动例程\06。

本章实验将编写Linux 下的自动创建设备节点实验代码,首先采用自动申请设备号的方式进行设备号的申请,并对获取的主设备号与次设备号进行打印,之后对字符设备进行注册(file_operations 结构体只填充owner 字段即可,会在下个章节对file_operations 结构体进行讲解),最后自动对设备节点进行创建。

编写完成的chrdev_node.c 代码如下(图11-5)所示

#include <linux/init.h>
#include <linux/module.h>
#include <linux/fs.h>
#include <linux/kdev_t.h>
#include <linux/cdev.h>

static dev_t dev_num;//定义dev_t类型变量dev_num来表示设备号
static struct cdev cdev_test;//定义struct cdev 类型结构体变量cdev_test,表示要注册的字符设备
static struct file_operations cdev_fops_test = {
    .owner = THIS_MODULE,//将owner字段指向本模块,可以避免在模块的操作正在被使用时卸载该模块
};//定义file_operations结构体类型的变量cdev_test_ops
static struct class *class_test;//定于struct class *类型结构体变量class_test,表示要创建的类

static int __init chrdev_fops_init(void)//驱动入口函数
{
	int ret;//定义int类型的变量ret,用来对函数返回值进行判断
    int major,minor;//定义int类型的主设备号major和次设备号minor
	ret = alloc_chrdev_region(&dev_num,0,1,"chrdev_name");//自动获取设备号,设备名chrdev_name
    if (ret  < 0){
        printk("alloc_chrdev_region is error \n");
    }
    printk("alloc_chrdev_region is ok \n");
    major = MAJOR(dev_num);//使用MAJOR()函数获取主设备号
    minor = MINOR(dev_num);//使用MINOR()函数获取次设备号
    printk("major is %d\n",major);
	printk("minor is %d\n",minor);
    cdev_init(&cdev_test,&cdev_fops_test);//使用cdev_init()函数初始化cdev_test结构体,并链接到cdev_test_ops结构体
	cdev_test.owner = THIS_MODULE;//将owner字段指向本模块,可以避免在模块的操作正在被使用时卸载该模块
	ret = cdev_add(&cdev_test,dev_num,1); //使用cdev_add()函数进行字符设备的添加
    if (ret < 0){
         printk("cdev_add is error \n");
    }
    printk("cdev_add is ok \n");                                                                                
    class_test  = class_create(THIS_MODULE,"class_test");//使用class_create进行类的创建,类名称为class_test
    device_create(class_test,NULL,dev_num,NULL,"device_test");//使用device_create进行设备的创建,设备名称为device_test
    return 0;
}

static void __exit chrdev_fops_exit(void)//驱动出口函数
{
    cdev_del(&cdev_test);//删除添加的字符设备cdev_test
	unregister_chrdev_region(dev_num,1);//释放字符设备所申请的设备号
    device_destroy(class_test,dev_num);//删除创建的设备
    class_destroy(class_test);//删除创建的类
    printk("module exit \n");
}

module_init(chrdev_fops_init);//注册入口函数
module_exit(chrdev_fops_exit);//注册出口函数
MODULE_LICENSE("GPL v2");//同意GPL开源协议
MODULE_AUTHOR("topeet");//作者信息

相较于上一章节实验,本章节代码在入口函数中添加了自动创建设备节点相关代码,在驱动出口函数中添加了相应的删除设备节点相关代码(相关代码已加粗)。

需要注意的是,在进行设备节点添加时,类的创建要放在设备创建之前;在进行设备节点删除时,类的删除要放在设备删除之后。

11.3 运行测试

11.3.1 编译驱动程序

在上一小节中的chrdev_node.c 代码同一目录下创建Makefile 文件,Makefile 文件内容与前面保持一致,改一下名称就行。来到存放chrdev_node.c 和Makefile 文件目录下,然后使用命令“make”进行驱动的编译,编译完生成chrdev_node.ko 目标文件,至此我们的驱动模块就编译成功了,下面进行驱动的运行测试。

11.3.2 运行测试

开发板启动之后,使用以下命令进行驱动模块的加载,如下图(图11-10)所示:

insmod cdev.ko

image-20240813112006037

可以看到动态申请设备号成功了,主设备号为236,次设备号为0,然后使用以下命令进行注册设备号的查看,如下图(图11-11)所示:

cat /proc/devices

image-20240813112121509

可以看到主设备号236 的设备名为chrdev_name,和驱动程序中设置的相同,证明我们的设备号注册成功了,然后使用以下命令对class 目录进行查看,如下图(图11-12)所示:

ls /sys/class/

image-20240813112149988

可以看到在驱动程序中创建的class_test 类已经被成功创建了,然后使用以下命令对class_test 目录进行查看,如下图(图11-12)所示:

ls /sys/class/class_test/

image-20240813112216130

可以看到在驱动程序中创建的名为device_test 的设备属性文件夹也被创建了,然后使用命令“ls /dev/device_test”对/dev 目录进行查看,相应的设备节点也已经被自动创建了,如下所示:

image-20240813112249992

最后可以使用以下命令进行驱动的卸载,卸载完成如下图(图11-14)所示:

rmmod chrdev_node.ko

image-20240813112308120

第12 章字符设备驱动框架实验

下面对前面三个章节进行总结,首先驱动向Linux 内核进行设备号申请,之后的字符设备注册时,会对申请的设备号进行使用。而Linux 内核会将字符设备抽象成一个具体的struct cdev结构体,该结构体记录了字符设备的字符设备号、内核对象等信息,**cdev_init(...)函数对结构体进行初始化之后cdev_add(...)函数将设备号和cdev 结构体进行链接这时设备号才真正指向了内核中注册的设备**。设备注册成功之后,此时还不能对字符设备进行文件操作,所以需要设备节节点来充当内核和用户层通信的桥梁,至此,前面三个章节就总结完成了,以上步骤并没有涉及到操作设备文件,本章节将对字符设备框架进行最终的完善。

12.1 文件操作集简介

在进行注册字符设备实验章节中,使用cdev_init(...)函数对struct cdev 结构体类型变量和struct file_operations 结构体类型变量相链接,struct file_operations 结构体就是把系统调用和驱动程序关联起来的关键数据结构。该结构体的每一个成员都对应着一个系统调用,读取file_operation 中相应的函数指针,接着把控制权转交给函数,从而完成了Linux 设备驱动程序的工作。
file_operations 结构体定义在“内核源码/include/linux/fs.h”文件中,下面对部分常用函数进行说明:

struct module *owner;

owner 是第一个file_operations 成员,它并不是一个操作, 而一个指向拥有该结构的模块的指针,避免正在操作时被卸载,一般为初始化为THIS_MODULES (在<linux/module.h> 中定义的宏)

ssize_t (*read) (struct file *, char __user *, size_t, loff_t *);

read 函数指针用来从设备中同步读取数据,读取成功返回读取的字节数。与应用程序中的read 函数对应。

ssize_t (*write) (struct file *, const char __user *, size_t, loff_t *);

write 函数指针用来发送数据给设备. 写入成功返回写入的字节数。与应用程序中的write函数对应。

long (*unlocked_ioctl) (struct file *, unsigned int, unsigned long);

unlocked_ioctl 函数指针提供对于设备的控制功能,与应用程序中的ioctl 函数对应。

int (*open) (struct inode *, struct file *);

open 函数指针用于打开设备,与应用程序中的open 函数对应。

int (*release) (struct inode *, struct file *);

release 函数指针在file 结构体释放时被调用
至此对于file_operations 文件操作集的部分常用函数就介绍完了,填充了部分常用函数的file_operations 结构体如下:

static struct file_operations cdev_fops_test = {
    .owner = THIS_MODULE,//将owner 字段指向本模块,可以避免在模块的操作正在被使用时卸载该模块
    .open = chrdev_open,//将open 字段指向chrdev_open(...)函数
    .read = chrdev_read,//将open 字段指向chrdev_read(...)函数
    .write = chrdev_write,//将open 字段指向chrdev_write(...)函数
    .release = chrdev_release,//将open 字段指向chrdev_release(...)函数
};//定义file_operations 结构体类型的变量cdev_test_ops

会在下个小节进行字符设备驱动框架实验代码的编写,在上一章节实验的基础上加入file_operations 结构体,并通过应用程序对字符设备驱动进行文件操作测试。

12.2 实验程序的编写

12.2.1 驱动程序编写

本实验驱动程序对应的网盘路径为: iTOP-RK3568 开发板【底板V1.7 版本】\03_【iTOP-RK3568 开发板】指南教程\02_Linux 驱动配套资料\04_Linux 驱动例程\07\module

本章实验将编写字符设备驱动框架实验,会在上一章节实验基础上对file_operation 结构体相关内容进行补充。

首先采用自动申请设备号的方式进行设备号的申请,然后对获取的主设备号与次设备号进行打印,之后对字符设备进行注册,并填充相应的file_openration 结构体和相关函数,最后自动对设备节点进行创建,编写完成的chrdev_fops.c 代码:

#include <linux/init.h>
#include <linux/module.h>
#include <linux/fs.h>
#include <linux/kdev_t.h>
#include <linux/cdev.h>

static int chrdev_open(struct inode *inode, struct file *file)
{
	printk("This is chrdev_open \n");
	return 0;
}

static ssize_t chrdev_read(struct file *file,char __user *buf, size_t size, loff_t *off)
{
	printk("This is chrdev_read \n");
	return 0;
}

static ssize_t chrdev_write(struct file *file,const char __user *buf,size_t size,loff_t *off)
{
	printk("This is chrdev_write \n");
	return 0;
}
static int chrdev_release(struct inode *inode, struct file *file)
{
	return 0;
}
static dev_t dev_num;//定义dev_t类型变量dev_num来表示设备号
static struct cdev cdev_test;//定义struct cdev 类型结构体变量cdev_test,表示要注册的字符设备
static struct file_operations cdev_fops_test = {
    .owner = THIS_MODULE,//将owner字段指向本模块,可以避免在模块的操作正在被使用时卸载该模块
	.open = chrdev_open,
	.read = chrdev_read,
	.write = chrdev_write,
	.release = chrdev_release,
};//定义file_operations结构体类型的变量cdev_test_ops
static struct class *class_test;//定于struct class *类型结构体变量class_test,表示要创建的类

static int __init chrdev_fops_init(void)//驱动入口函数
{
	int ret;//定义int类型的变量ret,用来对函数返回值进行判断
    int major,minor;//定义int类型的主设备号major和次设备号minor
	ret = alloc_chrdev_region(&dev_num,0,1,"chrdev_name");//自动获取设备号,设备名chrdev_name
    if (ret  < 0){
        printk("alloc_chrdev_region is error \n");
    }
    printk("alloc_chrdev_region is ok \n");
    major = MAJOR(dev_num);//使用MAJOR()函数获取主设备号
    minor = MINOR(dev_num);//使用MINOR()函数获取次设备号
    printk("major is %d\n",major);
	printk("minor is %d\n",minor);
    cdev_init(&cdev_test,&cdev_fops_test);//使用cdev_init()函数初始化cdev_test结构体,并链接到cdev_test_ops结构体
	cdev_test.owner = THIS_MODULE;//将owner字段指向本模块,可以避免在模块的操作正在被使用时卸载该模块
	ret = cdev_add(&cdev_test,dev_num,1); //使用cdev_add()函数进行字符设备的添加
    if (ret < 0){
         printk("cdev_add is error \n");
    }
    printk("cdev_add is ok \n");                                                                                
    class_test  = class_create(THIS_MODULE,"class_test");//使用class_create进行类的创建,类名称为class_test
    device_create(class_test,NULL,dev_num,NULL,"device_test");//使用device_create进行设备的创建,设备名称为device_test
    return 0;
}

static void __exit chrdev_fops_exit(void)//驱动出口函数
{
	device_destroy(class_test,dev_num);//删除创建的设备
    class_destroy(class_test);//删除创建的类
    cdev_del(&cdev_test);//删除添加的字符设备cdev_test
	unregister_chrdev_region(dev_num,1);//释放字符设备所申请的设备号
    printk("module exit \n");
}

module_init(chrdev_fops_init);//注册入口函数
module_exit(chrdev_fops_exit);//注册出口函数
MODULE_LICENSE("GPL v2");//同意GPL开源协议
MODULE_AUTHOR("topeet");//作者信息

12.2.2 编写测试APP

本实验应用程序对应的网盘路径为: iTOP-RK3568 开发板【底板V1.7 版本】\03_【iTOP-RK3568 开发板】指南教程\02_Linux 驱动配套资料\04_Linux 驱动例程\07\app

由于在驱动程序中,只是对一系列文件操作函数添加了标志打印(之后的章节会加入数据的读写),所以本小节的应用程序只是起简单的测试作用。编写完成的应用程序app.c 内容如下(图12-3)所示:

#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <string.h>

int main(int argc,char *argv[])
{
    int fd;//定义int类型的文件描述符
    char buf[32];//定义读取缓冲区buf
    fd=open(argv[1],O_RDWR,0666);//调用open函数,打开输入的第一个参数文件,权限为可读可写
    if(fd<0){
        printf("open is error\n");
        return -1;
    }
    printf("open is ok\n");
	/*如果第二个参数为read,条件成立,调用read函数,对文件进行读取*/                                                                                                                                  
    if(!strcmp(argv[2], "read")){
        read(fd,buf,32);
        }
	/*如果第二个参数为write,条件成立,调用write函数,对文件进行写入*/  
    else if(!strcmp(argv[2], "write")){
        write(fd,"hello\n",6);
    }
    close(fd);//调用close函数,对取消文件描述符到文件的映射
    return 0;
}

上述应用程序逻辑较为简单,第一个参数为要进行读写操作的设备节点,第二个参数为read 时,对设备节点进行读操作,第二个参数为write 时,对设备节点进行写操作。

12.3 运行测试

12.3.1 编译驱动程序

在上一小节中的chrdev_fops.c 代码同一目录下创建Makefile 文件,Makefile 文件内容与前面的一致。对于Makefile 的内容注释已在上图添加,保存退出之后,来到存放chrdev_fops.c 和Makefile 文件目录下,然后使用命令“make”进行驱动的编译,编译完生成chrdev_fops.ko 目标文件,至此我们的驱动模块就编译成功了,下面进行驱动的运行测试。

12.3.2 编译应用程序

来到应用程序app.c 文件的存放路径如下图(图12-8)所示:然后使用以下命令对app.c 进行交叉编译,编译完成如下图(图12-9)所示:

aarch64-linux-gnu-gcc -o app app.c -static
gcc -o app app.c		# 或者gcc app.c -o app 

生成的app 文件就是之后放在开发板上运行的可执行文件,至此应用程序的编译就完成了。

12.3.3 运行测试

开发板启动之后,使用以下命令进行驱动模块的加载,如下图(图12-10)所示:

insmod chrdev_fops.ko

image-20240813114239662

可以看到申请的主设备号和次设备号就被打印了出来,然后使用以下代码对自动生成的设备节点device_test 进行查看,如下图(图12-11)所示:

ls /dev/device_test

image-20240813114302283

可以看到device_test 节点已经被自动创建了,然后使用以下命令对open()函数进行测试,如下所示

./app /dev/device_test

image-20240813114329387

可以看到“This is chrdev_open”和“open is ok”信息被打印了,证明应用程序运行成功,且调用了驱动程序中的open(...)函数,而“Segmentation fault”相关打印是因为没有对第二个参数进行传入,这里忽略即可,随后使用以下命令对设备进行读测试,如下图(图12-13)所示:

./app /dev/device_test read

image-20240813114421826

可以看到“This is chrdev_read”信息被打印了出来,证明驱动程序中的read(…)函数被调用了,然后使用以下命令对设备进行写测试,如下图(图12-14)所示:

./app /dev/device_test write

image-20240813114445231

可以看到“This is chrdev_write”信息被打印了出来,证明驱动程序中的write(…)函数被调用了。最后可以使用以下命令进行驱动的卸载,如下图(图12-15)所示:

image-20240813114500456

至此,字符设备驱动框架实验就完成了。

第13 章杂项设备驱动实验

经过前面章节的学习,我们已经对字符设备驱动框架有了一定的理解,而本章要讲解的杂项设备属于特殊的一种字符型设备,是对字符设备的一种封装,为最简单的字符设备。为什么从字符设备中单独提取出了杂项设备呢?杂项设备又要如何进行使用呢?带着疑问,让我们进行杂项设备的学习吧!

13.1 杂项设备驱动简介

在Linux 中,把无法归类的五花八门的设备定义成杂项设备。相较于字符设备,杂项设备有以下两个优点:

(1)节省主设备号:杂项设备的主设备号固定为10,而字符设备不管是动态分配还是静态分配设备号,都会消耗一个主设备号,进而造成了主设备号浪费。当系统中注册了多个misc 设备驱动时,只需使用子设备号进行区分即可。

(2)使用简单:当使用普通的字符设备驱动时,如果开发人员需要导出操作接口给用户空间,就需要注册对应的字符驱动,并创建字符设备class 从而自动在/dev 下生成设备节点,而misc驱动只需要将基本信息通过结构体传递给相应处理函数即可。

在驱动中使用miscdevice 结构体描述misc 设备, 该结构体定义在“ 内核源码/include/linux/miscdevice.h”文件中(在下面的实验代码中需要加入该头文件的引用),具体内容如下所示:

miscdevice {
    int minor; 								/* 子设备号需要用户填写*/
    const char *name;						/* 设备名需要用户填写*/
    const struct file_operations *fops;		  /* 设备操作集需要用户填写*/
    struct list_head list;
    struct device *parent;
    struct device *this_device;
    const struct attribute_group **groups;
    const char *nodename;
    umode_t mode;
};

定义一个misc 设备,一般只需要填充minornamefops 这三个成员变量。

  • minor 指次设备号,可以从“内核源码/include/linux/miscdevice.h”文件中预定义的次设备号挑选,也可以自行定义子设备号(没有被其他设备使用即可),通常情况下将该参数设置为MISC_DYNAMIC_MINOR,表示自动分配子设备号。
  • name 表示misc 设备的名字。misc 设备驱动注册成功之后,会在dev 目录下生成名为name的设备节点。
  • fops 指向了file_operations 的结构体,表示字符设备的操作集合。

13.2 杂项设备的注册和卸载

不同于字符设备的注册和卸载的繁琐,杂项设备的注册可以直接使用函数misc_register 函数来完成,杂项设备的卸载可以直接使用misc_deregister 函数来完成。上述两个函数均定义在“内核源码/include/linux/miscdevice.h”文件当中。

杂项设备的注册:

函数原型:
    int misc_register(struct miscdevice *misc)
函数作用:
    基于misc_class 构造一个设备,将miscdevice 结构挂载到misc_list 列表上,并初始化与linux设备模型相关的结构。进而起到杂项设备注册的作用。
参数含义:
    misc: 杂项设备的结构体指针
函数返回值:申请成功返回0,申请失败返回负数

杂项设备的卸载:

函数原型:
    int misc_deregister(struct miscdevice *misc)
函数作用:
    从mist_list 中删除miscdevice,进而起到杂项设备卸载的作用。
参数含义:
    misc: 杂项设备的结构体指针
函数返回值:卸载成功返回0,申请失败返回负数

至此,注册和卸载杂项设备的API 函数就讲解完成了,会在接下来的驱动章节中对上述函数进行具体使用。

13.3 杂项设备驱动框架

MISC 驱动一般使用以下结构:

static struct file_operations xxx_fops{
    .owner = THIS_MODULE,
    .read = xxx_read,
    ......
};
struct miscdevice xxx_dev{
    .minor = MISC_DYNAMIC_MINOR,
    .name = "xxx",
    .fops = &xxx_fops
};
static int __init xxx_init(void) //驱动入口函数
{
    int ret;
    printk(KERN_EMERG "xxx_init\r\n");
    ret = misc_register(&xxx_dev);//注册杂项设备
    if(ret<0){
    	printk( "misc_register failed\r\n");
    }
    printk( "misc_register ok\r\n");
    return 0;
}
static void __exit xxx_exit(void) //驱动出口函数
{
    printk(KERN_EMERG "xxx_exit\r\n");
    misc_deregister(&xxx_dev); //卸载杂项设备
}
module_init(xxx_init); //注册入口函数
module_exit(xxx_exit);
MODULE_LICENSE("GPL");
MODULE_AUTHOR("topeet");

13.4 实验程序的编写

本实验对应的网盘路径为:iTOP-RK3568 开发板【底板V1.7 版本】\03_【iTOP-RK3568开发板】指南教程\02_Linux 驱动配套资料\04_Linux 驱动例程\08。

本小节将编写最简单的misc 驱动,在驱动入口函数中通过misc_register(…)函数注册杂项设备驱动,在驱动出口函数中通过misc_deregister(...)函数注销杂项设备驱动。编写完成的miscdevice.c 代码如下所示:

#include <linux/init.h>              //初始化头文件
#include <linux/module.h>            //最基本的文件,支持动态添加和卸载模块。
#include <linux/miscdevice.h>        //注册杂项设备头文件
#include <linux/fs.h>                //注册设备节点的文件结构体

struct file_operations misc_fops = { //文件操作集
    .owner = THIS_MODULE ////将owner字段指向本模块,可以避免在模块的操作正在被使用时卸载该模
};
struct miscdevice misc_dev = {       //杂项设备结构体
    .minor = MISC_DYNAMIC_MINOR,     //动态申请的次设备号
    .name = "test",                  //杂项设备名字是hello_misc
    .fops = &misc_fops,              //文件操作集
};

static int __init misc_init(void)           
{ 
    int ret;
    ret = misc_register(&misc_dev); //在初始化函数中注册杂项设备
    if (ret < 0)
    {
        printk("misc registe is error \n"); //打印注册杂项设备失败
    }
    printk("misc registe is succeed \n");//打印注册杂项设备成功
    return 0;
}

static void __exit misc_exit(void)
{ 
    misc_deregister(&misc_dev);     //在卸载函数中注销杂项设备
    printk(" misc goodbye! \n");
}

module_init(misc_init);
module_exit(misc_exit);
MODULE_LICENSE("GPL");
MODULE_AUTHOR("topeet");

13.5 运行测试

13.5.1 编译驱动程序

在上一小节中的miscdevice.c 代码同一目录下创建Makefile 文件,Makefile 文件内容用之前的,改一下名字就行,然后使用命令“make”进行驱动的编译,编译完生成miscdevice.ko 目标文件,至此我们的驱动模块就编译成功了,下面进行驱动的运行测试。

13.5.2 运行测试

将编译生成的驱动模块miscdevice.ko 拷贝到开发板上,输入以下命令加载驱动模块。

insmod miscdevice.ko

image-20240814171540545

可以看到驱动加载之后,打印“misc registe is succeed”,说明misc 驱动注册成功。输入以下命令查看加载的驱动模块,驱动加载成功如下(图13-5)所示:

image-20240814171555191

然后来到/sys/class/misc 目录下,可以看到名为“test”的文件夹已经被创建了,在/sys/class/misc 目录下有misc 类的所有设备,每个注册的杂项设备对应一个文件夹目录,如下图(图13-6)所示:

image-20240814171615437

image-20240814171622918

驱动加载成功之后会生成/dev/test 设备驱动文件,输入以下命令查看杂项设备的主次设备号。

ls /dev/test -al

结果如下图(图13-8)所示:

image-20240814171650842

从上图可以看出,/dev/test 这个杂项设备的主设备号为10,次设备号为53,最后可以使用以下命令对驱动进行卸载,卸载完成如下图(图13-9)所示:

rmmod miscdevice.ko

image-20240814171713371

第14 章内核空间与用户空间数据交互实验

在“第12 章字符设备驱动框架实验”中,已经对file_operations 结构体的进行了填充,该结构体的每一个成员都对应着一个系统调用,例如read、write 等,在对应的实验中,只是对调用函数进行了标志打印,并没有真正实现设备的读写功能,而在本章节将对内核空间与用户空间的数据交换功能进行实现。

14.1 内核空间与用户空间

Linux 系统将可访问的内存空间分为了两个部分,一部分是内核空间,一部分是用户空间。操作系统和驱动程序运行在内核空间(内核态),应用程序运行在用户空间(用户态)。

那么为什么要区分用户空间和内核空间呢?

  • (1)内核空间中的代码控制了硬件资源,用户空间中的代码只能通过内核暴露的系统调用接口来使用系统中的硬件资源,这样的设计可以保证操作系统自身的安全性和稳定性。
  • (2)从另一方面来说,内核空间的代码更偏向于系统管理,而用户空间中的代码更偏重业务逻辑实现,俩者的分工不同。

硬件资源管理都是在内核空间完成的,应用程序无法直接对硬件进行操作,只能通过调用相应的内核接口来完成相应的操作。比如应用程序要对磁盘上的一个文件进行读取,应用程序可以向内核发起一个“系统调用”申请——我要读取磁盘上的文件。这个过程其实是通过一个特殊的指令让进程从用户态进入到了内核态。在内核空间中,CPU 可以执行任何命令,包括从磁盘上读取数据,具体过程是先把数据读取到内核空间中,然后再把数据拷贝到用户空间并从内核态切换到用户态。此时应用程序已经从系统调用中返回并拿到了想要的数据,可以继续往下执行了。

进程只有从用户空间切换到内核空间才可以使用系统的硬件资源,切换的方式有三种:系统调用,软中断,硬中断,如下图(图14-1)所示:

image-20240814172904578

14.2 用户空间和内核空间数据交换

内核空间和用户空间的内存是不能互相访问的。但是很多应用程序都需要和内核进行数据的交换,例如应用程序使用read 函数从驱动中读取数据,使用write 函数向驱动中写数据,上述功能就需要使用copy_from_user 和copy_to_user 俩个函数来完成。copy_from_user 函数是将用户空间的数据拷贝到内核空间。copy_to_user 函数是将内核空间的数据拷贝到用户空间。这俩个函数定义在了kernel/include/linux/uaccess.h 文件下,如下所示:

copy_to_user

函数原型:
    unsigned long copy_to_user_inatomic(void __user *to, const void *from, unsigned long n);
函数作用:
    把内核空间的数据复制到用户空间。
参数含义:
    *to 是用户空间的指针
    *from 是内核空间的指针
    n 是从内核空间向用户空间拷贝的字节数

copy_from_user

函数原型:
    unsigned long copy_from_user(void *to, const void __user *from, unsigned long n)
函数作用:
    把用户空间的数据复制到内核空间。
参数含义:
    *to 是内核空间的指针
    *from 是用户空间的指针
    n 是从用户空间向内核空间拷贝的字节数

14.3 实验程序编写

14.3.1 驱动程序编写

本驱动程序对应的网盘路径为:iTOP-RK3568 开发板【底板V1.7 版本】\03_【iTOP-RK3568开发板】指南教程\02_Linux 驱动配套资料\04_Linux 驱动例程\09\module。

在该实验中将实现内核空间和用户空间进行数据交换的功能。以12 章编写的字符设备驱动框架实验为基础编写驱动程序,程序使用copy_to_user 函数和copy_from_user 函数来实现内核空间和用户空间互传数据的功能,编写完成的file.c 代码如下所示:

#include <linux/init.h>
#include <linux/module.h>
#include <linux/kdev_t.h>
#include <linux/fs.h>
#include <linux/cdev.h>
#include <linux/uaccess.h>

static dev_t dev_num;  //设备号
static int major = 0;        //主设备号
static int minor = 0;        //次设备号
struct cdev cdev_test;   // cdev

struct class *class;          //类
struct device *device;    //设备

/*打开设备函数*/
static int cdev_test_open(struct inode *inode, struct file *file)
{
    printk("This is cdev_test_open\r\n");
    return 0;
}

/*向设备写入数据函数*/
static ssize_t cdev_test_write(struct file *file, const char __user *buf, size_t size, loff_t *off)
{
    /*本章实验重点******/
    char kbuf[32] = {0};   //定义写入缓存区kbuf
    if (copy_from_user(kbuf, buf, size) != 0) // copy_from_user:用户空间向内核空间传数据
    {
        printk("copy_from_user error\r\n");//打印copy_from_user函数执行失败
        return -1;
    }
    printk("This is cdev_test_write\r\n");

    printk("kbuf is %s\r\n", kbuf);
    return 0;
}

/**从设备读取数据*/
static ssize_t cdev_test_read(struct file *file, char __user *buf, size_t size, loff_t *off)
{
    /*本章实验重点******/
    char kbuf[32] = "This is cdev_test_read!";//定义内核空间数据
    if (copy_to_user(buf, kbuf, strlen(kbuf)) != 0) // copy_to_user:内核空间向用户空间传数据
    {
        printk("copy_to_user error\r\n"); //打印copy_to_user函数执行失败
        return -1;
    }

    printk("This is cdev_test_read\r\n");
    return 0;
}

static int cdev_test_release(struct inode *inode, struct file *file)
{
    printk("This is cdev_test_release\r\n");
    return 0;
}

/*设备操作函数,定义file_operations结构体类型的变量cdev_test_fops*/
struct file_operations cdev_test_fops = {
    .owner = THIS_MODULE, //将owner字段指向本模块,可以避免在模块的操作正在被使用时卸载该模块
    .open = cdev_test_open, //将open字段指向chrdev_open(...)函数
    .read = cdev_test_read,  //将open字段指向chrdev_read(...)函数
    .write = cdev_test_write, //将open字段指向chrdev_write(...)函数
    .release = cdev_test_release, //将open字段指向chrdev_release(...)函数
};

static int __init chr_fops_init(void) //驱动入口函数
{
    /*注册字符设备驱动*/
    int ret;
    /*1 创建设备号*/
    ret = alloc_chrdev_region(&dev_num, 0, 1, "alloc_name"); //动态分配设备号
    if (ret < 0){
        printk("alloc_chrdev_region is error\n");//打印动态分配设备号失败
    }
    printk("alloc_chrdev_region is ok\n");

    major = MAJOR(dev_num); //获取主设备号
    minor = MINOR(dev_num); //获取次设备号

    printk("major is %d \r\n", major); //打印主设备号
    printk("minor is %d \r\n", minor); //打印次设备号
     /*2 初始化cdev*/
    cdev_test.owner = THIS_MODULE;
    cdev_init(&cdev_test, &cdev_test_fops);

    /*3 添加一个cdev,完成字符设备注册到内核*/
    cdev_add(&cdev_test, dev_num, 1);

    /*4 创建类*/
    class = class_create(THIS_MODULE, "test");

    /*5  创建设备*/
    device = device_create(class, NULL, dev_num, NULL, "test");

    return 0;
}

static void __exit chr_fops_exit(void) //驱动出口函数
{
    /*注销字符设备*/
    unregister_chrdev_region(dev_num, 1); //注销设备号
    cdev_del(&cdev_test);                                     //删除cdev
    device_destroy(class, dev_num);               			//删除设备
    class_destroy(class);                                     //删除类
}
module_init(chr_fops_init);   //注册入口函数
module_exit(chr_fops_exit);  //注册出口函数
MODULE_LICENSE("GPL v2");
MODULE_AUTHOR("topeet");

以上代码在cdev_test_read 函数中使用copy_to_user 函数将内核数据拷贝到用户空间,在cdev_test_write 函数中使用copy_from_user 函数将用户空间数据拷贝到内核空间。

14.3.2 编写测试APP
本应用程序对应的网盘路径为:iTOP-RK3568 开发板【底板V1.7 版本】\03_【iTOP-RK3568开发板】指南教程\02_Linux 驱动配套资料\04_Linux 驱动例程\09\app

编写测试APP 其实是在编写Linux 应用,编译完成的应用程序app.c 代码如下所示:

#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>

int main(int argc, char *argv[])  //主函数
{
    int fd;   //定义int类型的文件描述符
    char buf1[32] = {0}; //定义读取缓存区buf1
    char buf2[32] = "nihao"; //定义写入缓存区buf2
    fd = open("/dev/test", O_RDWR);  //打开字符设备驱动     //默认是阻塞的
    if (fd < 0) {
        perror("open error \n");
        return fd;
    }
    read(fd, buf1, sizeof(buf1));//从/dev/test文件读取数据
    printf("buf1 is %s \r\n", buf1); //打印读取的数据

    write(fd,buf2,sizeof(buf2));//向/dev/test文件写入数据
    close(fd);
    return 0;
}

14.4 运行测试

14.4.1 编译驱动程序

在上一小节中的file.c 代码同一目录下创建Makefile 文件,Makefile 文件内容与之前一致:然后使用命令“make”进行驱动的编译,编译完生成file.ko 目标文件,至此我们的驱动模块就编译成功了,下面进行应用程序编译.

14.4.2 编译应用程序

因为测试APP 是要在开发板上运行的,所以需要aarch64-linux-gnu-gcc 来编译,输入以下命令,编译完成以后会生成一个app 的可执行程序,如下图(图14-5)所示:

aarch64-linux-gnu-gcc app.c -o app

下面进行驱动程序的测试。

14.4.3 运行测试

驱动模块file.ko 和测试程序app 都已经准备好了,接下来就是运行测试。首先输入以下命令加载驱动程序,如下图(图14–6)所示:

insmod file.ko

image-20240814174110604

输入以下命令运行应用程序,如下图(图14-7)所示

image-20240814174119933

由上图可知,打印“This is cdev_test_open”信息说明成功打开了字符设备驱动。
打印“ This is cdev_test_read”和“buf1 is This is cdev_test_read!”说明应用程序成功读取到内核的数据。
打印“This is cdev_test_write”和“kbuf is nihao”说明应用程序向内核写数据成功。
最后打印“This is cdev_test_release”说明卸载字符设备。

第15 章文件私有数据实验

在之前章节编写的驱动程序中,将生成字符设备的一些硬件属性(设备号、类、设备名称等)全都写成了变量的形式,虽然这样编写驱动代码不会产生报错,但是会显得有点不专业。通常在驱动开发中会为设备定义相关的设备结构体,将硬件属性的描述信息全部放在该结构体中,在本章节中将对设备结构体的功能实现和文件私有数据进行学习。

15.1 文件私有数据简介

Linux 中并没有明确规定要使用文件私有数据,但是在linux 驱动源码中,广泛使用了文件私有数据,这是Linux 驱动遵循的“潜规则”,实际上也体现了Linux 面向对象的思想。struct file 结构体中专门为用户留了一个域用于定义私有数据。结构体内容如下所示:

struct file {
    union {
        struct llist_node fu_llist;
        struct rcu_head fu_rcuhead;
    } f_u;
    struct path f_path;
    struct inode *f_inode; /* cached value */
    const struct file_operations *f_op;
    
    
    /*
    * Protects f_ep_links, f_flags.
    * Must not be taken from IRQ context.
    */
    spinlock_t f_lock;
    enum rw_hint f_write_hint;
    atomic_long_t f_count;
    unsigned int f_flags;
    fmode_t f_mode;
    struct mutex f_pos_lock;
    loff_t f_pos;
    struct fown_struct f_owner;
    const struct cred *f_cred;
    struct file_ra_state f_ra;
    
    u64 f_version;
#ifdef CONFIG_SECURITY
    void *f_security;
#endif
    
    /* needed for tty driver, and maybe others */
    void *private_data;						//私有数据
#ifdef CONFIG_EPOLL
    /* Used by fs/eventpoll.c to link all the hooks to this file */
    struct list_headf_ep_links;
    struct list_headf_tfile_llink;
#endif /* #ifdef CONFIG_EPOLL */
    struct address_space *f_mapping;
    errseq_t f_wb_err;
}

文件私有数据的概念在Linux 驱动中有着非常广泛的应用,文件私有数据就是将私有数据private_data 指向设备结构体。通过它可以将私有数据一路从open 函数带到read, write 函数层层传入。一般是在open 的时候赋值,read、write 时使用。open 函数中私有数据的使用如下所示:

struct device_test dev1;
static int cdev_test_open(struct inode *inode,struct file *file){
    file->private_data=&dev1;
    return 0;
};

在上述代码中,定义了一个设备结构体dev1,然后在open 函数中,将私有数据private_data指向了设备结构体dev1
我们可以在read write 函数中通过private_data 访问设备结构体,如下所示:

static ssize_t cdev_test_write(struct file *file,const char _user *buf, size_t size,loff_t *off_t){
    struct device_test *test_dev=(struct device_test *)file->private_data;
    return 0;
}

15.2 实验程序编写

15.2.1 驱动程序编写

本驱动程序对应的网盘路径为:iTOP-RK3568 开发板【底板V1.7 版本】\03_【iTOP-RK3568开发板】指南教程\02_Linux 驱动配套资料\04_Linux 驱动例程\10\module
本章实验将编写Linux 下的使用文件私有数据实例代码,在open 函数中对私有数据结构体赋值,在write 函数中使用。编写完成的代码如下所示:

#include <linux/init.h>
#include <linux/module.h>
#include <linux/kdev_t.h>
#include <linux/fs.h>
#include <linux/cdev.h>
#include <linux/uaccess.h>

struct device_test{
    dev_t dev_num;              //设备号
    int major ;                         //主设备号
    int minor ;                          //次设备号
    struct cdev cdev_test;  // cdev
    struct class *class;         //类
    struct device *device;   //设备
    char kbuf[32];                  //缓存区buf
};

struct  device_test dev1;  //定义一个device_test结构体变量


/*打开设备函数*/
static int cdev_test_open(struct inode *inode, struct file *file)
{
    file->private_data=&dev1;  //设置私有数据
    printk("This is cdev_test_open\r\n");
    return 0;
}

/*向设备写入数据函数*/
static ssize_t cdev_test_write(struct file *file, const char __user *buf, size_t size, loff_t *off)
{
     struct device_test *test_dev=(struct device_test *)file->private_data; //在write函数中读取private_data

    if (copy_from_user(test_dev->kbuf, buf, size) != 0) // copy_from_user:用户空间向内核空间传数据
    {
        printk("copy_from_user error\r\n");
        return -1;
    }
    printk("This is cdev_test_write\r\n");

    printk("kbuf is %s\r\n", test_dev->kbuf); //打印kbuf的值
    return 0;
}

/**从设备读取数据*/
static ssize_t cdev_test_read(struct file *file, char __user *buf, size_t size, loff_t *off)
{
    struct device_test *test_dev=(struct device_test *)file->private_data;
    
    if (copy_to_user(buf, test_dev->kbuf, strlen( test_dev->kbuf)) != 0) // copy_to_user:内核空间向用户空间传数据
    {
        printk("copy_to_user error\r\n");
        return -1;
    }

    printk("This is cdev_test_read\r\n");
    return 0;
}

static int cdev_test_release(struct inode *inode, struct file *file)
{
    printk("This is cdev_test_release\r\n");
    return 0;
}

/*设备操作函数*/
struct file_operations cdev_test_fops = {
    .owner = THIS_MODULE,         //将owner字段指向本模块,可以避免在模块的操作正在被使用时卸载该模块
    .open = cdev_test_open,         //将open字段指向chrdev_open(...)函数
    .read = cdev_test_read,            //将open字段指向chrdev_read(...)函数
    .write = cdev_test_write,         //将open字段指向chrdev_write(...)函数
    .release = cdev_test_release,//将open字段指向chrdev_release(...)函数
};

static int __init chr_fops_init(void) //驱动入口函数
{
    /*注册字符设备驱动*/
    int ret;
    /*1 创建设备号*/
    ret = alloc_chrdev_region(&dev1.dev_num, 0, 1, "alloc_name"); //动态分配设备号
    if (ret < 0)
    {
        printk("alloc_chrdev_region is error\n");
    }
    printk("alloc_chrdev_region is ok\n");

    dev1.major = MAJOR(dev1.dev_num); //获取主设备号
    dev1.minor = MINOR(dev1.dev_num); //获取次设备号

    printk("major is %d \r\n", dev1.major); //打印主设备号
    printk("minor is %d \r\n", dev1.minor); //打印次设备号
     /*2 初始化cdev*/
    dev1.cdev_test.owner = THIS_MODULE;
    cdev_init(&dev1.cdev_test, &cdev_test_fops);

    /*3 添加一个cdev,完成字符设备注册到内核*/
    cdev_add(&dev1.cdev_test, dev1.dev_num, 1);

    /*4 创建类*/
  dev1. class = class_create(THIS_MODULE, "test");

    /*创建设备*/
  dev1.device = device_create(dev1.class, NULL, dev1.dev_num, NULL, "test");

    return 0;
}

static void __exit chr_fops_exit(void) //驱动出口函数
{
    /*注销字符设备*/
    unregister_chrdev_region(dev1.dev_num, 1); //注销设备号
    cdev_del(&dev1.cdev_test);                 //删除cdev
    device_destroy(dev1.class, dev1.dev_num);       //删除设备
    class_destroy(dev1.class);                 //删除类
}
module_init(chr_fops_init);
module_exit(chr_fops_exit);
MODULE_LICENSE("GPL v2");
MODULE_AUTHOR("topeet");

15.2.2 编写测试APP

本应用程序对应的网盘路径为:iTOP-RK3568 开发板【底板V1.7 版本】\03_【iTOP-RK3568开发板】指南教程\02_Linux 驱动配套资料\04_Linux 驱动例程\10\app
编写测试APP 其实是在编写Linux 应用,在应用程序中向设备文件写入数据,编写完成的应用程序app.c 代码如下所示:

#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>

int main(int argc, char *argv[]) //主函数
{
    int fd;
    char buf1[32] = "nihao";  //定义写入缓存区buf1
    fd = open("/dev/test", O_RDWR); //打开/dev/test设备
    if (fd < 0)
    {
        perror("open error \n");
        return fd;
    }
    write(fd,buf1,sizeof(buf1)); //向/dev/test设备写入数据
    close(fd);
    return 0;
}

15.3 运行测试

15.3.1 编译驱动程序

在上一小节中的file.c 代码同一目录下创建Makefile 文件,Makefile 文件内容如下:然后使用命令“make”进行驱动的编译,编译完生成file.ko 目标文件,至此我们的驱动模块就编译成功了,下面进行应用程序编译,

15.3.2 编译应用程序

因为测试APP 是要在开发板上运行的,所以需要aarch64-linux-gnu-gcc 来编译,输入以下命令,编译完成以后会生成一个app 的可执行程序,如下图(图15-4)所示:

aarch64-linux-gnu-gcc app.c -o app

下面进行驱动程序的测试。

15.3.3 运行测试

驱动模块file.ko 和测试程序app 都已经准备好了,接下来就是运行测试。输入以下命令加载驱动程序

insmod file.ko

image-20240814175600365

驱动加载成功之后会生成/dev/test 设备驱动文件,输入以下命令查看杂项设备的主次设备号。

ls /dev/test -al

结果如下图(图15-6)所示:

image-20240814175626657

运行应用程序,如下(图15-6)所示:

image-20240814175638881

在此实验中,将硬件属性的信息全部放在一个结构体private_data,依然可以实现字符设备的操作。

第16 章一个驱动兼容不同设备实验

在Linux 中,使用主设备号来表示对应某一类驱动,使用次设备号来表示这类驱动下的各个设备。假如现在驱动要支持的主设备号相同,但是次设备号不同的设备。驱动程序要怎样编写呢,上一章节学习的私有数据private_date 在此时就派上了用场,具体是怎样使用的呢,多个设备是如何在一个驱动中兼容的呢,带着疑问,让我们开始本章节的学习吧。

16.1 container_of 函数简介

container_of 在Linux 内核中是一个常用的宏,用于从包含在某个结构中的指针获得结构本身的指针,通俗地讲就是通过结构体变量中某个成员的首地址进而获得整个结构体变量的首地址。那么可以使用这个函数获取不同设备的地址,来对不同的设备进行操作,从而一个驱动可以兼容不同的设备。

container_of

函数原型:
    container_of(ptr,type,member)
函数作用:
    通过结构体变量中某个成员的首地址获取到整个结构体变量的首地址。
参数含义:
    ptr 是结构体变量中某个成员的地址。
    type 是结构体的类型
    member 是该结构体变量的具体名字

container_of 宏的作用是通过结构体内某个成员变量的地址和该变量名,以及结构体类型。找到该结构体变量的地址。

16.2 实验程序编写

16.2.1 驱动程序编写

本驱动程序对应的网盘路径为:iTOP-RK3568 开发板【底板V1.7 版本】\03_【iTOP-RK3568开发板】指南教程\02_Linux 驱动配套资料\04_Linux 驱动例程\11\module
本章实验将使用container_of 函数编写一个驱动兼容不同设备的实例代码,编写完成的代码如下所示:

#include <linux/init.h>
#include <linux/module.h>
#include <linux/kdev_t.h>
#include <linux/fs.h>
#include <linux/cdev.h>
#include <linux/uaccess.h>

struct device_test
{
    dev_t dev_num;             //设备号
    int major;                          //主设备号
    int minor;                         //次设备号
    struct cdev cdev_test; // cdev
    struct class *class;        //类
    struct device *device; //设备
    char kbuf[32];
};

struct device_test dev1;   //定义一个device_test结构体变量dev1
struct device_test dev2;  //定义一个device_test结构体变量dev2

/*打开设备函数*/
static int cdev_test_open(struct inode *inode, struct file *file)
{
    dev1.minor = 0;    //设置dev1的次设备号为0
    dev2.minor = 1;   //设置dev2的次设备号为1

//inode->i_rdev 为该 inode 的设备号,使用container_of函数找到结构体变量dev1 dev2的地址
//然后设置私有数据
    file->private_data = container_of(inode->i_cdev, struct device_test, cdev_test);
    printk("This is cdev_test_open\r\n");

    return 0;
}

/*向设备写入数据函数*/
static ssize_t cdev_test_write(struct file *file, const char __user *buf, size_t size, loff_t *off)
{
    struct device_test *test_dev = (struct device_test *)file->private_data;

    //如果次设备号是0,则为dev1
    if (test_dev->minor == 0)
    {

        if (copy_from_user(test_dev->kbuf, buf, size) != 0) // copy_from_user:用户空间向内核空间传数据
        {
            printk("copy_from_user error\r\n");
            return -1;
        }
        printk(" test_dev->kbuf is %s\r\n", test_dev->kbuf);
    }
    //如果次设备号是1,则为dev2
    else if(test_dev->minor == 1)
    {
        if (copy_from_user(test_dev->kbuf, buf, size) != 0) // copy_from_user:用户空间向内核空间传数据
        {
            printk("copy_from_user error\r\n");
            return -1;
        }
        printk(" test_dev->kbuf is %s\r\n", test_dev->kbuf);
    }
    return 0;
}

/**从设备读取数据*/
static ssize_t cdev_test_read(struct file *file, char __user *buf, size_t size, loff_t *off)
{

    struct device_test *test_dev = (struct device_test *)file->private_data;

    if (copy_to_user(buf, test_dev->kbuf, strlen(test_dev->kbuf)) != 0) // copy_to_user:内核空间向用户空间传数据
    {
        printk("copy_to_user error\r\n");
        return -1;
    }

    printk("This is cdev_test_read\r\n");
    return 0;
}

static int cdev_test_release(struct inode *inode, struct file *file)
{
    printk("This is cdev_test_release\r\n");
    return 0;
}

/*设备操作函数,定义file_operations结构体类型的变量cdev_test_fops*/
struct file_operations cdev_test_fops = {
    .owner = THIS_MODULE, //将owner字段指向本模块,可以避免在模块的操作正在被使用时卸载该模块
    .open = cdev_test_open, //将open字段指向chrdev_open(...)函数
    .read = cdev_test_read, //将open字段指向chrdev_read(...)函数
    .write = cdev_test_write, //将open字段指向chrdev_write(...)函数
    .release = cdev_test_release, //将open字段指向chrdev_release(...)函数
};

static int __init chr_fops_init(void) //驱动入口函数
{
    /*注册字符设备驱动*/
    int ret;
    /*1 创建设备号,,这里注册2个设备号*/
    ret = alloc_chrdev_region(&dev1.dev_num, 0, 2, "alloc_name"); //动态分配设备号
    if (ret < 0)
    {
        printk("alloc_chrdev_region is error\n");
    }
    printk("alloc_chrdev_region is ok\n");

    dev1.major = MAJOR(dev1.dev_num); //获取主设备号
    dev1.minor = MINOR(dev1.dev_num); //获取次设备号

    printk("major is %d \r\n", dev1.major); //打印主设备号
    printk("minor is %d \r\n", dev1.minor); //打印次设备号

    //对设备1进行操作
    /*2 初始化cdev*/
    dev1.cdev_test.owner = THIS_MODULE;
    cdev_init(&dev1.cdev_test, &cdev_test_fops);

    /*3 添加一个cdev,完成字符设备注册到内核*/
    cdev_add(&dev1.cdev_test, dev1.dev_num, 1);

    /*4 创建类*/
    dev1.class = class_create(THIS_MODULE, "test1");

    /*5 创建设备*/
    dev1.device = device_create(dev1.class, NULL, dev1.dev_num, NULL, "test1");

    dev2.major = MAJOR(dev1.dev_num + 1); //获取主设备号
    dev2.minor = MINOR(dev1.dev_num + 1); //获取次设备号

    printk("major is %d \r\n", dev2.major); //打印主设备号
    printk("minor is %d \r\n", dev2.minor); //打印次设备号

    //对设备2进行操作
  /*2 初始化cdev*/
    dev2.cdev_test.owner = THIS_MODULE;
    cdev_init(&dev2.cdev_test, &cdev_test_fops);

    /*3 添加一个cdev,完成字符设备注册到内核*/
    cdev_add(&dev2.cdev_test, dev1.dev_num + 1, 1);

    /*4 创建类*/
    dev2.class = class_create(THIS_MODULE, "test2");

    /*5  创建设备*/
    dev2.device = device_create(dev2.class, NULL, dev1.dev_num + 1, NULL, "test2");

    return 0;
}

static void __exit chr_fops_exit(void) //驱动出口函数
{
    /*注销字符设备*/
    unregister_chrdev_region(dev1.dev_num, 1); //注销设备号
    unregister_chrdev_region(dev1.dev_num + 1, 1); //注销设备号
    cdev_del(&dev1.cdev_test);                 //删除cdev
    cdev_del(&dev2.cdev_test);                     //删除cdev
    device_destroy(dev1.class, dev1.dev_num);  //删除设备
    device_destroy(dev2.class, dev1.dev_num + 1);  //删除设备
    class_destroy(dev1.class);                 //删除类
    class_destroy(dev2.class);                     //删除类
    
}
module_init(chr_fops_init);
module_exit(chr_fops_exit);
MODULE_LICENSE("GPL v2");
MODULE_AUTHOR("topeet");

16.2.2 编写测试APP

本应用程序对应的网盘路径为:iTOP-RK3568 开发板【底板V1.7 版本】\03_【iTOP-RK3568开发板】指南教程\02_Linux 驱动配套资料\04_Linux 驱动例程\11\app
编写应用程序,打开生成的俩个设备,并向俩个设备中写入数据,编写完成的应用程序app.c代码如下所示:

#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>

int main(int argc, char *argv[])
{
    int fd1;  //定义设备1的文件描述符
    int fd2;  //定义设备2的文件描述符
    char buf1[32] = "nihao /dev/test1";   //定义写入缓存区buf1
    char buf2[32] = "nihao /dev/test2";   //定义写入缓存区buf2
    fd1 = open("/dev/test1", O_RDWR);  //打开设备1:test1
    if (fd1 < 0)
    {
        perror("open error \n");
        return fd1;
    }
    write(fd1,buf1,sizeof(buf1));  //向设备1写入数据
    close(fd1); //取消文件描述符到文件的映射

    fd2= open("/dev/test2", O_RDWR); //打开设备2:test2
    if (fd2 < 0)
    {
        perror("open error \n");
        return fd2;
    }
    write(fd2,buf2,sizeof(buf2));  //向设备2写入数据
    close(fd2);   //取消文件描述符到文件的映射

    return 0;
}

16.3 运行测试

16.3.1 编译驱动程序

在上一小节中的file.c 代码同一目录下创建Makefile 文件,Makefile 文件与前面保持一致:然后使用命令“make”进行驱动的编译,编译完生成file.ko 目标文件,至此我们的驱动模块就编译成功了,下面进行应用程序编译

16.3.2 编译应用程序

因为测试APP 是要在开发板上运行的,所以需要aarch64-linux-gnu-gcc 来编译,输入以下命令,编译完成以后会生成一个app 的可执行程序,如下图(图16-4)所示:

aarch64-linux-gnu-gcc app.c -o app

image-20240815093718307

下面进行驱动程序的测试。

16.3.2 运行测试

驱动模块file.ko 和测试程序app 都已经准备好了,接下来就是运行测试。
输入以下命令加载驱动模块,如下图(图16-6)所示:

insmod file.ko

image-20240815093752441

驱动加载成功之后会生成/dev/test1/dev/test2 设备驱动文件,输入以下命令查看设备,可以看到一个驱动创建并管理了多个驱动设备,如下图(图16-8)所示:

ls /dev/test* -al

image-20240815093812836

运行应用程序,如下(图16-10)所示:

image-20240815094001862

如上图所示,可以看到用户顺利向俩个设备写入数据,且每个设备拥有私有数据。

第17 章Linux 错误处理实验

在前面章节进行的字符设备驱动实验中,即使是最简单的注册字符设备,也存在注册失败的可能性,因此在之前编写的驱动代码中采用检查函数返回值的方式,确认函数是否成功执行,而在本章节中将采用goto 语句对Linux 错误处理进行更进一步的处理。

17.1 goto 语句简介

在编写驱动程序时,驱动程序应该提供函数执行失败后处理的能力。如果驱动程序中函数执行失败了,必须取消掉所有失败前的注册,否则内核会处于一个不稳定的状态,因为它包含了不存在代码的内部指针。在处理Linux 错误时,最好使用goto 语句,goto 语句的使用示例如下所示:

int init my_init_function(void)
{
    int err;
    err = register_this(ptr1, "skull");
    if (err)
    	goto fail_this;
    
    err = register_that(ptr2, "skull");
    if (err)
        goto fail_that;
    
    err = register_those(ptr3, "skull");
    if (err)
        goto fail_those;
        return 0;
    
    fail_those:
    unregister_that(ptr2, "skull");
    
    fail_that:
    unregister_this(ptr1, "skull");
    
    fail_this:
    return err;
}

在以上代码中试图注册3 个虚构设备,goto 语句在失败情况下使用,对之前已经成功注册的设施进行注销。使用goto 语句处理的时候,应该遵循“先进后出”的原则,如下图(图17-1)所示:

image-20240815094459038

如果在驱动代码中初始化和卸载函数比较复杂,goto 方法可能变得难于管理,为了使代码重复性最小以及流程化,Linux 提供了更简便的方法,我们接着来学习下一小节。

17.2 IS_ERR()简介

对于任何一个指针来说,必然存在三种情况,一种是合法指针,一种是NULL(也就是空指针),一种是错误指针(也就是无效指针)。在Linux 内核中,所谓的错误指针已经指向了内核空间的最后一页,例如,对于一个64 位系统来说,内核空间最后地址为0xffffffffffffffff,那么最后一页的地址是0xfffffffffffff000~0xffffffffffffffff,这段地址是被保留的,如果指针落在这段地
址之内,说明是错误的无效指针。

在Linux 内核源码中实现了指针错误的处理机制,相关的函数接口主要有IS_ERR()PTR_ERR()ERR_PTR()等,其函数的源码在include/linux/err.h 文件中,如下所示:

image-20240815094609549

如上图所示,在Linux 源码中IS_ERR()函数其实就是判断指针是否出错,如果指针指向了内核空间的最后一页,就说明指针是一个无效指针,如果指针并不是落在内核空间的最后一页,就说明这指针是有效的。无效的指针能表示成一种负数的错误码,如果想知道这个指针是哪个错误码,使用PTR_ERR 函数转化。0xfffffffffffff000~0xffffffffffffffff 这段地址和Linux 错误码是一一对应的,内核错误码保存在errno-base.h 文件中。如下所示:

/* SPDX-License-Identifier: GPL-2.0 WITH Linux-syscall-note */
#ifndef _ASM_GENERIC_ERRNO_BASE_H
#define _ASM_GENERIC_ERRNO_BASE_H
#define EPERM 		1 /* Operation not permitted */
#define ENOENT 		2 /* No such file or directory */
#define ESRCH 		3 /* No such process */
#define EINTR 		4 /* Interrupted system call */
#define EIO 		5 /* I/O error */
#define ENXIO 		6 /* No such device or address */
#define E2BIG 		7 /* Argument list too long */
#define ENOEXEC 	8 /* Exec format error */
#define EBADF 		9 /* Bad file number */
#define ECHILD 		10 /* No child processes */
#define EAGAIN 		11 /* Try again */
#define ENOMEM 		12 /* Out of memory */
#define EACCES 		13 /* Permission denied */
#define EFAULT 		14 /* Bad address */
#define ENOTBLK 	15 /* Block device required */
#define EBUSY 		16 /* Device or resource busy */
#define EEXIST 		17 /* File exists */
#define EXDEV 		18 /* Cross-device link */
#define ENODEV 		19 /* No such device */
#define ENOTDIR 	20 /* Not a directory */
#define EISDIR 		21 /* Is a directory */
#define EINVAL 		22 /* Invalid argument */
#define ENFILE 		23 /* File table overflow */
#define EMFILE 		24 /* Too many open files */
#define ENOTTY 		25 /* Not a typewriter */
#define ETXTBSY 	26 /* Text file busy */
#define EFBIG 		27 /* File too large */
#define ENOSPC 		28 /* No space left on device */
#define ESPIPE 		29 /* Illegal seek */
#define EROFS 		30 /* Read-only file system */
#define EMLINK 		31 /* Too many links */
#define EPIPE 		32 /* Broken pipe */
#define EDOM 		33 /* Math argument out of domain of func */
#define ERANGE 		34 /* Math result not representable */
#endif

那么如何判断函数返回的指针是有效地址还是错误码呢?对于IS_ERR()的使用,实例代码如下所示:

myclass = class_create(THIS_MODULE, "myclass");
if (IS_ERR(myclass)) {
    ret = PTR_ERR(myclass);
    goto fail;
}

mydevice = device_create(myclass, NULL, MKDEV(major, 0), NULL, "simple-device");
if (IS_ERR(mydevice)) {
    class_destroy(myclass);
    ret = PTR_ERR(mydevice);
    goto fail;
}

在上述代码中,调用了class_create()device_create()函数,必须使用IS_ERR()函数判断返回的指针是否是有效的,如果是无效的,需要调用PTR_ERR()函数将无效指针转换为错误码,并进行错误码的返回。

17.3 实验程序编写

17.3.1 驱动程序编写

本驱动程序对应的网盘路径为:iTOP-RK3568 开发板【底板V1.7 版本】\03_【iTOP-RK3568开发板】指南教程\02_Linux 驱动配套资料\04_Linux 驱动例程\12\module

本实验在15 章的驱动程序基础上进行编写,进行Linux 错误处理实验。当创建设备号,初始化cdev,注册字符设备,创建类,创建设备的这些函数执行失败时,应该怎么处理呢,编写好的驱动程序如下所示:

#include <linux/init.h>
#include <linux/module.h>
#include <linux/kdev_t.h>
#include <linux/fs.h>
#include <linux/cdev.h>
#include <linux/uaccess.h>

struct device_test{
    dev_t dev_num;  //设备号
     int major ;  //主设备号
    int minor ;  //次设备号
    struct cdev cdev_test; // cdev
    struct class *class;   //类
    struct device *device; //设备
    char kbuf[32];  //定义缓存区kbuf
};

struct  device_test dev1;   //定义一个device_test结构体变量


/*打开设备函数*/
static int cdev_test_open(struct inode *inode, struct file *file)
{
    file->private_data=&dev1;  //设置私有数据
    printk("This is cdev_test_open\r\n");
    return 0;
}

/*向设备写入数据函数*/
static ssize_t cdev_test_write(struct file *file, const char __user *buf, size_t size, loff_t *off)
{
     struct device_test *test_dev=(struct device_test *)file->private_data;

    if (copy_from_user(test_dev->kbuf, buf, size) != 0) // copy_from_user:用户空间向内核空间传数据
    {
        printk("copy_from_user error\r\n");
        return -1;
    }
    printk("This is cdev_test_write\r\n");

    printk("kbuf is %s\r\n", test_dev->kbuf);
    return 0;
}

/**从设备读取数据*/
static ssize_t cdev_test_read(struct file *file, char __user *buf, size_t size, loff_t *off)
{
    
    struct device_test *test_dev=(struct device_test *)file->private_data;
    
    if (copy_to_user(buf, test_dev->kbuf, strlen( test_dev->kbuf)) != 0) // copy_to_user:内核空间向用户空间传数据
    {
        printk("copy_to_user error\r\n");
        return -1;
    }

    printk("This is cdev_test_read\r\n");
    return 0;
}

static int cdev_test_release(struct inode *inode, struct file *file)
{
    printk("This is cdev_test_release\r\n");
    return 0;
}

/*设备操作函数*/
struct file_operations cdev_test_fops = {
    .owner = THIS_MODULE, //将owner字段指向本模块,可以避免在模块的操作正在被使用时卸载该模块
    .open = cdev_test_open, //将open字段指向chrdev_open(...)函数
    .read = cdev_test_read, //将open字段指向chrdev_read(...)函数
    .write = cdev_test_write, //将open字段指向chrdev_write(...)函数
    .release = cdev_test_release, //将open字段指向chrdev_release(...)函数
};

static int __init chr_fops_init(void) //驱动入口函数
{
        /*注册字符设备驱动*/
    int ret;
    /*1 创建设备号*/
    ret = alloc_chrdev_region(&dev1.dev_num, 0, 1, "alloc_name"); //动态分配设备号
    if (ret < 0)
    {
       goto err_chrdev;
    }
    printk("alloc_chrdev_region is ok\n");

    dev1.major = MAJOR(dev1.dev_num); //获取主设备号
    dev1.minor = MINOR(dev1.dev_num); //获取次设备号

    printk("major is %d \r\n", dev1.major); //打印主设备号
    printk("minor is %d \r\n", dev1.minor); //打印次设备号
     /*2 初始化cdev*/
    dev1.cdev_test.owner = THIS_MODULE;
    cdev_init(&dev1.cdev_test, &cdev_test_fops);

    /*3 添加一个cdev,完成字符设备注册到内核*/
   ret =  cdev_add(&dev1.cdev_test, dev1.dev_num, 1);
    if(ret<0)
    {
        goto  err_chr_add;
    }
    /*4 创建类*/
  dev1. class = class_create(THIS_MODULE, "test");
    if(IS_ERR(dev1.class))
    {
        ret=PTR_ERR(dev1.class);
        goto err_class_create;
    }
    /*创建设备*/
  dev1.device = device_create(dev1.class, NULL, dev1.dev_num, NULL, "test");
    if(IS_ERR(dev1.device))
    {
        ret=PTR_ERR(dev1.device);
        goto err_device_create;
    }

    return 0;

 err_device_create:
        class_destroy(dev1.class);                 //删除类

err_class_create:
       cdev_del(&dev1.cdev_test);                 //删除cdev

err_chr_add:
        unregister_chrdev_region(dev1.dev_num, 1); //注销设备号

err_chrdev:
        return ret;
}

static void __exit chr_fops_exit(void) //驱动出口函数
{
    /*注销字符设备*/
    unregister_chrdev_region(dev1.dev_num, 1); //注销设备号
    cdev_del(&dev1.cdev_test);                 //删除cdev
    device_destroy(dev1.class, dev1.dev_num);       //删除设备
    class_destroy(dev1.class);                 //删除类
}
module_init(chr_fops_init);
module_exit(chr_fops_exit);
MODULE_LICENSE("GPL v2");
MODULE_AUTHOR("topeet");

17.3.2 编写测试APP

本应用程序对应的网盘路径为:iTOP-RK3568 开发板【底板V1.7 版本】\03_【iTOP-RK3568开发板】指南教程\02_Linux 驱动配套资料\04_Linux 驱动例程\12\app
编写应用程序app.c,完成的应用程序app.c 代码如下所示,应用程序只是起简单的测试作用。

#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>

int main(int argc, char *argv[]) //主函数
{
    int fd;
    char buf1[32] = "nihao";  //定义写入缓存区buf1
    fd = open("/dev/test", O_RDWR); //打开/dev/test设备
    if (fd < 0)
    {
        perror("open error \n");
        return fd;
    }
    write(fd,buf1,sizeof(buf1)); //向/dev/test设备写入数据
    close(fd);
    return 0;
}

17.4 运行测试

17.4.1 编译驱动程序

在上一小节中的file.c 代码同一目录下创建Makefile 文件,Makefile 文件内容用之前的,然后使用命令“make”进行驱动的编译,编译完生成file.ko 目标文件,至此我们的驱动模块就编译成功了。

17.4.2 编译应用程序

下面进行应用程序编译, 因为测试APP 是要在开发板上运行的, 所以需要aarch64-linux-gnu-gcc 来编译,输入以下命令,编译完成以后会生成一个app 的可执行程序,如下所示:

aarch64-linux-gnu-gcc app.c -o app

下面进行驱动程序的测试。

17.4.3 运行测试

驱动模块file.ko 和测试程序app 都已经准备好了,接下来就是运行测试。输入以下命令,加载驱动程序,如下图(图17-6)所示:

image-20240815100842288

运行应用程序如下(图17-7)所示:

image-20240815100852149

卸载驱动程序,如下图(图17-8)所示:

image-20240815100901919

第18 章 点亮LED 灯实验(GPIO基础知识)

经过前面章节的学习,我们已经对字符设备相关的知识进行了学习和实验,但实际上并没有涉及到对硬件的操作,而在本小节中将通过字符设备驱动及相关的应用程序对LED 灯进行控制,通过对硬件的实际操作,从而对之前学习到的知识进行整合与回顾。

18.1 查看原理图

首先打开底板原理图,如下图(图18-1)所示:

image-20240815101011101

由上图可以看出,LED 灯是由GPIO0_B7 控制的。当GPIO0_B7 为高电平时,三极管Q16导通,LED9 点亮。当GPIO0_B7 为低电平时,三极管Q16 截止,LED9 不亮。

18.2 查询寄存器地址

在上一小节,我们查询到了控制LED 灯的GPIO 为GPIO0_B7。在接下来的实验中需要对GPIO 进行配置,一般情况下需要对GPIO 的复用寄存器,方向寄存器,数据寄存器进行配置。接下来我们打开RK3568 的参考手册part1 查找这几个寄存器的地址。

查找复用寄存器

打开参考手册part1 的第三章,GPIOB 的复用寄存器的偏移地址如下(图18-2)所示:

image-20240815101240858

搜索gpio0b7,如下图(图18-3)所示,gpio0b7_sel 在PMU_GRF_GPIO0B_IOMUX_H 上,所以偏移地址为0x000Cgpio0b7 可以通过控制[14:12]位来选择复用为哪个功能,我们要控制led 灯,所以功能要复用为gpio。

image-20240815101446338

复用寄存器的基地址如下图(图18-4)所示:

image-20240815101537999

所以复用寄存器地址=基地址+偏移地址=0xFDC2000C 。使用io 命令查看此寄存器的地址:

io -r -4 0xFDC2000C

image-20240815101621842

如上图(图18-5)所示,寄存器值为00000001,[14:12]位为000,如下图(图18-6)所示,所以默认设置的为gpio 功能。

image-20240815101647450

查找方向寄存器

打开参考手册part1 的第16 章节,数据寄存器的偏移地址如下图(图18-7)所示:

image-20240815101704802

GPIO 有四组GPIO,分别是GPIOA,GPIOB,GPIOC,GPIOD。每组又以A0A7, B0B7, C0C7, D0D7 作为编号区分。GPIO0B7 在GPIO_SWPORT_DDR_L 上所以,方向寄存器的偏移地址为0x0008。接着查看GPIO_SWPORT_DDR_L 寄存器的具体描述,如下图(图18-8)所示:

image-20240815102343259

如上图(图18-8)所示,[31:16]位属性是WO,也就是只可写入。这[31:16]位是写标志位,是低16 位的写使能。如果低16 位中某一位要设置输入输入输出,则对应高位写标志也应该设置为1。[15:0] 是数据方向控制寄存器低位,如果要设置某个GPIO 为输出,则对应位置1,如果要设置某个GPIO 为输入,则对应位置0。那么GPIO0 B7 ,我们要设置第15 位为输入还是输出,那么对应的[31:16]位写使能也要置1。

打开参考手册part1 的1.1 小节Address Mapping。

image-20240815102406649

image-20240815102415311

如上图(图18-10)所示,GPIO0 的基地址为0xFDD60000。方向寄存器的地址=基地址+偏移地址=0xFDD60000+0x0008=0xFDD60008
然后使用IO 命令查看该寄存器的值,如下(图18-11)所示:

image-20240815102457349

如下图(图18-11)所示,第15 位默认为1,设置GPIO0_B7 为输出。

image-20240815102514204

查找数据寄存器

打开参考手册part1 的1.1 小节Address Mapping。

image-20240815102533184

image-20240815102538048

如上图(图18-13)所示,GPIO0 的基地址为0xFDD60000。
数据寄存器的偏移地址如下(图18-14)所示:

image-20240815102601867

所以数据寄存器的地址为基地址+偏移地址=0xFDD60000。使用IO 命令查看地址的值,如下(图18-15)所示:

image-20240815102624896

我们来看一下这个数据寄存器的描述,如下图(图18-16)所示,

image-20240815102636328

分析上图的方法和在分析方向寄存器的方法同理,由上图可知,如果要控制第15 位为高电平(置1),需要设置31 位为1,那么点亮灯,需要向数据寄存器写入0x8000c040,如下图(图18-17)所示:

image-20240815102700563

如果要灭灯,需要设置第15 位为0 ,第31 位为1,那么向数据寄存器中写入0x80004040,如下图(图18-18)所示:

image-20240815102716444

总结

  • 复用关系寄存器的基地址为0xFDC20000 ,偏移地址为000C ,所以要操作的地址为基地址+偏移地址=0xFDC2000C
  • GPIO 的基地址为0xFDD60000,偏移地址为0x0008,所以方向寄存器要操作的地址为基地址+偏移地址=0xFDD60008
  • GPIO 的基地址为0xFDD60000,偏移地址为0x0000,所以数据寄存器要操作的地址为基地址+偏移地址=0xFDD60000
  • 默认的数据寄存器的值:0x8000c040 亮灯,0x80004040 灭灯18.3 实验程序编写

18.3 实验程序编写

18.3.1 驱动程序编写

本驱动程序对应的网盘路径为:iTOP-RK3568 开发板【底板V1.7 版本】\03_【iTOP-RK3568开发板】指南教程\02_Linux 驱动配套资料\04_Linux 驱动例程\13\module
本次实验在15 章的驱动程序基础上进行编写,通过在应用层传入0/1 数据到内核,如果传入数据是1,则设置GPIO 的数据寄存器值为0x8000c040,如果应用层传入0,则设置GPIO的数据寄存器值为0x80004040,这样就可以达到控制led 的效果, 编写好的驱动程序file.c如下所示:

#include <linux/init.h>
#include <linux/module.h>
#include <linux/kdev_t.h>
#include <linux/fs.h>
#include <linux/cdev.h>
#include <linux/uaccess.h>
#include <linux/io.h>

#define  GPIO_DR 0xFDD60000

struct device_test{
   
    dev_t dev_num;  //设备号
     int major ;  //主设备号
    int minor ;  //次设备号
    struct cdev cdev_test; // cdev
    struct class *class;   //类
    struct device *device; //设备
    char kbuf[32];
    unsigned int *vir_gpio_dr;
};

struct  device_test dev1;  


/*打开设备函数*/
static int cdev_test_open(struct inode *inode, struct file *file)
{
    file->private_data=&dev1;//设置私有数据
    printk("This is cdev_test_open\r\n");

    return 0;
}

/*向设备写入数据函数*/
static ssize_t cdev_test_write(struct file *file, const char __user *buf, size_t size, loff_t *off)
{
     struct device_test *test_dev=(struct device_test *)file->private_data;

    if (copy_from_user(test_dev->kbuf, buf, size) != 0) // copy_from_user:用户空间向内核空间传数据
    {
        printk("copy_from_user error\r\n");
        return -1;
    }
    if(test_dev->kbuf[0]==1){   //如果应用层传入的数据是1,则打开灯
            *(test_dev->vir_gpio_dr) = 0x8000c040;   //设置数据寄存器的地址
              printk("test_dev->kbuf [0]  is %d\n",test_dev->kbuf[0]);  //打印传入的数据
    }
    else if(test_dev->kbuf[0]==0)  //如果应用层传入的数据是0,则关闭灯
    {
            *(test_dev->vir_gpio_dr) = 0x80004040; //设置数据寄存器的地址
            printk("test_dev->kbuf [0]  is %d\n",test_dev->kbuf[0]); //打印传入的数据
    }
    return 0;
}

/**从设备读取数据*/
static ssize_t cdev_test_read(struct file *file, char __user *buf, size_t size, loff_t *off)
{
    
    struct device_test *test_dev=(struct device_test *)file->private_data;
    
    if (copy_to_user(buf, test_dev->kbuf, strlen( test_dev->kbuf)) != 0) // copy_to_user:内核空间向用户空间传数据
    {
        printk("copy_to_user error\r\n");
        return -1;
    }

    printk("This is cdev_test_read\r\n");
    return 0;
}

static int cdev_test_release(struct inode *inode, struct file *file)
{
    printk("This is cdev_test_release\r\n");
    return 0;
}

/*设备操作函数*/
struct file_operations cdev_test_fops = {
    .owner = THIS_MODULE, //将owner字段指向本模块,可以避免在模块的操作正在被使用时卸载该模块
    .open = cdev_test_open, //将open字段指向chrdev_open(...)函数
    .read = cdev_test_read, //将open字段指向chrdev_read(...)函数
    .write = cdev_test_write, //将open字段指向chrdev_write(...)函数
    .release = cdev_test_release, //将open字段指向chrdev_release(...)函数
};

static int __init chr_fops_init(void) //驱动入口函数
{
    /*注册字符设备驱动*/
    int ret;
    /*1 创建设备号*/
    ret = alloc_chrdev_region(&dev1.dev_num, 0, 1, "alloc_name"); //动态分配设备号
    if (ret < 0)
    {
       goto err_chrdev;
    }
    printk("alloc_chrdev_region is ok\n");

    dev1.major = MAJOR(dev1.dev_num); //获取主设备号
   dev1.minor = MINOR(dev1.dev_num); //获取次设备号

    printk("major is %d \r\n", dev1.major); //打印主设备号
    printk("minor is %d \r\n", dev1.minor); //打印次设备号
     /*2 初始化cdev*/
    dev1.cdev_test.owner = THIS_MODULE;
    cdev_init(&dev1.cdev_test, &cdev_test_fops);

    /*3 添加一个cdev,完成字符设备注册到内核*/
   ret =  cdev_add(&dev1.cdev_test, dev1.dev_num, 1);
    if(ret<0)
    {
        goto  err_chr_add;
    }
    /*4 创建类*/
  dev1. class = class_create(THIS_MODULE, "test");
    if(IS_ERR(dev1.class))
    {
        ret=PTR_ERR(dev1.class);
        goto err_class_create;
    }
    /*5  创建设备*/
  dev1.device = device_create(dev1.class, NULL, dev1.dev_num, NULL, "test");
    if(IS_ERR(dev1.device))
    {
        ret=PTR_ERR(dev1.device);
        goto err_device_create;
    }
/*本实验重点*****/
    dev1.vir_gpio_dr=ioremap(GPIO_DR,4);  //将物理地址转化为虚拟地址
    if(IS_ERR(dev1.vir_gpio_dr)){
        ret=PTR_ERR(dev1.vir_gpio_dr);  //PTR_ERR()来返回错误代码
        goto err_ioremap;
    }


return 0;

err_ioremap:
        iounmap(dev1.vir_gpio_dr);

err_device_create:
        class_destroy(dev1.class);                 //删除类

err_class_create:
       cdev_del(&dev1.cdev_test);                 //删除cdev

err_chr_add:
        unregister_chrdev_region(dev1.dev_num, 1); //注销设备号

err_chrdev:
        return ret;
}

static void __exit chr_fops_exit(void) //驱动出口函数
{
    /*注销字符设备*/
    unregister_chrdev_region(dev1.dev_num, 1); //注销设备号
    cdev_del(&dev1.cdev_test);                 //删除cdev
    device_destroy(dev1.class, dev1.dev_num);       //删除设备
    class_destroy(dev1.class);                 //删除类
}
module_init(chr_fops_init);
module_exit(chr_fops_exit);
MODULE_LICENSE("GPL v2");
MODULE_AUTHOR("topeet");

18.3.2 编写测试APP
本应用程序对应的网盘路径为:iTOP-RK3568 开发板【底板V1.7 版本】\03_【iTOP-RK3568开发板】指南教程\02_Linux 驱动配套资料\04_Linux 驱动例程\13\app

编写测试app,led 驱动加载成功之后会生成/dev/test 节点,应用程序APP 通过操作/dev/test文件来完成对LED 设备的控制。向/dev/test 文件写入0 表示关闭LED 灯,写入1 表示打开LED灯。编写完成的应用程序app.c 代码如下所示:

#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <stdlib.h>

int main(int argc, char *argv[])  
{
    int fd;
    char buf[32] = {0};   
    fd = open("/dev/test", O_RDWR);  //打开led驱动
    if (fd < 0)
    {
        perror("open error \n");
        return fd;
    }
    buf[0] =atoi(argv[1]);    // atoi()将字符串转为整型,这里将第一个参数转化为整型后,存放在 buf[0]中
    write(fd,buf,sizeof(buf));  //向/dev/test文件写入数据
    close(fd);     //关闭文件
    return 0;
}

18.4 运行测试

18.4.1 编译驱动程序

在上一小节中的file.c 代码同一目录下创建Makefile 文件,Makefile 文件内容用之前的:然后使用命令“make”进行驱动的编译,编译完生成file.ko 目标文件,

18.4.2 编译应用程序

至此我们的驱动模块就编译成功了,下面进行应用程序编译,因为测试APP 是要在开发板上运行的,所以需要aarch64-linux-gnu-gcc 来编译,输入以下命令,编译完成以后会生成一个app 的可执行程序,如下图(图18-21)所示:

aarch64-linux-gnu-gcc app.c -o app

下面进行驱动程序的测试。

18.4.3 运行测试

驱动模块file.ko 和测试程序app 都已经准备好了,接下来就是运行测试。输入以下命令加载驱动程序,如下(图18-22)所示:

insmod file.ko

image-20240815103646449

然后运行测试程序,输入“./app 1”,LED 灯点亮,如下图(图18-24)所示:

image-20240815103700151

image-20240815103707877

输入“./app 0”,LED 灯熄灭,如下图(图18-26)所示:

image-20240815103720400

image-20240815103724995

第三篇并发与竞争

第19 章并发与竞争实验

在前面章节的学习中,相信大家已经对用户空间与内核空间数据传递进行了实验,假如要传递的数据被存放在了全局变量,该数据就可以作为共享资源被多个任务共同读写,从而造成数据的错误传输,多个程序同时访问一个共享资源产生的问题就叫做竞争。竞争产生的根本原因就是Linux 系统的并发访问。

在本章节中首先会对并发与并行的概念进行讲解,随后对竞争产生的原因进行总结,最后以一个实际的竞争实验加深大家的理解。下面就让我们开始本章节的学习吧。

19.1 并发与竞争

19.1.1 并发

早期计算机大多只有一个CPU 核心,一个CPU 在同一时间只能执行一个任务,当系统中有多个任务等待执行时,CPU 只能执行完一个再执行下一个。而计算机的很多指令会涉及I/O操作,执行速度远远低于CPU 内高速存储器的存取速度,这就导致CPU 经常处于空闲状态,只能等待I/O 操作完成后才能继续执行后面的指令。为了提高CPU 利用率,减少等待时间,提出了CPU 并发工作理论。

所谓并发,就是通过算法将CPU 资源合理地分配给多个任务,当一个任务执行I/O 操作时,CPU 可以转而执行其它的任务,等到I/O 操作完成以后,或者新的任务遇到I/O 操作时,CPU 再回到原来的任务继续执行。

下图(图19-1)展示了两个任务并发执行的过程(为了容易理解,这里以两个任务并发执行为例,当然一个CPU 核心并不仅仅只能两个任务并发):

image-20240815104101714

虽然CPU 在同一时刻只能执行一个任务,但是通过将CPU 的使用权在恰当的时机分配给不同的任务,使得多个任务看起来是一起执行的(CPU 的执行速度极快,多任务切换的时间也极短)。至此关于并发的概念就讲解完成了。

19.1.2 并行

并发是针对单核CPU 提出的,而并行则是针对多核CPU 提出的。和单核CPU 不同,多核CPU 真正实现了“同时执行多个任务”。多核CPU 的每个核心都可以独立地执行一个任务,而且多个核心之间不会相互干扰。在不同核心上执行的多个任务,是真正地同时运行,这种状态就叫做并行。双核CPU 的工作状态如下图(图19-2)所示:

image-20240815104149618

双核CPU 执行两个任务时,每个核心各自执行一个任务,和单核CPU 在两个任务之间不断切换相比,它的执行效率更高。
至此对于并行的概念就讲解完成了。

19.1.3 并发+并行

在并行的工作状态中,两个CPU 分别执行两个任务,是一种理想状态。但是在实际场景中,处于运行状态的任务是非常多的,以实际办公电脑为例,windows 系统在开机之后会运行几十个任务,而CPU 往往只有4 核、8 核等,远远低于任务的数量,这个时候就会同时存在并发和并行两种情况,即所有核心在并行工作的同时,每个核心还要并发工作。
例如一个双核CPU 要执行四个任务,它的工作状态如下图(图19-3)所示:

image-20240815104227812

为了容易理解,这里是以两个任务并发执行为例,当然一个CPU 核心并不仅仅只能两个任务并发,并发任务的数量和操作系统的分配方式、以及每个任务的工作状态有关系。
至此,对于并发+并行的概念讲解就结束了。

并发可以看作是并行的理想状态,为了便于讲解和避免产生歧义,之后的章节无论是并发还是并行,都会统称为并发。

19.1.4 竞争

并发可能会造成多个程序同时访问一个共享资源,这时候由并发同时访问一个共享资源产生的问题就叫做竞争。
竞争产生的原因如下所示:

  • (1)多线程的并发访问。由于Linux 是多任务操作系统,所以多线程访问是竞争产生的基本原因。
  • (2)中断程序的并发访问。中断任务产生后,CPU 会立刻停止当前工作,从而去执行中断中的任务,如果中断任务对共享资源进行了修改,就会产生竞争。
  • (3)抢占式并发访问。linux2.6 及更高版本引入了抢占式内核,高优先级的任务可以打断低优先级的任务。在线程访问共享资源的时候,另一个线程打断了现在正在访问共享资源的线程同时也对共享资源进行操作,从而造成了竞争。
  • (4)多处理器(SMP)并发访问。多核处理器之间存在核间并发访问。

19.1.5 共享资源的保护

竞争是由并发访问同一个共享资源产生的。为了防止“竞争”的产生就要对共享资源进行保护,这里提到的共享资源又是什么呢?
以实际生活中的共享资源为例,可以是公共电话,也可以是共享单车、共享充电宝等公共物品,以上都属于共享资源的范畴,以公共电话为例,每个人都可以对它进行使用,但在同一时间内只能由一个人进行使用,如果两个人都要对电话进行使用,则产生了竞争。而在实际的驱动的代码中,共享资源可以是全局变量,也可以是驱动中的设备结构体等,需要根据具体的驱动程序来进行分析。在下一小节的实验中,会以全局变量为例,进行并发与竞争实验。

19.2 实验程序的编写

19.2.1 驱动程序编写

本实验对应的网盘路径为:iTOP-RK3568 开发板【底板V1.7 版本】\03_【iTOP-RK3568开发板】指南教程\02_Linux 驱动配套资料\04_Linux 驱动例程\14\module
本实验将编写并发与竞争的驱动代码, 首先完善字符设备驱动框架, 然后通过copy_from_user(...)函数接收用户空间传递到内核空间的数据并进行判断,如果接收到的字符串数据为“topeet”会在睡眠4 秒钟后打印接收到的数据,如果接收到的字符串数据为“itop”会在睡眠2 秒钟后打印接收到的数据。

编写完成的example.c 代码如下所示

#include <linux/init.h>
#include <linux/module.h>
#include <linux/fs.h>
#include <linux/cdev.h>
#include <linux/kdev_t.h>
#include <linux/uaccess.h>
#include <linux/delay.h>

static int open_test(struct inode *inode,struct file *file)
{
	printk("\nthis is open_test \n");
	return 0;
}

static ssize_t read_test(struct file *file,char __user *ubuf,size_t len,loff_t *off)
{
	int ret;
	char kbuf[10] = "topeet";//定义char类型字符串变量kbuf
	printk("\nthis is read_test \n");
	ret = copy_to_user(ubuf,kbuf,strlen(kbuf));//使用copy_to_user接收用户空间传递的数据
	if (ret != 0){
		printk("copy_to_user is error \n");
	}
	printk("copy_to_user is ok \n");
	return 0;
}
static char kbuf[10] = {0};//定义char类型字符串全局变量kbuf
static ssize_t write_test(struct file *file,const char __user *ubuf,size_t len,loff_t *off)
{
	int ret;
	ret = copy_from_user(kbuf,ubuf,len);//使用copy_from_user接收用户空间传递的数据
	if (ret != 0){
		printk("copy_from_user is error\n");
	}
	if(strcmp(kbuf,"topeet") == 0 ){//如果传递的kbuf是topeet就睡眠四秒钟
		ssleep(4);
	}
	else if(strcmp(kbuf,"itop") == 0){//如果传递的kbuf是itop就睡眠两秒钟
		ssleep(2);
	}
	printk("copy_from_user buf is %s \n",kbuf);
	return 0;
}
static int release_test(struct inode *inode,struct file *file)
{
	//printk("\nthis is release_test \n");
	return 0;
}


struct chrdev_test {
       dev_t dev_num;//定义dev_t类型变量dev_num来表示设备号
       int major,minor;//定义int类型的主设备号major和次设备号minor
       struct cdev cdev_test;//定义struct cdev 类型结构体变量cdev_test,表示要注册的字符设备
       struct class *class_test;//定于struct class *类型结构体变量class_test,表示要创建的类
};
struct chrdev_test dev1;//创建chrdev_test类型的
struct file_operations fops_test = {
      .owner = THIS_MODULE,//将owner字段指向本模块,可以避免在模块的操作正在被使用时卸载该模块
      .open = open_test,//将open字段指向open_test(...)函数
      .read = read_test,//将read字段指向read_test(...)函数
      .write = write_test,//将write字段指向write_test(...)函数
      .release = release_test,//将release字段指向release_test(...)函数
};
 
static int __init atomic_init(void)
{
	if(alloc_chrdev_region(&dev1.dev_num,0,1,"chrdev_name") < 0 ){//自动获取设备号,设备名chrdev_name
		printk("alloc_chrdev_region is error \n");
	}
	printk("alloc_chrdev_region is ok \n");
	dev1.major = MAJOR(dev1.dev_num);//使用MAJOR()函数获取主设备号
	dev1.minor = MINOR(dev1.dev_num);//使用MINOR()函数获取次设备号
	printk("major is %d,minor is %d\n",dev1.major,dev1.minor);
	cdev_init(&dev1.cdev_test,&fops_test);//使用cdev_init()函数初始化cdev_test结构体,并链接到fops_test结构体
	dev1.cdev_test.owner = THIS_MODULE;//将owner字段指向本模块,可以避免在模块的操作正在被使用时卸载该模块
	cdev_add(&dev1.cdev_test,dev1.dev_num,1);//使用cdev_add()函数进行字符设备的添加
	dev1.class_test = class_create(THIS_MODULE,"class_test");//使用class_create进行类的创建,类名称为class_test
	device_create(dev1.class_test,0,dev1.dev_num,0,"device_test");//使用device_create进行设备的创建,设备名称为device_test
	return 0;
}

static void __exit atomic_exit(void){
	device_destroy(dev1.class_test,dev1.dev_num);//删除创建的设备
	class_destroy(dev1.class_test);//删除创建的类
	cdev_del(&dev1.cdev_test);//删除添加的字符设备cdev_test
	unregister_chrdev_region(dev1.dev_num,1);//释放字符设备所申请的设备号
	printk("module exit \n");
}
module_init(atomic_init);
module_exit(atomic_exit)
MODULE_LICENSE("GPL v2");
MODULE_AUTHOR("topeet");

对于重要逻辑部分已经加粗,后续章节的实验都是对上述并发与竞争实验的改进,以不同的方式来避免竞争的产生。

19.2.2 编写测试APP

本实验应用程序对应的网盘路径为:iTOP-RK3568 开发板【底板V1.7 版本】\03_【iTOP-RK3568 开发板】指南教程\02_Linux 驱动配套资料\04_Linux 驱动例程\14\app
本测试app 较为简单,需要输入两个参数,第一个参数为对应的设备节点,第二个参数为“topeet”或者“itop”,分别代表向设备写入的数据,编写完成的应用程序app.c 内容如下所示:

#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <string.h>
 #include <unistd.h>
int main(int argc, char *argv[])
{
	int fd;//定义int类型的文件描述符
	char str1[10] = {0};//定义读取缓冲区str1
	fd = open(argv[1],O_RDWR);//调用open函数,打开输入的第一个参数文件,权限为可读可写
	if(fd < 0 ){
		printf("file open failed \n");
		return -1;
	}
	/*如果第二个参数为topeet,条件成立,调用write函数,写入topeet*/    
	if (strcmp(argv[2],"topeet") == 0 ){
		write(fd,"topeet",10);
	}
	/*如果第二个参数为itop,条件成立,调用write函数,写入itop*/  
	else if (strcmp(argv[2],"itop") == 0 ){
		write(fd,"itop",10);
	}
	close(fd); 
	return 0;
}

19.3 运行测试

19.3.1 编译驱动程序

在上一小节中的example.c 代码同一目录下创建Makefile 文件,Makefile 文件内容同上,然后使用命令“make”进行驱动的编,编译完生成example.ko 目标文件,至此驱动模块就编译成功了,下面进行应用程序的编译。

19.3.2 编译应用程序

来到应用程序app.c 文件的存放路径如下图(图19-7)所示:

image-20240815105333625

然后使用以下命令对app.c 进行交叉编译,编译完成如下图(图19-8)所示:

aarch64-linux-gnu-gcc -o app app.c -static

image-20240815105355732

生成的app 文件就是之后放在开发板上运行的可执行文件,至此应用程序的编译就完成了。

19.3.3 运行测试

开发板启动之后,使用以下命令进行驱动模块的加载,如下图(图19-9)所示:

insmod example.ko

image-20240815105541977

可以看到申请的主设备号和次设备号就被打印了出来,然后使用以下代码对自动生成的设备节点device_test 进行查看,如下图(图19-10)所示:

ls /dev/device_test

image-20240815105612721

可以看到device_test 节点已经被自动创建了,然后使用以下命令运行测试app,运行结果如下图(图19-11)所示:

./app /dev/device_test topeet

image-20240815105643121

可以看到传递的buf 值为topeet,然后输入以下命令在后台运行两个app,来进行竞争测试,运行结果如下图(图19-12)所示:

./app /dev/device_test topeet &
./app /dev/device_test itop &

image-20240815105722883

在不存在竞争的情况下,传递的两个字符串数据应该是topeet 和itop,而在上图中的打印信息为两个itop,原因是第二个app 应用程序运行之后对共享资源进行了修改,两个app 应用程序就产生了竞争关系,会在之后的章节中使用不同的方法对上述驱动程序进行改进,从而避免竞争的产生。
最后可以使用以下命令进行驱动的卸载,如下图(图19-13)所示:

rmmod example.ko

image-20240815105803647

至此,并发与竞争的实验就完成了。

第20 章原子操作实验

在上一章节的实验中,对并发与竞争进行了实验,两个app 应用程序之间对共享资源的竞争访问引起了数据传输错误,而在Linux 内核中,提供了四种处理并发与竞争的常见方法,分别是原子操作、自旋锁、信号量、互斥体,在之后的几个章节中会依次对上述四种方法进行讲解。
本章首先对四种常见方法中的原子操作进行讲解。

20.1 原子操作

“原子”是化学世界中不可再分的最小微粒,一切物质都由原子组成。在Linux 内核中的原子操作可以理解为“不可被拆分的操作”,就是不能被更高等级中断抢夺优先的操作。在C语言中可以使用以下代码对一个整形变量赋值。

int v;//定义一个int 类型的变量v
v = 1;//将int 类型的变量v 赋值为1

而上述代码仍然不是“不可拆分的操作”,C 语言程序仍然需要翻译成汇编指令,在汇编指令的执行过程中仍可能会有竞争的产生。而原子操作会将整形变量的操作当成一个整体,不可再进行分割。而原子操作又可以进一步细分为“整型原子操作”和“位原子操作”,这里首先对整型原子操作进行讲解。

在Linux 内核中使用atomic_t 和atomic64_t 结构体分别来完成32 位系统和64 位系统的整形数据原子操作,两个结构体定义在“内核源码/include/linux/types.h”文件中,具体定义如下:

typedef struct {
    int counter;
} atomic_t;

#ifdef CONFIG_64BIT
typedef struct {
    long counter;
} atomic64_t;
#endif

例如可以使用以下代码定义一个64 位系统的原子整形变量:

atomic64_t v;

在成功定义原子变量之后,必然要对原子变量进行读取、加减等动作,原子操作的部分常用API 函数如下所示,定义在“内核源码/include/linux/atomic.h”文件中,所以在接下来的实验中需要加入该头文件的引用。

函数 描述
ATOMIC_INIT(int i) 定义原子变量的时候对其初始化,赋值为i
int atomic_read(atomic_t *v) 读取v 的值,并且返回。
void atomic_set(atomic_t *v, int i) 向原子变量v 写入i 值。
void atomic_add(int i, atomic_t *v) 原子变量v 加上i 值。
void atomic_sub(int i, atomic_t *v) 原子变量v 减去i 值。
void atomic_inc(atomic_t *v) 原子变量v 加1
void atomic_dec(atomic_t *v) 原子变量v 减1
int atomic_dec_return(atomic_t *v) 原子变量v 减1,并返回v 的值。
int atomic_inc_return(atomic_t *v) 原子变量v 加1,并返回v 的值。
int atomic_sub_and_test(int i, atomic_t *v) 原子变量v 减i,如果结果为0 就返回真,否则返回假
int atomic_dec_and_test(atomic_t *v) 原子变量v 减1,如果结果为0 就返回真,否则返回假
int atomic_inc_and_test(atomic_t *v) 原子变量v 加1,如果结果为0 就返回真,否则返回假
int atomic_add_negative(int i, atomic_t *v) 原子变量v 加i,如果结果为负就返回真,否则返回假

至此,对于整型原子操作的相关API 函数就讲解完成了,会在下一小节中使用上述原子整形操作API 进行相应的实验。

下面对原子位操作进行讲解,和原子整形变量不同,原子位操作没有atomic_t 的数据结构,原子位操作是直接对内存进行操作,原子位操作相关API 函数如下(图表20-2)所示:

image-20240815112758451

对于原子位操作的知识就不再深入讲解和实验,感兴趣的同学可以到相关网站上进行自主学习。
在下一小节中,将会使用原子整形操作对19 章的并发与竞争实验进行改进。

20.2 实验程序的编写

20.2.1 驱动程序编写

本实验对应的网盘路径为:iTOP-RK3568 开发板【底板V1.7 版本】\03_【iTOP-RK3568开发板】指南教程\02_Linux 驱动配套资料\04_Linux 驱动例程\15\module

为了解决第19 章实验中并发与竞争的问题,本章节实验将加入原子整形操作相关实验代码,在open()函数和release()函数中加入原子整形变量v 的赋值代码,并且在open()函数中加入原子整形变量v 的判断代码,从而实现同一时间内只允许一个应用打开该设备节点,以此来防止共享资源竞争的产生。

编写完成的atomic.c 代码如下所示

#include <linux/init.h>
#include <linux/module.h>
#include <linux/fs.h>
#include <linux/cdev.h>
#include <linux/kdev_t.h>
#include <linux/uaccess.h>
#include <linux/delay.h>
#include <linux/atomic.h>
#include <linux/errno.h>

static atomic64_t v = ATOMIC_INIT(1);//初始化原子类型变量v,并设置为1
static int open_test(struct inode *inode,struct file *file)
{
	if(atomic64_read(&v) != 1){//读取原子类型变量v的值并判断是否等于1
		return -EBUSY;
	}
	atomic64_set(&v,0);//将原子类型变量v的值设置为0
	//printk("\nthis is open_test \n");
	return 0;
}

static ssize_t read_test(struct file *file,char __user *ubuf,size_t len,loff_t *off)
{
	int ret;
	char kbuf[10] = "topeet";//定义char类型字符串变量kbuf
	printk("\nthis is read_test \n");
	ret = copy_to_user(ubuf,kbuf,strlen(kbuf));//使用copy_to_user接收用户空间传递的数据
	if (ret != 0){
		printk("copy_to_user is error \n");
	}
	printk("copy_to_user is ok \n");
	return 0;
}
static char kbuf[10] = {0};//定义char类型字符串全局变量kbuf
static ssize_t write_test(struct file *file,const char __user *ubuf,size_t len,loff_t *off)
{
	int ret;
	ret = copy_from_user(kbuf,ubuf,len);//使用copy_from_user接收用户空间传递的数据
	if (ret != 0){
		printk("copy_from_user is error\n");
	}
	if(strcmp(kbuf,"topeet") == 0 ){//如果传递的kbuf是topeet就睡眠四秒钟
		ssleep(4);
	}
	else if(strcmp(kbuf,"itop") == 0){//如果传递的kbuf是itop就睡眠两秒钟
		ssleep(2);
	}
	printk("copy_from_user buf is %s \n",kbuf);
	return 0;
}
static int release_test(struct inode *inode,struct file *file)
{
	//printk("\nthis is release_test \n");
	atomic64_set(&v,1);//将原子类型变量v的值赋1
	return 0;
}

struct chrdev_test {
       dev_t dev_num;//定义dev_t类型变量dev_num来表示设备号
       int major,minor;//定义int类型的主设备号major和次设备号minor
       struct cdev cdev_test;//定义struct cdev 类型结构体变量cdev_test,表示要注册的字符设备
       struct class *class_test;//定于struct class *类型结构体变量class_test,表示要创建的类
};
struct chrdev_test dev1;//创建chrdev_test类型的
struct file_operations fops_test = {
      .owner = THIS_MODULE,//将owner字段指向本模块,可以避免在模块的操作正在被使用时卸载该模块
      .open = open_test,//将open字段指向open_test(...)函数
      .read = read_test,//将read字段指向read_test(...)函数
      .write = write_test,//将write字段指向write_test(...)函数
      .release = release_test,//将release字段指向release_test(...)函数
};
 
static int __init atomic_init(void)
{
	if(alloc_chrdev_region(&dev1.dev_num,0,1,"chrdev_name") < 0 ){//自动获取设备号,设备名chrdev_name
		printk("alloc_chrdev_region is error \n");
	}
	printk("alloc_chrdev_region is ok \n");
	dev1.major = MAJOR(dev1.dev_num);//使用MAJOR()函数获取主设备号
	dev1.minor = MINOR(dev1.dev_num);//使用MINOR()函数获取次设备号
	printk("major is %d,minor is %d\n",dev1.major,dev1.minor);
	cdev_init(&dev1.cdev_test,&fops_test);//使用cdev_init()函数初始化cdev_test结构体,并链接到fops_test结构体
	dev1.cdev_test.owner = THIS_MODULE;//将owner字段指向本模块,可以避免在模块的操作正在被使用时卸载该模块
	cdev_add(&dev1.cdev_test,dev1.dev_num,1);//使用cdev_add()函数进行字符设备的添加
	dev1.class_test = class_create(THIS_MODULE,"class_test");//使用class_create进行类的创建,类名称为class_test
	device_create(dev1.class_test,0,dev1.dev_num,0,"device_test");//使用device_create进行设备的创建,设备名称为device_test
	return 0;
}

static void __exit atomic_exit(void)
{
	device_destroy(dev1.class_test,dev1.dev_num);//删除创建的设备
	class_destroy(dev1.class_test);//删除创建的类
	cdev_del(&dev1.cdev_test);//删除添加的字符设备cdev_test
	unregister_chrdev_region(dev1.dev_num,1);//释放字符设备所申请的设备号
	printk("module exit \n");
}
module_init(atomic_init);
module_exit(atomic_exit)
MODULE_LICENSE("GPL v2");
MODULE_AUTHOR("topeet");

20.2.2 编写测试APP

本实验应用程序对应的网盘路径为:iTOP-RK3568 开发板【底板V1.7 版本】\03_【iTOP-RK3568 开发板】指南教程\02_Linux 驱动配套资料\04_Linux 驱动例程\15\app
本测试app 代码和上一章节相同,需要输入两个参数,第一个参数为对应的设备节点,第二个参数为“topeet”或者“itop”,分别代表向设备写入的数据,编写完成的应用程序app.c 内容如下所示:

#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <string.h>
 #include <unistd.h>
int main(int argc, char *argv[])
{
	int fd;//定义int类型的文件描述符
	char str1[10] = {0};//定义读取缓冲区str1
	fd = open(argv[1],O_RDWR);//调用open函数,打开输入的第一个参数文件,权限为可读可写
	if(fd < 0 ){
		printf("file open failed \n");
		return -1;
	}
	/*如果第二个参数为topeet,条件成立,调用write函数,写入topeet*/    
	if (strcmp(argv[2],"topeet") == 0 ){
		write(fd,"topeet",10);
	}
	/*如果第二个参数为itop,条件成立,调用write函数,写入itop*/  
	else if (strcmp(argv[2],"itop") == 0 ){
		write(fd,"itop",10);
	}
	close(fd); 
	return 0;
}

20.3.3 运行测试

开发板启动之后,使用以下命令进行驱动模块的加载,如下图(图20-9)所示:

insmod atomic.ko

image-20240815114118054

可以看到申请的主设备号和次设备号就被打印了出来,然后使用以下代码对自动生成的设备节点device_test 进行查看,如下图(图20-10)所示:

ls /dev/device_test

image-20240815114139927

可以看到device_test 节点已经被自动创建了,然后使用以下命令运行测试app,运行结果如下图(图20-11)所示:

./app /dev/device_test topeet

image-20240815114153901

可以看到传递的buf 值为topeet,然后输入以下命令在后台运行两个app,来进行竞争测试,运行结果如下图(图20-12)所示:

./app /dev/device_test topeet &
./app /dev/device_test itop

image-20240815114218884

可以看到应用程序在打开第二次/dev/device_test 文件的时候,出现了“file open failed”打印,证明文件打开失败,只有在第一个应用关闭相应的文件之后,下一个应用才能打开,通过限制同一时间内设备访问数量,来对共享资源进行保护。

最后可以使用以下命令进行驱动的卸载,如下图(图20-13)所示:

rmmod flag.ko

image-20240815114246240

至此,原子操作实验就完成了。

第21 章自旋锁实验

在上一节中对原子操作进行了讲解,并使用原子整形操作对并发与竞争实验进行了改进,但是原子操作只能对整形变量或者位进行保护,而对于结构体或者其他类型的共享资源,原子操作就力不从心了,这时候就轮到自旋锁的出场了,下面就让我们一起来进行自旋锁的学习吧。

21.1 自旋锁

自旋锁是为了保护共享资源提出的一种锁机制。自旋锁(spin lock)是一种非阻塞锁,也就是说,如果某线程需要获取锁,但该锁已经被其他线程占用时,该线程不会被挂起,而是在不断的消耗CPU 的时间,不停的试图获取锁

在有些场景中,同步资源(用来保持一致性的两个或多个资源)的锁定时间很短,为了这一小段时间去切换线程,线程挂起和恢复现场的花费可能会让系统得不偿失。如果计算机有多个CPU 核心,能够让两个或以上的线程同时并行执行,这样我们就可以让后面那个请求锁的线程不放弃CPU 的执行时间,直到持有锁的线程释放锁,后面请求锁的线程才可以获取锁。

为了让后面那个请求锁的线程“稍等一下”,我们需让它进行自旋,如果在自旋完成后前面锁定同步资源的线程已经释放了锁,那么该线程便不必阻塞,并且直接获取同步资源,从而避免切换线程的开销。这就是自旋锁。我们再举个形象生动的例子,以现实生活中银行ATM 机办理业务为例,ATM 机防护舱在同一时间内只允许一个人进入,当有人进入ATM 机防护舱之后,两秒钟之后自动上锁,其他也想要存取款的人员,只能在外部等待,办理完相应的存取款业务之后,舱内人员需要手动打开防护锁,其他人才能进入其中,办理业务。而自旋锁在驱动中的使用和上述ATM 机办理业务流程相同,当一个任务要访问某个共享资源之前需要先获取相应的自旋锁,自旋锁只能被一个任务持有,在该任务持有自旋锁的过程中,其他任务只能原地等待该自旋锁的释放,在等待过程中的任务同样会持续占用CPU,消耗CPU 资源,所以临界区的代码不能太多。

如果自旋锁被错误使用可能会导致死锁的产生,对于自旋锁死锁会在下一章节进行详细说明,并进行相应的实验。
内核中以spinlock_t 结构体来表示自旋锁,定义在“内核源码/include/linux/spinlock_types.h”文件中,如下所示:

typedef struct spinlock {
    union {
    	struct raw_spinlock rlock;

#ifdef CONFIG_DEBUG_LOCK_ALLOC
# define LOCK_PADSIZE (offsetof(struct raw_spinlock, dep_map))
        struct {
            u8 __padding[LOCK_PADSIZE];
            struct lockdep_map dep_map;
    	};
#endif
    };
} spinlock_t;

自旋锁相关API 函数定义在“内核源码/include/linux/spinlock.h”文件中,所以在本章节的实验中要加入该头文件(spinlock.h 头文件包含spinlock_types.h 等,所以只需加入spinlock.h 头文件即可),部分API 函数如下(表21-1)所示,

函数 描述
DEFINE_SPINLOCK(spinlock_t lock) 定义并初始化自旋锁。
int spin_lock_init(spinlock_t *lock) 初始化自旋锁。
void spin_lock(spinlock_t *lock) 获取指定的自旋锁,也叫做加锁。
void spin_unlock(spinlock_t *lock) 释放指定的自旋锁。
int spin_trylock(spinlock_t *lock) 尝试获取指定的自旋锁,如果没有获取到就返回0
int spin_is_locked(spinlock_t *lock) 检查指定的自旋锁是否被获取,如果没有被获取就返回非0,否则返回0。

除了上述API 之外还有其他与终端相关的自旋锁API 函数,会在接下来的自旋锁死锁章节进行讲解。
自旋锁的使用步骤:

  • 1 在访问临界资源的时候先申请自旋锁
  • 2 获取到自旋锁之后就进入临界区,获取不到自旋锁就“原地等待”。
  • 3 退出临界区的时候要释放自旋锁。

在下一小节中将使用上述自旋锁API 进行相应的实验,利用自旋锁相关知识来对第19 章节的并发与竞争实验进行优化。

21.2 实验程序的编写

21.2.1 驱动程序编写

本实验对应的网盘路径为:iTOP-RK3568 开发板【底板V1.7 版本】\03_【iTOP-RK3568开发板】指南教程\02_Linux 驱动配套资料\04_Linux 驱动例程\16\module。

与上一章节使用原子整形操作避免并发与竞争逻辑相同,在驱动入口函数初始化自旋锁,然后在open 函数中使用自旋锁实现对设备的互斥访问,最后在release 函数中解锁,表示设备被释放了,可以被其他的应用程序使用。上述操作都将共享资源由自旋锁进行保护,从而实现同一时间内只允许一个应用打开该设备节点,以此来防止共享资源竞争的产生。

编写完成的spinlock.c 代码如下所示

#include <linux/init.h>
#include <linux/module.h>
#include <linux/fs.h>
#include <linux/cdev.h>
#include <linux/kdev_t.h>
#include <linux/delay.h>
#include <linux/uaccess.h>
#include <linux/spinlock.h>

static spinlock_t spinlock_test;//定义spinlock_t类型的自旋锁变量spinlock_test
static int flag = 1;//定义flag标准为,flag等于1表示设备没有被打开,等于0则证明设备已经被打开了
static int open_test(struct inode *inode,struct file *file)
{
	//printk("\nthis is open_test \n");
	spin_lock(&spinlock_test);//自旋锁加锁
	if(flag != 1){//判断标志位flag的值是否等于1
		spin_unlock(&spinlock_test);//自旋锁解锁
		return -EBUSY;
	 }
	flag = 0;//将标志位的值设置为0
	spin_unlock(&spinlock_test);//自旋锁解锁
	return 0;
}

static ssize_t read_test(struct file *file,char __user *ubuf,size_t len,loff_t *off)
{
	int ret;
	char kbuf[10] = "topeet";//定义char类型字符串变量kbuf
	printk("\nthis is read_test \n");
	ret = copy_to_user(ubuf,kbuf,strlen(kbuf));//使用copy_to_user接收用户空间传递的数据
	if (ret != 0){
		printk("copy_to_user is error \n");
	}
	printk("copy_to_user is ok \n");
	return 0;
}
static char kbuf[10] = {0};//定义char类型字符串全局变量kbuf
static ssize_t write_test(struct file *file,const char __user *ubuf,size_t len,loff_t *off)
{
	int ret;
	ret = copy_from_user(kbuf,ubuf,len);//使用copy_from_user接收用户空间传递的数据
	if (ret != 0){
		printk("copy_from_user is error\n");
	}
	if(strcmp(kbuf,"topeet") == 0 ){//如果传递的kbuf是topeet就睡眠四秒钟
		ssleep(4);
	}
	else if(strcmp(kbuf,"itop") == 0){//如果传递的kbuf是itop就睡眠两秒钟
		ssleep(2);
	}
	printk("copy_from_user buf is %s \n",kbuf);
	return 0;
}
static int release_test(struct inode *inode,struct file *file)
{
	printk("\nthis is release_test \n");
	spin_lock(&spinlock_test);//自旋锁加锁
	flag = 1;
	spin_unlock(&spinlock_test);//自旋锁解锁
	return 0;
}

struct chrdev_test {
       dev_t dev_num;//定义dev_t类型变量dev_num来表示设备号
       int major,minor;//定义int类型的主设备号major和次设备号minor
       struct cdev cdev_test;//定义struct cdev 类型结构体变量cdev_test,表示要注册的字符设备
       struct class *class_test;//定于struct class *类型结构体变量class_test,表示要创建的类
};
struct chrdev_test dev1;//创建chrdev_test类型的
struct file_operations fops_test = {
      .owner = THIS_MODULE,//将owner字段指向本模块,可以避免在模块的操作正在被使用时卸载该模块
      .open = open_test,//将open字段指向open_test(...)函数
      .read = read_test,//将read字段指向read_test(...)函数
      .write = write_test,//将write字段指向write_test(...)函数
      .release = release_test,//将release字段指向release_test(...)函数
};
 
static int __init atomic_init(void)
{
	spin_lock_init(&spinlock_test);
	if(alloc_chrdev_region(&dev1.dev_num,0,1,"chrdev_name") < 0 ){//自动获取设备号,设备名chrdev_name
		printk("alloc_chrdev_region is error \n");
	}
	printk("alloc_chrdev_region is ok \n");
	dev1.major = MAJOR(dev1.dev_num);//使用MAJOR()函数获取主设备号
	dev1.minor = MINOR(dev1.dev_num);//使用MINOR()函数获取次设备号
	printk("major is %d,minor is %d\n",dev1.major,dev1.minor);
	cdev_init(&dev1.cdev_test,&fops_test);//使用cdev_init()函数初始化cdev_test结构体,并链接到fops_test结构体
	dev1.cdev_test.owner = THIS_MODULE;//将owner字段指向本模块,可以避免在模块的操作正在被使用时卸载该模块
	cdev_add(&dev1.cdev_test,dev1.dev_num,1);//使用cdev_add()函数进行字符设备的添加
	dev1.class_test = class_create(THIS_MODULE,"class_test");//使用class_create进行类的创建,类名称为class_test
	device_create(dev1.class_test,0,dev1.dev_num,0,"device_test");//使用device_create进行设备的创建,设备名称为device_test
	return 0;
}

static void __exit atomic_exit(void)
{
	device_destroy(dev1.class_test,dev1.dev_num);//删除创建的设备
	class_destroy(dev1.class_test);//删除创建的类
	cdev_del(&dev1.cdev_test);//删除添加的字符设备cdev_test
	unregister_chrdev_region(dev1.dev_num,1);//释放字符设备所申请的设备号
	printk("module exit \n");
}
module_init(atomic_init);
module_exit(atomic_exit)
MODULE_LICENSE("GPL v2");
MODULE_AUTHOR("topeet");

21.2.2 编写测试APP

本实验应用程序对应的网盘路径为:iTOP-RK3568 开发板【底板V1.7 版本】\03_【iTOP-RK3568 开发板】指南教程\02_Linux 驱动配套资料\04_Linux 驱动例程\16\app
本测试app 代码和上一章节相同,需要输入两个参数,第一个参数为对应的设备节点,第二个参数为“topeet”或者“itop”,分别代表向设备写入的数据,编写完成的应用程序app.c 内容如下所示:

#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <string.h>
 #include <unistd.h>
int main(int argc, char *argv[]){
	int fd;//定义int类型的文件描述符
	char str1[10] = {0};//定义读取缓冲区str1
	fd = open(argv[1],O_RDWR);//调用open函数,打开输入的第一个参数文件,权限为可读可写
	if(fd < 0 ){
		printf("file open failed \n");
		return -1;
	}
	/*如果第二个参数为topeet,条件成立,调用write函数,写入topeet*/    
	if (strcmp(argv[2],"topeet") == 0 ){
		write(fd,"topeet",10);
	}
	/*如果第二个参数为itop,条件成立,调用write函数,写入itop*/  
	else if (strcmp(argv[2],"itop") == 0 ){
		write(fd,"itop",10);
	}
	close(fd); 
	return 0;
}

21.3 运行测试

21.3.3 运行测试

开发板启动之后,使用以下命令进行驱动模块的加载,如下图(图21-7)所示:

insmod spinlock.ko

image-20240815120554389

可以看到申请的主设备号和次设备号就被打印了出来,然后使用以下代码对自动生成的设备节点device_test 进行查看,如下图(21-8)所示:

ls /dev/device_test

image-20240815140149402

可以看到device_test 节点已经被自动创建了,然后使用以下命令运行测试app,运行结果如下图(图21-9)所示:

./app /dev/device_test topeet

image-20240815140212827

可以看到传递的buf 值为topeet,然后输入以下命令在后台运行两个app,来进行竞争测试,运行结果如下图(图21-10)所示:

./app /dev/device_test topeet &
./app /dev/device_test itop

image-20240815140243327

可以看到应用程序在打开第二次/dev/device_test 文件的时候,出现了“file open failed”打印,证明文件打开失败,只有在第一个应用关闭相应的文件之后,下一个应用才能打开。本次实验的自旋锁只是对标志位flag 进行保护,flag 用来表示设备的状态,确保同一时间内,该设备只能被一个应用程序打开。进而对共享资源进行保护。
最后可以使用以下命令进行驱动的卸载,如下图(图21-11)所示:

rmmod spinlock.ko

image-20240815140312012

至此,自旋锁实验就完成了。

第22 章自旋锁死锁实验

在上一小节中,学习了内核中自旋锁的使用,而自旋锁若是使用不当就会产生死锁,在本章将会对自旋锁的特殊情况-死锁进行讲解。

22.1 自旋锁死锁

死锁是指两个或多个事物在同一资源上相互占用,并请求锁定对方的资源,从而导致恶性循环的现象。当多个进程因竞争资源而造成的一种僵局(互相等待),若无外力作用,这些进程都将无法向前推进,这种情况就是死锁。

自旋锁死锁发生存在两种情况:
(1)第一种情况是拥有自旋锁的进程A 在内核态阻塞了,内核调度B 进程,碰巧B 进程也要获得自旋锁,此时B 只能自旋转。而此时抢占已经关闭(在单核条件下)不会调度A 进程了,B 永远自旋,产生死锁,如下图(图22-1)所示:

image-20240815141049857

相应的解决办法是,在自旋锁的使用过程中要尽可能短的时间内拥有自旋锁,而且不能在临界区中调用导致线程休眠的函数。
第二种情况是进程A 拥有自旋锁,中断到来,CPU 执行中断函数,中断处理函数需要获得自旋锁,访问共享资源,此时无法获得锁,只能自旋,从而产生死锁,如下图(图22-2)所示:

image-20240815141440388

对于中断引发的死锁,最好的解决方法就是在获取锁之前关闭本地中断,Linux 内核在“/include/linux/spinlock.h”文件中提供了相应的API 函数,如下(图22-3)所示:

函数 描述
void spin_lock_irq(spinlock_t *lock) 禁止本地中断,并获取自旋锁。
void spin_unlock_irq(spinlock_t *lock) 激活本地中断,并释放自旋锁。
void spin_lock_irqsave(spinlock_t *lock, unsigned long flags) 恢复中断状态,关闭中断并获取自旋锁。
void spin_unlock_irqrestore(spinlock_t *lock, unsigned long flags) 将中断状态恢复到以前的状态,打开中断并释放自旋锁
void spin_lock_bh(spinlock_t *lock) 关闭下半部,获取自旋锁
void spin_unlock_bh(spinlock_t *lock) 打开下半部,获取自旋锁

由于Linux 内核运行是非常复杂的,很难确定某个时刻的中断状态,因此建议使用spin_lock_irqsave/spin_unlock_irqrestore,因为这一组函数会保存中断状态,在释放锁的时候会恢复中断状态。

在下一小节中将进行自旋锁死锁实验,本次实验所采取的是第一种情况,即拥有自旋锁的进程A 在内核态阻塞了,内核调度B 进程,碰巧B 进程也要获得自旋锁,依次产生死锁

22.2 实验程序的编写

22.2.1 驱动程序编写

本实验对应的网盘路径为:iTOP-RK3568 开发板【底板V1.7 版本】\03_【iTOP-RK3568开发板】指南教程\02_Linux 驱动配套资料\04_Linux 驱动例程\17\module

本章节实验以19 章并发与竞争实验为基础,在open()函数中加入了自旋锁加锁,在close()函数中加入了自旋锁解锁,由于在write()函数中存在sleep()睡眠函数,所以会造成内核阻塞,睡眠期间如果使用另一个进程获取该自旋锁,就会造成死锁。

编写完成的dielock.c 代码如下所示

#include <linux/init.h>
#include <linux/module.h>
#include <linux/fs.h>
#include <linux/cdev.h>
#include <linux/kdev_t.h>
#include <linux/delay.h>
#include <linux/uaccess.h>
#include <linux/spinlock.h>

static spinlock_t spinlock_test;//定义spinlock_t类型的自旋锁变量spinlock_test
static int open_test(struct inode *inode,struct file *file)
{
	//printk("\nthis is open_test \n");
	spin_lock(&spinlock_test);//自旋锁加锁
	return 0;
}

static ssize_t read_test(struct file *file,char __user *ubuf,size_t len,loff_t *off)
{
	int ret;
	char kbuf[10] = "topeet";//定义char类型字符串变量kbuf
	printk("\nthis is read_test \n");
	ret = copy_to_user(ubuf,kbuf,strlen(kbuf));//使用copy_to_user接收用户空间传递的数据
	if (ret != 0){
		printk("copy_to_user is error \n");
	}
	printk("copy_to_user is ok \n");
	return 0;
}
static char kbuf[10] = {0};//定义char类型字符串全局变量kbuf
static ssize_t write_test(struct file *file,const char __user *ubuf,size_t len,loff_t *off)
{
	int ret;
	ret = copy_from_user(kbuf,ubuf,len);//使用copy_from_user接收用户空间传递的数据
	if (ret != 0){
		printk("copy_from_user is error\n");
	}
	if(strcmp(kbuf,"topeet") == 0 ){//如果传递的kbuf是topeet就睡眠四秒钟
		ssleep(4);
	}
	else if(strcmp(kbuf,"itop") == 0){//如果传递的kbuf是itop就睡眠两秒钟
		ssleep(2);
	}
	printk("copy_from_user buf is %s \n",kbuf);
	return 0;
}
static int release_test(struct inode *inode,struct file *file)
{
	printk("\nthis is release_test \n");
	spin_unlock(&spinlock_test);//自旋锁解锁
	return 0;
}

struct chrdev_test {
       dev_t dev_num;//定义dev_t类型变量dev_num来表示设备号
       int major,minor;//定义int类型的主设备号major和次设备号minor
       struct cdev cdev_test;//定义struct cdev 类型结构体变量cdev_test,表示要注册的字符设备
       struct class *class_test;//定于struct class *类型结构体变量class_test,表示要创建的类
};
struct chrdev_test dev1;//创建chrdev_test类型的
struct file_operations fops_test = {
      .owner = THIS_MODULE,//将owner字段指向本模块,可以避免在模块的操作正在被使用时卸载该模块
      .open = open_test,//将open字段指向open_test(...)函数
      .read = read_test,//将read字段指向read_test(...)函数
      .write = write_test,//将write字段指向write_test(...)函数
      .release = release_test,//将release字段指向release_test(...)函数
};
 
static int __init atomic_init(void)
{
	spin_lock_init(&spinlock_test);
	if(alloc_chrdev_region(&dev1.dev_num,0,1,"chrdev_name") < 0 ){//自动获取设备号,设备名chrdev_name
		printk("alloc_chrdev_region is error \n");
	}
	printk("alloc_chrdev_region is ok \n");
	dev1.major = MAJOR(dev1.dev_num);//使用MAJOR()函数获取主设备号
	dev1.minor = MINOR(dev1.dev_num);//使用MINOR()函数获取次设备号
	printk("major is %d,minor is %d\n",dev1.major,dev1.minor);
	cdev_init(&dev1.cdev_test,&fops_test);//使用cdev_init()函数初始化cdev_test结构体,并链接到fops_test结构体
	dev1.cdev_test.owner = THIS_MODULE;//将owner字段指向本模块,可以避免在模块的操作正在被使用时卸载该模块
	cdev_add(&dev1.cdev_test,dev1.dev_num,1);//使用cdev_add()函数进行字符设备的添加
	dev1.class_test = class_create(THIS_MODULE,"class_test");//使用class_create进行类的创建,类名称为class_test
	device_create(dev1.class_test,0,dev1.dev_num,0,"device_test");//使用device_create进行设备的创建,设备名称为device_test
	return 0;
}

static void __exit atomic_exit(void)
{
	device_destroy(dev1.class_test,dev1.dev_num);//删除创建的设备
	class_destroy(dev1.class_test);//删除创建的类
	cdev_del(&dev1.cdev_test);//删除添加的字符设备cdev_test
	unregister_chrdev_region(dev1.dev_num,1);//释放字符设备所申请的设备号
	printk("module exit \n");
}
module_init(atomic_init);
module_exit(atomic_exit)
MODULE_LICENSE("GPL v2");
MODULE_AUTHOR("topeet");

22.2.2 编写测试APP

本实验应用程序对应的网盘路径为:iTOP-RK3568 开发板【底板V1.7 版本】\03_【iTOP-RK3568 开发板】指南教程\02_Linux 驱动配套资料\04_Linux 驱动例程\17\app

本测试app 代码和上一章节相同,需要输入两个参数,第一个参数为对应的设备节点,第二个参数为“topeet”或者“itop”,分别代表向设备写入的数据,编写完成的应用程序app.c 内容如下所示:

#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <string.h>
 #include <unistd.h>
int main(int argc, char *argv[])
{
	int fd;//定义int类型的文件描述符
	char str1[10] = {0};//定义读取缓冲区str1
	fd = open(argv[1],O_RDWR);//调用open函数,打开输入的第一个参数文件,权限为可读可写
	if(fd < 0 ){
		printf("file open failed \n");
		return -1;
	}
	/*如果第二个参数为topeet,条件成立,调用write函数,写入topeet*/    
	if (strcmp(argv[2],"topeet") == 0 ){
		write(fd,"topeet",10);
	}
	/*如果第二个参数为itop,条件成立,调用write函数,写入itop*/  
	else if (strcmp(argv[2],"itop") == 0 ){
		write(fd,"itop",10);
	}
	close(fd); 
	return 0;
}

由于本次测试的CPU 为多核心CPU,其他核心仍旧可以调度其他进程,所以需要多次使用taskset 函数指定CPU 进行进程的运行,以此来产生死锁,在与app.c 同级目录下创建名为app.sh的脚本文件,脚本内容如下所示:

#!/bin/bash
taskset -c 0 ./app /dev/device_test topeet &
taskset -c 1 ./app /dev/device_test topeet &
taskset -c 2 ./app /dev/device_test topeet &
taskset -c 3 ./app /dev/device_test topeet &
taskset -c 0 ./app /dev/device_test topeet &
taskset -c 1 ./app /dev/device_test topeet &
taskset -c 2 ./app /dev/device_test topeet &
taskset -c 3 ./app /dev/device_test topeet &

保存退出之后,需要使用以下命令赋予脚本可执行权限,如下图(图22-4)所示:

chmod 777 app.sh

image-20240815164247412

至此测试程序app.c 和运行脚本app.sh 就编写完成了。

22.3 运行测试

22.3.1 编译驱动程序

在上一小节中的dielock.c 代码同一目录下创建Makefile 文件,Makefile 文件内容上小节:然后使用命令“make”进行驱动的编译,编译完生成dielock.ko 目标文件,至此驱动模块就编译成功了,下面进行应用程序的编译。

22.3.2 编译应用程序

来到应用程序app.c 文件的存放路径如下图(图22-8)所示:然后使用以下命令对app.c 进行交叉编译,编译完成如下图(图22-9)所示:

aarch64-linux-gnu-gcc -o app app.c -static

生成的app 文件就是之后放在开发板上运行的可执行文件,至此应用程序的编译就完成了。

22.3.3 运行测试

开发板启动之后,使用以下命令进行驱动模块的加载,如下图(图22-10)所示:

insmod dielock.ko

image-20240815165348452

可以看到申请的主设备号和次设备号就被打印了出来,然后使用以下代码对自动生成的设备节点device_test 进行查看,如下图(图22-11)所示:

ls /dev/device_test

image-20240815165413289

可以看到device_test 节点已经被自动创建了,然后使用以下命令运行app.sh 脚本,该脚本会指定CPU 在加锁之后进入内核休眠状态,如下图(图22-12)所示:

./app.sh

image-20240815165438938

在指令输入之后,串口终端无法输入,引发了死锁,进而造成了系统崩溃,所以在编写驱动的过程中,要尽可能的避免死锁的出现。
至此,自旋锁死锁驱动实验就完成了。

第23 章信号量实验

在上面两个章节对自旋锁和自旋锁死锁进行了学习,自旋锁会让请求的任务原地“自旋”,在等待的过程中会循环检测自旋锁的状态,进而占用系统资源,而本章节要讲解的信号量也是解决竞争的一种常用方法,与自旋锁不同的是,信号量会使等待的线程进入休眠状态,适用于那些占用资源比较久的场合。下面对信号量相关知识的进行讲解。

23.1 信号量

信号量是操作系统中最典型的用于同步和互斥的手段,本质上是一个全局变量,信号量的值表示控制访问资源的线程数,可以根据实际情况来自行设置,如果在初始化的时候将信号量量值设置为大于1,那么这个信号量就是计数型信号量,允许多个线程同时访问共享资源。如果将信号量量值设置为1,那么这个信号量就是二值信号量,同一时间内只允许一个线程访问共享资源,注意!信号量的值不能小于0。当信号量的值为0 时,想访问共享资源的线程必须等待,直到信号量大于0 时,等待的线程才可以访问。当访问共享资源时,信号量执行“减一”操作,访问完成后再执行“加一”操作。

相比于自旋锁,信号量具有休眠特性,因此适用长时间占用资源的场合,但由于信号量会引起休眠,所以不能用在中断函数中,最后如果共享资源的持有时间比较短,使用信号量的话会造成频繁的休眠,反而带来更多资源的消耗,使用自旋锁反而效果更好。在同时使用信号量和自旋锁的时候,要先获取信号量,再使用自旋锁,因为信号量会导致睡眠。

以现实生活中的银行办理业务为例,银行的业务办理窗口就是共享资源,业务办理窗口的数量就是信号量量值,进入银行之后,客户需要领取相应的排序码,然后在休息区进行等待,可以看作线程的睡眠阶段,当前面的客户办理完业务之后,相应的窗口会空闲出来,可以看作信号量的释放,之后银行会通过广播,提醒下一位客户到指定的窗口进行业务的办理,可以看作线程的唤醒并获取到信号量,访问共享资源的过程。

Linux 内核使用semaphore 结构体来表示信号量,该结构体定义在“内核源码/include/linux/semaphore.h”文件内(所以在下一章节的信号量实验中需要加入该头文件),结构体内容如下所示:

struct semaphore {
    raw_spinlock_t lock;
    unsigned int count;
    struct list_head wait_list;
};

与信号量相关的API 函数同样定义在semaphore.h 文件内,部分常用API 函数如下(表23-1)所示:

函数 描述
DEFINE_SEAMPHORE(name) 定义信号量,并且设置信号量的值为1。
void sema_init(struct semaphore *sem, int val) 初始化信号量sem,设置信号量值为val。
void down(struct semaphore *sem) 获取信号量,不能被中断打断,如ctrl+c
int down_interruptible(struct semaphore *sem) 获取信号量,可以被中断打断,如ctrl+c
void up(struct semaphore *sem) 释放信号量
int down_trylock(struct semaphore *sem); 尝试获取信号量,如果能获取到信号量就获取,并且返回0。
如果不能就返回非0

至此,关于信号量相关的知识就讲解完成了,上述API 函数会在下一小节的实验中用到。

23.2 实验程序的编写

23.2.1 驱动程序编写

本实验对应的网盘路径为:iTOP-RK3568 开发板【底板V1.7 版本】\03_【iTOP-RK3568 开发板】指南教程\02_Linux 驱动配套资料\04_Linux 驱动例程\18\module。

与之前章节设置标志位,在同一时间内只允许一个任务对共享资源进行访问的方式所不同,本小节将采用信号量的方式避免竞争的产生。本实验设置的信号量量值为1,所以需要在open()函数中加入信号量获取函数,在release()函数中加入信号量释放函数即可。

编写完成的semaphore.c 代码如下所示

#include <linux/init.h>
#include <linux/module.h>
#include <linux/fs.h>
#include <linux/cdev.h>
#include <linux/kdev_t.h>
#include <linux/uaccess.h>
#include <linux/delay.h>
#include <linux/semaphore.h>

struct semaphore semaphore_test;//定义一个semaphore类型的结构体变量semaphore_test
static int open_test(struct inode *inode,struct file *file)
{
	printk("\nthis is open_test \n");
	down(&semaphore_test);//信号量数量-1
	return 0;
}

static ssize_t read_test(struct file *file,char __user *ubuf,size_t len,loff_t *off)
{
	int ret;
	char kbuf[10] = "topeet";//定义char类型字符串变量kbuf
	printk("\nthis is read_test \n");
	ret = copy_to_user(ubuf,kbuf,strlen(kbuf));//使用copy_to_user接收用户空间传递的数据
	if (ret != 0){
		printk("copy_to_user is error \n");
	}
	printk("copy_to_user is ok \n");
	return 0;
}
static char kbuf[10] = {0};//定义char类型字符串全局变量kbuf
static ssize_t write_test(struct file *file,const char __user *ubuf,size_t len,loff_t *off)
{
	int ret;
	ret = copy_from_user(kbuf,ubuf,len);//使用copy_from_user接收用户空间传递的数据
	if (ret != 0){
		printk("copy_from_user is error\n");
	}
	if(strcmp(kbuf,"topeet") == 0 ){//如果传递的kbuf是topeet就睡眠四秒钟
		ssleep(4);
	}
	else if(strcmp(kbuf,"itop") == 0){//如果传递的kbuf是itop就睡眠两秒钟
		ssleep(2);
	}
	printk("copy_from_user buf is %s \n",kbuf);
	return 0;
}
static int release_test(struct inode *inode,struct file *file)
{
	up(&semaphore_test);//信号量数量加1	
	printk("\nthis is release_test \n");
	return 0;
}

struct chrdev_test {
       dev_t dev_num;//定义dev_t类型变量dev_num来表示设备号
       int major,minor;//定义int类型的主设备号major和次设备号minor
       struct cdev cdev_test;//定义struct cdev 类型结构体变量cdev_test,表示要注册的字符设备
       struct class *class_test;//定于struct class *类型结构体变量class_test,表示要创建的类
};
struct chrdev_test dev1;//创建chrdev_test类型的
struct file_operations fops_test = {
      .owner = THIS_MODULE,//将owner字段指向本模块,可以避免在模块的操作正在被使用时卸载该模块
      .open = open_test,//将open字段指向open_test(...)函数
      .read = read_test,//将read字段指向read_test(...)函数
      .write = write_test,//将write字段指向write_test(...)函数
      .release = release_test,//将release字段指向release_test(...)函数
};
 

static int __init atomic_init(void)
{
	sema_init(&semaphore_test,1);//初始化信号量结构体semaphore_test,并设置信号量的数量为1
	if(alloc_chrdev_region(&dev1.dev_num,0,1,"chrdev_name") < 0 ){//自动获取设备号,设备名chrdev_name
		printk("alloc_chrdev_region is error \n");
	}
	printk("alloc_chrdev_region is ok \n");
	dev1.major = MAJOR(dev1.dev_num);//使用MAJOR()函数获取主设备号
	dev1.minor = MINOR(dev1.dev_num);//使用MINOR()函数获取次设备号
	printk("major is %d,minor is %d\n",dev1.major,dev1.minor);
	cdev_init(&dev1.cdev_test,&fops_test);//使用cdev_init()函数初始化cdev_test结构体,并链接到fops_test结构体
	dev1.cdev_test.owner = THIS_MODULE;//将owner字段指向本模块,可以避免在模块的操作正在被使用时卸载该模块
	cdev_add(&dev1.cdev_test,dev1.dev_num,1);//使用cdev_add()函数进行字符设备的添加
	dev1.class_test = class_create(THIS_MODULE,"class_test");//使用class_create进行类的创建,类名称为class_test
	device_create(dev1.class_test,0,dev1.dev_num,0,"device_test");//使用device_create进行设备的创建,设备名称为device_test
	return 0;
}

static void __exit atomic_exit(void)
{
	device_destroy(dev1.class_test,dev1.dev_num);//删除创建的设备
	class_destroy(dev1.class_test);//删除创建的类
	cdev_del(&dev1.cdev_test);//删除添加的字符设备cdev_test
	unregister_chrdev_region(dev1.dev_num,1);//释放字符设备所申请的设备号
	printk("module exit \n");
}
module_init(atomic_init);
module_exit(atomic_exit)
MODULE_LICENSE("GPL v2");
MODULE_AUTHOR("topeet");

23.2.2 编写测试APP
本实验应用程序对应的网盘路径为: iTOP-RK3568 开发板【底板V1.7 版本】\03_【iTOP-RK3568 开发板】指南教程\02_Linux 驱动配套资料\04_Linux 驱动例程\18\app。
本测试app 代码和上一章节相同,需要输入两个参数,第一个参数为对应的设备节点,第二个参数为“topeet”或者“itop”,分别代表向设备写入的数据,编写完成的应用程序app.c内容如下所示:

int main(int argc, char *argv[])
{
	int fd;//定义int类型的文件描述符
	char str1[10] = {0};//定义读取缓冲区str1
	fd = open(argv[1],O_RDWR);//调用open函数,打开输入的第一个参数文件,权限为可读可写
	if(fd < 0 ){
		printf("file open failed \n");
		return -1;
	}
	/*如果第二个参数为topeet,条件成立,调用write函数,写入topeet*/    
	if (strcmp(argv[2],"topeet") == 0 ){
		write(fd,"topeet",10);
	}
	/*如果第二个参数为itop,条件成立,调用write函数,写入itop*/  
	else if (strcmp(argv[2],"itop") == 0 ){ 
		write(fd,"itop",10);
	}
	close(fd); 
	return 0;
}

23.3 运行测试

23.3.1 编译驱动程序

在上一小节中的semaphore.c 代码同一目录下创建Makefile 文件,然后使用命令“make”进行驱动的编译。编译完生成semaphore.ko 目标文件,至此驱动模块就编译成功了,下面进行应用程序的安装。

23.3.2 编译应用程序

来到应用程序app.c 文件的存放路径如下图(图23-5)所示:然后使用以下命令对app.c 进行交叉编译,编译完成如下图(图23-6)所示:

aarch64-linux-gnu-gcc -o app app.c -static

生成的app 文件就是之后放在开发板上运行的可执行文件,至此应用程序的编译就完成了。

23.3.3 运行测试

开发板启动之后,使用以下命令进行驱动模块的加载,如下图(图23-7)所示:

insmod semaphore.ko

image-20240816102553850

可以看到申请的主设备号和次设备号就被打印了出来,然后使用以下代码对自动生成的设备节点device_test 进行查看,如下图(图23-8)所示:

ls /dev/device_test

image-20240816102627600

可以看到device_test 节点已经被自动创建了,然后使用以下命令运行测试app,运行结果如下图(图23-9)所示:

./app /dev/device_test topeet

image-20240816102647072

可以看到传递的buf 值为topeet,然后输入以下命令在后台运行两个app,来进行竞争测试,运行结果如下图(图23-10)所示:

./app /dev/device_test topeet &
./app /dev/device_test itop

image-20240816102710368

上述打印信息正常,证明数据被正确传递了,没有发生共享资源的竞争,第一个任务运行之后,由于设置的信号量量值为1,所以第二个任务会进入休眠状态,第一个任务执行完毕之后,会唤醒第二个任务去执行,所以避免了并发与竞争。

最后可以使用以下命令进行驱动的卸载,如下图(图23-11)所示:

rmmod semaphore.ko

image-20240816102757761

至此,信号量实验就完成了。

第24 章互斥锁实验

在上一章节中对信号量进行了学习,而本章节要学习的互斥锁可以说是“量值”为1 的信号量,最终实现的效果相同,既然有了信号量,那为什么还要有互斥锁呢,带着疑问,让我们来进行本章节的学习吧!

24.1 互斥锁

在上一章节中,将信号量量值设置为1,最终实现的就是互斥效果,与本章节要学习的互斥锁功能相同,虽然两者功能相同但是具体的实现方式是不同的,但是使用互斥锁效率更高、更简洁,所以如果使用到的信号量“量值”为1,一般将其修改为使用互斥锁实现

当有多个线程几乎同时修改某一个共享数据的时候,需要进行同步控制。线程同步能够保证多个线程安全访问竞争资源,最简单的同步机制是引入互斥锁。互斥锁为资源引入一个状态:锁定或者非锁定。某个线程要更改共享数据时,先将其锁定,此时资源的状态为“锁定”,其他线程不能更改;直到该线程释放资源,将资源的状态变成“非锁定”,其他的线程才能再次锁定该资源。互斥锁保证了每次只有一个线程进行写入操作,从而保证了多线程情况下数据的正确性,能够保证多个线程访问共享数据不会出现资源竞争及数据错误。

为了方便大家理解,这里举个例子来说明。比如公司部门里,我在使用着打印机打印东西的同时(还没有打印完),别人刚好也在此刻使用打印机打印东西,如果不做任何处理的话,打印出来的东西肯定是错乱的。那么怎么解决这种情况呢?只要我在打印着的时候别人是不允许打印的,只有等我打印结束后别人才允许打印。这个过程有点类似于,把打印机放在一个房间里,给这个房间安把锁,这个锁默认是打开的。当A 需要打印时,他先过来检查这把锁有没有锁着,没有的话就进去,同时上锁在房间里打印。而在这时,刚好B 也需要打印,B 同样先检查锁,发现锁是锁住的,他就在门外等着。而当A 打印结束后,他会开锁出来,这时候B 才进去上锁打印。看了这个例子,相信大家已经理解了互斥锁。

互斥锁会导致休眠,所以在中断里面不能用互斥锁。同一时刻只能有一个线程持有互斥锁,并且只有持有者才可以解锁,并且不允许递归上锁和解锁。

内核中以mutex 结构体来表示互斥体,定义在“内核源码/include/linux/mutex.h”文件中,如下所示:

struct mutex {
    atomic_long_t owner;
    spinlock_t wait_lock;
#ifdef CONFIG_MUTEX_SPIN_ON_OWNER
    struct optimistic_spin_queue osq; /* Spinner MCS lock */
#endif
    
    struct list_head wait_list;
#ifdef CONFIG_DEBUG_MUTEXES
    void *magic;
#endif
#ifdef CONFIG_DEBUG_LOCK_ALLOC
    struct lockdep_map dep_map;
#endif
};

一些和互斥体相关的API 函数也定义在mutex.h 文件中,常用API 函数如下(表24-1)所示:

函数 描述
DEFINE_MUTEX(name) 定义并初始化一个mutex 变量。
void mutex_init(mutex *lock) 初始化mutex。
void mutex_lock(struct mutex *lock) 获取mutex,也就是给mutex 上锁。如果获取不到就进休眠。
void mutex_unlock(struct mutex *lock) 释放mutex,也就给mutex 解锁。
int mutex_is_locked(struct mutex *lock) 判断mutex 是否被获取,如果是的话就返回1,否则返回0。
int mutex_trylock(struct mutex *lock); 尝试获取mutex,,得到锁返回真1 反之返回0

至此,关于互斥体相关的知识就讲解完成了,在下一小节的实验中会对上述API 函数进行运用。

24.2 实验程序的编写

24.2.1 驱动程序编写

本实验对应的网盘路径为:iTOP-RK3568 开发板【底板V1.7 版本】\03_【iTOP-RK3568 开发板】指南教程\02_Linux 驱动配套资料\04_Linux 驱动例程\19\module。

本小节实验将使用互斥体对19 章的并发与竞争实验进行改进,由于互斥体在同一时间内只允许一个任务对共享资源进行,所以除了在atomic_init()函数内加入初始化互斥锁函数之外,只需要在open()函数中加入互斥锁加锁函数,在release()函数中加入互斥锁解锁函数即可。

编写完成的mutex.c 代码如下所示

#include <linux/init.h>
#include <linux/module.h>
#include <linux/fs.h>
#include <linux/cdev.h>
#include <linux/kdev_t.h>
#include <linux/uaccess.h>
#include <linux/delay.h>
#include <linux/errno.h>
#include <linux/mutex.h>

struct mutex mutex_test;//定义mutex类型的互斥锁结构体变量mutex_test
static int open_test(struct inode *inode,struct file *file)
{
	printk("\nthis is open_test \n");
	mutex_lock(&mutex_test);//互斥锁加锁
	return 0;
}

static ssize_t read_test(struct file *file,char __user *ubuf,size_t len,loff_t *off)
{
	int ret;
	char kbuf[10] = "topeet";//定义char类型字符串变量kbuf
	printk("\nthis is read_test \n");
	ret = copy_to_user(ubuf,kbuf,strlen(kbuf));//使用copy_to_user接收用户空间传递的数据
	if (ret != 0){
		printk("copy_to_user is error \n");
	}
	printk("copy_to_user is ok \n");
	return 0;
}
static char kbuf[10] = {0};//定义char类型字符串全局变量kbuf
static ssize_t write_test(struct file *file,const char __user *ubuf,size_t len,loff_t *off)
{
	int ret;
	ret = copy_from_user(kbuf,ubuf,len);//使用copy_from_user接收用户空间传递的数据
	if (ret != 0){
		printk("copy_from_user is error\n");
}
	if(strcmp(kbuf,"topeet") == 0 ){//如果传递的kbuf是topeet就睡眠四秒钟
		ssleep(4);
	}
	else if(strcmp(kbuf,"itop") == 0){//如果传递的kbuf是itop就睡眠两秒钟
		ssleep(2);
	}
	printk("copy_from_user buf is %s \n",kbuf);
	return 0;
}
static int release_test(struct inode *inode,struct file *file)
{
	mutex_unlock(&mutex_test);//互斥锁解锁
	printk("\nthis is release_test \n");
	return 0;
}

struct chrdev_test {
       dev_t dev_num;//定义dev_t类型变量dev_num来表示设备号
       int major,minor;//定义int类型的主设备号major和次设备号minor
       struct cdev cdev_test;//定义struct cdev 类型结构体变量cdev_test,表示要注册的字符设备
       struct class *class_test;//定于struct class *类型结构体变量class_test,表示要创建的类
};
struct chrdev_test dev1;//创建chrdev_test类型的
struct file_operations fops_test = {
      .owner = THIS_MODULE,//将owner字段指向本模块,可以避免在模块的操作正在被使用时卸载该模块
      .open = open_test,//将open字段指向open_test(...)函数
      .read = read_test,//将read字段指向read_test(...)函数
      .write = write_test,//将write字段指向write_test(...)函数
      .release = release_test,//将release字段指向release_test(...)函数
};

static int __init atomic_init(void)
{
	mutex_init(&mutex_test);
	if(alloc_chrdev_region(&dev1.dev_num,0,1,"chrdev_name") < 0 ){//自动获取设备号,设备名chrdev_name
		printk("alloc_chrdev_region is error \n");
	}
	printk("alloc_chrdev_region is ok \n");
	dev1.major = MAJOR(dev1.dev_num);//使用MAJOR()函数获取主设备号
	dev1.minor = MINOR(dev1.dev_num);//使用MINOR()函数获取次设备号
	printk("major is %d,minor is %d\n",dev1.major,dev1.minor);
	cdev_init(&dev1.cdev_test,&fops_test);//使用cdev_init()函数初始化cdev_test结构体,并链接到fops_test结构体
	dev1.cdev_test.owner = THIS_MODULE;//将owner字段指向本模块,可以避免在模块的操作正在被使用时卸载该模块
	cdev_add(&dev1.cdev_test,dev1.dev_num,1);//使用cdev_add()函数进行字符设备的添加
	dev1.class_test = class_create(THIS_MODULE,"class_test");//使用class_create进行类的创建,类名称为class_test
	device_create(dev1.class_test,0,dev1.dev_num,0,"device_test");//使用device_create进行设备的创建,设备名称为device_test
	return 0;
}

static void __exit atomic_exit(void)
{
	device_destroy(dev1.class_test,dev1.dev_num);//删除创建的设备
	class_destroy(dev1.class_test);//删除创建的类
	cdev_del(&dev1.cdev_test);//删除添加的字符设备cdev_test
	unregister_chrdev_region(dev1.dev_num,1);//释放字符设备所申请的设备号
	printk("module exit \n");
}
module_init(atomic_init);
module_exit(atomic_exit)
MODULE_LICENSE("GPL v2");
MODULE_AUTHOR("topeet");

24.2.2 编写测试APP

本实验应用程序对应的网盘路径为: iTOP-RK3568 开发板【底板V1.7 版本】\03_【iTOP-RK3568 开发板】指南教程\02_Linux 驱动配套资料\04_Linux 驱动例程\19\app。

本测试app 代码和上一章节相同,需要输入两个参数,第一个参数为对应的设备节点,第二个参数为“topeet”或者“itop”,分别代表向设备写入的数据,

24.3 运行测试

24.3.1 编译驱动程序

在上一小节中的mutex.c 代码同一目录下创建Makefile 文件,Makefile 文件内容。然后使用命令“make”进行驱动的编译,编译完生成mutex.ko 目标文件,至此驱动模块就编译成功了,下面进行应用程序的编译。

24.3.2 编译应用程序

来到应用程序app.c 文件的存放路径如下图(图24-5)所示:

image-20240816103619606

然后使用以下命令对app.c 进行交叉编译,编译完成如下图(图24-6)所示:

aarch64-linux-gnu-gcc -o app app.c -static

生成的app 文件就是之后放在开发板上运行的可执行文件,至此应用程序的编译就完成了。

24.3.3 运行测试
开发板启动之后,使用以下命令进行驱动模块的加载,如下图(图24-7)所示:

insmod mutex.ko

image-20240816103708022

可以看到申请的主设备号和次设备号就被打印了出来,然后使用以下代码对自动生成的设备节点device_test 进行查看,如下图(图24-8)所示:

ls /dev/device_test

image-20240816103726533

可以看到device_test 节点已经被自动创建了,然后使用以下命令运行测试app,运行结果如下图(图24-9)所示:

./app /dev/device_test topeet

image-20240816103747343

可以看到传递的buf 值为topeet,然后输入以下命令在后台运行两个app,来进行竞争测试,运行结果如下图(图24-10)所示:

./app /dev/device_test topeet &
./app /dev/device_test itop

image-20240816103812299

与23 章实验测试现象相同,两个app 被同时运行,最终打印信息正常,证明数据被正确传递了,没有发生共享资源的竞争,证明互斥量就起到了作用。
最后可以使用以下命令进行驱动的卸载,如下图(图24-11)所示:

rmmod mutex.ko

image-20240816103834491

至此,互斥体实验就完成了。

第四篇高级字符设备进阶

第25 章IO 模型引入实验

我们经常提到IO、NIO 这些名词。那么,到底什么是IO 呢?什么又是NIO 呢?另外,我们平时又会听到两组很相似的概念:阻塞/非阻塞、同步/异步。那么,阻塞和非阻塞有什么区别呢?同步和异步的差别又在哪里呢?

为了更好的理解IO 模型,在本章节将对IO 的概念、IO 的执行过程及IO 模型的分类进行详细分析,下面就让我们一起进入IO 的世界吧!

25.1 IO 的概念

IO 是英文Input 和Output 的首字母,代表了输入和输出,当然这样的描述有一点点抽象,更直观的意思是计算机的输入与输出。在冯.诺依曼结构中,将计算机分成了5 个部分,分别是运算器,控制器,存储器,输入设备,输出设备。如下图(图25-1)所示:

image-20240816104322308

上图中的输入设备指的是鼠标和键盘等向计算机输入数据和信息的设备,输出设备指的是电脑显示器等用于计算机信息输出的设备,下面对计算机输入输出过程进行实际举例,当敲击键盘(输入设备)任意按键后,按键的数据会传递给计算机,计算机CPU 会对数据进行运算,运算完成之后会将数据输出到显示器(输出设备)上,整个过程如下图(图25-2)所示:

image-20240816104343227

上述事例中,鼠标、显示器只是输入输出的直观表现形式,而在计算机架构层面上,IO 是涉及计算机核心与其他设备间数据迁移的过程。以磁盘IO 为例,内存读取磁盘数据和将内存数据写入磁盘,就是一对输入输出的过程。

至此,对于IO 的概念就讲解完成了,在下一小节中将对IO 执行过程进行分析。

25.2 IO 执行过程

操作系统(Linux)负责对计算机的资源进行管理和对进程进行调度,应用程序运行在操作系统上,处于用户空间。应用程序不能直接对硬件进行操作,只能通过操作系统提供的API 来操作硬件。需要将进程切换到内核空间,才能进行IO 操作,并且应用程序不能直接操作内核空间的数据,需要把内核空间的数据拷贝到用户空间。

应用程序运行在用户空间,它不存在实质的IO 过程,真正的IO 是在操作系统执行的。那么应用程序操作IO 就会有两个动作:IO 调用和IO 执行。IO 调用是应用程序向操作系统内核发起调用,IO 执行是操作系统内核完成的IO 操作。

一个完整的IO 过程需要包含以下三个步骤,如下图(图25-3)所示:

  • (1) 用户空间的应用程序向内核发起IO 调用请求(系统调用)
  • (2) 内核操作系统准备数据,把IO 设备的数据加载到内核缓冲区
  • (3) 操作系统拷贝数据,把内核缓冲区的数据拷贝到用户进程缓冲区

image-20240816104518918

25.3 IO 模型的分类

假设有这样一个场景,从磁盘中循环读取100M 的数据并处理,磁盘读取100M 需要花费20 秒的时间,CPU 同样也需要20 秒的时间处理完这些数据。如果采用传统的模式编写代码:读数据->等待数据读取完毕->数据处理,可以发现,数据的读取花费了一半的时间,而这就导致该任务的效率极其低下,那么能不能在等待数据的同时对数据进行处理呢?当然可以!这时候就轮到IO 编程模型来出场了。

IO 模型根据实现的功能可以划分为为阻塞IO、非阻塞IO、信号驱动IO, IO 多路复用和异步IO。根据等待IO 的执行结果进行划分,前四个IO 模型又被称为同步IO,如下图(图25-3)所示:

image-20240816104637272

所谓同步,即发出一个功能调用后,只有得到结果该调用才会返回。异步的概念和同步相对。当一个异步过程调用发出后,调用者并不能立刻得到结果,实际处理这个调用的部件在完成后,通过状态、通知和回调来通知调用者。

以现实生活去餐馆吃饭为例,根据菜单进行点餐之后,这时会存在两个选择,第一个选择是在餐馆等待饭菜制作完毕,这就是同步IO 的具体表现。第二个选择是,离开餐馆去做其他的事情,工作人员会在饭菜制作完成之后提醒你回餐馆取餐,这就是异步IO 的具体表现。

下面让我们来认识一下这五种IO 模型。

1 阻塞IO

以阻塞读为例:进程进行IO 操作时(如read 操作),首先会发起一个系统调用,从而转到内核空间进行处理,内核空间的数据没有准备就绪时,进程会被阻塞,不会继续向下执行,直到内核空间的数据准备完成后,数据才会从内核空间拷贝到用户空间,最后返回用户进程,由用户空间进行数据的处理,如下图(图25-5)所示:

image-20240816104822588

以现实生活中的钓鱼为例,在做好相应准备抛下鱼钩之后,需要耐心等待鱼儿的上钩,等待的过程中必须聚精会神的关注鱼竿的状态,鱼儿上钩之后立刻扬竿,这就是阻塞IO 在实际生活中的事例。

通过上述例子可以总结出阻塞IO 的优势与不足,首先可以及时的获取结果,并立刻对获取到的结果进行处理,然而在获取结果之前,无法去处理其他任务,需要时刻对结果进行监听。

阻塞IO 比较有代表性的是C 语言中的scanf()函数。编写好的io.c 文件,如下所示:

#include <stdio.h>
int main(void){
    int i;
    scanf("%d",&i);
    printf("i = %d\n",i);
    return 0;
}

在以上代码中,scanf 函数用于从键盘上接收数据,如果键盘不进行数据的输入,该任务会持续阻塞,只有键盘输入数据之后,才会有相应的输入值打印到系统终端上。输入以下命令进行可执行文件的编译,如下(图25-7)所示

gcc io.c -o io

编译完成之后,输入“./io”运行可执行文件,如下所示,键盘没有输入数据时,该任务会持续阻塞,当在键盘上输入“123”之后,输入的值才会被打印出来,如下(图25-9)所示:

image-20240816105003887

2 非阻塞IO

和阻塞IO 模型不同,非阻塞IO 进行IO 操作时,如果内核数据没有准备好,内核会立即向进程返回err,不会进行阻塞;如果内核空间数据准备就绪,内核会立即把数据返回给用户空间的进程,如下图(图25-10)所示:

image-20240816105055631

仍旧以现实生活中钓鱼为例,在做好相应准备抛下鱼钩之后,这次并没有持续不断的关注鱼竿的状态,而是去做其他的事情(不阻塞等待结果),每隔几分钟对鱼竿的状态进行检查,如果没有鱼儿上钩,就继续去做其他事情,如果上钩了就把鱼钓上来,这就是非阻塞IO 在实际生活中的事例。

从上述案例中可以看出非阻塞IO 的优点是效率高,同样的时间可以做更多的事。但是缺点也很明显,需要不断对结果进行轮询查看,从而导致结果获取不及时(结果可能在两次轮询之间就已经准备完毕,但是只能在发起轮询的时候才能知道),如果要增加非阻塞IO 的实时性,就要加快轮询的频率,但这样无疑也会增加CPU 的负担。

3 IO 多路复用

通常情况下使用select()、poll()、epoll()函数实现IO 多路复用。这里以select 函数为例进行讲解,使用时可以对select 传入多个描述符,并设置超时时间。当执行select 的时候,系统会发起一个系统调用,内核会遍历检查传入的描述符是否有事件发生(如可读、可写事件)。如有,立即返回,否则进入睡眠状态,使进程进入阻塞状态,直到任何一个描述符事件产生后(或者等待超时)立刻返回。此时用户空间需要对全部描述符进行遍历,以确认具体是哪个发生了事件,这样就能使用一个进程对多个IO 进行管理,如下图(图25-11)所示:

image-20240816105159819

继续以现实生活中的钓鱼为例,和之前案例只有一个鱼竿不同,这次会在十个不同的地方做好相应准备抛下鱼钩,并把十个鱼竿连在了一个铃铛上,这样只要铃铛响了就表示有鱼上钩,只需挨个检查到底是哪个鱼竿有鱼上钩即可。

这样的优点是一个进程/线程可以同时监听和处理多路IO,效率成倍提高。但是IO 多路复用并不是能医治百病的良药,虽然IO 多路复用可以监听多个IO,但是实际上对结果的处理也只能依次进行,比较适合IO 密集但是每一路IO 数据量不多且到达时间分散的场合(如网络聊天)。

另外select 监听的描述符有上限(一般描述符最大不超过1024),而且需要遍历究竟是哪一个IO 产生了数据。因此IO 较多时,效率不高(这个问题被epoll 解决,感兴趣的读者可以自行了解)。

4 信号驱动

信号驱动IO 顾名思义与信号相关。系统在一些事件发生之后,会对进程发出特定的信号,而信号与处理函数相绑定,当信号产生时就会调用绑定的处理函数。例如在Linux 系统任务执行的过程中可以按下ctrl+C 来对任务进行终止,系统实际上是对该进程发送一个SIGINT 信号,该信号的默认处理函数就是退出当前程序。

具体到IO 模型上,可以对SIGIO 信号注册相应的信号处理函数,并打开对应描述符的信号驱动。每当有IO 数据产生时,系统就会发送一个SIGIO 信号,进而调用相应的信号处理函数,从而在这个处理函数中对数据进行读取,如下图(图25-12)所示:

image-20240816105341698

仍旧以现实生活中的钓鱼为例,在做好相应准备抛下鱼钩之后,这次同样没有持续不断的关注鱼竿的状态,而是去做其他的事情(不阻塞等待结果),与之前不同的是,在鱼竿处绑定了一个提醒铃铛,当鱼咬钩之后,铃铛就会响(有SIGIO 信号),进而得知到鱼儿上钩的消息之,这样就可以及时把鱼钓上来了(调用处理函数)。

5 异步IO

aio_read 函数常常用于异步IO,当进程使用aio_read 读取数据时,如果数据尚未准备就绪就立即返回,不会阻塞。若数据准备就绪就会把数据从内核空间拷贝到用户空间的缓冲区中,然后执行定义好的回调函数对接收到的数据进行处理。

image-20240816105415638

最后,还是以钓鱼为例。小马同学喜欢吃新鲜的鱼,但是不想自己钓,所以他请了一个助手来帮他钓鱼,他自己去忙其他的事情(进程不阻塞,立即返回)。如果有鱼上钩助手会帮忙钓上来(将数据拷贝到指定的缓冲区),并立即通知小马同学回来把鱼取走(处理数据)。

第26 章阻塞IO 实验

在上一章节,对IO 的相关概念和五种IO 模型分类进行了学习,在接下来的章节中将分别对四种同步IO 进行详细的讲解和实验,本章节要讲解的IO 模型是阻塞IO,阻塞IO 在Linux内核中是非常常用的IO 模型,所依赖的机制是等待队列。下面让我们来开始阻塞IO 的学习吧。

26.1 什么是等待队列

在Linux 驱动程序中,阻塞进程可以使用等待队列来实现。等待队列是内核实现阻塞和唤醒的内核机制,以双循环链表为基础结构,由链表头和链表项两部分组成,分别表示等待队列头和等待队列元素,如下图(图26-1)所示:

image-20240816110111498

等待队列头使用结构体wait_queue_head_t 来表示,等待队列头是一个等待队列的头部,这个结构体定义在文件include/linux/wait.h 里面,结构体内容如下所示:

struct _wait_queue_head{
    spinlock_t lock; //自旋锁
    struct list_head task_list //链表头
};
typefef struct _wait_queue_head wait_queue_head_t;

等待队列项使用结构体wait_queue_t 来表示,等待队列项是等待队列元素,该结构体同样定义在文件include/linux/wait.h 里面,结构体内容如下所示:

struct _wait_queue{
    unsigned int flags;
    void *private;
    wait_queue_func_t func;
    struct list_head task_list;
};
typedef struct _wait_queue wait_queue_t;

26.2 等待队列API 函数

1 定义并初始化等待队列头

等待队列要想被使用,第一步就是对等待队列头进行初始化,有俩种办法如下所示:方法一:使用DECLARE_WAIT_QUEUE_HEAD 宏静态创建等待队列头,宏定义如下:

#define DECLARE_WAIT_QUEUE_HEAD(name) \
wait_queue_head_t name = __WAIT_QUEUE_HEAD_INITIALIZER(name)

参数name 表示要定义的队列头名字。通常以全局变量的方式定义,如下所示:

DECLARE_WAIT_QUEUE_HEAD(head);

方法二:使用init_waitqueue_head 宏动态初始化等待队列头,宏定义如下:

#define init_waitqueue_head(q) \
    do { \
        static struct lock_class_key __key; \
        \
        __init_waitqueue_head((q), #q, &__key); \
    } while (0)

参数q 表示需要初始化的队列头指针。使用宏定义如下所示:

wait_queue_head_t head; 		//等待队列头
init_waitqueue_head(&head); 	//初始化等待队列头指针

然后再来学习如何创建等待队列元素,也就是等待队列项。

2 创建等待队列项

一般使用宏DECLARE_WAITQUEUE(name,tsk)给当前正在运行的进程创建并初始化一个等待队列项,宏定义如下:

#define DECLARE_WAITQUEUE(name, tsk) \
struct wait_queue_entry name = __WAITQUEUE_INITIALIZER(name, tsk)

第一个参数name 是等待队列项的名字,第二个参数tsk 表示此等待队列项属于哪个任务(进程),一般设置为current。在Linux 内核中current 相当于一个全局变量,表示当前进程。创建等待队列项如下所示:

DECLARE_WAITQUEUE(wait,current); //给当前正在运行的进程创建一个名为wait 的等待队列项。
add_wait_queue(wq,&wait); //将wait 这个等待队列项加到wq 这个等待队列当中

3 添加/删除队列

当设备没有准备就绪(如没有可读数据)而需要进程阻塞的时候,就需要将进程对应的等待队列项添加到前面创建的等待队列中,只有添加到等待队列中以后进程才能进入休眠态。当设备可以访问时(如有可读数据),再将进程对应的等待队列项从等待队列中移除即可。

等待队列项添加队列函数如下所示:

函数原型:
    void add_wait_queue(wait_queue_head_t *q,wait_queue_t *wait)
函数功能:
    (通过等待队列头)向等待队列中添加队列项
参数含义:
    wq_head 表示等待队列项要加入等待队列的等待队列头
    wq_entry 表示要加入的等待队列项
函数返回值
    无

等待队列项移除队列函数如下所示:

函数原型:
    void remove_wait_queue(wait_queue_head_t *q,wait_queue_t *wait)
函数功能:
    要删除的等待队列项所处的等待队列头
函数含义:
    第一个参数q 表示等待队列项要加入等待队列的等待队列头
    第二个参数wait 表示要加入的等待队列项
函数返回值:
    无

4 等待事件

除了主动唤醒以外,也可以设置等待队列等待某个事件,当这个事件满足以后就自动唤醒等待队列中的进程,使用如下所示的宏,是不可中断的阻塞等待。

#define __wait_event(wq_head, condition) \
(void)___wait_event(wq_head, condition, TASK_UNINTERRUPTIBLE, 0, 0, \
schedule())

宏定义功能:
    不可中断的阻塞等待,让调用进程进入不可中断的睡眠状态,在等待队列里面睡眠直到condition 变成真,被内核唤醒。
参数含义:
    第一个参数wq: wait_queue_head_t 类型变量
    第二个参数condition : 等待条件,为假时才可以进入休眠。如果condition 为真,则不会休眠

除此之外,wait_event_interruptible 的宏是可中断的阻塞等待。

#define __wait_event_interruptible(wq_head, condition) \
___wait_event(wq_head, condition, TASK_INTERRUPTIBLE, 0, 0, \
schedule())

宏含义功能:
    可中断的阻塞等待,让调用进程进入可中断的睡眠状态,直到condition 变成真被内核唤醒或信号打断唤醒。
参数含义:
    第一个参数wq :wait_queue_head_t 类型变量
    第二个参数condition :等待条件。为假时才可以进入休眠。如果condition 为真,则不会休眠。

wait_event_timeout() 宏也与wait_event()类似.不过如果所给的睡眠时间为负数则立即返回.如果在睡眠期间被唤醒,且condition 为真则返回剩余的睡眠时间,否则继续睡眠直到到达或超过给定的睡眠时间,然后返回0。

wait_event_interruptible_timeout() 宏与wait_event_timeout()类似,不过如果在睡眠期间被信号打断则返回ERESTARTSYS 错误码。
wait_event_interruptible_exclusive() 宏同样和wait_event_interruptible()一样,不过该睡眠的进程是一个互斥进程

注意:调用的时要确认condition 值是真还是假,如果调用condition 为真,则不会休眠。

5 等待队列唤醒

当设备可以使用的时候就要唤醒进入休眠态的进程,唤醒可以使用如下俩个函数

函数原型:
    wake_up(wait_queue_head_t *q)
函数功能:
    唤醒所有休眠进程
参数含义:
    q 表示要唤醒的等待队列的等待队列头
函数原型:
    wake_up_interruptible(wait_queue_head_t *q)
函数功能:
    唤醒可中断的休眠进程
参数含义:
    q 表示要唤醒的等待队列的等待队列头

26.3 等待队列使用方法

  • 步骤一:初始化等待队列头,并将条件置成假(condition=0)。
  • 步骤二:在需要阻塞的地方调用wait_event(),使进程进入休眠状态。
  • 步骤三:当条件满足时,需要解除休眠,先将条件(condition=1),然后调用wake_up 函数唤醒等待队列中的休眠进程。

26.4 实验程序编写

26.4.1 驱动程序编写

本实验对应的驱动网盘路径为:iTOP-RK3568 开发板【底板V1.7 版本】\03_【iTOP-RK3568开发板】指南教程\02_Linux 驱动配套资料\04_Linux 驱动例程\20\module。
接着编写等待队列的实验代码,在此代码中,按照上一小节等待队列使用方法的三个步骤进行编写,在read 函数中调用wait_event_interruptible 函数阻塞,使进程进入休眠状态。在write 函数中唤醒等待队列中的休眠进程。编写好的驱动程序如下所示:

#include <linux/init.h>
#include <linux/module.h>
#include <linux/kdev_t.h>
#include <linux/fs.h>
#include <linux/cdev.h>
#include <linux/uaccess.h>
#include <linux/io.h>
#include  <linux/wait.h>


struct device_test{
    dev_t dev_num;  //设备号
     int major ;  //主设备号
    int minor ;  //次设备号
    struct cdev cdev_test; // cdev
    struct class *class;   //类
    struct device *device; //设备
    char kbuf[32];
    int  flag;  //标志位
};


struct  device_test dev1;  
DECLARE_WAIT_QUEUE_HEAD(read_wq); //定义并初始化等待队列头

/*打开设备函数*/
static int cdev_test_open(struct inode *inode, struct file *file)
{
    file->private_data=&dev1;//设置私有数据
    printk("This is cdev_test_open\r\n");
    return 0;
}

/*向设备写入数据函数*/
static ssize_t cdev_test_write(struct file *file, const char __user *buf, size_t size, loff_t *off)
{
     struct device_test *test_dev=(struct device_test *)file->private_data;

    if (copy_from_user(test_dev->kbuf, buf, size) != 0) // copy_from_user:用户空间向内核空间传数据
    {
        printk("copy_from_user error\r\n");
        return -1;
    }
    test_dev->flag=1;//将条件置1
    wake_up_interruptible(&read_wq); //并使用wake_up_interruptible唤醒等待队列中的休眠进程

    return 0;
}

/**从设备读取数据*/
static ssize_t cdev_test_read(struct file *file, char __user *buf, size_t size, loff_t *off)
{
    
    struct device_test *test_dev=(struct device_test *)file->private_data;

    wait_event_interruptible(read_wq,test_dev->flag); //可中断的阻塞等待,使进程进入休眠态

    if (copy_to_user(buf, test_dev->kbuf, strlen( test_dev->kbuf)) != 0) // copy_to_user:内核空间向用户空间传数据
    {
        printk("copy_to_user error\r\n");
        return -1;
    }

  
    return 0;
}

static int cdev_test_release(struct inode *inode, struct file *file)
{
    return 0;
}

/*设备操作函数*/
struct file_operations cdev_test_fops = {
    .owner = THIS_MODULE, //将owner字段指向本模块,可以避免在模块的操作正在被使用时卸载该模块
    .open = cdev_test_open, //将open字段指向chrdev_open(...)函数
    .read = cdev_test_read, //将open字段指向chrdev_read(...)函数
    .write = cdev_test_write, //将open字段指向chrdev_write(...)函数
    .release = cdev_test_release, //将open字段指向chrdev_release(...)函数
};

static int __init chr_fops_init(void) //驱动入口函数
{
    /*注册字符设备驱动*/
    int ret;
    /*1 创建设备号*/
    ret = alloc_chrdev_region(&dev1.dev_num, 0, 1, "alloc_name"); //动态分配设备号
    if (ret < 0)
    {
       goto err_chrdev;
    }
    printk("alloc_chrdev_region is ok\n");

    dev1.major = MAJOR(dev1.dev_num); //获取主设备号
   dev1.minor = MINOR(dev1.dev_num); //获取次设备号

    printk("major is %d \r\n", dev1.major); //打印主设备号
    printk("minor is %d \r\n", dev1.minor); //打印次设备号
     /*2 初始化cdev*/
    dev1.cdev_test.owner = THIS_MODULE;
    cdev_init(&dev1.cdev_test, &cdev_test_fops);

    /*3 添加一个cdev,完成字符设备注册到内核*/
   ret =  cdev_add(&dev1.cdev_test, dev1.dev_num, 1);
    if(ret<0)
    {
        goto  err_chr_add;
    }
    /*4 创建类*/
  dev1. class = class_create(THIS_MODULE, "test");
    if(IS_ERR(dev1.class))
    {
        ret=PTR_ERR(dev1.class);
        goto err_class_create;
    }
    /*5  创建设备*/
  dev1.device = device_create(dev1.class, NULL, dev1.dev_num, NULL, "test");
    if(IS_ERR(dev1.device))
    {
        ret=PTR_ERR(dev1.device);
        goto err_device_create;
    }

return 0;

 err_device_create:
        class_destroy(dev1.class);                 //删除类

err_class_create:
       cdev_del(&dev1.cdev_test);                 //删除cdev

err_chr_add:
        unregister_chrdev_region(dev1.dev_num, 1); //注销设备号

err_chrdev:
        return ret;
}




static void __exit chr_fops_exit(void) //驱动出口函数
{
    /*注销字符设备*/
    unregister_chrdev_region(dev1.dev_num, 1); //注销设备号
    cdev_del(&dev1.cdev_test);                 //删除cdev
    device_destroy(dev1.class, dev1.dev_num);       //删除设备
    class_destroy(dev1.class);                 //删除类
}
module_init(chr_fops_init);
module_exit(chr_fops_exit);
MODULE_LICENSE("GPL v2");
MODULE_AUTHOR("topeet");

26.4.2 编写测试APP

本实验对应的应用程序网盘路径为: iTOP-RK3568 开发板【底板V1.7 版本】\03_【iTOP-RK3568 开发板】指南教程\02_Linux 驱动配套资料\04_Linux 驱动例程\20\app
接下来编写应用程序read.c,此程序实现了从设备读取数据的功能。编写好的应用程序如下所示:

#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <stdlib.h>

int main(int argc, char *argv[])  
{
    int fd;
    char buf1[32] = {0};   
    char buf2[32] = {0};
    fd = open("/dev/test", O_RDWR);  //打开led驱动
    if (fd < 0)
    {
        perror("open error \n");
        return fd;
    }
    printf("read before \n");
    read(fd,buf1,sizeof(buf1));  //从/dev/test文件读取数据
    printf("buf is %s  \n",buf1);
    printf("read after \n");
    close(fd);     //关闭文件
    return 0;
}

同理,编写应用程序write.c,实现向设备写入数据的功能,编写好的应用程序如下所示:

#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <stdlib.h>
#include <string.h>

int main(int argc, char *argv[])  
{
    int fd;
    char buf1[32] = {0};   
    char buf2[32] = "nihao";
    fd = open("/dev/test", O_RDWR);  //打开led驱动
    if (fd < 0)
    {
        perror("open error \n");
        return fd;
    }
    printf("write before \n");
    write(fd,buf2,sizeof(buf2));  //向/dev/test文件写入数据
     printf("write after\n");
    close(fd);     //关闭文件
    return 0;
}

26.5 运行测试

26.5.1 编译驱动程序

在上一小节中的wq.c 代码同一目录下创建Makefile 文件,Makefile 文件内容。然后使用命令“make”进行驱动的编译,编译完生成wq.ko 目标文件,至此驱动模块就编译成功了,下面交叉编译应用程序。

26.5.2 编译应用程序

来到存放应用程序read.c 和write.c 的文件夹下,使用以下命令对read.c 和write.c 进行交叉编译,编译完成如下图(图26-4)所示:

aarch64-linux-gnu-gcc -o read read.c -static
aarch64-linux-gnu-gcc -o write write.c -static

image-20240816111649439

生成的read write 文件就是之后放在开发板上运行的可执行文件,至此应用程序的编译就完成了。

26.5.3 运行测试

开发板启动之后,使用以下命令进行驱动模块的加载,如下图(图26-5)所示:

insmod wq.ko

image-20240816111721981

输入以下命令运行read 可执行文件,如下图(图26-6)所示,read 应用程序进程阻塞

image-20240816111738676

然后输入以下命令运行write 可执行文件,如下图(图26-7)所示,使用write 函数向设备写入数据,唤醒等待队列中的休眠进程。

image-20240816111750501

在使用可执行程序write 向缓冲区写入数据时,read 可执行程序读取到了缓冲区的数据并打印。

image-20240816111836452

第27 章非阻塞IO 实验

上个章节中我们学习了阻塞IO,阻塞IO 是通过等待队列来实现的,那么如何让驱动实现非阻塞呢?带着疑问,让我们开始本章节非阻塞IO 的学习吧!

27.1 非阻塞IO 简介

应用程序可以使用如下所示示例代码来实现阻塞访问:

int fd;
int data = 0;
fd = open("/dev/xxx_dev", O_RDWR); /* 阻塞方式打开*/
ret = read(fd, &data, sizeof(data)); /* 读取数据*/

可以看出对于设备驱动文件的默认读取方式就是阻塞式的,所以之前实验例程测试APP都是采用阻塞IO。
如果应用程序要采用非阻塞的方式来访问驱动设备文件,可以使用如下所示代码:

int fd;
int data = 0;
fd = open("/dev/xxx_dev", O_RDWR | O_NONBLOCK); /* 非阻塞方式打开*/
ret = read(fd, &data, sizeof(data)); /* 读取数据*/

使用open 函数打开“/dev/xxx_dev”设备文件的时候添加了参数“O_NONBLOCK”,表示以非阻塞方式打开设备,这样从设备中读取数据的时候是非阻塞方式了。

27.2 实验程序编写

27.2.1 编写测试APP

非阻塞IO 实验需要应用程序和驱动配合,所以需要编写驱动代码和应用测试代码。本实验对应的应用程序网盘路径为:iTOP-RK3568 开发板【底板V1.7 版本】\03_【iTOP-RK3568 开发板】指南教程\02_Linux 驱动配套资料\04_Linux 驱动例程\21\app
首先来编写应用测试代码read.c,在此代码中使用非阻塞的方式打开设备,编写好的代码如下所示:

#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <stdlib.h>

int main(int argc, char *argv[])  
{
    int fd;  //定义int类型的文件描述符
    char buf1[32] = {0};   //定义读取缓冲区buf
    char buf2[32] = {0};  //定义读取缓冲区buf
    fd = open("/dev/test",O_RDWR| O_NONBLOCK);  //打开led驱动
    if (fd < 0)
    {
        perror("open error \n");
        return fd;
    }
printf("read before \n");
    while (1)
    {
         read(fd,buf1,sizeof(buf1));  //从/dev/test文件读取数据
         printf("buf is %s \n",buf1);  //打印读取的数据
         sleep(1);
    }
    printf("read after\n");

    close(fd);     //关闭文件
    return 0;
}

接着编写应用程序write.c,用来向设备文件写入数据,编写好的应用程序如下所示:

#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <stdlib.h>
#include <string.h>

int main(int argc, char *argv[])  
{
    int fd;
    char buf1[32] = {0};   
    char buf2[32] = "nihao";
    fd = open("/dev/test", O_RDWR|O_NONBLOCK);  //打开led驱动
    if (fd < 0)
    {
        perror("open error \n");
        return fd;
    }
    printf("write before \n");
    write(fd,buf2,sizeof(buf2));  //向/dev/test文件写入数据
    printf("write after\n");
    close(fd);     //关闭文件
    return 0;
}

27.2.1 驱动程序编写

本实验对应的网盘路径为:iTOP-RK3568 开发板【底板V1.7 版本】\03_【iTOP-RK3568开发板】指南教程\02_Linux 驱动配套资料\04_Linux 驱动例程\21\module
编写好的驱动程序wq.c 如下所示:

#include <linux/init.h>
#include <linux/module.h>
#include <linux/kdev_t.h>
#include <linux/fs.h>
#include <linux/cdev.h>
#include <linux/uaccess.h>
#include <linux/io.h>
#include  <linux/wait.h>


struct device_test{
   
    dev_t dev_num;  //设备号
     int major ;  //主设备号
    int minor ;  //次设备号
    struct cdev cdev_test; // cdev
    struct class *class;   //类
    struct device *device; //设备
    char kbuf[32];
    int  flag;  //标志位
};


struct  device_test dev1;  

DECLARE_WAIT_QUEUE_HEAD(read_wq); //定义并初始化等待队列头

/*打开设备函数*/
static int cdev_test_open(struct inode *inode, struct file *file)
{
    file->private_data=&dev1;//设置私有数据
    
    return 0;
}

/*向设备写入数据函数*/
static ssize_t cdev_test_write(struct file *file, const char __user *buf, size_t size, loff_t *off)
{
     struct device_test *test_dev=(struct device_test *)file->private_data;

    if (copy_from_user(test_dev->kbuf, buf, size) != 0) // copy_from_user:用户空间向内核空间传数据
    {
        printk("copy_from_user error\r\n");
        return -1;
    }
    test_dev->flag=1;    //将条件置1,并使用wake_up_interruptible唤醒等待队列中的休眠进程
    wake_up_interruptible(&read_wq); 

    return 0;
}

/**从设备读取数据*/
static ssize_t cdev_test_read(struct file *file, char __user *buf, size_t size, loff_t *off)
{
    
    struct device_test *test_dev=(struct device_test *)file->private_data;
    if(file->f_flags & O_NONBLOCK ){
        if (test_dev->flag !=1)
        return -EAGAIN;
    }
    wait_event_interruptible(read_wq,test_dev->flag); //可中断的阻塞等待,使进程进入休眠态

    if (copy_to_user(buf, test_dev->kbuf, strlen( test_dev->kbuf)) != 0) // copy_to_user:内核空间向用户空间传数据
    {
        printk("copy_to_user error\r\n");
        return -1;
    }


    return 0;
}

static int cdev_test_release(struct inode *inode, struct file *file)
{

    return 0;
}

/*设备操作函数*/
struct file_operations cdev_test_fops = {
    .owner = THIS_MODULE, //将owner字段指向本模块,可以避免在模块的操作正在被使用时卸载该模块
    .open = cdev_test_open, //将open字段指向chrdev_open(...)函数
    .read = cdev_test_read, //将open字段指向chrdev_read(...)函数
    .write = cdev_test_write, //将open字段指向chrdev_write(...)函数
    .release = cdev_test_release, //将open字段指向chrdev_release(...)函数
};

static int __init chr_fops_init(void) //驱动入口函数
{
    /*注册字符设备驱动*/
    int ret;
    /*1 创建设备号*/
    ret = alloc_chrdev_region(&dev1.dev_num, 0, 1, "alloc_name"); //动态分配设备号
    if (ret < 0)
    {
       goto err_chrdev;
    }
    printk("alloc_chrdev_region is ok\n");

    dev1.major = MAJOR(dev1.dev_num); //获取主设备号
   dev1.minor = MINOR(dev1.dev_num); //获取次设备号

    printk("major is %d \r\n", dev1.major); //打印主设备号
    printk("minor is %d \r\n", dev1.minor); //打印次设备号
     /*2 初始化cdev*/
    dev1.cdev_test.owner = THIS_MODULE;
    cdev_init(&dev1.cdev_test, &cdev_test_fops);

    /*3 添加一个cdev,完成字符设备注册到内核*/
   ret =  cdev_add(&dev1.cdev_test, dev1.dev_num, 1);
    if(ret<0)
    {
        goto  err_chr_add;
    }
    /*4 创建类*/
  dev1. class = class_create(THIS_MODULE, "test");
    if(IS_ERR(dev1.class))
    {
        ret=PTR_ERR(dev1.class);
        goto err_class_create;
    }
    /*5  创建设备*/
  dev1.device = device_create(dev1.class, NULL, dev1.dev_num, NULL, "test");
    if(IS_ERR(dev1.device))
    {
        ret=PTR_ERR(dev1.device);
        goto err_device_create;
    }

return 0;

 err_device_create:
        class_destroy(dev1.class);                 //删除类

err_class_create:
       cdev_del(&dev1.cdev_test);                 //删除cdev

err_chr_add:
        unregister_chrdev_region(dev1.dev_num, 1); //注销设备号

err_chrdev:
        return ret;
}

static void __exit chr_fops_exit(void) //驱动出口函数
{
    /*注销字符设备*/
    unregister_chrdev_region(dev1.dev_num, 1); //注销设备号
    cdev_del(&dev1.cdev_test);                 //删除cdev
    device_destroy(dev1.class, dev1.dev_num);       //删除设备
    class_destroy(dev1.class);                 //删除类
}
module_init(chr_fops_init);
module_exit(chr_fops_exit);
MODULE_LICENSE("GPL v2");
MODULE_AUTHOR("topeet");

27.3 运行测试

27.3.1 编译驱动程序

在上一小节中的wq.c 代码同一目录下创建Makefile 文件,Makefile 文件内容,然后使用命令“make”进行驱动的编译,编译完生成wq.ko 目标文件,至此驱动模块就编译成功了,下面交叉编译应用程序。

27.3.2 编译应用程序

来到存放应用程序read.c 和write.c 的文件夹下,使用以下命令对read.c 和write.c 进行交叉编译,编译完成如下图(图27-4)所示:

aarch64-linux-gnu-gcc -o read read.c -static
aarch64-linux-gnu-gcc -o write write.c -static

image-20240816112845130

生成的read write 文件就是之后放在开发板上运行的可执行文件,至此应用程序的编译就完成了。

27.3.3 测试

开发板启动之后,使用以下命令进行驱动模块的加载,如下图(图27-5)所示:

insmod wq.ko

image-20240816112905317

输入以下命令运行read 可执行文件,如下图(图27-6)所示,应用程序进程非阻塞,读取不到数据便返回,然后一直轮询查看是否有数据。

image-20240816112920176

然后输入以下命令运行write 可执行文件向设备文件写入数据,如下图(图27-7)所示:

image-20240816112932069

在使用可执行程序write 向缓冲区写入数据时,read 可执行程序读取到了缓冲区的数据并打印。

image-20240816112946639

第28 章IO 多路复用实验

在上俩个章节中,我们对阻塞IO 和非阻塞IO 进行了学习,本章节将学习第三种IO 模型-多路复用IO。

28.1 IO 多路复用简介

IO 多路复用是一种同步的IO 模型。IO 多路复用可以实现一个进程监视多个文件描述符。一旦某个文件描述符准备就绪,就通知应用程序进行相应的读写操作。没有文件描述符就绪时就会阻塞应用程序,从而释放出CPU 资源。

在第25 章中,我们以钓鱼为例,对IO 多路复用有了一个简单的认识。下面对钓鱼例子进行回顾:小李同时放置了十个鱼竿,并把十个鱼竿连在了一个铃铛上。这样小李就不必在岸边等待。当铃铛响了就表示有鱼上钩,再回来挨个检查到底是哪个鱼竿有鱼上钩即可。接着进一步体会IO 多路复用。

在应用层Linux 提供了三种实现IO 多路复用的模型,分别是select、poll 和epoll。在本驱动手册中主要偏重于对驱动的讲解,所以应用层中select、poll 和epoll 函数的使用在这里做重点讲解。

首先来学习下select、poll 和epoll 函数有什么区别呢?poll 函数和seslect 函数都可以监听多个文件描述符,通过轮询来获取已经准备好的文件描述符。但是epoll 函数将主动轮询变成了被动通知,当事件发生时被动接收通知。为了方便理解,举个形象的例子。假如poll 和select是公司的前台,某天一位客户来公司找硬件工程师-小李,请求前台帮忙找人。于是poll 和select前台带着这位客户挨个屋子寻找小李,直到找到小李为止。假如epoll 是公司的前台,他提前统计了公司每个员工的工位。当客户来找小李的时候,不必像poll select 一样,可以直接带着客户到硬件部门去找小李。从上面的俩个例子,明显对比epoll 的效率更高。假如公司园区很大,那么poll select 需要花费很长时间寻找小李,而epoll 已经提前知道小李坐在哪个工位了,直接带客户去找小李即可。

select,poll,epoll 有什么区别呢?在单个线程中,select 函数最大可以监视1024 个文件描述符而poll 函数和select 函数并没有什么区别只是poll 函数没有最大文件描述符的限制。在本章节的实验中,以poll 为例进行实验。在Linux 应用程序中poll 函数如下所示:

函数原型:
int poll(struct pollfd *fds, nfds_t nfds, int timeout);
函数功能:
    监视并等待多个文件描述符的属性变化
    
函数参数:
    第一个参数fds: 要监视的文件描述符集合以及要监视的事件,为一个数组,数组元素都是结构体pollfd 类型,pollfd 结构体如下所示:
        struct pollfd {
            int fd; //被监视的文件描述符
            short events; //等待的事件
            short revents; //实际发生的事件
        };
        在pollfd 结构体中,
            第一个成员fd 是被监视的文件描述符。
            第二个成员events 是要监视的事件,可监视的事件类型如下所示:
                    POLLIN 有数据可以读取
                    POLLPRI 有紧急的数据需要读取
                    POLLOUT 可以写数据
                    POLLERR 指定的文件描述符发生错误
                    POLLHUP 指定的文件描述符挂起
                    POLLNVAL 无效的请求
                    POLLRDNORM 等同于POLLIN
            第三个成员是返回事件,由Linux 内核设置具体的返回事件。
    第二个参数nfds: poll 函数要监视的文件描述符数量
    第三个参数timeout:指定等待的时间,单位是ms。无论I/O 是否准备好,时间到,POLL就会返回。如果timepoll 大于0 等待指定的时间,如果timeout 等于0,立即返回。如果timeout等于-1,事件发生以后才返回。
        
函数返回值:
    失败返回-1,成功返回revents 不为0 的文件描述符个数。

当应用程序使用select 或者poll 函数对驱动程序进行非阻塞访问时,驱动程序中file_operations 操作集的poll 函数会执行。所以需要完善驱动中的poll 函数。驱动中的poll 函数原型如下所示:

unsigned int (*poll)(struct file *filp,struct poll_table_struct *wait);
函数参数:
    filp:要打开的文件描述符
    wait: 结构体poll_table_struct 类型指针,此参数是由应用程序中传递的。一般此参数要传递给poll_wait 函数。
返回值:
    向应用程序返回资源状态,可以返回的资源状态如下:
    POLLIN 有数据可以读取
    POLLPRI 有紧急的数据需要读取
    POLLOUT 可以写数据
    POLLERR 指定的文件描述符发生错误
    POLLHUP 指定的文件描述符挂起
    POLLNVAL 无效的请求
    POLLRDNORM 等同于POLLIN,普通数据可读。
函数功能:
    这个函数要进行下面两项工作。首先,对可能引起设备文件状态变化的等待队列调用 poll_wait(),将对应的等待队列头添加到 poll_table.然后返回表示是否能对设备进行无阻塞读写访问的掩码。

驱动程序的poll 函数中调用poll_wait 函数,注意!poll_wait 函数是不会引起阻塞的。poll_wait 函数原型如下所示:

void poll_wait(struct file *filp,wait_queue_head_t *queue,poll_table *wait);

参数queue 是要添加到poll_table 中的等待队列头,参数wait 是poll_table,也就是file_operations 中poll 函数的wait 参数。

28.2 实验程序编写

28.2.1 编写测试APP

本实验对应的应用程序网盘路径为:iTOP-RK3568 开发板【底板V1.7 版本】\03_【iTOP-RK3568 开发板】指南教程\02_Linux 驱动配套资料\04_Linux 驱动例程\22\app。
**在应用层Linux 提供了三种API 函数,分别是select pollepoll**。本次实验使用poll 函数进行实验,如果对select 和epoll 函数感兴趣,可以查找一些系统编程课程学习。

编写好的应用程序read.c 如下所示:

#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <stdlib.h>
#include <poll.h>

int main(int argc, char *argv[])  
{
    int fd;//要监视的文件描述符
    char buf1[32] = {0};   
    char buf2[32] = {0};
    struct pollfd  fds[1];
    int ret;
    fd = open("/dev/test", O_RDWR);  //打开/dev/test设备,阻塞式访问
    if (fd < 0){
        perror("open error \n");
        return fd;
    }

    fds[0] .fd =fd;
    fds[0].events = POLLIN;
    printf("read before \n");
    while (1)
    {
        ret = poll(fds,1,3000);
    	if(!ret){
            printf("time out !!\n");
        }
        else if(fds[0].revents == POLLIN){
            read(fd,buf1,sizeof(buf1));  //从/dev/test文件读取数据
            printf("buf is %s \n",buf1);
            sleep(1);
    	}
    }
    printf("read after\n");
    close(fd);     //关闭文件
    return 0;
}

上述代码第16 行,在打开设备节点时不使用非阻塞方式,要使用阻塞方式,所以改为O_RDWR。

在上述代码的第28 行,使用poll 函数监视并等待多个文件描述符的属性变化。poll 函数第1个参数是被监视的文件描述符,是pollfd 结构体类型的数组,所以在14 行定义了pollfd结构体类型的数组fds。poll 函数第2个参数是要监视的文件描述符数量,这里监视的文件描述符为1 个。poll 函数第3 个参数是指定等待的时间3000ms。

然后编写应用程序write.c,实现向设备文件写入数据的功能,编写好的write.c 如下所示:

#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <stdlib.h>
#include <string.h>

int main(int argc, char *argv[])  
{
    int fd;
    char buf1[32] = {0};   
    char buf2[32] = "nihao";
    fd = open("/dev/test",O_RDWR);  //打开led驱动
    if (fd < 0)
    {
        perror("open error \n");
        return fd;
    }
    printf("write before \n");
    write(fd,buf2,sizeof(buf2));  //向/dev/test文件写入数据
     printf("write after\n");
    close(fd);     //关闭文件
    return 0;
}

28.2.2 驱动程序编写

本实验对应的驱动程序网盘路径为:iTOP-RK3568 开发板【底板V1.7 版本】\03_【iTOP-RK3568 开发板】指南教程\02_Linux 驱动配套资料\04_Linux 驱动例程\22\module
IO 多路复用实验需要应用程序和驱动程序进行配合,接下来编写驱动程序。编写好的驱动程序如下所示:

#include <linux/init.h>
#include <linux/module.h>
#include <linux/kdev_t.h>
#include <linux/fs.h>
#include <linux/cdev.h>
#include <linux/uaccess.h>
#include <linux/io.h>
#include  <linux/wait.h>
#include <linux/poll.h>

struct device_test{
   
    dev_t dev_num;  //设备号
     int major ;  //主设备号
    int minor ;  //次设备号
    struct cdev cdev_test; // cdev
    struct class *class;   //类
    struct device *device; //设备
    char kbuf[32];
    int  flag;  //标志位
};

struct  device_test dev1;  
DECLARE_WAIT_QUEUE_HEAD(read_wq); //定义并初始化等待队列头

/*打开设备函数*/
static int cdev_test_open(struct inode *inode, struct file *file)
{
    file->private_data=&dev1;//设置私有数据
    return 0;
}

/*向设备写入数据函数*/
static ssize_t cdev_test_write(struct file *file, const char __user *buf, size_t size, loff_t *off)
{
     struct device_test *test_dev=(struct device_test *)file->private_data;

    if (copy_from_user(test_dev->kbuf, buf, size) != 0) // copy_from_user:用户空间向内核空间传数据
    {
        printk("copy_from_user error\r\n");
        return -1;
    }
    test_dev->flag=1;
    wake_up_interruptible(&read_wq);

    return 0;
}

/**从设备读取数据*/
static ssize_t cdev_test_read(struct file *file, char __user *buf, size_t size, loff_t *off)
{
    struct device_test *test_dev=(struct device_test *)file->private_data;
    if(file->f_flags & O_NONBLOCK ){
        if (test_dev->flag !=1)
        return -EAGAIN;
    }
    wait_event_interruptible(read_wq,test_dev->flag);

    if (copy_to_user(buf, test_dev->kbuf, strlen( test_dev->kbuf)) != 0) // copy_to_user:内核空间向用户空间传数据
    {
        printk("copy_to_user error\r\n");
        return -1;
    }

    return 0;
}

static int cdev_test_release(struct inode *inode, struct file *file)
{
    return 0;
}

static  __poll_t  cdev_test_poll(struct file *file, struct poll_table_struct *p){
     struct device_test *test_dev=(struct device_test *)file->private_data;  //设置私有数据
     __poll_t mask=0;    
     poll_wait(file,&read_wq,p);     //应用阻塞
     if (test_dev->flag == 1)    
     {
         mask |= POLLIN;
     }
     return mask;
     
}

/*设备操作函数*/
struct file_operations cdev_test_fops = {
    .owner = THIS_MODULE, //将owner字段指向本模块,可以避免在模块的操作正在被使用时卸载该模块
    .open = cdev_test_open, //将open字段指向chrdev_open(...)函数
    .read = cdev_test_read, //将open字段指向chrdev_read(...)函数
    .write = cdev_test_write, //将open字段指向chrdev_write(...)函数
    .release = cdev_test_release, //将open字段指向chrdev_release(...)函数
    .poll = cdev_test_poll,  //将poll字段指向chrdev_poll(...)函数
};

static int __init chr_fops_init(void) //驱动入口函数
{
    /*注册字符设备驱动*/
    int ret;
    /*1 创建设备号*/
    ret = alloc_chrdev_region(&dev1.dev_num, 0, 1, "alloc_name"); //动态分配设备号
    if (ret < 0)
    {
       goto err_chrdev;
    }
    printk("alloc_chrdev_region is ok\n");

    dev1.major = MAJOR(dev1.dev_num); //获取主设备号
   dev1.minor = MINOR(dev1.dev_num); //获取次设备号

    printk("major is %d \r\n", dev1.major); //打印主设备号
    printk("minor is %d \r\n", dev1.minor); //打印次设备号
     /*2 初始化cdev*/
    dev1.cdev_test.owner = THIS_MODULE;
    cdev_init(&dev1.cdev_test, &cdev_test_fops);

    /*3 添加一个cdev,完成字符设备注册到内核*/
   ret =  cdev_add(&dev1.cdev_test, dev1.dev_num, 1);
    if(ret<0)
    {
        goto  err_chr_add;
    }
    /*4 创建类*/
  dev1. class = class_create(THIS_MODULE, "test");
    if(IS_ERR(dev1.class))
    {
        ret=PTR_ERR(dev1.class);
        goto err_class_create;
    }
    /*5  创建设备*/
  	dev1.device = device_create(dev1.class, NULL, dev1.dev_num, NULL, "test");
    if(IS_ERR(dev1.device))
    {
        ret=PTR_ERR(dev1.device);
        goto err_device_create;
    }
    return 0;

    err_device_create:
        class_destroy(dev1.class);                 //删除类
    err_class_create:
        cdev_del(&dev1.cdev_test);                 //删除cdev
    err_chr_add:
        unregister_chrdev_region(dev1.dev_num, 1); //注销设备号
    err_chrdev:
        return ret;
}

static void __exit chr_fops_exit(void) //驱动出口函数
{
    /*注销字符设备*/
    unregister_chrdev_region(dev1.dev_num, 1); //注销设备号
    cdev_del(&dev1.cdev_test);                 //删除cdev
    device_destroy(dev1.class, dev1.dev_num);       //删除设备
    class_destroy(dev1.class);                 //删除类
}
module_init(chr_fops_init);
module_exit(chr_fops_exit);
MODULE_LICENSE("GPL v2");
MODULE_AUTHOR("topeet");

首先在第9 行代码添加<linux/poll.h>头文件。然后在第94 行将poll 字段指向chrdev_poll(...)函数,最后在73 行到84 行编写这个函数。

28.3 运行测试

28.3.1 编译驱动程序

在上一小节中的poll.c 代码同一目录下创建Makefile 文件,Makefile 文件内容,然后使用命令“make”进行驱动的编译,编译完生成poll.ko 目标文件,至此驱动模块就编译成功了,下面进行应用程序read.c 和write.c 的编译。

28.3.2 编译应用程序

来到存放应用程序read.c 和write.c 的文件夹下,使用以下命令对read.c 和write.c 进行交叉编译,编译完成如下图(图28-4)所示:

aarch64-linux-gnu-gcc -o read read.c -static
aarch64-linux-gnu-gcc -o write write.c -static

image-20240816115729497

生成的read write 文件就是之后放在开发板上运行的可执行文件,至此应用程序的编译就完成了。

28.3.3 测试

开发板启动之后,使用以下命令进行驱动模块的加载,如下图(图28-5)所示:

image-20240816115751402

在加载驱动程序之后,会生成如下图(图28-6)所示的设备节点,在应用程序中也是操作这个设备节点。

image-20240816115802890

首先运行read 可执行程序,如下(图28-7)所示,在三秒钟以后打印“time out”。

image-20240816115809589

然后运行write 可执行程序写入数据,如下(图28-8)所示:

image-20240816115816950

接着可以看到read 读取到了数据,如下(图28-9)所示:

image-20240816115829563

第29 章信号驱动IO 实验

本章节要讲解的信号驱动IO 是最后一个IO 模型,在第25 章中我们已经对信号驱动IO 有了基本的认识,本章节将对信号驱动IO 进行深入的学习,最后通过相应的实验,来加深对信号驱动IO 的理解。

29.1 信号驱动IO 简介

信号驱动IO 不需要应用程序查询设备的状态,一旦设备准备就绪,会触发SIGIO 信号,进而调用注册的处理函数。仍旧以钓鱼为例。小马同学喜欢吃新鲜的鱼,但是不想自己钓,所以他请了一个助手来帮他钓鱼,他自己去忙其他的事情(进程不阻塞,立即返回)。如果有鱼上钩助手会帮忙钓上来(将数据拷贝到指定的缓冲区),并立即通知小马同学回来把鱼取走(处理数据)。

如果要实现信号驱动IO,需要应用程序和驱动程序配合,应用程序使用信号驱动IO 的步骤有三步:

  • 步骤1 :注册信号处理函数应用程序使用signal 函数来注册SIGIO 信号的信号处理函数。
  • 步骤2: 设置能够接收这个信号的进程
  • 步骤3: 开启信号驱动IO 通常使用fcntl 函数的F_SETFL 命令打开FASYNC 标志。

fcntl 函数如下所示:

函数原型:
    int fcntl(int fd,int cmd, ...)
函数功能:
    fcntl 函数可以用来操作文件描述符
函数参数:
    fd: 被操作的文件描述符
    cmd: 操作文件描述符的命令,cmd 参数决定了要如何操作文件描述符fd
    ...: 根据cmd 的参数来决定是不是需要使用第三个参数

操作文件描述符的命令如下表(表29-1)所示:

命令名 描述
F_DUPFD 复制文件描述符
F_GETFD 获取文件描述符标志
F_SETFD 设置文件描述符标志
F_GETFL 获取文件状态标志
F_SETFL 设置文件状态标志
F_GETLK 获取文件锁
F_SETLK 设置文件锁
F_SETLKW 类似F_SETLK,但等待返回
F_GETOWN 获取当前接收SIGIO 和SIGURG 信号的进程ID 和进程组ID
F_SETOWN 设置当前接收SIGIO 和SIGURG 信号的进程ID 和进程组ID

接下来学习驱动程序实现fasync 方法

步骤1

当应用程序开启信号驱动IO 时,会触发驱动中的fasync 函数。所以首先在file_operations结构体中实现fasync 函数,函数原型如下:

int (*fasync) (int fd,struct file *filp,int on)

步骤2

在驱动中的fasync 函数调用fasync_helper 函数来操作fasync_struct 结构体,fasync_helper函数原型如下:

int fasync_helper(int fd,struct file *filp,int on,struct fasync_struct **fapp)

步骤3:

当设备准备好的时候,驱动程序需要调用kill_fasync 函数通知应用程序,此时应用程序的SIGIO 信号处理函数就会被执行。kill_fasync 负责发送指定的信号,函数原型如下:

void kill_fasync(struct fasync_struct **fp,int sig,int band)
函数参数:
    fp: 要操作的fasync_struct
    sig: 发送的信号
    band: 可读的时候设置成POLLIN ,可写的时候设置成POLLOUT

29.2 实验程序编写

29.2.1 编写测试APP

本实验对应的应用程序网盘路径为:iTOP-RK3568 开发板【底板V1.7 版本】\03_【iTOP-RK3568 开发板】指南教程\02_Linux 驱动配套资料\04_Linux 驱动例程\23\app
编写应用程序write.c,在此代码中,调用write 函数向/dev/test 设备写入数据“nihao”。编写好的程序如下所示:

#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <stdlib.h>
#include <string.h>

int main(int argc, char *argv[])  
{
    int fd;
    char buf1[32] = {0};   
    char buf2[32] = "nihao";
    fd = open("/dev/test",O_RDWR);  //打开led驱动
    if (fd < 0)
    {
        perror("open error \n");
        return fd;
    }
    printf("write before \n");
    write(fd,buf2,sizeof(buf2));  //向/dev/test文件写入数据
     printf("write after\n");
    close(fd);     //关闭文件
    return 0;
}

然后来编写应用程序read.c,在此代码中要使用信号驱动IO 读取数据。编写好的应用程序如下所示:

#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <stdlib.h>
#include <poll.h>
#include <fcntl.h>
#include <signal.h>

int fd;
char buf1[32] = {0};   

//SIGIO信号的信号处理函数
static void func(int signum)
{
    read(fd,buf1,32);
    printf ("buf is %s\n",buf1);
}

int main(int argc, char *argv[])  
{
    int ret;
    int flags;
    fd = open("/dev/test", O_RDWR);  //打开led驱动
    if (fd < 0){
        perror("open error \n");
        return fd;
    }

    signal(SIGIO,func);  //步骤一:使用signal函数注册SIGIO信号的信号处理函数
    //步骤二:设置能接收这个信号的进程
    //fcntl函数用来操作文件描述符,
    //F_SETOWN 设置当前接收的SIGIO的进程ID
    fcntl(fd,F_SETOWN,getpid()); 

    flags = fcntl(fd,F_GETFD); //获取文件描述符标志
    //步骤三  开启信号驱动IO 使用fcntl函数的F_SETFL命令打开FASYNC标志
    fcntl(fd,F_SETFL,flags| FASYNC);
    while(1);

    close(fd);     //关闭文件
    return 0;
}

29.2.2 驱动程序编写

本实验对应的驱动程序网盘路径为:iTOP-RK3568 开发板【底板V1.7 版本】\03_【iTOP-RK3568 开发板】指南教程\02_Linux 驱动配套资料\04_Linux 驱动例程\23\module
接下来编写驱动程序,在29.1 小节中介绍了驱动程序中实现fasync 方法的三个步骤,按照这个思路,依次实现这三步,编写好的驱动程序如下所示:

#include <linux/init.h>
#include <linux/module.h>
#include <linux/kdev_t.h>
#include <linux/fs.h>
#include <linux/cdev.h>
#include <linux/uaccess.h>
#include <linux/io.h>
#include  <linux/wait.h>
#include <linux/poll.h>
#include <linux/fcntl.h>
#include <linux/signal.h>

struct device_test{
   
    dev_t dev_num;  //设备号
     int major ;  //主设备号
    int minor ;  //次设备号
    struct cdev cdev_test; // cdev
    struct class *class;   //类
    struct device *device; //设备
    char kbuf[32];
    int  flag;  //标志位
    struct fasync_struct *fasync;
};


struct  device_test dev1;  

DECLARE_WAIT_QUEUE_HEAD(read_wq); //定义并初始化等待队列头

/*打开设备函数*/
static int cdev_test_open(struct inode *inode, struct file *file)
{
    file->private_data=&dev1;//设置私有数据
   
    return 0;
}

/*向设备写入数据函数*/
static ssize_t cdev_test_write(struct file *file, const char __user *buf, size_t size, loff_t *off)
{
     struct device_test *test_dev=(struct device_test *)file->private_data;

    if (copy_from_user(test_dev->kbuf, buf, size) != 0) // copy_from_user:用户空间向内核空间传数据
    {
        printk("copy_from_user error\r\n");
        return -1;
    }
    test_dev->flag=1;
    wake_up_interruptible(&read_wq);
    
    kill_fasync(&test_dev->fasync,SIGIO,POLLIN);
    return 0;
}

/**从设备读取数据*/
static ssize_t cdev_test_read(struct file *file, char __user *buf, size_t size, loff_t *off)
{
    struct device_test *test_dev=(struct device_test *)file->private_data;
    if(file->f_flags & O_NONBLOCK ){
        if (test_dev->flag !=1)
        return -EAGAIN;
    }
    wait_event_interruptible(read_wq,test_dev->flag);

    if (copy_to_user(buf, test_dev->kbuf, strlen( test_dev->kbuf)) != 0) // copy_to_user:内核空间向用户空间传数据
    {
        printk("copy_to_user error\r\n");
        return -1;
    }

    return 0;
}

static int cdev_test_release(struct inode *inode, struct file *file)
{
    return 0;
}

static  __poll_t  cdev_test_poll(struct file *file, struct poll_table_struct *p){
     struct device_test *test_dev=(struct device_test *)file->private_data;  //设置私有数据
     __poll_t mask=0;    
     poll_wait(file,&read_wq,p);     //应用阻塞

     if (test_dev->flag == 1)    
     {
         mask |= POLLIN;
     }
     return mask;
     
}

static int cdev_test_fasync (int fd, struct file *file, int on)
{
     struct device_test *test_dev=(struct device_test *)file->private_data;  //设置私有数据
    return  fasync_helper(fd,file,on,&test_dev->fasync);
}
/*设备操作函数*/
struct file_operations cdev_test_fops = {
    .owner = THIS_MODULE, //将owner字段指向本模块,可以避免在模块的操作正在被使用时卸载该模块
    .open = cdev_test_open, //将open字段指向chrdev_open(...)函数
    .read = cdev_test_read, //将open字段指向chrdev_read(...)函数
    .write = cdev_test_write, //将open字段指向chrdev_write(...)函数
    .release = cdev_test_release, //将open字段指向chrdev_release(...)函数
    .poll = cdev_test_poll,  //将poll字段指向chrdev_poll(...)函数
    .fasync = cdev_test_fasync,   //将fasync字段指向cdev_test_fasync(...)函数
};

static int __init chr_fops_init(void) //驱动入口函数
{
    /*注册字符设备驱动*/
    int ret;
    /*1 创建设备号*/
    ret = alloc_chrdev_region(&dev1.dev_num, 0, 1, "alloc_name"); //动态分配设备号
    if (ret < 0)
    {
       goto err_chrdev;
    }
    printk("alloc_chrdev_region is ok\n");

    dev1.major = MAJOR(dev1.dev_num); //获取主设备号
   dev1.minor = MINOR(dev1.dev_num); //获取次设备号

    printk("major is %d \r\n", dev1.major); //打印主设备号
    printk("minor is %d \r\n", dev1.minor); //打印次设备号
     /*2 初始化cdev*/
    dev1.cdev_test.owner = THIS_MODULE;
    cdev_init(&dev1.cdev_test, &cdev_test_fops);

    /*3 添加一个cdev,完成字符设备注册到内核*/
   ret =  cdev_add(&dev1.cdev_test, dev1.dev_num, 1);
    if(ret<0)
    {
        goto  err_chr_add;
    }
    /*4 创建类*/
  dev1. class = class_create(THIS_MODULE, "test");
    if(IS_ERR(dev1.class))
    {
        ret=PTR_ERR(dev1.class);
        goto err_class_create;
    }
    /*5  创建设备*/
  dev1.device = device_create(dev1.class, NULL, dev1.dev_num, NULL, "test");
    if(IS_ERR(dev1.device))
    {
        ret=PTR_ERR(dev1.device);
        goto err_device_create;
    }

    return 0;

    err_device_create:
        class_destroy(dev1.class);                 //删除类
    err_class_create:
        cdev_del(&dev1.cdev_test);                 //删除cdev
    err_chr_add:
        unregister_chrdev_region(dev1.dev_num, 1); //注销设备号
    err_chrdev:
        return ret;
}


static void __exit chr_fops_exit(void) //驱动出口函数
{
    /*注销字符设备*/
    unregister_chrdev_region(dev1.dev_num, 1); //注销设备号
    cdev_del(&dev1.cdev_test);                 //删除cdev
    device_destroy(dev1.class, dev1.dev_num);       //删除设备
    class_destroy(dev1.class);                 //删除类
}
module_init(chr_fops_init);
module_exit(chr_fops_exit);
MODULE_LICENSE("GPL v2");
MODULE_AUTHOR("topeet");

29.3 运行测试

29.3.1 编译程序

在上一小节中的fasync.c 代码同一目录下创建Makefile 文件,Makefile 文件内容:然后使用命令“make”进行驱动的编译,编译完生成fasync.ko 目标文件,如下图(图29-4)所示:至此驱动模块就编译成功了,下面进行交叉编译应用程序。

29.3.2 编译应用程序

来到存放应用程序read.c 和write.c 的文件夹下,使用以下命令对read.c 和write.c 进行交叉编译,编译完成如下图(图29-5)所示:

aarch64-linux-gnu-gcc -o read read.c -static
aarch64-linux-gnu-gcc -o write write.c -static

image-20240816142309220

生成的read write 文件就是之后放在开发板上运行的可执行文件,至此应用程序的编译就完成了。

29.3.3 测试

输入以下命令加载驱动程序。

insmod fasync.ko

image-20240816142357316

输入以下命令运行read 应用程序,如下图(图29-7)所示:

image-20240816142406896

然后输入以下命令运行write 应用程序,如下图(图29-8)所示:

image-20240816142416924

如下图(图29-9)所示,read 程序窗口打印读取的数据。

image-20240816142511366

第30 章定时器实验

在Linux 内核中很多函数是基于定时器进行驱动的,所以时间管理在内核中占有非常重要的地位。本小节将对Linux 中的时间管理相关知识进行学习.

30.1 Linux 定时器

硬件为内核提供了一个系统定时器来计算流逝的时间(即基于未来时间点的计时方式,以当前时刻为计时开始的起点,以未来的某一时刻为计时的终点),内核只有在系统定时器的帮助下才能计算和管理时间,但是内核定时器的精度并不高,所以不能作为高精度定时器使用。并且内核定时器的运行没有周期性,到达计时终点后会自动关闭。如果要实现周期性定时,就要在定时处理函数中重新开启定时器

Linux 内核中使用timer_list 结构体表示内核定时器,该结构体定义在“内核源码/include/linux/timer.h”文件中,具体内容如下所示:

struct timer_list {
    struct hlist_node entry;
    unsigned long expires;					/* 定时器超时时间,单位是节拍数*/
    void (*function)(struct timer_list *);	 /* 定时处理函数*/
    u32 flags;
    
#ifdef CONFIG_LOCKDEP
    struct lockdep_map lockdep_map;
#endif
    ANDROID_KABI_RESERVE(1);
    ANDROID_KABI_RESERVE(2);
};

使用以下宏对timer_list 结构体进行定义,_name 为定义的结构体名称,_function 为定时处理函数,该宏同样定义在文件“内核源码/include/linux/timer.h”文件中,如下所示:

#define DEFINE_TIMER(_name, _function) \
struct timer_list _name = \
__TIMER_INITIALIZER(_function, 0)

例如可以使用以下代码对定时器和相应的定时处理函数进行定义

DEFINE_TIMER(timer_test,function_test);//定义一个定时器

定时器定义完成之后还需要通过一系列的API 函数来初始化此定时器,部分函数说明如下(表30-1)所示:

函数 作用
void add_timer(struct timer_list *timer) 向Linux 内核注册定时器,使用add_timer 函数
向内核注册定时器以后,定时器就会开始运行
int del_timer(struct timer_list * timer) 删除一个定时器
int mod_timer(struct timer_list *timer,unsigned long expires) 修改定时值, 如果定时器还没有激活的话,
mod_timer 函数会激活定时器

在使用add_timer()函数向Linux 内核注册定时器之前,还需要设置定时时间,定时时间由timer_list 结构体中的expires 参数所确定,单位为节拍数,可以通过图形化界面设置系统节拍的频率,具体路径如下图(图30-2)所示:

-> Kernel Features
    -> Timer frequency (<choice> [=y])

image-20240816155948715

从上图可以看出可选的系统节拍率为100Hz、250Hz、300Hz 和1000Hz,默认情况下选择300Hz。
通过全局变量jiffies 来记录自系统启动以来产生节拍的总数。启动时,内核将该变量初始化为0,此后,每次时钟中断处理程序都会增加该变量的值,一秒内jiffes 增加的值为设置的系统节拍数,该变量定义在”内核源码/include/linux/jiffies.h”文件中(timer.h 文件中已经包含,不需要重复引用),具体定义如下:

extern u64 __cacheline_aligned_in_smp jiffies_64;
extern unsigned long volatile __cacheline_aligned_in_smp __jiffy_arch_data jiffies;

其中jiffies_64 用于64 位系统,而jiffies 用于32 位系统。为了方便开发,Linux 内核还提供了几个jiffies 和ms、us、ns 之间的转换函数,如下(表30-2)所示:

函数 作用
int jiffies_to_msecs(const unsigned long j) 将jiffies 类型的参数j 分别转换为对应的毫秒
int jiffies_to_usecs(const unsigned long j) 将jiffies 类型的参数j 分别转换为对应的微秒
u64 jiffies_to_nsecs(const unsigned long j) 将jiffies 类型的参数j 分别转换为对应的纳秒
long msecs_to_jiffies(const unsigned int m) 将毫秒转换为jiffies 类型
long usecs_to_jiffies(const unsigned int u) 将微秒转换为jiffies 类型
unsigned long nsecs_to_jiffies(u64 n) 将纳秒转换为jiffies 类型

例如可以使用以下命令进行3 秒钟的定时:

timer_test.expires = jiffies_64 +msecs_to_jiffies(3000)

至此关于Linux 定时器相关的知识就讲解完成了,在下个小节中将进行相应的实验。

30.2 实验程序编写

本实验对应的网盘路径为:iTOP-RK3568 开发板【底板V1.7 版本】\03_【iTOP-RK3568开发板】指南教程\02_Linux 驱动配套资料\04_Linux 驱动例程\24\module

本实验将实现五秒钟的计时,五秒钟之后将打印“this is function test”相关字符,为了实现循环打印还需要在定时处理函数中使用mod_timer 函数重新设置定时时间。
编写好的驱动程序timer_mod.c 如下所示:

#include <linux/init.h>
#include <linux/module.h>
#include <linux/timer.h>

static void function_test(struct timer_list *t);//定义function_test定时功能函数
DEFINE_TIMER(timer_test,function_test);//定义一个定时器

static void function_test(struct timer_list *t)
{
	printk("this is function test \n");
	mod_timer(&timer_test,jiffies_64 + msecs_to_jiffies(5000));//使用mod_timer函数将定时时间设置为五秒后
}	

static int __init timer_mod_init(void) //驱动入口函数
{
	timer_test.expires = jiffies_64 + msecs_to_jiffies(5000);//将定时时间设置为五秒后
	add_timer(&timer_test);//添加一个定时器
	return 0;
}

static void __exit timer_mod_exit(void) //驱动出口函数
{
	del_timer(&timer_test);//删除一个定时器
	printk("module exit \n");
}

module_init(timer_mod_init);
module_exit(timer_mod_exit);
MODULE_LICENSE("GPL v2");
MODULE_AUTHOR("topeet");

30.3 运行测试

30.3.1 编译驱动程序

在上一小节中的timer_mod.c 代码同一目录下创建Makefile 文件,Makefile 文件内容:然后使用命令“make”进行驱动的编译,编译完生成timer_mod.ko 目标文件,至此驱动模块就编译成功了,下面交叉编译应用程序。

30.3.2 运行测试

开发板启动之后,使用以下命令进行驱动模块的加载,如下图(图30-7)所示:

insmod timer_mod.ko

image-20240819092438500

可以看到驱动加载之后,每隔五秒钟会打印“this is function test”相关打印,证明编写的驱动程序没有问题,最后使用以下命令卸载相应的驱动,如下图(图30-8)所示:

rmmod timer_mod.ko

image-20240819092507108

第31 章秒字符设备驱动实验

本章节将实现秒字符设备驱动,以此对之前学习到的知识进行巩固。本章节实验要实现的任务如下:

  1. 实现字符设备驱动框架,自动生成设备节点。
  2. 根据上一小节学到的知识,实现秒计时。
  3. 通过原子变量来记录递增的秒数,避免竞争的发生。
  4. 通过用户空间和内核空间的数据交换,将记录的秒数传递到应用空间,并通过应用程序打印出来。

31.1 实验程序编写

31.1.1 编写测试APP

本实验对应的应用程序网盘路径为:iTOP-RK3568 开发板【底板V1.7 版本】\03_【iTOP-RK3568 开发板】指南教程\02_Linux 驱动配套资料\04_Linux 驱动例程\25\app
首先来编写应用测试代码timer.c,在此代码中每隔一秒钟打印从用户空间传递来的秒数,具体代码内容如下所示:

#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>

int main(int argc,char *argv[]){
	int fd;//定义int类型的文件描述符fd
	int count;//定义int类型记录秒数的变量count
	fd = open("/dev/test",O_RDWR);//使用open()函数以可读可写的方式打开设备文件
	while(1)
	{
		read(fd,&count,sizeof(count));//使用read函数读取内核传递来的秒数
		printf("num is %d\n",count);
		sleep(1);
	}
	return 0;
}

31.1.2 驱动程序编写

本实验对应的网盘路径为:iTOP-RK3568 开发板【底板V1.7 版本】\03_【iTOP-RK3568开发板】指南教程\02_Linux 驱动配套资料\04_Linux 驱动例程\25\module

编写好的驱动程序timer_dev.c 如下所示:

#include <linux/module.h>
#include <linux/init.h>
#include <linux/fs.h>
#include <linux/cdev.h>
#include <linux/kdev_t.h>
#include <linux/uaccess.h>
#include <linux/atomic.h>

struct device_test{
    dev_t dev_num;  //设备号
    int major ;  //主设备号
    int minor ;  //次设备号
    struct cdev cdev_test; // cdev
    struct class *class;   //类
    struct device *device; //设备
	int sec; //秒
};
atomic64_t v = ATOMIC_INIT(0);//定义原子类型变量v,并定义为0
static struct device_test dev1;
static void function_test(struct timer_list *t);//定义function_test定时功能函数
DEFINE_TIMER(timer_test,function_test);//定义一个定时器

static void function_test(struct timer_list *t)
{
	atomic64_inc(&v);//原子变量v自增
	dev1.sec = atomic_read(&v);//将读取到的原子变量v,赋值给sec
	//printk("the sec is %d\n",dev1.sec);
	mod_timer(&timer_test,jiffies_64 + msecs_to_jiffies(1000));//使用mod_timer函数将定时时间设置为一秒后
}
static int cdev_test_open(struct inode *inode, struct file *file)
{
    file->private_data=&dev1;//设置私有数据
	add_timer(&timer_test);	//添加一个定时器
    return 0;
}

/**从设备读取数据*/
static ssize_t cdev_test_read(struct file *file, char __user *buf, size_t size, loff_t *off)
{
	if(copy_to_user(buf,&dev1.sec,sizeof(dev1.sec))){//使用copy_to_user函数将sec传递到应用层
		printk("copy_to_user error \n");
		return -1;
	}
    return 0;
}

static int cdev_test_release(struct inode *inode, struct file *file)
{
	del_timer(&timer_test);//删除一个定时器
    return 0;
}

/*设备操作函数*/
struct file_operations cdev_test_fops = {
    .owner = THIS_MODULE, //将owner字段指向本模块,可以避免在模块的操作正在被使用时卸载该模块
    .open = cdev_test_open, //将open字段指向chrdev_open(...)函数
    .read = cdev_test_read, //将open字段指向chrdev_read(...)函数
    .release = cdev_test_release, //将open字段指向chrdev_release(...)函数
};
static int __init timer_dev_init(void) //驱动入口函数
{
    /*注册字符设备驱动*/
    int ret;
    /*1 创建设备号*/
    ret = alloc_chrdev_region(&dev1.dev_num, 0, 1, "alloc_name"); //动态分配设备号
    if (ret < 0)
    {
       goto err_chrdev;
    }
    printk("alloc_chrdev_region is ok\n");

    dev1.major = MAJOR(dev1.dev_num); //获取主设备号
    dev1.minor = MINOR(dev1.dev_num); //获取次设备号

    printk("major is %d \r\n", dev1.major); //打印主设备号
    printk("minor is %d \r\n", dev1.minor); //打印次设备号
     /*2 初始化cdev*/
    dev1.cdev_test.owner = THIS_MODULE;
    cdev_init(&dev1.cdev_test, &cdev_test_fops);

    /*3 添加一个cdev,完成字符设备注册到内核*/
   ret =  cdev_add(&dev1.cdev_test, dev1.dev_num, 1);
    if(ret<0)
    {
        goto  err_chr_add;
    }
    /*4 创建类*/
  dev1. class = class_create(THIS_MODULE, "test");
    if(IS_ERR(dev1.class))
    {
        ret=PTR_ERR(dev1.class);
        goto err_class_create;
    }
    /*5  创建设备*/
  	dev1.device = device_create(dev1.class, NULL, dev1.dev_num, NULL, "test");
    if(IS_ERR(dev1.device))
    {
        ret=PTR_ERR(dev1.device);
        goto err_device_create;
    }

return 0;

err_device_create:
        class_destroy(dev1.class);                 //删除类

err_class_create:
       cdev_del(&dev1.cdev_test);                 //删除cdev

err_chr_add:
        unregister_chrdev_region(dev1.dev_num, 1); //注销设备号

err_chrdev:
        return ret;
}

static void __exit timer_dev_exit(void) //驱动出口函数
{
    /*注销字符设备*/
    unregister_chrdev_region(dev1.dev_num, 1); //注销设备号
    cdev_del(&dev1.cdev_test);                 //删除cdev
    device_destroy(dev1.class, dev1.dev_num);       //删除设备
    class_destroy(dev1.class);                 //删除类
}
module_init(timer_dev_init);
module_exit(timer_dev_exit);
MODULE_LICENSE("GPL v2");
MODULE_AUTHOR("topeet");

31.2 运行测试

31.2.1 编译驱动程序

在上一小节中的timer_dev.c 代码同一目录下创建Makefile 文件,Makefile 文件内容如下,然后使用命令“make”进行驱动的编译,编译完生成timer_dev.ko 目标文件,至此驱动模块就编译成功了,下面交叉编译应用程序。

31.2.2 编译应用程序

然后来到存放应用程序timer.c 的文件夹下,使用以下命令对timer.c 进行交叉编译,编译完成如下图(图31-4)所示:

aarch64-linux-gnu-gcc -o timer timer.c

生成的timer 文件就是之后放在开发板上运行的可执行文件,至此应用程序的编译就完成了。

31.2.3 运行测试

开发板启动之后,使用以下命令进行驱动模块的加载,如下图(图31-5)所示:

insmod timer_dev.ko

image-20240819093849295

然后输入以下命令进行可执行程序的运行,如下图(图31-6)所示:

./timer

image-20240819094006637

可以看到每隔一秒钟就会打印由内核空间传递来的秒数,我们要实现的任务就完成了,最后使用以下命令卸载对应的驱动,如下图(图31-7)所示:

rmmod timer_dev

image-20240819094031532

第32 章Linux 内核打印实验

本手册的实验都是在buildroot 系统上完成的,由于buildroot 系统已经设置了相应的打印等级,所以驱动的相关打印都能正常显示在串口终端上,如果将实验系统换成了ubuntu,然后加载同样的驱动,会发现打印信息不见了,这一现象的基本原因就是内核打印等级不同,那打印等级是如何修改的呢,查看打印等级的方式又有哪些呢,就让我们进入本章节的学习吧!

32.1 方法一:dmseg 命令

在终端使用dmseg 命令可以获取内核打印信息,该命令的具体使用方法如下所示:

dmesg 命令
英文全称:	display message(显示信息)
作用:		 kernel 会将打印信息存储在ring buffer 中。可以利用dmesg 命令来查看内核打印信息。。
常用参数:
            -C,--clear 清除内核环形缓冲区
            -c,—-read-clear 读取并清除所有消息
            -T,--显示时间戳
提示:dmesg 命令也可以与grep 命令组合使用。如查找待用usb 关键字的打印信息,就可以使用如下命令:dmseg | grep usb

首先在串口终端使用“dmseg”命令,可以看见相应的内核打印信息已经加载了出来,如下图(图32-1)所示:

image-20240819094313549

然后使用以下组合命令查找nfs 相关的打印信息,如下图(图32-2)所示:

dmesg | grep nfs

image-20240819094335910

至此关于dmesg 命令就讲解演示完成了。

32.2 方法二:查看kmsg 文件

内核所有的打印信息都会输出到循环缓冲区’log_buf’,为了能够方便的在用户空间读取内核打印信息,Linux 内核驱动将该循环缓冲区映射到了/proc 目录下的文件节点kmsg。通过cat 或者其他应用程序读取Log Buffer 的时候可以不断的等待新的log,所以访问/proc/kmsg的方式适合长时间的读取log,一旦有新的log 就可以被打印出来
首先使用以下命令读取kmsg 文件,在没有新的内核打印信息时会阻塞,如下图(图32-3)所示:

cat /proc/kmsg

image-20240819094729896

然后在该设备的其他终端加载任意有打印信息的驱动文件(这里使用的是ssh),如下图(图32-4)所示:

image-20240819094743937

在串口终端中可以看到对应驱动的打印信息就被打印了出来,如下图(图32-5)所示:

image-20240819094758680

32.3 方法三:调整内核打印等级

内核的日志打印由相应的打印等级来控制,可以通过调整内核打印等级来控制打印日志的输出。使用以下命令查看当前默认打印等级,如下图(图32-6)所示:

cat /proc/sys/kernel/printk

image-20240819094857735

可以看到内核打印等级由四个数字所决定,“7 4 1 7” 分别对应console_logleveldefault_message_loglevelminimum_c onsole_logleveldefault_console_loglevel,具体类型说明如下表(表32-7)所示:

终端打印类型 对应类型说明
console_loglevel 只有当printk 打印消息的log 优先级高于console_loglevel 时,才能输出到终端上
default_message_loglevel printk 打印消息时默认的log 等级
minimum_console_loglevel console_loglevel 可以被设置的最小值
default_console_loglevel console_loglevel 的缺省值

上面的“7 4 1 7”意味着只有优先级高于KERN_DEBUG(7)的打印消息才能输出到终端,在“内核源码/include/linux/kern_levels.h”文件中对于文件打印等级进行了如下打印等级定义:

#define KERN_EMERG KERN_SOH "0" 		/* system is unusable */
#define KERN_ALERT KERN_SOH "1" 		/* action must be taken immediately */
#define KERN_CRIT KERN_SOH "2" 			/* critical conditions */
#define KERN_ERR KERN_SOH "3" 			/* error conditions */
#define KERN_WARNING KERN_SOH "4" 		/* warning conditions */
#define KERN_NOTICE KERN_SOH "5" 		/* normal but significant condition */
#define KERN_INFO KERN_SOH "6" 			/* informational */
#define KERN_DEBUG KERN_SOH "7" 		/* debug-level messages */

printk 在打印信息前,可以加入相应的打印等级宏定义,具体格式如下所示:printk(打印等级"打印信息")
接下来将使用以下驱动例程进行实际的打印等级测试:

#include <linux/module.h>
#include <linux/kernel.h>

static int __init helloworld_init(void)
{
    printk(KERN_EMERG " 0000 KERN_EMERG\n");
    printk(KERN_ALERT " 1111 KERN_ALERT\n");
    printk(KERN_CRIT " 2222 KERN_CRIT\n");
    printk(KERN_ERR " 3333 KERN_ERR\n");
    printk(KERN_WARNING " 4444 KERN_WARNING\n");
    printk(KERN_NOTICE " 5555 KERN_NOTICE\n");
    printk(KERN_INFO " 6666 KERN_INFO\n");
    printk(KERN_DEBUG " 7777 KERN_DEBUG\n");
    printk(" 8888 no_fix\n");
    return 0;
}

static void __exit helloworld_exit(void)
{
    printk(KERN_EMERG "helloworld_exit\r\n");
}

module_init(helloworld_init);
module_exit(helloworld_exit);
MODULE_LICENSE("GPL v2");
MODULE_AUTHOR("topeet");

加载该驱动之后,第5-11 行0-6 等级的打印信息就被打印了出来,第13 行由于没有设置打印等级,所以会被赋予默认打印等级4,高于console_loglevel 打印等级,所以也会被打印出来,最后只有第12 行打印等级为7 的信息,和console_loglevel 打印等级相同,所以不会被打印出来,如下图(图32-8)所示:

image-20240819095429318

然后使用以下命令将console_loglevel 打印等级设置为4,如下图(图32-9)所示:

echo 4 4 1 7 > /proc/sys/kernel/printk

image-20240819095459052

卸载驱动之后,再一次加载驱动,发现只有打印等级高于4 的相关信息被打印了出来,如下图(图32-10)所示:

image-20240819095509064

至此关于内核打印等级的实验就结束了。

第33 章llseek 定位设备驱动实验

相信经过了前面章节的学习,大家已经对内核空间与用户空间的数据交互很是熟悉,但在之前的例子中都是对字符串的全部内容进行读写,假如现在有这样一个场景,将两个字符串依次进行写入,并对写入完成的字符串进行读取,如果仍采用之前的方式,第二次的写入值会覆盖第一次写入值,那要如何来实现上述功能呢?这就要轮到llseek 出场了。

33.1 定位设备llseek

33.1.1 lseek 函数的使用

在应用程序中使用lseek 函数进行读写位置的调整,该函数的具体使用说明如下所示:lseek 函数

函数原型:
    off_t lseek(int fd, off_t offset, int whence);
头文件:
    #include <sys/types.h>
    #include <unistd.h>
函数作用:
    移动文件的读写位置。
参数含义:
    fd: 文件描述符;
    off_t offset: 偏移量,单位是字节的数量,可以正负,如果是负值表示向前移动;如果是正值,表示向后移动。
    whence:当前位置的基点,可以使用以下三组值。
    SEEK_SET:相对于文件开头
    SEEK_CUR:相对于当前的文件读写指针位置
    SEEK_END:相对于文件末尾
函数返回值:
    成功返回当前位移大小,失败返回-1

函数使用示例:
把文件位置指针设置为5:

lseek(fd,5,SEEK_SET);

把文件位置设置成文件末尾:

lseek(fd,0,SEEK_END);

确定当前的文件位置:

lseek(fd,0,SEEK_CUR);

33.1.2 驱动程序的完善

上一小节中讲解的lseek 函数如果要对设备文件生效,还需要完善相应的驱动程序。lseek函数会调用file_operation 结构体中的llseek 接口,所以需要对驱动中的llseek 函数进行填充,并且完善read 和write 函数中偏移相关的部分。
下面对相关API 接口函数进行填充:

llseek 函数完善
llseek 填充完成的函数如下所示:

static loff_t cdev_test_llseek(struct file *file, loff_t offset, int whence)
{
    loff_t new_offset;//定义loff_t 类型的新的偏移值
    switch(whence)//对lseek 函数传递的whence 参数进行判断
    {
        case SEEK_SET:
            if(offset < 0){
                return -EINVAL;
                break;
            }
            if(offset > BUFSIZE){
                return -EINVAL;
                break;
            }
            new_offset = offset;//如果whence 参数为SEEK_SET,则新偏移值为offset
        break;
        case SEEK_CUR:
            if(file->f_pos + offset > BUFSIZE){
                return -EINVAL;
                break;
            }
            if(file->f_pos + offset < 0){
                return -EINVAL;
                break;
            }
            new_offset = file->f_pos + offset;//如果whence 参数为SEEK_CUR,则新偏移值为file->f_pos +
            offset,file->f_pos 为当前的偏移值
        break;
        case SEEK_END:
            if(file->f_pos + offset < 0){
                return -EINVAL;
                break;
            }
            new_offset = BUFSIZE + offset;//如果whence 参数为SEEK_END,则新偏移值为BUFSIZE + offset,
            BUFSIZE 为最大偏移量
            break;
            default:
        break;
    }
    file->f_pos = new_offset;//更新file->f_pos 偏移值
    return new_offset;
}

在第4 行使用switch 语句对传递的whence 参数进行判断,whence 在这里可以有三个取值,分别为SEEK_SETSEEK_CURSEEK_END
在6-16、17-28、29-38 行代码中,分别对三个参数所代表的功能进行实现,其中需要注意的是file->f_pos 指的是当前文件的偏移值。
在第40 行和41 行分别对f_pos 偏移值进行更新,对新的偏移值进行返回。

read 接口函数完善
填充完成的read 接口函数如下所示:

static ssize_t cdev_test_read(struct file *file, char __user *buf, size_t size, loff_t *off)
{
    loff_t p = *off;//将读取数据的偏移量赋值给loff_t 类型变量p
    int i;
    size_t count = size;
    if(p > BUFSIZE){//如果当前偏移值比最大偏移量大则返回错误
        return -1;
    }
    if(count > BUFSIZE - p){
        count = BUFSIZE - p;//如果要读取的偏移值超出剩余的空间,则读取到最后位置
    }
    if(copy_to_user(buf,mem+p,count)){//将mem 中的值写入buf,并传递到用户空间
        printk("copy_to_user error \n");
        return -1;
    }
    for(i=0;i<20;i++){
        printk("buf[%d] is %c\n",i,mem[i]);//将mem 中的值打印出来
    }
    printk("mem is %s,p is %llu,count is %d\n",mem+p,p,count);
    *off = *off + count;//更新偏移值
    return count;
}

相较于之前的read 接口函数,在第7 行和第10 行分别加入了对偏移值p 和读取数量进行判定,在第13 行通过偏移值p 进行内核空间和用户空间数据的传递,最后在第21 行对偏移值进行更新。

write 接口函数完善
write 接口函数的完善和read 接口函数相似,填充完成的write 接口函数如下所示:

static ssize_t cdev_test_write(struct file *file, const char __user *buf, size_t size, loff_t *off)
{
    loff_t p = *off;//将写入数据的偏移量赋值给loff_t 类型变量p
    size_t count = size;
    if(p > BUFSIZE){//如果当前偏移值比最大偏移量大则返回错误
        return 0;
    }
    if(count > BUFSIZE - p){
        count = BUFSIZE - p;//如果要写入的偏移值超出剩余的空间,则写入到最后位置
    }
    if(copy_from_user(mem+p,buf,count)){//将buf 中的值,从用户空间传递到内核空间
        printk("copy_to_user error \n");
        return -1;
    }
    printk("mem is %s,p is %llu\n",mem+p,p);//打印写入的值
    *off = *off + count;//更新偏移值
    return count;
}

相较于之前的write 接口函数,在第7 行和第10 行分别加入了对偏移值p 和读取数量进行判定,在第13 行通过偏移值p 进行内核空间和用户空间数据的传递,最后在第18 行对偏移值进行更新。

至此,关于定位设备相关的API 接口函数就都填充完成了,将在下一小节进行定位设备驱动实验代码的编写。

33.2 实验程序编写

33.2.1 编写测试APP

本实验对应的应用程序网盘路径为:iTOP-RK3568 开发板【底板V1.7 版本】\03_【iTOP-RK3568 开发板】指南教程\02_Linux 驱动配套资料\04_Linux 驱动例程\26\app
首先来编写应用测试代码llseek.c,编写好的代码如下所示:

#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>

int main(int argc,char *argv[]){
	int fd;//定义int类型文件描述符
	unsigned int off;//定义读写偏移位置
	char readbuf[13] = {0};//定义读取缓冲区readbuf
	char readbuf1[19] = {0};//定义读取缓冲区readbuf1

	fd = open("/dev/test",O_RDWR,666);//打开/dev/test设备
	if(fd < 0 ){
		printf("file open error \n");
	}
	write(fd,"hello world",13);//向fd写入数据hello world
	off = lseek(fd,0,SEEK_CUR);//读取当前位置的偏移量
	printf("off is %d\n",off);

    off = lseek(fd,0,SEEK_SET);//将偏移量设置为0
    printf("off is %d\n",off);

	read(fd,readbuf,sizeof(readbuf));//将写入的数据读取到readbuf缓冲区
	printf("read is %s\n",readbuf);

    off = lseek(fd,0,SEEK_CUR);//读取当前位置的偏移量
    printf("off is %d\n",off);

	off = lseek(fd,-1,SEEK_CUR);//将当前位置的偏移量向前挪动一位
	printf("off is %d\n",off);

    write(fd,"Linux",6);//向fd写入数据Linux
    off = lseek(fd,0,SEEK_CUR);//读取当前位置的偏移量
    printf("off is %d\n",off);

    off = lseek(fd,0,SEEK_SET);//将偏移量设置为0
    printf("off is %d\n",off);

    read(fd,readbuf1,sizeof(readbuf1));//将写入的数据读取到readbuf1缓冲区
    printf("read is %s\n",readbuf1);

    off = lseek(fd,0,SEEK_CUR);//读取当前位置的偏移量
    printf("off is %d\n",off);
	close(fd);
	return 0;
}

33.2.2 驱动程序编写

本实验对应的网盘路径为:iTOP-RK3568 开发板【底板V1.7 版本】\03_【iTOP-RK3568开发板】指南教程\02_Linux 驱动配套资料\04_Linux 驱动例程\26\module
编写好的驱动程序llseek.c 如下所示:

#include <linux/module.h>
#include <linux/init.h>
#include <linux/fs.h>
#include <linux/cdev.h>
#include <linux/kdev_t.h>
#include <linux/uaccess.h>
#include <linux/atomic.h>
#define BUFSIZE 1024//设置最大偏移量为1024
static char mem[BUFSIZE] = {0};//设置数据存储数组mem
struct device_test{
    dev_t dev_num;  //设备号
    int major ;  //主设备号
    int minor ;  //次设备号
    struct cdev cdev_test; // cdev
    struct class *class;   //类
    struct device *device; //设备
    char kbuf[32];
};
static struct device_test dev1;
static int cdev_test_open(struct inode *inode, struct file *file)
{
    file->private_data=&dev1;//设置私有数据

    return 0;
}


/*从设备读取数据*/
static ssize_t cdev_test_read(struct file *file, char __user *buf, size_t size, loff_t *off)
{
	loff_t p = *off;//将读取数据的偏移量赋值给loff_t类型变量p
	int i;
	size_t count = size;
	if(p > BUFSIZE){
		return 0; 
	}
	if(count > BUFSIZE - p){
		count  = BUFSIZE - p;
	}
	if(copy_to_user(buf,mem+p,count)){//将mem中的值写入buf,并传递到用户空间
		printk("copy_to_user error \n");
		return -1;
	}
	for(i=0;i<20;i++){
		printk("buf[%d] is %c\n",i,mem[i]);//将mem中的值打印出来
	}
	printk("mem is %s,p is %llu,count is %ld\n",mem+p,p,count);
	*off = *off + count;//更新偏移值
    return count;
}
/*向设备写入数据函数*/
static ssize_t cdev_test_write(struct file *file, const char __user *buf, size_t size, loff_t *off)
{

    loff_t p = *off;//将读取数据的偏移量赋值给loff_t类型变量p
    size_t count = size;
    if(p > BUFSIZE){
        return 0;
    }
    if(count > BUFSIZE - p){
        count  = BUFSIZE - p;
    }
	if(copy_from_user(mem+p,buf,count)){//将buf中的值,从用户空间传递到内核空间
 		printk("copy_to_user error \n");
        return -1;
    }
	printk("mem is %s,p is %llu\n",mem+p,p);//打印写入的值
	*off = *off + count;//更新偏移值
    return count;
}

static int cdev_test_release(struct inode *inode, struct file *file)
{

    return 0;
}
static loff_t cdev_test_llseek(struct file *file, loff_t offset, int whence)
{
	loff_t new_offset;//定义loff_t类型的新的偏移值
	switch(whence)//对lseek函数传递的whence参数进行判断
	{
		case SEEK_SET:
			if(offset < 0){
				return -EINVAL;
				break;
			}
			if(offset > BUFSIZE){
                return -EINVAL;
                break;	
			}
			new_offset = offset;//如果whence参数为SEEK_SET,则新偏移值为offset
			break;
		case SEEK_CUR:
            if(file->f_pos + offset > BUFSIZE){
                return -EINVAL;
                break;
            }
            if(file->f_pos + offset < 0){
                return -EINVAL;
                break;
            }
            new_offset = file->f_pos + offset;//如果whence参数为SEEK_CUR,则新偏移值为file->f_pos + offset,file->f_pos为当前的偏移值
			break;			
		case SEEK_END:
            if(file->f_pos + offset < 0){
                return -EINVAL;
                break;
            }
            new_offset = BUFSIZE + offset;//如果whence参数为SEEK_END,则新偏移值为BUFSIZE + offset,BUFSIZE为最大偏移量
			break;
		default:
			break;
	}
	file->f_pos = new_offset;//更新file->f_pos偏移值
	return new_offset;
}
/*设备操作函数*/
struct file_operations cdev_test_fops = {
    .owner = THIS_MODULE, //将owner字段指向本模块,可以避免在模块的操作正在被使用时卸载该模块
    .open = cdev_test_open, //将open字段指向chrdev_open(...)函数
    .read = cdev_test_read, //将open字段指向chrdev_read(...)函数
    .write = cdev_test_write, //将open字段指向chrdev_write(...)函数
    .release = cdev_test_release, //将open字段指向chrdev_release(...)函数
	.llseek = cdev_test_llseek,
};

static int __init timer_dev_init(void) //驱动入口函数
{
    /*注册字符设备驱动*/
    int ret;
    /*1 创建设备号*/
    ret = alloc_chrdev_region(&dev1.dev_num, 0, 1, "alloc_name"); //动态分配设备号
    if (ret < 0)
    {
       goto err_chrdev;
    }
    printk("alloc_chrdev_region is ok\n");

    dev1.major = MAJOR(dev1.dev_num); //获取主设备号
    dev1.minor = MINOR(dev1.dev_num); //获取次设备号

    printk("major is %d \r\n", dev1.major); //打印主设备号
    printk("minor is %d \r\n", dev1.minor); //打印次设备号
     /*2 初始化cdev*/
    dev1.cdev_test.owner = THIS_MODULE;
    cdev_init(&dev1.cdev_test, &cdev_test_fops);

    /*3 添加一个cdev,完成字符设备注册到内核*/
   ret =  cdev_add(&dev1.cdev_test, dev1.dev_num, 1);
    if(ret<0)
    {
        goto  err_chr_add;
    }
    /*4 创建类*/
  dev1. class = class_create(THIS_MODULE, "test");
    if(IS_ERR(dev1.class))
    {
        ret=PTR_ERR(dev1.class);
        goto err_class_create;
    }
    /*5  创建设备*/
  	dev1.device = device_create(dev1.class, NULL, dev1.dev_num, NULL, "test");
    if(IS_ERR(dev1.device))
    {
        ret=PTR_ERR(dev1.device);
        goto err_device_create;
    }
    return 0;

err_device_create:
        class_destroy(dev1.class);                 //删除类
err_class_create:
       cdev_del(&dev1.cdev_test);                 //删除cdev
err_chr_add:
        unregister_chrdev_region(dev1.dev_num, 1); //注销设备号
err_chrdev:
        return ret;
}

static void __exit timer_dev_exit(void) //驱动出口函数
{
    /*注销字符设备*/
    unregister_chrdev_region(dev1.dev_num, 1); //注销设备号
    cdev_del(&dev1.cdev_test);                 //删除cdev
    device_destroy(dev1.class, dev1.dev_num);       //删除设备
    class_destroy(dev1.class);                 //删除类
}
module_init(timer_dev_init);
module_exit(timer_dev_exit);
MODULE_LICENSE("GPL v2");
MODULE_AUTHOR("topeet");

33.3 运行测试

33.3.1 编译驱动程序

在上一小节中的llseek.c 代码同一目录下创建Makefile 文件,Makefile 文件内容如下所示:然后使用命令“make”进行驱动的编译,编译完生成llseek.ko 目标文件,至此驱动模块就编译成功了,下面交叉编译应用程序。

33.3.2 编译应用程序

来到存放应用程序llseek.c 的文件夹下,使用以下命令对llseek.c 进行交叉编译,编译完成如下图(图33-4)所示:

aarch64-linux-gnu-gcc -o read read.c -static

image-20240819101825187

生成的llseek 文件就是之后放在开发板上运行的可执行文件,至此应用程序的编译就完成了。

33.3.3 运行测试

开发板启动之后,使用以下命令进行驱动模块的加载,如下图(图33-5)所示:

insmod llseek.ko

image-20240819101849539

然后使用以下命令运行可执行文件llseek,运行结果如下图(图33-6)所示:

./llseek

image-20240819101904866

image-20240819101910954

然后使用以下命令卸载对应的驱动,如下图(图33-8)所示:

rmmod timer_dev

image-20240819102225130

第34 章IOCTL 驱动传参实验

用户如果要对外设进行操作,对应的设备驱动不仅要具备读写的能力,还需要对硬件进行控制。以点亮LED 灯驱动实验为例,应用程序通过向内核空间写入1 和0 从而控制LED 灯的亮灭,但是读写操作主要是数据流对数据进行操作,而一些复杂的控制通常需要非数据操作,这时本章节要学习的ioctl 函数就闪耀登场了。

34.1 ioctl 基础

ioctl 是设备驱动程序中用来控制设备的接口函数,一个字符设备驱动通常需要实现设备的打开、关闭、读取、写入等功能,而在一些需要细分的情况下,就需要扩展新的功能,通常以增设ioctl()命令的方式来实现。

下面将从应用层和驱动函数两个方面来对ioctl 函数进行学习。

应用层

函数原型:
    int ioctl(int fd, unsigned int cmd, unsigned long args);
头文件:
    #include <sys/ioctl.h>
函数作用:
    用于向设备发送控制和配置命令。
参数含义:
    fd :是用户程序打开设备时返回的文件描述符
    cmd :是用户程序对设备的控制命令,
    args:应用程序向驱动程序下发的参数,如果传递的参数为指针类型,则可以接收驱动向
用户空间传递的数据(在下面的实验中会进行使用)

上述三个参数中,最重要的是第二个cmd 参数,为unsigned int 类型,为了高效的使用cmd 参数传递更多的控制信息,一个unsigned int cmd 被拆分为了4 段,每一段都有各自的意义,unsigned int cmd 位域拆分如下:

cmd[31:30]—数据(args)的传输方向(读写)
cmd[29:16]—数据(args)的大小
cmd[15:8]—>命令的类型,可以理解成命令的密钥,一般为ASCII 码(0-255 的一个字符,有部分字符已经被占用,每个字符的序号段可能部分被占用)
cmd[7:0] —>命令的序号,是一个8bits 的数字(序号,0-255 之间)

cmd 参数由ioctl 合成宏定义得到,四个合成宏定义如下所示:
定义一个命令,但是不需要参数:

#define _IO(type,nr) _IOC(_IOC_NONE,(type),(nr),0)

定义一个命令,应用程序从驱动程序读参数:

#define _IOR(type,nr,size) _IOC(_IOC_READ,(type),(nr),(_IOC_TYPECHECK(size)))

定义一个命令,应用程序向驱动程序写参数:

#define _IOW(type,nr,size) _IOC(_IOC_WRITE,(type),(nr),(_IOC_TYPECHECK(size)))

定义一个命令,参数是双向传递的:

#define _IOWR(type,nr,size) _IOC(_IOC_READ|_IOC_WRITE,(type),(nr),(_IOC_TYPECHECK(size)))

宏定义参数说明如下所示:

type:命令的类型,一般为一个ASCII 码值,一个驱动程序一般使用一个type
nr:该命令下序号。一个驱动有多个命令,一般他们的type,序号不同
size:args 的类型

例如可以使用以下代码定义不需要参数、向驱动程序写参数、向驱动程序读参数三个宏:

#define CMD_TEST0 _IO('L',0)
#define CMD_TEST1 _IOW('L',1,int)
#define CMD_TEST2 _IOR('L',2,int)

至此,关于应用程序的ioctl 相关知识就讲解完成了。

驱动函数

应用程序中ioctl 函数会调用file_operation 结构体中的unlocked_ioctl 接口,接口定义如下所示:

long (*unlocked_ioctl) (struct file *file , unsigned int cmd, unsigned long arg);

参数说明如下所示:

file:文件描述符。
cmd:与应用程序的cmd 参数对应,在驱动程序中对传递来的cmd 参数进行判断从而做出不同的动作。
arg:与应用程序的arg 参数对应,从而实现内核空间和用户空间参数的传递。

至此,关于驱动函数中的ioctl 相关知识就讲解完成了。在下一小节中将进行ioctl 驱动传参实验。

34.2 实验程序编写

34.2.1 编写测试APP

本实验对应的应用程序网盘路径为:iTOP-RK3568 开发板【底板V1.7 版本】\03_【iTOP-RK3568 开发板】指南教程\02_Linux 驱动配套资料\04_Linux 驱动例程\27\app。

首先来编写应用测试代码ioctl.c,在此代码中使用非阻塞的方式打开设备,编写好的代码如下所示:

#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <sys/ioctl.h>
#include <string.h>

#define CMD_TEST0 _IO('L',0)
#define CMD_TEST1 _IOW('L',1,int)
#define CMD_TEST2 _IOR('L',2,int)

int main(int argc,char *argv[]){

	int fd;//定义int类型的文件描述符fd
	int val;//定义int类型的传递参数val
	fd = open("/dev/test",O_RDWR);//打开test设备节点
	if(fd < 0){
		printf("file open fail\n");
	}
	if(!strcmp(argv[1], "write")){
		ioctl(fd,CMD_TEST1,1);//如果第二个参数为write,向内核空间写入1
	}
	else if(!strcmp(argv[1], "read")){
		ioctl(fd,CMD_TEST2,&val);//如果第二个参数为read,则读取内核空间传递向用户空间传递的值
		printf("val is %d\n",val);
    }
	close(fd);
}

34.2.2 驱动程序编写

本实验对应的网盘路径为:iTOP-RK3568 开发板【底板V1.7 版本】\03_【iTOP-RK3568开发板】指南教程\02_Linux 驱动配套资料\04_Linux 驱动例程\27\module

编写好的驱动程序ioctl.c 如下所示:

#include <linux/module.h>
#include <linux/init.h>
#include <linux/fs.h>
#include <linux/cdev.h>
#include <linux/kdev_t.h>
#include <linux/uaccess.h>
#define CMD_TEST0 _IO('L',0)
#define CMD_TEST1 _IOW('L',1,int)
#define CMD_TEST2 _IOR('L',2,int)

struct device_test{

    dev_t dev_num;  //设备号
     int major ;  //主设备号
    int minor ;  //次设备号
    struct cdev cdev_test; // cdev
    struct class *class;   //类
    struct device *device; //设备
    char kbuf[32];
};
static struct device_test dev1;


static long cdev_test_ioctl(struct file *file, unsigned int cmd, unsigned long arg)
{
	int val;//定义int类型向应用空间传递的变量val
	switch(cmd){
        case CMD_TEST0:
            printk("this is CMD_TEST0\n");
            break;		
        case CMD_TEST1:
            printk("this is CMD_TEST1\n");
			printk("arg is %ld\n",arg);//打印应用空间传递来的arg参数
            break;
        case CMD_TEST2:
			val = 1;//将要传递的变量val赋值为1
            printk("this is CMD_TEST2\n");
			if(copy_to_user((int *)arg,&val,sizeof(val)) != 0){//通过copy_to_user向用户空间传递数据
				printk("copy_to_user error \n");	
			}
            break;			
	default:
			break;
	}
	return 0;
}
/*设备操作函数*/
struct file_operations cdev_test_fops = {
    .owner = THIS_MODULE, //将owner字段指向本模块,可以避免在模块的操作正在被使用时卸载该模块
	.unlocked_ioctl = cdev_test_ioctl,
};

static int __init timer_dev_init(void) //驱动入口函数
{
    /*注册字符设备驱动*/
    int ret;
    /*1 创建设备号*/
    ret = alloc_chrdev_region(&dev1.dev_num, 0, 1, "alloc_name"); //动态分配设备号
    if (ret < 0)
    {
       goto err_chrdev;
    }
    printk("alloc_chrdev_region is ok\n");

    dev1.major = MAJOR(dev1.dev_num); //获取主设备号
    dev1.minor = MINOR(dev1.dev_num); //获取次设备号

    printk("major is %d \r\n", dev1.major); //打印主设备号
    printk("minor is %d \r\n", dev1.minor); //打印次设备号
     /*2 初始化cdev*/
    dev1.cdev_test.owner = THIS_MODULE;
    cdev_init(&dev1.cdev_test, &cdev_test_fops);

    /*3 添加一个cdev,完成字符设备注册到内核*/
   ret =  cdev_add(&dev1.cdev_test, dev1.dev_num, 1);
    if(ret<0)
    {
        goto  err_chr_add;
    }
    /*4 创建类*/
  dev1. class = class_create(THIS_MODULE, "test");
    if(IS_ERR(dev1.class))
    {
        ret=PTR_ERR(dev1.class);
        goto err_class_create;
    }
    /*5  创建设备*/
  	dev1.device = device_create(dev1.class, NULL, dev1.dev_num, NULL, "test");
    if(IS_ERR(dev1.device))
    {
        ret=PTR_ERR(dev1.device);
        goto err_device_create;
    }
    return 0;

err_device_create:
        class_destroy(dev1.class);                 //删除类
err_class_create:
       cdev_del(&dev1.cdev_test);                 //删除cdev
err_chr_add:
        unregister_chrdev_region(dev1.dev_num, 1); //注销设备号
err_chrdev:
        return ret;
}

static void __exit timer_dev_exit(void) //驱动出口函数
{
    /*注销字符设备*/
    unregister_chrdev_region(dev1.dev_num, 1); //注销设备号
    cdev_del(&dev1.cdev_test);                 //删除cdev
    device_destroy(dev1.class, dev1.dev_num);       //删除设备
    class_destroy(dev1.class);                 //删除类
}
module_init(timer_dev_init);
module_exit(timer_dev_exit);
MODULE_LICENSE("GPL v2");
MODULE_AUTHOR("topeet");

34.3 运行测试

34.3.1 编译驱动程序

在上一小节中的ioctl.c 代码同一目录下创建Makefile 文件,Makefile 文件内容与之前一致:然后使用命令“make”进行驱动的编译,编译完生成ioctl.ko 目标文件,至此驱动模块就编译成功了,下面交叉编译应用程序。

34.3.2 编译应用程序

来到存放应用程序ioctl.c 的文件夹下,使用以下命令对ioctl.c 进行交叉编译,编译完成如下图(图34-5)所示:

aarch64-linux-gnu-gcc -o ioctl ioctl.c -static

image-20240819104518038

生成的ioctl 文件就是之后放在开发板上运行的可执行文件,至此应用程序的编译就完成了。

34.3.3 运行测试

开发板启动之后,使用以下命令进行驱动模块的加载,如下图(图34-6)所示:

insmod ioctl.ko

image-20240819104551699

然后使用以下命令通过ioctl 向内核空间传递arg 参数,传递成功如下图(图34-7)所示:

./ioctl write

image-20240819104614063

然后使用以下命令通过ioctl 读取内核空间向用户空间传递的val 值,读取成功如下图(图34-8)所示:

./ioctl read

image-20240819104636444

至此关于iocto 驱动传参实验就测试完成了,可以使用以下命令卸载对应的驱动,如下图(图34-9)所示:

rmmod ioctl.ko

image-20240819104654483

第35 章IOCTL 地址传参实验

在上一章节中对ioctl 基础知识进行了学习,并通过ioctl 进行了驱动传参实验,在本章节将以传递结构体为例,进行地址传参实验,从而加深大家对ioctl 的认识。

35.1 实验程序编写

35.1.1 编写测试APP

本实验对应的应用程序网盘路径为:iTOP-RK3568 开发板【底板V1.7 版本】\03_【iTOP-RK3568 开发板】指南教程\02_Linux 驱动配套资料\04_Linux 驱动例程\28\app。
首先编写应用程序ioctl.c,用来向设备文件写入数据,编写好的应用程序如下所示:

#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <sys/ioctl.h>

#define CMD_TEST0 _IOW('L',0,int)
struct args{//定义要传递的结构体
	int a;
int b;
	int c;
};
int main(int argc,char *argv[]){
	int fd;//定义int类型文件描述符
	struct args test;//定义args类型的结构体变量test
	test.a = 1;
	test.b = 2;
	test.c = 3;
	fd = open("/dev/test",O_RDWR,0777);//打开/dev/test设备
	if(fd < 0){
		printf("file open error \n");
	}
	ioctl(fd,CMD_TEST0,&test);//使用ioctl函数传递结构体变量test地址
	close(fd);
}

35.1.1 驱动程序编写

本实验对应的网盘路径为:iTOP-RK3568 开发板【底板V1.7 版本】\03_【iTOP-RK3568开发板】指南教程\02_Linux 驱动配套资料\04_Linux 驱动例程\28\module
编写好的驱动程序ioctl.c 如下所示:

#include <linux/module.h>
#include <linux/init.h>
#include <linux/fs.h>
#include <linux/cdev.h>
#include <linux/kdev_t.h>
#include <linux/uaccess.h>

#define CMD_TEST0 _IOW('L',0,int)
struct args{
	int a;
	int b;
	int c;
};
struct device_test{

    dev_t dev_num;  //设备号
     int major ;  //主设备号
    int minor ;  //次设备号
    struct cdev cdev_test; // cdev
    struct class *class;   //类
    struct device *device; //设备
    char kbuf[32];
};
static struct device_test dev1;

static long cdev_test_ioctl(struct file *file, unsigned int cmd, unsigned long arg)
{
	struct args test;  
	switch(cmd){
        case CMD_TEST0:
			if(copy_from_user(&test,(int *)arg,sizeof(test)) != 0){
				printk("copy_from_user error\n");
			}
			printk("a = %d\n",test.a);
  			printk("b = %d\n",test.b);
  	  		printk("c = %d\n",test.c);
            break;			
	default:
			break;
	}
	return 0;
}

/*设备操作函数*/
struct file_operations cdev_test_fops = {
    .owner = THIS_MODULE, //将owner字段指向本模块,可以避免在模块的操作正在被使用时卸载该模块
	.unlocked_ioctl = cdev_test_ioctl,
};

static int __init timer_dev_init(void) //驱动入口函数
{
    /*注册字符设备驱动*/
    int ret;
    /*1 创建设备号*/
    ret = alloc_chrdev_region(&dev1.dev_num, 0, 1, "alloc_name"); //动态分配设备号
    if (ret < 0)
    {
       goto err_chrdev;
    }
    printk("alloc_chrdev_region is ok\n");

    dev1.major = MAJOR(dev1.dev_num); //获取主设备号
    dev1.minor = MINOR(dev1.dev_num); //获取次设备号

    printk("major is %d \r\n", dev1.major); //打印主设备号
    printk("minor is %d \r\n", dev1.minor); //打印次设备号
     /*2 初始化cdev*/
    dev1.cdev_test.owner = THIS_MODULE;
    cdev_init(&dev1.cdev_test, &cdev_test_fops);

    /*3 添加一个cdev,完成字符设备注册到内核*/
   ret =  cdev_add(&dev1.cdev_test, dev1.dev_num, 1);
    if(ret<0)
    {
        goto  err_chr_add;
    }
    /*4 创建类*/
  dev1. class = class_create(THIS_MODULE, "test");
    if(IS_ERR(dev1.class))
    {
        ret=PTR_ERR(dev1.class);
        goto err_class_create;
    }
    /*5  创建设备*/
  	dev1.device = device_create(dev1.class, NULL, dev1.dev_num, NULL, "test");
    if(IS_ERR(dev1.device))
    {
        ret=PTR_ERR(dev1.device);
        goto err_device_create;
    }

    return 0;

err_device_create:
        class_destroy(dev1.class);                 //删除类
err_class_create:
       cdev_del(&dev1.cdev_test);                 //删除cdev
err_chr_add:
        unregister_chrdev_region(dev1.dev_num, 1); //注销设备号
err_chrdev:
        return ret;
}

static void __exit timer_dev_exit(void) //驱动出口函数
{
    /*注销字符设备*/
    unregister_chrdev_region(dev1.dev_num, 1); //注销设备号
    cdev_del(&dev1.cdev_test);                 //删除cdev
    device_destroy(dev1.class, dev1.dev_num);       //删除设备
    class_destroy(dev1.class);                 //删除类
}
module_init(timer_dev_init);
module_exit(timer_dev_exit);
MODULE_LICENSE("GPL v2");
MODULE_AUTHOR("topeet");

35.2 运行测试

35.2.1 编译驱动程序

在上一小节中的ioctl.c 代码同一目录下创建Makefile 文件,Makefile 文件内容:然后使用命令“make”进行驱动的编译,编译完生成wq.ko 目标文件,至此驱动模块就编译成功了,下面交叉编译应用程序。

35.2.2 编译应用程序

来到存放应用程序ioctl.c 的文件夹下,使用以下命令对ioctl.c 进行交叉编译,编译完成如下图(图35-4)所示:

aarch64-linux-gnu-gcc -o ioctl ioctl.c

生成的ioctl 文件就是之后放在开发板上运行的可执行文件,至此应用程序的编译就完成了。

35.2.3 运行测试

开发板启动之后,使用以下命令进行驱动模块的加载,如下图(图35-5)所示:

insmod ioctl.ko

image-20240819105924647

然后使用以下命令运行可执行程序,运行成功如下图(图35-6)所示:

./ioctl

image-20240819105938370

可以看到结构体类型变量test 已经成功传递到了内核空间,a、b、c 的值都被正确打印了出来,我们的ioctl 地址传参实验就完成了。

image-20240819105950047

第36 章封装驱动API 接口实验

相信经过前面两个章节的学习已经能够熟练的使用ioctl 函数了,在本章节会进行两个实验,每个实验的要完成的任务如下所示:

  • 实验一:通过ioctl 对定时器进行控制,分别实现打开定时器、关闭定时器和设置定时时间的功能。
  • 实验二:对实验一的应用程序进行封装,从而让应用编程人员更好的对设备进行编程。

36.1 ioctl 控制定时器实验

首先进行ioctl 控制定时器实验,通过该实验可以综合ioctl 函数和定时器相关知识,从而进一步加深对ioctl 的理解。

36.1.1 编写测试APP

本实验对应的应用程序网盘路径为:iTOP-RK3568 开发板【底板V1.7 版本】\03_【iTOP-RK3568 开发板】指南教程\02_Linux 驱动配套资料\04_Linux 驱动例程\29\app1
首先来编写应用测试代码ioctl.c,编写好的代码如下所示:

#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <sys/ioctl.h>

#define TIMER_OPEN _IO('L',0)
#define TIMER_CLOSE _IO('L',1)
#define TIMER_SET _IOW('L',2,int)

int main(int argc,char *argv[]){
	int fd;
	fd = open("/dev/test",O_RDWR,0777);
	if(fd < 0){
		printf("file open error \n");
	}
    ioctl(fd,TIMER_SET,1000);
	ioctl(fd,TIMER_OPEN);
	sleep(3);
    ioctl(fd,TIMER_SET,3000);
	sleep(7);
	ioctl(fd,TIMER_CLOSE);
	close(fd);
}

第8-10 行通过合成宏定义了三个ioctl 命令,分别代表定时器打开、定时器关闭、定时时间设置。
第18 行和第21 行将定时时间分别设置为1 秒和3 秒。
第19 行打开定时器。
第23 行关闭定时器。

36.1.2 驱动程序编写

本实验对应的网盘路径为:iTOP-RK3568 开发板【底板V1.7 版本】\03_【iTOP-RK3568开发板】指南教程\02_Linux 驱动配套资料\04_Linux 驱动例程\29\module
编写好的驱动程序ioctl_timer.c 如下所示:

#include <linux/module.h>
#include <linux/init.h>
#include <linux/fs.h>
#include <linux/cdev.h>
#include <linux/kdev_t.h>
#include <linux/uaccess.h>
#include <linux/timer.h>
#define TIMER_OPEN _IO('L',0)
#define TIMER_CLOSE _IO('L',1)
#define TIMER_SET _IOW('L',2,int)

struct device_test{
    dev_t dev_num;  //设备号
    int major ;  //主设备号
    int minor ;  //次设备号
    struct cdev cdev_test; // cdev
    struct class *class;   //类
    struct device *device; //设备
	int counter; 
};

static struct device_test dev1;
static void fnction_test(struct timer_list *t);//定义function_test定时功能函数
DEFINE_TIMER(timer_test,fnction_test);//定义一个定时器

void fnction_test(struct timer_list *t)
{
    printk("this is fnction_test\n");
    mod_timer(&timer_test,jiffies_64 + msecs_to_jiffies(dev1.counter));//使用mod_timer函数重新设置定时时间
}
static int cdev_test_open(struct inode *inode, struct file *file)
{
    file->private_data=&dev1;//设置私有数据
    return 0;
}

static int cdev_test_release(struct inode *inode, struct file *file)
{
    file->private_data=&dev1;//设置私有数据

    return 0;
}

static long cdev_test_ioctl(struct file *file, unsigned int cmd, unsigned long arg)
{
	struct device_test *test_dev = (struct device_test *)file->private_data;//设置私有数据
	switch(cmd){
        case TIMER_OPEN:
			add_timer(&timer_test);//添加一个定时器
            break;
        case TIMER_CLOSE:
			del_timer(&timer_test);//删除一个定时器
            break;
        case TIMER_SET:
			test_dev->counter = arg;
			timer_test.expires = jiffies_64 + msecs_to_jiffies(test_dev->counter);//设置定时时间
            break;

	default:
			break;
	}
	return 0;
}

/*设备操作函数*/
struct file_operations cdev_test_fops = {
    .owner = THIS_MODULE, //将owner字段指向本模块,可以避免在模块的操作正在被使用时卸载该模块
	.open = cdev_test_open,
	.release = cdev_test_release,
	.unlocked_ioctl = cdev_test_ioctl,
};

static int __init timer_dev_init(void) //驱动入口函数
{
    /*注册字符设备驱动*/
    int ret;
    /*1 创建设备号*/
    ret = alloc_chrdev_region(&dev1.dev_num, 0, 1, "alloc_name"); //动态分配设备号
    if (ret < 0)
    {
       goto err_chrdev;
    }
    printk("alloc_chrdev_region is ok\n");

    dev1.major = MAJOR(dev1.dev_num); //获取主设备号
    dev1.minor = MINOR(dev1.dev_num); //获取次设备号

    printk("major is %d \r\n", dev1.major); //打印主设备号
    printk("minor is %d \r\n", dev1.minor); //打印次设备号
     /*2 初始化cdev*/
    dev1.cdev_test.owner = THIS_MODULE;
    cdev_init(&dev1.cdev_test, &cdev_test_fops);

    /*3 添加一个cdev,完成字符设备注册到内核*/
   ret =  cdev_add(&dev1.cdev_test, dev1.dev_num, 1);
    if(ret<0)
    {
        goto  err_chr_add;
    }
    /*4 创建类*/
 dev1. class = class_create(THIS_MODULE, "test");
if(IS_ERR(dev1.class))
{
        ret=PTR_ERR(dev1.class);
        goto err_class_create;
    }
    /*5  创建设备*/
  	dev1.device = device_create(dev1.class, NULL, dev1.dev_num, NULL, "test");
    if(IS_ERR(dev1.device))
    {
        ret=PTR_ERR(dev1.device);
        goto err_device_create;
    }

    return 0;

err_device_create:
        class_destroy(dev1.class);                 //删除类
err_class_create:
       cdev_del(&dev1.cdev_test);                 //删除cdev
err_chr_add:
        unregister_chrdev_region(dev1.dev_num, 1); //注销设备号
err_chrdev:
        return ret;
}

static void __exit timer_dev_exit(void) //驱动出口函数
{
    /*注销字符设备*/
    unregister_chrdev_region(dev1.dev_num, 1); //注销设备号
    cdev_del(&dev1.cdev_test);                 //删除cdev
    device_destroy(dev1.class, dev1.dev_num);       //删除设备
    class_destroy(dev1.class);                 //删除类
}
module_init(timer_dev_init);
module_exit(timer_dev_exit);
MODULE_LICENSE("GPL v2");
MODULE_AUTHOR("topeet");

36.2 运行测试

36.2.1 编译驱动程序

在上一小节中的ioctl_timer.c 代码同一目录下创建Makefile 文件,Makefile 文件内容:然后使用命令“make”进行驱动的编译,编译完生成ioctl_timer.ko 目标文件,至此驱动模块就编译成功了,下面交叉编译应用程序。

36.2.2 编译应用程序

来到存放应用程序ioctl.c 的文件夹下,使用以下命令对ioctl.c 进行交叉编译,编译完成如下图(图36-4)所示:

aarch64-linux-gnu-gcc -o ioctl ioctl.c

生成的ioctl 文件就是之后放在开发板上运行的可执行文件,至此应用程序的编译就完成了。

36.2.3 运行测试

开发板启动之后,使用以下命令进行驱动模块的加载,如下图(图36-5)所示:

insmod ioctl_timer.ko

image-20240819110728360

输入以下命令运行可执行文件,运行成功如下图(图36-6)所示:

image-20240819110749562

可以看到前面三个打印信息间隔为1 秒钟,后面三个打印信息间隔为3 秒钟,至此,实验一就结束了,然后使用以下命令卸载驱动模块,如下图(图36-7)所示:

rmmod ioctl_timer.ko

36.3 封装驱动API 接口

至此,随着ioctl 练习的结束,字符设备驱动框架相关的知识也就完结了,相信细心的小伙伴在上一小节应用程序的编写中会发现问题,应用程序是从驱动的角度进行编写的,具体内容如下:

#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <sys/ioctl.h>

#define TIMER_OPEN _IO('L',0)
#define TIMER_CLOSE _IO('L',1)
#define TIMER_SET _IOW('L',2,int)

int main(int argc,char *argv[]){
	int fd;
	fd = open("/dev/test",O_RDWR,0777);
	if(fd < 0){
		printf("file open error \n");
	}
    ioctl(fd,TIMER_SET,1000);
	ioctl(fd,TIMER_OPEN);
	sleep(3);
    ioctl(fd,TIMER_SET,3000);
	sleep(7);
	ioctl(fd,TIMER_CLOSE);
	close(fd);
}

作为驱动工程师的我们当然可以理解每一行代码所要完成的功能,而一般情况下,应用都是由专业的应用工程师来进行编写的,上述代码编写方式很不利于应用工程师的理解和程序的移植,所以对于应用程序API 的封装是一件必然的事情。

封装好的应用程序网盘路径为:iTOP-RK3568 开发板【底板V1.7 版本】\03_【iTOP-RK3568开发板】指南教程\02_Linux 驱动配套资料\04_Linux 驱动例程\29\app2
首先来编写整体库文件timerlib.h,编写好的代码如下所示:

#ifndef _TIMELIB_H_
#define _TIMELIB_H_
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <sys/ioctl.h>

#define TIMER_OPEN _IO('L',0)
#define TIMER_CLOSE _IO('L',1)
#define TIMER_SET _IOW('L',2,int)

int dev_open();
int timer_open(int fd);
int timer_close(int fd);
int timer_set(int fd,int arg);

#endif

在9-11 行使用合成宏定义了三个ioctl 命令,分别代表定时器打开、定时器关闭、定时时间设置。
在第12-15 行定义了四个功能函数,所代表的功能分别为设备打开、定时器打开、定时器关闭、定时时间设置。
接下来将创建每个功能函数的c 文件,最后编译为单独的库,首先编写dev_open.c 文件,
编写好的代码如下所示:

#include <stdio.h>
#include "timerlib.h"
int dev_open()
{
	fd = open("/dev/test",O_RDWR,0777);
    if(fd < 0){
		printf("file open error \n");
	}
	return fd;
}

然后编写定时器打开函数timeropen.c 文件,编写好的代码如下所示:

#include <stdio.h>
#include "timerlib.h"
int timer_open(int fd)
{
	int ret;
	ret = ioctl(fd,TIMER_OPEN);
	if(ret < 0){
		printf("ioctl open error \n");
		return -1;
	}
	return ret;
}

编写定时器打开函数timerclose.c 文件,编写好的代码如下所示:

#include <stdio.h>
#include "timerlib.h"
int timer_close(int fd)
{
	int ret;
	ret = ioctl(fd,TIMER_CLOSE);
	if(ret < 0){
		printf("ioctl  close error \n");
		return -1;
	}
	return ret;
}

编写定时器打开函数timerset.c 文件,编写好的代码如下所示:

#include <stdio.h>
#include "timerlib.h"
int timer_set(int fd,int arg)
{
	int ret;
	ret = ioctl(fd,TIMER_SET,arg);
	if(ret < 0){
		printf("ioctl error \n");
		return -1;
	}
	return ret;
}

最后编写测试要用到的应用程序ioctl.c 文件,编写好的代码如下所示:

#include <stdio.h>
#include "timerlib.h"
int main(int argc,char *argv[]){
	int fd;
	fd = dev_open();
    timer_set(fd,1000);
	timer_open(fd);
	sleep(3);
	timer_set(fd,3000);
	sleep(7);
	timer_close(fd);
	close(fd);
}

至此,要用到的文件就都编写完成了,会在下一小节进行库的制作,以及应用程序的编译。

36.4 运行测试

36.4.1 编译应用程序

首先使用以下命令将存放功能函数的c 文件编译成.o 文件,编译完成如下图(图36-7)所示:

aarch64-linux-gnu-gcc -c dev_open.c
aarch64-linux-gnu-gcc -c timer*.c

image-20240819111351385

然后使用以下命令将相应的.o 文件编译成.a 静态库(这里要注意库的名称都以lib 开头),编译完成如下图(图36-8)所示:

aarch64-linux-gnu-ar rcs libtime.a timer*.o
aarch64-linux-gnu-ar rcs libopen.a dev_open.o

image-20240819111445392

最后使用以下命令对ioctl.c 进行交叉编译,编译完成如下图(图36-9)所示:

aarch64-linux-gnu-gcc -o ioctl ioctl.c -L./ -ltime -lopen

image-20240819111510682

生成的ioctl 文件就是之后放在开发板上运行的可执行文件,至此应用程序的编译就完成了。
36.4.2 运行测试
开发板启动之后,使用以下命令进行驱动模块的加载,如下图(图36-10)所示:

insmod ioctl_timer.ko

image-20240819111540927

输入以下命令运行可执行文件,运行成功如下图(图36-11)所示:

image-20240819111550053

可以看到前面三个打印信息间隔为1 秒钟,后面三个打印信息间隔为3 秒钟,至此,实验一就结束了,然后使用以下命令卸载驱动模块,如下图(图36-12)所示:

rmmod ioctl_timer.ko

image-20240819111608613

第37 章优化驱动稳定性和效率实验

在Linux 中应用程序运行在用户空间,应用程序错误之后,并不会影响其他程序的运行,而驱动工作在内核层,是内核代码的一部分,当驱动出现问题之后,可能会导致整个系统的崩溃。所以在驱动中,需要对各种判断、预处理等进行排查等,在本小节将对如何优化驱动稳定性和提高驱动效率进行学习。

37.1 方法一:检测ioctl 命令

ioctl 的cmd 命令是由合成宏合成得到的,也有相应的分解宏得到各个参数,四个分解宏如下所示:
分解cmd 命令,得到命令的类型:

_IOC_TYPE(cmd)

分解cmd 命令,得到数据(args)的传输方向:

_IOC_DIR(cmd)

分解cmd 命令,得到命令的序号:

_IOC_NR(cmd)

分解cmd 命令,得到数据(args)的大小:

_IOC_SIZE(cmd)

可以在驱动中通过上述分解宏对传入的ioctl 命令类型等参数进行判断,从而得到判断传入的参数是否正确,以此优化驱动的稳定性。

if(_IOC_TYPE(cmd) != 'L'){
    printk("cmd type error \n");
    return -1;
}

例如可以通过上述代码对传入参数的类型进行判断,如果传入的参数类型不为“L”,就返回错误,其他参数的检测方法相同。

37.2 方法二:检测传递地址是否合理

access_ok()函数

函数原型:
    access_ok(addr,size);
函数作用:
    检查用户空间内存块是否可用
参数含义:
    addr : 用户空间的指针变量,其指向一个要检查的内存块开始处。
    size : 要检查内存块的大小。
返回值:
    成功返回1,失败返回0

以第35 章的ioctl 地址传参实验为例,对传入的args 地址进行判断,具体代码如下所示:

struct args test;
int len;
switch(cmd){
    case CMD_TEST0:
        len = sizeof(struct args);
        if(!access_ok(arg,len)){
            return -1;
        }
        if(copy_from_user(&test,(int *)arg,sizeof(test)) != 0){
            printk("copy_from_user error\n");
        }
    break;

在第6 行对传入的args 参数地址进行判断,如果不合法则返回-1,从而保证了驱动运行的稳定性。

37.3 方法三:分支预测优化

现在的CPU 都有ICache 和流水线机制。即运行当前指令时,ICache 会预读取后面的指令,从而提升效率。但是如果条件分支的结果是跳转到了其他指令,那预取下一条指令就浪费时间了。而本章节要用到的likely 和unlikely 宏,会让编译器总是将大概率执行的代码放在靠前的位置,从而提高驱动的效率。

likelyunlikely 宏定义在“内核源码/include/linux/compiler.h”文件中,具体定义如下所示:

#define likely(x) __builtin_expect(!!(x), 1)
#define unlikely(x) __builtin_expect(!!(x), 0)

__builtin_expect 的作用是告知编译器预期表达式exp 等于c 的可能性更大,编译器可以根据该因素更好的对代码进行优化,所以likely 与unlikely 的作用就是表达性x 为真的可能性更大(likely)和更小(unlikely)。

这里以上一小节添加传递地址检测内容后的代码为例,对copy_from_user 函数添加分支预测优化函数,添加完成如下所示:

struct args test;
int len;
switch(cmd){
    case CMD_TEST0:
        len = sizeof(struct args);
        if(!access_ok(arg,len)){
            return -1;
        }
        if(unlikely(copy_from_user(&test,(int *)arg,sizeof(test)) != 0)){
            printk("copy_from_user error\n");
        }
    break;

传递地址检测成功之后才会使用执行copy_from_user 函数,在传递地址正确的前提下copy_from_user 函数运行失败为小概率事件,所以这里使用unlikely 函数进行驱动效率的优化。至此,关于分支预测优化相关的知识就讲解完成了。

第38 章驱动调试方法实验

在之前编写的驱动程序中,通常都使用printk 函数打印相应的提示信息从而对驱动进行调试,那有没有其他的方式来调试驱动呢,答案是肯定的,在本章节中将对不同驱动调试方法进行学习。

38.1 方法1:dump_stack 函数

作用:打印内核调用堆栈,并打印函数的调用关系。
这里以最简单的helloworld 驱动为例进行dump_stack 函数演示,实验代码如下所示:

#include <linux/module.h>
#include <linux/kernel.h>
static int __init helloworld_init(void)
{
    printk(KERN_EMERG "helloworld_init\r\n");
    dump_stack();
    return 0;
}
static void __exit helloworld_exit(void)
{
    printk(KERN_EMERG "helloworld_exit\r\n");
}
module_init(helloworld_init);
module_exit(helloworld_exit);
MODULE_LICENSE("GPL v2");
MODULE_AUTHOR("topeet");

和原helloworld 驱动程序相比,在第6 行添加了dump_stack(),驱动加载之后打印信息如下(图38-1)所示:

image-20240819112218913

可以看到helloworld_init 函数的调用关系就都打印了出来。 至此关于dump_stack 函数的测试就完成了。

38.2 方法2:WARN_ON(condition)函数

WARN_ON (condition)函数作用:在括号中的条件成立时,内核会抛出栈回溯,打印函数的调用关系。通常用于内核抛出一个警告,暗示某种不太合理的事情发生了。

WARN_ON 实际上也是调用dump_stack,只是多了参数condition 判断条件是否成立,例如WARN_ON (1)则条件判断成功,函数会成功执行。

这里仍然以最简单的helloworld 驱动为例进行WARN_ON 函数演示,实验代码如下所示:

#include <linux/module.h>
#include <linux/kernel.h>
static int __init helloworld_init(void)
{
    printk(KERN_EMERG "helloworld_init\r\n");
    WARN_ON(1);
    return 0;
}
static void __exit helloworld_exit(void)
{
    printk(KERN_EMERG "helloworld_exit\r\n");
}
module_init(helloworld_init);
module_exit(helloworld_exit);
MODULE_LICENSE("GPL v2");
MODULE_AUTHOR("topeet");

和原helloworld 驱动程序相比,在第6 行添加了WARN_ON(1),驱动加载之后打印信息如下(图38-2)所示:

image-20240819112326430

可以看到helloworld_init 函数的调用关系以及寄存器值就都打印了出来。 至此关于WARN_ON 函数的测试就完成了。

38.3 方法3:BUG_ON (condition)函数

内核中有许多地方调用类似BUG_ON()的语句,它非常像一个内核运行时的断言,意味着本来不该执行到BUG_ON()这条语句,一旦BUG_ON()执行内核就会立刻抛出oops,导致栈的回溯和错误信息的打印。大部分体系结构把BUG()和BUG_ON()定义成某种非法操作,这样自然会产生需要的oops。参数condition 判断条件是否成立,例如BUG_ON (1)则条件判断成功,函数会成功执行。

这里仍然以最简单的helloworld 驱动为例进行BUGON 函数演示,实验代码如下所示:

#include <linux/module.h>
#include <linux/kernel.h>
static int __init helloworld_init(void)
{
    printk(KERN_EMERG "helloworld_init\r\n");
    BUGON(1);
    return 0;
}
static void __exit helloworld_exit(void)
{
    printk(KERN_EMERG "helloworld_exit\r\n");
}
module_init(helloworld_init);
module_exit(helloworld_exit);
MODULE_LICENSE("GPL v2");
MODULE_AUTHOR("topeet");

和原helloworld 驱动程序相比,在第6 行添加了BUGON(1),驱动加载之后打印信息如下(图38-3)所示:

image-20240819112450829

可以看到helloworld_init 函数的调用关系以及寄存器值就都打印了出来。至此关于BUGON(1)函数的测试就完成了。

38.4 方法4:panic (fmt...)函数

panic (fmt...)函数:输出打印会造成系统死机并将函数的调用关系以及寄存器值就都打印了出来。

这里仍然以最简单的helloworld 驱动为例进行panic 函数的演示,实验代码如下所示:

#include <linux/module.h>
#include <linux/kernel.h>
static int __init helloworld_init(void)
{
    printk(KERN_EMERG "helloworld_init\r\n");
    panic("!!!!!!!!!!!!!!!!!!!!!!!!!!!!");
    return 0;
}
static void __exit helloworld_exit(void)
{
    printk(KERN_EMERG "helloworld_exit\r\n");
}
module_init(helloworld_init);
module_exit(helloworld_exit);
MODULE_LICENSE("GPL v2");
MODULE_AUTHOR("topeet");

和原helloworld 驱动程序相比,在第6 行添加了panic("!!!!!!!!!!!!!!!!!!!!!!!!!!!!"),驱动加载之后打印信息如下(图38-4)所示:

image-20240819112614704

可以看到helloworld_init 函数的调用关系以及寄存器值就都打印了出来,信息打印完成之后会发现系统已经崩溃了,终端已经无法再进行输入。
至此关于panic 函数的测试就完成了。

第五篇中断

这个图可以用来捋一下逻辑。

image-20240822092240126

第39 章中断实验

在前面的课程中,我们深入学习了高级字符设备的进阶知识,包括IO 模型、定时器原理、llseek 设备定位和通过ioctl 传递参数等。通过这些课程,我们对高级字符设备有了深入的理解,并掌握了一些实用的技术和编程方法。从今天开始,我们就进入中断课程的学习了。中断是操作系统中至关重要的机制,它能够显著提高系统的响应性能和并发处理能力。

39.1 什么是中断?

39.1.1 中断的概念

中断是指在CPU 正常运行期间,由外部或内部事件引起的一种机制。当中断发生时,CPU会停止当前正在执行的程序,并转而执行触发该中断的中断处理程序。处理完中断处理程序后,CPU 会返回到中断发生的地方,继续执行被中断的程序。中断机制允许CPU 在实时响应外部或内部事件的同时,保持对其他任务的处理能力。

可以想象这样一幅画面,你正在烹饪一顿美味的晚餐,准备了各种食材,点燃了炉灶,开始了幸福的烹饪过程,突然,你的手机响起,有人打来了一个紧急电话,打破了你正常的烹饪流程,这时候你需要立刻停止手中的工作,迅速接起电话,与对方进行交流,在接完电话之后,再回到厨房继续之前的烹饪流程。这就是一个在实际生活中的中断案例,中断的概念流程图如下(39-1)所示:

image-20240819112729887

39.1.2 中断的重要性

在上面的场景中,作为唯一具有处理能力的主体,我们一次只能专注于一个任务,可以等待水烧开、看电视等等。然而,当我们专心致志地完成一项任务时,常常会有紧迫或不紧迫的其他事情突然出现,需要我们关注和处理。有些情况甚至要求我们立即停下手头的工作来应对。只有在处理完这些中断事件之后,我们才能回到先前的任务。

中断机制赋予了我们处理意外情况的能力,而且如果我们能充分利用这个机制,就能够同时完成多个任务。回到烧水的例子,无论我们是否在厨房,煤气灶都会将水烧开。我们只需要在水烧开后及时关掉煤气。为了避免在厨房等待的时间,而水烧开时产生的声音就是中断信号,提醒我们炉子上的水已经烧开。这样,我们就可以在等待的时间里做其他事情,比如看电视。当水壶烧开发出声音之后,它会打断当前的任务,提醒水已经烧开,这时只需要前往厨房关掉煤气即可。

中断机制使我们能够有条不紊地同时处理多个任务,从而提高了并发处理能力。类似地,计算机系统中也使用中断机制来应对各种外部事件。例如,在键盘输入时,会发送一个中断信号给CPU,以便及时响应用户的操作。这样,CPU 就不必一直轮询键盘的状态,而可以专注于其他任务。中断机制还可以用于处理硬盘读写完成、网络数据包接收等事件,提高了系统的资源利用率和并发处理能力。

39.1.3 中断的上下半部

中断的执行需要快速响应,但并不是所有中断都能迅速完成。此外,Linux 中的中断不支持嵌套,意味着在正式处理中断之前会屏蔽其他中断,直到中断处理完成后再重新允许接收中断,如果中断处理时间过长,将会引发问题。

这里仍旧以烹饪的过程中接电话进行举例:当你正在烹饪一顿美味的晚餐时,所有的食材都准备好了,炉灶上的火焰跳跃着,你正享受着烹饪的乐趣。突然,你的手机响起,发出紧急电话的铃声,打破了你正常的烹饪流程,接电话的时间很短并不会对烹饪产生很大的影响,而接电话的时候可能就有问题了,水烧开之后可能会煮干、错过了最好的添加调味料的时间等等。

而为了让系统可以更好地处理中断事件,提高实时性和响应能力,将中断服务程序划分为上下文两部分

中断上文是中断服务程序的第一部分,它主要处理一些紧急且需要快速响应的任务。中断上文的特点是执行时间较短,旨在尽快完成对中断的处理。这些任务可能包括保存寄存器状态、更新计数器等,以便在中断处理完成后能够正确地返回到中断前的执行位置。

中断下文是中断服务程序的第二部分,它主要处理一些相对耗时的任务。由于中断上文需要尽快完成,因此中断下文负责处理那些不能立即完成的、需要更多时间的任务。这些任务可能包括复杂的计算、访问外部设备或进行长时间的数据处理等。

39.2 中断子系统框架

一个完整的中断子系统框架可以分为四个层次,由上到下分别为用户层、通用层、硬件相关层和硬件层,每个层相关的介绍如下(图39-2)所示:

  • 用户层:用户层是中断的使用者,主要包括各类设备驱动。这些驱动程序通过中断相关的接口进行中断的申请和注册。当外设触发中断时,用户层驱动程序会进行相应的回调处理,执行特定的操作。
  • 通用层:通用层也可称为框架层,它是硬件无关的层次。通用层的代码在所有硬件平台上都是通用的,不依赖于具体的硬件架构或中断控制器。通用层提供了统一的接口和功能,用于管理和处理中断,使得驱动程序能够在不同的硬件平台上复用。
  • 硬件相关层:硬件相关层包含两部分代码。一部分是与特定处理器架构相关的代码,比如ARM64 处理器的中断处理相关代码。这些代码负责处理特定架构的中断机制,包括中断向量表、中断处理程序等。另一部分是中断控制器的驱动代码,用于与中断控制器进行通信和配置。这些代码与具体的中断控制器硬件相关。
  • 硬件层:硬件层位于最底层,与具体的硬件连接相关。它包括外设与SoC(系统片上芯片)的物理连接部分。中断信号从外设传递到中断控制器,由中断控制器统一管理和路由到处理器。硬件层的设计和实现决定了中断信号的传递方式和硬件的中断处理能力。

image-20240819113451875

本小节的重点会聚集在硬件层各部分的详细讲解以及用户层编写驱动程序所用到的接口函数。

39.2.1 中断控制器GIC

中断控制器GIC(Generic Interrupt Controller)是中断子系统框架硬件层中的一个关键组件,用于管理和控制中断。它接收来自各种中断源的中断请求,并根据预先配置的中断优先级、屏蔽和路由规则,将中断请求分发给适当的处理器核心或中断服务例程。

GIC 是由ARM 公司提出设计规范,当前有四个版本,GIC V1-v4。设计规范中最常用的,有3 个版本V2.0、V3.1、V4.1,GICv3 版本设计主要运行在Armv8-A, Armv9-A 等架构上。ARM 公司并给出一个实际的控制器设计参考,比如GIC-400(支持GIC v2 架构)、gic500(支持GIC v3 架构)、GIC-600(支持GIC v3 和GIC v4 架构)。最终芯片厂商可以自己实现GIC 或者直接购买ARM提供的设计。

每个GIC 版本及相应特性如下表(表39-3)所示:

版本 关键特性 常用核心
GICv1 支持最多八个处理器核心(PE)
支持最多1020 个中断ID
ARM Cortex-A5 MPCore
ARM Cortex-A9 MPCore
ARM Cortex-R7 MPCore
GICv2 GICv1 的所有关键特性
支持虚拟化
ARM Cortex-A7 MPCore
ARM Cortex-A15 MPCore
ARM Cortex-A53 MPCore
ARM Cortex-A57 MPCore
GICv3 GICv2 的所有关键特性
支持超过8 个处理器核心
支持基于消息的中断
支持超过1020 个中断ID
CPU 接口寄存器的系统寄存器访问
增强的安全模型,分离安全和非安全的Group 1 中断
ARM Cortex-A53MPCore
ARM Cortex-A57MPCore
ARM Cortex-A72 MPCore
GICv4 GICv3 的所有关键特性
虚拟中断的直接注入
ARM Cortex-A53 MPCore
ARMCortex-A57MPCore
ARM Cortex-A72 MPCore

在RK3568 上使用的GIC 版本为GICv3,相应的中断控制器模型如下(图39-4)所示:

image-20240819114128421

GIC 中断控制器可以分为Distributor 接口、Redistributor 接口和CPU 接口,下面是每个部分的说明:

Distributor 中断仲裁器:

包含影响所有处理器核心中断的全局设置。包含以下编程接口:

  • ●启用和禁用SPI。
  • ●设置每个SPI 的优先级级别。
  • ●每个SPI 的路由信息。
  • ●将每个SPI 设置为电平触发或边沿触发。
  • ●生成基于消息的SPI。
  • ●控制SPI 的活动和挂起状态。
  • ●用于确定在每个安全状态中使用的程序员模型的控制(亲和性路由或遗留模型)。

Redistributor 重新分配器:

对于每个连接的处理器核心(PE),都有一个重新分配器(Redistributor)。重新分配器提供以下编程接口:

  • ●启用和禁用SGI(软件生成的中断)和PPI(处理器专用中断)。
  • ●设置SGI 和PPI 的优先级级别。
  • ●将每个PPI 设置为电平触发或边沿触发。
  • ●将每个SGI 和PPI 分配给一个中断组。
  • ●控制SGI 和PPI 的状态。
  • ●对支持关联 LP(I 低功耗中断)的中断属性和挂起状态的内存中的数据结构进行基址控制。
  • ●支持与连接的处理器核心的电源管理。

CPU 接口:

每个重新分配器都连接到一个CPU 接口。CPU 接口提供以下编程接口:

  • ●通用控制和配置,用于启用中断处理。
  • ●确认中断。
  • ●执行中断的优先级降低和停用。
  • ●为处理器核心设置中断优先级屏蔽。
  • ●定义处理器核心的抢占策略。
  • ●确定处理器核心最高优先级的挂起中断。

39.2.2 中断类型

GIC-V3 支持四种类型的中断,分别是SGI、PPI、SPI 和LPI,每个中断类型的介绍如下:

  • SGI(Software Generated Interrupt,软件生成中断):SGI 是通过向GIC 中的SGI 寄存器写入来生成的中断。它通常用于处理器之间的通信,允许一个PE 发送中断给一个或多个指定的PE,中断号ID0 - ID15 用于SGI。
  • PPI(Private Peripheral Interrupt,私有外设中断):针对特定PE 的外设中断。不与其他PE共享,中断号ID16 - ID31 用于PPI。
  • SPI(Shared Peripheral Interrupt,共享外设中断):全局外设中断,可以路由到指定的处理器核心(PE)或一组PE,它允许多个PE 接收同一个中断。中断号ID32 - ID1019 用于SPI,
  • LPI(Locality-specific Peripheral Interrupt,特定局部外设中断):LPI 是GICv3 中引入的一种中断类型,与其他类型的中断有几个不同之处。LPI 总是基于消息的中断,其配置存储在内存表中,而不是寄存器中。
INTID 范围 中断类型 备注
0 - 15 SGI(软件生成中断) 每个核心分别存储
16 - 31 PPI(私有外设中断) 每个核心分别存储
32 - 1019 SPI(共享外设中断)
1020 - 1023 特殊中断号 用于表示特殊情况
1024 - 8191 保留
8192 及更大 LPI(特定局部外设中断) 上限由实现定义

中断处理的状态机如下图(图39-6)所示:

image-20240819114654563

  • Inactive(非活动状态):中断源当前未被触发。
  • Pending(等待状态):中断源已被触发,但尚未被处理器核心确认。
  • Active(活动状态):中断源已被触发,并且已被处理器核心确认。
  • Active and Pending(活动且等待状态):已确认一个中断实例,同时另一个中断实例正在等待处理。

每个外设中断可以是以下两种类型之一:

  • 边沿触发(Edge-triggered):
    • 这是一种在检测到中断信号上升沿时触发的中断,然后无论信号状态如何,都保持触发状态,直到满足本规范定义的条件来清除中断。
  • 电平触发(Level-sensitive):
    • 这是一种在中断信号电平处于活动状态时触发的中断,并且在电平不处于活动状态时取消触发。

39.2.3 中断号

在linux 内核中,我们使用IRQ number 和HW interrupt ID 两个ID 来标识一个来自外设的中断:

  • IRQ number:CPU 需要为每一个外设中断编号,我们称之IRQ Number。这个IRQ number是一个虚拟的interrupt ID,和硬件无关,仅仅是被CPU 用来标识一个外设中断。
  • HW interrupt ID:对于GIC 中断控制器而言,它收集了多个外设的interrupt request line 并向上传递,因此,GIC 中断控制器需要对外设中断进行编码。GIC 中断控制器用HW interrupt ID来标识外设的中断。如果只有一个GIC 中断控制器,那IRQ number 和HW interrupt ID 是可以一一对应的,如下图(图39-7)所示:
image-20240819114919339

但如果是在GIC 中断控制器级联的情况下,仅仅用HW interrupt ID 就不能唯一标识一个外设中断,还需要知道该HW interrupt ID 所属的GIC 中断控制器(HW interrupt ID 在不同的Interrupt controller 上是会重复编码的)。

image-20240819115015377

这样,CPU 和中断控制器在标识中断上就有了一些不同的概念,但是,对于驱动工程师而言,我们和CPU 视角是一样的,我们只希望得到一个IRQ number,而不关系具体是那个GIC中断控制器上的那个HW interrupt ID。这样一个好处是在中断相关的硬件发生变化的时候,驱动软件不需要修改。因此,linux kernel 中的中断子系统需要提供一个将HW interrupt ID 映射到IRQ number 上来的机制,也就是irq domain。

39.2.4 中断申请函数

(1)request_irq

request_irq 函数是在Linux 内核中用于注册中断处理程序的函数。它用于请求一个中断号(IRQ number)并将一个中断处理程序与该中断关联起来。下面是对request_irq 函数的详细介绍:

函数原型:
    int request_irq(unsigned int irq, irq_handler_t handler, unsigned long flags, const char *name, void *dev);
头文件:
    #include <linux/interrupt.h>
函数作用:
    request_irq 函数的主要功能是请求一个中断号,并将一个中断处理程序与该中断号关联起来。当中断事件发生时,与该中断号关联的中断处理程序会被调用执行。
    
参数含义:
    irq:要请求的中断号(IRQ number)。
    handler:指向中断处理程序的函数指针。
    flags:标志位,用于指定中断处理程序的行为和属性,如中断触发方式、中断共享等。
    name:中断的名称,用于标识该中断。
    dev:指向设备或数据结构的指针,可以在中断处理程序中使用。
返回值:
    成功:0 或正数,表示中断请求成功。
    失败:负数,表示中断请求失败,返回的负数值表示错误代码。

irq 参数用来指定要请求的中断号,中断号需要通过gpio_to_irq 函数映射GPIO 引脚来获得(gpio_to_irq 函数接下来会进行介绍)。

irq_handler_t handler 参数是一个函数指针,指向了中断处理程序的函数。中断处理程序是在中断事件发生时调用的函数,用于处理中断事件(关于中断处理程序会在下个小节进行详细的讲解)。

unsigned long flags:中断处理程序的标志位

这个参数用于指定中断处理程序的行为和属性,如中断触发方式、中断共享等。可以使用不同的标志位进行位运算来组合多个属性。常用的标志位包括:

IRQF_TRIGGER_NONE:		//无触发方式,表示中断不会被触发。
IRQF_TRIGGER_RISING:	//上升沿触发方式,表示中断在信号上升沿时触发。
IRQF_TRIGGER_FALLING:	//下降沿触发方式,表示中断在信号下降沿时触发。
IRQF_TRIGGER_HIGH:		//高电平触发方式,表示中断在信号为高电平时触发。
IRQF_TRIGGER_LOW:		//低电平触发方式,表示中断在信号为低电平时触发。
IRQF_SHARED:			//中断共享方式,表示中断可以被多个设备共享使用。

(2)gpio_to_irq

gpio_to_irq 函数用于将GPIO 引脚的编号(GPIO pin number)转换为对应的中断请求号(interrupt request number)。

函数原型:
    unsigned int gpio_to_irq(unsigned int gpio);
头文件:
    #include <linux/gpio.h>
函数功能:
    gpio_to_irq 是一个用于将GPIO 引脚映射到对应中断号的函数。它的作用是根据给定的GPIO 引脚号,获取与之关联的中断号。
    
参数说明:
    gpio:要映射的GPIO 引脚号。
返回值:
    成功:返回值为该GPIO 引脚所对应的中断号。
    失败:返回值为负数,表示映射失败或无效的GPIO 引脚号。

(3)free_irq

free_irq 函数用于释放之前通过request_irq 函数注册的中断处理程序。它的作用是取消对中断的注册并释放相关的系统资源。下面是关于该函数的详细解释:

函数原型:
    void free_irq(unsigned int irq, void *dev_id);
头文件:
    #include <linux/interrupt.h>
函数功能:
    free_irq 函数用于释放之前通过request_irq 函数注册的中断处理程序。它会取消对中断的注册并释放相关的系统资源,包括中断号、中断处理程序和设备标识等。
    
参数说明:
    irq:要释放的中断号。
    dev_id:设备标识,用于区分不同的中断请求。它通常是在request_irq 函数中传递的设备特定数据指针。
返回值:
    free_irq 函数没有返回值。

39.2.5 中断服务函数

中断处理程序是在中断事件发生时自动调用的函数。它负责处理与中断相关的操作,例如读取数据、清除中断标志、更新状态等。
irqreturn_t handler(int irq, void *dev_id) 是一个典型的中断服务函数的函数原型。下面对该函数原型及其参数进行详细解释:

函数原型:
    irqreturn_t handler(int irq, void *dev_id);
函数功能:
    handler 函数是一个中断服务函数,用于处理特定中断事件。它在中断事件发生时被操作系统或硬件调用,执行必要的操作来响应和处理中断请求。
    
参数说明:
    irq:表示中断号或中断源的标识符。它指示引发中断的硬件设备或中断控制器。
    dev_id:是一个void 类型的指针,用于传递设备特定的数据或标识符。它通常用于在中断处理程序中区分不同的设备或资源。
    
返回值:
    irqreturn_t 是一个特定类型的枚举值,用于表示中断服务函数的返回状态。它可以有以下几种取值:
        IRQ_NONE:表示中断服务函数未处理该中断,中断控制器可以继续处理其他中断请求。
        IRQ_HANDLED:表示中断服务函数已成功处理该中断,中断控制器无需进一步处理。
        IRQ_WAKE_THREAD:表示中断服务函数已处理该中断,并且请求唤醒一个内核线程来继续执行进一步的处理。这在一些需要长时间处理的中断情况下使用。

在处理程序中,通常需要注意以下几个方面:
(1)处理程序应该尽可能地快速执行,以避免中断丢失或过多占用CPU 时间。
(2)如果中断源是共享的,处理程序需要处理多个设备共享同一个中断的情况。
(3)处理程序可能需要与其他部分的代码进行同步,例如访问共享数据结构或使用同步机制来保护临界区域。
(4)处理程序可能需要与其他线程或进程进行通信,例如唤醒等待的线程或发送信号给其他进程。

39.3 实验程序编写

本实验对应的网盘路径为:iTOP-RK3568 开发板【底板V1.7 版本】\03_【iTOP-RK3568开发板】指南教程\02_Linux 驱动配套资料\04_Linux 驱动例程\30_interrupt\03_中断驱动例程

本实验将实现注册显示屏触摸中断,每按当触摸LCD 显示屏就会触发中断服务函数,在中断服务函数中会打印申请的GPIO 号和This is irq_handler。

iTOP-RK3568 有5 组GPIO bank:GPIO0GPIO4,每组又以A0A7, B0B7, C0C7, D0~D7 作为编号区分,常用以下公式计算引脚:

GPIO pin 脚计算公式:pin = bank * 32 + number 	//bank 为组号,number 为小组编号
GPIO 小组编号计算公式:number = group * 8 + X

LCD 触摸屏对应的中断引脚标号为TP_INT_L_GPIO3_A5,对应的计算过程如下所示:

bank = 3; 		//GPIO3_A5=> 3, bank ∈ [0,4]
group = 0; 		//GPIO3_A5 => 0, group ∈ {(A=0), (B=1), (C=2), (D=3)}
X = 5; 			//GPIO3_A5 => 5, X ∈ [0,7]
number = group * 8 + X = 0 * 8 + 5 =5
pin = bank*32 + number= 3 * 32 + 5 = 101;

得到中断引脚的引脚标号后,下面开始编写对应的驱动程序,编写完成的interrupt.c 如下所示:

#include <linux/module.h>
#include <linux/init.h>
#include <linux/gpio.h>
#include <linux/interrupt.h>

#define GPIO_PIN 101

// 中断处理函数
static irqreturn_t gpio_irq_handler(int irq, void *dev_id)
{
    printk(KERN_INFO "Interrupt occurred on GPIO %d\n", GPIO_PIN);
    printk(KERN_INFO "This is irq_handler\n");
    return IRQ_HANDLED;
}

static int __init interrupt_init(void)
{
    int irq_num;
    printk(KERN_INFO "Initializing GPIO Interrupt Driver\n");

    // 将GPIO引脚映射到中断号
    irq_num = gpio_to_irq(GPIO_PIN);
    printk(KERN_INFO "GPIO %d mapped to IRQ %d\n", GPIO_PIN, irq_num);

    // 请求中断
    if (request_irq(irq_num, gpio_irq_handler, IRQF_TRIGGER_RISING, "irq_test", NULL) != 0) {
        printk(KERN_ERR "Failed to request IRQ %d\n", irq_num);

        // 请求中断失败,释放GPIO引脚
        gpio_free(GPIO_PIN);
        return -ENODEV;
    }
    return 0;
}

static void __exit interrupt_exit(void)
{
    int irq_num = gpio_to_irq(GPIO_PIN);

    // 释放中断
    free_irq(irq_num, NULL);
    printk(KERN_INFO "GPIO Interrupt Driver exited successfully\n");
}

module_init(interrupt_init);
module_exit(interrupt_exit);

MODULE_LICENSE("GPL");
MODULE_AUTHOR("topeet");

39.4 运行测试

39.4.1 编译驱动程序

在上一小节中的timer_mod.c 代码同一目录下创建Makefile 文件,Makefile 文件内容:然后使用命令“make”进行驱动的编译,编译完生成interrupt.ko 目标文件,至此驱动模块就编译成功了。

39.4.2 运行测试

开发板启动之后,使用以下命令进行驱动模块的加载,如下图(图39-12)所示:

insmod interrupt.ko

image-20240819120402829

可以看到驱动加载之后,打印了“Initializing GPIO Interrupt Driver”表示程序加载成功了,在后面又打印了gpio 映射后的中断请求号为113,然后触摸LCD 屏,触发中断服务程序,打印如下图(图39-13)所示:

image-20240819120415152

成功打印了GPIO 的引脚编号以及“This is irq_handler”,证明编写的驱动程序没有问题,最后使用以下命令卸载相应的驱动,如下图(图39-14)所示:

rmmod interrupt.ko

image-20240819120435241

第40 章中断申请流程

在上一章中,我们简单的认识了一下中断以及中断子系统框架,最后编写了中断申请和中断服务函数的实验,大家会发现虽然前面讲解的只是点很多,但实际用起来只需要两三个函数就可以了,但中断的具体申请流程是怎样的呢,大家就不是很清楚了,在本章节将带领大家研究中断的申请流程。

40.1 request_irq 函数

中断申请使用的是request_irq 函数,它用于请求一个中断号(IRQ number)并将一个中断处理程序与该中断关联起来,它定义在内核源码的“/include/linux/interrupt.h”目录下,具体定义如下所示:

request_irq(unsigned int irq, irq_handler_t handler,unsigned long flags,const char *name, void *dev)
{
    return request_threaded_irq(irq, handler, NULL, flags, name, dev);
}

从上面的内容可以得到request_irq()函数实际上是调用了request_threaded_irq()函数来完成中断申请的过程。request_threaded_irq()函数提供了线程化的中断处理方式,可以在中断上下文中执行中断处理函数。

40.2 request_threaded_irq 函数

request_threaded_irq 函数是Linux 内核提供的一个功能强大的函数,用于请求分配一个中断,并将中断处理程序与该中断关联起来。该函数的主要作用是在系统中注册中断处理函数,以响应对应中断的发生。以下是request_threaded_irq 函数的功能和作用的详细介绍:

  • (1)中断请求:request_threaded_irq 函数用于请求一个中断。它会向内核注册对应中断号的中断处理函数,并为该中断分配必要的资源。中断号是标识特定硬件中断的唯一标识符。
  • (2)中断处理函数关联:通过handler 参数,将中断处理函数与中断号关联起来。中断处理函数是一个预定义的函数,用于处理中断事件。当中断发生时,内核将调用该函数来处理中断事件。
  • (3)线程化中断处理:request_threaded_irq 函数还支持使用线程化中断处理函数。通过指定thread_fn 参数,可以在一个内核线程上下文中异步执行较长时间的中断处理或延迟敏感的工作。这有助于避免在中断上下文中阻塞时间过长。
  • (4)中断属性设置:通过irqflags 参数,可以设置中断处理的各种属性和标志。例如,可以指定中断触发方式(上升沿、下降沿、边沿触发等)、中断类型(边沿触发中断、电平触发中断等)以及其他特定的中断行为。
  • (5)设备标识关联:通过dev_id 参数,可以将中断处理与特定设备关联起来。这样可以在中断处理函数中访问与设备相关的数据。设备标识符可以是指向设备结构体或其他与设备相关的数据的指针。
  • (6)错误处理:request_threaded_irq 函数会返回一个整数值,用于指示中断请求的结果。如果中断请求成功,返回值为0;如果中断请求失败,则返回一个负数错误代码,表示失败的原因。

request_threaded_irq 函数定义在内核源码目录下的“/kernel/irq/manage.c”文件中,具体内容如下所示:

int request_threaded_irq(unsigned int irq, irq_handler_t handler,irq_handler_t thread_fn, unsigned long irqflags,const char *devname, void *dev_id){
    struct irqaction *action; // 中断动作结构体指针
    struct irq_desc *desc; // 中断描述符指针
    int retval; // 返回值
    // 检查中断号是否为未连接状态
    if (irq == IRQ_NOTCONNECTED)
        return -ENOTCONN;
    
    // 检查中断标志的有效性
    if (((irqflags & IRQF_SHARED) && !dev_id) 
            ||(!(irqflags & IRQF_SHARED) && (irqflags & IRQF_COND_SUSPEND)) 
            ||((irqflags & IRQF_NO_SUSPEND) && (irqflags & IRQF_COND_SUSPEND)))
        return -EINVAL;
    
    // 根据中断号获取中断描述符
    desc = irq_to_desc(irq);
    if (!desc)
        return -EINVAL;
    
    // 检查中断设置是否可以进行中断请求,以及是否为每个CPU 分配唯一设备ID
    if (!irq_settings_can_request(desc) ||WARN_ON(irq_settings_is_per_cpu_devid(desc)))
        return -EINVAL;
    
    // 如果未指定中断处理函数,则使用默认的主处理函数
    if (!handler) {
        if (!thread_fn)
            return -EINVAL;
        handler = irq_default_primary_handler;
    }
    
    // 分配并初始化中断动作数据结构
    action = kzalloc(sizeof(struct irqaction), GFP_KERNEL);
    if (!action)
        return -ENOMEM;
    
    action->handler = handler; // 中断处理函数
    action->thread_fn = thread_fn; // 线程处理函数
    action->flags = irqflags; // 中断标志
    action->name = devname; // 设备名称
    action->dev_id = dev_id; // 设备ID
    
    // 获取中断的电源管理引用计数
    retval = irq_chip_pm_get(&desc->irq_data);
    if (retval < 0) {
        kfree(action);
        return retval;
    }
    
    // 设置中断并将中断动作与中断描述符关联
    retval = __setup_irq(irq, desc, action);
    
    // 处理中断设置失败的情况
    if (retval) {
        irq_chip_pm_put(&desc->irq_data);
        kfree(action->secondary);
        kfree(action);
    }
    
#ifdef CONFIG_DEBUG_SHIRQ_FIXME
    if (!retval && (irqflags & IRQF_SHARED)) {
        unsigned long flags;
        disable_irq(irq);
        local_irq_save(flags);
        handler(irq, dev_id);
        local_irq_restore(flags);
        enable_irq(irq);
    }
#endif
    return retval; // 返回设置中断的结果
}

(1)声明变量和初始化:

struct irqaction *action; // 中断动作结构体指针
struct irq_desc *desc; // 中断描述符指针
int retval; // 返回值

第5 行:用于存储中断动作结构体的指针(会在下面的小节进行详细的讲解)。
第6 行:用于存储中断描述符的指针(会在下面的小节进行详细的讲解)。
第7 行:用于存储函数的返回值。

(2)参数检查:

// 检查中断号是否为未连接状态
if (irq == IRQ_NOTCONNECTED)
    return -ENOTCONN;

// 检查中断标志的有效性
if (((irqflags & IRQF_SHARED) && !dev_id) ||
    (!(irqflags & IRQF_SHARED) && (irqflags & IRQF_COND_SUSPEND)) ||
    ((irqflags & IRQF_NO_SUSPEND) && (irqflags & IRQF_COND_SUSPEND)))
    return -EINVAL;

第10 行:检查中断号是否为未连接状态(IRQ_NOTCONNECTED)。
第14-17 行:检查中断标志的有效性,包括共享标志与设备ID 的关联性,条件挂起标志的有效性,以及无挂起标志与条件挂起标志的关联性。

(3)获取中断描述符:

// 根据中断号获取中断描述符
desc = irq_to_desc(irq);
if (!desc)
    return -EINVAL;

第20 行:根据中断号调用irq_to_desc 函数获取对应的中断描述符。
第21 行:如果获取中断描述符失败,则返回-EINVAL 表示无效的参数。

(4)检查中断设置:

// 检查中断设置是否可以进行中断请求,以及是否为每个CPU 分配唯一设备ID
if (!irq_settings_can_request(desc) ||
    WARN_ON(irq_settings_is_per_cpu_devid(desc)))
    return -EINVAL;

第25-26 行:检查中断设置是否可以进行中断请求,以及是否为每个CPU 分配唯一设备ID。如果中断设置不满足要求,则返回-EINVAL 表示无效的参数。
(5)处理中断处理函数和线程处理函数:

// 如果未指定中断处理函数,则使用默认的主处理函数
if (!handler) {
    if (!thread_fn)
        return -EINVAL;
    handler = irq_default_primary_handler;
}

如果未指定中断处理函数,则将默认的主处理函数(irq_default_primary_handler)赋值给handler

(6)分配并初始化中断动作数据结构:

// 分配并初始化中断动作数据结构
action = kzalloc(sizeof(struct irqaction), GFP_KERNEL);
if (!action)
    return -ENOMEM;
action->handler = handler; // 中断处理函数
action->thread_fn = thread_fn; // 线程处理函数
action->flags = irqflags; // 中断标志
action->name = devname; // 设备名称
action->dev_id = dev_id; // 设备ID

第37 行:调用kzalloc 函数分配内存空间,大小为sizeof(struct irqaction)。
第38 行:如果分配内存失败,则返回-ENOMEM 表示内存不足。
第41 行-第45 行:将中断处理函数、线程处理函数、中断标志、设备名称和设备ID 赋值给相应的字段。

(7)获取中断的电源管理引用计数:

// 获取中断的电源管理引用计数
retval = irq_chip_pm_get(&desc->irq_data);
if (retval < 0) {
    kfree(action);
    return retval;
}

第48 行:调用irq_chip_pm_get 函数获取中断的电源管理引用计数。
第49 行:如果获取失败,则释放先前分配的内存空间,并返回获取失败的结果。

(8)设置中断并关联中断动作:

// 设置中断并将中断动作与中断描述符关联
retval = __setup_irq(irq, desc, action);

第55 行:调用__setup_irq 函数设置中断并将中断动作与中断描述符关联。

(9)处理设置中断失败的情况:

// 处理中断设置失败的情况
if (retval) {
    irq_chip_pm_put(&desc->irq_data);
    kfree(action->secondary);
    kfree(action);
}

第59 行:调用irq_chip_pm_put 函数释放中断的电源管理引用计数。
第60 行:释放次要中断动作的内存空间。
第61 行:释放中断动作的内存空间。

(10)可选的共享中断处理:

#ifdef CONFIG_DEBUG_SHIRQ_FIXME
    if (!retval && (irqflags & IRQF_SHARED)) {
        unsigned long flags;
        
        disable_irq(irq);
        local_irq_save(flags);
        
        handler(irq, dev_id);
        
        local_irq_restore(flags);
        enable_irq(irq);
    }
#endif

第65 行:如果设置中断成功且中断标志中包含共享标志(IRQF_SHARED),则执行以下操作:
第68 行:禁用中断。
第69 行:保存当前中断状态并禁用本地中断。
第70 行:调用主处理函数处理中断。
第73 行:恢复中断状态。
第74 行):重新使能中断。

40.3 irq_desc 结构体

irq_desc 结构体是Linux 内核中用于描述中断的数据结构之一。每个硬件中断都有一个对应的irq_desc 实例,它用于记录与该中断相关的各种信息和状态。该结构体的主要功能是管理中断处理函数、中断行为以及与中断处理相关的其他数据。

以下是irq_desc 结构体的主要作用和功能:

  • (1)中断处理函数管理:irq_desc 结构体中的handle_irq 字段保存中断处理函数的指针。当硬件触发中断时,内核会调用该函数来处理中断事件。
  • (2)中断行为管理:irq_desc 结构体中的action 字段是一个指向中断行为列表的指针。中断行为是一组回调函数,用于注册、注销和处理与中断相关的事件。
  • (3)中断统计信息:irq_desc 结构体中的kstat_irqs 字段是一个指向中断统计信息的指针。该信息用于记录中断事件的发生次数和处理情况,可以帮助分析中断的性能和行为。
  • (4)中断数据管理:irq_desc 结构体中的irq_data 字段保存了与中断相关的数据,如中断号、中断类型等。这些数据用于识别和管理中断。
  • (5)通用中断数据管理:irq_desc 结构体中的irq_common_data 字段保存了与中断处理相关的通用数据,如中断控制器、中断屏蔽等。这些数据用于处理和控制中断的行为。
  • (6)中断状态管理:irq_desc 结构体中的其他字段用于管理中断的状态,如嵌套中断禁用计数、唤醒使能计数等。这些状态信息帮助内核跟踪和管理中断的状态变化。

通过使用irq_desc 结构体,内核可以有效地管理和处理系统中的硬件中断。它提供了一个统一的接口,用于注册和处理中断处理函数、管理中断行为,并提供了必要的信息和数据结构来监视和控制中断的行为和状态。

irq_desc 结构体定义在内核源码目录的“include/linux/irqdesc.h”文件,具体内容如下所示:

struct irq_desc {
    struct irq_common_data irq_common_data; 				/* 通用中断数据*/
    struct irq_data irq_data; 								/* 中断数据*/
    unsigned int __percpu *kstat_irqs; 						/* 中断统计信息*/
    irq_flow_handler_t handle_irq; 							/* 中断处理函数*/
    
    #ifdef CONFIG_IRQ_PREFLOW_FASTEOI
    	irq_preflow_handler_t preflow_handler; 				/* 预处理中断处理函数*/
    #endif
    
    struct irqaction *action; 								/* IRQ action list */
    unsigned int status_use_accessors;
    unsigned int core_internal_state__do_not_mess_with_it; 	  /* 内核内部状态标志位,请勿修改*/
    unsigned int depth; 									/* 嵌套中断禁用计数*/
    unsigned int wake_depth; /* 嵌套唤醒使能计数*/
    unsigned int tot_count;
    unsigned int irq_count; /* 用于检测损坏的IRQ 计数*/
    unsigned long last_unhandled; /* 未处理计数的老化计时器*/
    unsigned int irqs_unhandled; /* 未处理的中断计数*/
    atomic_t threads_handled; /* 处理中断的线程计数*/
    int threads_handled_last;
    raw_spinlock_t lock; /* 自旋锁*/
    struct cpumask *percpu_enabled; /* 指向每个CPU 的使能掩码*/
    const struct cpumask *percpu_affinity; /* 指向每个CPU 亲和性掩码*/
    
    #ifdef CONFIG_SMP
        const struct cpumask *affinity_hint; /* CPU 亲和性提示*/
        struct irq_affinity_notify *affinity_notify; /* CPU 亲和性变化通知*/
        #ifdef CONFIG_GENERIC_PENDING_IRQ
            cpumask_var_t pending_mask; /* 等待处理的中断掩码*/
        #endif
    #endif
    
    unsigned long threads_oneshot;
    atomic_t threads_active; /* 活动中的线程计数*/
    wait_queue_head_t wait_for_threads; /* 等待线程的等待队列头*/
    #ifdef CONFIG_PM_SLEEP
        unsigned int nr_actions;
        unsigned int no_suspend_depth;
        unsigned int cond_suspend_depth;
        unsigned int force_resume_depth;
    #endif
    
    #ifdef CONFIG_PROC_FS
        struct proc_dir_entry *dir; /* proc 文件系统目录项*/
    #endif
    
    #ifdef CONFIG_GENERIC_IRQ_DEBUGFS
        struct dentry *debugfs_file; /* 调试文件系统文件*/
        const char *dev_name; /* 设备名称*/
    #endif
    
    #ifdef CONFIG_SPARSE_IRQ
        struct rcu_head rcu;
        struct kobject kobj; /* 内核对象*/
    #endif
    struct mutex request_mutex; /* 请求互斥锁*/
    int parent_irq; /* 父中断号*/
    struct module *owner; /* 模块拥有者*/
    const char *name; /* 中断名称*/
} ____cacheline_internodealigned_in_smp;

在irq_desc 结构体中最重要的就是action 字段,会在下个小节对action 字段进行详细的讲解。

40.4 irqaction 结构体

irqaction 结构体是Linux 内核中用于描述中断行为的数据结构之一。它用于定义中断处理过程中的回调函数和相关属性。irqaction 结构体的主要功能是管理与特定中断相关的行为和处理函数。

以下是irqaction 结构体的主要作用和功能:

  • (1)中断处理函数管理:irqaction 结构体中的handler 字段保存中断处理函数的指针。该函数在中断发生时被调用,用于处理中断事件。
  • (2)中断处理标志管理:irqaction 结构体中的flags 字段用于指定中断处理的各种属性和标志。这些标志控制中断处理的行为,例如触发方式、中断类型等。
  • (3)设备标识符管理:irqaction 结构体中的dev_id 字段用于保存与中断处理相关的设备标识符。它可以是指向设备结构体或其他与设备相关的数据的指针,用于将中断处理与特定设备关联起来。
  • (4)中断行为链表管理:irqaction 结构体中的next 字段是一个指向下一个irqaction 结构体的指针,用于构建中断行为的链表。这样可以将多个中断处理函数链接在一起,以便在中断发生时按顺序调用它们。

通过使用irqaction 结构体,内核可以灵活地定义和管理与特定中断相关的行为和处理函数。它提供了一个统一的接口,用于注册和注销中断处理函数,并提供了必要的属性和数据结构来控制中断处理的行为和顺序。

irqaction 体定义在内核源码的“include/linux/interrupt.h”文件中如下所示:

struct irqaction {
    irq_handler_t handler; // 中断处理函数
    void *dev_id; // 设备ID
    void __percpu *percpu_dev_id; // 每个CPU 的设备ID
    struct irqaction *next; // 下一个中断动作结构体
    irq_handler_t thread_fn; // 线程处理函数
    struct task_struct *thread; // 线程结构体指针
    struct irqaction *secondary; // 次要中断动作结构体
    unsigned int irq; // 中断号
    unsigned int flags; // 中断标志
    unsigned long thread_flags; // 线程标志
    unsigned long thread_mask; // 线程掩码
    const char *name; // 设备名称
    struct proc_dir_entry *dir; // proc 文件系统目录项指针
} ____cacheline_internodealigned_in_smp;

第41 章中断下文tasklet 实验

在上一个章节中,我们申请GPIO 中断,使用的是request_irq,但是request_irq 绑定的中断服务程序指的是中断上文。在之前的中断视频中讲解了:中断分为俩个部分——中断上文和中断下文。本章节我们来学习中断下文的一种实现方式——tasklet

41.1 什么是tasklet

在Linux 内核中,**tasklet 是一种特殊的软中断机制,被广泛用于处理中断下文相关的任务。它是一种常见且有效的方法,在多核处理系统上可以避免并发问题。Tasklet 绑定的函数在同一时间只能在一个CPU 上运行,因此不会出现并发冲突。然而,需要注意的是,tasklet 绑定的函数中不能调用可能导致休眠的函数,否则可能引起内核异常。**

在Linux 内核中,tasklet 结构体的定义位于include/linux/interrupt.h 头文件中。其原型如下:

struct tasklet_struct {
    struct tasklet_struct *next;
    unsigned long state;
    atomic_t count;
    void (*func)(unsigned long);
    unsigned long data;
};
typedef struct tasklet_struct tasklet_t;

tasklet_struct 结构体包含以下成员:

  • next:指向下一个tasklet 的指针,用于形成链表结构,以便内核中可以同时管理多个tasklet。
  • state:表示tasklet 的当前状态。
  • count:用于引用计数,用于确保tasklet 在多个地方调度或取消调度时的正确处理。
  • func:指向tasklet 绑定的函数的指针,该函数将在tasklet 执行时被调用。
  • data:传递给tasklet 绑定函数的参数

此外,为了方便,还定义了tasklet_t 类型作为struct tasklet_struct 的别名。这样我们可以使用tasklet_t 来声明tasklet 变量,而不是直接使用struct tasklet_struct

41.2 tasklet 相关接口函数

41.2.1 静态初始化函数

在Linux 内核中,有一个用于静态初始化tasklet 的宏函数:DECLARE_TASKLET。这个宏函数可以帮助我们更方便地进行tasklet 的静态初始化。
宏函数的原型如下:

#define DECLARE_TASKLET(name,func,data) \
struct tasklet_struct name = { NULL,0,ATOMIC_INIT(0),func,data}

其中,name 是tasklet 的名称,func 是tasklet 的处理函数,data 是传递给处理函数的参数。初始化状态为使能状态。

如果tasklet 初始化函数为非使能状态,使用以下宏定义:

#define DECLARE_TASKLET_DISABLED(name,func,data) \
struct tasklet_struct name = { NULL,0,ATOMIC_INIT(1),func,data}

其中,name 是tasklet 的名称,func 是tasklet 的处理函数,data 是传递给处理函数的参数。初始化状态为非使能状态。
下面是一个示例,展示了如何使用DECLARE_TASKLET 宏函数进行tasklet 的静态初始化:

#include <linux/interrupt.h>
// 定义tasklet 处理函数
void my_tasklet_handler(unsigned long data)
{
    // Tasklet 处理逻辑
    // ...
}

// 静态初始化tasklet
DECLARE_TASKLET(my_tasklet, my_tasklet_handler, 0);

// 驱动程序的其他代码

在上述示例中,my_tasklettasklet 的名称,my_tasklet_handlertasklet 的处理函数,0是传递给处理函数的参数。但是需要注意的是,使用DECLARE_TASKLET 静态初始化的tasklet无法在运行时动态销毁,因此在不需要tasklet 时,应该避免使用此方法。如果需要在运行时销毁tasklet,应使用tasklet_inittasklet_kill 函数进行动态初始化和销毁,接下来我们来学习动态初始化函数。

41.2.2 动态初始化函数tasklet_init

在Linux 内核中,可以使用tasklet_init 函数对tasklet 进行动态初始化。该函数原型为:

void tasklet_init(struct tasklet_struct *t, void (*func)(unsigned long), unsigned long data);

其中,t 是指向tasklet 结构体的指针,func 是tasklet 的处理函数,data 是传递给处理函数的参数

以下是一个示例,tasklet_init 函数进行动态初始化如下所示:

#include <linux/interrupt.h>
// 定义tasklet 处理函数
void my_tasklet_handler(unsigned long data)
{
    // Tasklet 处理逻辑
    // ...
}

// 声明tasklet 结构体
static struct tasklet_struct my_tasklet;

// 初始化tasklet
tasklet_init(&my_tasklet, my_tasklet_handler, 0);
// 驱动程序的其他代码

在示例中,我们首先定义了my_tasklet_handler 作为tasklet 的处理函数。然后,声明了一个名为my_tasklettasklet 结构体。接下来,通过调用tasklet_init 函数,进行动态初始化。

通过使用tasklet_init 函数,我们可以在运行时动态创建和初始化tasklet。这样,我们可以根据需要灵活地管理和控制tasklet 的生命周期。在不再需要tasklet 时,可以使用tasklet_kill函数进行销毁,以释放相关资源。

41.2.3 关闭函数tasklet_disabled

在Linux 内核中,可以使用tasklet_disabled 函数来关闭一个已经初始化的tasklet。该函数的原型如下:

void tasklet_disable(struct tasklet_struct *t);

其中,t 是指向tasklet 结构体的指针。
以下是一个示例,使用tasklet_disable 函数来关闭tasklet。

#include <linux/interrupt.h>
// 定义tasklet 处理函数
void my_tasklet_handler(unsigned long data)
{
    // Tasklet 处理逻辑
    // ...
}
// 声明tasklet 结构体
static struct tasklet_struct my_tasklet;

// 初始化tasklet
tasklet_init(&my_tasklet, my_tasklet_handler, 0);

// 关闭tasklet
tasklet_disable(&my_tasklet);
// 驱动程序的其他代码

在上述示例中,我们首先定义了my_tasklet_handler 作为tasklet 的处理函数。然后,声明了一个名为my_tasklet 的tasklet 结构体,并使用tasklet_init 函数对其进行初始化。最后,通过调用tasklet_disable 函数,我们关闭了my_tasklet。

关闭tasklet 后,即使调用tasklet_schedule 函数触发tasklet,tasklet 的处理函数也不会再被执行。这可以用于临时暂停或停止tasklet 的执行,直到再次启用(通过调用tasklet_enable函数)。

需要注意的是, 关闭tasklet 并不会销毁tasklet 结构体, 因此可以随时通过调用tasklet_enable 函数重新启用tasklet,或者调用tasklet_kill 函数来销毁tasklet

41.2.4 使能函数

在Linux 内核中,可以使用tasklet_enable 函数来使能(启用)一个已经初始化的tasklet。该函数的原型如下:

void tasklet_enable(struct tasklet_struct *t);

其中,t 是指向tasklet 结构体的指针。
以下是一个示例,展示如何使用tasklet_enable 函数来使能tasklet:

#include <linux/interrupt.h>
// 定义tasklet 处理函数
void my_tasklet_handler(unsigned long data)
{
// Tasklet 处理逻辑
// ...
}
// 声明tasklet 结构体
static struct tasklet_struct my_tasklet;
// 初始化tasklet
tasklet_init(&my_tasklet, my_tasklet_handler, 0);
// 使能tasklet
tasklet_enable(&my_tasklet);
// 驱动程序的其他代码

在上述示例中,我们首先定义了my_tasklet_handler 作为tasklet 的处理函数。然后,声明了一个名为my_tasklet 的tasklet 结构体,并使用tasklet_init 函数对其进行初始化。最后,通过调用tasklet_enable 函数,我们使能(启用)了my_tasklet。

使能tasklet 后,如果调用tasklet_schedule 函数触发tasklet,则tasklet 的处理函数将会被执行。这样,tasklet 将开始按计划执行其处理逻辑。

需要注意的是,使能tasklet 并不会自动触发tasklet 的执行,而是通过调用tasklet_schedule函数来触发。同时,可以使用tasklet_disable 函数来临时暂停或停止tasklet 的执行。如果需要永久停止tasklet 的执行并释放相关资源,则应调用tasklet_kill 函数来销毁tasklet。

41.2.5 调度函数tasklet_schedule

在Linux 内核中,可以使用tasklet_schedule 函数来调度(触发)一个已经初始化的tasklet执行。该函数的原型如下:

void tasklet_schedule(struct tasklet_struct *t);

其中,t 是指向tasklet 结构体的指针。
以下是一个示例,展示如何使用tasklet_schedule 函数来调度tasklet 执行:

#include <linux/interrupt.h>
// 定义tasklet 处理函数
void my_tasklet_handler(unsigned long data)
{
    // Tasklet 处理逻辑
    // ...
}
// 声明tasklet 结构体
static struct tasklet_struct my_tasklet;

// 初始化tasklet
tasklet_init(&my_tasklet, my_tasklet_handler, 0);

// 调度tasklet 执行
tasklet_schedule(&my_tasklet);

// 驱动程序的其他代码

在上述示例中,我们首先定义了my_tasklet_handler 作为tasklet 的处理函数。然后,声明了一个名为my_tasklet 的tasklet 结构体,并使用tasklet_init 函数对其进行初始化。最后,通过调用tasklet_schedule 函数,我们调度(触发)了my_tasklet 的执行。
需要注意的是,调度tasklet 只是将tasklet 标记为需要执行,并不会立即执行tasklet 的处理函数。实际的执行时间取决于内核的调度和处理机制。

41.2.6 销毁函数tasklet_kill

在Linux 内核中,可以使用tasklet_kill 函数来销毁一个已经初始化的tasklet,释放相关资源。该函数的原型如下:

void tasklet_kill(struct tasklet_struct *t);

其中,t 是指向tasklet 结构体的指针。
以下是一个示例,展示如何使用tasklet_kill 函数来销毁tasklet:

#include <linux/interrupt.h>
// 定义tasklet 处理函数
void my_tasklet_handler(unsigned long data)
{
    // Tasklet 处理逻辑
    // ...
}
// 声明tasklet 结构体
static struct tasklet_struct my_tasklet;

// 初始化tasklet
tasklet_init(&my_tasklet, my_tasklet_handler, 0);
tasklet_disable(&my_tasklet);

// 销毁tasklet
tasklet_kill(&my_tasklet);
// 驱动程序的其他代码

在上述示例中,我们首先定义了my_tasklet_handler 作为tasklet 的处理函数。然后,声明了一个名为my_tasklet 的tasklet 结构体,并使用tasklet_init 函数对其进行初始化。最后,通过调用tasklet_kill 函数,我们销毁了my_tasklet。

调用tasklet_kill 函数会释放tasklet 所占用的资源,并将tasklet 标记为无效。因此,销毁后的tasklet 不能再被使用。

需要注意的是, 在销毁tasklet 之前, 应该确保该tasklet 已经被停止( 通过调用tasklet_disable 函数)。否则,销毁一个正在执行的tasklet 可能导致内核崩溃或其他错误。

一旦销毁了tasklet,如果需要再次使用tasklet,需要重新进行初始化(通过调用tasklet_init函数)。在下一小节中我们将使用上述tasklet 函数相关接口函数进行相应的实验。

41.3 实验程序的编写

41.3.1 驱动程序编写

本实验对应的网盘路径为:iTOP-RK3568 开发板【底板V1.7 版本】\03_【iTOP-RK3568开发板】指南教程\02_Linux 驱动配套资料\04_Linux 驱动例程\32_tasklet\module
本实验将实现注册显示屏触摸中断,每按当触摸LCD 显示屏就会触发中断服务函数,在中断服务函数中调度中断下文tasklet 处理函数,打印“This id test_interrupt”和“data is 1”。

在驱动程序中的模块初始化函数中,我们将GPIO 转换为中断号,并使用request_irq 函数请求中断,然后对tasklet 进行初始化。在中断处理函数中,我们调度tasklet 执行,使得当中断触发时,tasklet 会被调度执行。在模块退出函数中,我们释放中断资源,并使能tasklet 销毁tasklet。

编写完成的interrupt.c 代码如下所示,添加的代码已加粗表示。

#include <linux/module.h>
#include <linux/init.h>
#include <linux/interrupt.h>
#include <linux/gpio.h>
// #include <linux/delay.h>

int irq;
struct tasklet_struct mytasklet;

// 定义tasklet处理函数
void mytasklet_func(unsigned long data)
{
  printk("data is %ld\n", data);
  // msleep(3000);
}

// 中断处理函数
irqreturn_t test_interrupt(int irq, void *args)
{
  printk("This is test_interrupt\n");
  tasklet_schedule(&mytasklet); // 调度tasklet执行
  return IRQ_RETVAL(IRQ_HANDLED);
}

// 模块初始化函数
static int interrupt_irq_init(void)
{
  int ret;
  irq = gpio_to_irq(101); // 将GPIO转换为中断号
  printk("irq is %d\n", irq);

  // 请求中断
  ret = request_irq(irq, test_interrupt, IRQF_TRIGGER_RISING, "test", NULL);
  if (ret < 0)
  {
    printk("request_irq is error\n");
    return -1;
  }
  // 初始化tasklet
  tasklet_init(&mytasklet, mytasklet_func, 1);
  return 0;
}

// 模块退出函数
static void interrupt_irq_exit(void)
{
  free_irq(irq, NULL);
  tasklet_enable(&mytasklet); // 使能tasklet(可选)
  tasklet_kill(&mytasklet);   // 销毁tasklet
  printk("bye bye\n");
}

module_init(interrupt_irq_init); // 指定模块的初始化函数
module_exit(interrupt_irq_exit); // 指定模块的退出函数

MODULE_LICENSE("GPL");   // 模块使用的许可证
MODULE_AUTHOR("topeet"); // 模块的作者

41.4 运行测试

41.4.1 编译驱动程序

在上一小节中的interrupt.c 代码同一目录下创建Makefile 文件,Makefile 文件内容:然后使用命令“make”进行驱动的编译,编译完生成interrupt.ko 目标文件,至此驱动模块就编译成功了。

41.4.2 运行测试

开发板启动之后,使用以下命令进行驱动模块的加载,如下图(图41-4)所示:

insmod interrupt.ko

image-20240820140224352

看到驱动加载之后,可以看到申请的中断号(113)被打印了出来,然后用手触摸连接的LVDS 7 寸屏幕,触发中断服务程序,打印如下图(41-5)所示:

image-20240820140244384

在上图中,可以看到打印中断处理函数中添加的打印“This is test_interrupt”和tasklet 处理函数中添加的打印“data is 1”,说明成功执行了中断下文tasklet 处理函数。
最后可以使用以下命令进行驱动的卸载,如下图(图图41-6)所示:

rmmod interrupt
image-20240820140326878

之前的理论章节我们强调说tasklet 函数中不能调用休眠的函数,在此我们在上述驱动实验的基础上实验一下,驱动文件中添加休眠函数,如下(图41-7)所示:

image-20240820140347597

同理,进行编译驱动模块,卸载掉之前的驱动模块后,加载新编译的驱动模块,如下图(图41-8)所示:

image-20240820140400935

然后用手触摸连接的LVDS 7 寸屏幕,打印如下图(41-9)所示,内核会崩溃。

image-20240820140428385

至此,中断下文tasklet 实验就完成了。

第42 章软中断实验

在上个章节我们学习了中断下文的一种实验方式——tasklet,本章节我们来学习中断下文的另一种实现方式——软中断。软中断的资料有限,对应的中断号不多,一般用在网络设备驱动,块设备驱动当中。这时本章节要学习的软中断就闪耀登场了。

42.1 什么是软中断

打开Linux 源码linux_sdk/kernel/include/linux/interrupt.h 文件,如下所示:

enum
{
    HI_SOFTIRQ=0,
    TIMER_SOFTIRQ,
    NET_TX_SOFTIRQ,
    NET_RX_SOFTIRQ,
    BLOCK_SOFTIRQ,
    IRQ_POLL_SOFTIRQ,
    TASKLET_SOFTIRQ,
    SCHED_SOFTIRQ,
    HRTIMER_SOFTIRQ, /* Unused, but kept as tools rely on the numbering. Sigh! */
    RCU_SOFTIRQ, /* Preferable RCU should always be the last softirq */
    NR_SOFTIRQS
};

以上代码定义了一个枚举类型,用于标识软中断的不同类型或优先级。每个枚举常量对应一个特定的软中断类型。
以下是每个枚举常量的含义:

HI_SOFTIRQ:高优先级软中断
TIMER_SOFTIRQ:定时器软中断
NET_TX_SOFTIRQ:网络传输发送软中断
NET_RX_SOFTIRQ:网络传输接收软中断
BLOCK_SOFTIRQ:块设备软中断
IRQ_POLL_SOFTIRQ:中断轮询软中断
TASKLET_SOFTIRQ:任务软中断
SCHED_SOFTIRQ:调度软中断
HRTIMER_SOFTIRQ, /* Unused, but kept as tools rely on the numbering. Sigh! */
RCU_SOFTIRQ, /* Preferable RCU should always be the last softirq */
NR_SOFTIRQS:表示软中断的总数,用于指示软中断类型的数据

中断号的优先级越小,代表优先级越高。在驱动代码中,我们可以使用Linux 驱动代码中上述的软中断,当然我们也可以自己添加软中断。我们添加一个自定义的软中断,如下所示,TEST_SOFTIRQ 为自定义添加的软中断。

enum
{
HI_SOFTIRQ=0,
TIMER_SOFTIRQ,
NET_TX_SOFTIRQ,
NET_RX_SOFTIRQ,
BLOCK_SOFTIRQ,
IRQ_POLL_SOFTIRQ,
TASKLET_SOFTIRQ,
SCHED_SOFTIRQ,
HRTIMER_SOFTIRQ, /* Unused, but kept as tools rely on the
numbering. Sigh! */
RCU_SOFTIRQ, /* Preferable RCU should always be the last softirq */
TEST_SOFTIRQ, //添加的自定义软中断
NR_SOFTIRQS
};

这里要注意:尽管我们添加一个自定义的软中断非常简单,但是Linux 内核的开发者并不希望我们这样去做,如果我们要用软中断,建议使用tasklet。虽然Linux 内核开发者不建议自定义软中断,但是我们抱着学习的态度,了解学习下软中断还是很有必要的。上述修改之后,重新编译内核源码,接下来我们来学习下软中断的使用方法。

42.2 软中断接口函数

软中断的接口函数非常简单,介绍如下所示:

1 注册软中断,使用open_softirq 函数,函数原型如下所示:

void open_softirq(int nr,void (*action)(struct softirq_action *));

函数的参数如下所示:
nr: 软中断的编号或优先级。它是一个整数,表示要注册的软中断的标识符。
action: 指向一个函数的指针,这个函数将作为软中断的处理程序。该函数接受一个structsoftirq_action 类型的参数。

2 触发软中断,使用raise_softirq 函数,函数原型如下所示:

void raise_softirq(unsigned int nr);

函数的参数如下所示:
nr: 软中断的编号或优先级。它是一个整数,表示要注册的软中断的标识符。

3 在禁用硬件中断的情况下,触发软中断使用raise_softirq_irqoff 函数,函数原型如下所示:

void raise_softirq_irqoff(unsigned int nr);

函数的参数如下所示:
nr: 软中断的编号或优先级。它是一个整数,表示要注册的软中断的标识符。

在下一小节中将使用上述软中断API 进行相应的实验。

42.3 实验程序的编写

42.3.1 驱动程序编写

本实验对应的网盘路径为:iTOP-RK3568 开发板【底板V1.7 版本】\03_【iTOP-RK3568开发板】指南教程\02_Linux 驱动配套资料\04_Linux 驱动例程\33_softirq\module。
本实验将实现注册显示屏触摸中断,每按当触摸LCD 显示屏就会触发中断服务函数,在中断服务函数中触发软中断,打印“This id test_interrupt”和“This is testsoft_func”。

在驱动程序中的模块初始化函数中,我们将GPIO 转换为中断号,并使用request_irq 函数请求中断,然后注册软中断函数。在中断处理函数中,我们触发软中断,使得当中断触发时,软中断处理函数会被调度执行。

接下来我们编写驱动代码,使用软中断来实现中断的下半部分。编写完成的interrupt.c 代码如下所示:

#include <linux/module.h>
#include <linux/init.h>
#include <linux/interrupt.h>
#include <linux/gpio.h>
// #include <linux/delay.h>

int irq;

// 软中断处理程序
void testsoft_func(struct softirq_action *softirq_action)
{
  printk("This is testsoft_func\n");
}

irqreturn_t test_interrupt(int irq, void *args)
{
  printk("This is test_interrupt\n");
  raise_softirq(TEST_SOFTIRQ); // 触发软中断
  return IRQ_RETVAL(IRQ_HANDLED);
}

static int interrupt_irq_init(void)
{
  int ret;
  irq = gpio_to_irq(101); // 将GPIO映射为中断号
  printk("irq is %d\n", irq);
  // 请求中断
  ret = request_irq(irq, test_interrupt, IRQF_TRIGGER_RISING, "test", NULL);
  if (ret < 0)
  {
    printk("request_irq is error\n");
    return -1;
  }
  // 注册软中断处理函数
  open_softirq(TEST_SOFTIRQ, testsoft_func);
  return 0;
}

static void interrupt_irq_exit(void)
{
  free_irq(irq, NULL); // 释放中断
  printk("bye bye\n");
}

module_init(interrupt_irq_init);
module_exit(interrupt_irq_exit);

MODULE_LICENSE("GPL");
MODULE_AUTHOR("topeet");

42.4 运行测试

42.4.1 编译驱动程序

在上一小节中的interrupt.c 代码同一目录下创建Makefile 文件,Makefile 文件内容:然后使用命令“make”进行驱动的编译,

image-20240820141313458

上图中提示open_softirqraise_softirq 没有被定义,但是为什么还会提示这样的错误呢?
这是因为Linux 内核开发者不希望驱动工程师擅自在枚举类型中添加软中断。我们将这俩个函数导出到符号表,修改linux_sdk/kernel/kernel/softirq.c,修改内容如下(图42-3)所示:

image-20240820141344490

修改完成后,重新编译内核源码,编译源码通过后,再次编译驱动模块,如下图(图42-4)所示:

image-20240820141400543

编译完生成interrupt.ko 目标文件,如下图(图42-5)所示:

image-20240820141415053

42.4.2 运行测试

开发板启动之后,使用以下命令进行驱动模块的加载,如下图(图42-6)所示:

insmod interrupt.ko

image-20240820141443168

驱动加载成功之后,可以看到申请的中断号被打印了出来,然后用手触摸连接的LVDS 7寸屏幕,触发中断服务程序,打印如下图(42-7)所示:

image-20240820141455888

在上图中,可以看到打印中断处理函数中添加的打印“This is test_interrupt”和软中断处理函数中添加的打印“This is testsoft_func”
最后可以使用以下命令进行驱动的卸载,如下图(图42-8)所示:

rmmod interrupt

image-20240820141519847

至此,软中断实验就完成了。

第43 章特殊的软中断tasklet 分析实验

Tasklet 是Linux 内核中的一种软中断机制,它可以被看作是一种轻量级的延迟处理机制。它是通过软中断控制结构来实现的,因此也被称为软中断。本章节我们来从代码层面分析一下为什么tasklet 是一个特殊的软中断呢?

软中断处理函数的定义内核源码kernel/kernel/softirq.c 文件中,如下所示:

void __init softirq_init(void)
{
    int cpu;
    // 初始化每个可能的CPU 的tasklet_vec 和tasklet_hi_vec
    // 将tail 指针设置为对应的head 指针的初始位置
    for_each_possible_cpu(cpu) {
        per_cpu(tasklet_vec, cpu).tail =&per_cpu(tasklet_vec, cpu).head;
        per_cpu(tasklet_hi_vec, cpu).tail =&per_cpu(tasklet_hi_vec, cpu).head;
    }
    // 注册TASKLET_SOFTIRQ 软中断,并指定对应的处理函数为tasklet_action
    open_softirq(TASKLET_SOFTIRQ, tasklet_action);
    
    // 注册HI_SOFTIRQ 软中断,并指定对应的处理函数为tasklet_hi_action
    open_softirq(HI_SOFTIRQ, tasklet_hi_action);
}

下面开始对上述代码详细解释:

  • for_each_possible_cpu(cpu):遍历每个可能的CPU。在多核系统中,此循环用于初始化每个CPU 的tasklet_vectasklet_hi_vec
  • per_cpu(tasklet_vec, cpu).tail = &per_cpu(tasklet_vec, cpu).head;:将每个CPU 的tasklet_vectail 指针设置为对应的head 指针的初始位置。这样做是为了确保tasklet_vec 的初始状态是空的。
  • per_cpu(tasklet_hi_vec, cpu).tail = &per_cpu(tasklet_hi_vec, cpu).head; : 将每个CPU 的tasklet_hi_vec 的tail 指针设置为对应的head 指针的初始位置。这样做是为了确保tasklet_hi_vec 的初始状态是空的。
  • open_softirq(TASKLET_SOFTIRQ, tasklet_action);:注册TASKLET_SOFTIRQ 软中断,并指定对应的处理函数为tasklet_action。这样,在TASKLET_SOFTIRQ 被触发时,将会调用tasklet_action函数来处理相应的任务。
  • open_softirq(HI_SOFTIRQ, tasklet_hi_action);:注册HI_SOFTIRQ 软中断,并指定对应的处理函数为tasklet_hi_action。这样,在HI_SOFTIRQ 被触发时,将会调用tasklet_hi_action 函数来处理相应的任务。

在执行__init softirq_init 函数时,会触发TASKLET_SOFTIRQ,然后会调用tasklet_action 函数,tasklet_action 函数如下所示:

static __latent_entropy void tasklet_action(struct softirq_action *a)
{
    tasklet_action_common(a, this_cpu_ptr(&tasklet_vec), TASKLET_SOFTIRQ);
}

上述函数中调用了tasklet_action_common 函数,如下所示:

static __latent_entropy void tasklet_action(struct softirq_action *a)
{
    tasklet_action_common(a, this_cpu_ptr(&tasklet_vec), TASKLET_SOFTIRQ);
}

tasklet_action_common 函数,如下所示:

static void tasklet_action_common(struct softirq_action *a,
                                    struct tasklet_head *tl_head,
                                    unsigned int softirq_nr)
{
    struct tasklet_struct *list;
    // 禁用本地中断
    local_irq_disable();
    // 获取tasklet_head 中的任务链表
    list = tl_head->head;
    // 清空tasklet_head 中的任务链表
    tl_head->head = NULL;
    // 将tail 指针重新指向head 指针的位置
    tl_head->tail = &tl_head->head;
    // 启用本地中断
    local_irq_enable();
    
    // 遍历任务链表,处理每一个tasklet
    while (list) {
        struct tasklet_struct *t = list;
        // 获取下一个tasklet,并更新链表
        list = list->next;
        if (tasklet_trylock(t)) { // 尝试获取tasklet 的锁
            if (!atomic_read(&t->count)) { // 检查count 计数器是否为0
                if (!test_and_clear_bit(TASKLET_STATE_SCHED,&t->state))
                    BUG(); // 如果state 标志位不正确,则发生错误
                t->func(t->data); // 执行tasklet 的处理函数
                tasklet_unlock(t); // 解锁tasklet
                continue;
            }
            tasklet_unlock(t); // 解锁tasklet
        }
        
        // 禁用本地中断
        local_irq_disable();
        // 将当前tasklet 添加到tasklet_head 的尾部
        t->next = NULL;
        *tl_head->tail = t;
        // 更新tail 指针
        tl_head->tail = &t->next;
        // 触发软中断
        __raise_softirq_irqoff(softirq_nr);
        // 启用本地中断
        local_irq_enable();
    }
}

在上面的代码中,tasklet_action_common()函数对任务链表中的每个tasklet 进行处理。它首先禁用本地中断,获取任务链表头指针,清空任务链表,并重新设置尾指针。然后它循环遍历任务链表,对每个tasklet 进行处理。如果tasklet 的锁获取成功,并且计数器为0,它将执行tasklet 的处理函数,并清除状态标志位。如果锁获取失败或计数不为0,它将tasklet 添加到任务链表的尾部,并触发指定的软中断。最后,它启用本地中断,完成任务处理过程。

那么tasklet 在什么时候加到链表里面的呢?tasklet 是通过__tasklet_schedule_common()函数加入到链表中的。如下所示:

static void __tasklet_schedule_common(struct tasklet_struct *t,
                                        struct tasklet_head __percpu *headp,
                                        unsigned int softirq_nr)
{
    struct tasklet_head *head;
    unsigned long flags;
    // 保存当前中断状态,并禁用本地中断
    local_irq_save(flags);
    // 获取当前CPU 的tasklet_head 指针
    head = this_cpu_ptr(headp);
    // 将当前tasklet 添加到tasklet_head 的尾部
    t->next = NULL;
    *head->tail = t;
    // 更新tasklet_head 的尾指针
    head->tail = &(t->next);
    // 触发指定的软中断
    raise_softirq_irqoff(softirq_nr);
    // 恢复中断状态
    local_irq_restore(flags);
}

通过上述代码,__tasklet_schedule_common()函数将tasklet 成功添加到链表的末尾。当软中断被触发时,系统会遍历链表并处理每个tasklet。因此,在添加到链表后,tasklet将在适当的时机被系统调度和执行。

经过上述分析,所以说**tasklet 是一个特殊的软中断。**

内核开发者不希望我们去添加软中断的软中断号,更希望我们使用tasklet。那么tasklet相比自己添加软中断有哪些优点和缺点呢?

使用Tasklet 相比自己添加软中断有一些优点和缺点。以下是它们的比较:

优点:

  1. 简化的接口和编程模型:Tasklet 提供了一个简单的接口和编程模型,使得在内核中处理延迟工作变得更加容易。相比自己添加软中断,Tasklet 提供了更高级的抽象。
  2. 低延迟:Tasklet 在软中断上下文中执行,避免了内核线程的上下文切换开销,因此具有较低的延迟。这对于需要快速响应的延迟敏感任务非常重要。
  3. 自适应调度:Tasklet 具有自适应调度的特性,当多个Tasklet 处于等待状态时,内核会合并它们以减少不必要的上下文切换。这种调度机制可以提高系统的效率。

缺点:

  1. 无法处理长时间运行的任务:Tasklet 适用于短时间运行的延迟工作,如果需要处理长时间运行的任务,可能会阻塞其他任务的执行。对于较长的操作,可能需要使用工作队列或内核线程来处理。
  2. 缺乏灵活性:Tasklet 的执行受限于软中断的上下文,不适用于所有类型的延迟工作。某些情况下,可能需要更灵活的调度和执行机制,这时自定义软中断可能更加适合。
  3. 资源限制:Tasklet 的数量是有限的,系统中可用的Tasklet 数量取决于架构和内核配置。如果需要大量的延迟工作处理,可能会受到Tasklet 数量的限制。

综上所述,**Tasklet 提供了一种简单且低延迟的延迟工作处理机制,适用于短时间运行的任务和对响应时间敏感的场景**。然而,对于长时间运行的任务和需要更灵活调度的情况,自定义软中断可能更合适。在选择使用Tasklet 还是自定义软中断时,需要根据具体的需求和系统特性进行权衡和决策。

第44 章共享工作队列实验

在上个章节我们学习了中断下文的一种实验方式——软中断,本章节我们来学习中断下文的另一种实现方式——工作队列。工作队列是操作系统中管理和调度异步任务执行的一种机制,接下来开始学习工作队列吧。

44.1 什么是工作队列

工作队列是实现中断下半部分的机制之一,是一种用于管理任务的数据结构或机制。它通常用于多线程,多进程或分布式系统中,用于协调和分配待处理的任务给可用的工作线程或工作进程

工作队列的基本原理是将需要执行的任务按顺序排列在队列中,并提供一组工作线程或者工作进程来处理队列中的任务。当有新的任务到达时,它们会被添加到队列的末尾,工作线程或工作进程从队列的头部获取任务,并执行相应的处理操作。

工作队列和之前学习的tasklet 有什么不同呢?tasklet 也是实现中断下半部分的机制之一。他们最主要的区别是tasklet 不能休眠,而工作队列是可以休眠的,所以tasklet 可以用来处理比较耗时间的事情,而工作队列可以处理更耗时间的事情。

工作队列将工作推后以后,会交给内核线程去执行。Linux 在启动过程中会创建一个工作者内核线程,这个线程创建以后处于sleep 状态。当有工作需要处理的时候,会唤醒这个线程去处理工作。

在内核中,工作队列包括共享工作队列和自定义工作队列这俩种类型。这两种类型的工作队列具有不同的特点和用途。

  1. 共享队列是由内核管理的全局工作队列,用于处理内核中一些系统级任务。共享工作队列是内核中一个默认工作队列,可以由多个内核组件和驱动程序共享使用。
  2. 自定义工作队列是由内核或驱动程序创建的特定工作队列,用于处理特定的任务。自定义工作队列通常与特定的内核模块或驱动程序相关联,用于执行该模块或驱动程序相关的任务。

本章节我们先来学习共享工作队列相关的知识。
在Linux 内核中,使用work_struct 结构体表示一个工作项,这些工作组织成工作队列,工作队列使用workqueue_struct 结构体表示,如下图所示,流水线相当于工作队列,流水线上一个个等待处理的物料相当于一个个工作。机器相当于内核线程或进程。

image-20240820143242999

work_struct 结构体表示一个工作项,定义在include/linux/workqueue.h 中,如下所示:

struct work_struct {
    atomic_long_t data;
    struct list_head entry;
    work_func_t func; /* 工作队列处理函数*/
};
typedef void (*work_func_t)(struct work_struct *work); //工作函数

44.2 工作队列相关接口函数

44.2.1 初始化函数

在实际的驱动开发中,我们只需要定义工作项(work_struct)即可,关于工作队列和工作者线程我们基本不用去管。简单创建工作很简单,直接定义一个work_struct 结构体变量即可,然后使用INIT_WORK 宏来初始化工作,INIT_WORK 宏定义如下:

#define INIT_WORK(_work,_func)

INIT_WORK 宏接受两个参数:_work_func,分别表示要初始化的工作项和工作项的处理函数。
也可以使用DECLARE_WORK 宏一次性完成工作的创建和初始化,宏定义如下:

#define DECLARE_WORK(n, f)

参数n 表示定义的工作(work_struct),f 表示工作对应的处理函数。

44.2.2 调度/取消调度工作队列函数

和tasklet 一样,工作也是需要调度才能运行的,工作的调度函数为schedule_work,函数原型如下所示:

static inline bool schedule_work(struct work_struct *work)

参数是指向工作项的指针。这个函数作用是将工作项提交到工作队列中,并请求调度器在合适的时机执行工作项。该函数会返回一个布尔值,表示工作项是否成功被提交到工作队列。

如果想要取消该工作项的调度,使用以下函数:

bool cancel_work_sync(struct work_struct *work);

参数是指向工作项的指针。这个函数的作用是取消该工作项的调度。如果工作项已经在工作队列中,它将被从队列中移除。如果工作项已经在工作队列中,它将被从队列中移除,并等待工作项执行完成。函数返回一个布尔值,表示工作项是否成功取消。

44.3 实验程序的编写

44.3.1 驱动程序编写

本实验对应的网盘路径为:iTOP-RK3568 开发板【底板V1.7 版本】\03_【iTOP-RK3568开发板】指南教程\02_Linux 驱动配套资料\04_Linux 驱动例程\34_workqueue\module
本实验将实现注册显示屏触摸中断,每按当触摸LCD 显示屏就会触发中断服务函数,在中断服务函数中提交工作项到共享工作队列中,打印“This id test_interrupt”和“This istest_work”。

在驱动程序中的模块初始化函数中,我们将GPIO 转换为中断号,并使用request_irq 函数请求中断,然后初始化工作项。当中断被触发时,中断处理函数被调用,并将工作项提交到共享工作队列中,最终由工作项处理函数异步执行。编写完成的interrupt.c 代码如下所示,添加的代码已加粗表示。

#include <linux/module.h>
#include <linux/init.h>
#include <linux/interrupt.h>
#include <linux/gpio.h>
#include <linux/delay.h>
#include <linux/workqueue.h>

int irq;

struct work_struct test_workqueue;
// 工作项处理函数
void test_work(struct work_struct *work)
{
  msleep(1000);
  printk("This is test_work\n");
}

// 中断处理函数
irqreturn_t test_interrupt(int irq, void *args)
{
  printk("This is test_interrupt\n");
  // 提交工作项到工作队列
  schedule_work(&test_workqueue);
  return IRQ_RETVAL(IRQ_HANDLED);
}

static int interrupt_irq_init(void)
{
  int ret;
  irq = gpio_to_irq(101); // 将GPIO映射为中断号
  printk("irq is %d\n", irq);
  // 请求中断
  ret = request_irq(irq, test_interrupt, IRQF_TRIGGER_RISING, "test", NULL);
  if (ret < 0)
  {
    printk("request_irq is error\n");
    return -1;
  }
  // 初始化工作项
  INIT_WORK(&test_workqueue, test_work);
  return 0;
}

static void interrupt_irq_exit(void)
{
  free_irq(irq, NULL); // 释放中断
  printk("bye bye\n");
}

module_init(interrupt_irq_init);
module_exit(interrupt_irq_exit);

MODULE_LICENSE("GPL");
MODULE_AUTHOR("topeet");

44.4 运行测试

44.4.1 编译驱动程序

在上一小节中的interrupt.c 代码同一目录下创建Makefile 文件,Makefile 文件内容:然后使用命令“make”进行驱动的编译,编译完生成interrupt.ko 目标文件,至此驱动模块就编译成功了,接下来进行测试。

44.4.2 运行测试

开发板启动之后,使用以下命令进行驱动模块的加载,如下图(图44-5)所示:

insmod interrupt.ko

image-20240820143808787

加载驱动之后,可以看到申请的中断号被打印了出来,然后用手触摸连接的LVDS 7 寸屏幕,
打印如下图(44-6)所示:

image-20240820143820241

在上图中,可以看到打印中断处理函数中添加的打印“This is test_interrupt”被多次打印,说明触发了好几次中断上文,那么中断上文会多次调度中断下文,所以也会打印工作项处理函数中添加的打印“This is test_work”。但是为什么只会打印俩次“This is test_work”呢?这是因为在中断上文调度工作项处理函数之后,内核没有来得及去执行工作项处理函数,没有执行相当于无效操作,有效的执行则打印了俩次“This is test_work”。

最后可以使用以下命令进行驱动的卸载,如下图(图44-7)所示:

rmmod interrupt

image-20240820143906078

至此,共享工作队列实验就完成了。

第45 章自定义工作队列实验

在上一章节中对工作队列以及共享工作队列知识进行了学习,并使用共享队列进行了实验。共享队列是由内核管理的全局工作队列自定义工作队列是由内核或驱动程序创建的特定工作队列,用于处理特定的任务。下面就让我们一起来进行自定义工作队列的学习吧。

45.1 工作队列相关结构体

在Linux 内核中,结构体struct work_struct 描述的是要延迟执行的工作项,定义在include/linux/workqueue.h 当中,如下所示

struct work_struct {
    atomic_long_t data; // 工作项的数据字段
    struct list_head entry; // 工作项在工作队列中的链表节点
    work_func_t func; // 工作项的处理函数
    #ifdef CONFIG_LOCKDEP
        struct lockdep_map lockdep_map; // 锁依赖性映射
    #endif
};

这些工作组织成工作队列,内核使用struct workqueue_struct 结构体描述一个工作队列,定义在include/linux/workqueue.h 当中,如下所示:

struct workqueue_struct {
    struct list_head pwqs; // 工作队列上的挂起工作项列表
    struct list_head delayed_works; // 延迟执行的工作项列表
    struct delayed_work_timer dwork_timer; // 延迟工作项的定时器
    struct workqueue_attrs *unbound_attrs; // 无绑定工作队列的属性
    struct pool_workqueue *dfl_pwq; // 默认的池化工作队列
    ...
};

45.2 工作队列相关接口函数

在Linux 内核中,create_workqueue 函数用于创建一个工作队列,函数原型如下所示:

struct workqueue_struct *create_workqueue(const char *name);

参数name 是创建的工作队列的名字。使用这个函数可以给每个CPU 都创建一个CPU 相关的工作队列。创建成功返回一个struct workqueue_struct 类型指针,创建失败返回NULL。

如果只给一个CPU 创建一个CPU 相关的工作队列,使用以下函数。

#define create_singlethread_workqueue(name) \ alloc_workqueue("%s", WQ_SINGLE_THREAD, 1, name)

参数name 是创建的工作队列的名字。使用这个函数只会给一个CPU 创建一个CPU 相关的工作队列。创建成功之后返回一个struct workqueue_struct 类型指针,创建失败返回NULL。

当工作队列创建好之后,需要将要延迟执行的工作项放在工作队列上,调度工作队列,使用queue_work_on 函数,函数原型如下所示:

bool queue_work_on(int cpu, struct workqueue_struct *wq, struct work_struct *work);

该函数有三个参数, 第一个参数是一个整数cpu , 第二个参数是一个指向struct workqueue_struct 的指针wq,第三个参数是一个指向struct work_struct 的指针work。
该函数的返回类型是布尔值,表示是否成功调度工作队列。queue_work_on 函数还有其他变种,比如queue_work 函数,这里略过,其实思路是一致的,用于将定义好的工作项立即添加到工作队列中,并在工作队列可用时立即执行。

如果要取消一个已经调度的工作,使用函数bool cancel_work_sync,函数原型如下所示:

bool cancel_work_sync(struct work_struct *work);

函数的作用是取消一个已经调度的工作,如果被取消的工作已经正在执行,则会等待他执行完成再返回。

在Linux 内核中,使用flush_workqueue 函数将刷新该工作队列中所有已提交但未执行的工作项。函数原型如下所示:

void flush_workqueue(struct workqueue_struct *wq);

该函数参数是一个指向struct workqueue_struct 类型的指针wq。函数的作用是刷新工作队列,告诉内核尽快处理工作队列上的工作。

如果要删除自定义的工作队列,使用destroy_workqueue 函数,函数原型如下所示:

void destroy_workqueue(struct workqueue_struct *wq);

该函数参数是一个指向struct workqueue_struct 类型的指针wq

在下一小节中将使用上述工作队列API 函数进行相应的实验。

45.3 实验程序的编写

45.3.1 驱动程序编写

本实验对应的网盘路径为:iTOP-RK3568 开发板【底板V1.7 版本】\03_【iTOP-RK3568开发板】指南教程\02_Linux 驱动配套资料\04_Linux 驱动例程\35_workqueue_share\module
本实验将实现注册显示屏触摸中断,每按当触摸LCD 显示屏就会触发中断服务函数,在中断服务函数中提交工作项到工作队列中,打印“This id test_interrupt”和“This istest_work”。

在驱动程序中的模块初始化函数中,我们将GPIO 转换为中断号,并使用request_irq 函数请求中断,然后创建自定义工作队列,初始化工作项。当中断被触发时,中断处理函数被调用,并将工作项提交到自定义工作队列中,最终由工作项处理函数异步执行。编写完成的interrupt.c 代码如下所示,添加的代码已加粗表示。

#include <linux/module.h>
#include <linux/init.h>
#include <linux/interrupt.h>
#include <linux/gpio.h>
#include <linux/delay.h>
#include <linux/workqueue.h>

int irq;
struct workqueue_struct *test_workqueue;
struct work_struct test_workqueue_work;

// 工作项处理函数
void test_work(struct work_struct *work)
{
  msleep(1000);
  printk("This is test_work\n");
}

// 中断处理函数
irqreturn_t test_interrupt(int irq, void *args)
{
  printk("This is test_interrupt\n");
  queue_work(test_workqueue, &test_workqueue_work); // 提交工作项到工作队列
  return IRQ_RETVAL(IRQ_HANDLED);
}

static int interrupt_irq_init(void)
{
  int ret;
  irq = gpio_to_irq(101); // 将GPIO映射为中断号
  printk("irq is %d\n", irq);

  // 请求中断
  ret = request_irq(irq, test_interrupt, IRQF_TRIGGER_RISING, "test", NULL);
  if (ret < 0)
  {
    printk("request_irq is error\n");
    return -1;
  }

  test_workqueue = create_workqueue("test_workqueue"); // 创建工作队列
  INIT_WORK(&test_workqueue_work, test_work);          // 初始化工作项

  return 0;
}

static void interrupt_irq_exit(void)
{
  free_irq(irq, NULL);                    // 释放中断
  cancel_work_sync(&test_workqueue_work); // 取消工作项
  flush_workqueue(test_workqueue);        // 刷新工作队列
  destroy_workqueue(test_workqueue);      // 销毁工作队列
  printk("bye bye\n");
}

module_init(interrupt_irq_init);
module_exit(interrupt_irq_exit);

MODULE_LICENSE("GPL");
MODULE_AUTHOR("topeet");

45.4 运行测试

45.4.1 编译驱动程序

在上一小节中的interrupt.c 代码同一目录下创建Makefile 文件,Makefile 文件内容:然后使用命令“make”进行驱动的编译,编译完生成interrupt.ko 目标文件,至此驱动模块就编译成功了,接下来进行测试。

45.4.2 运行测试

开发板启动之后,使用以下命令进行驱动模块的加载,如下图(图45-4)所示:

insmod interrupt.ko

image-20240820151728963

加载驱动之后,可以看到申请的中断号被打印了出来,然后用手触摸连接的LVDS 7 寸屏幕,打印如下图(45-5)所示:

image-20240820151739201

在上图中,可以看到打印中断处理函数中添加的打印“This is test_interrupt”被打印了俩次,说明触发了2 次中断上文,那么中断上文会调度2 次中断下文,所以也会打印2 次工作项处理函数中添加的打印“This is test_work”。
在按屏幕之后,立即输入ps -aux|grep test_workqueue 命令可以查看自己创建的工作队列,如下(图45-6)所示:

image-20240820151804887

最后可以使用以下命令进行驱动的卸载,如下图(图45-7)所示:

rmmod interrupt

image-20240820151827357

至此,自定义工作队列实验就完成了。

第46 章延迟工作实验

在之前的章节中,我们学习了共享工作队列和自定义工作队列,为了更形象地理解学习,将流水线比作工作队列,流水线上一个个等待处理的物料比作一个个工作。机器比作内核线程或进程。本章节我们即将学习的延迟工作,可以类比为将物料延迟一定时间,再放到生产线上加工。延迟工作不仅可以在自定义工作队列中实现也可以在共享工作队列上实现。现在,我们对延迟工作有了一个感性的认识,接下来详细的学习下延迟工作吧。

46.1 什么是延迟工作

延迟工作是一种将工作的执行延迟到稍后时间点进行处理的技术。通常情况下,当某个任务需要花费较长时间,不需要立即执行或需要按时执行时,延迟工作就会派上用场

延迟工作的基本思想是将任务放入一个队列中,然后由后台的工作进程会任务调度程序来处理队列中的任务。任务可以在指定的延迟时间后执行,也可以根据优先级,任务类型或者其他条件进行排序和处理。

延迟工作在许多应用场景中都非常有用,尤其是在需要处理大量任务,提供系统性能和可靠性的情况下。以下是一些常用的应用场景:

  1. 延迟工作常用于处理那些需要花费较长时间的任务,比如发送电子邮件,处理图像等。通过将这些任务放入队列中并延迟执行,可以避免阻塞应用程序的主线程,提高系统的响应速度。
  2. 延迟工作可以用来执行定时任务,比如定时备份数据库,通过将任务设置为在未来的某个时间点执行,提高系统的可靠性和效率。

为了方便大家理解,我们再举个形象点的例子,比如说开发板上的按键,现在我们想通过驱动程序读取按键的状态,那么只需要读取这个按键所连接的GPIO 的状态就可以了。

理想型的按键电压变化过程如图(图46-1)所示:

image-20240820152105572

在上图中,按键没有按下的时候按键值为1,当按键在t1 时刻按键被按下以后按键值就变为0,这是最理想的状态。但是实际的按键是机械结构,加上刚按下去的一瞬间人手可能也有抖动,实际的按键电压变化过程如下图(图46-2)所示:

image-20240820152129112

在上图中,t1 时刻按键被按下,但是由于抖动的原因,直到t2 时刻才稳定下来,t1 到t2 这段时间就是抖动。一般这段时间就是十几ms 左右,从上图中可以看出在抖动期间会有多次触发,如果不消除这段抖动的话软件就会误判,本来按键就按下了一次,结果软件读取IO值发现电平多次跳变以为按下了多次。所以我们需要跳过这段抖动时间再去读取按键的IO 值,也就是至少要在t2 时刻以后再去读IO 值。在之前的驱动视频中,我们使用了定时器来实现消抖。按键采用中断驱动方式,当按键按下以后触发按键中断,在按键中断中开启一个定时器,定时周期为10ms,当定时时间到了以后就会触发定时器中断,最后在定时器中断处理函数中读取按键的值,如果按键值还是按下状态那就表示这是一次有效的按键。定时器按键消抖如下图(图46-3)所示:

image-20240820152201643

在上图中t1t3 这一段时间就是按键抖动,是需要消除的。设置按键为下降沿触发,因此会在t1、t2 和t3 这三个时刻会触发按键中断,每次进入中断处理函数都会重新开器定时器中断,所以会在t1、t2 和t3 这三个时刻开器定时器中断。但是t1t2 和t2~t3 这两个时间段是小于我们设置的定时器中断周期(也就是消抖时间,比如10ms),所以虽然t1 开启了定时器,但是定时器定时时间还没到呢t2 时刻就重置了定时器,最终只有t3 时刻开启的定时器能完整的完成整个定时周期并触发中断,我们就可以在中断处理函数里面做按键处理了,这就是定时器实现按键防抖的原理,Linux 里面的按键驱动用的就是这个原理!

除了使用定时器方式进行消抖,也可以使用本章节讲述的延迟工作。在中断下文中将工作延迟3 秒之后,再去读GPIO 电平状态。

在Linux 内核中,使用struct delayed_work 来描述延迟工作,定义在include/linux/workqueue.h 当中,原型定义如下所示:

struct delayed_work{
    struct work_struct work;// 延迟工作的基本工作结构
    struct timer_list timer;// 定时器,用于延迟执行工作
};

struct delayed_work 结构体包含了两个成员:

  1. work:这是一个struct work_struct 类型的成员,用于表示延迟工作的基本工作结构。struct work_struct 是表示工作的常见数据结构,用于定义要执行的工作内容。
  2. timer:这是一个struct timer_list 类型的成员,用于管理延迟工作的定时器。struct timer_list是Linux 内核中的定时器结构,用于设置延迟时间和触发工作执行的时机。

使用struct delayed_work 结构体,可以将需要执行的工作封装成一个延迟工作,并使用定时器来控制工作的延迟执行。通过设置定时器的延迟时间,可以指定工作在一定时间后执行。
接下来我们学习下延迟工作相关的接口函数吧。

46.2 延迟工作相关接口函数

46.2.1 初始化延迟工作函数

静态定义并初始化延迟工作使用宏DECLARE_DELAYED_WORK,函数原型如下所示:

#define DECLARE_DELAYED_WORK(n,f) struct delayed_work n = { .work = __WORK_INITIALIZER(n.work,(f)) }

使用宏定义后,可以将上述代码简化为#define DECLARE_DELAYED_WORK(n,f),n 代表延迟工作的变量名,f 是延迟工作的处理函数。

动态定义并初始化延迟工作使用宏INIT_DELAYED_WORK,函数原型如下所示:

#define INIT_DELAYED_WORK(_work, _func) \
    do { \
        INIT_WORK(&(_work)->work, (_func)); \
        (_work)->timer = TIMER_INITIALIZER((_work)->timer, 0, 0); \
    } while (0)

使用宏定义后,可以将上述代码简化为#define INIT_DELAYED_WORK(_work, _func), n 代表延迟工作的变量名,f 是延迟工作的处理函数。

46.2.2 调度/取消调度延迟工作函数

如果是在共享工作队列上调度延迟工作,使用以下函数:

static inline bool schedule_delayed_work(struct delayed_work *dwork,unsigned long delay )
该函数是一个内联函数,用于在给定的延迟时间后调度延迟工作执行。
函数参数
	dwork:是指向延迟工作的指针,即要被调度的延迟工作。
	delay:表示延迟的时间长度,以内核时钟节拍数jiffies 为单位。

如果是在自定义工作队列上调度延迟工作,使用以下函数:

static inline bool queue_delayed_work(struct workqueue_struct *wq, struct delayed_work *dwork,unsigned long delay)
该函数是一个内联函数,用于将延迟工作加入工作队列后在指定的延迟时间后执行。
函数参数
    wq 是指向工作队列结构的指针,即要将延迟工作加入的目标工作队列。
    dwork:指向延迟工作的指针,也就是要被加入工作队列的延迟工作。
    delay: 表示延迟的时间长度,以内核时钟节拍数jiffies 为单位。

如果要取消调度函数,使用以下函数:

extern bool cancel_delayed_work_sync(struct delayed_work *dwork);
该函数是一个外部声明的函数,用于取消延迟工作并等待其完成。dwork 参数是指向延迟工作的指针,也就是要被取消的延迟工作。函数如果返回true,说明成功取消延迟工作并等待其完成。函数如果返回false,说明无法取消延迟工作或等待其完成。

在下一小节中将在自定义工作队列实验的基础上修改驱动,进行延迟工作实验。

46.3 实验程序的编写

46.3.1 驱动程序编写

本实验对应的网盘路径为:iTOP-RK3568 开发板【底板V1.7 版本】\03_【iTOP-RK3568开发板】指南教程\02_Linux 驱动配套资料\04_Linux 驱动例程\36_workqueue_delay\module

本实验将实现注册显示屏触摸中断,每按当触摸LCD 显示屏就会触发中断服务函数,在中断服务函数中提交延迟工作项到工作队列中,打印“This id test_interrupt”,并延迟打印“This is test_work”。

在驱动程序中的模块初始化函数中,我们将GPIO 转换为中断号,并使用request_irq 函数请求中断,然后创建自定义工作队列,初始化延迟工作项。当中断被触发时,中断处理函数被调用,并将延迟工作项提交到自定义工作队列中,最终由工作项处理函数异步执行。编写完成的interrupt.c 代码如下所示,添加的代码已加粗表示。

#include <linux/module.h>
#include <linux/init.h>
#include <linux/interrupt.h>
#include <linux/gpio.h>
#include <linux/delay.h>
#include <linux/workqueue.h>

int irq;
struct workqueue_struct *test_workqueue;
struct delayed_work test_workqueue_work;

// 工作项处理函数
void test_work(struct work_struct *work)
{
  msleep(1000);
  printk("This is test_work\n");
}

// 中断处理函数
irqreturn_t test_interrupt(int irq, void *args)
{
  printk("This is test_interrupt\n");
  // 提交延迟工作项到自定义工作队列
  queue_delayed_work(test_workqueue, &test_workqueue_work, 3 * HZ); 
  return IRQ_RETVAL(IRQ_HANDLED);
}

static int interrupt_irq_init(void)
{
  int ret;
  irq = gpio_to_irq(101); // 将GPIO映射为中断号
  printk("irq is %d\n", irq);

  // 请求中断
  ret = request_irq(irq, test_interrupt, IRQF_TRIGGER_RISING, "test", NULL);
  if (ret < 0)
  {
    printk("request_irq is error\n");
    return -1;
  }
  // 创建工作队列
  test_workqueue = create_workqueue("test_workqueue"); 
  // 初始化延迟工作项
  INIT_DELAYED_WORK(&test_workqueue_work, test_work);  

  return 0;
}

static void interrupt_irq_exit(void)
{
  free_irq(irq, NULL);                            // 释放中断
  cancel_delayed_work_sync(&test_workqueue_work); // 取消延迟工作项
  flush_workqueue(test_workqueue);                // 刷新工作队列
  destroy_workqueue(test_workqueue);              // 销毁工作队列
  printk("bye bye\n");
}

module_init(interrupt_irq_init);
module_exit(interrupt_irq_exit);

MODULE_LICENSE("GPL");
MODULE_AUTHOR("topeet");

46.4 运行测试

46.4.1 编译驱动程序

在上一小节中的interrupt.c 代码同一目录下创建Makefile 文件,Makefile 文件内容:然后使用命令“make”进行驱动的编译,编译完生成interrupt.ko 目标文件,至此驱动模块就编译成功了,接下来进行测试。

46.4.2 运行测试

开发板启动之后,使用以下命令进行驱动模块的加载,如下图(图46-7)所示:

insmod interrupt.ko

image-20240820153049966

加载驱动之后,可以看到申请的中断号被打印了出来,然后用手触摸连接的LVDS 7 寸屏幕,打印如下图(46-8)所示:

image-20240820153113008

在上图中,可以看到打印中断处理函数中添加的打印“This is test_interrupt”被打印了多次,说明触发了多次中断上文,在4 秒之后,打印工作项处理函数中的“This is test_work”。4 秒之后打印“This is test_work”是因为调度延迟工作函数写了延迟3 秒,再加上工作项处理函数中延迟了一秒,所以一共是4 秒。

最后可以使用以下命令进行驱动的卸载,如下图(图46-9)所示:

rmmod interrupt

image-20240820153213198

至此,延迟工作实验就完成了。

第47 章工作队列传参实验

在41 章节中,我们使用tasklet 来实现中断下文,并使用tasklet 给中断下文传参,如果我们使用工作队列来实现中断的下半部分,那么如何用工作队列给中断下文传参呢?本章节我们来一探究竟!

47.1 工作队列传参

在Linux 内核的工作队列中,可以通过使用工作项的方式向工作队列传递参数。工作项是一个抽象的结构,可以用于封装需要执行的工作及其相关的参数。

首先我们定义工作项结构,如下所示,在结构体struct work_data 中定义了需要传递给工作项处理函数的参数a 和b,然后定义一个类型为struct work_data 的变量test_workqueue_work

struct work_data {
    struct work_struct test_work;
    int a;
    int b;
};
struct work_data test_workqueue_work;

接下来在模块初始化函数interrupt_irq_init 中创建了一个工作队列test_workqueue 和一个工作项test_workqueue_work。

test_workqueue = create_workqueue("test_workqueue"); // 创建工作队列
INIT_WORK(&test_workqueue_work.test_work, test_work); // 初始化工作项

然后在模块初始化函数中,为工作项的参数a 和b 赋值。

test_workqueue_work.a = 1;
test_workqueue_work.b = 2;

当中断触发时,在中断处理函数test_interrupt 中,通过调用queue_work 函数将工作项test_workqueue_work.test_work 提交到工作队列test_workqueue 中。

queue_work(test_workqueue, &test_workqueue_work.test_work);

然后工作项处理函数test_work 定义了一个指针pdata,将工作项转换为struct work_data结构,并通过该结构访问参数a 和b。如下所示:

void test_work(struct work_struct *work)
{
    struct work_data *pdata;
    pdata = container_of(work, struct work_data, test_work);

    printk("a is %d\n", pdata->a);
    printk("b is %d\n", pdata->b);
}

这样,当工作队列被调度执行时,工作项处理函数test_work 将能够访问到传递给工作项的参数a 和b,并在内核日志中打印他们的值。
注意,工作项处理函数中的container_of 宏用于从工作项结构的指针获取整个struct work_data 结构的指针。这样可以通过指针偏移来访问工作项结构中的其他字段,例如参数a和b。

详细的驱动代码编写见下一小节。

47.2 实验程序的编写

47.2.1 驱动程序编写

本实验对应的网盘路径为:iTOP-RK3568 开发板【底板V1.7 版本】\03_【iTOP-RK3568开发板】指南教程\02_Linux 驱动配套资料\04_Linux 驱动例程\37_workqueue_data\module

编写完成的interrupt.c 代码如下所示:

#include <linux/module.h>
#include <linux/init.h>
#include <linux/interrupt.h>
#include <linux/gpio.h>
#include <linux/delay.h>
#include <linux/workqueue.h>

int irq;

struct work_data
{
  struct work_struct test_work;
  int a;
  int b;
};

struct work_data test_workqueue_work;

struct workqueue_struct *test_workqueue;

// 工作项处理函数
void test_work(struct work_struct *work)
{
  struct work_data *pdata;
  pdata = container_of(work, struct work_data, test_work);

  printk("a is %d", pdata->a);
  printk("b is %d", pdata->b);
}

// 中断处理函数
irqreturn_t test_interrupt(int irq, void *args)
{
  printk("This is test_interrupt\n");
  // 提交工作项到工作队列
  queue_work(test_workqueue, &test_workqueue_work.test_work);
  return IRQ_RETVAL(IRQ_HANDLED);
}

static int interrupt_irq_init(void)
{
  int ret;
  irq = gpio_to_irq(101); // 将GPIO映射为中断号
  printk("irq is %d\n", irq);

  // 请求中断
  ret = request_irq(irq, test_interrupt, IRQF_TRIGGER_RISING, "test", NULL);
  if (ret < 0)
  {
    printk("request_irq is error\n");
    return -1;
  }
  // 创建工作队列
  test_workqueue = create_workqueue("test_workqueue");
  // 初始化工作项
  INIT_WORK(&test_workqueue_work.test_work, test_work);

  test_workqueue_work.a = 1;
  test_workqueue_work.b = 2;

  return 0;
}

static void interrupt_irq_exit(void)
{
  free_irq(irq, NULL);                              // 释放中断
  cancel_work_sync(&test_workqueue_work.test_work); // 取消工作项
  flush_workqueue(test_workqueue);                  // 刷新工作队列
  destroy_workqueue(test_workqueue);                // 销毁工作队列
  printk("bye bye\n");
}

module_init(interrupt_irq_init);
module_exit(interrupt_irq_exit);

MODULE_LICENSE("GPL");
MODULE_AUTHOR("topeet");

47.3 运行测试

47.3.1 编译驱动程序

在上一小节中的interrupt.c 代码同一目录下创建Makefile 文件,Makefile 文件内容:然后使用命令“make”进行驱动的编译,编译完生成interrupt.ko 目标文件,至此驱动模块就编译成功了,接下来进行测试。

47.3.2 运行测试

开发板启动之后,使用以下命令进行驱动模块的加载,如下图(图47-4)所示:

insmod interrupt.ko

image-20240820154151484

驱动加载之后,可以看到申请的中断号被打印了出来,然后用手触摸连接的LVDS 7 寸屏幕,打印如下图(47-5)所示:

image-20240820154204072

在上图中,可以看到打印中断处理函数中添加的打印“This is test_interrupt”和传递给工作项的参数a 和b 的值。

最后可以使用以下命令进行驱动的卸载,如下图(图47-6)所示:

rmmod interrupt

image-20240820154226124

至此,工作队列传参实验就完成了。

第48 章并发管理工作队列实验

在现代的软件开发中,我们常常面临着需要同时处理多个任务的挑战。这些任务可能是并行的、独立的,或者需要以某种顺序进行处理。为了高效地管理这些并发任务,我们需要一种有效的机制来协调它们的执行。这就是并发管理工作队列发挥作用的地方。本章节我们来学习并发管理工作队列。

48.1 工作队列的实现

在44 章节和45 章节,我们学习了共享工作队列和自定义工作队列,在使用工作队列时,我们首先定义一个work 结构体,然后将work 添加到workqueue(工作队列)中,最后worker thread执行workqueue。当工作队列中有新work 产生时,工作线程(worker thread)会执行工作队列中每个work。当执行完结束的时候,worker thread 会睡眠,等到新的中断产生,work 再继续添加到工作队列,然后工作线程执行每个工作,周而复始。

在单核线程的系统中,通常会为每个CPU(核心)初始化一个工作线程并关联一个工作队列。这种默认设置确保每个CPU 都有一个专门的线程来处理与其绑定的工作队列上的工作项。如下图(48-1)所示:

image-20240820154331016

在多核线程系统中,工作队列的设计与单核线程系统有所不同。在多核线程系统中,通常会存在多个工作队列,每个工作队列与一个工作线程(Worker Thread)绑定。这样可以充分利用多个核心的并行处理能力。如下图(48-2)所示:

image-20240820154349505

当有新的工作项产生时,系统需要决定将其分配给哪个工作队列。一种常见的策略是使用负载均衡算法,根据工作队列的负载情况来平衡分配工作项,以避免某个工作队列过载而导致性能下降。每个工作队列独立管理自己的工作项。当有新的工作项添加到工作队列时,工作线程会从其关联的工作队列中获取待执行的工作项,并执行相应的处理函数。在多核线程系统中,多个工作线程可以同时执行各自绑定的工作队列中的工作项。这样可以实现并行处理,提高系统的整体性能和响应速度。

了解了工作队列是如何实现的,接下来我们看看传统的工作队列有什么弊端呢?

48.2 workqueue 队列弊端

假如说有三个work 放到了同一个工作队列上,接下来CPU 会启动工作线程去执行这三个work,如下图(48-3)所示:

image-20240820154423187

在上图中,工作项w0、w1、w2 被排队到同一个CPU 上的绑定工作队列上。w0 工作项执行的时候,先工作5 毫秒,然后睡觉10 毫秒,然后再工作CPU 5 毫秒,然后完成。工作项w1 和w2 都是工作5ms,然后睡眠10 ms,然后完成。传统工作队列的弊端如下所示:

  • 1 在工作项w0 工作甚至是睡眠时,工作项w1 w2 是排队等待的,在繁忙的系统中,工作队列可能会积累大量的待处理工作项,导致任务调度的延迟,这可能会影响系统的响应性能,并增加工作项的处理时间。
  • 2 在工作队列中,不同的工作项可能具有不同的处理时间和资源需求。如果工作项的处理时间差异很大,一些工作线程可能会一直忙于处理长时间的工作项,而其他工作线程则处于空闲状态,导致资源利用不均衡。
  • 3 在多线程环境下,多个工作线程同时访问和修改工作队列可能会导致竞争条件的发生。为了确保数据的一致性和正确性,需要采用适当的同步机制,如锁或原子操作,来保护共享数据,但这可能会引入额外的同步开销。
  • 4 工作队列通常按照先进先出(FIFO)的方式处理工作项,缺乏对工作项优先级的细粒度控制。在某些场景下,可能需要根据工作项的重要性或紧急程度进行优先级调度,而工作队列本身无法提供这种级别的优先级控制。
  • 5 当工作线程从工作队列中获取工作项并执行时,可能需要频繁地进行上下文切换,将处理器的执行上下文从一个线程切换到另一个线程。这种上下文切换开销可能会影响系统的性能和效率。

48.2 什么是并发管理工作队列

通过上一小节的学习,我们认识到传统的工作队列无论是单核系统还是多核系统上都是有缺陷的。比如无法充分利用多核处理器的计算能力以及对于不同优先级的工作项无法提供公平的调度。为了解决这些问题,Con Kolivas 提出了CMWQ 调度算法。

CMWQ 全称是concurrency Managed Workqueue,意为并发管理工作队列。并发管理工作队列是一种并发编程模式,用于有效地管理和调度待执行的任务或工作项。它通常用于多线程或多进程环境中,以实现并发执行和提高系统的性能。CMWQ 工作实现如下图(48-4)所示:

image-20240820154531509

当我们需要在一个系统中同时处理多个任务或工作时,使用并发管理工作队列是一种有效的方式。

想象一下,你是一个餐厅的服务员,有很多顾客同时来到餐厅用餐。为了提高效率,你需要将顾客的点菜请求放到一个队列中,这就是工作队列。然后,你和其他服务员可以从队列中获取顾客的点菜请求,每个服务员独立地为顾客提供服务。通过这种方式,你们可以并发地处理多个顾客的点菜请求,而不需要等待上一个顾客点完菜再去处理下一个顾客的请求。每个服务员可以独立地从队列中获取任务,并根据需要执行相应的服务。这种独立获取任务的过程就是从工作队列中取出任务并执行的过程。

通过并发管理工作队列,你们能够更高效地处理顾客的点菜请求,提高服务的速度和质量。同时,这种方式也能够更好地利用你们的工作能力,因为每个服务员都可以独立处理任务,而不会相互干扰或等待。

总的来说,通过并发管理工作队列,我们可以同时处理多个任务或工作,提高系统的并发性和性能。每个任务独立地从队列中获取并执行,这种解耦使得整个系统更加高效、灵活,并且能够更好地应对多任务的需求。

48.3 并发管理工作队列接口函数

alloc_workqueue 是Linux 内核中的一个函数,用于创建和分配一个工作队列。工作队列是一种用于管理和调度工作项的机制,可用于实现并发处理和异步任务处理。alloc_workqueue函数的原型如下:

struct workqueue_struct *alloc_workqueue(const char *fmt, unsigned int flags, int max_active);
参数说明:
    fmt:指定工作队列的名称格式。
    flags:指定工作队列的标志,可以控制工作队列的行为和属性,如WQ_UNBOUND 表示无绑定的工作队列,WQ_HIGHPRI 表示高优先级的工作队列等。
    max_active:指定工作队列中同时活跃的最大工作项数量。
返回值:
    函数返回一个指向工作队列结构体(struct workqueue_struct)的指针,或者返回NULL 表示创建失败。

在下一小节中将使用上述API 进行相应的实验。

48.4 实验程序的编写

48.4.1 驱动程序编写

本实验对应的网盘路径为:iTOP-RK3568 开发板【底板V1.7 版本】\03_【iTOP-RK3568开发板】指南教程\02_Linux 驱动配套资料\04_Linux 驱动例程\38_CMWQ\module
本实验在35 自定义工作队列实验的基础上进行修改,使用alloc_workqueue 函数创建和分配一个工作队列。编写完成的interrupt.c 代码如下所示,添加的代码已加粗表示。

#include <linux/module.h>
#include <linux/init.h>
#include <linux/interrupt.h>
#include <linux/gpio.h>
#include <linux/delay.h>
#include <linux/workqueue.h>

int irq;
struct workqueue_struct *test_workqueue;
struct work_struct test_workqueue_work;

// 工作项处理函数
void test_work(struct work_struct *work)
{
  msleep(1000);
  printk("This is test_work\n");
}

// 中断处理函数
irqreturn_t test_interrupt(int irq, void *args)
{
  printk("This is test_interrupt\n");
  queue_work(test_workqueue, &test_workqueue_work); // 提交工作项到工作队列
  return IRQ_RETVAL(IRQ_HANDLED);
}

static int interrupt_irq_init(void)
{
  int ret;
  irq = gpio_to_irq(101); // 将GPIO映射为中断号
  printk("irq is %d\n", irq);

  // 请求中断
  ret = request_irq(irq, test_interrupt, IRQF_TRIGGER_RISING, "test", NULL);
  if (ret < 0)
  {
    printk("request_irq is error\n");
    return -1;
  }
  // 用于创建和分配一个工作队列
  test_workqueue = alloc_workqueue("test_workqueue", WQ_UNBOUND, 0);
  INIT_WORK(&test_workqueue_work, test_work); // 初始化工作项

  return 0;
}

static void interrupt_irq_exit(void)
{
  free_irq(irq, NULL);                    // 释放中断
  cancel_work_sync(&test_workqueue_work); // 取消工作项
  flush_workqueue(test_workqueue);        // 刷新工作队列
  destroy_workqueue(test_workqueue);      // 销毁工作队列
  printk("bye bye\n");
}

module_init(interrupt_irq_init);
module_exit(interrupt_irq_exit);

MODULE_LICENSE("GPL");
MODULE_AUTHOR("topeet");

48.5 运行测试

48.5.1 编译驱动程序

在上一小节中的interrupt.c 代码同一目录下创建Makefile 文件,Makefile 文件内容:然后使用命令“make”进行驱动的编译,编译完生成interrupt.ko 目标文件,至此驱动模块就编译成功了,接下来进行测试。

48.5.2 运行测试

开发板启动之后,使用以下命令进行驱动模块的加载,如下图(图48-8)所示:

insmod interrupt.ko

image-20240820154924953

驱动加载之后,可以看到申请的中断号被打印了出来,然后用手触摸连接的LVDS 7 寸屏幕,打印如下图(48-9)所示:

image-20240820154935036

我们按一下屏幕,立即输入“ps -aux | grep test_workqueue”,可以看到工作线程,如下图(48-10)所示,u 代表无绑定的工作队列。

image-20240820154951409

最后可以使用以下命令进行驱动的卸载,如下图(图48-11)所示:

rmmod interrupt

image-20240820155010610

至此,并发管理工作队列实验就完成了。

第49 章中断线程化实验

中断线程化是实时Linux 项目开发的一个新特性,目的是降低中断处理对系统实时延迟的影响。本章节我们来一项新技术——中断线程化。

49.1 什么是中断线程化

中断线程化是一种优化技术,用于提高多线程程序的性能。

想象一下,你正在做一项任务,但是总是被别人的打扰所中断,每次都要停下手头的工作去处理别人的事情。这样频繁的中断会让你的工作效率变低,因为你需要反复切换任务,无法专心做好自己的工作。

在多线程程序中,也存在类似的问题。有时硬件或其他事件会发出中断信号,打断正在执行的线程,需要切换到中断处理程序去处理这些事件。这种频繁的中断切换会导致额外的开销和延迟,影响程序的性能。

为了解决这个问题,中断线程化提出了一种优化方案。它将中断处理程序从主线程中独立出来,创建一个专门的线程来处理这些中断事件。这样,主线程就不再受到中断的干扰,可以专注于自己的工作,不再频繁地被打断。

中断线程化的核心思想是将中断处理和主线程的工作分开,让它们可以并行执行。中断线程负责处理中断事件,而主线程负责执行主要的工作任务。这样一来,不仅可以减少切换的开销,还可以提高整个程序的响应速度和性能。

需要注意的是,中断线程化还需要处理线程之间的同步和数据共享问题。因为中断线程和主线程可能会同时访问和修改共享的数据,所以需要合理地进行同步操作,确保数据的一致性和正确性

总而言之,中断线程化是一种优化技术,通过将中断处理和主线程的工作分开,提高多线程程序的性能。让主线程不再频繁被中断,可以专注于自己的工作,从而提高程序的效率和响应速度。

中断线程化的处理仍然可以看作是将原来的中断上半部分和中断下半部分。上半部分还是用来处理紧急的事情,下半部分也是出路比较耗时的操作,但是下半部分会交给一个专门的内核线程来处理。这个内核线程只用于这个中断。当发生中断的时候,会唤醒这个内核线程,然后由这个内核线程来执行中断下半部分的函数。

49.2 中断线程化接口函数

request_threaded_irq 是Linux 内核中用于请求并注册一个线程化的中断处理函数的函数。

int request_threaded_irq(unsigned int irq, irq_handler_t handler,
                        irq_handler_t thread_fn, unsigned long irqflags,
                        const char *devname, void *dev_id);

参数说明
    irq:中断号,表示要请求的中断线路。
    handler:是在发生中断时首先要执行的处理程序,非常类似于顶半部,该函数最后会返回IRQ_WAKE_THREAD 来唤醒中断,一般handler 设为NULL,用系统提供的默认处理。
    thread_fn:线程化的中断处理函数,非常类似于底半部。如果此处设置为NULL 则表示没有使用中断线程化。
    irqflags:中断标志,用于指定中断的属性和行为。
    devname:中断的名称,用于标识中断请求的设备。
    dev_id:设备标识符,用于传递给中断处理函数的参数。
函数返回值
    函数返回一个整数值,表示中断请求的结果。如果中断请求成功,返回值为0,否则返回一个负数错误代码。

在下一小节中将使用上述API 进行相应的实验,利用中断线程化相关知识来对共享工作队列实验进行优化。

49.3 实验程序的编写

49.3.1 驱动程序编写

本实验对应的网盘路径为:iTOP-RK3568 开发板【底板V1.7 版本】\03_【iTOP-RK3568开发板】指南教程\02_Linux 驱动配套资料\04_Linux 驱动例程\39_request_threaded_irq\module

本实验将实现注册显示屏触摸中断,每按当触摸LCD 显示屏就会触发中断服务函数,在中断服务函数中提交工作项到工作队列中,打印“This id test_interrupt”,并打印“Thisis test_work”。

我们要实现一个简单的中断处理的例子,用于展示中断的顶半部和底半部处理的概念,并通过线程化的工作队列实现了底半部的延时处理。编写完成的interrupt.c 代码如下所示,添加的代码已加粗表示。

#include <linux/module.h>
#include <linux/init.h>
#include <linux/interrupt.h>
#include <linux/gpio.h>
#include <linux/delay.h>
#include <linux/workqueue.h>

int irq;

// 中断处理函数的底半部(线程化中断处理函数)
irqreturn_t test_work(int irq, void *args)
{
  // 执行底半部的中断处理任务
  msleep(1000);
  printk("This is test_work\n");
  return IRQ_RETVAL(IRQ_HANDLED);
}

// 中断处理函数的顶半部
irqreturn_t test_interrupt(int irq, void *args)
{
  printk("This is test_interrupt\n");
  // 将中断处理工作推迟到底半部
  return IRQ_WAKE_THREAD;
}

static int interrupt_irq_init(void)
{
  int ret;
  irq = gpio_to_irq(101); // 将GPIO映射为中断号
  printk("irq is %d\n", irq);
  // 用于请求并注册一个线程化的中断处理函数
  ret = request_threaded_irq(irq, test_interrupt, test_work, IRQF_TRIGGER_RISING, "test", NULL);
  if (ret < 0)
  {
    printk("request_irq is error\n");
    return -1;
  }

  return 0;
}

static void interrupt_irq_exit(void)
{
  free_irq(irq, NULL); // 释放中断
  printk("bye bye\n");
}

module_init(interrupt_irq_init);
module_exit(interrupt_irq_exit);

MODULE_LICENSE("GPL");
MODULE_AUTHOR("topeet");

49.4 运行测试

49.4.1 编译驱动程序

在上一小节中的interrupt.c 代码同一目录下创建Makefile 文件,Makefile 文件内容:然后使用命令“make”进行驱动的编译,编译完生成interrupt.ko 目标文件,至此驱动模块就编译成功了,接下来进行测试。

49.4.2 运行测试

开发板启动之后,使用以下命令进行驱动模块的加载,如下图(图49-4)所示:

insmod interrupt.ko

image-20240820155511176

可以看到申请的中断号被打印了出来,然后用手触摸连接的LVDS 7 寸屏幕,打印如下图(49-5)所示:

image-20240820155525065

我们按一下屏幕,立即输入“ps -aux | grep test_workqueue”,可以看到工作线程,如下图(49-6)所示,u 代表无绑定的工作队列。

image-20240820155542890

最后可以使用以下命令进行驱动的卸载,如下图(图49-7)所示:

rmmod interrupt

image-20240820155612628

至此,中断线程化实验就完成了。

第六篇平台总线

image-20240822092824970

image-20240822092946040

image-20240822093023431

第50 章平台总线模型介绍

在前面所有章节中,无论要完成何种需求,我们都编写了一个独立的驱动程序,但这样编写出来的驱动程序在重用性和可移植性上是很低的,无论之后要编写一个同类型的驱动还是将该驱动更换一个平台,都要花费时间重新修改驱动代码,而驱动的分离和分层这一软件思路的提出(即本章节要讲解的平台总线模型),就是为了解决这个问题,下面让我们一起进入平台总线模型的学习吧。

50.1 什么是平台总线?

平台总线(Platform bus)是Linux 内核中提供的一种虚拟总线,用于管理和组织与特定硬件平台相关的设备和驱动。它充当了平台设备(platform device)和平台驱动(platform driver)之间的桥梁,负责将它们进行匹配和绑定。

当系统注册一个平台设备时,平台总线会寻找与之匹配的平台驱动。它会遍历已注册的平台驱动列表,尝试与每个平台驱动进行匹配,直到找到与平台设备匹配的驱动为止。一旦找到匹配的驱动,平台总线会将平台设备与平台驱动进行绑定,使得设备可以被正确地初始化和操作。

同样地,当系统注册一个平台驱动时,平台总线会寻找与之匹配的平台设备。它会遍历已注册的平台设备列表,尝试与每个平台设备进行匹配,直到找到与平台驱动匹配的设备为止。一旦找到匹配的设备,平台总线会将平台设备与平台驱动进行绑定,使得驱动可以管理和控制与该设备相关的操作。

设备、平台总线、驱动的关系如下图(图50-1)所示:

image-20240821115707652

通过引入平台总线,Linux 内核提供了一种通用的机制来管理和组织与特定硬件平台相关的设备和驱动。它使得设备和驱动之间的匹配过程更加自动化和灵活,同时也提高了嵌入式系统的可移植性和可扩展性。

50.2 平台总线的优势

在前面的章节中,我们编写的驱动程序将驱动和设备相关的内容放在一起,但是当涉及到多个相同类型的设备时,这种方法会引发一系列问题。举个例子,假设我们有一个硬件平台,该硬件平台上存在了500 个模块,这些模块都使用了LED 灯。如果我们使用杂项设备来编写驱动,虽然相比字符设备,杂项设备的代码量较少,但我们仍旧需要编写500 份类似的代码,从而生成相应的设备节点,以供上层应用在不同模块上控制LED 灯。

编写500 份重复的代码会带来两个问题。首先,会造成大量重复劳动。其次,代码的重用性较差。如果我们需要将这些驱动从一个平台移植到另一个平台,就需要逐个修改驱动代码,尽管只需修改与硬件相关的部分,但仍旧是一个很大的工作量。

而在引入了平台总线模型后,这些问题就得到了很好地解决。通过使用平台总线模型,将设备驱动和平台设备进行了分离。这样一来,我们只需编写一份通用的驱动代码即可,然后针对不同的平台设备进行配置,这就大大减少了重复编写代码的工作量,并提高了驱动代码的重用性。当我们需要将驱动移植到不同的平台时,只需对硬件相关的部分进行适配即可,其他部分可以保持不变。

整理出来的平台总线优势如下所示:

  • (1)设备与驱动的分离:传统的设备驱动模型将设备和驱动代码合并在同一个文件中,导致代码冗余和可维护性差。而平台总线模型将设备代码和驱动代码分离,设备代码放在device.c 文件中,驱动代码放在driver.c 文件中。这种分离使得设备和驱动的职责更加清晰,提高了代码的可读性和可维护性。
  • (2)提高代码的重用性:平台总线模型使得相同类型的设备可以共享相同的驱动代码。例如,在一个硬件平台上存在多个相同类型的设备,传统的驱动模型需要为每个设备编写独立的驱动代码。而使用平台总线模型,只需编写一个通用的驱动代码,然后为每个设备创建相应的device.c 文件,将设备特定的代码放在其中。这样可以减少代码的重复性,提高了代码的重用性和可维护性。
  • (3)减少重复性代码:在传统的设备驱动模型中,如果有多个相同类型的设备存在,就需要为每个设备编写独立的驱动代码。而使用平台总线模型,只需编写一个通用的驱动代码,然后为每个设备创建相应的device.c 文件,将设备特定的代码放在其中。这样可以避免大量的重复性代码,简化了驱动开发过程。
  • (4)提高可移植性:平台总线模型可以提高驱动的可移植性。开发者可以编写适应平台总线的平台驱动程序,从而支持特定的外设,而无需依赖于特定的标准总线。这使得驱动可以更容易地在不同的硬件平台之间进行移植和重用。

第51 章注册platform 设备实验

51.1 注册platform 设备

51.1.1 platform_device_register 函数

platform_device_register 函数用于将platform_device 结构体描述的平台设备注册到内核中。下面是对platform_device_register 函数的详细介绍:

函数原型:
    int platform_device_register(struct platform_device *pdev);
头文件:
    #include <linux/platform_device.h>
函数作用:
    platform_device_register 函数用于将platform_device 结构体描述的平台设备注册到内核中,使其能够参与设备的资源分配和驱动的匹配。
参数含义:
    pdev:指向platform_device 结构体的指针,描述要注册的平台设备的信息。
返回值:
    成功:返回0,表示设备注册成功。
    失败:返回负数,表示设备注册失败,返回的负数值表示错误代码。

pdev 参数是一个指向platform_device 结构体的指针,其中包含了描述平台设备的各种属性和信息。platform_device 结构体包含了设备名称、设备资源、设备ID 等信息,用于描述和标识平台设备,会在接下来的小节对该结构体进行详细的介绍。
该函数在内核源码目录下的“/include/linux/platform_device.h”文件中,具体内容如下所示:

extern int platform_device_register(struct platform_device *);

函数声明中的extern 关键字表示该函数在其他地方定义,而不是在当前文件中实现。这样的声明通常出现在头文件中,用于告诉编译器该函数的定义存在于其他源文件中,以便在编译时能够正确引用该函数。

platform_device_register 实际定义在“/drivers/base/platform.c”文件中,相关定义如下所示:

int platform_device_register(struct platform_device *pdev)
{
    device_initialize(&pdev->dev);
    arch_setup_pdev_archdata(pdev);
    return platform_device_add(pdev);
}

函数内部有三个主要的操作。

  • 第3 行:调用了device_initialize 函数,用于对pdev->dev 进行初始化。pdev->devstruct platform_device 结构体中的一个成员,它表示平台设备对应的struct device 结构体。通过调用device_initialize 函数,对pdev->dev 进行一些基本的初始化工作,例如设置设备的引用计数、设备的类型等。
  • 第4 行:调用了arch_setup_pdev_archdata 函数,用于根据平台设备的架构数据来设置pdev的架构相关数据。这个函数的具体实现可能与具体的架构相关,它主要用于在不同的架构下对平台设备进行特定的设置。
  • 第5 行: 调用了platform_device_add 函数, 将平台设备pdev 添加到内核中。platform_device_add 函数会完成平台设备的添加操作,包括将设备添加到设备层级结构中、添加设备的资源等。它会返回一个int 类型的结果,表示设备添加的结果。

platform_device_register 函数的主要作用是将platform_device 结构体描述的平台设备注册到内核中,包括设备的初始化、添加到platform 总线和设备层级结构、添加设备资源等操作。通过该函数,平台设备被注册后,就能够参与设备的资源分配和驱动的匹配过程。函数的返回值可以用于判断设备注册是否成功。

51.1.2 platform_device_unregister 函数

platform_device_unregister 函数用于取消注册已经注册的平台设备,即从内核中移除设备。在设备不再需要时,调用该函数可以进行设备的清理和释放操作。

函数原型:
    void platform_device_unregister(struct platform_device *pdev);
头文件:
    #include <linux/platform_device.h>
函数作用:
    platform_device_unregister 函数用于取消注册已经注册的平台设备,从内核中移除设备。
参数含义:
    pdev:指向要取消注册的平台设备的platform_device 结构体指针。
返回值:
    无返回值。

该函数在内核源码目录下的“/include/linux/platform_device.h”文件中,具体内容如下所示:

extern int platform_device_unregister(struct platform_device *);

函数声明中的extern 关键字表示该函数在其他地方定义,而不是在当前文件中实现。这样的声明通常出现在头文件中,用于告诉编译器该函数的定义存在于其他源文件中,以便在编译时能够正确引用该函数。

platform_device_unregister 实际定义在“/drivers/base/platform.c”文件中,相关定义如下所示:

void platform_device_unregister(struct platform_device *pdev)
{
    platform_device_del(pdev);
    platform_device_put(pdev);
}

函数内部有两个主要的操作:

  • 第3 行:调用了platform_device_del 函数,用于将设备从platform 总线的设备列表中移除。它会将设备从设备层级结构中移除,停止设备的资源分配和驱动的匹配。
  • 第4 行:这一步调用了platform_device_put 函数,用于减少对设备的引用计数。这个函数会检查设备的引用计数,如果引用计数减为零,则会释放设备结构体和相关资源。通过减少引用计数,可以确保设备在不再被使用时能够被释放。

platform_device_unregister 函数的作用是取消注册已经注册的平台设备,从内核中移除设备。它先调用platform_device_del 函数将设备从设备层级结构中移除, 然后调用platform_device_put 函数减少设备的引用计数,确保设备在不再被使用时能够被释放。

51.1.3 platform_device 结构体

platform_device 结构体是用于描述平台设备的数据结构。它包含了平台设备的各种属性和信息,用于在内核中表示和管理平台设备。该结构体定义在内核的“/include/linux/platform_device.h”文件中,具体内容如下所示:

struct platform_device {
    const char *name; 			// 设备的名称,用于唯一标识设备
    int id; 					// 设备的ID,可以用于区分同一种设备的不同实例
    bool id_auto; 				// 表示设备的ID 是否自动生成
    struct device dev; 			// 表示平台设备对应的struct device 结构体,用于设备的基本管理和操作
    u32 num_resources; 			// 设备资源的数量
    struct resource *resource; 	// 指向设备资源的指针
    const struct platform_device_id *id_entry; // 指向设备的ID 表项的指针,用于匹配设备和驱动
    char *driver_override; 		// 强制设备与指定驱动匹配的驱动名称
    /* MFD cell pointer */
    struct mfd_cell *mfd_cell; 	// 指向多功能设备(MFD)单元的指针,用于多功能设备的描述
    /* arch specific additions */
    struct pdev_archdata archdata; // 用于存储特定于架构的设备数据
};

下面对于几个重要的参数和结构体进行讲解

  • const char *name:设备的名称,用于唯一标识设备。必须提供一个唯一的名称,以便内核能够正确识别和管理该设备。
  • int id:设备的ID,可以用于区分同一种设备的不同实例。这个参数是可选的,如果不需要使用ID 进行区分,可以将其设置为-1,
  • struct device dev:表示平台设备对应的struct device 结构体,用于设备的基本管理和操作。必须为该参数提供一个有效的struct device 对象,该结构体的release 方法必须要实现,否则在编译的时候会报错。
  • u32 num_resources:设备资源的数量。如果设备具有资源(如内存区域、中断等),则需要提供资源的数量。
  • struct resource *resource:指向设备资源的指针。如果设备具有资源,需要提供一个指向资源数组的指针,会在下个小节对该结构体进行详细的讲解。

51.1.4 resource 结构体

struct resource 结构体用于描述系统中的设备资源,包括内存区域、I/O 端口、中断等,该结构体定义在内核的“/include/linux/ioport.h”文件中,具体内容如下所示:

struct resource {
    resource_size_t start; /* 资源的起始地址*/
    resource_size_t end; /* 资源的结束地址*/
    const char *name; /* 资源的名称*/
    unsigned long flags; /* 资源的标志位*/
    unsigned long desc; /* 资源的描述信息*/
    struct resource *parent; /* 指向父资源的指针*/
    struct resource *sibling; /* 指向同级兄弟资源的指针*/
    struct resource *child; /* 指向子资源的指针*/
    
    /* 以下宏定义用于保留未使用的字段*/
    ANDROID_KABI_RESERVE(1);
    ANDROID_KABI_RESERVE(2);
    ANDROID_KABI_RESERVE(3);
    ANDROID_KABI_RESERVE(4);
};

其中最重要的是前四个参数,每个参数的具体介绍如下所示:

  • (1)resource_size_t start:资源的起始地址。它表示资源的起始位置或者起始寄存器的地址。
  • (2)resource_size_t end:资源的结束地址。它表示资源的结束位置或者结束寄存器的地址。
  • (3)const char *name:资源的名称。它是一个字符串,用于标识和描述资源。
  • (4)unsigned long flags:资源的标志位。它包含了一些特定的标志,用于表示资源的属性或者特征。例如,可以用标志位来指示资源的可用性、共享性、缓存属性等。flags 参数的具体取值和含义可以根据系统和驱动的需求进行定义和解释,但通常情况下,它用于表示资源的属性、特征或配置选项。下面是一些常见的标志位及其可能的含义:
  1. 资源类型相关标志位
IORESOURCE_IO:表示资源是I/O 端口资源。
IORESOURCE_MEM:表示资源是内存资源。
IORESOURCE_REG:表示资源是寄存器偏移量。
IORESOURCE_IRQ:表示资源是中断资源。
IORESOURCE_DMA:表示资源是DMA(直接内存访问)资源。
  1. 资源属性和特征相关标志位:
IORESOURCE_PREFETCH:表示资源是无副作用的预取资源。
IORESOURCE_READONLY:表示资源是只读的。
IORESOURCE_CACHEABLE:表示资源支持缓存。
IORESOURCE_RANGELENGTH:表示资源的范围长度。
IORESOURCE_SHADOWABLE:表示资源可以被影子资源替代。
IORESOURCE_SIZEALIGN:表示资源的大小表示对齐。
IORESOURCE_STARTALIGN:表示起始字段是对齐的。
IORESOURCE_MEM_64:表示资源是64 位内存资源。
IORESOURCE_WINDOW:表示资源由桥接器转发。
IORESOURCE_MUXED:表示资源是软件复用的。
IORESOURCE_SYSRAM:表示资源是系统RAM(修饰符)。
  1. 其他状态和控制标志位:
IORESOURCE_EXCLUSIVE:表示用户空间无法映射此资源。
IORESOURCE_DISABLED:表示资源当前被禁用。
IORESOURCE_UNSET:表示尚未分配地址给资源。
IORESOURCE_AUTO:表示地址由系统自动分配。
IORESOURCE_BUSY:表示驱动程序将此资源标记为繁忙。

51.2 实验程序的编写

本实验对应的网盘路径为:iTOP-RK3568 开发板【底板V1.7 版本】\03_【iTOP-RK3568开发板】指南教程\02_Linux 驱动配套资料\04_Linux 驱动例程\40_platform_device\。

本实验将注册一个名为”my_platform_device” 的平台设备,当注册平台设备时,该驱动程序提供了两个资源:一个内存资源和一个中断资源。这些资源被定义在名为my_resources 的结构体数组中,具体内容如下:

内存资源:
起始地址:MEM_START_ADDR(0xFDD60000)
结束地址:MEM_END_ADDR(0xFDD60004)
标记:IORESOURCE_MEM
中断资源:
中断资源号:IRQ_NUMBER(101)
标记:IORESOURCE_IRQ
编写完成的platform_device.c 代码如下所示:

#include <linux/module.h>
#include <linux/platform_device.h>
#include <linux/ioport.h>

#define MEM_START_ADDR 0xFDD60000
#define MEM_END_ADDR   0xFDD60004
#define IRQ_NUMBER     101

static struct resource my_resources[] = {
    {
        .start = MEM_START_ADDR,    // 内存资源起始地址
        .end = MEM_END_ADDR,        // 内存资源结束地址
        .flags = IORESOURCE_MEM,    // 标记为内存资源
    },
    {
        .start = IRQ_NUMBER,        // 中断资源号
        .end = IRQ_NUMBER,          // 中断资源号
        .flags = IORESOURCE_IRQ,    // 标记为中断资源
    },
};

static void my_platform_device_release(struct device *dev)
{
    // 释放资源的回调函数
}

static struct platform_device my_platform_device = {
    .name = "my_platform_device",                  // 设备名称
    .id = -1,                                      // 设备ID
    .num_resources = ARRAY_SIZE(my_resources),     // 资源数量
    .resource = my_resources,                      // 资源数组
    .dev.release = my_platform_device_release,     // 释放资源的回调函数
};

static int __init my_platform_device_init(void)
{
    int ret;

    ret = platform_device_register(&my_platform_device);   // 注册平台设备
    if (ret) {
        printk(KERN_ERR "Failed to register platform device\n");
        return ret;
    }

    printk(KERN_INFO "Platform device registered\n");
    return 0;
}

static void __exit my_platform_device_exit(void)
{
    platform_device_unregister(&my_platform_device);   // 注销平台设备
    printk(KERN_INFO "Platform device unregistered\n");
}

module_init(my_platform_device_init);
module_exit(my_platform_device_exit);

MODULE_LICENSE("GPL");
MODULE_AUTHOR("topeet");

51.3 运行测试

51.3.1 编译驱动程序

在上一小节中的platform_device.c 代码同一目录下创建Makefile 文件,Makefile 文件内容:然后使用命令“make”进行驱动的编译,编译完生成platform_device.ko 目标文件,至此驱动模块就编译成功了。

51.3.2 运行测试

开发板启动之后,使用以下命令进行驱动模块的加载,如下图(图51-4)所示:

insmod platform_device.ko

image-20240821141110282

然后来到/sys/bus/platform/devices 目录下,可以看到我们创建的my_platform_device 设备文件夹就成功生成了。

image-20240821141132962

然后使用以下命令进行驱动模块的卸载,如下图(图51-6)所示:

rmmod platform_device.ko

image-20240821141151341

至此,注册platform 设备实验就完成了。

第52 章注册platform 驱动实验

在上个章节我们学习了如何注册platform 设备,而本章节就要学习如何注册platform 驱动了。

52.1 注册platform 驱动

52.1.1 platform_driver_register 函数

platform_driver_register 函数用于在Linux 内核中注册一个平台驱动程序。下面是对该函数的详细介绍:

函数原型:
    int platform_driver_register(struct platform_driver *driver);
头文件:
    #include <linux/platform_device.h>
函数作用:
    platform_driver_register 函数用于将一个平台驱动程序注册到内核中。通过注册平台驱动程序,内核可以识别并与特定的平台设备进行匹配,并在需要时调用相应的回调函数。
    
参数含义:
    driver:指向struct platform_driver 结构体的指针,描述了要注册的平台驱动程序的属性和回调函数(会在下面的小节对该结构体进行详细的讲解)。
    
返回值:
    返回一个整数值,表示函数的执行状态。如果注册成功,返回0;如果注册失败,返回一个负数错误码。

该函数在内核源码目录下的“/include/linux/platform_device.h”文件中,具体内容如下所示:

#define platform_driver_register(drv) \
__platform_driver_register(drv, THIS_MODULE)
extern int __platform_driver_register(struct platform_driver *, struct module *);

这个宏用于简化平台驱动程序的注册过程。它将实际的注册函数__platform_driver_register 与当前模块(驱动程序)关联起来。宏的参数drv 是一个指向struct platform_driver 结构体的指针,描述了要注册的平台驱动程序的属性和回调函数。THIS_MODULE 是一个宏,用于获取当前模块的指针。

__platform_driver_register 实际定义在“/drivers/base/platform.c”文件中,相关定义如下所示:

int __platform_driver_register(struct platform_driver *drv, struct module *owner)
{
    drv->driver.owner = owner; // 将平台驱动程序的所有权设置为当前模块
    drv->driver.bus = &platform_bus_type; // 将平台驱动程序的总线类型设置为平台总线
    drv->driver.probe = platform_drv_probe; // 设置平台驱动程序的探测函数
    drv->driver.remove = platform_drv_remove; // 设置平台驱动程序的移除函数
    drv->driver.shutdown = platform_drv_shutdown;// 设置平台驱动程序的关机函数
    
    return driver_register(&drv->driver); // 将平台驱动程序注册到内核
}
  • 第3 行:将指向当前模块的指针owner 赋值给平台驱动程序的owner 成员。这样做是为了将当前模块与平台驱动程序关联起来,以确保模块的生命周期和驱动程序的注册和注销相关联。
  • 第4 行:将指向平台总线类型的指针&platform_bus_type 赋值给平台驱动程序的bus 成员。这样做是为了指定该驱动程序所属的总线类型为平台总线,以便内核能够将平台设备与正确的驱动程序进行匹配。
  • 第5 行:将指向平台驱动程序探测函数platform_drv_probe 的指针赋值给平台驱动程序的probe 成员。这样做是为了指定当内核发现与驱动程序匹配的平台设备时,要调用的驱动程序探测函数。
  • 第6 行:将指向平台驱动程序移除函数platform_drv_remove 的指针赋值给平台驱动程序的remove 成员。这样做是为了指定当内核需要从系统中移除与驱动程序匹配的平台设备时,要调用的驱动程序移除函数。
  • 第7 行:platform_drv_shutdown;:将指向平台驱动程序关机函数platform_drv_shutdown 的指针赋值给平台驱动程序的shutdown 成员。这样做是为了指定当系统关机时,要调用的驱动程序关机函数。
  • 第9 行:调用driver_register 函数,将平台驱动程序的driver 成员注册到内核中。该函数负责将驱动程序注册到相应的总线上,并在注册成功时返回0,注册失败时返回一个负数错误码。

通过这些操作,__platform_driver_register 函数将平台驱动程序与内核关联起来,并确保内核能够正确识别和调用驱动程序的各种回调函数,以实现与平台设备的交互和管理。函数的返回值表示注册过程的执行状态,以便在需要时进行错误处理。

52.1.2 platform_driver_unregister 函数

platform_driver_unregister 函数用于取消注册已经注册的平台驱动,即从内核中移除驱动。在驱动不再需要时,调用该函数可以进行设备的清理和释放操作。

函数原型:
    void platform_driver_unregister(struct platform_driver *pdev);
头文件:
    #include <linux/platform_device.h>
函数作用:
	platform_driver_unregister 函数用于从内核中注销平台驱动。通过调用该函数,可以将指定的平台设备从系统中移除。
    
参数含义:
	pdev:指向要注销的平台设备的指针。
返回值:
	无返回值。

该函数在内核源码目录下的“/include/linux/platform_device.h”文件中,具体内容如下所示:

extern void platform_driver_unregister(struct platform_driver *);

函数声明中的extern 关键字表示该函数在其他地方定义,而不是在当前文件中实现。这样的声明通常出现在头文件中,用于告诉编译器该函数的定义存在于其他源文件中,以便在编译时能够正确引用该函数。
platform_driver_unregister 实际定义在“/drivers/base/platform.c”文件中,相关定义如下所示:

void platform_driver_unregister(struct platform_driver *drv)
{
    driver_unregister(&drv->driver);
}

该函数又调用了driver_unregister 函数进行嵌套, 追踪之后找到定义在“/drivers/base/driver.c”目录下的driver_unregister 函数,具体内容如下所示:

void driver_unregister(struct device_driver *drv)
{
    // 检查传入的设备驱动程序指针和p 成员是否有效
    if (!drv || !drv->p) {
        WARN(1, "Unexpected driver unregister!\n");
        return;
    }
    driver_remove_groups(drv, drv->groups); // 移除与设备驱动程序关联的属性组
    bus_remove_driver(drv); // 从总线中移除设备驱动程序
}

函数内部有三个主要的操作:

  • 第4-7 行:检查传入的设备驱动程序指针drv 是否为空,或者驱动程序的p 成员是否为空。如果其中任何一个条件为真,表示传入的参数无效,会发出警告并返回。
  • 第9 行:调用driver_remove_groups 函数,用于从内核中移除与设备驱动程序关联的属性组。drv->groups 是指向属性组的指针,指定了要移除的属性组列表。
  • 第10 行:调用bus_remove_driver 函数,用于从总线中移除设备驱动程序。该函数会执行以下操作:
    • (1)从总线驱动程序列表中移除指定的设备驱动程序。
    • (2)调用与设备驱动程序关联的remove 回调函数(如果有定义)。
    • (3)释放设备驱动程序所占用的资源和内存。
    • (4)最终销毁设备驱动程序的数据结构。

通过调用driver_unregister 函数,可以正确地注销设备驱动程序,并在注销过程中进行必要的清理工作。这样可以避免资源泄漏和其他问题。在调用该函数后,应避免继续使用已注销的设备驱动程序指针,因为该驱动程序已不再存在于内核中。

52.1.3 platform_driver 结构体

platform_driver 结构体是Linux 内核中用于编写平台设备驱动程序的重要数据结构。它提供了与平台设备驱动相关的函数和数据成员,以便与平台设备进行交互和管理。该结构体定义在内核的“/include/linux/platform_device.h”文件中,具体内容如下所示:

struct platform_driver {
    int (*probe)(struct platform_device *); /* 平台设备的探测函数指针*/
    int (*remove)(struct platform_device *); /* 平台设备的移除函数指针*/
    void (*shutdown)(struct platform_device *);/* 平台设备的关闭函数指针*/
    int (*suspend)(struct platform_device *, pm_message_t state);/* 平台设备的挂起函数指针*/
    int (*resume)(struct platform_device *);/* 平台设备的恢复函数指针*/
    struct device_driver driver;/* 设备驱动程序的通用数据*/
    const struct platform_device_id *id_table;/* 平台设备与驱动程序的关联关系表*/
    bool prevent_deferred_probe; /* 是否阻止延迟探测*/
};
  • probe:平台设备的探测函数指针。当系统检测到一个平台设备与该驱动程序匹配时,该函数将被调用以初始化和配置设备。
  • remove:平台设备的移除函数指针。当平台设备从系统中移除时,该函数将被调用以执行清理和释放资源的操作。
  • shutdown:平台设备的关闭函数指针。当系统关闭时,该函数将被调用以执行与平台设备相关的关闭操作。
  • suspend:平台设备的挂起函数指针。当系统进入挂起状态时,该函数将被调用以执行与平台设备相关的挂起操作。
  • resume:平台设备的恢复函数指针。当系统从挂起状态恢复时,该函数将被调用以执行与平台设备相关的恢复操作。
  • driver:包含了与设备驱动程序相关的通用数据,它是struct device_driver 类型的实例。其中包括驱动程序的名称、总线类型、模块拥有者、属性组数组指针等信息,该结构体的name参数需要与上个章节的platform_device 的.name 参数相同才能匹配成功,从而进入probe 函数
  • id_table:指向struct platform_device_id 结构体数组的指针,用于匹配平台设备和驱动程序之间的关联关系。通过该关联关系,可以确定哪个平台设备与该驱动程序匹配,和.driver.name起到相同的作用,但是优先级高于.driver.name。
  • prevent_deferred_probe:一个布尔值,用于确定是否阻止延迟探测。如果设置为true,则延迟探测将被禁用。

使用struct platform_driver 结构体,开发人员可以定义平台设备驱动程序,并将其注册到内核中。当系统检测到与该驱动程序匹配的平台设备时,内核将调用相应的函数来执行设备的初始化、配置、操作和管理。驱动程序可以利用提供的函数指针和通用数据与平台设备进行交互,并提供必要的功能和服务。

需要注意的是,struct platform_driver 结构体继承了struct device_driver 结构体,因此可以直接访问struct device_driver 中定义的成员。这使得平台驱动程序可以利用通用的驱动程序机制,并与其他类型的设备驱动程序共享代码和功能。

52.2 实验程序的编写

本实验对应的网盘路径为:iTOP-RK3568 开发板【底板V1.7 版本】\03_【iTOP-RK3568开发板】指南教程\02_Linux 驱动配套资料\04_Linux 驱动例程\41。
本小节的实验只是编写一个platform 驱动的一个大体框架,在下一个章节中再讲解platform 设备和platform 驱动的匹配

编写完成的platform_driver.c 代码如下所示:

#include <linux/module.h>
#include <linux/platform_device.h>

// 平台设备的探测函数
static int my_platform_probe(struct platform_device *pdev)
{
    printk(KERN_INFO "my_platform_probe: Probing platform device\n");

    // 添加设备特定的操作
    // ...

    return 0;
}

// 平台设备的移除函数
static int my_platform_remove(struct platform_device *pdev)
{
    printk(KERN_INFO "my_platform_remove: Removing platform device\n");

    // 清理设备特定的操作
    // ...

    return 0;
}

// 定义平台驱动结构体
static struct platform_driver my_platform_driver = {
    .probe = my_platform_probe,
    .remove = my_platform_remove,
    .driver = {
        .name = "my_platform_device",
        .owner = THIS_MODULE,
    },
};

// 模块初始化函数
static int __init my_platform_driver_init(void)
{
    int ret;

    // 注册平台驱动
    ret = platform_driver_register(&my_platform_driver);
    if (ret) {
        printk(KERN_ERR "Failed to register platform driver\n");
        return ret;
    }

    printk(KERN_INFO "my_platform_driver: Platform driver initialized\n");

    return 0;
}

// 模块退出函数
static void __exit my_platform_driver_exit(void)
{
    // 注销平台驱动
    platform_driver_unregister(&my_platform_driver);

    printk(KERN_INFO "my_platform_driver: Platform driver exited\n");
}

module_init(my_platform_driver_init);
module_exit(my_platform_driver_exit);

MODULE_LICENSE("GPL");
MODULE_AUTHOR("topeet");

52.3 运行测试

52.3.1 编译驱动程序

在上一小节中的platform_driver.c 代码同一目录下创建Makefile 文件,Makefile 文件内容:然后使用命令“make”进行驱动的编译,编译完生成platform_driver.ko 目标文件,至此驱动模块就编译成功了。

52.3.2 运行测试

本小节的测试需要用到两个驱动ko 文件,即上一章节的注册platform 设备ko 文件和本章节的注册platform 驱动ko 文件。
开发板启动之后,使用以下命令进行驱动模块的加载,如下图(图21-7)所示:

insmod platform_driver.ko

image-20240821143528898

然后来到/sys/bus/platform/drivers 目录下,可以看到我们创建的my_platform_driver 驱动文件夹就成功生成了。

image-20240821143541363

然后使用以下命令加载注册platform 设备ko 文件,加载成功之后如下图所示:

insmod platform_device.ko

image-20240821143559792

可以看到匹配成功之后就会进入probe 函数,显示出了相应的打印(加载上述两个ko 文件不分先后顺序)。然后使用以下命令进行驱动模块的卸载,如下图(图21-7)所示:

rmmod platform_driver.ko
rmmod platform_device.ko

image-20240821143629702

至此,注册platform 驱动实验就完成了。

第53 章probe 函数编写实验

在上面的两个章节中分别注册了platform 设备和platform 驱动,匹配成功之后会进入在注册platform 驱动程序中编写的probe 函数,在上个章节只是为了验证是否匹配成功,所以只是在probe 中加入了一句相关打印,而驱动是要控制硬件的,但是平台总线模型对硬件的描述写在了platform_device.c 中,platform 设备和platform 驱动匹配成功之后,那我们如何在驱动platform_driver.c 的probe 函数中,得到platform_device.c 中编写的硬件资源呢。下面开始本节课程的学习吧。

53.1 获取device 资源

方法1:直接访问platform_device 结构体的资源数组

在上一章节的讲解中提到:struct platform_driver 结构体继承了struct device_driver 结构体,因此可以直接访问struct device_driver 中定义的成员。实例代码如下所示:

if (pdev->num_resources >= 2) {
    struct resource *res_mem = &pdev->resource[0];
    struct resource *res_irq = &pdev->resource[1];
    
    // 使用获取到的硬件资源进行处理
    printk("Method 1: Memory Resource: start = 0x%lld, end = 0x%lld\n", 
           res_mem->start, res_mem->end);
    printk("Method 1: IRQ Resource: number = %lld\n", res_irq->start);
}

在这种方法中, 直接访问platform_device 结构体的资源数组来获取硬件资源。pdev->resource 是一个资源数组,其中存储了设备的硬件资源信息。通过访问数组的不同索引,可以获取到特定的资源。

在这个示例中,假设资源数组的第一个元素是内存资源,第二个元素是中断资源。所以我们将第一个元素的指针赋值给res_mem,第二个元素的指针赋值给res_irq。

方法2:使用platform_get_resource() 获取硬件资源

platform_get_resource()函数用于获取设备的资源信息。它的声明位于<linux/platform_device.h>头文件中,与平台设备(platform_device)相关。

函数原型:
struct resource *platform_get_resource(struct platform_device *pdev, unsigned int type, 
                                       unsigned int num);
参数说明:
    pdev:指向要获取资源的平台设备(platform_device)结构体的指针。
    type:指定资源的类型,可以是以下值之一:
        IORESOURCE_MEM:表示内存资源。
        IORESOURCE_IO:表示I/O 资源。
        IORESOURCE_IRQ:表示中断资源。
        其他资源类型的宏定义可在<linux/ioport.h><linux/irq.h>头文件中找到。
    num:指定要获取的资源的索引。在一个设备中可能存在多个相同类型的资源,通过索引可以选择获取特定的资源。
返回值:
    如果成功获取资源,则返回指向资源(struct resource)的指针。
    如果获取资源失败,或者指定的资源不存在,则返回NULL

platform_get_resource()函数用于从平台设备的资源数组中获取指定类型和索引的资源。在平台设备的资源数组中,每个元素都是一个struct resource 结构体,描述了一个资源的信息,如起始地址、结束地址、中断号等。

示例用法:

struct platform_device *pdev;
struct resource *res;
res = platform_get_resource(pdev, IORESOURCE_MEM, 0);
if (!res) {
    // 处理获取内存资源失败的情况
}
// 使用获取到的内存资源进行处理
unsigned long start = res->start;
unsigned long end = res->end;
...

在上述示例中,首先通过platform_get_resource()函数获取平台设备的第一个内存资源(索引为0)。如果获取资源失败(返回NULL),则可以根据实际情况进行错误处理。如果获取资源成功,则可以使用返回的资源指针来访问资源的信息,如起始地址和结束地址。

通过platform_get_resource()函数,可以方便地在驱动程序中获取平台设备的资源信息,并根据这些信息进行后续的操作和配置。

53.2 实验程序的编写

本实验对应的网盘路径为:iTOP-RK3568 开发板【底板V1.7 版本】\03_【iTOP-RK3568开发板】指南教程\02_Linux 驱动配套资料\04_Linux 驱动例程\42_probe。

在上一章程序的基础上,添加第一小节两种获取设备资源的方式并打印出来。编写完成的probe.c 代码如下所示。

#include <linux/module.h>
#include <linux/platform_device.h>
#include <linux/ioport.h>

static int my_platform_driver_probe(struct platform_device *pdev)
{
    struct resource *res_mem, *res_irq;

    // 方法1:直接访问 platform_device 结构体的资源数组
    if (pdev->num_resources >= 2) {
        struct resource *res_mem = &pdev->resource[0];
        struct resource *res_irq = &pdev->resource[1];

        // 使用获取到的硬件资源进行处理
        printk("Method 1: Memory Resource: start = 0x%llx, end = 0x%llx\n",
                res_mem->start, res_mem->end);
        printk("Method 1: IRQ Resource: number = %lld\n", res_irq->start);
    }

    // 方法2:使用 platform_get_resource() 获取硬件资源
    res_mem = platform_get_resource(pdev, IORESOURCE_MEM, 0);
    if (!res_mem) {
        dev_err(&pdev->dev, "Failed to get memory resource\n");
        return -ENODEV;
    }

    res_irq = platform_get_resource(pdev, IORESOURCE_IRQ, 0);
    if (!res_irq) {
        dev_err(&pdev->dev, "Failed to get IRQ resource\n");
        return -ENODEV;
    }

    // 使用获取到的硬件资源进行处理
    printk("Method 2: Memory Resource: start = 0x%llx, end = 0x%llx\n",
            res_mem->start, res_mem->end);
    printk("Method 2: IRQ Resource: number = %lld\n", res_irq->start);

    return 0;
}

static int my_platform_driver_remove(struct platform_device *pdev)
{
    // 设备移除操作
    return 0;
}

static struct platform_driver my_platform_driver = {
    .driver = {
        .name = "my_platform_device", // 与 platform_device.c 中的设备名称匹配
        .owner = THIS_MODULE,
    },
    .probe = my_platform_driver_probe,
    .remove = my_platform_driver_remove,
};

static int __init my_platform_driver_init(void)
{
    int ret;

    ret = platform_driver_register(&my_platform_driver); // 注册平台驱动
    if (ret) {
        printk("Failed to register platform driver\n");
        return ret;
    }

    printk("Platform driver registered\n");
    return 0;
}

static void __exit my_platform_driver_exit(void)
{
    platform_driver_unregister(&my_platform_driver); // 注销平台驱动
    printk("Platform driver unregistered\n");
}

module_init(my_platform_driver_init);
module_exit(my_platform_driver_exit);

MODULE_LICENSE("GPL");
MODULE_AUTHOR("topeet");

53.3 运行测试

53.3.1 编译驱动程序

在上一小节中的probe.c 代码同一目录下创建Makefile 文件,Makefile 文件内容:然后使用命令“make”进行驱动的编译,编译完生成probe.ko 目标文件,至此驱动模块就编译成功了。

53.3.2 运行测试

本小节的测试要使用两个ko 文件,第一个ko 文件为第53 章编译出来的platform_device.ko驱动,第二个ko 文件为在上一小节编译出的probe.ko 驱动文件。
开发板启动之后,首先使用以下命令进行platform 设备的注册,如下图(图53-4)所示:

insmod platform_device.ko

image-20240821144321781

然后继续使用以下命令加载probe.ko 驱动,打印如下图(53-5)所示:

insmod probe.ko

image-20240821144331619

在上图中,打印了两种方式下获取得到的内存信息和中断信息,最后可以使用以下命令进行驱动的卸载,如下图(图53-6)所示:

rmmod probe.ko
rmmod platform_device.ko

image-20240821144349804

第54 章点亮LED 灯实验(平台总线)

在上个章节中,我们成功在platform 驱动程序中读取到了设备资源信息,在本章节将进行具体的项目实践,要求在上节platform 驱动程序的基础上,加入控制LED 灯相关的代码(这部分代码可以参考“第18 章点亮LED 灯实验”)。

54.1 实验程序的编写

54.1.1 驱动程序编写

本实验对应的网盘路径为:iTOP-RK3568 开发板【底板V1.7 版本】\03_【iTOP-RK3568开发板】指南教程\02_Linux 驱动配套资料\04_Linux 驱动例程\43_platform_led\module
编写完成的platform_led.c 代码如下所示,添加的代码已加粗表示。

#include <linux/module.h>
#include <linux/platform_device.h>
#include <linux/ioport.h>
#include <linux/kdev_t.h>
#include <linux/fs.h>
#include <linux/cdev.h>
#include <linux/uaccess.h>
#include <linux/io.h>

struct device_test{

    dev_t dev_num;  //设备号
     int major ;  //主设备号
    int minor ;  //次设备号
    struct cdev cdev_test; // cdev
    struct class *class;   //类
    struct device *device; //设备
    char kbuf[32];
    unsigned int *vir_gpio_dr;
};

struct  device_test dev1;


/*打开设备函数*/
static int cdev_test_open(struct inode *inode, struct file *file)
{
    file->private_data=&dev1;//设置私有数据
    printk("This is cdev_test_open\r\n");

    return 0;
}

/*向设备写入数据函数*/
static ssize_t cdev_test_write(struct file *file, const char __user *buf, size_t size, loff_t *off)
{
     struct device_test *test_dev=(struct device_test *)file->private_data;

    if (copy_from_user(test_dev->kbuf, buf, size) != 0) // copy_from_user:用户空间向内核空间传数据
    {
        printk("copy_from_user error\r\n");
        return -1;
    }
    if(test_dev->kbuf[0]==1){   //如果应用层传入的数据是1,则打开灯
            *(test_dev->vir_gpio_dr) = 0x8000c040;   //设置数据寄存器的地址
              printk("test_dev->kbuf [0]  is %d\n",test_dev->kbuf[0]);  //打印传入的数据
    }
    else if(test_dev->kbuf[0]==0)  //如果应用层传入的数据是0,则关闭灯
    {
            *(test_dev->vir_gpio_dr) = 0x80004040; //设置数据寄存器的地址
            printk("test_dev->kbuf [0]  is %d\n",test_dev->kbuf[0]); //打印传入的数据
    }
    return 0;
}

/**从设备读取数据*/
static ssize_t cdev_test_read(struct file *file, char __user *buf, size_t size, loff_t *off)
{

    struct device_test *test_dev=(struct device_test *)file->private_data;

    if (copy_to_user(buf, test_dev->kbuf, strlen( test_dev->kbuf)) != 0) // copy_to_user:内核空间向用户空间传数据
    {
        printk("copy_to_user error\r\n");
        return -1;
    }

    printk("This is cdev_test_read\r\n");
    return 0;
}

static int cdev_test_release(struct inode *inode, struct file *file)
{
    printk("This is cdev_test_release\r\n");
    return 0;
}

/*设备操作函数*/
struct file_operations cdev_test_fops = {
    .owner = THIS_MODULE, //将owner字段指向本模块,可以避免在模块的操作正在被使用时卸载该模块
    .open = cdev_test_open, //将open字段指向chrdev_open(...)函数
    .read = cdev_test_read, //将open字段指向chrdev_read(...)函数
    .write = cdev_test_write, //将open字段指向chrdev_write(...)函数
    .release = cdev_test_release, //将open字段指向chrdev_release(...)函数
};

static int my_platform_driver_probe(struct platform_device *pdev)
{
    struct resource *res_mem;
	int ret;
	res_mem = platform_get_resource(pdev, IORESOURCE_MEM, 0);
    if (!res_mem) {
        dev_err(&pdev->dev, "Failed to get memory resource\n");
        return -ENODEV;
    }

	/*注册字符设备驱动*/
    /*1 创建设备号*/
    ret = alloc_chrdev_region(&dev1.dev_num, 0, 1, "alloc_name"); //动态分配设备号
    if (ret < 0)
    {
       goto err_chrdev;
    }
    printk("alloc_chrdev_region is ok\n");

    dev1.major = MAJOR(dev1.dev_num); //获取主设备号
   dev1.minor = MINOR(dev1.dev_num); //获取次设备号

    printk("major is %d \r\n", dev1.major); //打印主设备号
    printk("minor is %d \r\n", dev1.minor); //打印次设备号
     /*2 初始化cdev*/
    dev1.cdev_test.owner = THIS_MODULE;
    cdev_init(&dev1.cdev_test, &cdev_test_fops);

    /*3 添加一个cdev,完成字符设备注册到内核*/
   ret =  cdev_add(&dev1.cdev_test, dev1.dev_num, 1);
    if(ret<0)
    {
        goto  err_chr_add;
    }
    /*4 创建类*/
  dev1. class = class_create(THIS_MODULE, "test");
    if(IS_ERR(dev1.class))
    {
        ret=PTR_ERR(dev1.class);
        goto err_class_create;
    }
    /*5  创建设备*/
  dev1.device = device_create(dev1.class, NULL, dev1.dev_num, NULL, "test");
    if(IS_ERR(dev1.device))
    {
        ret=PTR_ERR(dev1.device);
        goto err_device_create;
    }
    dev1.vir_gpio_dr=ioremap(res_mem->start,4);  //将物理地址转化为虚拟地址
    if(IS_ERR(dev1.vir_gpio_dr))
    {
        ret=PTR_ERR(dev1.vir_gpio_dr);  //PTR_ERR()来返回错误代码
        goto err_ioremap;
    }


return 0;

err_ioremap:
        iounmap(dev1.vir_gpio_dr);

 err_device_create:
        class_destroy(dev1.class);                 //删除类

err_class_create:
       cdev_del(&dev1.cdev_test);                 //删除cdev

err_chr_add:
        unregister_chrdev_region(dev1.dev_num, 1); //注销设备号

err_chrdev:
        return ret;
}

static int my_platform_driver_remove(struct platform_device *pdev)
{
    // 设备移除操作
    return 0;
}

static struct platform_driver my_platform_driver = {
    .driver = {
        .name = "my_platform_device", // 与 platform_device.c 中的设备名称匹配
        .owner = THIS_MODULE,
    },
    .probe = my_platform_driver_probe,
    .remove = my_platform_driver_remove,
};

static int __init my_platform_driver_init(void)
{
    int ret;

    ret = platform_driver_register(&my_platform_driver); // 注册平台驱动
    if (ret) {
        printk("Failed to register platform driver\n");
        return ret;
    }

    printk("Platform driver registered\n");
    return 0;
}

static void __exit my_platform_driver_exit(void)
{
        /*注销字符设备*/
    unregister_chrdev_region(dev1.dev_num, 1); //注销设备号
    cdev_del(&dev1.cdev_test);                 //删除cdev
    device_destroy(dev1.class, dev1.dev_num);       //删除设备
    class_destroy(dev1.class);                 //删除类
	platform_driver_unregister(&my_platform_driver); // 注销平台驱动
    printk("Platform driver unregistered\n");
}

module_init(my_platform_driver_init);
module_exit(my_platform_driver_exit);

MODULE_LICENSE("GPL");
MODULE_AUTHOR("topeet");

54.1.2 编写测试APP

本应用程序对应的网盘路径为:iTOP-RK3568 开发板【底板V1.7 版本】\03_【iTOP-RK3568开发板】指南教程\02_Linux 驱动配套资料\04_Linux 驱动例程\43_platform_led\app
编写测试app,led 驱动加载成功之后会生成/dev/test 节点,应用程序APP 通过操作/dev/test文件来完成对LED 设备的控制。向/dev/test 文件写入0 表示关闭LED 灯,写入1 表示打开LED灯。编写完成的应用程序app.c 代码如下所示:

#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <stdlib.h>

int main(int argc, char *argv[])  
{
    int fd;
    char buf[32] = {0};   
    fd = open("/dev/test", O_RDWR);  //打开led驱动
    if (fd < 0)
    {
        perror("open error \n");
        return fd;
    }
    buf[0] =atoi(argv[1]);    // atoi()将字符串转为整型,这里将第一个参数转化为整型后,存放在 buf[0]中
    write(fd,buf,sizeof(buf));  //向/dev/test文件写入数据
    close(fd);     //关闭文件
    return 0;
}

54.2 运行测试

54.2.1 编译驱动程序

在上一小节中的platform_led.c 代码同一目录下创建Makefile 文件,Makefile 文件内容:然后使用命令“make”进行驱动的编译,编译完生成platform_led.ko 目标文件,至此驱动模块就编译成功了。

54.2.2 编译应用程序

下面进行应用程序编译, 因为测试APP 是要在开发板上运行的, 所以需要aarch64-linux-gnu-gcc 来编译,输入以下命令,编译完成以后会生成一个app 的可执行程序,如下图(图54-4)所示:

aarch64-linux-gnu-gcc app.c -o app

54.2.3 运行测试

本小节的测试要使用两个ko 文件和一个测试应用程序,第一个ko 文件为第53 章编译出来的platform_device.ko 驱动,第二个ko 文件为在上一小节编译出的probe.ko 驱动文件,应用程序为上一小节编译出来的app。
开发板启动之后,首先使用以下命令进行platform 设备的注册,如下图(图54-5)所示:

insmod platform_device.ko
image-20240821144932358

然后继续使用以下命令加载platform_led.ko 驱动,打印如下图(54-6)所示:

insmod platform_led.ko

image-20240821145002003

可以看到led 字符设备成功注册了,主设备号为236,次设备号为0,相应的test 节点也成功创建了,如下图(54-7)所示:

image-20240821145016429

默认情况下led 灯的状态为常亮,然后输入“./app 0”命令LED 灯熄灭,如下图(图54-8)所示:

image-20240821145049309

image-20240821145055194

然后输入“./app 0”,LED 灯点亮,如下图(图54-10)所示:

image-20240821145113586

image-20240821145124112

最后可以使用以下命令进行驱动的卸载,如下图(图54-12)所示:

rmmod platform_led.ko
rmmod platform_device.ko

image-20240821145154668

至此,使用平台总线的点亮LCD 灯实验就完成了。


文章作者: 葛杨文
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 葛杨文 !
评论
  目录