zoukankan      html  css  js  c++  java
  • 第九章:内核同步介绍

    程序员需要留意保护共享资源,防止共享资源禀赋访问,如果多个执行线程同时访问和操作数据,有可能发生各现场之间相互覆盖共享数据的情况,造成被访问数据处于不一致的状态。
    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争用和扩展性
    锁的争用:指当锁正在被占用时,其他线程试图获得该锁。
    一个锁处于高争用状态:表示多个其他线程在等待获得该锁。
    锁的作用:可以使程序按照串行的方式对资源进行访问,所以使用锁会降低系统的性能。
    高度争用的锁会成为系统的瓶颈,严重会降低系统性能。
     
    扩展性:是对系统可扩展性程度的一个度量;
     
    加锁的粗细粒度会影响系统的性能,例如对整个链表加锁和对链表中每个节点加锁,这两加锁方式,前一种不如后一种性能高,并且可以降低竞争的发生,提供操作性能。
     
    当加锁严重时,会降低可扩张性,而锁争用不明显时,加锁过细会加大系统开销,带来浪费。
     
    注意:在设计初期加锁方案应该力求简单,仅当需要时再进一步细化加锁方案,精髓在于力求简单。
    收藏文章数量从多到少与“把书读薄”是一个道理
  • 相关阅读:
    在给定的区间上对每个数都开方 最后还是在一段上求和
    简单的覆盖问题,,通过覆盖的g不同 有这不同的价值 最后还是一段上求和
    codevs 3094 寻找sb4
    noi 04:网线主管
    codevs 1031 质数环
    codevs 1061 重复子串
    codevs 1204 寻找子串位置
    codevs 3223 素数密度
    各种用法
    codevs1073 家族
  • 原文地址:https://www.cnblogs.com/use-D/p/10556094.html
Copyright © 2011-2022 走看看