spinlock 自旋锁

自旋锁是专为防止多处理器并发而引入的一种锁,它在内核中大量应用于中断处理等部分(对于单处理器来说,防止中断处理中的并发可简单采用关闭中断的方式,即在标志寄存器中关闭/打开中断标志位,不需要自旋锁)。
何谓自旋锁?它是为实现保护共享资源而提出一种锁机制。其实,自旋锁与互斥锁比较类似,它们都是为了解决对某项资源的互斥使用。无论是互斥锁,还是自旋锁,在任何时刻,最多只能有一个保持者,也就说,在任何时刻最多只能有一个执行单元获得锁。但是两者在调度机制上略有不同。对于互斥锁,如果资源已经被占用,资源申请者只能进入睡眠状态。但是自旋锁不会引起调用者睡眠,如果自旋锁已经被别的执行单元保持,调用者就一直循环在那里看是否该自旋锁的保持者已经释放了锁,”自旋”一词就是因此而得名。
跟互斥锁一样,一个执行单元要想访问被自旋锁保护的共享资源,必须先得到锁,在访问完共享资源后,必须释放锁。如果在获取自旋锁时,没有任何执行单元保持该锁,那么将立即得到锁;如果在获取自旋锁时锁已经有保持者,那么获取锁操作将自旋在那里,直到该自旋锁的保持者释放了锁。由此我们可以看出,自旋锁是一种比较低级的保护数据结构或代码片段的原始方式,这种锁可能存在两个问题:
死锁。试图递归地获得自旋锁必然会引起死锁:递归程序的持有实例在第二个实例循环,以试图获得相同自旋锁时,不会释放此自旋锁。在递归程序中使用自旋锁应遵守下列策略:递归程序决不能在持有自旋锁时调用它自己,也决不能在递归调用时试图获得相同的自旋锁。此外如果一个进程已经将资源锁定,那么,即使其它申请这个资源的进程不停地疯狂“自旋”,也无法获得资源,从而进入死循环。
过多占用cpu资源。如果不加限制,由于申请者一直在循环等待,因此自旋锁在锁定的时候,如果不成功,不会睡眠,会持续的尝试,单cpu的时候自旋锁会让其它process动不了. 因此,一般自旋锁实现会有一个参数限定最多持续尝试次数. 超出后, 自旋锁放弃当前time slice. 等下一次机会。
由此可见,自旋锁比较适用于锁使用者保持锁时间比较短的情况。正是由于自旋锁使用者一般保持锁时间非常短,因此选择自旋而不是睡眠是非常必要的,自旋锁的效率远高于互斥锁。信号量和读写信号量适合于保持时间较长的情况,它们会导致调用者睡眠,因此只能在进程上下文使用,而自旋锁适合于保持时间非常短的情况,它可以在任何上下文使用。如果被保护的共享资源只在进程上下文访问,使用信号量保护该共享资源非常合适,如果对共享资源的访问时间非常短,自旋锁也可以。但是如果被保护的共享资源需要在中断上下文访问(包括底半部即中断处理句柄和顶半部即软中断),就必须使用自旋锁。自旋锁保持期间是抢占失效的,而信号量和读写信号量保持期间是可以被抢占的。自旋锁只有在内核可抢占或SMP(多处理器)的情况下才真正需要,在单CPU且不可抢占的内核下,自旋锁的所有操作都是空操作。
上面简要介绍了自旋锁的基本原理,以下将给出具体的例子,进一步阐释自旋锁在实际系统中的应用。上面我们已经讲过自旋锁只有在内核可抢占或SMP(多处理器)的情况下才真正需要,下面我们就以SMP为例,来说明为什么要使用自旋锁,以及自旋锁实现的基本算法。
实现编辑
在单处理机环境中可以使用特定的原子级汇编指令swap和test_and_set实现进程互斥,(Swap指令:交换两个内存单元的内容;test_and_set指令取出内存某一单元(位)的值,然后再给该单元(位)赋一个新值,关于为何这两条指令能实现互斥我们不在赘述,读者可以了解其算法) 这些指令涉及对同一存储单元的两次或两次以上操作,这些操作将在几个指令周期内完成,但由于中断只能发生在两条机器指令之间,而同一指令内的多个指令周期不可中断,从而保证swap指令或test_and_set指令的执行不会交叉进行.
但在多处理机环境中情况有所不同,例如test_and_set指令包括“取”、“送”两个指令周期,两个CPU执行test_and_set(lock)可能发生指令周期上的交叉,假如lock初始为0, CPU1和CPU2可能分别执行完前一个指令周期并通过检测(均为0),然后分别执行后一个指令周期将lock设置为1,结果都取回0作为判断临界区空闲的依据,从而不能实现互斥. 如图4-3所示.
为在多CPU环境中利用test_and_set指令实现进程互斥,硬件需要提供进一步的支持,以保证test_and_set指令执行的原子性. 这种支持目前多以“锁总线”(bus locking)的形式提供的,由于test_and_set指令对内存的两次操作都需要经过总线,在执行test_and_set指令之前锁住总线,在执行test_and_set指令后开放总线,即可保证test_and_set指令执行的原子性,用法如下:
算法4-6:多处理机互斥算法(自旋锁算法)
do{
b=1;
while(b){
lock(bus);
b = test_and_set(&lock);
unlock(bus);
}
临界区
lock = 0;
其余部分
}while(1)
总之,自旋锁是一种对多处理器相当有效的机制,而在单处理器非抢占式的系统中基本上没有作用。自旋锁在SMP系统中应用得相当普遍。在许多SMP系统中,允许多个处理机同时执行目态程序,而一次只允许一个处理机执行操作系统代码,利用一个自旋锁可以很容易实现这种控制.一次只允许一个CPU执行核心代码并发性不够高,若期望核心程序在多CPU之间的并行执行,将核心分为若干相对独立的部分,不同的CPU可以同时进入和执行核心中的不同部分,实现时可以为每个相对独立的区域设置一个自旋锁.
初衷编辑
事实上,自旋锁的初衷就是:在短期间内进行轻量级的锁定。一个被争用的自旋锁使得请求它的线程在等待锁重新可用的期间进行自旋(特别浪费处理器时间),所以自旋锁不应该被持有时间过长。如果需要长时间锁定的话, 最好使用信号量。
1自旋锁实际上是忙等锁
当锁不可用时,CPU一直循环执行“测试并设置”该锁直到可用而取得该锁,CPU在等待自旋锁时不做任何有用的工作,仅仅是等待。因此,只有在占用锁的时间极短的情况下,使用自旋锁才是合理的。当临界区很大或有共享设备的时候,需要较长时间占用锁,使用自旋锁会降低系统的性能。
自旋锁可能导致系统死锁
引发这个问题最常见的情况是递归使用一个自旋锁,即如果一个已经拥有某个自旋锁的CPU 想第二次获得这个自旋锁,则该CPU 将死锁。此外,如果进程获得自旋锁之后再阻塞,也有可能导致死锁的发生。copy_from_user()、copy_to_user()和kmalloc()等函数都有可能引起阻塞,因此在自旋锁的占用期间不能调用这些函数。代码清单7.2 给出了自旋锁的使用实例,它被用于实现使得设备只能被最多一个进程打开。
基本形式编辑
自旋锁的基本形式如下:
spin_lock(&mr_lock);
//临界区
spin_unlock(&mr_lock);
因为自旋锁在同一时刻只能被最多一个内核任务持有,所以一个时刻只有一个线程允许存在于临界区中。这点很好地满足了对称多处理机器需要的锁定服务。在单处理器上,自旋锁仅仅当作一个设置内核抢占的开关。如果内核抢占也不存在,那么自旋锁会在编译时被完全剔除出内核。
简单的说,自旋锁在内核中主要用来防止多处理器中并发访问临界区,防止内核抢占造成的竞争。另外自旋锁不允许任务睡眠(持有自旋锁的任务睡眠会造成自死锁——因为睡眠有可能造成持有锁的内核任务被重新调度,而再次申请自己已持有的锁),它能够在中断上下文中使用。
死锁:假设有一个或多个内核任务和一个或多个资源,每个内核都在等待其中的一个资源,但所有的资源都已经被占用了。这便会发生所有内核任务都在相互等待,但它们永远不会释放已经占有的资源,于是任何内核任务都无法获得所需要的资源,无法继续运行,这便意味着死锁发生了。自死琐是说自己占有了某个资源,然后自己又申请自己已占有的资源,显然不可能再获得该资源,因此就自缚手脚了。

自旋锁
Linux的的内核最常见的锁是自旋锁。自旋锁最多只能被一个可执行线程持有。如果一个执行线程试图获得一个被已经持有(争用)的自旋锁,那么该线程就会一直进行忙循环-旋转-等待锁重新可用要是锁未被争用,请求锁的执行线程就可以立即得到它,继续执行。



在任意时间,自旋锁都可以防止多于一个的执行线程同时进入临界区。


同一个锁可以用在多个位置,例如,对于给定数据的所有访问都可以得到保护和同步。






在Linux的2.6.11.12内核版本中,自旋锁的实现接口定义在包含\ linux的\ <spinlock.h>中,与体系结构相关的代码定义包含在\ ASM \ <spinlock.h>中。


基本结构
自旋锁的结构体是spinlock_t,



typedef struct {
/**
* 该字段表示自旋锁的状态,值为1表示未加锁,任何负数和0都表示加锁
*/
volatile unsigned int slock;
#ifdef CONFIG_DEBUG_SPINLOCK
unsigned magic;
#endif
#ifdef CONFIG_PREEMPT
/**
* 表示进程正在忙等待自旋锁。
* 只有内核支持SMP和内核抢占时才使用本标志。
*/
unsigned int break_lock;
#endif
} spinlock_t;
spin_lock()
/**



  • 当内核不可抢占时,spin_lock的实现过程。
    */
    #define _spin_lock(lock)

    do {

    /**

    • 调用preempt_disable禁用抢占。
      */
      preempt_disable();

      /**

    • _raw_spin_lock对自旋锁的slock字段执行原子性的测试和设置操作。
      */
      _raw_spin_lock(lock);

      __acquire(lock);

      } while(0)
      函数_raw_spin_lock()对自旋锁的SLOCK字段执行原子性的测试和设置操作。





#define _raw_spin_lock(x)

do {

CHECK_LOCK(x);

if ((x)->lock&&(x)->babble) {

(x)->babble–;

printk(“%s:%d: spin_lock(%s:%p) already locked by %s/%d\n”,

FILE,LINE, (x)->module,

(x), (x)->owner, (x)->oline);

}

(x)->lock = 1;

(x)->owner = FILE;

(x)->oline = LINE;

} while (0)



spin_unlock()
#define _spin_unlock(lock)

do {

_raw_spin_unlock(lock);

preempt_enable();

release(lock);

} while (0)
static inline void _raw_spin_unlock(spinlock_t *lock)
{
#ifdef CONFIG_DEBUG_SPINLOCK
BUG_ON(lock->magic != SPINLOCK_MAGIC);
BUG_ON(!spin_is_locked(lock));
#endif
__asm
volatile(
spin_unlock_string
);
}
宏函数spin_unlock_string
在spin_unlock_string中,%0即为锁 - > s 锁,movb指令将锁 - > s 锁定为1,movb指令本身就是原子操作,所以不需要锁总线。
#define spin_unlock_string

“movb $1,%0”

:”=m” (lock->slock) : : “memory”



自旋锁在同一时刻至多被一个执行线程持有,所以一个时刻只有一个线程位于临界区内,这就为多处理器机器提供了防止并发访问所需的保护机制。

在单处理机器上,编译的时候不会加入自旋锁,仅会被当作一个设置内核抢占机制是否被启用的开关。如果禁止内核抢占,那么在编译时自旋锁就会被剔除出内核。


内核提供的禁止中断同时请求锁的接口
(1)_spin_lock_irqsave()
保存中断的当前状态,并禁止本地中断,然后再去获取指定的锁。



unsigned long __lockfunc _spin_lock_irqsave(spinlock_t *lock)
{
unsigned long flags;



local_irq_save(flags);
preempt_disable();
_raw_spin_lock_flags(lock, flags);
return flags; } (2)_spin_unlock_irqrestore()
对指定的锁解锁,然后让中断恢复到加锁前的状态


void __lockfunc _write_unlock_irqrestore(rwlock_t *lock, unsigned long flags)
{
_raw_write_unlock(lock);
local_irq_restore(flags);
preempt_enable();
}



如果能确定中断在加锁前是激活的,那就不需要在解锁后恢复中断以前的状态。也就可以无条件地在解锁时激活中断。这时可以使用spin_lock_irq()和spin_unlock_irq()。


_spin_lock_irq()
禁止本地中断并获取指定的锁。
void __lockfunc _read_lock_irq(rwlock_t *lock)
{
local_irq_disable();
preempt_disable();
_raw_read_lock(lock);
}
_spin_unlock_irq()
释放指定的锁,并激活本地中断。



void __lockfunc _spin_unlock_irq(spinlock_t *lock)
{
_raw_spin_unlock(lock);
local_irq_enable();
preempt_enable();
}
在使用spin_lock_irq()方法时,需要确定中断原来是否处于激活状态。一般不建议使用。



spin_lock_init()
动态初始化指定的spinlock_t,(此时只有一个指向spinlock_t类型地指针,没有它的实体)



#define spin_lock_init(lock) do { (void)(lock); } while(0)



spin_try_lock()
试图获的某个特定的自旋锁。如果该锁已经被争用,那么该函数立即返回一个非0值,而不会自旋等待锁被释放;



如果成功地获得了这个自旋锁,该函数返回0。



int __lockfunc _spin_trylock(spinlock_t *lock)
{
preempt_disable(); //使抢占计数加1
if (_raw_spin_trylock(lock))
return 1;



preempt_enable();	// 使抢占计数减1,并在thread_info描述符的TIF_NEED_RESCHED标志被置为1的情况下,调用preempt_schedule()
return 0; }


spin_is_locked()
用于检查特定的锁当前是否已被占用,如果已被占用,返回非0值;否则返回0。



#define spin_is_locked(x)

({

CHECK_LOCK(x);

if ((x)->lock&&(x)->babble) {

(x)->babble–;

printk(“%s:%d: spin_is_locked(%s:%p) already locked by %s/%d\n”,

FILE,LINE, (x)->module,

(x), (x)->owner, (x)->oline);

}

0;

})






总结
(1) 一个被争用的自旋锁使得请求它的线程在等待锁重新可用时自旋,会特别浪费处理器时间。所以自旋锁不应该被长时间持有。因此,自旋锁应该使用在:短时间内进行轻量级加锁。



(2)还可以采取另外的方式来处理对锁的争用:让请求线程睡眠,直到锁重新可用时再唤醒它这样处理器不必循环等待,可以执行其他任务。



但是让请求线程睡眠的处理也会带来一定开销:会有两次上下文切换,被阻塞的线程要换出和换入所以,自旋持有锁的时间最好小于完成两次上下文e月刊的耗时,也就是让持有自旋锁的时间尽可能短。(在抢占式内核中,的锁持有等价于系统-的调度等待时间),信号量可以在发生争用时,等待的线程能投入睡眠,而不是旋转。


(3)在单处理机器上,自旋锁是无意义的。因为在编译时不会加入自旋锁,仅仅被当作一个设置内核抢占机制是否被启用的开关。如果禁止内核抢占,那么在编译时自旋锁会被完全剔除出内核。



(4)Linux内核中,自旋锁是不可递归的。如果试图得到一个你正在持有的锁,你必须去自旋,等待你自己释放这个锁。但这时你处于自旋忙等待中,所以永远不会释放锁,就会造成死锁现象。



(5)在中断处理程序中,获取锁之前一定要先禁止本地中断(当前处理器的中断),否则,中断程序就会打断正持有锁的内核代码,有可能会试图去争用这个已经被持有的自旋锁。这样就会造成双重请求死锁(中断处理程序会自旋,等待该锁重新可用,但锁的持有者在这个处理程序执行完之前是不可能运行的)



(6)锁真正保护的是数据(共享数据),而不是代码。对于BLK(大内核锁)保护的是代码。



补充:
BLK:大内核锁
BLK是一个全局自旋锁,主要目的是使Linux的最初的SMP过渡到细粒度加锁机制。



特性如下:



·持有BLK的任务可以睡眠的,是安全的。因为当任务无法被调度时,所加锁会自动被丢弃;当任务被调度时,锁会被重新获得。



·BLK是一种递归锁。一个进程可以多次请求一个锁,而不会像自旋锁那样造成死锁现象。



·BLK只可以用在进程上下文中。不同于自旋锁可在中断上下文中加锁。



·BLK锁保护的是代码。
一:自旋锁(spin lock)
自旋锁是一种用于保护多线程共享资源的锁,与一般互斥锁(mutex)不同之处在于当自旋锁尝试获取锁时以忙等待(busy waiting)的形式不断地循环检查锁是否可用。
在多CPU的环境中,对持有锁较短的程序来说,使用自旋锁代替一般的互斥锁往往能够提高程序的性能。



最后标红的句子很重要,本文将针对该结论进行验证。

下面是man手册中对自旋锁pthread_spin_lock()函数的描述:


DESCRIPTION
The pthread_spin_lock() function shall lock the spin lock referenced by lock. The calling thread shall acquire the lock if
it is not held by another thread. Otherwise, the thread shall spin (that is, shall not return from the pthread_spin_lock()
call) until the lock becomes available. The results are undefined if the calling thread holds the lock at the time the
call is made. The pthread_spin_trylock() function shall lock the spin lock referenced by lock if it is not held by any
thread. Otherwise, the function shall fail.



   The results are undefined if any of these functions is called with an uninitialized spin lock.

可以看出,自选锁的主要特征:当自旋锁被一个线程获得时,它不能被其它线程获得。如果其他线程尝试去phtread_spin_lock()获得该锁,那么它将不会从该函数返回,而是一直自旋(spin),直到自旋锁可用为止。

使用自旋锁时要注意: 由于自旋时不释放CPU,因而持有自旋锁的线程应该尽快释放自旋锁,否则等待该自旋锁的线程会一直在哪里自旋,这就会浪费CPU时间。 持有自旋锁的线程在sleep之前应该释放自旋锁以便其他咸亨可以获得该自旋锁。内核编程中,如果持有自旋锁的代码sleep了就可能导致整个系统挂起。(下面会解释)
使用任何锁都需要消耗系统资源(内存资源和CPU时间),这种资源消耗可以分为两类:
1.建立锁所需要的资源
2.当线程被阻塞时所需要的资源


POSIX提供的与自旋锁相关的函数有以下几个,都在中。



int pthread_spin_init(pthread_spinlock_t *lock, int pshared);
初始化spin lock, 当线程使用该函数初始化一个未初始化或者被destroy过的spin lock有效。该函数会为spin lock申请资源并且初始化spin lock为unlocked状态。
有关第二个选项是这么说的:
If the Thread Process-Shared Synchronization option is supported and the value of pshared is PTHREAD_PROCESS_SHARED, the
implementation shall permit the spin lock to be operated upon by any thread that has access to the memory where the spin
lock is allocated, even if it is allocated in memory that is shared by multiple processes.



   If the Thread Process-Shared Synchronization option is supported and the value of pshared is PTHREAD_PROCESS_PRIVATE, or if
the option is not supported, the spin lock shall only be operated upon by threads created within the same process as the
thread that initialized the spin lock. If threads of differing processes attempt to operate on such a spin lock, the behav‐
ior is undefined.


所以,如果初始化spin lock的线程设置第二个参数为PTHREAD_PROCESS_SHARED,那么该spin lock不仅被初始化线程所在的进程中所有线程看到,而且可以被其他进程中的线程看到,PTHREAD_PROESS_PRIVATE则只被同一进程中线程看到。如果不设置该参数,默认为后者。



int pthread_spin_destroy(pthread_spinlock_t *lock);
销毁spin lock,作用和mutex的相关函数类似,就不翻译了:
The pthread_spin_destroy() function shall destroy the spin lock referenced by lock and release any resources used by the
lock. The effect of subsequent use of the lock is undefined until the lock is reinitialized by another call to
pthread_spin_init(). The results are undefined if pthread_spin_destroy() is called when a thread holds the lock, or if this
function is called with an uninitialized thread spin lock.
不过和mutex的destroy函数一样有这样的性质(当初害惨了我):
The result of referring to copies of that object in calls to pthread_spin_destroy(), pthread_spin_lock(), pthread_spin_try‐
lock(), or pthread_spin_unlock() is undefined.



int pthread_spin_lock(pthread_spinlock_t *lock);
加锁函数,功能上文都说过了,不过这么一点值得注意:
EBUSY A thread currently holds the lock.
These functions shall not return an error code of [EINTR].



int pthread_spin_trylock(pthread_spinlock_t *lock);
还有这个函数,这个一般很少用到。



int pthread_spin_unlock(pthread_spinlock_t *lock);
解锁函数。不是持有锁的线程调用或者解锁一个没有lock的spin lock这样的行为都是undefined的。
二:自旋锁和互斥锁的区别
从实现原理上来讲,Mutex属于sleep-waiting类型的 锁。例如在一个双核的机器上有两个线程(线程A和线程B),它们分别运行在Core0和Core1上。假设线程A想要通过 pthread_mutex_lock操作去得到一个临界区的锁,而此时这个锁正被线程B所持有,那么线程A就会被阻塞(blocking),Core0 会在此时进行上下文切换(Context Switch)将线程A置于等待队列中,此时Core0就可以运行其他的任务(例如另一个线程C)而不必进行忙等待。而Spin lock则不然,它属于busy-waiting类型的锁,如果线程A是使用pthread_spin_lock操作去请求锁,那么线程A就会一直在 Core0上进行忙等待并不停的进行锁请求,直到得到这个锁为止。
如果大家去查阅Linux glibc中对pthreads API的实现NPTL(Native POSIX Thread Library) 的源码的话(使用”getconf GNU_LIBPTHREAD_VERSION”命令可以得到我们系统中NPTL的版本号),就会发现pthread_mutex_lock()操作如果 没有锁成功的话就会调用system_wait()的系统调用并将当前线程加入该mutex的等待队列里。而spin lock则可以理解为在一个while(1)循环中用内嵌的汇编代码实现的锁操作(印象中看过一篇论文介绍说在linux内核中spin lock操作只需要两条CPU指令,解锁操作只用一条指令就可以完成)。有兴趣的朋友可以参考另一个名为sanos的微内核中pthreds API的实现:mutex.c spinlock.c,尽管与NPTL中的代码实现不尽相同,但是因为它的实现非常简单易懂,对我们理解spin lock和mutex的特性还是很有帮助的。
对于自旋锁来说,它只需要消耗很少的资源来建立锁;随后当线程被阻塞时,它就会一直重复检查看锁是否可用了,也就是说当自旋锁处于等待状态时它会一直消耗CPU时间。
对于互斥锁来说,与自旋锁相比它需要消耗大量的系统资源来建立锁;随后当线程被阻塞时,线程的调度状态被修改,并且线程被加入等待线程队列;最后当锁可用 时,在获取锁之前,线程会被从等待队列取出并更改其调度状态;但是在线程被阻塞期间,它不消耗CPU资源。
因此自旋锁和互斥锁适用于不同的场景。自旋锁适用于那些仅需要阻塞很短时间的场景,而互斥锁适用于那些可能会阻塞很长时间的场景。



四:自旋锁与linux内核进程调度关系



现在我们就来说一说之前的问题,如果临界区可能包含引起睡眠的代码则不能使用自旋锁,否则可能引起死锁:
那么为什么信号量保护的代码可以睡眠而自旋锁会死锁呢?

先看下自旋锁的实现方法吧,自旋锁的基本形式如下:
spin_lock(&mr_lock):

//critical region

spin_unlock(&mr_lock);
跟踪一下spin_lock(&mr_lock)的实现

#define spin_lock(lock) _spin_lock(lock)
#define _spin_lock(lock) __LOCK(lock)
#define __LOCK(lock) \
do { preempt_disable(); __acquire(lock); (void)(lock); } while (0)

注意到“preempt_disable()”,这个调用的功能是“关抢占”(在spin_unlock中会重新开启抢占功能)。从中可以看出,使用自旋锁保护的区域是工作在非抢占的状态;即使获取不到锁,在“自旋”状态也是禁止抢占的。了解到这,我想咱们应该能够理解为何自旋锁保护 的代码不能睡眠了。试想一下,如果在自旋锁保护的代码中间睡眠,此时发生进程调度,则可能另外一个进程会再次调用spinlock保护的这段代码。而我们 现在知道了即使在获取不到锁的“自旋”状态,也是禁止抢占的,而“自旋”又是动态的,不会再睡眠了,也就是说在这个处理器上不会再有进程调度发生了,那么 死锁自然就发生了。

总结下自旋锁的特点: 单CPU非抢占内核下:自旋锁会在编译时被忽略(因为单CPU且非抢占模式情况下,不可能发生进程切换,时钟只有一个进程处于临界区(自旋锁实际没什么用了) 单CPU抢占内核下:自选锁仅仅当作一个设置抢占的开关(因为单CPU不可能有并发访问临界区的情况,禁止抢占就可以保证临街区唯一被拥有) 多CPU下:此时才能完全发挥自旋锁的作用,自旋锁在内核中主要用来防止多处理器中并发访问临界区,防止内核抢占造成的竞争。


五:linux发生抢占的时间
linux抢占发生的时间,抢占分为用户抢占和内核抢占。



用户抢占在以下情况下产生: 从系统调用返回用户空间 从中断处理程序返回用户空间
内核抢占会发生在: 当从中断处理程序返回内核空间的时候,且当时内核具有可抢占性 当内核代码再一次具有可抢占性的时候(如:spin_unlock时) 如果内核中的任务显示的调用schedule() (这个我暂时不太懂)
基本的进程调度就是发生在时钟中断后,并且发现进程的时间片已经使用完了,则发生进程抢占。通常我们会利用中断处理程序返回内核空间的时候可进行内核抢占这个特性来提高一些I/O操作的实时性,如:当I/O事件发生的时候,对应的中断处理程序被激活,当它发现有进程在等待这个I/O事件的时候,它 会激活等待进程,并且设置当前正在执行进程的need_resched标志,这样在中断处理程序返回的时候,调度程序被激活,原来在等待I/O事件的进程 (很可能)获得执行权,从而保证了对I/O事件的相对快速响应(毫秒级)。可以看出,在I/O事件发生的时候,I/O事件的处理进程会抢占当前进程,系统 的响应速度与调度时间片的长度无关。


六:spin_lock和mutex实际效率对比
1.++i是否需要加锁?
我分别使用POSIX的spin_lock和mutex写了两个累加的程序,启动了两个线程,并利用时间戳计算它们执行完累加所用的时间。



下面这个是使用spin_lock的代码,我启动两个线程同时对num进行++,使用spin_lock保护临界区,实际上可能会有疑问++i(++i和++num本文中是一个意思)为什么还要加锁?
i++需要加锁是很明显的事情,对i++的操作的印象是,它一般是三步曲,从内存中取出i放入寄存器中,在寄存器中对i执行inc操作,然后把i放回内存中。这三步明显是可打断的,所以需要加锁。
但是++i可能就有点犹豫了。实际上印象流是不行的,来看一下i++和++i的汇编代码,其实他们是一样的,都是三步,我只上一个图就行了,如下:

所以++i也不是原子操作,在多核的机器上,多个线程在读取内存中的i时,可能读取到同一个值,这就导致多个线程同时执行+1,但实际上它们得到的结果是一样的,即i只加了一次。还有一点:这几句汇编正说明了++i和i++i对于效率是一样的,不过这只是针对内建POD类型而言,如果是class的话,我们都写过类的++运算符的重载,如果一个类在单个语句中不写++i,而是写i++的话,那无疑效率会有很大的损耗。(有点跑题) 1、自旋锁:


采用让当前线程不停的在循环体内执行实现,当循环的条件被其它线程改变时才能进入临界区
举例如下
优缺点分析:
由于自旋锁只是将当前线程不停地执行循环体,不进行线程状态的改变,所以响应速度更快。但当线程数不停增加时,性能下降明显,因为每个线程都需要执行,占用CPU时间。如果线程竞争不激烈,并且保持锁的时间段。适合使用自旋锁。
2、阻塞锁:



阻塞锁改变了线程的运行状态,让线程进入阻塞状态进行等待,当获得相应的信号(唤醒或者时间)时,才可以进入线程的准备就绪状态,转为就绪状态的所有线程,通过竞争,进入运行状态。
优缺点分析:
阻塞锁的优势在于,阻塞的线程不会占用cpu时间,不会导致 CPu占用率过高,但进入时间以及恢复时间都要比自旋锁略慢。在竞争激烈的情况下 阻塞锁的性能要明显高于自旋锁。
3、重入锁:
Java中的synchronized同步块是可重入的。这意味着如果一个java线程进入了代码中的synchronized同步块,并因此获得了该同步块使用的同步对象对应的管程上的锁,那么这个线程可以进入由同一个管程对象所同步的另
一个java代码块。
ReentrantLock与synchronized比较:



前者使用灵活,但是必须手动开启和释放锁
前者扩展性好,有时间锁等候(tryLock( )),可中断锁等候(lockInterruptibly( )),锁投票等,适合用于高度竞争锁和多个条件变量的地方
前者提供了可轮询的锁请求,可以尝试去获取锁(tryLock( )),如果失败,则会释放已经获得的锁。有完善的错误恢复机制,可以避免死锁的发生。
优缺点分析:
可重入锁的最大优点就是可以避免死锁。缺点是必须手动开启和释放锁。


Category linux