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

相关文章

SpringBoot+RustFS 实现文件切片极速上传的实例代码

《SpringBoot+RustFS实现文件切片极速上传的实例代码》本文介绍利用SpringBoot和RustFS构建高性能文件切片上传系统,实现大文件秒传、断点续传和分片上传等功能,具有一定的参考... 目录一、为什么选择 RustFS + SpringBoot?二、环境准备与部署2.1 安装 RustF

Nginx部署HTTP/3的实现步骤

《Nginx部署HTTP/3的实现步骤》本文介绍了在Nginx中部署HTTP/3的详细步骤,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学... 目录前提条件第一步:安装必要的依赖库第二步:获取并构建 BoringSSL第三步:获取 Nginx

MyBatis Plus实现时间字段自动填充的完整方案

《MyBatisPlus实现时间字段自动填充的完整方案》在日常开发中,我们经常需要记录数据的创建时间和更新时间,传统的做法是在每次插入或更新操作时手动设置这些时间字段,这种方式不仅繁琐,还容易遗漏,... 目录前言解决目标技术栈实现步骤1. 实体类注解配置2. 创建元数据处理器3. 服务层代码优化填充机制详

Python实现Excel批量样式修改器(附完整代码)

《Python实现Excel批量样式修改器(附完整代码)》这篇文章主要为大家详细介绍了如何使用Python实现一个Excel批量样式修改器,文中的示例代码讲解详细,感兴趣的小伙伴可以跟随小编一起学习一... 目录前言功能特性核心功能界面特性系统要求安装说明使用指南基本操作流程高级功能技术实现核心技术栈关键函

Java实现字节字符转bcd编码

《Java实现字节字符转bcd编码》BCD是一种将十进制数字编码为二进制的表示方式,常用于数字显示和存储,本文将介绍如何在Java中实现字节字符转BCD码的过程,需要的小伙伴可以了解下... 目录前言BCD码是什么Java实现字节转bcd编码方法补充总结前言BCD码(Binary-Coded Decima

防止Linux rm命令误操作的多场景防护方案与实践

《防止Linuxrm命令误操作的多场景防护方案与实践》在Linux系统中,rm命令是删除文件和目录的高效工具,但一旦误操作,如执行rm-rf/或rm-rf/*,极易导致系统数据灾难,本文针对不同场景... 目录引言理解 rm 命令及误操作风险rm 命令基础常见误操作案例防护方案使用 rm编程 别名及安全删除

Linux下MySQL数据库定时备份脚本与Crontab配置教学

《Linux下MySQL数据库定时备份脚本与Crontab配置教学》在生产环境中,数据库是核心资产之一,定期备份数据库可以有效防止意外数据丢失,本文将分享一份MySQL定时备份脚本,并讲解如何通过cr... 目录备份脚本详解脚本功能说明授权与可执行权限使用 Crontab 定时执行编辑 Crontab添加定

SpringBoot全局域名替换的实现

《SpringBoot全局域名替换的实现》本文主要介绍了SpringBoot全局域名替换的实现,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一... 目录 项目结构⚙️ 配置文件application.yml️ 配置类AppProperties.Ja

Python实现批量CSV转Excel的高性能处理方案

《Python实现批量CSV转Excel的高性能处理方案》在日常办公中,我们经常需要将CSV格式的数据转换为Excel文件,本文将介绍一个基于Python的高性能解决方案,感兴趣的小伙伴可以跟随小编一... 目录一、场景需求二、技术方案三、核心代码四、批量处理方案五、性能优化六、使用示例完整代码七、小结一、

Java实现将HTML文件与字符串转换为图片

《Java实现将HTML文件与字符串转换为图片》在Java开发中,我们经常会遇到将HTML内容转换为图片的需求,本文小编就来和大家详细讲讲如何使用FreeSpire.DocforJava库来实现这一功... 目录前言核心实现:html 转图片完整代码场景 1:转换本地 HTML 文件为图片场景 2:转换 H