内核同步介绍

(一):临界区和竞争条件

所谓临界区(也称为临界段)就是访问和操作共享数据的代码段.多个执行线程并发访问同一个资源通常是不安全的,为了避免在临界区中并发访问,编程者必须保证这些代码原子的执行--也就是说,操作在执行结束前不能被打断,就如同整个临界区是一个不可分割的指令一样.如果两个执行线程有可能处于同一个临界区中同时执行,那么这就是程序包含的一个bug.如果这种情况确实发生了,我们就成他是竞争条件,这样命名是因为这里会存在线程竞争.避免并发和防止竞争条件称为同步.

1:单个变量

现在,首先看一个特殊计算的例子.考虑一个非常简单的共享资源:一个全局整型变量和一个简单的临界区.其中的操作仅仅是将整型变量的值增加1.

i++;

该操作可以转换成类似于下面动作的机器指令序列:

得到当前变量i 的值并且拷贝到一个寄存器中
将寄存器中的值加1
把i的新值写会到内存中

现在假定有两个执行线程同时进入临界区,如果i的初始值是7,那么我们所期望的结果应该像下面这样(每一行代表一个时间单元):

线程1        线程2
获取i(7)         --------
增加i(7->8)    --------
写回i(8)         --------
---------          获得i(8)
---------          增加i(8->9)
---------          写回i(9)

这是我们所期望的,但是实际上的运行情况可能是这样的:

线程1        线程2
获得i(7)        获得i(7)
增加i(7->8)   ---------
-------------     增加i(7->8)
写回i(8)        -----------
------------     写回i(8)

这样就不是我们希望出现的结果了,但是实际情况中这种现象也是可能会发生的.那么这种情况解决起来也是相对比较简单的---我们仅仅需要将这些指令作为一个不可分割的整体来执行就可以了.多数处理器都提供了指令来原子的读变量,增加变量,然后写回变量,使用这样的指令就能解决一些问题.使用这个指令唯一的可能结果为:

线程1        线程2
增加i(7->8)    ---------
--------------    增加i(8->9)

或者是相反

线程1        线程2
--------    增加i(7->8)
增加i(8->9)    ---------

两个原子操作执行根本就不可能发生,因为处理器会从物理上确保这种不可能.

(二):加锁

锁有多种多样的形式,而且加锁的粒度也各不相同---Linux自身实现了几种不同的锁机制.各种锁机制之间的区别主要在于:当锁已经被其他线程持有,因而不可用时的行为表现--一些锁被争用时会简单的执行忙等待,而另外一些锁会使当前任务睡眠知道锁可用为止.

1:造成并发执行的原因

用户空间之所以需要同步,是因为用户程序会被调度程序抢占和重新调度.由于用户进程可能在任何时刻被抢占,而调度程序完全可能选择另一个高优先级的进程到处理器上执行,所以就会使得一个程序正处于临界区的时候,被非自愿地抢占了.如果新调度的进程随后也进入同一个临界区(比如说这两个进程要操作共享的内存,或者向同一个文件描述符写入),前后两个进程相互之间就会产生竞争.另外,因为信号处理是异步发生的,所以,即使是单线程的多个进程共享文件,或者是在同一程序内部处理信号,也有可能产生竞争条件.这种类型的并发操作--这里其实两者并不真是同时发生的,但他们相互交叉进行,所以也被称作伪并发执行.

如果有一台支持对称多处理器的机器,那么两个进程就可以真正的在临界区中同时执行,这种类型称为真并发.

内核中有类似可能造成并发执行的原因.他们是:
中断--中断几乎可以在任何时刻异步发生,也就可能随时打断当前正在执行的代码
软中断和tasklet--内核能在任何时刻唤醒或调度软中断和tasklet,打断当前正在执行的代码
内核抢占--因为内核具有抢占性,所以内核中的任务可能会被另一个任务抢占
睡眠及用户空间的同步--在内核执行的进程可能会睡眠,这就会唤醒调度程序,从而导致调度一个新的用户进程执行
对称多处理器--两个或多个处理器可以同时执行代码

在中断处理程序中能避免并发访问的安全代码称作中断安全代码;在对称多处理器的机器中能避免并发访问的安全代码称为SMP安全代码(SMP-safe).在内核抢占的时候能避免并发访问的安全代码称为抢占安全代码.

2:需要给那些数据加锁

如果有其他线程可以访问这些数据,那么就给这些数据加上锁;如果任何其他什么东西都能看到他,那么就要锁住他.记住:要给数据加锁而不是给代码加锁.

(三)死锁

死锁的产生需要一定条件:要有一个或多个执行线程和一个或多个资源,每个线程都在等待其中的一个资源,但所有的资源都已经被占用了.所有的线程都在相互等待,但他们永远不会释放已经占有的资源.于是任何线程都无法继续,这便意味着死锁的发生.

最简单的死锁的例子是自死锁:如果一个执行线程试图去获得一个自己已经持有的锁,他将不得不等待锁被释放,但他正在忙着等待这个锁,所以自己永远不会有机会释放所,最终结果是死锁.

一些简单的规则对避免死锁有非常大的帮助:

1:按顺序加锁,尽量按相反的顺序释放锁.使用嵌套的锁时,必须保证以相同的顺序获取锁,这样可以阻止致命拥抱类型的死锁.
2:防止发生饥饿
3:不要重复请求同一个锁
4:设计力求简单--越复杂的加锁方案有可能造成死锁

(四):争用和扩展性

锁的争用,或简称争用,是指当锁正在被占用的时候,有其他线程试图获得该锁.说一个锁处于高度争用状态,就是指有多个其他线程在等待获得该锁.

扩展性是对系统可扩展程度的一个度量.

你可能感兴趣的:(锁,死锁,linux内核,内核同步)