Linux并发与同步(一)原子操作/spinlock/mutex

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/zhouhuacai/article/details/78074468

背景

在linux内核和驱动代码中,需要对共享数据进行保护,防止共度资源被并发访问而发生数据覆盖,被访问数据不一致的情况.这些情况可能会造成系统不稳定,且很难跟踪调试.

  • 临界区: 访问和操作共享数据的代码段.
  • 临界资源: 共享的数据(静态局部变量,全局变量,buffer, 链表等).
  • 并发源:访问临界区的线程或执行路径.

内核4种并发源:

  • 中断和异常 
  • 软中断和taskelt
  • 内核抢占
  • 多处理器并发执行

原子操作

原子操作是指指令以原子方式执行,过程不会被打断.
ARM中使用ldrex与strex指令保证操作的原子性.
在Linux内核中提供了一系统的原子整数操作函数。

ATOMIC_INIT(int i)  //在声明一个atmoic_t变量时,将它初始化为i
ATOMIC_INIT(int i)  //在声明一个atmoic_t变量时,将它初始化为i
int atmoic_read(atmoic_t *v)    //原子地读取整数变量v
void atmoic_set(atmoic_t *v,int i)  //原子地设置v值为i
void atmoic_add(atmoic_t *v,int i)  //原子地从v值加i
void atmoic_sub(atmoic_t *v,int i)  //原子地从v值减i
void atmoic_inc(atmoic_t *v)    //原子地从v值加1
void atmoic_dec(atmoic_t *v)    //原子地从v值减1
int atmoic_sub_and_test(int i,atmoic_t *v)  //原子地从v值减i,如果结果等于0返回真,否则返回假
int atmoic_add_negative(int i,atmoic_t *v)  //原子地从v值减i,如果结果是负数返回真,否则返回假
int atmoic_dec_and_test(atmoic_t *v)    //原子地给v减1,如果结果等于0返回真,否则返回假
int atmoic_inc_and_test(atmoic_t *v)    //原子地给v加1,如果结果等于0返回真,否则返回假

原子操作最常见的用途就是实现计数器,使用复杂的锁机制来保护一个单纯的计数是很笨拙的,原子操作比起复杂的同步方法来说,给系统带来的开销小,对高速缓存行的影响也小。

除了原子整数操作外,内核还提供了一组针对位这一级数据进行操作的函数,位操作函数是对普通的内在地址进行操作的,它的参数是一个指针和一个位号。由于是对普通的指针进程操作,所以没有像atomic_t这样的类型约束。

void set_bit(int nr,void *addr) //原子地设置addr所指对象的第nr位
void clear_bit(int nr,void *addr)   //原子地清空addr所指对象的第nr位
void change_bit(int nr,void *addr)  //原子地翻转addr所指对象的第nr位
int test_and_set_bit(int nr,void *addr) //原子地设置addr所指对象的第nr位,并返回原先的值
int test_and_clear_bit(int nr,void *addr)   //原子地清空addr所指对象的第nr位,并返回原先的值
int test_and_change_bit(int nr,void *addr)  //原子地翻转addr所指对象的第nr位,并返回原先的值
int test_bit(int nr,void *addr) //原子地返回addr所指对象的第nr位

内存屏障

原因

我们编程时,指令一般不会按照它们在源程序的顺序执行。原因是计算机为了提高程序执行的性能,会对它进行优化,这种优化主要有两种:

  • 编译器的优化:为了提高系统的性能,编译器在不影响逻辑的情况下会调整指令的顺序。
  • CPU执行的优化:为了提高流水线的性能,CPU的乱序执行可能会让后面的没有寄存器冲突的汇编指令先于前面的指令完成。
    当处理程序的同步时,这样的顺序的调整会造成某些失控,所以我们有时候需要有措施来保证程序的顺序不被打乱。

措施

  • 优化屏障:保证编译程序将放在原语操作前面的汇编指令不与放在原语操作之后的汇编语言指令混淆,保证在操作原语之前的指令不会被优化到任何出现在操作原语之后的指令的后面,反之也不会操作原语之后的指令被优化到任何出现在操作原语之前的指令的前面。
#define barrier()       __asm__ __volatile__("": : :"memory") 
  • 保证CPU执行时原语操作之前的指令在原语之后的指令前完成。
#define mb() alternative("lock; addl $0,0(%%esp)", "mfence", X86_FEATURE_XMM2)  
#define rmb() alternative("lock; addl $0,0(%%esp)", "lfence", X86_FEATURE_XMM2)  
#define wmb() alternative("lock; addl $0,0(%%esp)", "sfence", X86_FEATURE_XMM)  
#ifdef CONFIG_X86_OOSTORE  
#define wmb() alternative("lock; addl $0,0(%%esp)", "sfence", X86_FEATURE_XMM)  
#else  
#define wmb()   __asm__ __volatile__ ("": : :"memory")  
#endif  
#ifdef CONFIG_SMP  
#define smp_mb()    mb()  
#define smp_rmb()   rmb()  
#define smp_wmb()   wmb()  
#else  
#define smp_mb()    barrier()  
#define smp_rmb()   barrier()  
#define smp_wmb()   barrier()  
#endif 

spinlock

spinlock是linux中最常用的锁,该锁会关闭内核抢占.为什么spinlock允许内核抢占(这里不考虑RT-linux)?

  • 抢占调度相当于持有锁的进程睡眠,违背了spinlock不能睡眠和快速执行完成的设计语义.
  • 抢占调度进程也有可能会去申请spinlock锁,会导致死锁.

spinlock有几个变种:

spin_lock: //只禁止内核抢占,不会关闭本地中断
spin_lock_irq //禁止内核抢占,且关闭本地中断
spin_lock_irqsave //禁止内核抢占,关闭中断,保存中断状态寄存器的标志位

关于几个变种使用的总结:

  • 任何情况下使用spin_lock_irq都是安全的。因为它既禁止本地中断,又禁止内核抢占。
  • spin_lock比spin_lock_irq速度快,但是它并不是任何情况下都是安全的。适合在确保不会发生中断的场合使用.
  • spin_lock_irqsave在锁返回时,之前开的中断,之后也是开的;之前关,之后也是关。但是spin_lock_irq则不管之前的开还是关,返回时都是开的

信号量

信号量与spinlock不同,允许进程在取不到锁的时候进入睡眠.
信号量在创建时需要设置一个初始值,表示同时可以有几个任务可以访问该信号量保护的共享资源,初始值为1就变成互斥锁(Mutex),即同时只能有一个任务可以访问信号量保护的共享资源。

Mutex互斥锁

mutex互斥锁就是信号量的一个特例.但比信号量执行速度更快,可扩展性更好,mutex数据结构的定义也比信号量小.

如何选择锁?

在编写内核代码时,需要考虑以下几个问题:

  • 除了当前执行路径外,是否会有其它路径会访问它?例如中断处理程序, worker,tasklet,软中断等?
  • 当前执行代码路径被抢占时,被调度的进程会不会访问该临界资源?
  • 进程会不会睡眠阻塞等待资源?

如何选择使用spinlock与mutex?
在中断上下文中,使用spinlock,如果临界区有睡眠,隐含睡眠的动作(如kmalloc等),使用mutex.在信号量与Mutex之间,优先选择使用mutex.

参考资料

<1>. <<奔跑吧linux>>
<2>.http://www.cnblogs.com/aaronLinux/p/5890924.html
<3>.http://blog.csdn.net/qq_34665912/article/details/51192133

阅读更多
换一批

没有更多推荐了,返回首页