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_lockspin_unlock,还可以加上各种后缀,这表示在加锁或解锁的同时,还会做额外的事情:

 

睡眠锁

简单地说就是无法获得锁时,当前线程就会休眠。有这些休眠锁:

 

锁的内核函数

自旋锁

spinlock函数在内核文件include\linux\spinlock.h中声明,如下表:

 

自旋锁的加锁、解锁函数是:spin_lockspin_unlock,还可以加上各种后缀,这表示在加锁或解锁的同时,还会做额外的事情:

 

信号量semaphore

semaphore函数在内核文件include\linux\semaphore.h中声明,如下表:(down()休眠的过程不会被唤醒)

 

互斥量mutex

mutex函数在内核文件include\linux\mutex.h中声明,如下表:

 

semaphoremutex的区别

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

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

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

 

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

 

上图中“struct task_struct *owner”,指向某个进程。描述错误,

owner并不一定存在!

owner2个用途: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_lockmutex_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、别的CPUSoftirq都无法来抢占当前程序的临界资源。

释放锁的函数是spin_unlock_bh。

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

示例代码如下:

 

3 在用户上下文与Tasklet之间加锁

Tasklet也是Softirq的一种,所以跟前面是“在用户上下文与Softirqs之间加锁”完全一样。

 

4 在用户上下文与Timer之间加锁

Timer也是Softirq的一种,所以跟前面是“在用户上下文与Softirqs之间加锁”完全一样。

 

5 TaskletTimer之间加锁

假设在Tasklet中访问临界资源,另一个CPU会不会同时运行这个Tasklet?不会的,所以如果只是在某个Tasklet中访问临界资源,无需上锁。

 

假设在Timer中访问临界资源,另一个CPU会不会同时运行这个timer?不会的,所以如果只是在某个Timer中访问临界资源,无需上锁。

 

如果在有2个不同的Tasklet或Timer都会用到一个临界资源,那么可以使用spin_lock()、spin_unlock()来保护临界资源。不需要用spin_lock_bh(),因为一旦当前CPU已经处于TaskletTimer中,同一个CPU不会同时再执行其他TaskletTimer。

 

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/

 

posted on 2024-05-03 16:11  拉风摊主  阅读(129)  评论(0编辑  收藏  举报

导航