Linux驱动学习——并发和竞态

本文详细介绍了Linux内核中的并发与竞态问题,包括竞态产生的条件、常见的竞态场景,以及解决竞态问题的多种机制如中断屏蔽、原子操作、自旋锁和信号量等。

linux内核并发和竞态:

并发:多个执行单元同时发生(进程和中断)

竞态:多个执行单元对共享资源的访问。

          条件:

          1.并发

          2.共享资源

          3.同时访问

共享资源:硬件资源和软件上的全局变量。硬件资源大到一个串口设备,小到寄存器的某个bit位

互斥访问:当一个执行单元在访问共享资源时,其他执行单元禁止访问。

临界区:访问和操作共享数据的代码区域。

 

产生竞态的情况:

1.对称多处理器(SMP),因为多个CPU之间共享内存,外存,系统的IO等,必然成竞态

2.单CPU的内核支持进程的抢占,高优先级的进程能够抢占的优先级进程的CPU资源。如果这两个进程对共享资源访问,会形成竞态

3.中断和进程,硬件中断的优先级高于软中断,软中断高于进程!

4.中断和中断

(这些情况设计的任务必须同时要访问共享资源!否则不会形参竞态)

 

linux内核提供解决竞态,实现同步访问的机制:

中断屏蔽

原子操作

自旋锁

信号量

 

1.中断屏蔽

  解决的竞态问题:进程之间的抢占,进程和中断,中断和中断。

  CPU具备屏蔽中断和打开中断的功能:可以操作CPSR或者中断控制器。

   使用:

   unsigne long flags;

         local_irq_disable();//关中断

    临界区

    local_irq_enable(); //使能中断

   或者

         local_irq_save(flags); //关中断保存状态

         临界区

         local_irq_restore(flags); //使能中断恢复状态

 

使用注意事项:长时间的屏蔽中断,会导致操作系统系统很多跟中断相关的功能无法得到正常的运行,比如进程的调度,定时器的,时间的更新等,都无法进行,这种情况会造成数据的丢失,甚至是系统的崩溃。所以在使用中断屏蔽这个互斥机制时,一定要求临界区的代码执行速度越快越好。

 

2.原子操作

  原子操作的实现严重依赖CPU的架构!

原子操作分为:

1)位原子操作:

  位操作:例如

       intg_data = 0x55; //共享资源

                  led_open(...) {

               data = g_data & 0x 1; //位操作

        }//这种位操作具备原子性

如果要对共享资源进行位操作,并且考虑竞态问题,这时推荐使用内核提供的位原子操作,就是利用内核提供的位操作方法来对共享资源进行位操作。这些方法保证对共享资源的位操作是原子的。

set_bit/clear_bit/change_bit/test_bit 组合函数

头文件#include <asm/bitops.h>

 

内核set_bit等函数的实现依赖ARM的两条特殊指定:ldrex,strex,这两条指令是原子的加载和存储指令。CPU在执行这个指令,保证在CPU硬件上是原子的。比如一个CPU在执行这个代码时,另一个CPU就会在忙等待。如果使用这两条指令,虽然保证互斥,但是代码的执行效率上会降低。

 

2)整型原子操作:

涉及的数据类型:atomic_t

如果以后在驱动程序中,使用的共享资源是一个整型数,并且是一个共享资源,如果要对这个数进行相关的运算,可以考虑使用内核提供的整型原子操作的方法,保证原子性。

切记:一下代码不具备原子性

if (--open_cnt != 0) ...//不具备原子性

 

使用:

1.分配一个原子整型变量

  atomic_t v = ATOMIC_INIT(1); //intopen_cnt = 1;

2.利用内核提供的整型原子操作的方法对变量进行运算

  atomic_set/atomic_read/atomic_add/atomic_inc...

这些函数内部的实现还是使用ldrex,strex这两条原子加载和存储指令,保证整型原子变量的访问具备原子性!

 

3.自旋锁

  数据类型:spinlock_t

特点:光有自旋锁是没有太大意义的,必须要与共享资源同时存在。如果没有获取自旋锁,其他的任务将会一直原地打转。直到持有自旋锁者释放自旋锁。自旋锁不会引起休眠。

  如何使用自旋锁:

1.分配自旋锁

  spinlock_t lock

2.初始化自旋锁

  spin_lock_init(&lock);

3.如果要访问共享资源

  获取锁

  spin_lock(&lock); //如果获取锁,立即返回,否则一直忙等待

  或者

  spin_trylock(&lock);//如果获取锁,返回true,否则返回false,所以使用这个函数一定要对其返回值做判断!

4.如果获取锁,则访问共享资源

5.如果访问完毕,释放锁

  spin_unlock(&lock);

 

注意:以上涉及的自旋锁的操作方法,除了中断,其他竞态问题都能解决。如果要使用自旋锁解决中断引起的竞态,可以使用衍生自旋锁(可以解决所有的竞态问题)。

衍生自旋锁的使用:

获取锁:

unsigned long flags;

spin_lock_irq()/spin_lock_irqsave(flags);

释放锁:

spin_unlock_irq();/spin_unlock_irqrestore(flags);

 

自旋锁使用的注意事项:

自旋锁保护的临界区要求执行的速度越快越好,如果长时间的占有CPU资源,会降低系统的性能。如果使用衍生自旋锁,长时间占有CPU资源,不仅仅是性能影响,有可能还会造成数据的丢弃,甚至是操作系统的崩溃!临界区不能调用可能引起休眠的函数。

 

4.信号量

  由于自旋锁保护的临界区不能做休眠的操作,但是某些场合需要在临界区中进行休眠,需要让其他的任务进行休眠。这时自旋锁无法满足需求,可以使用信号量。

信号量能够让其他任务再没有获取信号时,进入休眠状态,也可以在临界区中进行休眠。实际上是一个睡眠锁。它和应用程序使用的信号量是一样的。

 

数据结构:

struct semaphore

 

如何使用信号量:

1.分配信号量

  struct semaphore sema;

2.初始化信号量

  sema_init(&sema, 1); //初始化为互斥信号量

3.如果要访问临界区,获取信号量

  down(&sema); //如果获取信号量,立即返回,否则进程将进入休眠状态,这种状态是不可中断的休眠状态(不会立即处理信号kill,如果醒来会判断是否接受过信号,如果有接收到信号,处理信号)

  或者

  down_interruptible(&sema);//如果获取信号量,立即返回,否则进程将进入休眠状态,这种状态是可中断的休眠状态(如果有信号kill到来,会立即处理信号),所以对这个函数一定要进行返回值的判断,如果返回0,表明正常获取信号量,如果返回非0,表明接收到了信号)

注意:以上两个函数不能在中断上下文中使用!

  或者

  down_trylock(&sema);//如果获取信号量,返回0,否则返回非0.所以这个函数也要判断判断返回值,可以在中断上下文中使用。

  

4.执行临界区

 

5.释放信号量

  up(&sema);

   一方面会释放信号量,另一方面还会唤醒休眠的进程!
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值