详解Linux中的锁

详解Linux中的锁

今天呢,我们来讲讲锁,锁在实现同步与互斥中也是有着举足轻重的地位,下面我们先来看看锁的类型:

Linux内核提供了很多类型的锁,它们可以分为两类:

① 自旋锁(spinning lock);

② 睡眠锁(sleeping lock)。

接着来看看锁的一些函数:

从上面我们可以看出,其实信号量跟锁是非常相似的,我们来看看他们的区别:

semaphore中可以指定count为任意值,比如有10个厕所,所以10个人都可以使用厕所。

而mutex的值只能设置为1或0,只有一个厕所。

是不是把semaphore的值设置为1后,它就跟mutex一样了呢?不是的。

看一下mutex的结构体定义,如下:

它里面有一项成员“struct task_struct *owner”,指向某个进程。一个mutex只能在进程上下文中使用:谁给mutex加锁,就只能由谁来解锁。

而semaphore并没有这些限制,它可以用来解决“读者-写者”问题:程序A在等待数据──想获得锁,程序B产生数据后释放锁,这会唤醒A来读取数据。semaphore的锁定与释放,并不限定为同一个进程。

主要区别列表如下:

大致讲完了锁的类型以及他相关的一些内核函数,那么下面的话,我们就该来讲讲他的使用了,但是在这之前呢,我们先来讲讲为什么需要锁,我们来看看内核调度:

内核抢占(preempt)等额外的概念:

早期的的Linux内核是“不可抢占”的,假设有A、B两个程序在运行,当前是程序A在运行,什么时候轮到程序B运行呢?

① 程序A主动放弃CPU:

比如它调用某个系统调用、调用某个驱动,进入内核态后执行了schedule()主动启动一次调度。

② 程序A调用系统函数进入内核态,从内核态返回用户态的前夕:

这时内核会判断是否应该切换程序。

③ 程序A正在用户态运行,发生了中断:

内核处理完中断,继续执行程序A的用户态指令的前夕,它会判断是否应该切换程序。

从这个过程可知,对于“不可抢占”的内核,当程序A运行内核态代码时进程是无法切换的(除非程序A主动放弃),比如执行某个系统调用、执行某个驱动时,进程无法切换。

这会导致2个问题:

① 优先级反转:

一个低优先级的程序,因为它正在内核态执行某些很耗时的操作,在这一段时间内更高优先级的程序也无法运行。

② 在内核态发生的中断不会导致进程切换

为了让系统的实时性更佳,Linux内核引入了“抢占”(preempt)的功能:进程运行于内核态时,进程调度也是可以发生的。

回到上面的例子,程序A调用某个驱动执行耗时的操作,在这一段时间内系统是可以切换去执行更高优先级的程序。

对于可抢占的内核,编写驱动程序时要时刻注意:你的驱动程序随时可能被打断、随时是可以被另一个进程来重新执行。对于可抢占的内核,在驱动程序中要考虑对临界资源加锁。

讲清楚了为什么需要锁,下面我们就可以来看看他的应用场景了:

对于down_interruptible函数,如果信号量暂时无法获得,此函数会令程序进入休眠;别的程序调用up()函数释放信号量时会唤醒它。

在down_interruptible函数休眠过程中,如果进程收到了信号,则会从down_interruptible中返回;对应的有另一个函数down,在它休眠过程中会忽略任何信号。

注意:“信号量”(semaphore),不是“信号”(signal)。

也可以使用mutex,代码如下:

注意:一般来说在同一个函数里调用mutex_lock或mutex_unlock,不会长期持有它。这只是惯例,如果你使用mutex来实现驱动程序只能由一个进程打开,在drv_open中调用mutex_lock,在drv_close中调用mutex_unlock,这也完全没问题。

在用户上下文与Softirqs之间加锁:

假设这么一种情况:程序A运行到内核态时,正在访问一个临界资源;这时发生了某个硬件中断,在硬件中断处理完后会处理Softirq,而某个Softirq也会访问这个临界资源。

怎么办?

在程序A访问临界资源之前,干脆禁止Softirq好了!

可以使用spin_lock_bh函数,它会先禁止本地CPU的中断下半部即Softirq,这样本地Softirq就不会跟它竞争了;假设别的CPU也想获得这个资源,它也会调用spin_lock_bh禁止它自己的Softirq。这2个CPU都禁止自己的Softirq,然后竞争spinlock,谁抢到谁就先执行。可见,在执行临界资源的过程中,本地CPU的Softirq、别的CPU的Softirq都无法来抢占当前程序的临界资源。

释放锁的函数是spin_unlock_bh。

spin_lock_bh/spin_unlock_bh的后缀是“_bh”,表示“Bottom Halves”,中断下半部,这是软件中断的老名字。这些函数改名为spin_lock_softirq也许更恰当,请记住:spin_lock_bh会禁止Softirq,而不仅仅是禁止“中断下半部”(timer、tasklet里等都是Softirq,中断下半部只是Softirq的一种)。

示例代码如下:

硬中断上下文:

假设一个硬件中断服务例程与一个Softirq共享数据,需要考虑2点:

① Softirq执行的过程中,可能会被硬件中断打断;

② 临界区可能会被另一个CPU上的硬件中断进入。

怎么办?

在Softirq获得锁之前,禁止当前CPU的中断。

在硬件中断服务例程中不需要使用spin_lock_irq(),因为当它在执行的时间Softirq是不可能执行的;它可以使用spin_lock()用来防止别的CPU抢占。

如果硬件中断A、硬件中断B都要访问临界资源,怎么办?这篇文章里说要使用spin_lock_irq():

Unreliable Guide To Locking

但是我认为使用spin_lock()就足够了。因为Linux不支持中断嵌套,即当前CPU正在处理中断A时,中断B不可能在当前CPU上被处理,不需要再次去禁止中断;当前CPU正在处理中断A时,假如有另一个CPU正在处理中断B,它们使用spin_lock()实现互斥访问临界资源就可以了。

spin_lock_irq()/spin_unlock_irq()会禁止/使能中断,另一套函数是spin_lock_irqsave()/spin_unlock_irqrestore(),spin_lock_irqsave()会先保存当前中断状态(使能还是禁止),再禁止中断;spin_unlock_irqrestore()会恢复之前的中断状态(不一定是使能中断,而是恢复成之前的状态)。

篇幅有点长了,我们先讲到这里,下一篇,我们来具体讲讲自旋锁和信号量在内核中是怎么实现的(doge.)

🎊 相关推荐

8g内存卡多少钱(16g内存卡和8g的店里分别多少钱?)
《刺客信条影》再次开放预购:标准版价格未变,预购免费领首个DLC
一台电脑,如何安装多个JDK版本并保证jdk版本自由切换(多个jdk如何配置环境变量)