为什么拥有自旋锁的代码段不能睡眠?

参考博文:

宋宝华: 是谁关闭了Linux抢占,而抢占又关闭了谁?

Linux中的preempt_count - 知乎

[宏]preempt_disable - DoOrDie - 博客园

LInux中ThreadInfo中的preempt_count字段 - jack.chen - 博客园

Linux进程内核栈与thread_info结构详解--Linux进程的管理与调度(九)_OSKernelLAB(gatieme)-CSDN博客_thread_info

linux - Why can't you sleep while holding spinlock? - Stack Overflow

spin_lock为什么要关闭抢占?_小野猫-CSDN博客

一. 自旋锁的实现

实现部分参考《深入Linux设备驱动程序内核机制》

spinlock的上锁操作是借助ARM的LDREX和STREX互斥指令完成的,这里不详细介绍

简单说来spinlock就干了两件事:1)关闭抢占2)获取锁操作

二. 什么是内核抢占以及preempt_disable/enable的实现

什么是内核抢占?文字性的描述如下:《深入Linux设备驱动程序内核机制》

内核抢占调度的代码实现:(./kernel/sched/core.c)

可以看到抢占调度的本质就是通过schedule把当前进程换出处理器,当然在schedule之前要先进行一系列的检查:当当前进程的preempt_count变量非0或者中断关闭时,当前进程都是不能被抢占的,即当前进程不能被挂起换出

./include/linux/preempt.h

./include/asm-generic/preempt.h

thread info

thread_info:arch/arm/include/asm/thread_info.h

thread_info是体系结构相关的,结构的定义在thread_info.h中,保存了进程所有依赖于体系结构的信息, 同时也保存了一个指向进程描述符task_struct的指针 

获取当前在CPU上正在运行进程的thread_info

current_thread_info()可获得当前执行进程的thread_info实例指针

current给出了当前进程进程描述符task_struct的地址,该地址往往通过current_thread_info()来确定:current = current_thread_info()->task

./include/asm-generic/current.h

./arch/arm/include/asm/thread_info.h

preempt_count(具体实现在./include/linux/preempt.h)

本质上是一个int型变量,是每一个进程的struct thread_info的内嵌成员,利用位段标记了各种上下文

详细介绍可以参考以下博文:Linux中的preempt_count - 知乎

其中bit0~bit7叫做preemption count,用来记录调用preempt_disable()显示关闭抢占的次数,一共八位,可以记录0~255次共256次嵌套

preempt_diable/enable()

参考如下代码:每使用一次preempt_disable(),preemption count的值就会加1,使用preempt_enable()则会让preemption count的值减1

需要注意一点preempt_count_add实际上是(current_thread_info()->preempt_count)++,所以这里代表关闭每个进程的抢占,而不是每个核!

./include/asm-generic/preempt.h

 那么可以被抢占调度的条件preemptible()定义为:preemption count的值为0且中断响应未关闭

三. 为什么拥有自旋锁的代码段必须是原子的

看了上面自旋锁的实现与preempt_disable()的实现,可以讲明白为什么spinlock中不能睡眠了:

首先,并不是睡眠了一定会出问题,而是在spinlock中睡眠是一个坏主意:可能有死锁的风险

假设是单处理器场景:

P1进程关闭进程抢占并获取锁,这时P1->thread_info->preempt_count->preemption count++,P1进程不可以被抢占

但是P1能够自己交出自己,假设P1调用了某些可能导致睡眠的函数(kmalloc/copy_from_user/copy_to_user/sleep...),最终会调到schedule()函数

调度器调度P2进程

这时恰好P2进程也去获取同一把spinlock,在真正操作锁之前P2还要关闭自己的进程抢占,这时P2->thread_info->preempt_count->preemption count++,P2进程不可以被抢占且P2进程无法获取spinlock而产生了自旋

P2进程此时的状态就很尴尬: 首先它没有主动调度schedule()换出自己,其次由于自身进程关闭了抢占,外部调度器无法通过抢占换出P2,再加上获取不到锁的自旋,所以P2进入了无尽自旋的状态,导致死锁占住了CPU

linux - Why can't you sleep while holding spinlock? - Stack Overflow

最后,同样参考《深入Linux设备驱动内核机制》

假如对自旋锁的竞争发生在了真正并发执行的两条路径上,一旦持锁的执行路径执行时间过长,甚至被换出处理器,另一个自旋等待的执行路径就要等待更长时间,尤其是在中断上下文等待过长时间甚至卡住对系统的稳定性是极大的破坏

四. 为什么自旋锁要关闭抢占

参考《LDD3》

其实spinlock中的睡眠都有进程因未放锁而换出导致的死锁风险,这种睡眠分为两大类:

第一类就是spinlock临界区内调用可能睡眠的函数主动睡眠

第二类就是spinlock临界区内可能被外部抢占导致进程被换出(当然内核spinlock已经考虑到了这种情况关闭了抢占)

其次,参考《深入Linux设备驱动内核机制》

 抢占的发生,使原来的执行路径被调度出去,新的执行路径除了会有获取锁导致的死锁风险外,还有可能破坏原来受保护的共享资源

所以说重点要理解的一句话如下,最重要的一个单词是A-T-O-M-I-C

 五. 拓展 & 思考

1. spin_unlock的顺序问题

./include/linux/spinlock_api_smp.h

为什么是lock(关中断-关抢占),unlock(开中断-开抢占),而不是

lock(关中断-关抢占),unlock(开抢占-开中断)?

参考:宋宝华: 是谁关闭了Linux抢占,而抢占又关闭了谁?

 

 2. 单处理器上的spinlock系列函数与多处理器上的spinlock系列函数的区别

参考《深入Linux设备驱动内核机制》

参考:面试必备进程同步机制--内核自旋锁 - SegmentFault 思否

 ./include/linux/spinlock_api_up.h

  • 5
    点赞
  • 19
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值