码迷,mamicode.com
首页 > 其他好文 > 详细

第九章:内核同步介绍

时间:2019-03-19 01:40:15      阅读:212      评论:0      收藏:0      [点我收藏+]

标签:共享   复杂   原子操作   防止   帮助   标记   div   寄存器   将不   

程序员需要留意保护共享资源,防止共享资源禀赋访问,如果多个执行线程同时访问和操作数据,有可能发生各现场之间相互覆盖共享数据的情况,造成被访问数据处于不一致的状态。
Linux内核是抢占式内核,意味着调度程序可以在任何时刻抢占正在运行的内核代码,重新调度其他的进程执行。
 
9.1 临界区竞争条件
所谓临界区就是访问和操作共享数据的代码段。
由于多线程操作共享资源的不安全,为避免在临界区中并发访问,编程者必须保证这些代码时原子性的,就是说操作在结束前不可被打断。
所谓竞争条件:如果两个线程有可能处于同一个临界区中同时执行,那么这就是程序包含的一个bug。如果发生了这种情况,我们称之为竞争条件。
避免并发和防止竞争条件称为同步(synchronization)
 
9.1.1 为什么我们需要保护
以银行取钱为例:
A、B同时读取卡里的金额都是100,A取了90元的同时,B取了10元,可能会发生最后余额显示剩余90元的情况。
为了保证类似的情况不发生,需要在某些操作中增加锁,确保每个事务相对于其他任何事务的操作都是原子性的。
 
9.1.2单个变量
考虑一个非常简单的共享资源:一个全局整型变量和一个简单的临界区,其中操作仅仅是将整型的值加1:
操作过程如下:
得到当前变量i的值并拷贝到一个寄存器中
将寄存器中的值加1
把i的新值写回到内存中。
如果现在两个线程同时执行并进入这个临界区,如果i的初始值为7,那么所期望的值应该为9,例如下面:
A线程获得i(7),增加i(7->8),写回i(8), 然后B线程获得i(8),增加i(8->9),写回i(9)
但是实际情况可能是如下:
A、B线程获得i(7),A增加i(7->8),同时B增加i(7->8),最后A、B两个线程写回i都是8.
以上是一个最简单的临界区的例子。
对于以上这个例子,我们需要将这些指令变为不可分割的整体来执行就可以了。多数处理器提供了指令来原子地读取变量、增加变量,然后再写会变量。
 
9.2加锁
当处理的是一个队列的所有请求时,我们可以假定该队列是以链表方式实现的,所有链表中的每个节点都代表一个请求,两个函数一个是向队列中增加数据,一个是从队列中获得数据,如果存在多个线程同时读取或者添加该链表结构,就可能发生数据不一致的问题。
针对以上情况,需要一个方法确保一次有且只有一个线程对数据结构进行操作,或者当另一个线程在对临界区标记时,就禁止其他访问。
前面讲的请求队列,可以使用一个单独的锁进行保护,当一个新请求要加入队列时,线程会首先锁住队列,然后就可以安全的将请求加入到队列中,结束操作后再释放该锁。
一个时刻只能有一个线程持有锁,所以在一个时刻只有一个线程可以操作队列。
如果一个线程正在操作队列,另一个线程出现了,那么第二个线程必须等待第一个线程释放锁,他才可以继续进行。
锁有多种多样的形式,而且枷锁的粒度范围也不同,LInux自身实现了集中不同的锁机制。
锁是采用原子操作实现的,而原子操作不存在竞争。
 
9.2.1造成并发执行的原因
用户空间之所以需要同步,是因为用户程序会被调度程序抢占和重新调度。
由于用户进程可以在任何时刻被抢占,而调度程序完全可能选择另一个优先级更高的进程到处理器上执行,所以就会使得一个程序正处于临界区,被非自愿的抢占了,如果新调度的进程随后进入同一临界区,前后两个进程互相之间就会产生竞争,
内核中类似可能造成并发执行的原因,它们是:
中断:中断几乎可以在任何时刻异步发生。
软中断和tasklet:内核能在任何时刻唤醒或者调度软中断和tasklet。
内核抢占:因为内核具有抢占行,所以内核中的任务可能会被另一任务抢占。
睡眠及与用户空的同步:在内核执行的进程可能会睡眠,这就会唤醒调度程序,从而导致调度一个新的用户进程执行。
 
9.2.2了解要保护些什么
找出哪些数据需要保护是关键所在。
到底什么数据需要加锁那?大多数内核数据结构都需要加锁!,有一条很好的经验可以帮助我们判断:如果有其他执行线程可以访问这些数据,那么久给这些数据加上某种形式的锁:
记住:要给数据而不是代码加锁。
 
9.3死锁
死锁产生的条件:要一个或者多个执行线程和一个或者多个资源,每个线程都在登台其中一个资源,但是所有的资源都已经被锁占用,所有的线程都互相等待其他线程的已经被锁定的资源,但他们永远不会释放已占用的资源。
最简单的死锁是自死锁:如果一个执行线程试图获得一个自己已经持有的锁,它将不得不等待锁释放,但因为它正在忙着等待这个锁,所以自己永远不会有机会释放锁,最终的结果如下:
获得锁;
再次试图获得锁;
等待锁重新可用;
 
预防死锁的一些简单规则:
按顺序加锁:使用嵌套锁时必须按顺序获得锁,这样可以阻止致命拥抱类型的死锁。
防止发生饥饿:不要一直等待获得一个锁;
不要重复请求同一个锁;
设计应力求简单:越复杂的加锁方案越有可能造成死锁;
 
9.4争用和扩展性
锁的争用:指当锁正在被占用时,其他线程试图获得该锁。
一个锁处于高争用状态:表示多个其他线程在等待获得该锁。
锁的作用:可以使程序按照串行的方式对资源进行访问,所以使用锁会降低系统的性能。
高度争用的锁会成为系统的瓶颈,严重会降低系统性能。
 
扩展性:是对系统可扩展性程度的一个度量;
 
加锁的粗细粒度会影响系统的性能,例如对整个链表加锁和对链表中每个节点加锁,这两加锁方式,前一种不如后一种性能高,并且可以降低竞争的发生,提供操作性能。
 
当加锁严重时,会降低可扩张性,而锁争用不明显时,加锁过细会加大系统开销,带来浪费。
 
注意:在设计初期加锁方案应该力求简单,仅当需要时再进一步细化加锁方案,精髓在于力求简单。

第九章:内核同步介绍

标签:共享   复杂   原子操作   防止   帮助   标记   div   寄存器   将不   

原文地址:https://www.cnblogs.com/use-D/p/10556094.html

(0)
(0)
   
举报
评论 一句话评论(0
登录后才能评论!
© 2014 mamicode.com 版权所有  联系我们:gaon5@hotmail.com
迷上了代码!