linux 自旋锁和信号量
自旋锁最多只能被一个可执行线程持有(读写自旋锁除外)。自旋锁不会引起调用者睡眠,如果一个执行线程试图获得一个已经被持有的自旋锁,那么线程就会一直进行忙循环,一直等待下去(一直占用 CPU ),在那里看是否该自旋锁的保持者已经释放了锁, " 自旋 " 一词就是因此而得名。
由于自旋锁使用者一般保持锁时间非常短,因此选择自旋而不是睡眠是非常必要的,自旋锁的效率远高于互斥锁。
信号量和读写信号量适合于保持时间较长的情况,它们会导致调用者睡眠,因此只能在进程上下文使用(因为中断的上下文不允许休眠)( _trylock 的变种能够在中断上下文使用);而自旋锁 适合于保持时间非常短的情况,因为一个被争用的自旋锁使得请求它的线程在等待重新可用时自旋,特别浪费处理时间,这是自旋锁的要害之处,所以自旋锁不应该被长时间持有。在实际应用中自旋锁代码只有几行,而持有自旋锁的时间也一般不会超过两次上下方切换,因线程一旦要进行切换,就至少花费切出切入两次,自旋 锁的占用时间如果远远长于两次上下文切换,我们就可以让线程睡眠,这就失去了设计自旋锁的意义。
如果被保护的共享资源只在进程上下文访问,使用信号量保护该共享资源非常合适,如果对共享资源的访问时间非常短,自旋锁也可以。但是如果被保护的共享资源需要在中断上下文访问(包括底半部即中断处理句柄和顶半部即软中断),就必须使用自旋锁。
自旋锁保持期间是抢占失效的(内核不允许被抢占) ,而信号量和读写信号量保持期间是可以被抢占的。自旋锁只有在内核可抢占或 SMP 的情况下才真正需要,在单 CPU 且不可抢占的内核下,自旋锁的所有操作都是空操作。
(问题:在单 CPU 可抢占内核中,如果一个进程请求自旋锁,那么他一直占用 CPU 循环等待自旋锁可用,那么原来占用自旋锁的进程得不到使用 CPU 的机会,怎么释放它自己占用的自旋锁呢?
回答:这种情况应该是在实际中不会发生的。因为考虑最开始的情况,一个进程需要自旋锁,而且这个时候自旋锁没有被占用,所以他加锁,进入临界区。因为这个时候内核已经被自旋锁设置成了非抢占式,所以正在运行的进程不会被调用处 CPU ,因此也不会再有其他的进程来请求这个已经被占用的自旋锁,当临界区代码处理完成后,释放了自旋锁,此时内核又被自旋锁设置成了可抢占模式,这个时候其他的进程可以被调度,因此可以再请求自旋锁等操作。因此问题中的情况应该在实际中不会发生。
注意,如果在一个已经对某个自旋锁加锁的进程的临界区中又申请对这个自旋锁加锁,则会导致进程自旋在那里,引起死机。)
一个执行单元要想访问被自旋锁保护的共享资源,必须先得到锁,在访问完共享资源后,必须释放锁。如果在获取自旋锁时,没有任何执行单元保持该锁,那么将立即得到锁;如果在获取自旋锁时锁已经有保持者,那么获取锁操作将自旋在那里,直到该自旋锁的保持者释放了锁。
无论是互斥锁,还是自旋锁,在任何时刻,最多只能有一个保持者,也就说,在任何时刻最多只能有一个执行单元获得锁(读写锁除外)。自旋锁的实现和体系结构密切相关,代码一般通过汇编实现,定义在文件 , 实际用到的接口定义在文件夹中。
因为自旋锁在同一时刻至多被一个执行线程持有,所以一个时刻只能有一个线程位于临界区,这就为多处理器提供了防止并发访问所需的保护机制,但是在单处理器上,编译的时候不会加入自旋锁。它仅仅被当作一个设置内核抢占机制是否被启用的开关( gx 自己的理解:在单内核可抢占式内核中,对自旋锁加锁导致禁止抢占,对自旋锁解锁导致恢复抢占模式。)。 注意, Linux 内核实现的自旋锁是不可递归的,这一点不同于自旋锁 在其他操作系统中的实现,如果你想得到一个你正持有的锁,你必须自旋,等待你自己释放这个锁,但是你处于自旋忙等待中,所以永远没有机会释放锁,于是你就 被自己锁死了,一定要注意!
自旋锁可以用在中断处理程序中,但是在使用时一定要在获取锁之前,首先禁止本地中断(当前处理器上的中 断),否则中断处理程序就可能打断正持有锁的内核代码,有可能会试图争用这个已经被持有的自旋锁。这样一来,中断处理程序就会自旋,等待该锁重新可用, 但是锁的持有者在这个中断处理程序执行完毕之前不可能运行,这就会造成双重请求死锁。
自旋锁与下半部:由于下半部(中断程序下半部)可以抢占进程上下文中的代码,所以当下半部和进程上下文共享数据时,必须对进程上下文中的共享数据进行保护,所以需要加锁的同时还要禁止下半部执行。同样, 由于中断处理程序可以抢占下半部,所以如果中断处理程序和下半部共享数据,那么就必须在获取恰当的锁的同时还要禁止中断。对于软中断,无论是否同种类型, 如果数据被软中断共享,那么它必须得到锁的保护,因为同种类型的两个软中断也可以同进运行在一个系统的多个处理器上。但是,同一个处理器上的一个软中断绝 不会抢占另一个软中断( gx :因为在中断处理代码运行上下文中禁止响应同类型的中断?) ,因此,根本不需要禁止下半部。
自旋锁在内核中主要用来防止多处理器中并发访问临界区,防止内核抢占造成的竞争。 另外自旋锁不允许任务睡眠 ( 持有自旋锁的任务睡眠会造成自死锁 —— 因为睡眠有可能造成持有锁的内核任务被重新调度,而再次申请自己已持有的锁 ) ,它能够在中断上下文中使用 。
Lnux 中的信号量是一种睡眠锁。如果有一个任务试图获得一个已被持有的信号量时,信号量会将其推入等待队列,然后让其睡眠。这时处理器获得自由去执行其它代码。当持有信号量的进程将信号量释放后,在等待队列中的一个任务将被唤醒,从而便可以获得这个信号量。
信号量的睡眠特性,使得信号量适用于锁会被长时间持有的情况;只能在进程上下文中使用,因为中断上下文中是不能被调度的;另外当代码持有信号量时,不可以再持有自旋锁。
虽然听起来两者之间的使用条件复杂,其实在实际使用中信号量和自旋锁并不易混淆。注意以下原则 :
如果代码需要睡眠 —— 这往往是发生在和用户空间同步时 —— 使用信号量是唯一的选择。由于不受睡眠的限制,使用信号量通常来说更加简单一些。如果需要在自旋锁和信号量中作选择,应该取决于锁被持有的时间长短。理想情况是所有的锁都应该尽可能短的被持有,但是如果锁的持有时间较长的话,使用信号量是更好的选择。另外,信号量不同于自旋锁,它不会关闭内核抢占 ,所以持有信号量的代码可以被抢占。这意味者信号量不会对影响调度反应时间带来负面影响。
-------- 自旋锁对信号量 ------------------------------------------------------
需求 建议的加锁方法
低开销加锁 优先使用自旋锁
短期锁定 优先使用自旋锁
长期加锁 优先使用信号量
中断上下文中加锁 使用自旋锁
持有锁是需要睡眠、调度 使用信号量
原子操作、信号量、读写信号量和自旋锁:
http://kom118.blog.163.com/blog/static/476673182010312113630768/
自旋锁和信号量:
http://canlynet.blog.163.com/blog/static/2550136520091025069172/
自旋锁:一种当获取不到锁时,采用循环 “ 测试并设置 ” 的方式等待获取锁的一种互斥锁。
获取方法:采用 test-and-set 原子操作测试并设置某个内存变量。 —— 实现互斥的方法。
不被打断:通过禁止抢占实现。
使用场合:可在进程上下文和中断上下文使用。
特点:
1 、这种锁在获取不到时系统开销大,所以获取锁和释放锁的区间代码执行时间应该尽可能短。
2 、在单 CPU 内核可抢占系统中,自旋锁持有期间内核抢占被禁止。但在单 CPU 内核不支持抢占的系统,自旋锁退化为空操作。 —— 也就是说,如果持有锁的区间代码执行时间过长,会出现其它操作的不响应(假死机)现象。
3 、因为抢占被禁止,自旋锁可保证临界区不受 “ 别的 CPU 和本 CPU 内 ” 的抢占进程进程打扰。(但是可响应中断,如果连中断都不响应,需要特定的加锁函数)
4 、可能受到中断和底半部( BH) 的影响,为此,与开、关中断配合,为此有:
spin_lock_irq(),spin_unlock_irq()
spin_lock_irqsave(),spin_unlock_irqstore()
spin_lock_bh(),spin_unlock_bh()
具体含义请参考教材或网络搜索。
自旋锁的扩展: 1 、读写自旋锁; 2 、顺序锁; 3 、读 - 拷贝 - 更新( RCU)
******************************************************************
信号量:一种当获取不到锁时,采用睡眠以等待唤醒的方式的一种同步机制。
不被打断:通过何种方式实现没找到参考,但可推断为同自旋锁。
获取方法:同自旋锁。
适用场合:由于会导致睡眠,只能在进程上下文中使用。但是 int down_trylock(struct semphore *sem) 可以。
特点:
获取不到锁时,进入睡眠状态,系统开销为上下文切换的时间 Tsw 。
自旋锁和信号量的对比:
1 、当锁不能获取时,信号量开销为 Tsw (上下文切换时间),自旋锁开销为等待获取时间 Tcs ,这两个时间对比权衡使用哪种机制。
2 、信号量可用于保护包含可能引起阻塞的代码 ( 即保护的代码中有可引起睡眠的函数如copy_to_user(),copy_from_user()) ,自旋锁不能。自旋锁如果也使用这样的代码,当睡眠时另一个程进也要获取这把锁时,会 进入循环,而睡眠时间一般相对较长,系统资源开销大的时间过长时,资源耗尽的情况会发生,直到这个睡眠的进程被唤醒,代码执行完毕,资源才得以释放。至于 自旋锁区间睡眠引起死锁的情况我实在想不出来。但教材中都这么说。
3 、中断上下文中只能使用自旋锁,不可使用信号量。因为中断上下文中是不能被调度的,但睡眠后会发生上下文切换,需要调度,在中断上下文中睡眠只能永久睡眠 —— 死机!
如果一个函数需要一个锁,并且接着调用另外一个函数也试图请求这个锁,那么会导致代码死锁。
获得多个锁可能是危险的,然而,如果你有 2 个锁,称为 lock1 和 lock2 ,代码需要同时获取,你有一个潜在的死锁。这个问题的解决方法常常是简单的:当多个锁必须获得时,它们应当一直以同样的顺序获得,只要遵照这个惯例,像上面描述的简单的死锁能够避免。
如果你怀疑锁竞争在损坏性能,你可能发现 lockmeter 工具有用,这个补丁装备内核来测量在锁等待花费的时间,通过看这个报告你能够很快知道是否锁竞争真的是问题。
对于 2.6.10 ,有一个通用的环形缓存实现在内核中可用,如何使用它的信息看 <linux/kfifo.h> 。
加锁方式的几种选择:
1、 有时候一个共享资源是一个简单的整数值,对于这样的情况,内核提供了一个原子整数称为 atomic_t ,定义在 <asm/atomic.h> 中, atomic_t 的操作非常快,因为他们在认识可能时编译成一条机器指令。有一系列函数支持 atomic_t 操作。
Atomic_t 数据项必须通过这些函数存取,如果你传递一个原子项给一个期望一个整数参数的函数,你会得到一个编译错误。
2、 内核提供了一套函数来原子的修改或测试单个位,因为整个操作在单步内发生,没有中断能干扰。参见<asm/bitops.h> ,他们保证是原子的,及时在 SMP 上。
可以通过为操作来模拟锁,但是此中情况最好使用自旋锁,自旋锁很好的调试过,而且很好的处理了中断和内核抢占。
一段模拟锁的为操作代码:
/*try to set lock*/
While(test_and_set_bit(nr,addr) ! = 0)
Wait_for_a_while();
/*do your work*/
/*release lock,and check… */
If(test_and_clear_bit(nr,addr) == 0)
Something_went_wrong(); /*already release:error*/
为操作例子:
Static int main_init(void)
{
Unsigned long v = 0x00;
Unsigned log ret = test_and_rest_bit(0,&v);
Printk(KERN_INFO “%X,%X/n”,ret,v);
Return 0;
}
返回结果为 0,1 。
3、 信号量
4、 自旋锁
5、 Seqlock 机制,参见 <linux/seqlock.h>
6、 RCU( 读取 - 拷贝 - 更新 ) ,参见 <linux/rcupdate.h> 。在真实世界中使用 RCU 的例子如下: <1> 、网络路由表; <2> 、无线 IP 驱动。
Rcu_read_lock() 速度很快且禁止内核抢占,因此是原子的。
7、 Cpmpletion 完成机制。
原子操作:一个原子上下文只是一个状态,这里多个步骤必须在没有任何类型的并发存取的情况下进行。