Linux内核并发与同步机制解读(arm64)上

关键词 原子操作、自旋锁、信号量、mutex、读写锁、percpu-rwsem

概述  

从浅到深,逐步分析各种同步机制的功能。

1、原子操作  

解决“读-修改-回写”的完整性,一般用于静态全局变量的保护,静态全局变量的操作过程.

例如,我们写一行代码把变量a加1,编译器把代码编译成3条汇编指令。

(1)把变量a从内存加载到寄存器。

(2)把寄存器的值加1。

(3)把寄存器的值写回内存。

由于是三条指令,无法保障三条指令中间不会被抢占。通过一个实际的案例场景来帮助理解。    

d96b7b4b88732293a8f07313c13a3f5d.png

在上面的执行场景,进程2对a+1的结果被进程1覆盖了,最终看到的结果是a仅被+1,我们期望a被+2。

1.1.1常用原子操作函数  

函数原型

语义

atomic_read(v)

读取原子变量v的值

atomic_add_return(i, v)

把原子变量v的值加上i,并且返回新值

atomic_add(i, v)

把原子变量v的值加上i

atomic_inc(v)

把原子变量v的值加上1

int atomic_add_unless(atomic_t *v, int a, int u);

如果原子变量v的值不是u,那么把原子变量v的值加上a,并且返回1,否则返回0

atomic_inc_not_zero(v)        

如果原子变量v的值不是0,那么把原子变量v的值加上1,并且返回1,否则返回0。

atomic_sub_return(i, v)

把原子变量v的值减去i,并且返回新值

atomic_sub(i, v)

把原子变量v的值减去i

atomic_dec(v)

把原子变量v的值减去1

atomic_cmpxchg(v, old, new)

执行原子比较交换,如果原子变量v的值等于old,那么把原子变量v的值设置为new。返回值总是原子变量v的旧值

{}_relaxed

不内嵌内存屏障原语

{}_acquire

内置了加载-获取内存屏障园原语

{}_release

内置了存储-释放内存屏障原语

在这些基础原子操作函数上,又进行了一些演进,得到了很多的原子操作函数。所有的原子操作函数可以直接查看atomic-fallback.h

2、经典自旋锁  

2.1原理介绍  

自旋锁用于处理器之间的互斥,适合保护很短的临界区,并且不允许在临界区睡眠。申请自旋锁的时候,如果自旋锁被其他处理器占有,本处理器自旋等待(也称为忙等待)。    

进程、软中断和硬中断都可以使用自旋锁。

目前内核的自旋锁是排队自旋锁(queued spinlock,也称为“FIFO ticket spinlock”),算法类似于银行柜台的排队叫号。

(1)锁拥有排队号和服务号,服务号是当前占有锁的进程的排队号。

(2)每个进程申请锁的时候,首先申请一个排队号,然后轮询锁的服务号是否等于自己的排队号,如果等于,表示自己占有锁,可以进入临界区,否则继续轮询。

(3)当进程释放锁时,把服务号加1,下一个进程看到服务号等于自己的排队号,退出自旋,进入临界区。

2.2代码实现  

2.2.1关键数据结构  

b29a951ec8583bb79bcae50e168ebd34.jpeg

数据结构中除了owner是指针变量,其他都是实体变量,也就是说spinlock 结构体定义一个spinlock变量,那么结构体里的变量都会被实例化。

2.2.2关键函数接口    

2.2.2.1spin_lock_init  

154c2bf869c63034f4ff917e5aff950b.jpeg

如果没有开debug宏,初始化函数仅对raw_lock初始化为0

2.2.2.2spin_lock  

spin_lock() -> raw_spin_lock() -> _raw_spin_lock() -> __raw_spin_lock()  -> do_raw_spin_lock() -> arch_spin_lock()    

749a63bdfeb4532f9edefaf2f92b695a.jpeg

代码实现很简单,lock->slock先读一份保存在本地lockval,然后lock->slock的next位域+1,然后在while循环中判断owner域是否等于本地记录的next域,如果相等就表示拿锁成功。

2.2.2.3spin_unlock  
spin_unlock->__raw_spin_unlock->do_raw_spin_unlock->arch_
spin_unlock

a81125f79d4ce1741dbf8be5a317aeec.jpeg

释放锁的逻辑也非常简单直接对ower位域+1.    

2.3稳定性维测能力介绍  

假设某个thread拿了spinlock后长时间不释放,引发了稳定性问题,
如何来定位具体是哪个线程拿了锁。可以通过CONFIG_DEBUG_SPINLOCK开关打开持锁的owner跟踪。

b6a8c621b6c75c71b08741810c845109.jpeg

2.4应用场景   

保护执行路径短且快。

2.5思考  

2.5.1开CONFIG_DEBUG_SPINLOCK是否会引入性能问题  

Debug的原理就是增加了一个owner指针变量来记录拿锁的task,对owner的操作仅是简单的赋值,因此对性能的影响可以忽略不计。    

2.5.2raw_spinlock与spinlock的区别?  

在arm64上,两个函数完全等价。

3、信号量  

3.1原理介绍  

869bbb7b8e165290b613da6312a6395d.png

锁创建时通过count(钥匙数量)值定义了最多可以有多少条路径同时访问临界区。假设初始时count值初始化为n(制造n把进入临界区的钥匙),那么允许有n个thread同时获得锁,n把钥匙都用完了,还有想申请钥匙的线程,就只能放到等待队列中了。

这个机制没有定义进入临界区的线程的具体行为(read,write),如果拿到钥匙的线程都写临界区的数据,那么临界区最终的数据是不可预测的。这也就导致了这个机制无法推广使用(不是不用,实在没法用)。一些铁杆粉丝非要使用它,那么只能将count值初始化为1,当互斥信号量使用。

为了对后续其他锁理解,简要说明信号量机制关键数据结构与关键函数接口。

3.2代码实现  

3.2.1关键数据结构    

0ddcbfd024d2c5f89311a2c34061e80a.jpeg

3.2.2关键函数接口  

3.2.2.1semaphore 的初始化  

e6a5d98b492a80a4da32d4b01d28edca.jpeg

主要完成struct semaphore的3个变量初始化,DEFINE_SEMAPHORE初始化宏,给count值默认为1.

3.2.2.2down  

7be50d4910fc79378a7bcd184741aa77.jpeg    

实现逻辑比较简单,count > 0就可以快速拿到锁,如果count = 0,就走慢速路径__down,慢路径里可能会睡眠。慢速路径不再做详细分析。

3.2.2.3up  

504de91bff3e008580a8ff33a22469fb.jpeg

释放锁,对count++,如果wait_list不为null,唤醒第一个waiter。

3.3应用场景  

不推荐使用。

3.4思考  

  • Count是个全局变量,多个CPU可能并行操作,可能造成致命的同步问题。

  • 如果出现死锁造成稳定性问题,无法根据问题现场定位出问题根因。    

4、互斥锁-mutex  

4.1原理介绍  

主要实现资源的互斥操作,确保在一个时刻只有一个线程可以操作临界资源。

f6a15711629d98161d461ecc592fee6a.png

Mutex在变量owner上做较多文章。通过判断owner是0与非0来区分 锁是否空闲状态。当有线程进入临界区(获得锁),将该线程的task赋值给owner,方便定位死锁问题。通过flag标记位实现乐观自旋与handoff机制。

为了快速理解代码的实现,下面用问答的形式,来帮助深度理解锁的实现原理。

4.1.1互斥锁与互斥信号量原理类似,为什么还要实现互斥锁?  

互斥锁相对互斥信号量要轻便一些,数据结构比信号量小,执行速度比信号量快,加入了乐观自旋等待机制。

  • 互斥锁最先实现自旋等待;

  • 互斥锁在睡眠之前会尝试获取锁;

  • 互斥锁通过实现MCS锁避免多个CPU争用锁导致的CPU高速缓存颠簸;

  • 互斥锁允许睡眠,不能在中断处理函数等实时性要求高的场景使用; 

  • 互斥锁用owner(原子变量)替换了count(全局变量),避免“读-写”的同步问题,同时便于定位死锁问题。

互斥信号量是一个简陋的同步机制。

4.1.2什么是偷锁,偷锁是怎么产生的  

锁的持有者A,用完锁理应传递给B,B去A这里接手锁的时候发现锁已经在C那里了。锁在交接的过程中被C偷走了。

进程A释放锁,并把B唤醒让B持锁,但是巧了,C在A释放锁的时候正在申请锁,在看B这会儿还睡者,A一释放,C就抢走了,等B醒来,没自己啥事了。如果不好理解,举一个乌鸦与狐狸的故事,乌鸦嘴里衔着一块肉,乌鸦准备把这块肉给正再睡觉的小乌鸦,乌鸦张嘴叫小乌鸦的时候,而正好被狐狸路过看见,等小乌鸦醒来肉已不见了只能继续睡。

4.1.3为什么要引入乐观自旋  

假设一种场景:持锁的进程A即将放锁,进程B发起锁的申请,B看锁已被占用,立即就会睡眠,睡眠后立即又被A唤醒,这期间在调度上的开销不可忽略。也就是说进程B不睡眠乐观的忙等,开销可能比睡眠要小得多。这就是乐观自旋机制的作用。

4.1.4什么情况下该乐观且乐观多久  

还有剩余时间片且owner仍然在CPU上运行。一般是50us。用户可以根据自行的实际情况来设置。

4.1.5为什么需要handoff机制    

乐观自旋加剧了偷锁的概率。乐观自旋机制的加入改变了“乌鸦与狐狸”剧本,上文中的狐狸恰巧撞上一块肉,加入乐观自旋后,狐狸盯着乌鸦嘴里的肉一段时间,狐狸偷走肉的概率大大提升了。要命的是如果运气不好接下来的几天肉都被狐狸偷走,小乌鸦就活活被饿死了。经过反思后就设计了HANDOFF机制来避免waiter饿死悲剧的发生。

4.1.6HANDOFF机制的原理  

Wait_list上第一个waiter被唤醒后立即设置HANDOFF标记位,如果锁此时被偷,小偷释放锁时,直接把锁交给第一个waiter(在__mutex_unlock_slowpath中直接将第一个waiter的task赋值给lock->owner),这就保证了第一个waiter的锁最多被偷一次。

4.2代码实现  

4.2.1关键数据结构  

9fe85606ca74bbc755c62062eb63a5cd.jpeg    

  • Owner变量的解读

00111c38f58e33a5cf0e5faffac2ea32.png

Owner变量为0时表示没有持锁。非0表示持有锁。BIT2~BIT0标记位主要用于乐观自旋等待。

4.2.2关键函数接口  

4.2.2.1mutex_init  

Linux内核中定义了两个mutex初始化接口,DEFINE_MUTEX(mutexname)与mutex_init,他们实现的功能完全相同。程序员可以根据自己的偏好,选其一。

5dae4d97e3dea04e06a81080353a843f.jpeg

主要完成struct mutex的成员变量初始化。

4.2.2.2mutex_lock    

申请获取mutex锁,未申请到锁,将在该函数中睡眠,该函数返回就表示申请获取成功,申请线程将持有对应的mutex锁。

  • 快速路径分析

0eb651a7310e3076381597110aa73eb5.jpeg

申请获取mutex锁首先进入快速路径__mutex_trylock_fast,如果申请失败进入慢速路径__mutex_lock_slowpath。

8964391360a8c2db6a65a6da564f0aca.jpeg

快速路径的实现非常简单,原子的比较owner变量,确认是否有其他线程已经持锁。

  • 慢速路径分析    

037bdbd2f71caae3648e92686a9ac4ce.jpeg__mutex_lock里直接调用了__mutex_lock_common,接下来展开__mutex_lock_common分析,这是mutex的灵魂所在。代码比较长,仅截取关键片段(过滤debug代码与非arm64的代码)分析。30e6932a4a213c681ef0d032aae57b11.jpeg

4c99a4c71de9a39a2b00ee42a333d435.jpeg

  • __mutex_trylock    

47e013b581c85fd6e5901e2b5362f904.jpeg

  • mutex_optimistic_spin    

043b66de9133a7d7cf6447518650f959.jpeg

阅读完源代码总结一下主要流程:    

f98038ff9a281110edc819db966395bf.png

尝试获取锁失败,进入乐观者自旋获取锁,如果还是失败,将当前进程加入到wait_list,然后再次尝试获取锁,还是失败,让出CPU,睡眠,持锁的owner用完锁后释放并唤醒waiter,唤醒后继续尝试获取锁。

乐观自旋机制mutex_optimistic_spin是为了解决性能问题,对锁本身的功能没有影响。

4.2.2.3mutex_unlock  

mutex_unlock直接调用了__mutex_unlock_slowpath,下面注释__mutex_unlock_slowpath的实现。    

513a73a782064c5a44a9b4296d0ac7d4.jpeg

4.3应用场景   

资源互斥场景,由于可能会存在睡眠,禁止在实时性要求高的场景使用,例如中断上半部中。

4.4思考  

4.4.1Mutex锁申请的慢速路径里,为什么进入函数就关闭了抢占,如果不关闭会有什么影响?  

4.4.2释放锁的时候为什么不直接判断是否有waiter,如果有waiter,直接把锁传递给waiter,这样不就避免了所有偷锁的情况吗?  

4.4.3不同优先级的进程不同的自旋时长会不会带来更好的体验?  

 由于篇幅过长,下章将为大家带来

5、读写信号量-rw_semaphore

6、percpu-rwsem

深度好文 | Android高性能音频解析

干货 | 刷机流程介绍

crash实战:手把手教你使用crash分析内核dump

bee35c7b5b4cc02380ce76ae5d2a1544.gif

长按关注内核工匠微信

Linux内核黑科技| 技术文章| 精选教程

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/585766.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

【计算机网络】快速做题向 极限数据传输率的计算(有噪声/无噪声)

首先需要理解什么是码元 码元在课本上的概念比较难理解 但是只要记住 二进制码元在图上显示的就是有两种高度的横杠“—”(对应0,1),即,有两种二进制码元 四进制就是有四种高度的横杠“—”(对应00&…

【C语言】一篇文章深入解析联合体和枚举且和结构体的区别

文章目录 📝前言🌠 联合体类型的声明🌉联合体的特点 🌠相同成员的结构体和联合体对⽐🌉联合体⼤⼩的计算 🌠联合体应用🌉枚举类型的声明 🌠枚举类型的优点🌉 枚举类型的使…

方案:智能分析网关V4在高校实验室安全管理中的应用

一、方案背景 实验室作为科研与教学的核心场所,其重要性不言而喻。高校实验室由于其开放性与多样性,安全管理尤为重要。高校实验室的安全管理,不仅是保障科研与教学质量的基础,更是校园安全的重要组成部分。一旦发生安全事故&…

LabVIEW在电机噪声与振动探测的应用

LabVIEW在电机噪声与振动探测的应用 硬件部分是电机噪声和振动测试分析系统的基础,主要由三大核心组件构成:高灵敏度振动传感器、先进的信号调理电路和高性能数据采集卡。这些设备协同工作,确保了从电机捕获的噪声和振动信号的准确性和可靠性…

山西电力市场日前价格预测【2023-12-28】

日前价格预测 预测说明: 如上图所示,预测明日(2023-12-28)山西电力市场全天平均日前电价为814.30元/MWh。其中,最高日前电价为1500.00元/MWh,预计出现在08:00~08:45,17:00~20:15。最低日前电价为394.61元/…

Vue3-29-路由-编程式导航的基本使用

补充一个知识点 路由配置中的 name 属性 : 可以给你的 路由 指定 name属性,称之为 命名路由。 这个 name 属性 在 编程式导航 传参时有重要的作用。 命名路由的写法如下 : 像指定 path 一样,直接指定一个 name 属性即可。{path:/d…

ubuntu 安装apisix -亲测可用

官方未提供在ubuntu系统中安装apisix的方式,似乎只能通过源码方式安装,但是并不推荐,非常容易失败, 具体操作方式如下: ubuntu和Debian其实类似的,可使用DEB方式安装,如下截图 注意&#xff1…

实用的二进制文件分割器

自己写的一个能方便分割文件的小工具 1.可以按照任意方式分割文件 (1)分割范围 (2)分割块大小 (3)分割份数 (4)可以反向分割(从文件末尾向文件头分割) 2.可以指定输出文件名规则 (1)文件名前缀 (2)文件名序号 (3)文件名后缀(扩展名) (4)文件名…

Ansible Windows批量安装软件

文章目录 1:Windows配置WINRM2: ansible安装3:操作步骤3.1 配置主机清单3.2 测试ansible执行命令3.3 测试安装7Z ansible操作通过winrm协议windows,经过实践精简以下方法能快速配置,并能通过测试 更多文档参考: 支持的windows版本…

vscode 支持c,c++编译调试方法

概述:tasks.jason launch.json settings.json一定要有,没有就别想跑。还有就是c 和c配置有区别,切记,下文有说 1.安装扩展插件。 2.安装编译器,gcc.我用的是x86_64-8.1.0-release-win32-seh-rt_v6-rev0.7z &#xf…

Linux之缓冲区的理解

目录 一、问题引入 二、缓冲区 1、什么是缓冲区 2、刷新策略 3、缓冲区由谁提供 4、重看问题 三、缓冲区的简单实现 一、问题引入 我们先来看看下面的代码:我们使用了C语言接口和系统调用接口来进行文件操作。在代码的最后,我们还使用fork函数创建…

万界星空低代码云MES-才是工业MES的未来

万界星空科技作为一家在云MES系统的研发、生产自动化方面拥有很多年行业经验的科技型企业,多年来专注于云MES系统的研发与技术支持服务,目前已成为国内知名的智能制造整体解决方案提供商。 近几年,万界星空科技发掘制造行业生产及物流难点、…

图像的颜色及Halcon颜色空间转换transfrom_rgb/trans_to_rgb/create_color_trans lut

图像的颜色及Halcon颜色空间转换 文章目录 图像的颜色及Halcon颜色空间转换一. 图像的色彩空间1. RGB颜色 2. 灰度图像3. HSV/ HSI二. Bayer 图像三. 颜色空间的转换1. trans_from_rgb算子2. trans_to_rgb算子3. create_color_trans_lut算子 图像的颜色能真实地反映人眼所见的真…

挑战Python100题(8)

100+ Python challenging programming exercises 8 Question 71 Please write a program which accepts basic mathematic expression from console and print the evaluation result. 请编写一个从控制台接受基本数学表达式的程序,并打印评估结果。 Example: If the follo…

每日一练:LeeCode-20. 有效的括号(简)【栈】

给定一个只包括 ‘(’,‘)’,‘{’,‘}’,‘[’,‘]’ 的字符串 s ,判断字符串是否有效。 有效字符串需满足: 左括号必须用相同类型的右括号闭合。 左括号必须以正确的顺序闭合。 每个右括号都有…

隧道代理HTTP工作原理:一场奇妙的网络魔法表演

嘿,小伙伴们!今天我们要一起探索一个有趣的话题——隧道代理HTTP的工作原理。这不是普通的表演,而是一场奇妙的网络魔法表演! 首先,让我们想象一下,网络世界就像一个大舞台,而我们每个人都是这…

Linux驱动开发简易流程

推荐视频: 正点原子【第四期】手把手教你学 Linux之驱动开发篇 小智-学长嵌入式Linux&Android底层开发入门教程 能力矩阵 基础能力矩阵 熟悉c/c、熟悉数据结构 熟悉linux系统,Shell脚本,Makefile/cmake/mk 文件IO、多线程、竞争、并发…

深度优先和广度优先

文章目录 前言一、深度和广度的区别二、代码演示1.准备数据,构造树2.深度优先遍历3.广度优先遍历 总结 前言 深度优先和广度优先的区别: 搜索方式不同 。深度优先搜索算法不全部保留结点,扩展完的结点从数据库中弹出删去;广度优先搜索算法需…

oracle-sga-shared_pool

shared pool 缓冲sql语句和执行计划 shared pool由三部分组成 free libray:缓存sql执行计划 row cathe :缓存数据字典 硬解析:1判断语法2判断对象是否存在3有没有权限4 从n个执行方案中选出最优解,生成执行计划,这一…

壮志酬筹>业务被裁>副业转正>收入回正。一个前黑马程序员老师的2023

从年初时的踌躇满志,到年中时整个业务线被砍。全职做前端训练营,四个多月的时间帮助100多名同学拿到了满意的offer,同时也让我的收入重归正轨。仅以这个视频记录我,一个普通程序员的 2023 。 视频版可直接访问 Hello,大…