两个 app 应用程序之间对共享资源的竞争访问引起了数据传输错误, 而在 Linux 内核中, 提供了四种处理并发与竞争的常见方法:
分别是原子操作、 自旋锁、 信号量、 互斥体, 这里了解下原子操作
前面了解了自旋锁,这里重点看自旋锁死锁问题了解、分析。
文章目录
参考资料
前面了解了原子操作和自旋锁,当然还有之前的字符设备相关操作,前面基础知识还是需要重点掌握的,才能将知识点串联起来:
接下来还是以前面字符设备 动态参数传递实验为基础,打开访问字符设备实验。 所以以前知识点 建议了解
在字符设备这块内容,所有知识点都是串联起来的,需要整体来理解,缺一不可,建议多了解一下基础知识
驱动-申请字符设备号
驱动-注册字符设备
驱动-创建设备节点
驱动-字符设备驱动框架
驱动-杂项设备
驱动-内核空间和用户空间数据交换
驱动-文件私有数据
Linux驱动之 原子操作
Linux驱动—原子操作
驱动-自旋锁
自旋锁死锁 概念
死锁是指两个或多个事物在同一资源上相互占用, 并请求锁定对方的资源, 从而导致恶性循环的现象。 当多个进程因竞争资源而造成的一种僵局(互相等待) , 若无外力作用, 这些进程都将无法向前推进, 这种情况就是死锁。
自旋锁死锁发生存在两种情况:
-
第一种情况是拥有自旋锁的进程 A 在内核态阻塞了, 内核调度 B 进程, 碰巧 B 进程也要获得自旋锁, 此时 B 只能自旋转。 而此时抢占已经关闭(在单核条件下)不会调度 A 进程了,B 永远自旋, 产生死锁
相应的解决办法是, 在自旋锁的使用过程中要尽可能短的时间内拥有自旋锁, 而且不能在临界区中调用导致线程休眠的函数 -
第二种情况是进程 A 拥有自旋锁, 中断到来, CPU 执行中断函数, 中断处理函数, 中断处理函数需要获得自旋锁, 访问共享资源, 此时无法获得锁, 只能自旋, 从而产生死锁
对于中断引发的死锁, 最好的解决方法就是在获取锁之前关闭本地中断
这里从概念上面理解,尝试思考死锁情况。
自旋锁死锁的常见原因
递归锁定
spin_lock(&lock);
spin_lock(&lock); // 第二次尝试获取已持有的锁,导致死锁
中断上下文与进程上下文竞争
// 进程上下文
spin_lock(&lock);
// 中断处理程序
spin_lock(&lock); // 如果中断发生在进程持有锁时,导致死锁
锁顺序不一致
// 线程A
spin_lock(&lock1);
spin_lock(&lock2);
// 线程B
spin_lock(&lock2);
spin_lock(&lock1); // 可能导致死锁
长时间持有自旋锁
spin_lock(&lock);
msleep(1000); // 睡眠期间可能造成其他CPU空转
spin_unlock(&lock);
实验
这里以 自旋锁 驱动程序进行实验,实验自旋锁死锁问题
在 open()函数中加入了自旋锁加锁, 在 close()函数中加入了自旋锁解锁, 由于在 write()函数中存在 sleep()睡眠函数, 所以会造成内核阻塞,睡眠期间如果使用另一个进程获取该自旋锁, 就会造成死锁
驱动-原子操作实验
实验源码 spinlock.c
#include <linux/init.h>
#include <linux/module.h>
#include <linux/fs.h>
#include <linux/kdev_t.h>
#include <linux/cdev.h>
#include <linux/uaccess.h>
#include <linux/delay.h>
#include <linux/atomic.h>
#include <linux/errno.h>
static spinlock_t spinlock_test;//定义spinlock_t类型自旋变量
static int flag=1;
static int open_test(struct inode *inode,struct file *file)
{
spin_lock(&spinlock_test); //自旋枷锁
if(flag!=1){ //判断标志位flag 的值是否等于1
spin_unlock(&spinlock_test);
return -EBUSY;
}
flag=0; //将标志位的值设置为0
spin_unlock(&spinlock_test); //自旋锁解锁
printk("\n this 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");
spin_lock(&spinlock_test);//自旋锁加锁
flag = 1;
spin_unlock(&spinlock_test);//自旋锁解锁
return 0;
}
struct chrdev_test
{
dev_t dev_num; //定义dev_t类型变量来表示设备号
int major,minor; //定义int 类型的主设备号和次设备号
struct cdev cdev_test; //定义字符设备
struct class *class_test; //定义结构体变量class 类
};
struct chrdev_test dev1; //创建chardev_test类型结构体变量
static struct file_operations fops_test = {
.owner=THIS_MODULE,//将owner字段指向本模块,可以避免在模块的操作正在被使用时卸载该模块
.open = open_test,//将open字段指向chrdev_open(...)函数
.read = read_test,//将open字段指向chrdev_read(...)函数
.write = write_test,//将open字段指向chrdev_write(...)函数
.release = release_test,//将open字段指向chrdev_release(...)函数
};//定义file_operations结构体类型的变量cdev_test_ops
static int __init chrdev_fops_init(void)//驱动入口函数
{
if(alloc_chrdev_region(&dev1.dev_num,0,1,"chrdev_name") < 0){
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\n",dev1.major);
printk("minor is %d\n",dev1.minor);
使用cdev_init()函数初始化cdev_test结构体,并链接到cdev_test_ops结构体
cdev_init(&dev1.cdev_test,&fops_test);
dev1.cdev_test.owner = THIS_MODULE;//将owner字段指向本模块,可以避免在模块的操作正在被使用时卸载该模块
cdev_add(&dev1.cdev_test,dev1.dev_num,1);
printk("cdev_add is ok\n");
dev1.class_test = class_create(THIS_MODULE,"class_test");//使用class_create进行类的创建,类名称为class_test
device_create(dev1.class_test,NULL,dev1.dev_num,NULL,"device_test");//使用device_create进行设备的创建,设备名称为device_test
return 0;
}
static void __exit chrdev_fops_exit(void)//驱动出口函数
{
cdev_del(&dev1.cdev_test);//使用cdev_del()函数进行字符设备的删除
unregister_chrdev_region(dev1.dev_num,1);//释放字符驱动设备号
device_destroy(dev1.class_test,dev1.dev_num);//删除创建的设备
class_destroy(dev1.class_test);//删除创建的类
printk("module exit \n");
}
module_init(chrdev_fops_init);//注册入口函数
module_exit(chrdev_fops_exit);//注册出口函数
MODULE_LICENSE("GPL v2");//同意GPL开源协议
MODULE_AUTHOR("wang fang chen "); //作者信息
部分源码解读
- 上面测试源码demo,完全还是基于之前字符设备的一套,和 原子操作实验代码完全一样,唯一区别是没有用原子操作来防止竞争,用了自旋锁来防止资源竞争
- 可以简要看一下 自旋锁这里是一堆一堆出现的。 spin_lock(&spinlock_test); //自旋枷锁 spin_unlock(&spinlock_test); //自旋锁解锁
就是枷锁 设置flag,然后解锁。 恢复flag 状态时候 就枷锁->设置flag 的值->解锁。
Makefile 编译文件
#!/bin/bash
export ARCH=arm64
export CROSS_COMPILE=aarch64-linux-gnu-
obj-m += spinlock.o
KDIR :=/home/wfc123/Linux/rk356x_linux/kernel
PWD ?= $(shell pwd)
all:
make -C $(KDIR) M=$(PWD) modules
clean:
make -C $(KDIR) M=$(PWD) clean
测试程序 app.c
#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 str1[10]={0};//定义读取缓冲区str1
fd=open(argv[1],O_RDWR,0666);//调用open函数,打开输入的第一个参数文件,权限为可读可写
//fd=open("/dev/device_test",O_RDWR,0666);//调用open函数,打开输入的第一个参数文件,权限为可读可写
if(fd<0){
printf("open is error\n");
return -1;
}
printf("open is ok\n");
if(strcmp(argv[2],"topeet")==0){
write(fd,"topeet",sizeof(str1));
}else if(strcmp(argv[2],"itop")==0)
{
write(fd,"itop",sizeof(str1));
}
close(fd);//调用close函数,对取消文件描述符到文件的映射
return 0;
}
编译 测试程序 app
aarch64-linux-gnu-gcc -o app app.c -static
加载驱动 insmod
执行命令,测试结果如下:
查看 dev 下生成的字符设备
ls /dev/device_test
[root@topeet:/mnt/sdcard]# ls /dev/device_test
/dev/device_test
[root@topeet:/mnt/sdcard]#
测试脚本 app.sh
本次测试的 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 &
保存退出之后, 需要使用以下命令赋予脚本可执行权限:
chmod 777 app.sh
执行脚本验证
结果是,卡死,系统已经死掉了,只能重启。
总结
- 通过程序验证,和自旋锁死锁 有了初步认识
- 为什么会死锁,为什么之前的自旋锁环境没有死锁,这个地方需要思考下。 指定了CPU、内核睡眠等待、其它进程重复获得自旋锁等原因造成的
- 自旋锁的实际案例和规避死锁,后面再进一步理解的要