在上一节中对原子操作进行了讲解,并使用原子整形操作对并发与竞争实验进行了改进,但是原子操作只能对整形变量或者位进行保护,而对于结构体或者其他类型的共享资源,原子操作就力不从心了,这时候就轮到自旋锁的出场了,下面就让我们一起来进行自旋锁的学习吧。
自旋锁是为了保护共享资源提出的一种锁机制。自旋锁(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。 |
表 21-1
除了上述API之外还有其他与终端相关的自旋锁API函数,会在接下来的自旋锁死锁章节进行讲解。
自旋锁的使用步骤:
1 在访问临界资源的时候先申请自旋锁
2 获取到自旋锁之后就进入临界区,获取不到自旋锁就“原地等待”。
3 退出临界区的时候要释放自旋锁。
在下一小节中将使用上述自旋锁API进行相应的实验,利用自旋锁相关知识来对第19章节的并发与竞争实验进行优化。
本实验对应的网盘路径为:iTOP-RK3568开发板【底板V1.7版本】\03_【iTOP-RK3568开发板】指南教程\02_Linux驱动配套资料\04_Linux驱动例程\16\module。
与上一章节使用原子整形操作避免并发与竞争逻辑相同,在驱动入口函数初始化自旋锁,然后在open函数中使用自旋锁实现对设备的互斥访问,最后在 release 函数中解锁,表示设备被释放了,可以被其他的应用程序使用。上述操作都将共享资源由自旋锁进行保护,从而实现同一时间内只允许一个应用打开该设备节点,以此来防止共享资源竞争的产生。
编写完成的spinlock.c代码如下所示
#include
#include
#include
#include
#include
#include
#include
#include
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");
本实验应用程序对应的网盘路径为:iTOP-RK3568开发板【底板V1.7版本】\03_【iTOP-RK3568开发板】指南教程\02_Linux驱动配套资料\04_Linux驱动例程\16\app。
本测试app代码和上一章节相同,需要输入两个参数,第一个参数为对应的设备节点,第二个参数为“topeet”或者“itop”,分别代表向设备写入的数据,编写完成的应用程序app.c内容如下所示:
#include
#include
#include
#include
#include
#include
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;
}
在上一小节中的spinlock.c代码同一目录下创建 Makefile 文件,Makefile 文件内容如下所示:
export ARCH=arm64#设置平台架构
export CROSS_COMPILE=aarch64-linux-gnu-#交叉编译器前缀
obj-m += spinlock.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的内容注释已在上图添加,保存退出之后,来到存放spinlock.c和Makefile文件目录下,如下图(图21-2)所示:
图 21-2
然后使用命令“make”进行驱动的编译,编译完成如下图(图21-3)所示:
图 21-3
编译完生成spinlock.ko目标文件,如下图(图21-4)所示:
图 21-4
至此驱动模块就编译成功了,下面进行应用程序的编译。
来到应用程序app.c文件的存放路径如下图(图21-5)所示:
图 21-5
然后使用以下命令对app.c进行交叉编译,编译完成如下图(图21-6)所示:
aarch64-linux-gnu-gcc -o app app.c -static
图 21-6
生成的app文件就是之后放在开发板上运行的可执行文件,至此应用程序的编译就完成了。
开发板启动之后,使用以下命令进行驱动模块的加载,如下图(图21-7)所示:
insmod spinlock.ko
图21-7
可以看到申请的主设备号和次设备号就被打印了出来,然后使用以下代码对自动生成的设备节点device_test进行查看,如下图(21-8)所示:
ls /dev/device_test
图 21-8
可以看到device_test节点已经被自动创建了,然后使用以下命令运行测试app,运行结果如下图(图21-9)所示:
./app /dev/device_test topeet
图 21-9
可以看到传递的buf值为topeet,然后输入以下命令在后台运行两个app,来进行竞争测试,运行结果如下图(图21-10)所示:
./app /dev/device_test topeet &
./app /dev/device_test itop
图 21-10
可以看到应用程序在打开第二次/dev/device_test 文件的时候,出现了“file open failed”打印,证明文件打开失败,只有在第一个应用关闭相应的文件之后,下一个应用才能打开。本次实验的自旋锁只是对标志位flag进行保护,flag用来表示设备的状态,确保同一时间内,该设备只能被一个应用程序打开。进而对共享资源进行保护。
最后可以使用以下命令进行驱动的卸载,如下图(图21-11)所示:
rmmod spinlock.ko
图 21-11
至此,自旋锁实验就完成了。
【最新驱动资料(文档+例程)】
链接 https://pan.baidu.com/s/1M4smUG2vw_hnn0Hye-tkog
提取码:hbh6
【B 站配套视频】
https://b23.tv/XqYa6Hm
【RK3568 购买链接】
https://item.taobao.com/item.htm?spm=a1z10.5-c-s.w4002-2245
以使用以下命令进行驱动的卸载,如下图(图21-11)所示:
rmmod spinlock.ko
[外链图片转存中…(img-yiRoqXk4-1694222614580)]
图 21-11
至此,自旋锁实验就完成了。
【最新驱动资料(文档+例程)】
链接 https://pan.baidu.com/s/1M4smUG2vw_hnn0Hye-tkog
提取码:hbh6
【B 站配套视频】
https://b23.tv/XqYa6Hm
【RK3568 购买链接】
https://item.taobao.com/item.htm?spm=a1z10.5-c-s.w4002-2245
2452613.11.2fec74a6elWNeA&id=669939423234