内核中的同步问题是一个很复杂,而且经常让人感到崩溃的问题,即使是应用程序,多线程也是一个复杂的问题。一般的同步方法包括有锁机制,互斥量,信号量等。而各种应用场景下的方法的选择,尤其是涉及不同的上下文和不同的体系结构时,这个问题会更加复杂。
锁和互斥量是保护临界区的两种基本方法。锁保证在同一时刻只有一个线程能够进入临界区,其他线程只有在这个线程退出后在能进入临界区。其使用方法很简单,
而 互斥量和锁的功能差不多,区别在于等待锁的线程总是在进行忙等待,而等待互斥量的线程会进入睡眠状态,直到其他线程释放互斥量。一般情况下,如果线程需要 睡眠,只能使用互斥量,获取锁后不能调度,抢占或者睡眠。而在中断处理函数中,由于其不能睡眠,所以只能使用锁机制。互斥量的使用方法如下:
互斥量接口取代了原来的信号量接口,当然原来的接口还可以使用,但谁也不敢保证未来怎么样。
随着操作系统以及系统结构的发展,这种同步问题越来越复杂。操作系统方面,内核是不是能够抢占,程序运行在什么上下文环境,系统结构方面,单核和多核的区别等。根据这些,可以分为多个情况:
(1)进程上下文和中断上下文 && 单处理器 && 非抢占内核
因为中断发生时要优先处理中断,因此中断处理程序不需要采取多余的措施,而对于运行在进程上下文的普通进程来说,则需要在执行时屏蔽中断。
假设A,B为运行在进程上下文的两个进程,而C运行在中断上下文,三个进程共享一个临界区。一旦C开始执行,除非C运行结束或者主动让出CPU,否则C会一直运行下去,因此C不用担心A,B带来的影响。再来看A,B,两者也是无须担心对方的,因为内核时非抢占的,唯一可以干扰他们执行的,只有中断处理程序,如C。如果A,B,C三者试图进入同一个临界区,则只需考虑A,B在临界区内被C抢占的情况。假设A已经进入临界区内,这意味着它已经成功获得了锁,此时中断发生,程序C开始执行,由于C无法获得锁,则此时将造成死锁。因此,A,B在进入临界区前,需要屏蔽中断。如下:
如果考虑到A在屏蔽中断之前,中断已被屏蔽,那么local_irq_enable()的调用可能造成影响,因此更妥善的方法时屏蔽中断时记录下中断屏蔽字,然后退出临界区后恢复中断屏蔽字。如下
(2)进程上下文和中断上下文 && 单处理器 && 抢占内核
和第一种情况类似,这种情况下屏蔽中断依然是必需的。但是,内核的可抢占特性增加了一种情况,就是A,B也可能在对方与临界区卿卿我我时横刀夺爱,所以此时A,B此时也要防止对方第三者插足,方法就是在进入临界区之前不仅要屏蔽中断,还要禁止内核抢占。
在上面的函数spin_lock_irqsave中不光屏蔽了中断,还禁止内核抢占。内核通过一个特定变量来确定当前是否可以进行抢占,只有当这个变量等于零时,内核才允许抢占。在spin_lock_irqsave()中调用了preempt_disable()函数,这个函数增加这个变量的值(加一),而在spin_unlock_irqrestore中调用函数preempt_enable()减少这个变量的值(减一)。
上面的情况下,锁的功能好像被弱化了,其实屏蔽中断和禁止抢占都是类似锁的功能----防止多个进程同时进入临界区。
(3)进程上下文和中断上下文 && 多处理器 && 抢占内核
上面的两种情况下,C无须考虑A,B带来的影响。而当内核运行在对称多处理器的机器上,由于可能有程序同时运行(这是真正的并行),所以中断处理程序C在进入临界区前,也要获取锁,以免A,B在其他处理器上运行并进入临界区而产生冲突。
至于A,B,因为锁机制的实现时SMP安全的,所以其执行没有变化,和(2)中一样。