内核同步方法-自旋锁

来源:互联网 发布:vc2010 mfc编程实例 编辑:程序博客网 时间:2024/05/02 02:04

一些关于自旋锁的说法:

       linux内核中最常见的锁就是自旋锁(spin lock)。自旋锁最多只能被一个可执行线程持有。如果一个执行线程试图获得一个被争用的自旋锁,那么该线程就会一直忙循环—旋转—等待锁重新可用。要是锁未被争用,请求锁的线程便能立刻得到他,继续执行,在任意时刻,自旋锁都可以防止多于一个的执行线程同时进入临界区。

要点:

      一直自旋,直到获得到被争用的自旋锁,自旋锁的时间最好是小于2次上下文切换时间。

自旋锁的基本形式如下:

      spinlock_tmr_lock=SPIN_LOCK_UNLOCKED;

  spin_lock(&mr_lock);

  //临界区

  spin_unlock(&mr_lock);

      因为自旋锁在同一时刻只能被最多一个内核任务持有,所以一个时刻只有一个线程允许存在于临界区中。这点很好地满足了对称多处理机器需要的锁定服务。在单处理器上,自旋锁仅仅当作一个设置内核抢占的开关。如果内核抢占也不存在,那么自旋锁会在编译时被完全剔除出内核。

  简单的说,自旋锁在内核中主要用来防止多处理器中并发访问临界区,防止内核抢占造成的竞争。另外自旋锁不允许任务睡眠(持有自旋锁的任务睡眠会造成自死锁——因为睡眠有可能造成持有锁的内核任务被重新调度,而再次申请自己已持有的锁),它能够在中断上下文中使用。

自旋锁 API 简介
  自旋锁原语要求的包含文件是 <linux/spinlock.h>. 一个实际的锁有类型 spinlock_t. 象任何其他数据结构, 一个 自旋锁必须
初始化. 这个初始化可以在编译时完成, 如下:
  spinlock_t my_lock = SPIN_LOCK_UNLOCKED;
  或者在运行时使用:
  void spin_lock_init(spinlock_t *lock);
  在进入一个临界区前, 你的代码必须获得需要的 lock , 用:
  void spin_lock(spinlock_t *lock);
  注意所有的自旋锁等待时, 由于它们的特性, 不可中断的. 一旦你调用 spin_lock, 你将自旋直到锁变为可用.
  为释放一个你已获得的锁, 传递它给:
  void spin_unlock(spinlock_t *lock);
  有很多其他的自旋锁函数, 我们将很快都看到. 但是没有一个背离上面列出的函数所展示的核心概念. 除了加锁和释放, 没有什么可对一个锁所作的. 但是, 有几个规则关于你必须如何使用自旋锁. 我们将用一点时间来看这些, 在进入完整的自旋锁接口之前.
  自旋锁和原子上下文
  想象一会儿你的驱动请求一个自旋锁并且在它的临界区里做它的事情. 在中间某处, 你的驱动失去了处理器. 或许它已调用了一个函数( copy_from_user, 假设) 使进程进入睡眠. 或者, 也许, 内核抢占发威, 一个更高优先级的进程将你的代码推到一边. 你的代码现在持有一个锁, 在可见的将来的如何时间不会释放这个锁. 如果某个别的线程想获得同一个锁, 它会, 在最好的情况下, 等待( 在处理器中自旋 )很长时间. 最坏的情况, 系统可能完全死锁.
  大部分读者会同意这个场景最好是避免. 因此, 应用到自旋锁的核心规则是任何代码必须, 在持有自旋锁时, 是原子性的. 它不能睡眠; 事实上, 它不能因为任何原因放弃处理器, 除了服务中断(并且有时即便此时也不行)
  内核抢占的情况由自旋锁代码自己处理. 内核代码持有一个自旋锁的任何时间, 抢占在相关处理器上被禁止. 即便单处理器系统必须以这种方式禁止抢占以避免竞争情况. 这就是为什么需要正确的加锁, 即便你从不期望你的代码在多处理器机器上运行.
  在持有一个锁时避免睡眠是更加困难; 很多内核函数可能睡眠, 并且这个行为不是都被明确记录了. 拷贝数据到或从用户空间是一个明显的例子: 请求的用户空间页可能需要在拷贝进行前从磁盘上换入, 这个操作显然需要一个睡眠. 必须分配内存的任何操作都可能睡眠. kmalloc 能够决定放弃处理器, 并且等待更多内存可用除非它被明确告知不这样做. 睡眠可能发生在令人惊讶的地方; 编写会在自旋锁下执行的代码需要注意你调用的每个函数.
  这有另一个场景: 你的驱动在执行并且已经获取了一个锁来控制对它的设备的存取. 当持有这个锁时, 设备发出一个中断, 使得你的中断处理运行. 中断处理, 在存取设备之前, 必须获得锁. 在一个中断处理中获取一个自旋锁是一个要做的合法的事情; 这是自旋锁操作不能睡眠的其中一个理由. 但是如果中断处理和起初获得锁的代码在同一个处理器上会发生什么? 当中断处理在自旋, 非中断代码不能运行来释放锁. 这个处理器将永远自旋.
  避免这个陷阱需要在持有自旋锁时禁止中断( 只在本地 CPU ). 有各种自旋锁函数会为你禁止中断( 我们将在下一节见到它们 ). 但是, 一个完整的中断讨论必须等到第 10 章了.
  关于自旋锁使用的最后一个重要规则是自旋锁必须一直是尽可能短时间的持有. 你持有一个锁越长, 另一个进程可能不得不自旋等待你释放它的时间越长, 它不得不完全自旋的机会越大. 长时间持有锁也阻止了当前处理器调度, 意味着高优先级进程 -- 真正应当能获得 CPU 的 -- 可能不得不等待. 内核开发者尽了很大努力来减少内核反应时间( 一个进程可能不得不等待调度的时间 )在 2.5 开发系列. 一个写的很差的驱动会摧毁所有的进程, 仅仅通过持有一个锁太长时间. 为避免产生这类问题, 重视使你的锁持有时间短.
  自旋锁函数
  我们已经看到 2 个函数, spin_lock 和 spin_unlock, 可以操作自旋锁. 有其他几个函数, 然而, 有类似的名子和用途. 我们现在会展示全套. 这个讨论将带我们到一个我们无法在几章内适当涵盖的地方; 自旋锁 API 的完整理解需要对中断处理和相关概念的理解.
  实际上有 4 个函数可以加锁一个自旋锁:
  void spin_lock(spinlock_t *lock);
  void spin_lock_irqsave(spinlock_t *lock, unsigned long flags);
  void spin_lock_irq(spinlock_t *lock);
  void spin_lock_bh(spinlock_t *lock)
  我们已经看到自旋锁如何工作. spin_loc_irqsave 禁止中断(只在本地处理器)在获得自旋锁之前; 之前的中断状态保存在 flags 里. 如果你绝对确定在你的处理器上没有禁止中断的(或者, 换句话说, 你确信你应当在你释放你的自旋锁时打开中断), 你可以使用 spin_lock_irq 代替, 并且不必保持跟踪 flags. 最后, spin_lock_bh 在获取锁之前禁止软件中断, 但是硬件中断留作打开的.
  如果你有一个可能被在(硬件或软件)中断上下文运行的代码获得的自旋锁, 你必须使用一种 spin_lock 形式来禁止中断. 其他做法可能死锁系统, 迟早. 如果你不在硬件中断处理里存取你的锁, 但是你通过软件中断(例如, 在一个 tasklet 运行的代码, 在第 7 章涉及的主题 ), 你可以使用 spin_lock_bh 来安全地避免死锁, 而仍然允许硬件中断被服务.
  也有 4 个方法来释放一个自旋锁; 你用的那个必须对应你用来获取锁的函数.
  void spin_unlock(spinlock_t *lock);
  void spin_unlock_irqrestore(spinlock_t *lock, unsigned long flags);
  void spin_unlock_irq(spinlock_t *lock);
  void spin_unlock_bh(spinlock_t *lock);
  每个 spin_unlock 变体恢复由对应的 spin_lock 函数锁做的工作. 传递给 spin_unlock_irqrestore 的 flags 参数必须是传递给 spin_lock_irqsave 的同一个变量. 你必须也调用 spin_lock_irqsave 和 spin_unlock_irqrestore 在同一个函数里. 否则, 你的代码可能破坏某些体系.
  还有一套非阻塞的自旋锁操作:
  int spin_trylock(spinlock_t *lock);
  int spin_trylock_bh(spinlock_t *lock);
  这些函数成功时返回非零( 获得了锁 ), 否则 0. 没有"try"版本来禁止中断.
读者/写者自旋锁
  内核提供了一个自旋锁的读者/写者形式, 直接模仿我们在本章前面见到的读者/写者旗标. 这些锁允许任何数目的读者同时进入临界区, 但是写者必须是排他的存取. 读者写者锁有一个类型 rwlock_t, 在 <linux/spinlokc.h> 中定义. 它们可以以 2 种方式被声明和被
初始化:
  rwlock_t my_rwlock = RW_LOCK_UNLOCKED; /* Static way */
  rwlock_t my_rwlock;
  rwlock_init(&my_rwlock); /* Dynamic way */
  可用函数的列表现在应当看来相当类似. 对于读者, 下列函数是可用的:
  void read_lock(rwlock_t *lock);
  void read_lock_irqsave(rwlock_t *lock, unsigned long flags);
  void read_lock_irq(rwlock_t *lock);
  void read_lock_bh(rwlock_t *lock);
  void read_unlock(rwlock_t *lock);
  void read_unlock_irqrestore(rwlock_t *lock, unsigned long flags);
  void read_unlock_irq(rwlock_t *lock);
  void read_unlock_bh(rwlock_t *lock);
  有趣地, 没有 read_trylock. 对于写存取的函数是类似的:
  void write_lock(rwlock_t *lock);
  void write_lock_irqsave(rwlock_t *lock, unsigned long flags);
  void write_lock_irq(rwlock_t *lock);
  void write_lock_bh(rwlock_t *lock);
  int write_trylock(rwlock_t *lock);
  void write_unlock(rwlock_t *lock);
  void write_unlock_irqrestore(rwlock_t *lock, unsigned long flags);
  void write_unlock_irq(rwlock_t *lock);
  void write_unlock_bh(rwlock_t *lock);