并发控制 原子操作 自旋锁 信号量

linux并发与竞争

深刻理解编写驱动的时候,处理并发操作的时候的方法


文章目录

  • linux并发与竞争
  • 一、原子操作
  • 二、自旋锁
    • 1.自旋锁
    • 2.读写锁
    • 3.顺序锁
    • 4、RCU
  • 三、信号量
  • 四、互斥体
  • 总结


提示:这里可以添加本文要记录的大概内容:
例如:随着人工智能的不断发展,机器学习这门技术也越来越重要,很多人都开启了学习机器学习,本文就介绍了机器学习的基础内容。


提示:以下是本篇文章正文内容,下面案例可供参考

一、原子操作

linux整数原子操作函数
一般的c语言赋值的时候 在汇编中其实进行了三步操作 那么在具有优先级的linux内核中 可以在执行三步得得时候进程被抢占那么就会出现错误的数据 (简单理解)
使用linux内核中的atomic_t (64位 atomic64_t)就可以避免这种情况
原子位操作 linux中就是直接操作地址了
一般的驱动中的全局变量都需要把他变成原子操作
原子操作原理:
保证一个整形数据修改的是排他性的。对于ARM处理器底层使用LEDEX和STREX指令
这两个指令可以让总线监控到ldrex到serex之间是否有其他实体存取该地址
并发控制 原子操作 自旋锁 信号量_第1张图片
T3时间点CPU0会执行失败 然后T4时间点 CPU1执行成功 所以在当时只有CPU1执行成功了
然后CPU0会通过bne 1b 再次进入 ldrex
通过原子锁使下面驱动只能被一个进程打开

static atomic_t int_atomic_available=ATOMIC_INIT(1);   //定义原子变量

static int atomic_open(struct inode *node,struct file *file){
        if(!atomic_dec_and_test(&int_atomic_available)){    //函数是原子自减并且判断是否为零  为零返回true   外部判断就是返回false  说明1表示空闲  然后   释放的时候加上一  又变回一了
			atomic_inc(&int_atomic_available);
            return -EBUSY;
    }
    printk("atomic dev open successfully !\n");
    return 0;
}

static int atomic_release(struct inode *node,struct file *file){
     atomic_inc(&int_atomic_available);
    printk("atomic dev release successfully !\n");
    return 0;
}

二、自旋锁

1.自旋锁

其实就是CPU运行的代码需要先执行一个原子操作,这个原子操作测试并设置某一个内存变量,由于是原子操作所以在当前测试并设置的时候其他的执行单元不能访问这个内存变量,当执行单元测试内存变量是空的时候那么会循环执行测试并设置这个操作 形成所谓的自旋
简单理解就是把自旋锁当做一个变量,假设A先得到那么A就持有该变量,并且变量会被标记已经被A持有,暂时无法被使用,当B想要持有的时候会发现标记然后等待A释放这个变量,标记被抹除然后B就可以使用变量了
自旋锁用于多个CPU系统中,在单处理器系统中,自旋锁不起锁的作用,只是禁止或启用内核抢占。在自旋锁忙等待期间,内核抢占机制还是有效的,等待自旋锁释放的线程可能被更高优先级的线程抢占CPU。
编写驱动注意的几个点

  1. 自旋锁实际上忙等锁。当占用锁的时间很少的时候才适合使用自旋锁
  2. 自旋锁可能导致死锁。常见就是递归使用自旋锁很明显不行 cpu已经拥有该自旋锁想要在拥有肯定会进入死锁
  3. 自旋锁锁定期间不能调用可能会引起进程调度的函数。如果进程获取自旋锁在调用阻塞如:copy_from_user(), copy_to_user() kmalloc() msleep() 等 可能导致内核崩溃
  4. 注意跨设备编程 在单核编程的时候,中断和进程可能访问同一临界区,进程调用了spin_lock_irqsave()是安全的,在中断即使不调用spin_lock()也没事 因为会屏蔽掉这个cpu的中断服务.但是在多核中spin_lock_irqsave() 不会屏蔽其他核的中断 所以我们在中断中应该也调用spin_lock()
//**************************使用自旋锁使设备只能被一个进程打开**********************
int xxx_count = 0;//定义文件打开次数计数  
       static int xxx_open(struct inode *inode,struct file *filp)  
       {  
        ...  
        spinlock(&xxx_lock);  
        if(xxx_count)//已打开  
        {  
            spin_unlock(&xxx_lock);  
            return - EBUSY;  
        }  
        xxx_count++;//增加使用计数  
        spin_unlock(&xxx_lock);  
        ...  
        return 0;//成功  
        }  
        static int xxx_release(struct inode *inode,struct file *filp)  
        {  
            ...  
            spinlock(&xxx_lock);  
            xxx_count--;//减少使用计数  
              
            spin_unlock(&xxx_lock);  
            return 0;//成功  
              
        } 

2.读写锁

自旋锁不论资源在做什么都会把临界资源锁住,但是一些并发读的操作并不会影响,读写锁是比自旋锁更小的锁机制,他保留了"自旋"",但是在写的时候最多有一个进程,但是读操作的时候可以有多个读执行单元.当然读和写肯定不能同时进行的

//*******************读写锁通常操作方式****************
	rwlock_t lock;
	rwlock_init(&lock)
	/*读时操作*/
	read_lock(&lock);
	***
	red_unlock(&lock);
	/*写操作*/
	write_lock_irqsave(&lock);
	***
	write_unlock_irqsave(&lock);
	

3.顺序锁

读写锁的优化,读写锁更偏向于读者,顺序锁更偏向于写者,简单说就是写的同时可以去读执行单元,读和写之间是不互斥的,但是写之间还是互斥的,但是如果读操作执行期间进行了写操作,那么还是要重新进行读的,所以使用这种锁可能要反复读多次才能读到数据
seqlock适用于:

(1)read操作比较频繁

(2)write操作较少,但是性能要求高,不希望被reader thread阻挡(之所以要求write操作较少主要是考虑read side的性能)

//*******************顺序锁通常操作方式****************
seqlock_t seqlock
seqlock_init(&seqlock)   //定义顺序锁
/*******************写操作**************************/
write_seqlock(&seqlock);
/* -------- 写临界区 ---------*/
write_sequnlock(&seqlock);
/*******************读操作**************************/
unsigned long seq;
 
do { 
     seq = read_seqbegin(&seqlock); 
/* ---------- 这里读临界区数据 ----------*/
} while (read_seqretry(&seqlock, seq)); 

4、RCU

RCU(Read-Copy Update)是数据同步的一种方式.RCU是在RWLOCK机制上演化来的
这里引用一个知乎大佬的文章
RCU介绍
我自己理解的话就是先copy一个我们要改的资源然后修改资源的复制,最后在资源空闲的时候在进行发布,保证了资源要么是全新的要么是全旧的,保证了一致性
上面这种是基于synchronize_rcu() 但是有一个问题 我们更新资源的时候会等待上一个读的程序,如果读的占用时间很长的话,那我们的程序不就一直在等待
并发控制 原子操作 自旋锁 信号量_第2张图片

解决上面问题可以使用挂接回调
与synchronize_rcu() 这个会引起堵塞不同,这个不会引起执行单元堵塞,因此可以在中断上下文或者软中断中使用cll_rcu()会把一个函数挂接到RCU上挂接的回调函数会在RCU执行(其实就是省掉了那个等待的时间)结束后执行。
暂时不写了

三、信号量

简单理解就是会对某个东西做一个标记,不同动作会改变标记,相应的不同标记对应不同状态,
信号量的特点:

  1. 因为信号量可以使等待资源线程进入休眠状态,因此适用于那些占用资源比较久的场合。
  2. 信号量不能用于中断中,因为信号量会引起休眠,中断不能休眠。
  3. 如果共享资源的持有时间比较短,那就不适合使用信号量了,因为频繁的休眠、切换线程引起的开销要远大于信号量带来的那点优势。
structsemaphore sem;/* 定义信号量*/
sema_init(&sem,1)/* 初始化信号量*/
down(&sem);/* 申请信号量  这个函数会导致睡眠  因此不要再中断上下文中使用*/
/* 临界区*/
up(&sem);/* 释放信号量   唤醒等待者 */

作为互斥的手段信号量,不再推荐内核中使用,但是信号量可以用于同步
eg:类比操作系统中的PV操作并发控制 原子操作 自旋锁 信号量_第3张图片
进程一执行up()操作 去唤醒进程二 这样子进程二就同步等待了进程一

四、互斥体

互斥体—mutex。互斥访问表示一次只有一个线程可以访问共享资源,不能递归申请互斥体,建议在需要互斥的地方都是用这个。
当然怎么选择互斥体和自旋锁呢?

  1. 互斥体会进行进程上下文切换,时间开销较大。所以在临界区较小时用自旋锁,临界区较大时用互斥体。
  2. 互斥体 可以保护还有肯能引起阻塞的临界区。自旋锁要保护的临界区要绝对避免发生阻塞。因为阻塞意味着进程切换,若果切换后另一个进程又试图获取本自旋锁,死锁就会发生。
  3. 互斥体存在于进程上下文,因此如过在中断中保护共享资源只能使用自旋锁。当然如果要用互斥体,可以用mutex_trylock() 不能获取就直接返回。
/********************互斥体的使用***********************/
structmutex lock;/* 定义一个互斥体*/
mutex_init(&lock);/* 初始化互斥体*/
mutex_lock(&lock);/* 上锁*/
/* 临界区*/
mutex_unlock(&lock);/* 解锁*/

总结

还需要在实践中理解,多用才知道他们的使用场景和经常出现的错误 加油吧

你可能感兴趣的:(linux,linux,多线程,并发)