您的位置:首页 > 运维架构 > Linux

Linux内核同步机制

2016-08-23 21:38 375 查看
Linux内核同步机制,常用的有自旋锁,信号量,互斥体,原子操作,顺序锁,RCU,内存屏障等。

本篇主要介绍原子操作,自旋锁,信号量和内存屏障。

原子操作

原子操作可以保证指令以原子的方式进行执行,执行过程不被打断。

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)。阻止软中断的底半部的执行。
在Linux内核中何时使用spin_lock,何时使用spin_lock_irqsave很容易混淆,下面详细解释各个API。

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的赋值孰先孰后,屏障则不做干预。

有了内存屏障,就可以在隐式因果关系的场景中,保证因果关系逻辑正确。
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息