第九章 内核同步介绍

时间:2021-01-15 23:25:08

临界区

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

竞争

如果两个执行线程有可能处于同一个临界区中执行,那么这就是程序的一个Bug,如果这种情况确实发生了,我们就称它为竞争条件。

同步

避免并发和防止竞争条件称为同步。

内核同步的手段——加锁

它如同一把门锁,门后的房间可以想象成一个临界区,在指定的时间内,房间里只有一个执行线程可以执行,当它访问完共享数据时,走出房间打开门锁。
锁本身是通过原子操作实现的,原子操作不存在竞争。

内核中有可能造成并发执行的原因

  • 中断随时会发生,也就会随时打断当前执行的代码。如果中断和被打断的代码在相同的临界区,就产生了竞争条件
  • 软中断和tasklet也会随时被内核唤醒执行,也会像中断一样打断正在执行的代码
  • 内核具有抢占性,发生抢占时,如果抢占的线程和被抢占的线程在相同的临界区,就产生了竞争条件
  • 用户进程睡眠后,调度程序会唤醒一个新的用户进程,新的用户进程和睡眠的进程可能在同一个临界区中
  • 对称多处理,2个或多个处理器可以同时执行相同的代码

哪些数据需要保护

  • 执行线程的局部数据仅仅被它本身所访问,显然不需要保护,比如,局部自动变量(还有动态分配的数据结构,其结构存放在堆栈中)不需要任何形式的锁。类似的,如果数据只被特定的进程访问,那么要不需要加锁(因为进程一次只在一个处理器上执行)。
  • 到底什么数据需要加锁?大多数内核数据结构都需要加锁!有一条很好的经验可以帮助我们判断:如果有其它执行线程可以访问这些数据,那么就给这些数据加上某种形式的锁;如果任何什么东西都能看到它,那么就要锁住它。记住:要给数据而不是给代码加锁。

在编写内核代码时,你要问自己下面这些问题

  • 这个数据是不是全局的?除了当前线程以外,其他线程能不能访问它?
  • 这个数据会不会在进程上下文或者中断上下文*享?它是不是要在两个不同的中断处理程序*享?
  • 进程在访问数据时可不可能被抢占?被调度的新程序会不会访问同一数据?
  • 当前进程会不会睡眠(或者阻塞)在某些资源上,如果是,它会让共享数据处于何种状态?
  • 怎样防止数据失控?
  • 如果这个函数又在另一个处理器上被调度将会发生什么?

死锁

死锁就是所有线程都在相互等待释放资源,导致谁也无法继续执行下去。

下面一些简单的规则可以帮助我们避免死锁

  • 如果有多个锁的话,尽量确保每个线程都是按相同的顺序加锁,按加锁相反的顺序解锁。(即加锁a->b->c,解锁c->b->a)
  • 防止发生饥饿。即设置一个超时时间,防止一直等待下去。
  • 不要重复请求同一个锁。
  • 设计应力求简单。加锁的方案越复杂就越容易出现死锁。