天天看点

《linux内核设计与实现》 - 读书笔记:内核同步介绍

多年之前,在单一处理器的时候,只有在中断发生的时候,或者在内核代码中显示地请求重新调度,执行另一任务的时候,数据才有可能被并发访问。从2.0开始,内核开始支持对称多处理器,这就意味着内核代码可以运行在两个或者更多的处理器上,因此,如果不加以保护,运行在两个不同处理器上的内核代码完全可能在同一时刻并发访问共享数据。随着2.6内核的出现,linux内核以发展成为抢占式内核,这意味这不加保护的情况下,调度程序可以在任何时刻抢占正在运行的内核代码,重新调度其他进程执行。

并发与竞态

       并发(concurrency)环境中的多个进程如果需要访问同一个临界资源,在不加以保护的情况下,很容易导致竞态(race condition)。

       为了避免某临界资源的临界区被并发执行,编程者必须保证临界区原子地执行。

       竞争状态出现的几率非常小,但是这种因竞争引起的错误非常不易重视,所以调试这种错误才会非常困难。

       避免并发和防止竞态出现的机制被称为同步(synchronization)。

       锁机制

       任何需要进入临界区的进程首先需要占住对应的锁,这样就能阻止来自其他线程的并发访问。需要注意的是,锁的应用是自愿的,非强制的。

       锁有多种多样的形式,而且加锁的粒度范围也各不相同。Linux自身实现了几种不同的锁机制,各种锁机制之间的区别主要在于锁被争用时的行为表现 --- 一些锁被争用时会简单地执行忙等待,而有些锁会使当前任务睡眠直到锁可用为止。

       到底是什么造成了并发执行

       内核中有以下可能造成并发执行的情况:

    · 中断 —— 中断几乎可以在任何时刻异步发生,也就可能随时打断当前正在执行的代码。(中断服务程序访问被打断进程正在访问的资源)

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

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

    · 睡眠以及用户空间的同步 —— 在内核执行的进程可能会睡眠,这就会唤醒调度程序,从而导致调度一个新的用户进程执行。(内核代码在临界区中睡眠)

    · 对称对处理器 —— 两个或多个处理器可以同时执行代码。(多个处理器绝对不能同时访问同一共享资源)

       当我们清楚什么样的数据需要保护时,提供锁来保护代码的安全也就不难做到了。然而,真正困难的却是发现上述这些潜在并发执行的可能,并有意识地采取某些措施来防止并发执行。

       基本原则:在编写代码的开始阶段就要设计恰当的锁。

       中断安全代码(interrupt-safe) —— 中断处理程序中能避免并发访问的代码。

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

       抢占安全代码(preempt-safe) —— 在内核抢占时能避免并发访问的安全代码。

       要保护些什么

       找出哪些数据需要保护是关键所在。

       到底什么数据需要加锁呢?大多数内核数据结构都需要加锁!有一条很好的经验可以帮助我们判断:如果有其他执行线程可以访问这些数据,那么就给这些数据加上某种形式的锁;如果任何其他什么东西都能看见这个数据,那么就要锁住它。要给数据而不是代码加锁。

       死锁

       死锁的产生需要一定的条件:一个或多个执行线程和一个或多个资源,每个线程都在等待被其他线程持有的资源。

以下是一些简单的规则对避免死锁有很大帮助:

1.       加锁的顺序。使用嵌套的锁时必须保证以相同的顺序获取锁,这样可以阻止致命拥抱类型的死锁。尽管是否锁的顺序和死锁无关,但最好还是以获得锁相反的顺序来释放锁。

2.       防止发生饥饿。

3.       不要重复请求同一个锁。

4.       加锁力求简单。

继续阅读