Linux用户态进程可以在持有内核锁的情况下返回用户态么?

检测写一个测试模块,在内核中定义一把互斥锁并获取,然后返回到用户态,观察内核是否报告异常,代码逻辑如下:

测试发现,内核并没有报任何的WARNING,似乎说明用户态线程是可以在持有内核锁的情况下返回用户态的,先别急着下结论,我们打开内核的LOCKDEP功能重新编译内核在跑一下看看。

选中所有的LOCKDEP配置,保存配置后,前后配置的变化对比如下:

之后重新编译完整内核并安装,再次运行测试用例,发现了内和报告了WARNING:

WARNING: lock held when returning to user space!

虽然是WARNING,严重程度不算大,但是至少说明了内核不建议用户线程在返回用户态的时候仍然持有内核锁,即便这把锁仅仅是模块私有的也不行。

找到内核中打印WARNING的位置,分析其逻辑,发现其是在内核函数lockdep_sys_exit中报告的:

判断逻辑是curr->lockdep_depth,顾名思义,struct task_struct中的lockdep_depth字段记录了线程在内核中持有锁的层数(深度),如果深度不为0,就会在返回用户态之前,打印上图中的警告。

之所以说是在返回用户态之前打印,和lockdep_sys_exit的调用位置是有关系的,如下图,当执行完毕系统调用的逻辑时,会将获取锁和释放锁的行为转换为对lockdep_depth的增减,获取锁++,释放锁--,在返回用户态之前,进行锁平衡的检测,如果系统调用中锁没有平衡使用,则内核会报告上图中的警告通知开发者。

所以,内核不允许线程持有锁的情况下返回用户态,但是允许持有用户态锁(比如pthread mutex)的情况下进入内核态,比如,持有FUTEX的时候执行sleep操作。

LOCKDEP机制分析

Linux 内核犹如一个磕了药的蜘蛛编织成的网, 包含千百个线程,大量的共享资源,数不清的并发路径,以及只可能在运行中才能触发的种种corner case,内核中使用各种各样的锁建立秩序,规范资源访问,协调不同的并行路径。

但用锁也是有代价的,如果不小心,非常容易引入难查的死锁问题。开发者不可能要求所有的用户都知晓潜在出问题的风险。所以,在死锁发生前,编写代码阶段,就要做好预防工作,防患于未然,尽量提前发现并且提前在开发阶段发现和解决这其中潜在的死锁风险,而不是等到最后真正出现死锁时给用户带来糟糕的体验。应运而生的就是 lockdep 死锁检测模块。

LOCKDEP机制需要打开前面提到的几个配置项后才能使用,主要是CONFIG_LOCKDEP宏所控制的menuconfig菜单的相关选项。

打开配置后,内核会在检测到锁使用问题的时候,通过DMESG汇报DEBUG信息。同时通过/proc/文件系统开放了一些文件节点用于用户态的调试和profile:

此时内核会自动进行locktorture的压力测试,具体可以参考linux-5.4.260/kernel/locking/locktorture.c文件。

查看当前上下文LOCK获取信息

可以调用内核函数debug_show_held_locks(current)和debug_show_all_locks.获取当前上下文的拿锁信息:

debug_show_all_locks函数可以获取调用点系统的拿锁信息:

设备驱动注册时的拿锁信息

以典型设备字符设备驱动为例,分析字符设备驱动在关键调用路径上的拿锁情况,为了全面,我们用注册平台设备的方式创建字符设备驱动,字符设备驱动作为平台设备的子设备存在.

首先在module_init/module_exit函数内,上下文没有拿锁:

其次,平台设备的probe/remove中,拿有设备级的MUTEX锁:

而其拿到的锁是设备锁:

而在设备级的释放函数中,也是不拿锁的:

drv_release是设备释放函数,它是平台设备驱动最后调用/销毁的用户驱动资源。

接下来看字符设备驱动struct file_operations各个回调函数的拿锁情况:

open上下文不拿锁:

release上下文不拿锁:

flush上下文不拿锁:

write上下文不拿锁:

read上下文不拿锁:

ioctl上下文不拿锁:

所以可以看到,字符设备驱动的回调函数上下文中,是没有持有系统/框架级的锁的,也就是lock free的。

为什么字符设备IOCTL驱动中没有拿锁?

内核地址空间是永久的,嵌入到每个进程的地址空间中,而用户空间是暂时的,可以随时消失,如果允许用户进程通过IOCTL拿到锁后返回用户态,因为用户态进程的生命期是不可控的,何时释放这把锁就成了大问题,所有的关于锁的释放平衡操作都需要驱动开发者关心。IOCTL没有对等的反向调用接口被框架使用,只有字符设备的RELEASE接口会被框架自动调用,而RELEASE接口也不会做锁平衡的操作,所以,IOCTL中不允许带驱动锁返回。

进一步分析,以系统调用为粒度,所有的系统调用在返回用户态的时候都不应该持有内核锁,原因如前面所述,毕竟返回用户态的进程可能随时会被KILL掉,这样内核锁将永远得不到释放,进而造成更大的内核问题。


结束

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

papaofdoudou

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值