正文:

  1. 临界区:访问和操作共享数据的代码段。
  2. 竞争条件:如果两个执行线程有可能处于同一个临界区中同时执行。
  3. 避免并发和防止竞争条件称同步。
  4. 线程持有锁,锁保护了数据结构。
  5. 锁是采用原子操作实现的,不存在竞争。
  6. 中断安全代码,SMP安全代码,抢占安全代码。
  7. 单核伪并发,多核真并发。
  8. 局部自动变量(还有动态分配的数据结构,其地址仅存放在堆栈中)不需要锁,因为它们独立存在于执行线程的栈中。如果数据只会被特定的进程访问,那么也不需要加锁(因为进程一次只在一个处理器上执行)。
  9. 给数据加锁而不是给代码。
  10. 死锁产生条件:要有一个或多个执行线程和一个或多个资源,每个线程都在等待其中的一个资源,但所有的资源都已经被占用了,所有线程都在相互等待,但它们永远不会释放已经占有的资源。
  11. 一个执行线程也有可能会试图去获得一个自己已经持有的锁。
  12. 有些内核提供了递归锁来防止自死锁现象。
  13. 避免死锁的规则:按顺序加锁;防止发生饥饿;不要重复请求同一个锁;设计应力求简单。
  14. 只要嵌套地使用多个锁,就必须按照相同的顺序去获取它们。而释放锁就按照相反顺序.
  15. 锁的争用:指锁正在被占用时,有其他线程试图获得该锁。
  16. 加锁粒度用来描述加锁保护的数据规模。一个过粗的锁保护大块数据,一个过于精细的锁保护很小的一块数据。
  17. 当锁争用过于严重时,加锁太粗会降低可扩展性;而锁争用不明显时,加锁过细会加大系统开销。
  18. 内核提供了两组原子操作接口:一组针对整数进行操作,一组针对单独的位进行操作。(所有体系结构都实现了这两组接口,大多数体系结构还提供了支持原子操作的简单算数指令,有些为单步执行提供了锁内存总线的指令)
  19. 针对整数的原子操作只能对atomic_t类型的数据进行处理。使用atomic_t类型确保编译器不对相应的值进行访问优化。
  20. 原子操作通常是内联函数,往往是通过内嵌汇编指令来实现。
  21. 在编写代码时,尽可能使用原子操作,而不是更复杂的加锁机制。
  22. 自旋锁最多只能被一个可执行线程持有,如果一个执行线程试图获得一个已经被持有的自旋锁,那么该线程就会一直进行忙循环—旋转—等待锁重新可用。所以自旋锁不应该被长时间持有(适合在短时间内进行轻量级加锁)。
  23. 自旋锁的实现与体系结构密切相关。代码往往通过汇编实现<asm/spinlock.h>,实际需要的接口定义在<linux/spinlock.h>。
  24. 自旋锁在linux内核中是不可递归的。自旋锁可以使用在中断处理程序中(不能用信号量,因为他们会导致睡眠)。
  25. 读—写自旋锁(共享/排斥锁):一个或多个读任务可以并发的持有读者锁;相反,用于写的锁最多只能被一个写任务持有,而且此时不能有并发的读操作。自旋等待的写者必须等所有的读者释放锁之后才能获得锁。
  26. 如果加锁时间不长并且代码不会睡眠,利用自旋锁是最佳选择。
  27. 信号量是一种睡眠锁,如果有一个任务试图获得一个不可用的信号量时,信号量会将其推入一个等待队列,然后让它睡眠。信号量适用于锁会被长时间持有的情况,且线程只能在进程上下文中才能获取信号量锁(因为中断上下文中是不能进行调度的)。
  28. 信号量不同于自旋锁,它不会禁止内核抢占,所有持有信号量的代码可以被抢占。
  29. 二值信号量(同一时刻最多有一个线程持有),计数信号量(同一时刻可以有多个线程持有)。
  30. 信号量支持两个原子操作P()和V(),前者叫做测试操作,后者叫做增加操作。(后来的系统把两重操作分别叫做down(),up())
  31. 读—写信号量(互斥信号量),直对写者互斥。
  32. 互斥体(mutex):任何可以睡眠的强制锁。
  33. 任何时刻中只有一个任务可以持有mutex;给mutex上锁者必须负责给其解锁,即在同一上下文中上锁和解锁;不允许递归上锁和解锁;当持有一个mutex时,进程不可以退出;mutex不能在中断或者下半部中使用。
  34. 完成变量:如果在内核中一个任务需要发出信号通知另一个任务发生了某个特定事件,利用完成变量是使两个任务得以同步的简单方法。
  35. 大内核锁(BKL)是一个全局自旋锁。持有大内核锁的任务任然可以睡眠;BKL是一种递归锁,只使用在进程上下文中。
  36. 顺序锁(seq)提供了一种很简单的机制,用于读写共享数据。顺序锁的实现主要依靠一个序列计数器。
  37. 如果一个自旋锁被持有,内核便不能进行抢占。
  38. 指令重排序的发生是因为现代处理器为了优化其传送管道,打乱了分派和提交指令的顺序。