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

Linux内核同步:同步规则和说明

2017-02-05 15:22 423 查看

什么是同步

像下面的代码段里边一样,有一个栈。一个系统调用的函数从栈里边读,一个中断函数保存数据到栈里边。

这时候,这个栈的数据就是一个需要保护的数据。



在多进程系统中,有以下几种情况下会出现race condition。(这里只讨论单核的情况)

1.发生中断: task1在进入临界区准备读栈的内容的时候发生中断。中断函数放数据到栈。task1在中断返回之后所读的内容,就不是原来预计的内容了。



2.内核抢占:task1进入临界区准备读栈数据的时候,被task2抢占。task2进入临界区读栈。重新调度会task1的时候,栈里的内容已经不是原先想要读的内容了。



3.进程睡眠:task1进入临界区准备读栈数据。但是在kmalloc中进入睡眠。调度器调度到task2。task2进入临界区读栈。等task1被唤醒,重新试图去读栈数据的时候,已经不是原来的数据了。

所以所谓同步(Synchronization)就是防止在上述情况下出现race condition的方法。

Linux内核提供一些同步方法。

For single core

Disabling preemption (or interrupts)

Prevent other tasks (or ISRs) from running

For multi-core

Atomic operations

Perform multiple actions at once

Locking primitives

Prevent other tasks from entering a critical section

对于单核的系统,同步只需要在进入临界区域之前防止中断和防止进程抢占,或者防止自身由于不当的操作而进入睡眠即可(比如kmalloc传入GPF_KERNEL时,会在申请不到内存时进入睡眠)。

对于多核系统,禁止中断或者禁止抢占,都不能有效防止race conditions(这个会在下面仔细说明)。在多核系统中,需要1.atomic operations 或者2. spinlock,mutex,semaphore等同步的方法,才能防止race conditions。

禁止抢占方式对于单核系统是有效的同步方法,但在多核系统中是无效的。





禁止抢占函数接口:



在单核系统中,如果task和isr都可以访问要保护的数据,那只禁止抢占是没有用的,比如下面的情况。



所以在单核系统中,要保护可以被task和isr同时访问的数据,需要再禁止中断。



但禁止中断,同样不能同步多核系统中的数据。而且Linux2.4以后,没有一个像cli()这样的函数可以直接把所有CPU上面的中断都给禁止掉。现在有以下几个接口可以用来禁止当前CPU的进程。

local_disable() //中断禁止
...
local_enable() //中断使能


unsigned long flags;
local_irq_save(flags);
...
local_irq_restore(flags);


内核和用户控件的同步方法如下:





怎么同步

首先要明确同步所保护的对象是全局数据(global data)。

还要确定要保护的全局数据是怎么被访问的。如下面的几个顺序。

1. 要保护的数据是全局数据否

2. 数据是否是task和isr上下问都在访问的

3. task在进入临界区之后,是否可以进入睡眠?

4. 一个CPU的task或者isr进入临界区中之后,其他CPU也通过task或者isr访问这个临界区会怎么样?

多核系统同步,atomic接口:











spinlock

Spin lock

Spin lock is a mutual exclusion mechanism where a process

spins (or busy-waits) until the lock becomes available

Spin lock is meaningful in SMP

so that a task running in another CPU can release the lock

Spin lock is useful when the length of critical section is short

so that spinning time < rescheduling overhead

spin_lock, spin_lock_irq, spin_lock_irqsave 这几种宏的说明

在multi thread程序中,为了给众多thread进行同步(synchronization)才有了lock算法。

mutex是也是一种同步机制,但这种机制在试图或者lock,但lock被其他进程抓着的时候会让当前进程进入睡眠。所以如果critical section包着的代码很少,这种锁的效率就不是很高。因为有时候在准备让当前进程进入睡眠的过程中,锁可能就已经被放开了。

所以spinlock就诞生了。

1.spinlock与mutex不同,在获取不到lock的时候不是进入让当前进程进入睡眠,而是跑一个loop,让当前进程busy-waiting。所以如果critical section比较少的时候用spinlock会比较高效。



2.spinlock可以解决所有问题吗? 不是的。当中断上下文也可以访问要保护的数据的时候,只用spin_lock函数是有问题的。因为一个进程抓着spinlock的时候发生了中断,然后中断处理函数再准备抓当前的spinlock的时候,会因为之前的进程抓着spinlock而等不到spinlock释放,就会发生daedlock。

为了解决这样的场景,登场的是spin_lock_irq函数。这个函数在获取spinlock的同时,会把中断disable掉。使用spin_lock_irq函数,在执行critical section的时候就不会发生中断,也就避免了上面说的问题。

3.但是如果中断嵌套发生的情况,就不能使用spin_lock_irq函数。因为spin_lock_irq函数无论disable了中断多少次,只要有一次enable中断就会被重新打开。这种场景下使用的函数就是spin_lock_irqsave函数。spin_lock_irqsave函数会在disable中断的同时保存中断enable状态还是disable次数。这个函数在中断会嵌套发生的情况下使用会比较方便,只不过因为要保存当前的enable/disable状态,所以占用多一点的内存。



spin_lock (spinlock)

最基本的spinlock,如果不需要使用其他的spinlock接口就用这个。

spin_lock_irq (spin lock irq disable)

这个函数会把所有的中断都会给disable掉,所以对interactivity等系统的性能有影响。

这个函数一般用在interrupt handler和一般thread共享一个spinlock的时候使用。

但要注意这个函数在spin_unlock_irq的时候,不管disable过多少次中断,都会把当前的中断打开。

spin_lock_irqsave (spin lock irq save)

spin_lock_irq一样,但会保存enable,disable的次数。.

如果在spinlock的时候,无法判断当前的中断的打开或者关闭状态的时候使用。

spin lock在单核系统中也是有的,不过和多核系统不同,在没有定义CONFIG_SMP的时候。

spin_lock() = preempt_disable()
spin_lock_irqsave() ==local_irqsave()


在多核系统中,也就是CONFIG_SMP定义了的话

spin_lock() ==preempt_disable() + arch_spin_lock()
spin_lock_irqsave() ==local_irqsave() + arch_spin_lock()


在使用spinlock的时候,必须要注意死锁!!因为一不小心就会在使用spin lock接口的时候出现死锁。

1.ABBA deadlock示例



像上面这样,有两个tam,mic锁。在task1获取tam锁,到get_free_tam()函数的时候进入睡眠。然后调度到task2在获取mic锁,到get_free_mic的时候进入睡眠。重新调度到task1的时候,准备获取mic锁,但这个锁被task2获取了,所以task1一直loop等待。然后当task2醒来之后准备获取tam锁的时候,发现这个被task1获取,所以task2也一直在那里loop等待task1释放tam。这时候task1和task2,互相等待对方的释放锁,就出现了所谓ABBA的deadlock。

这里可以看到出现这种死锁的根本原因也是在持有某个锁的时候进入睡眠!! 所以一般书上说持有spinlock锁不能进入睡眠也是因为这个。

2. Deadly embrace(indefinite postponement)



这个很简单就是获取了一个锁,然后跑进去再去试图获取同样的锁,,当然就不行了~

在自旋锁,信号量及中断禁止之间选择

访问数据结构的内核控制路径单处理器保护多处理器进一步保护
异常信号量无 (????)
中断本地中断禁止自旋锁
可延迟函数无或者自旋锁 (看下面可延迟函数保护)
异常与中断本地中断禁止自旋锁
异常与可延迟函数本地软中断禁止自旋锁
中断与可延迟函数本地中断禁止自旋锁
异常,中断与可延迟函数本地中断禁止自旋锁

保护异常所访问的数据结构

当一个数据结构仅由异常处理程序访问时,竞争条件通常是易于理解也易于避免的。

这里注意,上面的意思是数据结构仅由异常访问的情况,上面的表也是。在读取修改其他进程也可以访问到的数据结构的时候还是需要用到自选锁等。

最常见的产生同步问题的异常就是系统调用服务例程,在这种情况下,CPU运行在内核态而为用户态程序提供服务。因此,仅由异常访问的数据结构通常表示一种资源,可以分配给一个或多个进程。

竞争条件可以通过信号量避免,因为信号量原语允许进程睡眠到资源变为可用。注意,信号量工作方式在单处理器系统和多处理器系统上完全不同。

内核抢占不会引起太大问题。如果一个拥有信号量的进程是可以被抢占的,运行在同一个CPU上新进程就可能试图获得这个信号量。在这种情况下,让新进程处于睡眠状态,而且原来拥有信号量的进程最终会释放信号量。只有在访问每CPU变量的情况下,必须显式地禁用内核抢占。(???)

保护可延迟函数所访问的数据结构

只被可延迟函数访问的数据结构需要哪种保护呢?这主要取决于可延迟函数的种类。

比如tasklet和软中断本质上有不同的并发度,所以保护方式也有不同。

首先,在单处理器系统上不存在竞争条件。这时因为可延迟函数的执行总是在一个CPU上穿行执行—也就是说,一个可延迟函数不会被另一个可延迟函数中断。因此,根本不需要同步原语。

相反,在多处理器系统上,竞争条件的确存在,因为几个可延迟函数可以并发运行。

以下是在多处理器系统(SMP)中,可延迟函数访问的数据结构所需的保护

访问数据结构的可延迟函数保护
软中断自旋锁
一个tasklet
多个tasklet自旋锁
由软中断访问的数据结构必须受到保护,通常使用自旋锁进行保护,因为同一个软中断可以在两个或多个CPU上并发运行。相反,仅由一个tasklet访问的数据结构不需要保护,因为同种tasklet不能并发运行。但是,如果数据结构被几种tasklet访问,那么,就必须对数据结构进行保护。

#The first of these rules is: never sleep when you are running in an atomic context

What that means, with regard to sleeping, is that your driver cannot sleepwhile holding aspinlock, seqlock, or RCU lock. You also cannot sleep if you have

disabled interrupts. It is legal to sleep while holding a semaphore, but you should

look very carefully at any code that does so.

If code sleeps while holding a sema-phore, any other thread waiting for that semaphore also sleeps. So any sleeps that

happen while holding semaphores should be short,and yous hould convince your-self that,by holding these maphore,youarenotblockingtheprocessthatwilleven-tually wake you up.
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: