##5.内核同步
临界区:访问和操作共享数据的代码段。
内核提供了两组原子操作接口:一组针对整数进行操作,另一组针对单独的位进行操作。Linux支持的所有体系结构上都实现了这两组接口。
自旋锁:最多只能被一个可执行线程持有,可以防止多于一个的执行线程同时进入临界区。一个被占用的自旋锁使得请求它的线程在等待锁重新可用时自旋。注意自旋锁不可递归,所以小心自旋锁。
互斥锁:最多只能被一个可执行线程持有,可以防止多于一个的执行线程同时进入临界区。一个被占用的互斥锁使得请求它的线程在等待锁重新可用时睡眠。
###5.1原子操作
某些汇编指令具有“读-修改-写”类型,他们访问寄存器单元两次,一次读,一次写。 进行一次或零次对内存访问的汇编指令是原子的。
Linux中的原子操作:
asm/atomic中:
原子变量:
typedef struct {
int counter;
} atomic_t;
原子操作:
#define atomic_read(v) (*(volatile int *)&(v)->counter)
#deinfe atomic_set(y,i) (((v)->counter) = (i))
原子位操作:
#define set_bit(nr ,p) ATOMIC_BITOP_LE(set_bit ,nr ,p)
#define clear_bit(nr ,p) ATOMIC_BITOP_LE(clear_bit, nr, p)
#define change_bit(nr, p) ATOMIC_BITOP_LE(change_bit, nr, p)
#define test_and_set_bit(nr, p) ATOMIC_BITOP_LE(test_and_set_bit, nr, p)
#define test_and_clear_bit(nr, p) ATOMIC_BITOP_LE(test_and_clear_bit, nr, p)
###5.2优化和内存屏障
优化屏障:
Linux用宏barrier实现优化屏障,gcc编译器的优化屏障宏定义列出如下(在include/linux/compiler-gcc.h中):
/* Optimization barrier */
/* The “volatile” is due to gcc bugs */
#define barrier() __asm__ __volatile__("": : :"memory")
内存屏障:
#ifdef CONFIG_ARCH_HAS_BARRIERS
#include <mach/barriers.h>
#elif defined(CONFIG_ARM_DMA_MEM_BUFFERABLE) || defined(CONFIG_SMP)
#define mb() do { dsb(); outer_sync(); } while (0)
#define rmb() dsb()
#define wmb() do { dsb(st); outer_sync(); } while (0)
#define dma_rmb() dmb(osh)
#define dma_wmb() dmb(oshst)
#else
#define mb() barrier()
#define rmb() barrier()
#define wmb() barrier()
#define dma_rmb() barrier()
#define dma_wmb() barrier()
#endif
###5.3自旋锁
###5.3.1基本原理 自旋锁与互斥锁有点类似,只是自旋锁不会引起调用者睡眠,如果自旋锁已经被别的执行单元保持,调用者就一直循环在那里看是否该自旋锁的保持者已经释放了锁,“自旋”一词就是因此而得名。
由于自旋锁使用者一般保持锁时间非常短,因此选择自旋而不是睡眠是非常必要的,自旋锁的效率远高于互斥锁,而且他可以在任何上下文使用。
如果被保护的共享资源只在进程上下文访问,使用信号量保护该共享资源非常合适,如果对共享资源的访问时间非常短,自旋锁也可以。但是如果被保护的共享资源需要在中断上下文访问(包括底半部即中断处理句柄和顶半部即软中断),就必须使用自旋锁。
自旋锁保持期间是抢占失效的,而信号量和读写信号量保持期间是可以被抢占的。自旋锁只有在内核可抢占或SMP的情况下才真正需要,在单CPU且不可抢占的内核下,自旋锁的所有操作都是空操作。
###5.3.2自旋锁API
spin_lock_init(x)
该宏用于初始化自旋锁x。自旋锁在真正使用前必须先初始化。该宏用于动态初始化。
DEFINE_SPINLOCK(x)
该宏声明一个自旋锁x并初始化它。该宏在2.6.11中第一次被定义,在先前的内核中并没有该宏。
spin_trylock(lock)
该宏尽力获得自旋锁lock,如果能立即获得锁,它获得锁并返回真,否则不能立即获得锁,立即返回假。它不会自旋等待lock被释放。
spin_lock(lock)
该宏用于获得自旋锁lock,如果能够立即获得锁,他就马上返回,否则,它将自旋在那里,知道该自旋锁的保持着释放,这时,它获得并返回。总之,只有它获得锁才返回。
spin_lock_irqsave(lock, flags)
该宏获得自旋锁的同时把标识寄存器的值保存到变量flags中并失效本地中断。
spin_lock_irq(lock)
该宏类似于spin_lock_irqsave,只是该宏不保存标识寄存器的值。
spin_lock_bh(lock)
该宏在得到自旋锁的同时失效本地软中断。
spin_unlock(lock)
该宏释放自旋锁lock,它与spin_trylock或spin_lock配对使用。如果spin_trylock返回假,表明没有获得自旋锁,因此不必使用spin_unlock释放。
spin_unlock_irqrestore(lock, flags)
该宏释放自旋锁lock的同时,也恢复标识寄存器的值为变量flags保存的值。它与spin_lock_irqsave配对使用。
spin_unlock_irq(lock)
该宏释放自旋锁lock的同时,也使能本地中断。它与spin_lock_irq配对应用。
spin_unlock_bh(lock)
该宏释放自旋锁lock的同时,也使能本地的软中断。它与spin_lock_bh配对使用。
###5.3.3自旋锁使用场景
-
如果被保护的共享资源只在进程上下文访问和软中断上下文访问,那么当在进程上下文访问共享资源时,可能被软中断打断,从而可能进入软中断上下文来对被保护的共享资源访问,因此对于这种情况,对共享资源的访问必须使用spin_lock_bh和spin_unlock_bh来保护。
-
当然使用spin_lock_irq和spin_unlock_irq以及spin_lock_irqsave和spin_unlock_irqstore也可以,他们失效了本地硬中断,失效硬中断隐式地也失效了软中断。但是使用spin_lock_bh和spin_unlock_bh是最恰当的,他比其他两个快。
-
如果被保护的共享资源只在进程上下文和tasklet或timer上下文访问,那么应该使用与上面情况相同的获得释放锁的宏,因为tasklet和timer是用软中断实现的。
-
如果被保护的共享资源只在一个tasklet或timer上下文访问,那么不需要任何自旋锁保护,因为同一个tasklet或timer只能在一个CPU上运行,即使是在SMP环境下也是如此。实际上tasklet在调用tasklet_schedule标记其需要被调度时已经把该tasklet绑定到当前CPU,因此同一个tasklet绝不可能在其他CPU上运行。
-
timer也是在其被使用add_timer添加到timer队列中时已经被绑定到当前CPU,所以同一个timer绝不可能运行在其他CPU上。当然同一个tasklet有两个实例同时运行在同一个CPU就更可能了。
-
如果被保护的共享资源只在两个或多个tasklet或timer上下文访问,那么对共享资源的访问仅需要用spin_lock和spin_unlock来保护,不必使用_bh版本,因为当tasklet或timer运行时,不可能有其他tasklet或timer在当前CPU上运行。
-
如果被保护的共享资源只在一个软中断(tasklet和timer除外)上下文访问,那么这个共享资源需要用spin_lock和spin_unlock来保护,因为同样的软中断可以同时在不同的CPU上运行。
-
如果被保护的共享资源在两个或多个软中断上下文访问,那么这个共享资源当然更需要用spin_lock和spin_unlock来保护,不同的软中断能够同时在不同的CPU上运行。
-
如果被保护的共享资源在软中断(包括tasklet和timer)或进程上下文和硬中断上下文访问,那么在软中断或进程上下文访问期间,可能被硬中断打断,从未进入硬中断上下文对共享资源进行访问,因此,在进程或软中断上下文需要使用spin_lock_irq和spin_unlock_irq来保护对共享资源的访问。
-
而在中断处理函数中使用什么版本,需依情况而定,如果只有一个中断处理句柄访问该共享资源,那么在中断处理句柄中仅需要spin_lock和spin_unlock来保护对共享资源的访问就可以了。因为在执行中断处理句柄期间,不可能被同一CPU上的软中断或进程打断。但是如果有不同的中断处理句柄访问该共享资源,那么需要在中断处理句柄中是哟个spin_lock_irq和spin_unlock_irq来保护对共享资源的访问。
-
在使用spin_lock_irq和spin_unlock_irq的情况下,完全可以用spin_lock_irqsave和spin_unlock_irqresotre取代,那具体应该使用哪一个也需要依情况而定,如果可以确信对共享资源访问前中断是使能的,那么使用spin_lock_irq更好一些。因为它比spin_lock_irqsave要快一些,但是如果你不能确定是否中断使能,那么使用spin_lock_irqsave和spin_unlock_irqrestore更好,因为他将恢复访问共享资源前的中断标识而不是直接使能中断。当然,有些情况下需要在访问共享资源时必须中断失效,而访问完后必须中断使能,这样的情形使用spin_lock_irq和spin_unlock_irq最好。
需要特别注意
spin_lock用于阻止在不同CPU上的执行单元对共享资源的同时访问以及不同的进程上下文互相抢占导致的对共享资源的非同步访问,而中断失效和软中断失效却是为了阻止在同一CPU上软中断或中断对共享资源的非同步访问。
###5.4信号量和互斥
加锁原语,让等待者睡眠直到等待资源变为空闲。执行信号量和互斥的时候内核是可以抢占的。
###5.5 Completion原语 Completion原语使用线程等待队列来作为它的等待队列。
struct completion {
unsigned int done;
wait_queue_head_t wait;
};
static inline void init_completion(struct completion *x)
{
x->done = 0;
init_waitqueue_head(&x->wait);
}
void __sched wait_for_completion(struct completion *x)
{
wait_for_common(x, MAX_SCHEDULE_TIMEOUT, TASK_UNINTERRUPTIBLE);
}
EXPORT_SYMBOL(wait_for_completion);
static long __sched
wait_for_common(struct completion *x, long timeout, int state)
{
return __wait_for_common(x, schedule_timeout, timeout, state);
}
static inline long __sched
__wait_for_common(struct completion *x,
long (*action)(long), long timeout, int state)
{
might_sleep();
spin_lock_irq(&x->wait.lock);
timeout = do_wait_for_common(x, action, timeout, state);
spin_unlock_irq(&x->wait.lock);
return timeout;
}
static inline long __sched
do_wait_for_common(struct completion *x,
long (*action)(long), long timeout, int state)
{
if (!x->done) {
DECLARE_WAITQUEUE(wait, current);
注释:初始化一个等待队列结构体wait_queue_t,并设置当前的进程描述符指针与其关联。
__add_wait_queue_tail_exclusive(&x->wait, &wait);
注释:把该队列加入到等待队列中,类型是互斥的,一次只能唤醒一次。
do {
if (signal_pending_state(state, current)) {
timeout = -ERESTARTSYS;
break;
}
__set_current_state(state);
spin_unlock_irq(&x->wait.lock);
timeout = action(timeout);
spin_lock_irq(&x->wait.lock);
} while (!x->done && timeout);
__remove_wait_queue(&x->wait, &wait);
if (!x->done)
return timeout;
}
x->done--;
return timeout ?: 1;
}
###5.6禁止本地中断 盲目的禁止本地中断来避免竞争是无能的表现!
并发避免死锁技巧
线程按照相同的顺序获得锁: 第一个任务加锁数据顺序:A –> B –> C 第二个任务加锁数据顺序:A –> B –> C 否则容易死锁
获得锁顺序加注释
释放锁的顺序和死锁无关,但最好还是以获得锁的相反顺序释放锁
The spinning behavior is optimal for short hold times and code that cannot sleep (interrupt handlers, for example). In cases where the sleep time might be long or you potentially need to sleep while holding the lock, the semaphore is a solution.
Requirement Recommended Lock
Low overhead locking Spin lock is preferred.
Short lock hold time Spin lock is preferred.
Long lock hold time Mutex is preferred.
Need to lock from interrupt context Spin lock is required.
Need to sleep while holding lock Mutex is required.
禁止抢占
task A manipulates per-processor variable foo, which is not protected by a lock
task A is preempted
task B is scheduled
task B manipulates variable foo
task B completes
task A is rescheduled
task A continues manipulating variable foo
有些架构上,编译器和处理器为了提高效率,使指令重新排序 在访问硬件时,需要确保一个给定的读写顺序,使用屏障禁止指令重新排序