1.linux 内核中发生竞态的几种情况:
a) 对称多处理器的多个CPU
b) 单CPU内进程与抢占它的进程(2.6内核以后支持内核抢占)。
c) 中断与进程之间。
2.linux内核中的互斥机制:
a) 中断屏蔽:中断屏蔽一般不单独使用,一般和自旋锁配合使用。因为这几个函数,只能禁用本cpu内的中断,解决不了SMP引发的竞态问题。
Local_irq_disable()
Local_irq_save(flags)
Local_irq_enable()
Local_irq_restore(flags)
只禁止中断的底半部:local_bh_disable() local_bh_enable()
b) 原子操作:位和整形变量的原子操作都依赖于CPU的原子操作,和CPU架构密切相关。
(1)整形原子操作一些函数:
在atomic_t变量中不能记录大于24位的整数。
①设置原子变量的值:
Void atomic_set(atomic_t *v, int i)
Atomic_t v = ATOMIC_INIT(int i);
②atomic_read(atomic_t *v)
Atomic_add (int i, atomic_t *v)
Atomic_sub(int i, atomic_t *v);
Atomic_inc(atomic_t *v);
Atomic_dec(atomic_t *v);
Int Atomic_inc_and_test(atomic_t *v);//自增后测试是否为0 ,为0返回true。
Int Atomic_dec_and_test(atomic_t *v);
Int atomic_sub_and_test (int i, atomic_t *v);//注意没有加。
③atomic_add_and_return(int i, atomic_t *v);
atomic_sub_and_return(int i, atomic_t *v);
atomic_inc_and_return(int i, atomic_t *v);
atomic_dec_and_return(int i, atomic_t *v);
操作后返回新值。
(2) 位原子操作:
void clear_bit (nr, void * addr);
Void set_bit(nr, void* addr);
Void Change_bit(nr, void* addr);
Test_bit(nr, void *addr);
Int test_and_set_bit(nr, void *addr);
Int test_and_clear_bit(nr, void *addr)
Int test_and_change_bit(nr, void *addr)
返回旧值,并操作。
c) 自旋锁:
spinlock_t lock;
Spin_lock_int(lock);
Spin_lock(lcok);
Spin_trylock(lock);
Spin_unlock(lock);
自旋锁主要针对SMP或单CPU但内核可抢占的情况。对于单CPU内核不可抢占的系统,自旋锁为空操作。但是还会受到中断和底半部的影响。衍生出一下几个函数:
Spin_lock_irq() = spin_lock() + local_irq_disable();
Spin_unlock_irq = spin_lock() + local_irq_enable();
Spin_lock_irqsave();
Spin_unlock_irq_restore();
使用自旋锁注意:
(1)已经拥有某个自旋锁的CPU不能第二次获得这个自旋锁,否则CPU将死锁。
(2)当进程上下文和中断上下文都会使用同一个自旋锁的时候才需要禁用中断。(因为中断程序运行期间不能进行进程切换)
(3)获取自旋锁后不能阻塞。因为进程阻塞将会导致内核抢占,另一进程被调用,另一进程可能企图获得同一个自旋锁。这会引起(4)条所说的问题。
(4)自旋锁为什么要禁止抢占:假如进程A拥有自旋锁的过程被B抢占,而B企图获取同一个自旋锁,理想情况下,B会自耗尽自己的时间片。糟糕情况系统会死锁或崩溃。
d) 信号量
Struct semaphore sem;
Void sema_init(struct semaphore *sem, int val);
#define init_MUTEX(sem) sema_init(sem, 1);
#define init_MUTEXT_LOCKED (sem) sema_init(sem, 0);
DECLARE_MUTEX(name);
DECLARE_MUTEX_LOCKED(name);
Void down (struct semaphore * sem);
Int down_interruptable (struct semaphore * sem);//被信号打断时,返回非零值,通常立即返回-ERESTARTSYS;
Int down_trylock(struct semahore *sem);得不到信号量返回非零值。
该函数可在中断上下文中使用,因为他不会导致调用者睡眠。
Void up(struct semaphore *sem);
Linux还提供了更好的同步机制,即 完成量。
Struct completion my_completion;
Init_completion(&my_completion);
DECLARE_COMPLETION(my_completion);
Void wait_for_completion(struct completion &my_completion);//等待完成量
Void complete(struct completion * c);
Void complet_all(struct completion *c);
completion机制的典型应用是模块退出时的内核线程终止:void complete_and_exit(struct completion *c, long retval);
Complete唤醒一个等待的执行单元,complete_all释放所有等待同一完成量的执行单元。
自旋锁VS信号量:
(1)信号量是进程级别的。代表进程来争夺资源,如果竞争失败会发生进程上下文切换,鉴于进程上下文切换开销很大,一次只有当进程占用资源时间较长时,用信号量才是较好的选择。
(2)信号量会引起休眠并且存在于进程上下文中,所以如果在中断上下文中,只能选择自旋锁。但也可以使用down_trylock。但是如果下半部选择工作队列的话,则也可以使用信号量。
(3)自旋锁保护的临界区不能包含有引起阻塞的代码,如果临界区有引起阻塞的代码,只能选择信号量。
虽然信号量能完成互斥体的功能,不过内核还是提供了正宗的互斥体:
Struct mutex my_mutex;
Mutex_init(&my_mutex);
Void inline __sched mutex_lock(struct mutex *lock);
Int __sched mutex_lock_interruptible(struct mutex *lock);
Int __sched mutex_trylock(struct mutex *lock);
Void __sched mutex_unlock(struct mutex *lock);
互斥体的应用场合和信号量一样。
总结:中断屏蔽很少单独使用,原子操作只能针对整数进行,因此自旋锁和信号量应用最为广泛。自旋锁 锁定期间不允许阻塞,要求临界区小。信号量允许临界区阻塞,可以使用临界区大的情况。