linux锁的介绍和使用 -04
本节参考:
https://www.kernel.org/doc/html/latest/locking/index.html
https://mirrors.edge.kernel.org/pub/linux/kernel/people/rusty/kernel-locking/
锁的类型
Linux内核提供了很多类型的锁,它们可以分为两类:(锁都会利用前面介绍的原子操作)
① 自旋锁(spinning lock);
② 睡眠锁(sleeping lock)。
自旋锁
简单地说就是无法获得锁时,不会休眠,会一直循环等待。有这些自旋锁:
自旋锁的加锁、解锁函数是:spin_lock、spin_unlock,还可以加上各种后缀,这表示在加锁或解锁的同时,还会做额外的事情:
睡眠锁
简单地说就是无法获得锁时,当前线程就会休眠。有这些休眠锁:
锁的内核函数
自旋锁
spinlock函数在内核文件include\linux\spinlock.h中声明,如下表:
自旋锁的加锁、解锁函数是:spin_lock、spin_unlock,还可以加上各种后缀,这表示在加锁或解锁的同时,还会做额外的事情:
信号量semaphore
semaphore函数在内核文件include\linux\semaphore.h中声明,如下表:(down()休眠的过程不会被唤醒)
互斥量mutex
mutex函数在内核文件include\linux\mutex.h中声明,如下表:
semaphore和mutex的区别
semaphore中可以指定count为任意值,比如有10个厕所,所以10个人都可以使用厕所。
而mutex的值只能设置为1或0,只有一个厕所。
是不是把semaphore的值设置为1后,它就跟mutex一样了呢?不是的。
看一下mutex的结构体定义,如下:
上图中“struct task_struct *owner”,指向某个进程。描述错误,
owner并不一定存在!
owner有2个用途:debug(CONFIG_DEBUG_MUTEXES)或spin_on_owner(CONFIG_MUTEX_SPIN_ON_OWNER)。
什么叫spin on owner?
我们使用mutex的目的一般是用来保护一小段代码,这段代码运行的时间很快。这意味着一个获得mutex的进程,可能很快就会释放掉mutex。
针对这点可以进行优化,特别是当前获得mutex的进程是在别的CPU上运行、并且“我”是唯一等待这个mutex的进程。在这种情况下,那“我”就原地spin等待吧:懒得去休眠了,休眠又唤醒就太慢了。
所以,mutex是做了特殊的优化,比semaphore效率更高。但是在代码上,并没有要求“谁获得mutex,就必须由谁释放mutex”,只是在使用惯例上是“谁获得mutex,就必须由谁释放mutex”。
而semaphore并没有这些限制,它可以用来解决“读者-写者”问题:程序A在等待数据──想获得锁,程序B产生数据后释放锁,这会唤醒A来读取数据。semaphore的锁定与释放,并不限定为同一个进程。
主要区别列表如下:(关于mutex的锁加锁,谁释放的描述是错误的)
何时用何种锁
本节参考:https://wenku.baidu.com/view/26adb3f5f61fb7360b4c656e.html
英文原文:https://mirrors.edge.kernel.org/pub/linux/kernel/people/rusty/kernel-locking/
你可能看不懂下面这个表格,请学习完后面的章节再回过头来看这个表格。
举例简单介绍一下,上表中第一行“IRQ Handler A”和第一列“Softirq A”的交叉点是“spin_lock_irq()”,意思就是说如果“IRQ Handler A”和“Softirq A”要竞争临界资源,那么需要使用“spin_lock_irq()”函数。为什么不能用spin_lock而要用spin_lock_irq?也就是为什么要把中断给关掉?假设在Softirq A中获得了临界资源,这时发生了IRQ A中断,IRQ Handler A去尝试获得自旋锁,这就会导致死锁:所以需要关中断。
内核抢占(preempt)等额外的概念
早期的的Linux内核是“不可抢占”的,假设有A、B两个程序在运行,当前是程序A在运行,什么时候轮到程序B运行呢?
① 程序A主动放弃CPU:
比如它调用某个系统调用、调用某个驱动,进入内核态后执行了schedule()主动启动一次调度。
② 程序A调用系统函数进入内核态,从内核态返回用户态的前夕:
这时内核会判断是否应该切换程序。
③ 程序A正在用户态运行,发生了中断:
内核处理完中断,继续执行程序A的用户态指令的前夕,它会判断是否应该切换程序。
从这个过程可知,对于“不可抢占”的内核,当程序A运行内核态代码时进程是无法切换的(除非程序A主动放弃),比如执行某个系统调用、执行某个驱动时,进程无法切换。
这会导致2个问题:
① 优先级反转:
一个低优先级的程序,因为它正在内核态执行某些很耗时的操作,在这一段时间内更高优先级的程序也无法运行。
② 在内核态发生的中断不会导致进程切换
为了让系统的实时性更佳,Linux内核引入了“抢占”(preempt)的功能:进程运行于内核态时,进程调度也是可以发生的。
回到上面的例子,程序A调用某个驱动执行耗时的操作,在这一段时间内系统是可以切换去执行更高优先级的程序。
对于可抢占的内核,编写驱动程序时要时刻注意:你的驱动程序随时可能被打断、随时是可以被另一个进程来重新执行。对于可抢占的内核,在驱动程序中要考虑对临界资源加锁。
使用场景
本节参考:https://wenku.baidu.com/view/26adb3f5f61fb7360b4c656e.html
英文原文:https://mirrors.edge.kernel.org/pub/linux/kernel/people/rusty/kernel-locking/
1 只在用户上下文加锁
假设只有程序A、程序B会抢占资源,这2个程序都是可以休眠的,所以可以使用信号量,代码如下:
对于down_interruptible函数,如果信号量暂时无法获得,此函数会令程序进入休眠;别的程序调用up()函数释放信号量时会唤醒它。
在down_interruptible函数休眠过程中,如果进程收到了信号,则会从down_interruptible中返回;对应的有另一个函数down,在它休眠过程中会忽略任何信号。
注意:“信号量”(semaphore),不是“信号”(signal)。
也可以使用mutex,代码如下:
注意:一般来说在同一个函数里调用mutex_lock或mutex_unlock,不会长期持有它。这只是惯例,如果你使用mutex来实现驱动程序只能由一个进程打开,在drv_open中调用mutex_lock,在drv_close中调用mutex_unlock,这也完全没问题。
2 在用户上下文与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的一种)。
示例代码如下:
3 在用户上下文与Tasklet之间加锁
Tasklet也是Softirq的一种,所以跟前面是“在用户上下文与Softirqs之间加锁”完全一样。
4 在用户上下文与Timer之间加锁
Timer也是Softirq的一种,所以跟前面是“在用户上下文与Softirqs之间加锁”完全一样。
5 在Tasklet与Timer之间加锁
假设在Tasklet中访问临界资源,另一个CPU会不会同时运行这个Tasklet?不会的,所以如果只是在某个Tasklet中访问临界资源,无需上锁。
假设在Timer中访问临界资源,另一个CPU会不会同时运行这个timer?不会的,所以如果只是在某个Timer中访问临界资源,无需上锁。
如果在有2个不同的Tasklet或Timer都会用到一个临界资源,那么可以使用spin_lock()、spin_unlock()来保护临界资源。不需要用spin_lock_bh(),因为一旦当前CPU已经处于Tasklet或Timer中,同一个CPU不会同时再执行其他Tasklet或Timer。
6 在Softirq之间加锁
这里讲的softirq不含tasklet、timer。
同一个Softirq是有可能在不同CPU上同时运行的,所以可以使用spin_lock()、spin_unlock()来访问临界区。如果追求更高的性能,可以使用“per-CPU array”,本章不涉及。
不同的Softirq之间,可以使用spin_lock()、spin_unlock()来访问临界区。
总结起来,在Softirq之间(含timer、tasklet、相同的Softirq、不同的Softirq),都可以使用spin_lock()、spin_unlock()来访问临界区。
示例代码如下:
7 硬中断上下文
假设一个硬件中断服务例程与一个Softirq共享数据,需要考虑2点:
① Softirq执行的过程中,可能会被硬件中断打断;
② 临界区可能会被另一个CPU上的硬件中断进入。
怎么办?
在Softirq获得锁之前,禁止当前CPU的中断。
在硬件中断服务例程中不需要使用spin_lock_irq(),因为当它在执行的时间Softirq是不可能执行的;它可以使用spin_lock()用来防止别的CPU抢占。
如果硬件中断A、硬件中断B都要访问临界资源,怎么办?这篇文章里说要使用spin_lock_irq():
https://mirrors.edge.kernel.org/pub/linux/kernel/people/rusty/kernel-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()会恢复之前的中断状态(不一定是使能中断,而是恢复成之前的状态)。
示例代码如下:
写在最后:这个链接是一篇很好的文档,以后我们会完全翻译出来,现在讲的知识暂时够用了。
https://mirrors.edge.kernel.org/pub/linux/kernel/people/rusty/kernel-locking/