并发与同步——spin_lock

内核中产生并发访问的并发源主要由以下4中

1、中断和异常:中断程序程序和被中断进程之前可能存在并发访问(spin_lock_irqsave/local_irq_disable,即对于进程中存在和中断并发访问的数据,需要屏蔽中断)

2、软中断和tasklet:软中断优先级高于进程,能够抢占正在执行的进程(local_bh_disable等等)

3、内核抢占:调度器支持可抢占特性,会导致进程之间并发访问

4、多处理器并发执行:每个处理器独立,各自能够同时运行进程

对于SMP对称多处理器系统,同一类型的中断处理程序不会并发(那中断嵌套也不会出现同一类型的中断咯),但是不同类型的中断可能并发执行;同一类型的软中断,在同一cpu上串行,但会在不同的cpu上并发执行;同一类型的tasklet是串行执行,不会在多个cpu上并发

原子性与顺序性:

原子性:一个字长的读取总是原子的发生,不可能对同一个字交错的进行写;读总是返回一个完整的子,这或是发生在写操作之前或者之后,绝不可能发生在写的过程中。这就是原子性

顺序性:要求读必须在待定的写之前发生,这种属于顺序要求。顺序性可以通过屏障(barrier)指令来实施

内存屏障

数据存储屏障DMB(data memory barrier)

数据同步屏障DSB(data synchronization barrier)

指令同步屏障ISB(Instruction synchronization barrier)

并发与同步——spin_lock_第1张图片

自旋锁:最多只能被一个可执行线程持有。如果一个执行线程试图获得一个已被他人持有的自旋锁,那么该线程就会一直循环,等待锁重新可用。

由于获取锁失败会导致线程原地自旋,一直占用CPU。因此自旋锁不适合长时间被持有。

针对这种锁争用,还有一种处理方式是让获取锁失败的线程睡眠,直到锁重新可用,才唤醒该线程。这种方式在获取锁失败时会睡眠,从而让出CPU。这种方式会有让出cpu和重新被换上cpu上次的上下文切换开销。因此自旋锁被持有的时间最好是小于完成两次上下文切换的耗时。

1、自旋锁不允许递归获取,即不能在锁未被释放前,再次去获取锁。这样会导致死锁。

2、自旋锁可以被用到中断处理程序中。但是需要禁止本地中断,不然中断处理程序被新的中断打断以后,其他地方可能会再次获取该锁(如果没有其他地方获取这个锁,是不是就不用禁用中断了啊?毕竟同一个中断是不会重复触发的哇)。spin_unlock_irqrestore().会用flag恢复到加锁之前的状态,这样即使之前是禁止中断的,也不会因为我们释放了自旋锁,开中断。而错误地激活中断,使得加锁前是屏蔽了中断,解锁后将中断打开了。因此如果不清楚加锁之前的中断是否激活,建议直接使用spin_lock_irqsave()。

3、自旋锁不能休眠

自旋锁定义

typedef struct spinlock {
	union {
		struct raw_spinlock rlock;
	};
} spinlock_t;

typedef struct raw_spinlock {
	arch_spinlock_t raw_lock;
} raw_spinlock_t;

typedef struct {
	union {
		u32 slock;
		struct __raw_tickets {
			u16 owner;
			u16 next;
		} tickets;
	};
} arch_spinlock_t;

 最开始自旋锁的slock只是一个无符号类型变量。slock为1表示锁未被持有,0则表示被持有。但是在锁争用比较激烈的情况下,在同一个NUMA节点上的cpu,由于L1 cache中存储了该锁,导致该节点上的cpu比其他节点更先获取到该锁,而导致其他节点的cpu很久都不能获取到,只能忙等。因此在自旋锁实现了一套FIFO ticker-based的自旋机制:

大致思想:owner:表示当前应该持有锁的号码,next表示下一个应该持有锁的号码。当A尝试获取锁时,owner和next为0,锁被A持有,next++变为1;当B尝试获取锁时,因为被A持有,B将获得1号牌,next++,变为2;当A释放锁的时候,owner++变为1。这个时候能抢到锁的人,只能是owner和next相等的人,即B。

linux-3.16

自旋锁加锁

static inline void spin_lock(spinlock_t *lock)
{
	raw_spin_lock(&lock->rlock);
}

void __lockfunc _raw_spin_lock(raw_spinlock_t *lock)
{
	__raw_spin_lock(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);
}

禁止抢占,其实就是修改thread_info中的变量preempt_count

#define preempt_disable() \
do { \
	preempt_count_inc(); \
	barrier(); \
} while (0)

static inline void do_raw_spin_lock(raw_spinlock_t *lock) __acquires(lock)
{
	__acquire(lock);
	arch_spin_lock(&lock->raw_lock);
}
static inline void arch_spin_lock(arch_spinlock_t *lock)
{
	unsigned long tmp;
	u32 newval;
	arch_spinlock_t lockval;

	prefetchw(&lock->slock);
	__asm__ __volatile__(
"1:	ldrex	%0, [%3]\n"			ldrex locaval, [&lock->slock]
"	add	%1, %0, %4\n"			addr newval, 1 << TICKET_SHIFT
"	strex	%2, %1, [%3]\n"		strex tmp, newval, [&lock->slock]
"	teq	%2, #0\n"				teq tmp, #0
"	bne	1b"						bne	1b
	: "=&r" (lockval), "=&r" (newval), "=&r" (tmp)
	: "r" (&lock->slock), "I" (1 << TICKET_SHIFT)
	: "cc");
/* 
首先通过ldrex将lock->slock加载到lockval中,
然后把lockval中的next加1,并保存到newval.
然后newval写入到lock->slock中
*/
	while (lockval.tickets.next != lockval.tickets.owner) {
		wfe();
		lockval.tickets.owner = ACCESS_ONCE(lock->tickets.owner);
	}

	smp_mb();
}

而STREX在更新内存数值时,会检查该段内存是否已经被标记为独占访问,并以此来决定是否更新内存中的值:

STREX Rx, Ry, [Rz]《==》strex tmp, newval, [&lock->slock]

如果执行这条指令的时候发现已经被标记为独占访问了,则将寄存器Ry中的值更新到寄存器Rz指向的内存,并将寄存器Rx设置成0。指令执行成功后,会将独占访问标记位清除。

ARM平台下独占访问指令LDREX和STREX的原理与使用详解_anieoo的博客-CSDN博客

arch_spin_lock:

1、可以看到lockval其实是在改动slock之前的一个副本。只有经过那段汇编代码之后,next才++

因此是在next增加之前的值。因此lockval的next可以看做是当前尝试获取锁的人拿到的号码牌

2、lockval是尝试获取锁的人拿到的号码牌,那么接下来我们就只需要一直查看lock中的owner和next是不是相同(ACCESS_ONCE(lock->tickets.owner)),即可知道是不是自己获得了锁。

while (lockval.tickets.next != lockval.tickets.owner) {
		wfe();
		lockval.tickets.owner = ACCESS_ONCE(lock->tickets.owner);
}

 关键词 volatile,所以它的含义就是强制编译器每次使用 x 都从内存中获取。

#define ACCESS_ONCE(x) (*(volatile typeof(x) *)&(x))

wfe(wait for event)会让cpu进入低功耗模式,但是并不会让出cpu,所以自旋锁仍然还是在原地自旋。wfe是直到有wfe唤醒时间发生才会唤醒cpu.

自旋锁释放

static inline void spin_unlock(spinlock_t *lock)
{
	raw_spin_unlock(&lock->rlock);
}

void __lockfunc _raw_spin_unlock(raw_spinlock_t *lock)
{
	__raw_spin_unlock(lock);
}

static inline void __raw_spin_unlock(raw_spinlock_t *lock)
{
	spin_release(&lock->dep_map, 1, _RET_IP_);
	do_raw_spin_unlock(lock);
	preempt_enable();
}

static inline void do_raw_spin_unlock(raw_spinlock_t *lock) __releases(lock)
{
	arch_spin_unlock(&lock->raw_lock);
	__release(lock);
}

 __raw_spin_unlock:会重新使能抢占

static inline void arch_spin_unlock(arch_spinlock_t *lock)
{
	smp_mb();
	lock->tickets.owner++;
	dsb_sev();
}

static inline void dsb_sev(void)
{

	dsb(ishst);
	__asm__(SEV);
}

 arch_spin_unlock:可以看到释放的时候只是将owner++,然后使用SEV指令唤醒其他cpu

spin_lock变种

1、spin_lock_irq:可以看到相比spin_lock,它多了关闭本地中断的操作。如果同一个数量中断处理程序和进程上下文都会被访问,那就需要屏蔽中断。但是在中断处理程序应该是不能用这个的,需要换为spin_lock_irqsave。因为__raw_spin_unlock_irq会直接将中断打开,而spin_lock_irqrestore是将其恢复到加锁之前的状态。

static inline void spin_lock_irq(spinlock_t *lock)
{
	raw_spin_lock_irq(&lock->rlock);
}
void __lockfunc _raw_spin_lock_irq(raw_spinlock_t *lock)
{
	__raw_spin_lock_irq(lock);
}
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);
}
static inline void __raw_spin_unlock_irq(raw_spinlock_t *lock)
{
	spin_release(&lock->dep_map, 1, _RET_IP_);
	do_raw_spin_unlock(lock);
	local_irq_enable();
	preempt_enable();
}

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;
}
static inline void __raw_spin_unlock_irqrestore(raw_spinlock_t *lock,
					    unsigned long flags)
{
	spin_release(&lock->dep_map, 1, _RET_IP_);
	do_raw_spin_unlock(lock);
	local_irq_restore(flags);
	preempt_enable();
}

 以下内容来自于奔跑吧linux内核:

spin_lock_irq只能关闭本cpu中断,但是其他cpu仍然能够响应中断。假设cpu0使用spin_lock_irq获得了锁,但是cpu1出现中断(其实不需要中断,只需要cpu1获取锁就行),然后中断处理函数还是会去获取该锁,会怎么样呢?因为cpu0上的锁持有者在继续执行,当cpu0释放了锁,cpu1就可以运行了。

假设在上述情形了即cpu0拿着锁,cpu1等待锁释放。在这种情况下cpu0进行了调度怎么办?首先cpu0在获取锁之前使用preempt_disable显示禁用了抢占,因此内核不会主动发生抢占。除非是主动调用schedule或者调用了睡眠函数。如果内核运行在持有spin_lock的情况下进行进程切换,那么cpu1就不知道何时能够获取到锁了。因此spin_lock锁保护的临界区是不能休眠的。

因此 schedule的时候就会检查preempt_count的值,是否运行调度。如果不行则会出现如下打印

BUG: scheduling while atomic......

《linux内核设计与实现》关于内核抢占有这样一种说法,preempt_count计数器初值为0,获取锁+1,释放锁-1。当计数为0说明内核可执行抢占,即调度。反之不为0,说明当前进程或者线程仍持有锁,进行调度是不安全的。

static void __sched __schedule(void)
{
...................
	schedule_debug(prev);
	................
}
static inline void schedule_debug(struct task_struct *prev)
{

	if (unlikely(in_atomic_preempt_off() && prev->state != TASK_DEAD))
		__schedule_bug(prev);
...................................
}
static noinline void __schedule_bug(struct task_struct *prev)
{
	if (oops_in_progress)
		return;

	printk(KERN_ERR "BUG: scheduling while atomic: %s/%d/0x%08x\n",
		prev->comm, prev->pid, preempt_count());

...............................
	dump_stack();
	add_taint(TAINT_WARN, LOCKDEP_STILL_OK);
}

另外的变种还有spin_lock_bh等

参考链接

linux内核中使用自旋锁一定要禁止当前处理器的中断吗? - 知乎

感觉是spin_lock只是禁用抢占,但是没有禁用中断。会出问题,所以才有了spin_lock的变种

自旋锁不可递归:一个线程获取了锁(spin_lock不会禁用中断),此时被中断处理程序打断。该中断处理程序也需要获取该锁。此时就发生死锁。

以下场景来自于上面链接

场景1(内核抢占):

(1)进程A某个系统调用中访问了共享资源R;(2)进程B某个系统调用中访问了共享资源R

A访问共享资源R时发生了中断(已获取了自旋锁)。由于自旋锁是会禁用抢占的(preempt_disable)。因此当从中断返回时,不会被换下CPU(不是很理解在A进程获取spin lock的时候,禁止本CPU上的抢占。我感觉禁止抢占的标记不是在thread_info里面吗?应该是和进程相关的,怎么和本cpu扯上关系了。现在来解释一下这问题。在中断处理程序中使用的sp_svc其实就是被中断进程的sp_svc,因此在中断程序中使用current获得的是被中断进程的task。thread_info就是被放到了内核栈sp_svc的最下面。如果一个进程禁止了抢占,被中断打断,在中断退出时,自然能够通过preempt_count知道是否允许抢占调度)

如果进程A和进程B不是在同一个CPU上。两个进程想访问共享资源R.后面一个进程只能等到另外一个进程释放之后,才能进行临界区(不在同一个cpu的两个进程访问同一个临界区,不会死锁)。

场景2(中断上下文):

        一个线程获取了锁(spin_lock不会禁用中断),此时被中断处理程序打断。该中断处理程序也需要获取该锁。如果中断处理程序,也运行在同一个CPU上,此时就会发生死锁。如果中断处理程序和线程运行在不同的cpu上。会等到线程释放spin_lock后才能执行。

因此如果涉及到中断上下文的访问,spin lock需要和禁止本 CPU 上的中断联合使用spin_lock_irq

static inline void __raw_spin_lock_irq(raw_spinlock_t *lock)
{
    local_irq_disable();//关闭本地CPU中断
    preempt_disable();//禁用内核抢占
    spin_acquire(&lock->dep_map, 0, 0, _RET_IP_);
    LOCK_CONTENDED(lock, do_raw_spin_trylock, do_raw_spin_lock);
}

这样保证了获取了自旋锁以后,本cpu上不会发生中断

场景3(中断下半部):

        如果上面的中断处理程序,是在中断下半部访问共享资源。那么spin_lock+禁用中断下半部即可。spin_lock_bh

你可能感兴趣的:(linux)