1.前言
为什么要考虑内核同步
在单一CPU 的情况下,中断或者内核代码明确调度时,多个执行线程并发访问共享数据。目前多处理器以及抢占式内核的存在,更需要注意保护共享资源。
造成并发的原因
- Interrupts— An interrupt can occur asynchronously at almost any time, interrupting the currently executing code.
- Softirqs and tasklets— The kernel can raise or schedule a softirq or tasklet at
almost any time, interrupting the currently executing code.- Kernel preemption— Because the kernel is preemptive, one task in the kernel
can preempt another.- Sleeping and synchronization with user-space— A task in the kernel can
sleep and thus invoke the scheduler, resulting in the running of a new process.- Symmetrical multiprocessing— Two or more processors can execute kernel
code at exactly the same time.
临界区
临界区( Critical Regions ) 指的是访问内存中共享数据的 代码段,编程者需要保证这一部分代码段执行期间不被打断。如果两个线程在同一个临界区执行,就会有Bug。避免这种情况发生称之为 同步(synchronization).
例子
比如银行取款的代码,同一个账号同时在用两种方式取款,需要调用同一个取款函数,这时候必须考虑同步。有两种方法来解决这个问题,一个是原子性,一个是加锁。
将取款的整个函数原子化,使得每一个事务是原子性并且不可打断的。CPU 和内核提供了原子操作的接口(算术运算或比较大小)。
有时候共享数据是一个复杂的数据结构,例如队列等不定长度的数据类型,这种复杂的情况没有办法原子化,就需要用到加锁
2.加锁
- 锁的机制就像一个房间的门锁一样,房间内只能有一个线程存在。后来的线程需要在门外排队。
- linux 自身实现了几种不同的锁,主要体现在锁在被占用的情况下的表现。
- 加锁的过程是采用原子操作实现的,和体系结构相关。一般 0 意味着开锁。
- 伪并发与真并发 都存在竞争,都需要保护。
- 伪并发是指单处理器系统中,一个程序在运行中被调度到另一个程序,两个程序执行在一个临界区。
- 真并发是指在多处理器系统中,两个程序同时执行,执行在一个临界区。
- 加锁的方法并不难,难的是找到哪些数据和临界区需要加锁,在编写代码的时候就要考虑到
- Is the data global? Can a thread of execution other than the current one access it?
- Is the data shared between process context and interrupt context? Is it shared between two different interrupt handlers?
- If a process is preempted while accessing this data, can the newly scheduled process access the same data?
- Can the current process sleep (block) on anything? If it does, in what state does that leave any shared data?
- What prevents the data from being freed out from under me?
- What happens if this function is called again on another processor?
- Given the proceeding points, how am I going to ensure that my code is safe from
concurrency?
- 不需要加锁的数据
- 局部变量,只被特定进程访问的数据
- 需要加锁的数据
- 大多数内核的数据结构,有其他线程可以访问的数据
- 内核在编译时是可以配置的,可以不设置SMP 以及 抢占的模块,减少开销。
3.死锁
死锁的产生条件:一个或多个执行线程,和一个或多个共享数据。 ==彼此需要的资源被彼此锁住了== 。
- 自死锁,等待自己已经拿到的锁
- 避免死锁的规则:
- 按顺序加锁,多个线程按照同样的顺序获得多个锁, A - B -C 这样 第二个进程就不会先获得C 造成死锁。
- 防止发生饥饿
- 不重复请求同一个锁
- 设计应简单
- 内核提供了调试工具在运行时检测死锁
4.扩展性
- 并不是多CPU 就是成倍的增加性能,因为共享数据的锁,需要排队等待锁机制。
- 锁的加锁 ==粒度==,是对一个大块数据加锁还是对大块数据的一个元素加锁。
- 可以对(一整个链表/链表的一个节点/节点的一个元素)进行加锁。
- 锁的增用严重的话,就需要使加锁的粒度更精细,但是粒度过于精细也会使得系统开销增大
- 调度队列 O(1) 到 CFS 就提高了精细度