Linux内核同步机制
2016-08-23 21:38
375 查看
Linux内核同步机制,常用的有自旋锁,信号量,互斥体,原子操作,顺序锁,RCU,内存屏障等。
本篇主要介绍原子操作,自旋锁,信号量和内存屏障。
内核提供了两组原子操作接口,一组针对数组,一组针对单独的位进行操作:
自旋锁用结构spinlock_t描述,在include/linux/spinlock.h
在Linux内核中何时使用spin_lock,何时使用spin_lock_irqsave很容易混淆,下面详细解释各个API。
spin_lock (spin_lock –> raw_spin_lock)*
只禁止内核抢占,不会关闭本地中断
spin_lock_irq ( spin_lock_irq—> raw_spin_lock_irq )
spin lock irq禁止内核抢占,且关闭本地中断
假如中断中也想获得这个锁,使用spin_lock_irq将中断禁止,就不会出现死锁的情况
spin_lock_irqsave (spin_lock_irqsave—>__raw_spin_lock_irqsave)
spin_lock_irqsave禁止内核抢占,关闭中断,保存中断状态寄存器的标志位.
spin_lock_irqsave在锁返回时,之前开的中断,之后也是开的;之前关,之后也是关。而spin_lock_irq在自旋的时候,不会保存当前的中断标志寄存器,只会在自旋结束后,将之前的中断打开。
因此,spin_lock时要明确知道该锁不会在中断处理程序中使用,如果在中断处理程序中使用,根据你期望在离开临界区后,是否改变中断的开启/关闭状态,来选择使用spin_lock_irq 或者 spin_lock_irqsave
自旋锁使用注意事项
1.自旋锁不应该长时间的持有。自旋是一种忙等待,当条件不满足时,会一直不断的循环判断条件是否满足,如果满足就解锁,运行之后的代码。因此会对linux的系统的性能有些影响。
2.自旋锁不能递归使用。
自旋锁使用示例:
如果有一个临界资源rx_signal,对该资源频繁的读写时的打开时,就有可能出现错误的rx_signal的状态,所以必须对rx_signal进行保护
由于使用信号量时,线程会睡眠,所以等待的过程不会占用CPU时间。所以信号量适用于等待时间较长的临界区。
持有信号量的代码可以被抢占,可以在持有信号量时去睡眠,但是当占用信号量的时候不能同时占有自旋锁,因为在等待信号量时可能会睡眠,而在持有自旋锁时是不允许睡眠的。
信号量可以同时允许任意数量的锁持有者,通常只有一个持有者的信号量叫互斥信号量,我们常用dowm_inperruptible函数获取信号量,它将把调用进程设置为TASK_INTERRUPTIBLE状态进入睡眠,如果用dowm函数获取信号量,它则将把调用进程设置为TASK_UNINTERRUPTIBLE状态进入睡眠,但这样进程在等待信号量的时候就不再响应信号了,所以,常用dowm_inperruptible函数获取信号量,用up函数释放信号量。
信号量结构体具体如下:
信号量结构体中有个自旋锁,这个自旋锁的作用是保证信号量的down和up等操作不会被中断处理程序打断。
使用方法
比如下面的代码:
a = 1;
b = 2;
编译器和处理器看不出a和b之间的依赖关系,有可能“优化”为b在a之前被赋值。
如果是
a = 1;
b= a;
这种情况下,因为a和b有依赖关系,所以编译器和处理器会顺序执行
内存屏障主要有:读屏障、写屏障、通用屏障等。
以读屏障为例,它用于保证读操作有序。屏障之前的读操作一定会先于屏障之后的读操作完成,写操作不受影响,同属于屏障的某一侧的读操作也不受影响。类似的,写屏障用于限制写操作。而通用屏障则对读写操作都有作用。而优化屏障则用于限制编译器的指令重排,不区分读写。前三种屏障都隐含了优化屏障的功能。比如:
tmp = ttt; *addr = 5; mb(); val = *data;
有了内存屏障就了确保先设置地址端口,再读数据端口。而至于设置地址端口与tmp的赋值孰先孰后,屏障则不做干预。
有了内存屏障,就可以在隐式因果关系的场景中,保证因果关系逻辑正确。
本篇主要介绍原子操作,自旋锁,信号量和内存屏障。
原子操作
原子操作可以保证指令以原子的方式进行执行,执行过程不被打断。API | 描述 |
---|---|
static inline void atomic_add(int i, atomic_t *v) | 给一个原子变量v增加i |
static inline int atomic_add_return(int i, atomic_t *v) | 同上,只不过将变量v的最新值返回 |
static inline void atomic_sub(int i, atomic_t *v) | 给一个原子变量v减去i |
static inline int atomic_sub_return(int i, atomic_t *v) | 同上,只不过将变量v的最新值返回 |
static inline int atomic_cmpxchg(atomic_t *ptr, int old, int new) | 比较old和原子变量ptr中的值,如果相等,那么就把new值赋给原子变量。 返回旧的原子变量ptr中的值 |
atomic_read | 获取原子变量的值 |
atomic_set | 设定原子变量的值 |
atomic_inc(v) | 原子变量的值加一 |
atomic_inc_return(v) | 同上,只不过将变量v的最新值返回 |
atomic_dec(v) | 原子变量的值减去一 |
atomic_dec_return(v) | 同上,只不过将变量v的最新值返回 |
atomic_sub_and_test(i, v) | 给一个原子变量v减去i,并判断变量v的最新值是否等于0 |
atomic_add_negative(i,v) | 给一个原子变量v增加i,并判断变量v的最新值是否是负数 |
static inline int atomic_add_unless(atomic_t *v, int a, int u) | 只要原子变量v不等于u,那么就执行原子变量v加a的操作。如果v不等于u,返回非0值,否则返回0值 |
原子整数操作
//定义 atomic_t v; //初始化 atomic_t u = ATOMIC_INIT(0); //操作 atomic_set(&v,4); // v = 4 atomic_add(2,&v); // v = v + 2 = 6 atomic_inc(&v); // v = v + 1 = 7 //实现原子操作函数实现 static inline void atomic_add(int i, atomic_t *v) { unsigned long tmp; int result; __asm__ __volatile__("@ atomic_add\n" "1: ldrex %0, [%3]\n" " add %0, %0, %4\n" " strex %1, %0, [%3]\n" " teq %1, #0\n" " bne 1b" : "=&r" (result), "=&r" (tmp), "+Qo" (v->counter) : "r" (&v->counter), "Ir" (i) : "cc"); }
原子位操作
//定义 unsigned long word = 0; //操作 set_bit(0,&word); //第0位被设置1 set_bit(0,&word); //第1位被设置1 clear_bit(1,&word); //第1位被清空0 //原子位操作函数实现 static inline void ____atomic_set_bit(unsigned int bit, volatile unsigned long *p) { unsigned long flags; unsigned long mask = 1UL << (bit & 31); p += bit >> 5; raw_local_irq_save(flags); *p |= mask; raw_local_irq_restore(flags); }
自旋锁
自旋锁,某个进程在试图加锁的时候,若当前锁已经处于“锁定”状态,试图加锁进程就进行不断的“旋转”,用一个死循环测试锁的状态,直到成功的获得锁。自旋锁用结构spinlock_t描述,在include/linux/spinlock.h
typedef struct { raw_spinlock_t raw_lock; #ifdef CONFIG_GENERIC_LOCKBREAK /*引入另一个自旋锁*/ unsigned int break_lock; #endif #ifdef CONFIG_DEBUG_SPINLOCK /*用于调试自旋锁*/ unsigned int magic, owner_cpu; void *owner; #endif #ifdef CONFIG_DEBUG_LOCK_ALLOC struct lockdep_map dep_map; /*映射lock实例到lock-class对象 #endif } spinlock_t;
API | 描述 |
---|---|
spin_lock_init(lock) | 初始化自旋锁,将自旋锁设置为1,表示有一个资源可用。 |
spin_is_locked(lock) | 如果自旋锁被置为1(未锁),返回0,否则返回1。 |
spin_unlock_wait(lock) | 等待直到自旋锁解锁(为1),返回0;否则返回1。 |
spin_trylock(lock) | 尝试锁上自旋锁(置0),如果原来锁的值为1,返回1,否则返回0。 |
spin_lock(lock) | 循环等待直到自旋锁解锁(置为1),然后,将自旋锁锁上(置为0)。 |
spin_unlock(lock) | 将自旋锁解锁(置为1)。 |
spin_lock_irqsave(lock, flags) | 循环等待直到自旋锁解锁(置为1),然后,将自旋锁锁上(置为0)。关中断,将状态寄存器值存入flags。 |
spin_unlock_irqrestore(lock, flags) | 自旋锁解锁(置为1)。开中断,将状态寄存器值从flags存入状态寄存器。 |
spin_lock_irq(lock) | 循环等待直到自旋锁解锁(置为1),然后,将自旋锁锁上(置为0)。关中断。 |
spin_unlock_ 4000 irq(lock) | 将自旋锁解锁(置为1)。开中断。 |
spin_unlock_bh(lock) | 将自旋锁解锁(置为1)。开启底半部的执行。 |
spin_lock_bh(lock) | 循环等待直到自旋锁解锁(置为1),然后,将自旋锁锁上(置为0)。阻止软中断的底半部的执行。 |
spin_lock (spin_lock –> raw_spin_lock)*
static inline void __raw_spin_lock(raw_spinlock_t *lock) { preempt_disable(); spin_acquire(&lock->dep_map, 0, 0, _RET_IP_); LOCK_CONTENDED(lock, do_raw_spin_trylock, do_raw_spin_lock); }
只禁止内核抢占,不会关闭本地中断
spin_lock_irq ( spin_lock_irq—> raw_spin_lock_irq )
static inline void __raw_spin_lock_irq(raw_spinlock_t *lock) { local_irq_disable(); preempt_disable(); spin_acquire(&lock->dep_map, 0, 0, _RET_IP_); LOCK_CONTENDED(lock, do_raw_spin_trylock, do_raw_spin_lock); }
spin lock irq禁止内核抢占,且关闭本地中断
假如中断中也想获得这个锁,使用spin_lock_irq将中断禁止,就不会出现死锁的情况
spin_lock_irqsave (spin_lock_irqsave—>__raw_spin_lock_irqsave)
static inline unsigned long __raw_spin_lock_irqsave(raw_spinlock_t *lock) { unsigned long flags; local_irq_save(flags); preempt_disable(); spin_acquire(&lock->dep_map, 0, 0, _RET_IP_); /* * On lockdep we dont want the hand-coded irq-enable of * do_raw_spin_lock_flags() code, because lockdep assumes * that interrupts are not re-enabled during lock-acquire: */ #ifdef CONFIG_LOCKDEP LOCK_CONTENDED(lock, do_raw_spin_trylock, do_raw_spin_lock); #else do_raw_spin_lock_flags(lock, &flags); #endif return flags; }
spin_lock_irqsave禁止内核抢占,关闭中断,保存中断状态寄存器的标志位.
spin_lock_irqsave在锁返回时,之前开的中断,之后也是开的;之前关,之后也是关。而spin_lock_irq在自旋的时候,不会保存当前的中断标志寄存器,只会在自旋结束后,将之前的中断打开。
因此,spin_lock时要明确知道该锁不会在中断处理程序中使用,如果在中断处理程序中使用,根据你期望在离开临界区后,是否改变中断的开启/关闭状态,来选择使用spin_lock_irq 或者 spin_lock_irqsave
自旋锁使用注意事项
1.自旋锁不应该长时间的持有。自旋是一种忙等待,当条件不满足时,会一直不断的循环判断条件是否满足,如果满足就解锁,运行之后的代码。因此会对linux的系统的性能有些影响。
2.自旋锁不能递归使用。
自旋锁使用示例:
如果有一个临界资源rx_signal,对该资源频繁的读写时的打开时,就有可能出现错误的rx_signal的状态,所以必须对rx_signal进行保护
int rx_signal; spinlock_t spinlock; int xxxx_init(void) { spin_lock_init(&spinlock); ............ } int xxxx_open(struct inode *inode, struct file *filp) { ............ spin_lock(&spinlock); if (rx_signal){ spin_unlock(&spinlock); return -EBUSY; } rx_signal ++; spin_unlock(&spinlock); ........... } int xxxx_release(struct inode *inode, struct file *filp) { ............ spin_lock(&spinlock); rx_signal --; ........... }
信号量
信号量也是一种锁,和自旋锁不同的是,线程获取不到信号量的时候,不会像自旋锁一样循环的去试图获取锁,而是进入睡眠,直至有信号量释放出来时,才会唤醒睡眠的线程,进入临界区执行。由于使用信号量时,线程会睡眠,所以等待的过程不会占用CPU时间。所以信号量适用于等待时间较长的临界区。
持有信号量的代码可以被抢占,可以在持有信号量时去睡眠,但是当占用信号量的时候不能同时占有自旋锁,因为在等待信号量时可能会睡眠,而在持有自旋锁时是不允许睡眠的。
信号量可以同时允许任意数量的锁持有者,通常只有一个持有者的信号量叫互斥信号量,我们常用dowm_inperruptible函数获取信号量,它将把调用进程设置为TASK_INTERRUPTIBLE状态进入睡眠,如果用dowm函数获取信号量,它则将把调用进程设置为TASK_UNINTERRUPTIBLE状态进入睡眠,但这样进程在等待信号量的时候就不再响应信号了,所以,常用dowm_inperruptible函数获取信号量,用up函数释放信号量。
信号量结构体具体如下:
struct semaphore { spinlock_t lock; unsigned int count; struct list_head wait_list; };
信号量结构体中有个自旋锁,这个自旋锁的作用是保证信号量的down和up等操作不会被中断处理程序打断。
API | 描述 |
---|---|
sema_init(struct semaphore *, int) | 以指定的计数值初始化动态创建的信号量 |
init_MUTEX(struct semaphore *) | 以计数值1初始化动态创建的信号量 |
init_MUTEX_LOCKED(struct semaphore *) | 以计数值0初始化动态创建的信号量(初始为加锁状态) |
down_interruptible(struct semaphore *) | 以试图获得指定的信号量,如果信号量已被争用,则进入可中断睡眠状态 |
down(struct semaphore *) | 以试图获得指定的信号量,如果信号量已被争用,则进入不可中断睡眠状态 |
down_trylock(struct semaphore *) | 以试图获得指定的信号量,如果信号量已被争用,则立即返回非0值 |
up(struct semaphore *) | 以释放指定的信号量,如果睡眠队列不空,则唤醒其中一个任务 |
/* 定义并声明一个信号量,名字为mr_sem,用于信号量计数 */ static DECLARE_MUTEX(mr_sem); /* 试图获取信号量...., 信号未获取成功时,进入睡眠 * 此时,线程状态为 TASK_INTERRUPTIBLE */ down_interruptible(&mr_sem); /* 这里也可以用: * down(&mr_sem); * 这个方法把线程状态置为 TASK_UNINTERRUPTIBLE 后睡眠 */ /* 临界区 ... */ /* 释放给定的信号量 */ up(&mr_sem);
内存屏障
编译器和处理器为了提升效率,可能对读和写进行排序,即“乱序”。我们需要一些手段来干预编译器和CPU, 使其限制指令顺序。内存屏障就是这样的干预手段. 他能保证处于内存屏障两边的内存操作满足有序执行。比如下面的代码:
a = 1;
b = 2;
编译器和处理器看不出a和b之间的依赖关系,有可能“优化”为b在a之前被赋值。
如果是
a = 1;
b= a;
这种情况下,因为a和b有依赖关系,所以编译器和处理器会顺序执行
内存屏障主要有:读屏障、写屏障、通用屏障等。
#define mb() __asm__ __volatile__("mb": : :"memory") #define rmb() __asm__ __volatile__("mb": : :"memory") #define wmb() __asm__ __volatile__("wmb": : :"memory")
以读屏障为例,它用于保证读操作有序。屏障之前的读操作一定会先于屏障之后的读操作完成,写操作不受影响,同属于屏障的某一侧的读操作也不受影响。类似的,写屏障用于限制写操作。而通用屏障则对读写操作都有作用。而优化屏障则用于限制编译器的指令重排,不区分读写。前三种屏障都隐含了优化屏障的功能。比如:
tmp = ttt; *addr = 5; mb(); val = *data;
有了内存屏障就了确保先设置地址端口,再读数据端口。而至于设置地址端口与tmp的赋值孰先孰后,屏障则不做干预。
有了内存屏障,就可以在隐式因果关系的场景中,保证因果关系逻辑正确。
相关文章推荐
- C#信号量用法简单示例
- linux多线程编程详解教程(线程通过信号量实现通信代码)
- PHP信号量基本用法实例详解
- C#多线程编程中的锁系统(四):自旋锁
- JAVA 多线程之信号量(Semaphore)实例详解
- Java锁之自旋锁详解
- 单核,多核CPU的原子操作
- redis并发环境下的使用
- [转载]Pthreads mutex VS Pthreads spinlock
- Linux设备驱动并发控制详解(自旋锁,信号量)
- 内核同步之(读-拷贝-更新)RCU
- 信号量
- 信号量、互斥锁,读写锁和条件变量的区别
- 互斥锁与条件变量的配合!
- 信号量 互斥锁 条件变量的区别
- 关于哲学家就餐问题 源代码(linux)
- 多个生产者与多个消费者的问题
- Posix信号量
- 使用信号量和关键区来实现生产者消费者
- Python 信号量