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

相关文章

windows和Linux使用命令行计算文件的MD5值

《windows和Linux使用命令行计算文件的MD5值》在Windows和Linux系统中,您可以使用命令行(终端或命令提示符)来计算文件的MD5值,文章介绍了在Windows和Linux/macO... 目录在Windows上:在linux或MACOS上:总结在Windows上:可以使用certuti

使用Python实现IP地址和端口状态检测与监控

《使用Python实现IP地址和端口状态检测与监控》在网络运维和服务器管理中,IP地址和端口的可用性监控是保障业务连续性的基础需求,本文将带你用Python从零打造一个高可用IP监控系统,感兴趣的小伙... 目录概述:为什么需要IP监控系统使用步骤说明1. 环境准备2. 系统部署3. 核心功能配置系统效果展

Python实现微信自动锁定工具

《Python实现微信自动锁定工具》在数字化办公时代,微信已成为职场沟通的重要工具,但临时离开时忘记锁屏可能导致敏感信息泄露,下面我们就来看看如何使用Python打造一个微信自动锁定工具吧... 目录引言:当微信隐私遇到自动化守护效果展示核心功能全景图技术亮点深度解析1. 无操作检测引擎2. 微信路径智能获

Python中pywin32 常用窗口操作的实现

《Python中pywin32常用窗口操作的实现》本文主要介绍了Python中pywin32常用窗口操作的实现,pywin32主要的作用是供Python开发者快速调用WindowsAPI的一个... 目录获取窗口句柄获取最前端窗口句柄获取指定坐标处的窗口根据窗口的完整标题匹配获取句柄根据窗口的类别匹配获取句

Linux之systemV共享内存方式

《Linux之systemV共享内存方式》:本文主要介绍Linux之systemV共享内存方式,具有很好的参考价值,希望对大家有所帮助,如有错误或未考虑完全的地方,望不吝赐教... 目录一、工作原理二、系统调用接口1、申请共享内存(一)key的获取(二)共享内存的申请2、将共享内存段连接到进程地址空间3、将

在 Spring Boot 中实现异常处理最佳实践

《在SpringBoot中实现异常处理最佳实践》本文介绍如何在SpringBoot中实现异常处理,涵盖核心概念、实现方法、与先前查询的集成、性能分析、常见问题和最佳实践,感兴趣的朋友一起看看吧... 目录一、Spring Boot 异常处理的背景与核心概念1.1 为什么需要异常处理?1.2 Spring B

Python位移操作和位运算的实现示例

《Python位移操作和位运算的实现示例》本文主要介绍了Python位移操作和位运算的实现示例,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一... 目录1. 位移操作1.1 左移操作 (<<)1.2 右移操作 (>>)注意事项:2. 位运算2.1

如何在 Spring Boot 中实现 FreeMarker 模板

《如何在SpringBoot中实现FreeMarker模板》FreeMarker是一种功能强大、轻量级的模板引擎,用于在Java应用中生成动态文本输出(如HTML、XML、邮件内容等),本文... 目录什么是 FreeMarker 模板?在 Spring Boot 中实现 FreeMarker 模板1. 环

Qt实现网络数据解析的方法总结

《Qt实现网络数据解析的方法总结》在Qt中解析网络数据通常涉及接收原始字节流,并将其转换为有意义的应用层数据,这篇文章为大家介绍了详细步骤和示例,感兴趣的小伙伴可以了解下... 目录1. 网络数据接收2. 缓冲区管理(处理粘包/拆包)3. 常见数据格式解析3.1 jsON解析3.2 XML解析3.3 自定义

SpringMVC 通过ajax 前后端数据交互的实现方法

《SpringMVC通过ajax前后端数据交互的实现方法》:本文主要介绍SpringMVC通过ajax前后端数据交互的实现方法,本文给大家介绍的非常详细,对大家的学习或工作具有一定的参考借鉴价... 在前端的开发过程中,经常在html页面通过AJAX进行前后端数据的交互,SpringMVC的controll