Contents
  1. 1. 内核抢占
  2. 2. 每CPU变量
  3. 3. 原子操作
  4. 4. 优化和内存屏障
  5. 5. 自旋锁
  6. 6. 读-拷贝-更新(RCU)
  7. 7. 信号量

内核抢占

抢占内核的主要特点是:一个在内核态运行的进程,可能在执行内核函数期间被另外一个进程取代。
只有当内核正在执行异常处理程序(尤其是系统调用),而且内核抢占没有被显式地禁用时,才可能抢占内核。
内核使用的各种同步技术:每CPU变量、原子操作、内存屏障、自旋锁、信号量、顺序锁、本地中断的禁止、本地软中断的禁止、读-拷贝-更新(RCU)。

每CPU变量

主要是数据结构的数组,系统的每个CPU对应数组的一个元素。
一个CPU不应该访问与其他CPU对应的数组元素。另外,它可以随意读或修改它自己的元素而不用担心出现竞争条件,因为它是唯一有资格这么做的CPU。在单处理器和多处理器系统中,内核抢占都可能使每CPU变量产生竞争条件。总的原则是内核控制路径应该在禁用抢占的情况下访问每CPU变量。

原子操作

若干汇编语言指令具有“读-修改-写”类型——也就是说,它们访问存储器单元两次,第一次读原值,第二次写新值。
Linux内核提供了一个专门的atomic_t类型(一个原子计数器)和一些专门的函数和宏。在多处理器系统中,每条这样的指令都有一个lock字节(“锁定内存总线,直到这条指令执行完成”)的前缀。

优化和内存屏障

当使用优化的编译器时,编译器为了优化可能会重新安排汇编语言指令以便寄存器以最优的方式使用。
内存屏障(memory barrier)原语确保,在原语之后的操作开始执行之前,原语之前的操作已经完成。

自旋锁

自旋锁(spin lock)是用来在多处理器环境中工作的一种特殊的锁。如果内核控制路径发现自旋锁“开着”,就获取锁并继续自己的执行。相反,如果内核控制路径发现锁由运行在另一个CPU上的内核控制路径“锁着”,就在周围“旋转”。反复执行一条紧凑的循环指令,直到锁被释放。
一般来说,由自旋锁所保护的每个临界区都是禁止内核抢占的。在单处理器系统上,这种锁本身并不起锁的作用,自旋锁原语仅仅是禁止或启用内核抢占。
在linux中,每个自旋锁都用spinlock_t结构表示,其中包含两个字段:

1.slock——表示自旋锁的状态。(1—未加锁 负数和0—加锁)
2.break_lock——表示进程正在忙等自旋锁(只在内核支持SMP和内核抢占的情况下使用该标志)

读写自旋锁的引入是为了增加内核的并发能力。只要没有内核控制路径堆数据结构进行修改,读写自旋锁就允许多个内核控制路径读同一数据结构。允许对数据结构并发度可以提高系统性能。
顺序锁:与读写自旋锁非常相似,只是它为写者赋予了较高的优先级;事实上,即使在读者正在读的时候也允许写者继续运行。这种策略的好处是写者永远不会等待(除非另外一个写者正在写),缺点是有些时候读者不得不反复多次读相同的数据直到获得有效的副本。
当读者进入临界区时,不必禁用内核抢占;另一方面,由于写者获取自旋锁,所以它进入临界区时自动禁用内核抢占。
一般来说,在满足以下条件时才能使用顺序锁:

1.被保护的数据结构不包括被写者修改和被读者间接引用的指针(否则,写者可能在读者的眼鼻下就修改指针)。
2.读者的临界区代码没有副作用(否则,多个读者的操作会与单独的读操作有不同的结果)。

读-拷贝-更新(RCU)

读-拷贝-更新(RCU)是为了保护在多数情况下被多个CPU读的数据结构而设计的另一种同步技术。RCU允许多个读者和写者并发执行,RCU不使用锁,就是说它不使用被所有CPU共享的锁或计数器。
RCU同步的关键思想是:

1.RCU只保护被动态分配并通过指针引用的数据结构。
2.在被RCU保护的临界区中,任何内核控制路径都不能睡眠。

使用RCU技术的真正困难在于:写者修改指针时不能立即释放数据结构的旧副本。实际上,写着开始修改时,正在访问数据结构的读者可能还在读旧副本。只有在CPU上的所有(潜在的)读者都执行完宏rcu_read_unlock()之后,才可以释放旧副本。
RCU是Linux2.6中新加的功能,用在网络层和虚拟文件系统中。

信号量

Linux提供两种信号量:

1.内核信号量,由内核控制路径使用
2.Systerm V IPC 信号量,由用户态进程使用

内核信号量:类似于自旋锁,因为当锁关闭着时,它不允许内核控制路径继续进行。只有可以睡眠的函数才能获取内核信号量;中断处理程序和可延迟函数都不能使用内核控制量。
内核信号量是struct semaphore类型的对象。
TASK_INTERRUPTIBLE是可以被信号和wake_up()唤醒的,当信号到来时,进程会被设置为可运行。
而TASK_UNINTERRUPTIBLE只能被wake_up()唤醒。
读/写信号量:类似于前面的“读写自旋锁”,有一点不同的是,在信号量再次变为打开之前,等待进程挂起而不是自旋。内核以严格的FIFO顺序处理等待读写信号量的所有进程。
每个读写信号量都是有rw_semaphore结构描述的。
补充原语(completion):其和信号量之间的真正区别在于如何使用等待队列中包含的自旋锁。在补充原语中,自旋锁用来确保complete()和wait_for_completion()不会并发执行。在信号量中,自旋锁用于避免并发执行的down()函数弄乱信号量的数据结构。


本文章参考自《深入理解linux内核》。

Contents
  1. 1. 内核抢占
  2. 2. 每CPU变量
  3. 3. 原子操作
  4. 4. 优化和内存屏障
  5. 5. 自旋锁
  6. 6. 读-拷贝-更新(RCU)
  7. 7. 信号量