我们知道,在Linux内核中,为了防止竞争的产生,需要加锁。我们最常见的是两种锁,信号量的semphore和自旋锁spin_lock。
semaphore
信号量semaphore这个锁是经常在进程中加上的。那么,当加上这个锁的时候,进程之间仍然可以互相切换。
举一个例子,第一个进程拿到了semaphore,进入到了临界区。在临界区内,第一个进程被中断。在中断处理函数中,不能再次使用这个semaphore了。因为肯定这个中断处理函数是不能拿到这个semaphore锁的,肯定会引起死锁。这还不是最后的结果。因为拿不到锁,当前就会进入到休眠状态。在中断处理函数中出现休眠是linux内核代码中一个禁忌。如果在中断上下文中休眠,那么,将不会再有机会重新调度,也不再会被唤醒,也就是说系统将会死在中断中。所以说,中断处理函数不得出现可能引起休眠的操作。比如使用semaphore锁,使用kmalloc这样的函数调用。
spinlock_t
那么,如果我们想在中断处理函数中使用锁时,应当使用什么锁呢?linux系统给我提供了不会引起休眠的锁,他就是spinlock_t这个锁。spinlock_t锁叫做自旋锁。
我们仍然举上一个例子。在一个进程中已经获得了spinlock_t锁。当前进程被中断,在中断处理函数中,再次去取得spinlock_t,可以吗?
是否可以,我们得看spinlock_t锁的机制。这个锁其实是在多处理器系统中使用的。在中断处理函数中,常常多出使用spinlock_t。在第一个cpu的中断处理函数中取得锁,第二个cpu接着运行了相同的中断服务函数,那么第二个cpu也会获得锁。那么,他是不能拿到这个锁的。会不会引起死锁呢?不会。因为这里有两个cpu。第二个cpu会进入到循环代码中,他会不断的查询spinlock_t的值,并一直等待到第一个cpu释放了spinlock_t之后。
那么,如果我们不是多处理器系统,而是单处理器系统。这个时候spinlock_t还能起什么作用呢?这个时候spinlock_t是禁止本地内核抢占功能的作用。也就是说,我们在上文中举得例子,肯定会出现死锁的。所以说我们在内核中断处理函数中是否使用spin_lock函数,应当考虑这个锁是锁的多个cpu之间的关系,而不是进程与中断的关系。
那么,我们在进程相互之间是否应该使用spinlock_t这个锁呢?当然是可以的。并且因为spinlock_t是的作用是禁止内核抢占的功能。如果使用spin_lock_irq_save函数还会禁止掉本地中断。也就是说拿到锁之后,进入到临界区之后,这个临界区,将不会被切除出去,也不会进入中断处理函数中。也就是说我们在进程中使用spinlock_t锁的时候,此时,我们会一直在这个区域执行。此时的抢占性内核摇身一变成为了非抢占性内核。因此,我们可以说,spinlock自旋锁在单处理器器系统下,是使用禁止内核抢占的方案,来避免竞争的。
锁的本质
也许,面对semaphore这个锁,我们说锁的本质是使用引用计数来实现。但是spinlock_t锁却不是这样的。
因此,我们不能认为锁就是设置使用计数这样的简单方案。加锁的机制,目的是为了防止竞争条件,而加锁是真正的从引起竞争的根源来解决问题。而不是单单的使用整数计数来堵这个竞争条件。
锁的用法
有上述的分析,在可休眠的场景下,我们可使用semaphore锁。但是semaphore不但使用了引用计数,还有队列。用来管理当前进程。当无法获得锁的时候,进程进入休眠状态,进入到队列中。当被唤醒时,将从队列中被唤醒,就完成获得锁。因此,semaphore的消耗比较大。当临界区较小时,不推荐使用。但是,对等待外部设备获得数据时,还是常常使用semaphore锁的,这也是经典用法。
spinlock_t锁用在保护小段的临界区中。spinlock_t的使用在多处理器中也适用。在中断处理函数中也适用。但是,不能再中断处理函数和外部进程中搭配使用,如果使用spin_lock这个上锁函数,则会引起死锁。