自旋锁、信号量以及互斥锁

并发访问

自旋锁

自旋锁在同一时刻只能被一个内核代码路径持有。如果另外一个内核代码路径试图获取一个已经持有的自旋锁,那么该内核代码路径需要一直忙等待,直到自旋锁持有者释放该锁。自旋锁的特性:

  1. 自旋锁是忙等待的锁,锁机制分为两类:一类是忙等待一类是睡眠等待。忙等待的锁在无法获取自旋锁的时候会不断尝试,直到获取锁为止。
  2. 同一时刻只能有一个内核代码路径可以获得该锁。
  3. 自旋锁临界区中不能睡眠,所以要求在临界区中的执行时间不能太长。
  4. 自旋锁可以在中断上下文中使用

自旋锁的临界区中不能允许抢占

  1. 临界区中发生中断后,中断返回时会检查抢占调度,抢占调度会导致持有锁的进程睡眠,这违背了自旋锁不能睡眠和快速执行完成的设计。
  2. 抢占调度进程也可能会申请自旋锁,这样会导致发生死锁。

使用自旋锁的重要原则就是拥有自旋锁的代码必须原子地执行,不能休眠和主动调度。

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指针的值。

猜你喜欢

转载自blog.csdn.net/qq_41323475/article/details/127856457
今日推荐