并发指的是多个执行单元同时、并行被执行,而并发的执行单元对共享资源(硬件资源和软件上的全局变量、静态变量等)的访问则很容易导致竞态。
一, linux 系统并发产生的原因很复杂,主要下面几个原因:
1、多线程并发访问,linux 是多任务(线程)的系统,所以多线程访问是最基本的原因。
2、抢占式并发访问,从内核2.6版本开始,linux 内核支持抢占,也就是说调度程序可以在任意时刻抢占正在运行的线程,从而运行其他的线程。
3、中断程序并发访问,学过STM32应该知道,硬件中断的权利可是很大的。
4、SMP(多核)核间并发访问,现在ARM架构的多核SOC很常见,多核CPU存在核间并发访问。并发访问带来的问题就是竞争,学过FreeRTOS和UCOS的同学应该知道临界区这个概念,所谓的临界区就是共享数据段,对于临界区必须保证一次只有一个线程访问,也就是要保证临界区是原子访问的,这里的原子访问就表示这一个访问是一个步骤,不能再进行拆分。如果多个线程同时操作临界区就表示存在竞争,我们在编写驱动的时候一定要注意避免并发和防止竞争访问。我们一般在编写驱动的时候就要考虑到并发与竞争,而不是驱动都编写完了然后再处理并发与竞争。
二, linux系统竞态问题描述
SMP(多核)核间并发访问,现在 ARM 架构的多核SOC很常见,多核 CPU 存在核间并发访问。并发访问带来的问题就是竞争,学过FreeRTOS和UCOS的同学应该知道临界区这个概念,所谓的临界区就是共享数据段,对于临界区必须保证一次只有一个线程访问,也就是要保证临界区是原子访问的,这里的原子访问就表示这一个访问是一个步骤,不能再进行拆分。如果多个线程同时操作临界区就表示存在竞争,我们在编写驱动的时候一定要注意避免并发和防止竞争访问。在驱动程序中埋下了隐患,这类问题往往又很不容易查找,导致驱动调试难度加大。所以我们一般在编写驱动的时候就要考虑到并发与竞争,而不是驱动都编写完了然后再处理并发与竞争。
竞态:多个执行单元对共享资源同时访问,形成竞争的状态,形成竞态的三个条件:
1.要有共享资源 2.要有多个执行单元 3.还要同时访问共享资源
共享资源:软件上的全局变量(open_cnt)或者硬件寄存器(GPIOCON...)
(1)中断屏蔽 (2)自旋锁 (3)信号量 (4)原子操作
三,linux内核解决竞态问题的方法之 —— 中断屏蔽
1.能够解决的竞态问题如下能够解决硬件中断和软中断之间的竞态能够解决高优先级的软中断和低优先级的软中断的竞态由于进程与进程之间的抢占基于软中断实现,也能够解决进程与进程之间的抢占引起的竞态,多核引起的竞态,无法解决。
2.中断屏蔽就是当一个执行单元(中断或者进程)在访问临界区之前,先去屏蔽中断,这样其它的执行单元(中断或者高优先级的进程)就不会产生,那么这个执行单元就可以踏踏实实的访问临界区也就不会发生CPU资源的切换! 由于是屏蔽中断,中断一旦屏蔽,操作系统里很多跟中断相关的机制(tasklet,软件定时器,硬件中断等)就无法得到运行,如果长时间的无法得到CPU资源,势必对系统造成不可预测的后果(例如软件定时器无法获取正确的超时时间等)!总结:中断屏蔽保护的临界区,要求代码执行速度要快, 更不能进行休眠操作;
3.编程使用步骤:
(1)明确驱动代码中哪些是共享资源
(2).明确驱动代码中哪些是临界区
(3).明确临界区中是否有休眠操作
如果有休眠,势必不考虑使用中断屏蔽,选择别的方法;如果没有休眠,"可以考虑"使用中断屏蔽。
4.在访问临界区之前屏蔽中断
unsigned long flags
local_irq_save(flags); //宏
5.执行单元踏踏实实的访问临界区也不会发生CPU资源的切换
6.访问临界区之后,记得恢复中断
local_irq_restore(flags); 切记:屏蔽中断和恢复中断一定要逻辑上成对使用。
四,linux内核解决竞态问题方法之—— 自旋锁
1.特点
(1)自旋锁必须附加在某个共享资源上
(2)想访问临界区的任务,在访问之前,先去获取自旋锁如果获取成功,即可访问临界区,但是访问临界区的速度要快,也就是说自旋锁保护的临界区的代码执行速度要快, 更不能进行休眠操作(后果很惨痛)! 如果获取失败,此任务将会原地空转,进入忙等待状态,等待的时间较长,势必浪费CPU资源,影响系统性能!所以:"自旋"不是锁自旋,而是没有获取自旋锁的任务自旋!
注意:自旋锁能够解决除了中断,其它竞态问题都可以解决 也就是:能够解决多核引起的竞态问题,能够解决同一个CPU上的,进程与进程的抢占而中断和进程,中断和中断无法解决!
总结:自旋锁能解决除了中断(中断和进程,中断和中断)以外的其他竞态(多核, 同一个CPU上的, 进程与进程的抢占)
2,自旋锁的数据类型:spinlock_t
3,编程使用操作:
(1).明确驱动代码中哪些是共享资源
(2).明确驱动代码中哪些是临界区
(3).明确临界区中是否有休眠
如果有,势必不考虑此方法
如果没有,还要考虑是否有中断
如果有中断参与的竞态,势必也不会考虑此方法
如果没有,可以考虑使用此方法
(4).访问临界区之前获取自旋锁前期要做:
a.定义初始化一个自旋锁对象spinlock_t lock; //定义对象
b.初始化对象spin_lock_init(&lock);
c.获取自旋锁spin_lock(&lock);
d. 释放自旋锁spin_unlock(&lock);
如果获取成功,此函数立即返回,代码继续往下执行
如果获取失败,当前任务再找个函数里进入忙等待状态
4,一旦获取自旋锁成功,踏踏实实的执行临界区, 但此时此刻,再次明确:自旋锁解决不了中断引起的竞态问题。
5,访问临界区之后,记得要释放自旋锁spin_unlock(&lock); 一旦释放锁,忙等待获取锁的任务立马就获取自旋锁,就立马继续访问临界区。
6,获取锁和释放锁必须在逻辑上成对使用,否则造成死锁。如果有中断参与的竞态,可以考虑使用衍生自旋锁!
7 ,综合关于自旋锁的信息,在使用自旋锁的时候要注意:
①因为在等待自旋锁的时候处于“自旋”状态,因此锁的持有时间不能太长,一定要短,否则的话
会降低系统性能。如果临界区比较大,运行时间比较长的话要选择其他的并发处理方式,
②自旋锁保护的临界区内不能调用任何可能导致线程休眠的 API 函数,否则的话可能导致死锁。
③不能递归申请自旋锁,因为一旦通过递归的方式申请一个你正在持有的锁,那么你就必须“自
旋”,等待锁被释放,然而你正处于“自旋”状态,根本没法释放锁。结果就是自己把自己锁死了!
④在编写驱动程序的时候我们必须考虑到驱动的可移植性,因此不管你用的是单核的还是多核的 SOC,都将其当做多核 SOC 来编写驱动程序。
8,衍生自旋锁( linux内核解决竞态问题方法自旋锁中最特殊的一种)
① 自旋锁的数据类型:spinlock_t 。特点:衍生自旋锁是基于自旋锁扩展而来,具有自旋锁的所有。衍生自旋锁能够解决所有的竞态问题,本质上就是基于自旋锁加了一个中断屏蔽机制。
② 编程使用操作:
(1)明确驱动代码中哪些是共享资源
(2)明确驱动代码中哪些是临界区
(3)明确临界区中是否有休眠:如果有,势必不考虑此方法;如果没有,可以考虑使用此方法。
(4)访问临界区之前获取衍生自旋锁前期要做:
a.定义初始化一个衍生自旋锁对象spinlock_t lock;
b. 初始化对象spin_lock_init(&lock);
c. unsigned long flags;
先屏蔽中断,再获取自旋锁spin_lock_irqsave(&lock, flags); 如果获取成功,此函数立即返回,代码继续往下执行,执行临界区。如果获取失败,当前任务在这个函数里进入忙等待状态
d. 释放衍生自旋锁 spin_unlock_irqresotore(&lock, flags);
③注意项:
获取锁和释放锁必须在逻辑上成对使用,否则造成死锁。一旦获取衍生自旋锁成功,踏踏实实的执行临界区;访问临界区之后,要释放衍生自旋锁spin_unlock_irqresotore(&lock, flags); 一旦释放锁,忙等待获取锁的任务立马就获取自旋锁,就立马继续访问临界区。
④总结:(1).普通自旋锁除了中断,都可以解决(2)衍生自旋锁横扫所有的竞态问题(3)保护的临界区都不能进行休眠操作
五,linux内核解决竞态问题方法之 —— 信号量(谈谈进程和线程的区别,以及同步互斥的方法)
1.特点:信号量又称睡眠锁,信号量产生的根本原因就是因为中断屏蔽,自旋锁,衍生自旋锁保护的临界区不能休眠这种问题,有些场合需要在临界区中进行休眠操作,此时此刻要必须使用信号量来加以保护!信号量是允许临界区进行休眠操作!
2.数据结构:struct semaphore
3.编程使用操作步骤:
(1).明确驱动代码中哪些是共享资源
(2).明确驱动代码中哪些是临界区
(3).明确临界区中是否有休眠如果有,必须用此方法,如果没有,可以考虑使用中断屏蔽啦,自旋锁啦,衍生自旋锁啦或者信号量
(4).访问临界区之前获取信号量
a.定义初始化好一个信号量对象 struct semaphore sema;
b. 将信号量初始化为互斥信号量 sema_init(&sema, 1); // (信号量的值只有两个:1和0)
//获取信号量成功1减1=0
//释放信号量0加1=1
c.获取信号量的方法:
方法1:down(&sema); //1->0
如果获取信号量成功,函数立即返回,代码继续执行,访问临界区,如果获取信号量失败,进程在此函数中将进入不可中断的休眠状态,
“不可中断的休眠状态”:进程在休眠期间,如果接受到了信号(kill/ctrl+c).休眠的进程不会立即响应处理信号,而是进程被唤醒 以后才会处理信号(去死)进程如果进入这种休眠状态,将来被唤醒的方法只有一个:获取信号量的任务在释放信号量的同时再唤醒这个休眠的进程。
方法2:down_interruptible(&sema); //1->0
如果获取信号量成功,函数立即返回,代码继续执行,访问临界区如果获取信号量失败,进程在此函数中将进入可中断的休眠状态。 “可中断的休眠状态”:进程在休眠期间,如果接受到了信号(kill/ctrl+c). 休眠的进程会立即响应处理信号,进程如果进入这种休眠状态,将来被唤醒的将来唤醒的方法有两个:获取信号量的任务在释放信号量的同时再唤醒这个休眠的进程 和 信号唤醒。
if(down_interruptible(&sema))
{
printk("进程是由于接受到了去死信号引起的唤醒!\n");
return -ERESTARTSYS;
}
else
{
printk("获取信号量的进程释放信号量引起当前休眠的进程唤醒!\n");//可以继续访问临界区
}
(5).一旦获取信号量,踏踏实实访问临界区
(6)访问临界区之后,释放信号量并且唤醒之前休眠的进程 up(&sema); //0->1
(7)获取信号量和释放信号量在逻辑上必须要成对使用
六,linux内核解决竞态方法之原子操作
1.原子操作所有的竞态问题都可以解决
2.位原子操作:位操作具有原子性,对共享资源位操作的时候不允许发生CPU资源的切换
3. 编程步骤:
(1)明确驱动代码中哪些是共享资源
(2)明确驱动代码中哪些是临界区
(3)观察临界区的代码中是否有对共享资源进行位操作,如果有位操作,可以考虑使用位原子操作相关的函数,调用内核提供的位原子操作的相关函数对共享资源进行位操作,这个过程具有原子性,也不会发生CPU资源的切换!
(4)内核提供的位原子操作的相关函数:
void set_bit(int nr, void *addr); //将addr地址内的数据的第nr位(从0开始)设置1
void clear_bit(int nr, void *addr); //将addr地址内的数据的第nr为清为0
void change_bit(int nr, void *addr); //将addr地址内的数据的第nr为反转
int test_bit(int nr, void *addr); //获取addr地址内的数据的第nr位的值
总结:利用以上函数对共享资源进行位操作具有原子性
4,参考代码
static int open_cnt = 1; //共享资源
open_cnt &= (1 << 5); //临界区
//此时此刻此代码没有考虑竞态问题,相当危险
解决方案:
(1)采用中断屏蔽
unsigned long flags
local_irq_save(flags);
open_cnt &= ~(1 << 5); //临界区
local_irq_restore(flags);
(2)采用自旋锁(衍生自旋锁)
spin_lock(&lock);
open_cnt &= (1 << 5); //临界区
spin_unlock(&lock);
(3).采用信号量
down(&sema);
open_cnt &= (1 << 5); //临界区
up(&sema);
(4)采用位原子操作
clear_bit(5, &open_cnt);
整型原子操作:整型操作具有原子性,对共享资源进行整型操作的过程不允许发生CPU资源的切换
整型原子变量数据类型:atomic_t(本质结构体,最最本质就unsigned int counter)
编程步骤:
<1>明确驱动代码中哪些是共享资源
<2>明确驱动代码中哪些是临界区
<3>观察临界区的代码中是否有对共享资源进行整型操作如果有,可以考虑使用内核提供的整型原子操作来解决竞态问题
<4>具体使用如下:
原先共享资源可能用char/int/long/short数据类型来定义、此时可以考虑使用整型原子变量的数据类型定义一个整型原子变量进行替换:
static int open_cnt = 1; //之前
static atomic_t open_cnt = ATOMIC_INIT(1); //采用整型原子操作替换
注意:“atmic_t当成int即可”
接下来只需利用内核提供的整型原子操作的相关函数
对整型原子变量访问即可,访问过程具有原子性:
atomic_add
atomic_sub
atomic_inc
atomic_dec
atomic_return
atomic_sub_and_test://分析源码
整型原子变量减1,然后判断整数原子变量的值是否为0如果为0,返回真;否则返回假等(去看SI)
参考代码:结论:此代码裸奔中,相当危险!
static int open_cnt = 1; //共享资源
if (--open_cnt != 0) { //临界区}
解决方案:
(1).采用中断屏蔽
local_irq_save(flags);
if (--open_cnt != 0)
local_irq_restore(flags);
(2).采用自旋锁(衍生自旋锁)
spin_lock(&lock);
if (--open_cnt != 0)
spin_unlock(&lock);
(3)采用信号量
down(&sema);
if (--open_cnt != 0)
up(&sema);
(4)整型原子操作
static atomic_t open_cnt = ATOMIC_INIT(1); //整型原子变量
if (!atomic_dec_and_test(&open_cnt)) //临界区
{
//打开失败
}
七,驱动中原子操作函数分析
1,atomic64_t //在 type.h 头文件中,对于原子变量的结构体的定义
typedef struct
{
long counter;
}
2,atomic64_read函数
long long atomic64_read(const atomic64_t *v); //读取原子变量所使用到的函数
3,atomic64_set函数
void atomic64_set(atomic64_t *v, long long i); //原子变量设置函数,用来设置原子变量的数值
参数分析:
• *v:atomic64_t 结构体地址
• i:需要设置的值
• 返回值:空
4,atomic64_add函数
void atomic64_##op(long long a, atomic64_t *v);
含义:原子变量加法函数,作用是把指定的 atomic64_t 结构体的值加上指定的数值。这个函数用op代替了add这个函数,因为其同样也承载了减法函数的功能,使用op来复用了函数。
参数分析:
• a:指定的数
• *v:atomic64_t 结构体
• 返回值:空
5,atomic64_sub函数
void atomic64_##op(long long a, atomic64_t *v);
含义:既然有加,那就也应该有减,这是原子变量的减法函数,使用方法和加法函数一样。
参数分析:
• a:指定的数
• *v:atomic64_t 结构体
• 返回值:空
6,atomic64_inc函数
#define atomic64_inc(v) atomic64_add(1, (v)) //其中参数v是指atomic64_t 结构体
含义:原子变量自增函数,调用这个函数可让指定函数自增一。原理就是通过 atomic64_add 函数加一。
7,atomic64_dec函数
#define atomic64_dec(v) atomic64_sub(1, (v)) //其中参数v是指atomic64_t 结构体
含义:原子变量自减函数,与自增的原理一样,是通过 atomic64_sub 函数减一。
八,驱动程序源码
//添加头文件
#include
#include
#include
#include
#include
#include
#define ZYNQMP_GPIO_NR_GPIOS 118
#define MIO_PIN_51 (ARCH_NR_GPIOS - ZYNQMP_GPIO_NR_GPIOS + 51)
// #define MIO_PIN_38 (ARCH_NR_GPIOS - ZYNQMP_GPIO_NR_GPIOS + 38)
//设置一个设备全局变量
struct lock_device
{
dev_t devno;
struct cdev cdev;
struct class *class;
struct device *device;
atomic_t lock;
} lock_dev;
int lock_open(struct inode *inode, struct file *filp)
{
printk("-lock_open-\n");
if (!atomic_read(&lock_dev.lock)) //读取锁的状态
atomic_inc(&lock_dev.lock); //把原子变量加 1, 上锁
else
return -EBUSY; //若检测到已上锁,则返回设备忙
return 0;
}
ssize_t lock_write(struct file *flip, const char __user *buf, size_t count, loff_t *fops)
{
int flag = 0, i = 0;
flag = copy_from_user(&i, buf, count); //使用copy_from_user读取用户态发送过来的数据
printk(KERN_CRIT "flag = %d, i = %d, count = %d\n", flag, i, count);
if (flag != 0)
{
printk("Kernel receive data failed!\n");
return 1;
}
if (i == 48)
{
gpio_set_value(MIO_PIN_51, 0);
// gpio_set_value(MIO_PIN_38, 0);
}
else
{
gpio_set_value(MIO_PIN_51, 1);
// gpio_set_value(MIO_PIN_38, 1);
}
return 0;
}
int lock_close(struct inode *inode, struct file *filp)
{
printk("-lock_close-\n");
atomic_set(&lock_dev.lock, 0); //将变量设为0,意为解锁
return 0;
}
const struct file_operations lock_fops = {
.open = lock_open,
.write = lock_write,
.release = lock_close,
};
//实现装载入口函数和卸载入口函数
static __init int lock_drv_init(void)
{
int ret = 0;
printk("----^v^-----lock drv v1 init\n");
//动态申请设备号
ret = alloc_chrdev_region(&lock_dev.devno, 0, 1, "lock_device");
if (ret < 0)
{
printk("alloc_chrdev_region fail!\n");
return 0;
}
//设备初始化
cdev_init(&lock_dev.cdev, &lock_fops);
lock_dev.cdev.owner = THIS_MODULE;
//自动创建设备节点
//创建设备的类别
//参数1----设备的拥有者,当前模块,直接填THIS_MODULE
//参数2----设备类别的名字,自定义
//返回值:类别结构体指针,其实就是分配了一个结构体空间
lock_dev.class = class_create(THIS_MODULE, "lock_class");
if (IS_ERR(lock_dev.class))
{
printk("class_create fail!\n");
return 0;
}
//创建设备
//参数1----设备对应的类别
//参数2----当前设备的父类,直接填NULL
//参数3----设备节点关联的设备号
//参数4----私有数据直接填NULL
//参数5----设备节点的名字
lock_dev.device = device_create(lock_dev.class, NULL, lock_dev.devno, NULL, "lock_device");
if (IS_ERR(lock_dev.device))
{
printk("device_create fail!\n");
return 0;
}
//向系统注册一个字符设备
cdev_add(&lock_dev.cdev, lock_dev.devno, 1);
//MIO_PIN_51 38申请GPIO口
ret = gpio_request(MIO_PIN_51, "led1");
if (ret < 0)
{
printk("gpio request led1 error!\n");
return ret;
}
//GPIO口方向设置成输出
ret = gpio_direction_output(MIO_PIN_51, 1);
if (ret != 0)
{
printk("gpio direction output MIO_PIN_51 fail!\n");
}
//将原子变量置0,相当于初始化
atomic_set(&lock_dev.lock, 0);
return 0;
}
static __exit void lock_drv_exit(void)
{
printk("----^v^-----lock drv v1 exit\n");
//释放按键GPIO
gpio_free(MIO_PIN_51);
// gpio_free(MIO_PIN_38);
//注销字符设备
cdev_del(&lock_dev.cdev);
//删除设备节点
device_destroy(lock_dev.class, lock_dev.devno);
//删除设备类
class_destroy(lock_dev.class);
//注销设备号
unregister_chrdev_region(lock_dev.devno, 1);
}
//申明装载入口函数和卸载入口函数
module_init(lock_drv_init);
module_exit(lock_drv_exit);
//添加GPL协议
MODULE_LICENSE("GPL");
MODULE_AUTHOR("subomb");
九,应用程序分析
#include
#include
#include
#include
#include
#include
#include
int main(int argc, char *argv[])
{
int fd, ret = 0;
char *filename;
char writebuf[1] = {0};
filename = argv[1];
fd = open(filename, O_RDWR); //打开设备
if (fd < 0)
{
printf("Can't open file %s\n", filename);
return -1;
}
//通过驱动程序接口,发送指令至驱动程序,此处 0 代表灯灭,1 代表灯亮
memcpy(writebuf, argv[2], 1); //将内容拷贝到缓冲区
ret = write(fd, writebuf, 1); //写数据
if (ret < 0)
{
printf("Write file %s failed!\n", filename);
}
else
{
printf("Write file success!\n");
}
sleep(15); //线程暂停15秒,留出时间测试
printf("Finish.\n");
ret = close(fd); //关闭设备
if (ret < 0)
{
printf("Can't close file %s\n", filename);
return -1;
}
return 0;
}