Linux设备驱动中的并发控制

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_lockspin_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,总结
    并发和竞态广泛存在,中断屏蔽,原子操作,自旋锁和互斥体都是解决并发问题的机制。中断屏蔽很少单独使用,原子操作只能针对整数进行,因此自旋锁和互斥体应用最为广泛。
       自旋锁会导致死循环,锁期间不予许阻塞,因此要求锁的临界区小,互斥体允许临界区阻塞,可适用于临界区大的情况。







 







 

    

        

    

    

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值