- 并发控制要解决的问题是:多个进程对共享资源的并发访问,即解决竞态问题。
一、竞态产生的几种情况
- 对称多处理器(SMP)的多个CPU。多给CPU意味着同时有多个执行单元,使用共同的系统总线,可访问共同的外设和存储器。
- 单CPU内进程之间的抢占。
- 中断与进程之间。中断打断正在执行的进程,如果中断处理程序访问进程正在访问的资源,则竞态也会发生。同时,中断也可以被更高优先级的中断打断,多个中断之间也可能导致竟态
二、解决竟态的办法
很简单,只要保证共享资源的互斥访问就ok了。访问共享资源的代码区称为临界区。临界区需要以某种机制加以保护。中断屏蔽、原子操作、自旋锁和信号量。
- 中断屏蔽。cpu一般都具备屏蔽中断和打开中断的功能,这项功能可以保证正在执行的内核执行路径不被中断处理程序所抢占,防止某种竟态条件的产生。由于linux kernel的进程调度都是依靠中断来进行的,内核抢占的并发久得以避免了。但是,这种方法只对单cpu有效。local_irq_disable() 和local_irq_enable()都只禁止和使能本cpu的中断,不能解决smp引发的竟态
- 原子操作,指在这个原子操作中不会被打断。原子操作分为整型原子操作和位原子操作。
整型原子操作:
1.设置原子变量的值 void atomic_set(atomic_t *v, int i);//设置原子变量v的值为i
atomic_t v = ATOMIC_INIT(0);//定义原子变量v并初始化为0
2.获取原子变量的值
atomic_read(atomic_t *v);//返回原子变量的值
3.原子变量的加/减
void atomic_add(int i, atomic_t *v);//原子变量增加i
void atomic_sub(int i, atomic_t *v);//原子变量减少i
4.原子变量自增/自减
void atomic_inc(atomic_t *v);//原子变量增加1
void atomic_dec(atomic_t *v);//原子变量减少1
5.操作并测试
int atomic_inc_and_test(atomic_t *v);
int atomic_dec_and_test(atomic_t *v);
int atomic_sub_and_test(atomic_t *v);
6.操作并返回
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);
|
位原子操作:
1.设置位
void set_bit(nr, void *addr);//设置addr地址的第nr位,设置第nr位为1
2.清除位
void clear_bit(nr, void *addr);//设置第nr位为0
3.改变位
void change_bit(nr, void *addr);//对第nr位取反
4.测试位
void test_bit(nr, void *addr);//返回addr地址的第nr位
|
- 自旋锁。是一种对临界资源进行互斥访问的典型手段。为了获得一个自旋锁,在某个cpu上运行的代码先执行一个原子操作,测试并设置(test-and-set)某个内存变量,由于是原子的,其他执行单元是不能访问这个内存变量的。如果测试结果表明这个这锁空闲,则程序获得这个自旋锁并继续执行;如果测试结果表明锁仍然被占用,程序将在一个小的循环内重复这个“测试并执行”操作,即进行所谓的“自旋”。
linux中与自旋锁相关的操作:
1.定义自旋锁 spinlock_t lock;
2.初始化自旋锁 spin_lock_init(&lock)
3.获得自旋锁 spin_lock(lock);//尝试获得自旋锁,如果能够立即获得锁,将立即返回,否则将自旋到该锁的保持者释放。
4.释放自旋锁
spin_unlock(&lock);
|
自旋锁一般的使用方法:
spinlock_t lock; spin_lock_init(&lock);//定义并初始化锁
spin_lock(&lock);
...//临界区
spin_unlock(&lock);
|
使用锁,执行单元在临界区执行的时候,还是可以被中断打断,还是不能完全解决并发的问题,那就有了一种新的结合方法,在临界区内关中断。
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_irqsave() = spin_unlock() + local_irq_restore()
spin_lock_bh() = spin_lock() + local_bh_disable()
spin_unlock_bh() = spin_unlock() + local_bh_enable()
|
使用锁注意的问题:A.自旋锁是忙等锁,当锁不可用时,cpu在空转。所以,只有在占有锁的时间极短的情况下使用!!B.自旋锁可能导致系统死锁。在获得锁的时候不能够第二次使用这个锁,否则造成死锁,不能够调用能够引起阻塞的函数,如copy_from_user(),copy_to_user(),kmalloc等函数。
- 读写自旋锁。这种锁在写操作方面,只能最多有一个写进程,在读操作方面,同时可以有多个读执行单元。注意,读和写是不能同时进行的。
1.定义并初始化自旋锁
rwlock_t my_rwlock = RW_LOCK_UNLOCKED;//静态初始化
rwlock_t my_rwlock;
rwlock_init(&my_rwlock);/*动态初始化*/
2.读锁定
void read_lock(rwlock_t *lock);
void read_lock_irqsave(rwlock_t *lock, unsigned long flags);
void read_lock_irq(rwlock_t *lock);
void read_lock_bh(rwlock_t *lock);
3.读解锁.在对共享资源进行读取之前,应该先调用读锁定函数,完成后调用读解锁函数。
void read_unlock(rwlock_t *lock);
void read_unlock_irqsave(rwlock_t *lock, unsigned long flags);
void read_unlock_irq(rwlock_t *lock);
void read_unlock_bh(rwlock_t *lock);
4.写锁定
void write_lock(rwlock_t *lock);
void write_lock_irqsave(rwlock_t *lock, unsigned long flags);
void write_lock_irq(rwlock_t *lock);
void write_lock_bh(rwlock_t *lock);
int write_trylock(rwlock_t *lock);
5.写解锁.在对共享资源进行写之前,要先调用写锁定函数,完成后调用写解锁函数。
void write_unlock(rwlock_t *lock);
void write_unlock_irqrestore(rwlock_t *lock, unsigned long flags);
void write_unlock_irq(rwlock_t *lock);
void write_unlock_bh(rwlock_t *lock);
|
- 顺序锁(seqlock)。写与写可以宏观上同时进行,读与写可以在宏观上同时进行,但是,写与写之间是互斥的。要求:顺序锁保护的共享资源不能够保护指针。
1.获得自旋锁
void write_seqlock(seqlock_t *sl);
int write_tryseqlock(seqlock_t *sl);
write_seqlock_irqsave(lock,flags);
write_seqlock_irq(lock);
write_seqlock_bh(lock);
2.释放自旋锁
void write_sequnlock(seqlock_t *sl);
write_sequnlock_irqrestore(lock,flags);
write_sequnlock_irq(lock);
write_sequnlock_bh(lock); |
写执行单元使用顺序锁的模式如下:
write_seqlock(&seqlock_a); ...//写操作代码 write_sequnlock(&seqlock_a);
|
读执行单元涉及如下顺序锁操作:
1.读开始
unsigned read_seqbegin(const seqlock_t *sl);//返回顺序锁s1的当前顺序号。
read_seqbegin_irqsave(lock,flags); //local_irq_save() + read_seqbegin()
2.重读.读执行单元在访问完被顺序锁s1保护的共享资源后,需要调用该函数来检查,看是否在读期间有写操作对共享资源做了改变。如果有写操作,读执行单元就需要重新进行读操作。
int read_seqretry(const seqlock_t *sl, unsigned iv);
read_seqretry_irqrestore(lock,iv,flags);//== read_seqretry() + local_irq_restore().
|
读执行单元使用顺序锁的模式如下:
do{
seqnum = read_seqbin(&seqlock_a);
//读操作代码块
...
}while(read_seqretry(&seqlock_a,seqnum));
|
- RCU(read-copy-update).对于被RCU保护的共享数据结构,读执行单元不需要获得任何锁就可以访问它,不使用任何指令。使用RCU的写执行单元在访问它前需首先复制一个副本,然后对副本进行修改,最后使用一个回调机制在适当的时机把指向原来数据的指针重新指向新的被修改的数据,这个时机就是所有引用该数据的cpu都退出对共享数据的操作的时候。读执行单元没有任何同步开销,而写执行单元的同步开销则取决于使用的写执行单元间的同步机制。Linux系统中提供的RCU操作如下4种:
1.读锁定
rcu_read_lock()
rcu_read_lock_bh()
2.读解锁
rcu_read_unlock()
rcu_read_lock_bh()
使用RCU进行读的模式如下:
rcu_read_lock()
...//读临界区
rcu_read_unlock()
3.同步RCU
synchronize_rcu()
该函数由RCU写执行单元调用,它将阻塞写执行单元,直到所有的读执行单元已经完成读执行单元临界区,写执行单元才可以继续下一步操作。synchronize_rcu()保证所有CPU都处理完正在运行读执行单元临界区。
4.挂接回调
void fastcall call_rcu(struct rcu_head *head, void (*func)(struct rcu_head *rcu));
函数call_rcu也由rcu写执行单元调用,它不会使写执行单元阻塞,可以在中断上下文中使用。该函数将把函数func挂接到rcu回调函数中,然后立即返回。
|
- 信号量。Linux系统中与信号量相关的操作主要有如下4种:
1.定义信号量
struct semaphore sem;
2.初始化信号量
void sema_init(struct semaphore *sem, int val);//该函数初始化信号量,并设置信号量sem的值为val。
void init_MUTEX(struct semaphore *sem);//该函数用于初始化一个用于互斥的信号量,它把信号量sem的值设置为1,等同于sema_init(struct semaphore *sem,1).
void init_MUTEX_LOCKED(struct semaphore *sem);
3. 获取信号量
void down(struct semaphore *sem);//它会导致睡眠,不能在中断上下文使用
int down_interruptible(struct semaphore *sem);//该函数与down类似,不同之处为,因为down而进入睡眠状态的进程不能被信号打断,后者可以被信号打断,信号也会导致该函数返回,这个时候的返回值非0。
int down_trylock(struct semaphore *sem);//该函数尝试获得信号量sem,如果能够立刻获得,它就获得该信号量并返回0。否则,返回非0值。不会导致调用者睡眠,可以在中断上下文使用。在使用down_interruptible()获得信号量时,对返回值一般都会进行检查,如果非0,通常立即返回-ERESTARTSYS,如:
if(down_inerruptible(&sem)){
return -ERESTARTSYS;
}
4.释放信号量
void up(struct semaphore *sem);//该函数释放信号量sem,唤醒等待者。信号量一般这样使用:
//定义信号量
DELARE_MUTEX(mount_sem);
down(&mount_sem);//获取信号量,包含临界区
//临界区
up(&mount_sem);//释放信号量
|
三、同步。
同步就是一个执行单元的继续执行需要等待另一个执行单元完成某事,保证执行的先后顺序。信号量就可以用来完成同步的功能。
当然,linux提供了另一个比信号量更好的同步机制,即完成量(completion,),它用于一个执行单元等待另一个执行单元执行完某事。linux系统中与completion相关的操作主要有以下4种:
1.定义完成量
struct completion my_completion;
2.初始化completion
init_completion(&my_completion);
对my_completion的定义和初始化可以通过如下快捷方式实现:
DELARE_COMPLETION(my_completion);
3.等待完成量
void wait_for_completion(struct completion *c);
4.唤醒完成量
void complete(struct completion *c);//只唤醒一个等待的执行单元
void complet_all(struct completion *c);//唤醒所有等待同一完成量的执行单元
|
四、何时使用自旋锁和信号量?
信号量是进程级的。用于多个进程之间对资源的互斥,代表进程来竞争资源,如果竞争失败,会发生进程上下文切换,当前进程进入睡眠状态,cpu将运行其他进程。进程上下文切换的开销很大,因此,只有当进程占用资源时间较长时,用信号量才是较好的选择。当访问所要保护的资源时,自旋锁方便。
- 进程上下文切换时间小,使用自旋锁,比较大,使用信号量。
- 信号量可以运行临界区内包含引起阻塞的代码,而自旋是不允许的。
- 如果被包含的共享资源在中断或者软中断的情况下使用,那就只能选择自旋锁。
读写信号量:
读写信号量与信号量的关系与读写自旋锁和自旋锁的关系类似,读写信号量可能引起进程阻塞,可允许N个读执行单元同时访问共享资源,而最多只能有一个写执行单元。
1.定义和初始化读写信号量
struct rw_semaphore my_rws;
void init_rwsem(struct rw_semaphore *sem);
2.获取/释放 读信号量
void down_read(struct rw_semaphore *sem);
int down_read_trylock(struct rw_semaphore *sem);
void up_read(struct rw_semaphore *sem);
3.写信号量获取
void down_write(struct rw_semaphore *sem);
int down_write_trylock(struct rw_semaphore *sem);
4.写信号量释放
void up_write(struct rw_semaphore *sem);
使用方法:
rw_semaphore rw_sem;//定义读写信号量
init_rwsem(&rw_sem);//初始化读写信号量
//读时获取信号量
down_read(&rw_sem);
...//临界资源
up_read(&rw_sem);
// 写时获取信号量
down_write(&rw_sem);
//临界资源
up_write(&rw_sem);
|
阅读(668) | 评论(0) | 转发(0) |