第十章:内核同步方法

10.1 原子操作
同步方法中的原子操作是其他同步方法的基石;
原子操作可以保证指令以原子的方式执行------执行过程不被打断。
原子操作可以把读取和增加变量的行为包含在一个单步中执行,从而防止竞争的发生,保证了操作结果总是一致的;
两个原子操作不可能同时访问同一个变量。
 
linux内核提供了两组原子操作接口:一组针对整数进行操作,另一种针对单独的位进行操作。
 
10.1.1原子整数操作
针对整数的原子操作只能对atomic_t类型进行处理。
之所以引入一个特殊的类型,一方面让原子函数只能接收atomic_t数据类型,可以确保原子操作只能与这种特殊数据类型一起使用;另一方面也保证了该类型不会被传入其他任务非原子函数。
使用atomic_t类型确保编译器不会对相应的值进行访问优化--------这点使得原子操作最终接收到正确的内存地址,而不是一个别名。
原子整数操作最常用的用途是实现计数器;
还可以使用原子整数操作原子的执行一个操作并检查结果,一个常见的例子是原子的减操作并且检查。
 
原子操作与顺序的比较
一个字长的读取总是原子的发生,绝对不可能对同一个字交错的进行写:读总是返回一个完整的字,这或者发生在写之前,或者发生在写之后,绝对不可能发生在写的过程中。例如一个整数初始值是54,然后又置为76,那么读取这个整数肯定会返回54或者76,不会是其他值。
在代码上,可能要求读必须在待定的写之前发生,这种需求不属于原子性要求,而是顺序要求。原子性确保指令执行期间不被打断,要么全部执行,要么根本不执行。
另一方面, 顺序性确保即使两条或者多条指令出现在独立的执行线程中,甚至独立的处理器上,它们本该的执行顺序却依然要保持。
注意:在编写代码时,能使用原子操作时,就尽量不用复杂的加锁机制。
 
10.2自旋锁
临界区可以跨越多个函数,例如先从一个数据结构中移出数据,在对其进行格式转换和解析,最后在把它加入到另一个数据结构中。整个操作必须是原子的,在数据被更新完毕前,不能有其他代码读取这些数据。这种情况显然需要复杂的同步方法-------锁来提供保护、
.自旋锁最多只能有一个可执行的线程。如果另一个执行线程试图获得一个被已经持有的自旋锁,那么该线程将进行循环等待锁重新可用。如果锁未被争用,则该线程立即获得该锁。
等待自旋锁的线程,在等待锁重新可用时自旋(特别浪费处理器时间),这种行为是自旋锁的要点,所以自旋锁不应该被长时间持有。
自旋锁的初衷:在短时间内进行轻量级加锁。
还可以采用另一种方式来处理对锁的争用:让请求线程睡眠。 直到锁重新可用时再唤醒他。 但是让线程睡眠,缺点是需要两次明显的上下文切换,被阻塞的线程要换出和换入 ,与实现自旋锁的少量代码相比,上下文切换明有较多的代码。因此持有自旋锁的时间最好少于两次上下文切换的时间。
警告:自旋锁是不可以递归的。
使用锁的时候一定要对症药效,需要知道保护的是数据而不是代码,真正保护的是临界区的数据。
大原则:针对代码加锁会使得程序难以立即,并且容易引发竞争条件,正确的做法是针对数据加锁。
如果中断在加锁前是激活的,那么久不需要在解锁后恢复中断以前的状态。
 
10.3读--写自旋锁
锁的用途可以明确的分为读取和写入两个场景。
一个或者多个读锁可以并发的持有读锁,相反,用于写的锁最多只能有一个写任务持有,而且此时不能有并发的读操作。
当先执行读锁操作,后执行写锁操作时,写锁会不停的自旋,等待所有的读锁释放锁,其中也包括它自己,所以当需要写锁的时候,一开始就要请求写锁。
多个读锁可以安全的获得同一个读锁,事实上,集市一个线程递归的获得同一个读锁也是安全。
在使用Linux读-写自旋锁时, 这种锁机制照顾读比写要多一些 。当读锁被持有时,写操作作为互斥访问只能等待,但是,读锁却可以继续成功的获得读锁。 而自旋的写锁必须等待所有读锁释放锁后才可以获得锁。
特点:自旋锁提供了一种快速简单的锁实现方式,如果加锁时间不长并且代码不会睡眠,利用自旋锁时最佳选择。
如果加锁时间长或者代码在持有锁的时候有可能休眠,那么使用信号量来完成加锁功能。
 
10.4信号量
Linux中信号量是一种睡眠锁,如果一个任务试图获得一个不可用的信号量时,信号量会将其推进一个等待队列,然后让其睡眠。这时候处理器就重新获得自由,从而去执行其他代码。当持有的信号量可用时,处于等待队列中的那个任务将被唤醒,并获得信号量。
信号量比自旋锁的有点是:提供了更好的处理器利用率,因此没有把时间花费在忙等待上,但是,信号量比自旋锁有更大的开销。
从信号量的睡眠等待特性可以得出以下结论:
信号量适用于锁会被长时间持有的情况;
锁被短时间持有时,使用信号量就不太适合,因为睡眠、维护等待队列以及好唤醒所花费的开销可能比锁被占用的全部时间还要长。
由于执行线程在锁被争用时会睡眠,所有只能在进程上下文中才可以获得信号量锁,因为在中断上下文中时不可能进行调度的。
可以在获得信号量时进行休眠,不会导致其他其他试图获得锁的线程出现死锁情况。
在占用信号量时不能同时占用自旋锁,因为在持有信号量时可能会休眠,而在持有自旋锁时是不允许睡眠的。
如果需要在自旋锁和信号量之间做选择时,应该根据锁持有的时间长短来判断。
持有信号量的代码时可以被抢占的,这意味着信号量不会对调度的等待时间带来负面影响。
信号量可以同时允许任意数量的锁持有者,而自旋锁在一个时刻最多允许一个任务持有它。
 
10.5 读-写信号量
信号量也区分读、写访问。读-写信号量比读-写自旋锁更具优势。
所有的读写信号量都是互斥信号量,也就是说他们的引用计数器都等于1、
并发持有读锁的读操作数量不限。相反只有唯一一个写信号量可以获得写锁。所有读-写信号量的睡眠都不会被信号打断。
读-写信号量可以动态的将获得的写锁转换为读锁。
读-写信号量与读-写自旋锁一样,除非代码中的读-写可以分开,否则最好不要使用它。
 
10.6互斥体
互斥体(mutex)指的是任何可以睡眠的强制互斥锁,比如使用计数是1的信号量。
互斥体有如下特点:
任何时刻只有一个任务可以持有mutex;
给mutex上锁者必须负责给其解锁;
递归的上锁和解锁是不允许的。
当持有一个mutex时,进程不可以退出
mutex不能在中断和下半部中使用
mutex只能通过官方API管理。
10.6.1信号量与互斥体
除非mutex的某个约束妨碍你使用,否则相比信号量要优先使用互斥体。
10.6.2自旋锁和互斥体
 
需求 建议的加锁方法
低开销加锁 优先使用自旋锁
短期加锁 优选使用自旋锁
长期加锁 优选使用互斥体
中断上下文中加锁 使用自旋锁
持有锁需要睡眠 使用互斥体
 
10.9顺序锁
顺序锁,通常称为seq锁,这种锁提供了一种简单的机制,用户读写共享数据。实现这种锁主要依赖一个序列计数器。当有疑义的数据被写入时会等到锁,并且序列号会增阿基,在读取数据之前和之后,序列号都会被读取,如果读取的序列号值相同,说明在读操作进行的过程中没有被写操作打断过,此外如果读取的值时偶数,那么久表明写操作没有发生。(默认为0,写锁会使值变为奇数,释放的时候变为偶数。)
在多个读锁和少量写锁的共享一把锁时,seq锁有助于提供一种非常轻量级和具有可扩展新的外观,但是seq锁对写操作更有利,只要没有其他写操作,写锁总是能够被成功获得。读操作不会影响写锁。
seq锁在如下情况下是最理想的选择:
你的数据存在很多读操作
你的数据写操作很少
虽然写操作很好,但是你希望写操作优选与读,而且不允许读操作让写操作饥饿
你的数据很简单,如果简单结构,甚至是见得整型,在某些场合,你是不能给你使用原子量的。
 
10.11顺序和屏障
当处理多处理器和硬件设备之间的同步问题时,有时需要你程序代码中以指定的顺序发出读内存(读入)和写内存(存储)指令,
编译器和处理器为了提升效率,可能对读和写重新排序。Linux对所有可能的重排序和写的处理器提供了机器指令来确保顺序要求。同样也可以指示编译器不要对给定点周围的指令序列进行重新排序,这些确保顺序的指令称为屏障barrier。
例如在某些处理器上存在这样的代码:
a=1;
b=2;
有可能在a存放新值前就在b中存在新值。
前面的例子可能会被重新排序,但是对下面这样的不会进行重新排序:
a=1;
b=a;
a和b均为全局变量,因为a和b之间有明确的数据依赖关系。
Linux提供rmb方法提供了一个“读”内存屏障,它确保跨越rmb方法的载入动作不会发生重排序。也就是说rmb之前的载入操作不会被重新排在该调用之后,同理rmb之后的载入操作也不会被重排序在该调用之前。
wmb方法提供一个“写”内存屏障,它确保跨越屏障的存储不会发生重排序。
mb方法即提供了写内存屏障又提供了读内存屏障。
 
内存屏障barrier方法可以防止编译器跨越屏障对载入或者存储操作进行优化。
 

猜你喜欢

转载自www.cnblogs.com/use-D/p/10569027.html