1.1 基本概念
首先我们在一起探讨这个问题的同时,需要明白几个最基本的概念。并发:指的是多个执行单元同时,并行被执行。
竞争:因为多个单元同时,并行执行而对共享资源(全局变量,静态变量)访问容易导致竞争。比如全局变量,int a[1000]; 在A进程中对其数组成员都写入10,由于linux内核支持抢占调度(类似于单片机的中断优先级)。然后在B进程中对其数组成员都写入20,然后C进程去读取变量数组a的值。那么工程师的原本是想读取A进程中对变量的赋值,那么就会导致错误。
互斥访问:指一个执行单元在在访问共享资源的时候,其他执行单元禁止访问。解决竞争的途经是保证对共享资源的互斥访问。常用解决竞争的具体方法有:中断屏蔽,原子操作,自旋锁,信号量,互斥体。用来对临界区加以保护(共享资源的代码区)。2.1,下面我们将来学习相关的操作。
1,中断屏蔽:通过设置CPSR中的I位,关闭处理器的中断功能。驱动程序统称要考虑在多核中运行而不是在单核中,因此该方法不值得推荐。
相关代码为:
local_irq_disable(); //屏蔽中断
critical section; //临界区代码
local_irq_enable(); //开中断
2,整形原子操作:原子操作可以保证对一个整形数据的修改是排他性的。在Linux内核中,原子操作分为两类。1),整形原子操作,2),位原子操作。相关原子操作函数如下:
第一个,设置原子变量的值。
void atomic_set(atomic_t ,int i); //设置原子变量的值 i;
atomic_t v=ATOMIC_INIT( 0 ); //定义原子变量V并初始化为0
第二个,获取原子变量的值。
atomic_read(atomic_t *v);
第三个,原子变量的加/减。
void atomic_add(int i,atomoc_t *v);
void atomic_sub(int i ,atomic_t *v); //原子变量的加/减i
第四个,原子变量的自增/自减
void atomic_inc(atomic *v);
void atomic_dec(atomic *v); //原子变量减少1
第五个,原子变量的测试
int atomic_inc_and_test(atomic_t *v);
int atomic_dec_and_test(atomic_t *v);
int atomic_sub_and_test(atomic_t *v);
上述操作对原子变量进行自加,自减和减操作后,测试其值是否为0,为0则返回ture,否则返回false.
第六个,操作并返回
int atomic_add_return(int i,atomic_t *v);
int atomic_sub_return(int i,atomic_t *v);
int atomic_inc_return(atomic_t *v);
int atomic_dec_return(atomic_t *v);
上述操作对原子变量进行加/减操作和自加/自减操作,并返回新的值。
3,原子位操作。
第一个,设置位
void set_bit(nr,void *addr); //对addre地址的nr位设置成1.
第二个,清除位
void clear_bit(nr,void *addr); //将对addre地址的nr位设置成0
第三个,改变位
void change_bit(nr ,void *addr); //对上述地址addr地址的第nr位进行反置
第四个,测试位
test_bit(nr,void *addr); //返回addr的第nr位的值
第五个,测试并操作
int test_and_set_bit(nr ,void *addr);
int
test_and_clear_bit(nr ,void *addr);
int
test_and_change_bit(nr ,void *addr);
//等价于执行test_bit(nr,void *addr)后再执行xxx_bit(nr,void *addr).
4,自旋锁:是一种典型的对临界资源进行互斥访问的手段。其工作方式是当一个执行单元占用互斥锁的时候,另一个单元将无法同时占用互斥锁的而将在原地重复执行“测试并设置”的操作。(可理解为while(1);注意该执行单元并没有休眠,因此自旋锁适用于临界区较短的情况)。
自旋锁操作的相关函数。
第一个,定义自旋锁。
spin_lock lock;
第二个初始化自旋锁。
spin_lock_init( lock); //该宏用于动态初始化自旋锁
第三个,获得自旋锁。
spin_lock(lock);
该宏用于获得自旋锁lock,如果能立即获得锁,它就能马上返回,否则它将在那里自旋打转,直到自旋锁拥有者释放。
spin_trylock(lock);该宏用于获得自旋锁lock,如果能立即获得锁,它就返回ture,当不能获得自旋锁时,它将立即返回flase.
第四个,释放互斥锁
spin_unlock(lock); 它与spin_lock或spin_trylock配对使用。
尽管用了自旋锁可以保证临界区不受别的cpu和本cpu内的抢占进程打扰,但是得到锁的代码路径在临界区的时候,还有可能受到中断或者底半部的影响。为了防止这种影响,就需要用到自旋锁的衍生。
spin_lock_irq()=spin_lock()+local_irq_disable();
spin_unlock_irq()=spin_unlock()+local_irq_enable();
spin_lock_irqsave()=spin_lock()+local_irq_save();
spin_unlock_irqrestore()=spin_unlock()+local_irq_restore() ;
spin_lock_bh()=spin_lock()+local_bh_disable();
spin_unlock_bh()=spin_unlock()+local_bh_enable();
5,读写自旋锁
实际上,对共享资源的并发访问,多个执行单元同时读取它,是不会有问题的,自旋锁的衍生锁读写自旋锁可允许读的并发操作,只能最多有个写进程,在读的时候可允许多个单位同时执行。
操作示例:
定义和初始化读写自旋锁。
rwlock_t my_rwlock;
//定义读写锁
rwlock_init(&my_rwlock); //动态初始化
/*读时获取锁*/
read_lock(&
my_rwlock);
............
read_unlock
(&
my_rwlock
);
/*写时获取锁*/
write_lock_irqsave(&my_lock,flags);
.....................................
write_unlock_irqrestore(&my_lock,flags);
6,顺序锁
它是一种对读写锁的优化操作。写锁占有读写锁的时候,读锁仍然可以占用读写锁。读锁占有读写锁时,写锁也不必阻塞等待。当写锁占有读写锁的时候,另一个进程对其进行写锁操作时,就会阻塞。对于读写锁之间不互相互斥,但是如果在执行单元读操作的时候,写执行单元 已经发生了写操作,那么读执行单元必须重新读取数据,以确保得到的数据是完整的。
7,读—复制-更新(RCU)
RCU可以看作读写锁的高性能版本,相比读写锁,RCU的优点在于即允许多个读执行单元同时访问被保护的数据,又允许多个写执行单元的操作。但是RCU不能替代读写锁,因为如果在写操作比较多时,写执行单元的操作开销会比较大。
8 信号量
信号量是操作系统中最典型的用于同步和互斥的手段,信号量的值可以是0,1或 n。信号量与操作系统中的经典概念PV操作。
P(S):将信号量S的值减1,即S=S-1.如果S>=0,则该进程继续执行,否则该进程设置为等待状态,排入等待队列。
V(S):将信号量S的值加1,即S=S+1;如果 S>0,就唤醒等待信号量的进程。相关的函数。
第一个,定义信号量
struct semaphore sem;
第二个,初始化信号量
void sema_init(struct semaphore *sem,int val); /*该函数初始化,并设置信号量sem值为val;
第三个,获得信号量。
void down(struct semaphore *sem);
该函数用于获得信号量sem,它会导致睡眠,因此不能在中断上下文中使用。
void
down_interruptible(struct semaphore *sem);
该函数功能与down()类似,不同之处,因为down()进入睡眠状态不能被信号打断,但因为down_interruptible()进入睡眠状态能被信号打断,信号也会导致该函数返回,非0;
第四个,信号量的释放
void up(struct semaphore *sem) //该函数释放信号量,唤醒等待者。
作为一种可能的互斥手段,信号量可以保护临界区,它的使用方式和自旋类似。但与自旋锁不同的是,当获取不到信号时,进程不会在原地打转而是进入休眠等待状态。
对于具体关心数值的生产者,消费者问题,使用信号量较为合适。
9,互斥体
互斥体是进程级的,用于多个进程之间资源的互斥。具体使用方法如下:
strcut mutex my_mutex; //定义互斥体
mutex_init(&my_mutex); //初始化互斥体
mutex_lock(&my_lock); //获取mutex
......................
mutex_unlock(&my_mutex);//释放mutex.
自旋锁和互斥体都是解决互斥问题的基本手段,面对特定情况,应该如何取舍呢?
自旋锁和互斥体选用三原则:
第一,当锁不能获取时,使用互斥体的开销是进程上下文切换的时间,而使用自旋锁的开销是等待获取自旋锁。临界区较小时,宜选用自旋锁,临界区大时,宜选用互斥锁。
第二,若临界区包含引起阻塞的代码,则应该选用互斥体。因为阻塞意味着要进行进程的切换,如果进程被切换出去,另一个进程企图获取本自旋锁,就会发生死锁。
第三,互斥体存在于进程上下文,因此,如果被保护的共享资源需要在中断或软中断情况下使用,则在互斥体和自旋锁之间只能选择自旋锁。
10,总结
并发和竞态广泛存在,中断屏蔽,原子操作,自旋锁和互斥体都是解决并发问题的机制。中断屏蔽很少单独使用,原子操作只能针对整数进行,因此自旋锁和互斥体应用最为广泛。
自旋锁会导致死循环,锁期间不予许阻塞,因此要求锁的临界区小,互斥体允许临界区阻塞,可适用于临界区大的情况。