(十)内核同步方法
原子操作
原子操作可以保证指令以原子的方式执行——执行过程不被打断。
内核提供了两组原子操作接口——一组针对整数进行操作,另一组针对单独的位进行操作。
针对整数的原子操作只能对atomic_t类型的数据进行处理。首先,让原子函数只接收atomic_t类型的操作数,可以确保原子操作只与这种特殊类型数据一起使用。同时,这也保证了该类型的数据不会被传递给任何非原子函数。
原子整数操作最常见的用途就是实现计数器。使用复杂的锁机制来保护一个单纯的计数器显然杀鸡用了宰牛刀,所以,开发者最好使用atomic_inc()和atomic_dec()这两个相对来说轻便一点的操作。
原子操作只保证原子性,顺序性通过屏障指令来实施。
真正的原子操作需要的是——所有中间结果都正确无误。假定给出两个原子位操作:先对某位置位,然后清0。如果没有原子操作,那么这一位可能的确清0了,但是也可能根本没有置位。置位操作可能与清除操作同时发生,但没有成功。清除操作可能成功了,这一位如愿呈现为清0。但是有了原子操作置位会真正发生,可能有那么一刻,读操作显示所置的位,然后清除操作才执行,该位变为0了。
自旋锁
我们经常会碰到这种情况:先得从一个数据结构中移出数据,对其进行格式转换和解析,最后再把它加入到另一个数据结构中。整个执行过程必须是原子的,在数据被更新完毕前,不能有其他代码读取这些数据。显然,简单的原子操作对此无能为力,这就需要使用更为复杂的同步方法——锁来提供保护。
Linux内核中最常见的锁是自旋锁,自旋锁最多只能被一个可执行线程持有。
一个被争用的自旋锁使得请求它的线程在等待锁重新可用时自旋(特别浪费处理器时间),所以自旋锁不应该被长时间持有。使用自旋锁的初衷是在短期间内进行轻量级加锁。还可以使用另外的方式来处理对锁的争用:让请求线程睡眠,直到锁重新可用时再唤醒它。(睡眠)这里有两次明显的上下文切换,因此持有自旋锁的时间最好小于完成两次上下文切换的耗时。
由于下半部可以抢占进程上下文中的代码,所以当下半部和进程上下文共享数据时,必须对进程上下文中的共享数据进行保护,所以需要加锁的同时还要禁止下半部执行。
由于中断处理程序可以抢占下半部,所以如果中断处理程序和下半部共享数据,那么就必须在获取恰当的锁的同时还要禁止中断。
如果加锁时间不长并且代码不会睡眠,利用自旋锁是最佳选择。如果加锁时间可能很长或者代码在持有锁时有可能睡眠,那么最好使用信号量来完成加锁功能。
信号量
Linux中的信号量是一种睡眠锁。如果有一个任务试图获得一个不可用的信号量时,信号量会将其推进一个等待队列,然后让其睡眠。这时处理器能重获自由,从而去执行其他代码。当持有的信号量可用后,处于等待队列中的那个任务将被唤醒,并获得该信号量。
信号量比自旋锁提供了更好的处理器利用率,因为没有把时间花费在忙等待上,但是,信号量比自旋锁有更大的开销。
信号量适用于锁会被长时间持有的情况;如果锁被短时间持有,睡眠、维护等待队列以及唤醒所花费的开销可能比锁被占用的全部时间还要长。
占用信号量的同时不能占用自旋锁,因为在等待信号量的时候可能会睡眠,而在持有自旋锁时是不允许睡眠的。
信号量可以同时允许任意数量的锁持有者,而自旋锁在一个时刻最多允许一个任务持有它。(即计数信号量,它允许在一个时刻至多有count个锁持有者,但用到的情况不多,基本上都是用互斥信号量)
互斥体
“互斥体(mutex)”这个称谓现在用于一种实现互斥的特定睡眠锁。其行为和使用计数为1的信号量类似,但操作接口更简单,实现也更高效,而且使用限制更强。
任何时刻中只有一个任务可以持有mutex,也就是说,mutex的使用计数永远为1。
给mutex上锁者必须负责给其再解锁——你不能在一个上下文中锁定一个mutex,而在另一个上下文中给它解锁。这个限制使得mutex不适合内核同用户空间复杂的同步场景。最常使用的方式是:在同一上下文中上锁和解锁。
递归地上锁和解锁是不允许的。也就是说,你不能递归地持有同一个锁,同样你也不能再去解锁一个已经被解开的mutex。
当持有一个mutex时,进程不可以退出。
mutex不能在中断或者下半部中使用,即使使用mutex_trylock()也不行。
除非mutex的某个约束妨碍你使用,否则相比信号量要优先使用mutex。当你写新代码时,只有碰到特殊场合才会需要使用信号量。
完成变量
如果在内核中一个任务需要发出信号通知另一任务发生了某个特定事件,利用完成变量是使两个任务得以同步的简单方法。
在一个指定的完成变量上,需要等待的任务调用wait_for_completion()来等待特定事件。当特定事件发生后,产生事件的任务调用complete()来发送信号唤醒正在等待的任务。
顺序和屏障
当处理多处理器之间或硬件设备之间的同步问题时,有时需要在你的程序代码中以指定的顺序发出读内存和写内存指令。在和硬件交互时,时常需要确保一个给定的读操作发生在其他读或写操作之前。这些确保顺序的指令称作屏障。
rmb()方法提供了一个“读”内存屏障,在rmb()之前的载入操作不会被重新排在该调用之后,同理,在rmb()之后的载入操作不会被重新排在该调用之前。
wmb()方法与rmb()类似。
mb()方法既提供了读屏障也提供了写屏障。