内核同步

来源:互联网 发布:在线询问医生软件 编辑:程序博客网 时间:2024/06/05 09:15

内核同步

在使用共享内存的应用程序中,程序员必须特别留意保护共享资源,防l}共享资源并发访问。内核也不例外。共享资源之所以要防止并发访问,是因为如果多个执行线程同时访问和操作数据,就有可能发生各线程之间相互覆盖共掌数据的情况,造成被访问数据处手不一致状态。

临界区和竟争条件

所谓临界区(也称为临界段)就是访问和操作共享数据的代码段。

多个执行线程并发访问同一个资源通常是不安全的,为了避免在临界区中井发访问,编程者必须保证这些代码原子地执行,操作在执行结束前不可被打断,就如同整个临界区是一个不可分割的指令一样。

如果两个执行线程有可能处于同一个临界区中同时执行,那么这就是程序包含的一个bug,如果这种情况确实发生了,我们就称它是竞争条件.(race condition).

竞争的例子不再叙述。比如在银行存款系统中就存在取数据,增加,两个子过程。如果存在两个或以上的人去访问同一个账号时,可能乱序,这就存在竞争。

而针对账号数值这一单一变量,内核提供了对单一变量的原子操作。

加锁

一种更加常见的情形是:我们把数据放在一个队列,使用链表实现。而有两个进程在这个链表队列中写数据,任何的修改行为都涉及了多条指令,在这个时候如果另一个进程访问就会导致乱序。最终链表队列被破坏。

针对以上大的数据结构,不能使用原子操作,我们采取锁的方式解决。

锁提供的就是这种机制:它就如同一把门锁,门后的房间可想象成一个临界区。在一个指定时间内,房间里只能有一个执行线程存在,当一个线程进入房间后,它会锁住身后的房门;当它结束对共享数据的操作后,就会走出房间,打开门锁。如果另一个线程在房门上锁时来子,那么它就必须等待房间内的线程出来井打开门锁后,才能进入房间。这样,线程持有锁,而锁保护了数据。

造成并发执行的原因

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

如果你有一台支持对称多处理器的机器,那么两个进程就可以真正地在临界区中同时执行了,这种类型称为真并发。虽然真并发和伪并发的原因和含义不同,但它们都同样会造成竟争条件,而且也需要同样的保护。

内核中有类似可能造成井发执行的原因。它们是:

  • 中断—中断几乎可以在任何时刻异步发生,也就可能随时打断当前正在执行的代码。

  • “软中断和tasklet—内核能在任何时刻唤醒或调度软中断和tasklet,打断当前正在执行的代码.

  • 内核抢占—因为内核具有抢占性,所以内核中的任务可能会被另一任务抢占。

  • 睡眠及与用户空间的同步—在内核执行的进程可能会睡眠,这就会唤醒调度程序,从而导致调度一个新的用户进程执行。

  • 对称多处理—两个或多个处理器可以同时执行代码。

  1. 在中断处理程序中能避免并发访问的安全代码称作中断安全代码(interrupt-safe)

  2. 在对称多处理的机器中能避免并发访问的安全代码称为SMP安全代码(SMP-safe)

  3. 在内核抢占时能避免并发访问的安全代码称为枪占安全代码(preempt-safe)

什么数据需要加锁

找出哪些数据需要保护是关键所在。由于任何可能被并发访问的代码都几乎无例外地需要保护,所以寻找哪些代码不需要保护反而相对更容易些,我们也就从这里人手。执行线程的局部数据仅仅被它本身访问,显然不需要保护,比如.局部自动变(还有动态分配的数据结构,其地址仅存放在堆栈中〕不需要任何形式的锁,因为它们独立存在于执行线程的栈中。类似的,如果数据只会被特定的进程访问,那么也不需要加锁(因为进程一次只在一个处理器上执行)。

  • 这个数据是不是全局的?除了当前线程外,其他线程能不能访问它?

  • 这个数据会不会在进程上下文和中断上下文中共享?它是不是要在两个不同的中断处理程序中共享?

  • 进程在访问数据时可不可能被抢占?被调度的新程序会不会访问同一数据?

  • 当前进程是不是会睡眠(阻塞)在某些资源上,如果是,它会让共享数据处千何种状态?

  • 怎样防止数据失控?

  • 如果这个函数又在另一个处理器上被调度将会发生什么呢?

  • 如伺确保代码远离并发威胁呢?

死锁

  1. 互斥条件:一个资源每次只能被一个进程使用。

  2. 请求与保持条件:一个进程因请求资源而阻塞时,对已获得的资源保持不放。

  3. 不剥夺条件:进程已获得的资源,在末使用完之前,不能强行剥夺。

  4. 循环等待条件:若干进程之间形成一种头尾相接的循环等待资源关系。

以上是死锁的必要条件,那么破坏其中一条就可以了。

  • 按顺序加锁。使用嵌套的锁时必须保证以相同的顺序获取锁,这样可以阻止致命拥抱类型的死锁。最好能记录下锁的顺序,以便其他人也能照此顺序使用。

  • 防止发生饥饿。试问,这个代码的执行是否一定会结束?如果“张”不发生“王”要一直等待下去玛?

  • 不要重复请求同一个锁。

  • 设计应力求简.单—越复杂的加锁方案越有可能造成死锁。

尽管释放锁的顺序和死锁是无关的,但最好还是以获得锁的相反顺序来释放锁。

争用和扩展性

lock contention 就是锁的 争用。说的是锁在被占用的时候,其他线程试图获得锁的情形。

关键在于,在设计锁的开始阶段就应该考虑到要保证良好的扩展性。因为即使在小型机器上,如果对重要资源锁得太粗,也很容易造成系统性能瓶颈。锁加得过粗或过细.差别往往只在一线之间。当锁争用严重时,加锁太粗会降低可扩展性统性能下降而锁争用不明显时,加锁过细会加大系统开销,带来浪费,这两种情况都会造成系但要记住:设计初期加锁方案应该力求简单,仅当需要时再进一步细化加锁方案。精髓在于力求简单。