Linux内核源码阅读之自旋锁的作用及其实现

2024-05-28 14:32

本文主要是介绍Linux内核源码阅读之自旋锁的作用及其实现,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

作用:
内核中的自旋锁的作用是保护一段临界区域的操作是独占的,不能因为多个CPU或者多个进程同时访问破坏数据结构。在单核系统和多核系统中自旋锁的实现有所不同。
多核系统:
对于多核系统,主要考虑一个cpu进入临界代码区域之后,其它cpu不能再次进入这个临界代码区域。
单核系统:
对于单核系统,主要的情景是一个进程进入了临界区域后,不能被其它进程抢占,如果被其他进程抢占,会导致进程抢占的cpu会再次进入这个临界代码区域。
单核系统自旋锁的实现:
对于单核系统,不存在多个cpu竞争的情况。内核中一段代码(进程上下文)进入临界区域后能够打断这个进程执行临界区域代码的只有两种情况。第一种是中断,也就是中断中也存在访问临界区域的操作,第二种情况就是系统开启了抢占。对于第一种情况,中断发生后,原来执行的进程被抢占,换入的进程进入了临界区域。对于中断进入临界区域的这种情况,本质上自旋锁是不能保护的。遇到这样的情况,进入临界区域前,系统直接关掉中断,不能使用自旋锁。对于第二种情况,自旋锁在单核系统上的实现实际上是关闭抢占,使得进入临界区域的进程能被中断打断,但是不能被抢占,也就避免了其它进程同时进入到临界区中。
使用方法
内核中提供的普通自旋锁API为spin_lock()和spin_unlock(),使用方法为:

        spin_lock();...临界区...spin_unlock();

普通自旋锁的数据类型spinlock_t

typedef struct {raw_spinlock_t raw_lock;#ifdef  (CONFIG_PREEMPT) && defined(CONFIG_SMP)unsigned int break_lock;#endif#ifdef CONFIG_DEBUG_SPINLOCKunsigned int magic, owner_cpu;void *owner;#endif#ifdef CONFIG_DEBUG_LOCK_ALLOCstruct lockdep_map dep_map;#endif} spinlock_t;

去掉编译配置选项之后,spinlock_t为空的数据类型,这是因为在单核系统中不需要对数据结构进行处理。
下面是spin_lock在单核系统上的实现,等同于禁止抢占操作:

#define spin_lock(lock)            _spin_lock(lock) 
#define _spin_lock(lock)            __LOCK(lock) #define __LOCK(lock) \ do { preempt_disable(); __acquire(lock); (void)(lock); } while (0) # define __acquire(x) (void)0 

其中preempt_disable()的工作是关闭内核抢占,__acquire(lock)是空操作,(void)(lock)是简单的数据转型操作,防止编译器对lock未使用报警。看到这里,读者应该就明白了,在单处理器中spin_lock()所做的工作仅仅是关闭内核抢占而已,仅此而已,这就保证了在运行期间不会发生进程抢占,从而也就保证了临界区里的代码只有当前进程才会访问到,在当前进程释放临界区之前都不会有别的进程能够访问。
spin_unlock()的实现:

        #define spin_unlock(lock)   _spin_unlock(lock)#define _spin_unlock(lock)  __UNLOCK(lock)       #define __UNLOCK(lock) \do { preempt_enable(); __release(lock); (void)(lock); } while (0)  #define __release(x) (void)0

看懂了上面的spin_lock(),spin_unlock()所做的工作也就很清楚了。spin_unlock()和spin_lock()所做的工作是相反的,spin_lock()关闭了内核抢占,则spin_unlock()开启内核抢占。也就是说在关闭了内核抢占后,进程进入临界区,由于内核抢占已经关闭,因此当前进程不会被其他进程所抢占。完成相应任务后开启内核抢占,释放临界区,此时其他进程就可以抢占CPU从而访问临界区了。
也就是说,普通自旋锁执行过程就是关闭内核抢占->访问临界区代码->开启内核抢占。
普通自旋锁存在的风险
虽然普通自旋锁通过关闭内核抢占并独占CPU资源阻止了其余进程访问临界区资源,但是还有一种特殊的情况。关闭内核抢占只是阻止了其余进程对CPU的抢占,但是并不能阻止中断程序对CPU的抢占,如果中断服务程序想要访问临界区资源的话就有可能造成并发访问,导致中断结束后进程访问的资源被改变了,从而导致错误。为此,Linux内核提供了更加安全的自旋锁API:spin_lock_irqsave()和spin_unlock_irqrestore()。
spin_lock_irqsave()先将处理器状态指令寄存器IF的内容保存起来,然后通过cli指令关闭中断,然后再执行和spin_lock()相同的步骤。spin_unlock_irqrestore()先执行和spin_unlock()相同的步骤,即打开内核抢占,然后通过sti指令开启中断,最后之前保存的值恢复到处理器状态指令寄存器IF中。
运行由自旋锁保护的临界区代码时不允许系统进入睡眠状态,不仅临界区内的代码不允许进入睡眠状态,临界区内代码所调用的代码也不允许进入睡眠状态。首先,自旋锁一般只在需要短时间访问共享资源时才会使用,一般都是马上就能完成的任务,不需要进入睡眠等待资源。其次,进入临界区之后提供的中断和内核抢占都已经关闭,基于系统时钟中断的进程切换也就失效了,也就是说进入睡眠状态之后可能就会永远的睡下去了,因为没有激励信号来把进入睡眠的进程唤醒。但是有一个特例,那就是kmalloc函数,当分配失败时它可以进入睡眠状态。总而言之,运行由自旋锁保护的临界区代码时,不允许程序进入睡眠;同时临界区应该是短时间就可以完成的任务,因为在多处理器架构中自旋锁会进行忙等待,白白占用CPU资源,接下来就讲解多处理器架构中自旋锁的实现。
多处理器(SMP)普通自旋锁
之前讲过,自旋锁要保证任意时刻只有一个CPU运行在临界区内,同时运行在临界区内的CPU不允许进行进程切换。在单处理器中通过关闭内核抢占保证了进程对资源的访问不被打扰,在多处理器中情况就要麻烦一些了,因为还要应付来自其他处理器的干扰。在多处理器中也是通过关闭内核抢占来保证对临界区的访问不受运行在当前CPU上的其余进程的打扰,那么怎么应付来自其他处理器的干扰呢,带着这个问题让我们接着往下看。
数据结构
多处理器中spinlock_t的定义和单处理器中的一样,这里就不再赘述了,不同的是raw_spinlock_t不再是空数据结构了:

       typedef struct {unsigned int slock;} raw_spinlock_t;

由此可以看出,在多处理其中普通自旋锁其实是使用了一个整数作为计数器,自旋锁初始化的时候会将计数器的值设为1,表示自旋锁当前可用。下面来看自旋锁API的实现。
API的实现

       #define spin_lock(lock)   _spin_lock(lock)    //lock数据类型为*spinlock_t,虽然没有用到-_-。void __lockfunc _spin_lock(spinlock_t *lock){preempt_disable();//关抢占spin_acquire(&lock->dep_map, 0, 0, _RET_IP_);//空操作LOCK_CONTENDED(lock, _raw_spin_trylock, _raw_spin_lock);}

LOCK_CONTENDED是个宏定义,在当前lock为普通自旋锁时,会以lock为参数运行_raw_spin_lock()函数,_raw_spin_lock()定义如下:

    #define _raw_spin_lock(lock)  __raw_spin_lock(&(lock)->raw_lock);

__raw_spin_lock()的实现是跟体系结构相关的,下面来看看在x86里面的实现:

     static inline void __raw_spin_lock(raw_spinlock_t *lock){asm volatile("\n1:\t"LOCK_PREFIX " ; decl %0\n\t""jns 2f\n""3:\n""rep;nop\n\t""cmpl $0,%0\n\t""jle 3b\n\t""jmp 1b\n""2:\t" : "=m" (lock->slock) : : "memory");}   

指令前缀LOCK_PREFIX表示执行这条指令时将总线锁住,不让其他处理器方位,以此来保证这条指令执行的“原子性”,%0表示lock->slock,第一句话表示将lock->slock减一。第二句话进行判断,如果减一之后大于或等于零则表示加锁成功,则调到标号2处,代码2后面没有继续执行的代码了,因此会返回。如果减一之后小于零,则表示之前已经有进程进行了加锁操作,则跳到标号3处执行,将lock->slock与0进行比较,如果小于零则再次跳到3处执行,即循环执行标号3处的指令。直到加锁者释放锁将lock->slock设为1,此时会跳到标号1处进行加锁操作。
与 __raw_spin_lock()相对应的解锁函数是 __raw_spin_unlock(),他的作用是将lock->slock的值设为1,仅此而已。只有加锁者将lock->slock设为1之后其他在忙等待的CPU才能进行加锁,结合之前的__raw_spin_lock()应该不难理解。

       static inline void __raw_spin_unlock(raw_spinlock_t *lock){asm volatile("movl $1,%0" :"=m" (lock->slock) :: "memory");}

总结
单处理器自旋锁的工作流程是:关闭内核抢占->运行临界区代码->开启内核抢占。更加安全的单处理器自旋锁工作流程是:保存IF寄存器->关闭当前CPU中断->关闭内核抢占->运行临界区代码->开启内核抢占->开启当前CPU中断->恢复IF寄存器。
多处理器自旋锁的工作流程是:关闭内核抢占->(忙等待->)获取自旋锁->运行临界区代码->释放自旋锁->开启内核抢占。更加安全的多处理器自旋锁工作流程是:保存IF寄存器->关闭当前CPU中断->关闭内核抢占->(忙等待->)获取自旋锁->运行临界区代码->释放自旋锁->开启内核抢占->开启当前CPU中断->恢复IF寄存器。

这篇关于Linux内核源码阅读之自旋锁的作用及其实现的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/1010835

相关文章

linux-基础知识3

打包和压缩 zip 安装zip软件包 yum -y install zip unzip 压缩打包命令: zip -q -r -d -u 压缩包文件名 目录和文件名列表 -q:不显示命令执行过程-r:递归处理,打包各级子目录和文件-u:把文件增加/替换到压缩包中-d:从压缩包中删除指定的文件 解压:unzip 压缩包名 打包文件 把压缩包从服务器下载到本地 把压缩包上传到服务器(zip

hdu1043(八数码问题,广搜 + hash(实现状态压缩) )

利用康拓展开将一个排列映射成一个自然数,然后就变成了普通的广搜题。 #include<iostream>#include<algorithm>#include<string>#include<stack>#include<queue>#include<map>#include<stdio.h>#include<stdlib.h>#include<ctype.h>#inclu

JAVA智听未来一站式有声阅读平台听书系统小程序源码

智听未来,一站式有声阅读平台听书系统 🌟&nbsp;开篇:遇见未来,从“智听”开始 在这个快节奏的时代,你是否渴望在忙碌的间隙,找到一片属于自己的宁静角落?是否梦想着能随时随地,沉浸在知识的海洋,或是故事的奇幻世界里?今天,就让我带你一起探索“智听未来”——这一站式有声阅读平台听书系统,它正悄悄改变着我们的阅读方式,让未来触手可及! 📚&nbsp;第一站:海量资源,应有尽有 走进“智听

【C++】_list常用方法解析及模拟实现

相信自己的力量,只要对自己始终保持信心,尽自己最大努力去完成任何事,就算事情最终结果是失败了,努力了也不留遗憾。💓💓💓 目录   ✨说在前面 🍋知识点一:什么是list? •🌰1.list的定义 •🌰2.list的基本特性 •🌰3.常用接口介绍 🍋知识点二:list常用接口 •🌰1.默认成员函数 🔥构造函数(⭐) 🔥析构函数 •🌰2.list对象

【Prometheus】PromQL向量匹配实现不同标签的向量数据进行运算

✨✨ 欢迎大家来到景天科技苑✨✨ 🎈🎈 养成好习惯,先赞后看哦~🎈🎈 🏆 作者简介:景天科技苑 🏆《头衔》:大厂架构师,华为云开发者社区专家博主,阿里云开发者社区专家博主,CSDN全栈领域优质创作者,掘金优秀博主,51CTO博客专家等。 🏆《博客》:Python全栈,前后端开发,小程序开发,人工智能,js逆向,App逆向,网络系统安全,数据分析,Django,fastapi

让树莓派智能语音助手实现定时提醒功能

最初的时候是想直接在rasa 的chatbot上实现,因为rasa本身是带有remindschedule模块的。不过经过一番折腾后,忽然发现,chatbot上实现的定时,语音助手不一定会有响应。因为,我目前语音助手的代码设置了长时间无应答会结束对话,这样一来,chatbot定时提醒的触发就不会被语音助手获悉。那怎么让语音助手也具有定时提醒功能呢? 我最后选择的方法是用threading.Time

Linux 网络编程 --- 应用层

一、自定义协议和序列化反序列化 代码: 序列化反序列化实现网络版本计算器 二、HTTP协议 1、谈两个简单的预备知识 https://www.baidu.com/ --- 域名 --- 域名解析 --- IP地址 http的端口号为80端口,https的端口号为443 url为统一资源定位符。CSDNhttps://mp.csdn.net/mp_blog/creation/editor

【Python编程】Linux创建虚拟环境并配置与notebook相连接

1.创建 使用 venv 创建虚拟环境。例如,在当前目录下创建一个名为 myenv 的虚拟环境: python3 -m venv myenv 2.激活 激活虚拟环境使其成为当前终端会话的活动环境。运行: source myenv/bin/activate 3.与notebook连接 在虚拟环境中,使用 pip 安装 Jupyter 和 ipykernel: pip instal

Android实现任意版本设置默认的锁屏壁纸和桌面壁纸(两张壁纸可不一致)

客户有些需求需要设置默认壁纸和锁屏壁纸  在默认情况下 这两个壁纸是相同的  如果需要默认的锁屏壁纸和桌面壁纸不一样 需要额外修改 Android13实现 替换默认桌面壁纸: 将图片文件替换frameworks/base/core/res/res/drawable-nodpi/default_wallpaper.*  (注意不能是bmp格式) 替换默认锁屏壁纸: 将图片资源放入vendo