1.临界区和竞争条件
所谓临界区(critical region)就是访问和操作共享数据的代码段。为了避免在临界区中并发访问,必须保证这些代码是原子地执行,即代码在执行结束前不可被打断,就如同整个临界区是一个不可分割的指令一样。
如果两个执行线程有可能处于同一个临界区中,称为竞争条件(race condition)。
避免并发和防止竞争条件被称为同步(Synchronization)。
2.加锁
共享资源是简单变量的话,一些体系结构提供简单的原子指令实现算术运算和比较之类的原子操作。比如要进行i++,多数处理器都提供了指令来原子地读变量、增加变量然后再写回变量。当共享资源是一个复杂的数据结构时,竞争条件往往会使该数据结构遭到破坏。锁机制可以防止并发执行,并且保护队列不受竞争条件的影响。锁的使用是自愿的、非强制的,它完全属于一种编程者自选的编程手段。
锁有多种多样的形式,而且加锁的粒度范围也各不相同。各种锁机制之间的区别主要在于当锁被争用时(已被使用)的行为表现-----简单的忙等或者使当前任务睡眠直到锁可以用为止。
锁采用原子操作实现的,而原子操作不存在竞争,其实现是与具体的体系结构密切相关的。几乎所有的处理器都实现了测试和设置指令,这一指令测试整数的值,如果其值为0,就设置一新值。0意味着开锁。
3.造成并发的原因
中断 中断几乎可以在任何时刻异步发生,也就是可能随时打断当前正在运行的代码。
软中断和tasklet 内核能在任何时刻唤醒或调度中断和tasklet,打断当前正在执行的代码。
内核抢占 因为内核具有抢占性,所以内核中的任务可能会被另一任务抢占。
睡眠及用户空间的同步 在内核执行的进程可能会睡眠,这就会唤醒调度程序从而导致调度一个新的用户进程执行。
对称多处理 两个或多个处理器可以同时执行代码。
在代码的开始阶段就要设计恰当的锁!!
中断安全代码(interrupt-safe) 在中断处理程序中能避免并发访问的安全代码
SMP安全代码(SMP-safe) 在对称多处理机中能避免并发访问的安全代码
抢占安全代码(preempt-safe) 在内核抢占时能避免并发访问的安全代码。
4.保护的对象
执行线程的局部数据仅仅被它本身访问故不需要保护。
大部分内核数据结构都要加锁!!如果有其他执行线程可以访问这些数据,那么就给这些数据加上某种形式的锁;如果任何其他什么东西能看到他,那么就加锁。
要给数据加锁而不是代码加锁!!
在编写内核代码时,要对自己提出下面的问题:
这个数据是不是全局的?除了当前线程,其他线程能不能访问它?
这个数据会不会在进程上下文和中断上下文中共享?它是否在两个不同的中断处理程序中共享?
进程在访问数据时可不可能被抢占?被调度的新程序会不会访问同一数据?
当前进程是不是会睡眠(阻塞)在某些资源上?如果是,它会让共享数据处于何种状态?
怎样费那个值数据失控?
如果这个函数又在另一个处理器上被调度将会发生什么呢?
你要对这些代码作甚么?
几乎访问所有的内核全局变量和共享数据都要某种形式的同步方法。
5.死锁
死锁产生的条件:要有一个或多个执行线程和一个或多个资源,每个线程都在等待其中的一个资源,但所有资源都已被占用。所以线程相互等待,但它们永远不会释放已经占有的资源。于是任何线程都无法继续,死锁发生。
自死锁:如果一个执行线程试图去获得一个自己已经持有的锁,它不得不等待锁被释放。但因为它正在忙着等待这个锁,所以自己永远也不会有机会释放锁,死锁产生。
预防死锁:
加锁的顺序是关键。使用嵌套的锁时必须保证以相同的顺序获取锁,这样可以阻止致命拥抱类型的死锁。最好能记录下锁的顺序,以便其他人能照此顺序使用。
防止发生饥饿。判断这个代码的执行是否会结束。如果A不发生,B要一直等待下去吗?
不要重复请求同一个锁。
越复杂的加锁方案越可能造成死锁。---设计应力求简单。
尽管释放锁的顺序与死锁无关,但最好还是以获得锁的相反顺序来释放锁。
6.争用和扩展性
锁的争用(lock contention)是指当锁正在被占用,有其他线程试图夺得该锁。
扩展性(scalability)是对系统可扩展成都的一个度量。
加锁的粒度用来描述加锁保护的数据规模。一个过粗的锁保护大块数据,比如一个子系统的所有数据结构;一个过细的锁保护小块数据,比如一个大数据结构中的一个元素。