并发访问
自旋锁
自旋锁在同一时刻只能被一个内核代码路径持有。如果另外一个内核代码路径试图获取一个已经持有的自旋锁,那么该内核代码路径需要一直忙等待,直到自旋锁持有者释放该锁。自旋锁的特性:
- 自旋锁是忙等待的锁,锁机制分为两类:一类是忙等待,一类是睡眠等待。忙等待的锁在无法获取自旋锁的时候会不断尝试,直到获取锁为止。
- 同一时刻只能有一个内核代码路径可以获得该锁。
- 自旋锁临界区中不能睡眠,所以要求在临界区中的执行时间不能太长。
- 自旋锁可以在中断上下文中使用。
自旋锁的临界区中不能允许抢占
- 临界区中发生中断后,中断返回时会检查抢占调度,抢占调度会导致持有锁的进程睡眠,这违背了自旋锁不能睡眠和快速执行完成的设计。
- 抢占调度进程也可能会申请自旋锁,这样会导致发生死锁。
使用自旋锁的重要原则就是拥有自旋锁的代码必须原子地执行,不能休眠和主动调度。
atomic_t
具体定义:
<include/linux/types.h>
typedef struct {
int counter;
} atomic_t;
atomic_t类型的原子操作函数可以保证一个操作的原子性和完整性。原子操作就像是一条汇编语句,保证了操作时不会被打断。
函数
linux内核提供了基本的原子操作函数包括atomic_read()
函数和atomic_write()
函数。定义在<include/asm-generic/atomic.h>
文件中。
#define ATMOIC_INIT(i)
:原子变量初始化为i;#define atomic_read(v)
:读取原子变量的值。#define atomic_write(v, i)
:设置变量v的值为i。
以上两个函数是直接调用READ_ONCE()
宏或者WRITE_ONCE()
宏来实现,不包括“读-修改-回写“机制,直接使用上述函数容易导致并发访问。
atomic_inc(v)
:原子地给v加1。atomic_dec(v)
:原子地给v减1。atomic_add(i, v)
:原子地给v加i。atomic_and(i, v)
:原子地给v和i做“与”操作。atomic_or(i, v)
:原子地给v和i做“或”操作。atomic_xor(i, v)
:原子地给v和i做“异或”操作。
上述函数是不带返回值的操作函数,会实现“读-修改-回写”机制,可以避免多处理器并发访问同一个原子变量带来的并发问题。这些函数会调用指令cmpxchg来实现。宏实现如下所示:
<include/sam-generic/atomic.h>
#define ATOMIC_OP(op, c_op) \
static inline void atomic_##op(int i, atomic_t *v) \
{ \
int c, old;
c = v->counter; \
while((old = cmpxchg(&v->counter, c, c c_op i)) != c) \
c = old; \
}
linux中提供了两类来返回值的原子操作函数,一类返回原子变量的新值,一类返回原子变量的旧值。
返回新值的函数有:
atomic_add_return(int i, atomic_t *v)
:原子地给v加上i,并且返回v的新值。atomic_sub_return(int i, atomic_t *v)
:原子地给v减i,并且返回v的新值。atomic_inc_return(v)
:原子地给v加1,并且返回v的新值。atomic_dec_return(v)
:原子地给v减1,并且返回v的新值。
返回旧值的函数有:
atomic_fetch_add(int i, atomic_t *v)
:原子地给v加i,并且返回v的旧值。atomic_fetch_sub(int i, atomic_t *v)
:原子地给v减i,并且返回v的旧值。atomic_fetch_and(int i, atomic_t *v)
:原子地给v和i做与操作,并且返回v的旧值。atomic_fetch_or(int i, atomic_t *v)
:原子地给v和i做或操作,并且返回v的旧值。atomic_fetch_xor(int i, atomic_t *v)
:原子的给v和i做异或操作,并且返回v的旧值。
原子交换函数:
atomic_cmpxchg(ptr, old, new)
:原子的比较ptr的值是否和old相等,若相等就把new的值设置到ptr地址中,返回old的值;如果不相等则返回ptr指向的内容。atomic_xchg(ptr, new)
:原子的将new的值设置到ptr地址中并返回ptr的原值。atomic_try_cmpxchg(ptr, old, new)
:与atomic_cmpxchg()函数类似,但是返回值发生变化,返回一个bool值,以判断cmpxchg()函数的返回值是否与old相等。
处理引用计数的原子操作函数:
atomic_add_unless(atomic_t *v, int a, int u)
:比较v的值是否等于u。atomic_inc_not_zero(v)
:比较v的值是否等于0.atomic_inc_and_test(v)
:原子的给v加1,然后判断v的新值是否等于0。atomic_dec_and_test(v)
:原子地给v减1,然后判断v的新值是否等于0。
自旋锁函数:
spin_lock(spinlock_t *lock)
:设置自旋锁。spin_lock_irq(spinlock_t *lock)
:会关闭本地中断。spin_lock_irqsave()
spin_irq_save()
local_irq_restore()
spin_lock_bh()
atomic_add
atomic_add()函数通过**cmpxchg()**函数来实现“读写-修改-会写”机制,保证原子变量的完整性。linux内核根据是否支持大系统扩展(LSE)有两种实现方式,一种是使用ldxr和stxr指令的组合,另一种是使用原子加法指令stadd。
ldxr和stxr的指令实现方式:
<arch/arm64/include/asm/atomic_ll_sc.h>
void atomic_op(int i, atomic_t *v)
{
unsigned long tmp;
int result;
asm volatile(
" prfm pstllstrm, %2\n"
"1: ldxr %w0, %2\n"
" add %w0, %w0, %w3\n"
" stxr %w1, %w0, %2\n"
" cbnz %w1, 1b"
: "=&r" (result), "=&r" (tmp), "+Q" (v->counter)
: "Ir" (i)
);
}
LSE的指令实现方式:
<arch/arm64/include/asm/atomic_lse.h>
#define ATOMIC_OP(op, asm_op)
static inline void atomic_##op(int i, atomic_t *v)
{
register int w0 asm ("w0") = i;
register atomic_t *x1 asm("x1") = v;
asm volatile(ARM64_LSE_ATOMIC_INSN(__LL_SC_ATOMIC(op),
" " #asm_op " %w[i], &[v]\n")
: [i] "+r" (w0), [v] "+Q" (v->counter)
: "r" (x1)
: __LL__SC_CLOBBERS);
}
ATOMIC_OP(add, stadd)
信号量
信号量(semaphore)是操作系统中最常用的同步原语之一,自旋锁死一种实现忙等待的锁,信号量这是允许进程进入睡眠状态。
<include/linux/semaphore.h>
struct semaphore{
raw_spinlock_t lock;
unsigned int count;
struct list_head wait_list;
}
lock
:是自旋锁,用于保护count和wait_list成员。count
:用于表示允许进入临界区的内核执行路径的个数。wait_list
:用于管理所有在该信号量上睡眠的进程,没有成功获取锁的进程会在这个链表上睡眠。
通常使用sema_init()函数对信号量进行初始化,sema_init(struct semaphore *sem, int val)
,使用down和up对信号量进行增减。信号量允许任意数量的锁持有者,当count大于1的时候,信号量称为计数信号量,当count等于1的时候,信号量称为互斥信号量或二进制信号量。在linux内核中,大多使用count为1的信号量,相比于自旋锁,信号量是一个允许睡眠的锁,信号量适用于一些情况复杂、加锁时间长的应用场景。
互斥锁
互斥锁(mutex)的定义为。
<include/linux/mutex.h>
struct mutex {
atomic_long_t owner;
spinjlock_t wait_lock;
#ifdef CONFIG_MUTEX_SPIN_ON_OWNER
struct optimistic_spin_queue osq;
#endif
struct list_head wait_list;
}
wait_lock
:自旋锁,用于保护wait_list随便等待队列。wait_list
:用于管理所有在互斥锁上睡眠的进程,没有成功获取锁的进程会在此链表上睡眠。owner
:0表示锁没有被持有,非零值表示锁持有者的task_struct指针的值。