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.xdnf.cn/news/823095.html

如若内容造成侵权/违法违规/事实不符,请联系一条长河网进行投诉反馈,一经查实,立即删除!

相关文章

网络交换机端口管理会面临的问题

交换机端口管理是跟踪网络交换机及其端口连接详细信息的过程,在大型网络中,交换机端口管理过程通常使用自动化交换机端口管理工具执行。 通过网络交换机端口提供的完全控制和可见性使交换机端口管理工具在管理网络时必不可少,在网络中部署交…

javaWeb学生信息管理系统2

一、学生信息管理系统SIMS 一款基于纯Servlet技术开发的学生信息管理系统(SIMS),在设计中没有采用SpringMVC和Spring Boot等框架。系统完全依赖于Servlet来处理HTTP请求和管理学生信息,实现了信息的有效存储、检索和更新&#xf…

Python三级 每周练习题32

如果你感觉有收获,欢迎给我微信扫打赏码 ———— 以激励我输出更多优质内容 练习一: 作业1 (1)用 python 新建一个文件名为jscj.csv 文件,将上述的数据写入表格 (2)计算输出每个班的平均分(结果保留两位小数) 答案: with open(cjm.csv,w,encodingu…

基于Java图书借阅管理系统设计与实现(源码+部署文档)

博主介绍: ✌至今服务客户已经1000、专注于Java技术领域、项目定制、技术答疑、开发工具、毕业项目实战 ✌ 🍅 文末获取源码联系 🍅 👇🏻 精彩专栏 推荐订阅 👇🏻 不然下次找不到 Java项目精品实…

AI又进化了,AI 写代码工具

今年 AI 的发展可谓一日千里,相信不少同学应该都用过 AI 来帮助自己提高开发效率吧? 比如让 AI 根据注释生成代码、解释整段代码、提供技术问题的答疑、修改 Bug、生成单元测试等等。 在 12 月 28 日刚刚结束的 WAVE SUMMIT 深度学习开发者大会上&…

Postgresql源码(119)PL/pgSQL中ExprContext的生命周期

前言 在PL/pgSQL语言中,执行任何SQL都需要通过SPI调用SQL层解析执行,例如在SQL层执行表达式的入口: static bool exec_eval_simple_expr(PLpgSQL_execstate *estate,PLpgSQL_expr *expr,Datum *result,bool *isNull,Oid *rettype,int32 *re…

C练习——判断三角形并求面积

题目:从健盘任意输入三角形的三边长为a,b,c,编程判断a,b,c的值能否构成一个三角形,若能构成三角形,则计算并输出三角形的面积,否则提示不能构成三角形。 已知构成三角形的条件是:任意两边之和大于第三边。 解析&#…

nodejs+vue+微信小程序+python+PHP的冷链物流配送系统-计算机毕业设计推荐

对于冷链物流信息调度系统所牵扯的管理及数据保存都是非常多的,例如管理员;首页、用户管理(管理员、客户、业务员、配送员)客户管理(货物信息、客户运输单、车辆信息、调度安排)这给管理者的工作带来了巨大…

Java多线程<三>常见的多线程设计模式

多线程的设计模式 两阶段线程终止 park方法 interrupted() 会让他失效。 使用volatile关键字进行改写 单例模式 双锁检测 保护性暂停 实现1: package threadBase.model;/*** author: Zekun Fu* date: 2022/5/29 19:01* Description:* 保护性暂停,* …

PostgreSQL10数据库源码安装及plpython2u、uuid-ossp插件安装

PostgreSQL10数据库源码安装及plpython2u、uuid-ossp插件安装 1、环境2、安装包下载3、安装3.1 、解压3.2、配置3.3、编译安装3.4 、启动与关闭 4、安装 uuid-ossp 、plpython2u插件5、参考 1、环境 centos 7 、 postgresql 10.19 2、安装包下载 postgres 源码安装包 3、安…

万界星空科技车间生产管理系统解决方案

车间管理系统解决方案:   (一)车间生产计划管理解决方案   车间管理系统解决方案对于一般的生产计划,需完成编制、审批、下达、执行、完工等操作,车间管理系统解决方案立足于减少中间环节浪费,节约成本&#xff0c…

提升效率:使用注解实现精简而高效的Spring开发

IOC/DI注解开发 1.0 环境准备1.1 注解开发定义bean步骤1:删除原XML配置步骤2:Dao上添加注解步骤3:配置Spring的注解包扫描步骤4:运行程序步骤5:Service上添加注解步骤6:运行程序知识点1:Component等 1.2 纯注解开发模式1.2.1 思路分析1.2.2 实现步骤步骤1:创建配置类…

Redis为何如此快速?

1. 引言 Redis(Remote Dictionary Server)是一个高性能的键值对存储数据库。它以其出色的性能和灵活的数据结构而闻名,今天就来谈谈redis为什么会这么快。 1.1 Redis是单线程吗? Redis 的单线程主要是指 Redis 的网络 IO 和键值对…

发掘企业知识宝藏,开启专属知识付费新时代

明理信息科技saas知识服务平台 随着互联网的快速发展,人们越来越重视知识的获取和价值的挖掘。在这个信息爆炸的时代,知识付费已经成为了一种新的商业模式,为知识的传播和价值的转化提供了更加高效和便捷的途径。本文将探讨知识付费的发展背…

微软技术分享带您探索C语言的魅力:C语言与C++的区别概述

🌷🍁 微软技术分享 带您 Go to New World.✨🍁 🦄 博客首页——微软技术分享🎐 🐳《灰帽黑客:攻守道》 文章图文并茂🦕生动形象🦖简单易学!欢迎大家来踩踩~&a…

scanf函数返回值被忽略

心怀希望的前进 前言 最近在复习c语言,发现了许多之前不了解的知识,今天想来与大家分享一下scanf返回值值被忽略的问题。 很多人应该都在vs中见到过,我们先说原因,再说改进方法 原因: scanf函数在读取数据时不会检…

MySQL MVCC精讲

版本链 我们前面说过,对于使用InnoDB存储引擎的表来说,它的聚簇索引记录中都包含两个必要的隐藏列(row_id并不是必要的,我们创建的表中有主键或者非NULL的UNIQUE键时都不会包含row_id列): trx_id&#xff…

LNK2038、LNK2001

今天在编译PCL的DLL版本是发生错误,配置环境为:pcl1.10.1、msvc2019。 错误列表: 控制台输出: libboost_thread-vc142-mt-gd-x64-1_72.lib(tss_pe.obj) : error LNK2038: 检测到“_ITERATOR_DEBUG_LEVEL”的不匹配项: 值“2”不匹…

基于ssm的程序设计实践项目管理系统+jsp论文

摘 要 现代经济快节奏发展以及不断完善升级的信息化技术,让传统数据信息的管理升级为软件存储,归纳,集中处理数据信息的管理方式。本实践项目管理系统就是在这样的大环境下诞生,其可以帮助管理者在短时间内处理完毕庞大的数据信息…

Ajax学习

Ajax Ajax 是什么 AJAX 即"Asynchronous Javascript And XML"(异步 JavaScript 和 XML)Ajax 是一种浏览器异步发起请求(指定发哪些数据),局部更新页面的技术Ajax 经典应用场景 搜索引擎根据用户输入关键字,自动提示检索关键字动态加载数据,按需取得数据【树形菜单…