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);
一方面会释放信号量,另一方面还会唤醒休眠的进程!
本文详细介绍了Linux内核中的并发与竞态问题,包括竞态产生的条件、常见的竞态场景,以及解决竞态问题的多种机制如中断屏蔽、原子操作、自旋锁和信号量等。
1422

被折叠的 条评论
为什么被折叠?



