第20章 原子操作实验(iTOP-RK3568开发板驱动开发指南 )

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

本章首先对四种常见方法中的原子操作进行讲解。

20.1 原子操作

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

c 复制代码
int v;//定义一个int类型的变量v
v = 1;//将int类型的变量v赋值为1

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

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

c 复制代码
 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,如果结果为负就返回真,否则返回假

图表20- 1

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

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

函数 描述
void set_bit(int nr, void *p) 将 p 地址的第 nr 位置 1。
void clear_bit(int nr,void *p) 将 p 地址的第 nr 位清零。
void change_bit(int nr, void *p) 将 p 地址的第 nr 位进行翻转。
int test_bit(int nr, void *p) 获取 p 地址的第 nr 位的值。
int test_and_set_bit(int nr, void *p) 将 p 地址的第 nr 位置 1,并且返回 nr 位原来的值。
int test_and_clear_bit(int nr, void *p) 将 p 地址的第 nr 位清零,并且返回 nr 位原来的值。
int test_and_change_bit(int nr, void *p) 将 p 地址的第 nr 位翻转,并且返回 nr 位原来的值。

图表20- 2

对于原子位操作的知识就不再深入讲解和实验,感兴趣的同学可以到相关网站上进行自主学习。

在下一小节中,将会使用原子整形操作对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代码如下所示

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内容如下所示:

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 运行测试

20.3.1 编译驱动程序

在上一小节中的atomic.c代码同一目录下创建 Makefile 文件,Makefile 文件内容如下所示:

makefile 复制代码
export ARCH=arm64#设置平台架构
export CROSS_COMPILE=aarch64-linux-gnu-#交叉编译器前缀
obj-m += atomic.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的内容注释已在上图添加,保存退出之后,来到存放atomic.c和Makefile文件目录下,如下图(图20-4)所示:

图 20-4

然后使用命令"make"进行驱动的编译,编译完成如下图(图20-5)所示:

图 20-5

编译完生成atomic.ko目标文件,如下图(图20-6)所示:

图 20-6

至此驱动模块就编译成功了,下面进行应用程序的编译。

20.3.2 编译应用程序

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

图 20-7

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

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

图 20-8

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

20.3.3 运行测试

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

insmod atomic.ko

图 20-9

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

 ls /dev/device_test

图 20-10

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

./app /dev/device_test topeet

图 20-11

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

./app /dev/device_test topeet &

./app /dev/device_test itop 

图 20-12

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

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

rmmod flag.ko

图 20-13

p /dev/device_test itop

[外链图片转存中...(img-ggVvfwTR-1694222528726)] 

图 20-12

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

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

rmmod flag.ko

[外链图片转存中...(img-hcalLUxj-1694222528726)] 

图 20-13

至此,原子操作实验就完成了。
相关推荐
gopher95111 小时前
linux驱动开发-中断子系统
linux·运维·驱动开发
gopher95111 天前
linux驱动开发-设备树
linux·驱动开发
三菱-Liu2 天前
三菱变频器以模拟量电流进行频率设定(电流输入)
驱动开发·单片机·嵌入式硬件·硬件工程·制造
三菱-Liu2 天前
三菱FX5U CPU 内置以太网功能
网络·驱动开发·硬件工程·制造·mr
让开,我要吃人了2 天前
OpenHarmony鸿蒙( Beta5.0)摄像头实践开发详解
驱动开发·华为·移动开发·harmonyos·鸿蒙·鸿蒙系统·openharmony
OH五星上将3 天前
如何更换OpenHarmony SDK API 10
驱动开发·嵌入式硬件·sdk·harmonyos·openharmony·鸿蒙开发
OH五星上将4 天前
OpenHarmony(鸿蒙南向开发)——标准系统移植指南(二)Linux内核
linux·驱动开发·嵌入式硬件·移动开发·harmonyos·鸿蒙开发·鸿蒙内核
芊言芊语5 天前
蓝牙驱动开发详解
驱动开发
让开,我要吃人了5 天前
OpenHarmony鸿蒙( Beta5.0)RTSPServer实现播放视频详解
驱动开发·嵌入式硬件·华为·移动开发·harmonyos·鸿蒙·openharmony
OH五星上将5 天前
OpenHarmony(鸿蒙南向开发)——轻量和小型系统三方库移植指南(二)
驱动开发·移动开发·harmonyos·内存管理·openharmony·鸿蒙内核·鸿蒙移植