原子变量原理剖析

一、原子操作

原子操作保证指令以原子的方式执行,执行过程不被打断。先看一个实例,如下所示,如果thread_func_a和thread_func_b同时运行,执行完成后,i的值是多少?

`// test.c
static int i = 0;void thread_func_a()
{i++;
}
void thread_func_b()
{i++;
}`

有的读者认为是2,也有的读者认为是1,在给出正确的结果之前,我们先看下这段代码的汇编:

// aarch64-linux-gnu-gcc -S test.c
// vim test.s
.LFB0:.cfi_startprocadrp    x0, iadd     x0, x0, :lo12:i ldr     w0, [x0]        // 加载内存地址为x0寄存器的值,也就是i的值到w0寄存器add     w1, w0, 1       // 将w0寄存器的值与1相加,结果存在w1寄存器adrp    x0, iadd     x0, x0, :lo12:istr     w1, [x0]        // 把w1寄存器的值,加载到x0所在的地址nopret.cfi_endproc
...

可以看到虽然在我们写的代码中,i++只有一条指令,实际上汇编指令需要三条:

  • 加载内存地址的值

  • 修改变量的值

  • 将修改后的值写回原先的地址

两个cpu在执行过程中,顺序是随机的,结果也是随机的,这里为了更直观,给大商家列一下实际可能的执行顺序,以及对应的结果:

可能的结果:i = 2,执行顺序如下:

可能的结果:i = 1,执行顺序如下

针对上面的问题,linux提供了atomic_t类型的原子变量来解决,它可以保证对一个整形数据的原子性。

在内核看来,原子操作函数就像一条汇编语句,保证了操作时不被打断,如上述i++语句就可能被打断,要保证操作的原子性,通常需要原子地(不间断地)完成"读-修改-回写"机制,中间不能被打断。

二、原子变量

linux提供了atomic_t类型的原子变量,它的实现依赖于不同的架构,不同处理器的实现方式不一样。我们首先看下都有哪些原子操作可供使用,然后再针对arm64的实现方式进行解读(其他架构原理都类似,大家自己揣摩)。

2.1 原子操作函数

linux内核提供了很多操作原子变量的函数,了解这些内容,方便我们后续使用。我们以arm64为例进行讲解。

2.1.1 基本原子操作函数

接口:
ATOMIC_INIT(i)
atomic_read(const atomic_t *v)
atomic_set(atomic_t *v, int i)
实现:
// linux-6.9.1/arch/arm64/include/asm/atomic.h#define arch_atomic_read(v)             __READ_ONCE((v)->counter)
#define arch_atomic_set(v, i)           __WRITE_ONCE(((v)->counter), (i))

2.1.2 不带返回值的原子操作函数

接口:
atomic_add(i, v)
atomic_sub(i, v)
atomic_and(i, v)
atomic_or(i, v)
atomic_xor(i, v)
atomic_andnot(i, v)
实现
// linux-6.9.1/arch/arm64/include/asm/atomic.h#define ATOMIC_OP(op)                           \
static __always_inline void arch_##op(int i, atomic_t *v)       \
{                                   \__lse_ll_sc_body(op, i, v);                 \
}ATOMIC_OP(atomic_andnot)
ATOMIC_OP(atomic_or)
ATOMIC_OP(atomic_xor)
ATOMIC_OP(atomic_add)
ATOMIC_OP(atomic_and)
ATOMIC_OP(atomic_sub)

2.1.3 带返回值的原子操作

linux内核提供了两类带返回值的原子操作函数,一类返回原子变量的新值,一类返回原子变量的旧值。 然会原子变量新值的原子操作函数如下。

接口:
atomic_add_return(i, v)
atomic_sub_return(i, v)
实现;
// linux-6.9.1/arch/arm64/include/asm/atomic.h#define ATOMIC_FETCH_OP(name, op)                   \
static __always_inline int arch_##op##name(int i, atomic_t *v)      \
{                                   \return __lse_ll_sc_body(op##name, i, v);            \
}ATOMIC_FETCH_OPS(atomic_add_return)
ATOMIC_FETCH_OPS(atomic_sub_return)
返回原子变量旧值的原子操作函数如下:
接口:
atomic_fetch_add(i, v)
atomic_fetch_sub(i, v)
atomic_fetch_and(i, v)
atomic_fetch_or(i, v)
atomic_fetch_xor(i, v)
atomic_fetch_andnot(i, v)
实现:
// linux-6.9.1/arch/arm64/include/asm/atomic.h
#define ATOMIC_FETCH_OP(name, op)                   \
static __always_inline int arch_##op##name(int i, atomic_t *v)      \
{                                   \return __lse_ll_sc_body(op##name, i, v);            \
}ATOMIC_FETCH_OPS(atomic_fetch_andnot)
ATOMIC_FETCH_OPS(atomic_fetch_or)
ATOMIC_FETCH_OPS(atomic_fetch_xor)
ATOMIC_FETCH_OPS(atomic_fetch_add)
ATOMIC_FETCH_OPS(atomic_fetch_and)
ATOMIC_FETCH_OPS(atomic_fetch_sub)
ATOMIC_FETCH_OPS(atomic_add_return)
ATOMIC_FETCH_OPS(atomic_sub_return)

3.1.4 内嵌内存屏障的原子操作函数

接口:

{}_relexd     // 不内嵌内存屏障原语
{}_acquire    // 内置加载-获取内存屏障原语
{}_release    // 内置存储-释放内存屏障原语
实现:
// linux-6.9.1/arch/arm64/include/asm/atomic.h#define ATOMIC_FETCH_OP(name, op)                   \
static __always_inline int arch_##op##name(int i, atomic_t *v)      \
{                                   \return __lse_ll_sc_body(op##name, i, v);            \
}#define ATOMIC_FETCH_OPS(op)                        \ATOMIC_FETCH_OP(_relaxed, op)                   \ATOMIC_FETCH_OP(_acquire, op)                   \ATOMIC_FETCH_OP(_release, op)                   \ATOMIC_FETCH_OP(        , op)ATOMIC_FETCH_OPS(atomic_fetch_andnot)
ATOMIC_FETCH_OPS(atomic_fetch_or)
ATOMIC_FETCH_OPS(atomic_fetch_xor)
ATOMIC_FETCH_OPS(atomic_fetch_add)
ATOMIC_FETCH_OPS(atomic_fetch_and)
ATOMIC_FETCH_OPS(atomic_fetch_sub)
ATOMIC_FETCH_OPS(atomic_add_return)
ATOMIC_FETCH_OPS(atomic_sub_return)

2.2 原子操作的实现

2.2.1 原子操作的实现

原子操作的实现依赖处理器硬件提供支持,在不同的处理器体系结构上,原子操作会有不同的实现,例如在x86体系结构下,通常使用锁缓存/总线的方式实现原子操作。目前在ARMv8体系结构下支持两种方式来实现原子操作:

  • 一种是经典的独占内存访问机制,也叫做LL/SC(Load-Link/Store-Conditional),早期ARM体系结构下的原子操作都是基于这种方式实现;

  • 另一种是ARMv8.1体系结构上新增的LSE(Large System Extension)扩展,LSE提供了多种原子内存访问操作指令。

具体选择哪一种,CONFIG_ARM64_LSE_ATOMICS决定

// linux-6.9.1/arch/arm64/include/asm/lse.h
#ifdef CONFIG_ARM64_LSE_ATOMICS#define __LSE_PREAMBLE  ".arch_extension lse\n"#include <linux/compiler_types.h>
#include <linux/export.h>
#include <linux/stringify.h>
#include <asm/alternative.h>
#include <asm/alternative-macros.h>
#include <asm/atomic_lse.h>
#include <asm/cpucaps.h>#define __lse_ll_sc_body(op, ...)                   \
({                                  \alternative_has_cap_likely(ARM64_HAS_LSE_ATOMICS) ?     \__lse_##op(__VA_ARGS__) :               \__ll_sc_##op(__VA_ARGS__);              \
})/* In-line patching at runtime */
#define ARM64_LSE_ATOMIC_INSN(llsc, lse)                \ALTERNATIVE(llsc, __LSE_PREAMBLE lse, ARM64_HAS_LSE_ATOMICS)#else   /* CONFIG_ARM64_LSE_ATOMICS */#define __lse_ll_sc_body(op, ...)       __ll_sc_##op(__VA_ARGS__)#define ARM64_LSE_ATOMIC_INSN(llsc, lse)    llsc#endif  /* CONFIG_ARM64_LSE_ATOMICS */
#endif  /* __ASM_LSE_H */

2.2.2 ll/sc方式

LL/SC机制使用多个指令,并且每个处理器都需要实现一个专有监视器,LL/SC机制利用独占内存访问指令和独占监视器共同实现原子操作。首先看下ARMv8体系结构提供的独占内存访问指令。

独占内存访问指令

ARMv8体系结构实现的独占内存访问指令为LDXR/STXR:

  • LDXR:内存独占加载指令,它从内存中以独占方式加载内存地址的值到寄存器中;

  • STXR:内存独占存储指令,它以独占的方式把数据存储到内存中。 LDXR/STXR的指令格式如下:

ldxr    <xt>, [xn | sp] 
stxr    <ws>, <xt>, [xn | sp]

多字节独占内存访问指令

LDXP和STXP指令是多字节独占内存访问指令,一条指令可以独占地加载和存储16字节。

ldxp    <xt1>, <xt2>, [xn | sp]
stxp    <ws>, <xt1>, <xt2>, [<xn | sp>]

独占监视器

独占监视器是一个硬件状态机,用于跟踪读-修改-写序列,并支持Load和Store操作。当CPU执行LDXR指令时,独占监视器会把对应内存地址标记为独占访问模式,保证以独占的方式来访问这个内存地址;而STXR是有条件的存储指令,当CPU执行STRX指令将新数据写入到LDXR指令标记的独占访问内存地址时,会根据独占监视器的状态来进行处理:

  • 若独占监视器为独占访问状态,那么STRX指令执行成功,并且独占监视器会切换状态到开放访问状态;

  • 若独占监视器为开放访问状态,则STRX指令执行失败,数据无法存储。

ARMv8体系提供了三类独占监视器:

  • 本地独占监视器

  • 内部缓存一致性全局独占监视器

  • 外部全局独占监视器

这些独占监视器分别位于系统存储结构的不同层次,如下

atomic_op实现:
// linux-6.9.1/arch/arm64/include/asm/atomic_ll_sc.h
#define ATOMIC_OP(op, asm_op, constraint)               \
static __always_inline void                             \
__ll_sc_atomic_##op(int i, atomic_t *v)                 \
{                                                       \unsigned long tmp;                                  \int result;                                         \\asm volatile("// atomic_" #op "\n"                  \"   prfm    pstl1strm, %2\n"                        \"1: ldxr    %w0, %2\n"                              \"   " #asm_op " %w0, %w0, %w3\n"                    \"   stxr    %w1, %w0, %2\n"                         \"   cbnz    %w1, 1b\n"                              \: "=&r" (result), "=&r" (tmp), "+Q" (v->counter)    \: __stringify(constraint) "r" (i));                 \
}

第11行:将v->counter的值以内存独占加载的方式存储到w0寄存器,即result = v->counter

第12行:将w0的值和i的值操作(add/sub等)结果保存在w0,即result = result + i

第13行:将w0的值写回v->counter,成功的为给w1赋0,否则等于1

第14行:判断temp的值,为0代表成功;为1代表失败,跳转到ldxr。

说白了,这里也是一个自旋

2.2.3 lse方式

在ARMV8.1指令集中增加了一些新的原子操作指令,可以一个指令实现整形运算。

新增的整形原子指令:

接口:
stclr
stset
steor
stadd实现:
#define ATOMIC_OP(op, asm_op)                       \
static __always_inline void                     \
__lse_atomic_##op(int i, atomic_t *v)                   \
{                                   \asm volatile(                           \__LSE_PREAMBLE                          \"   " #asm_op " %w[i], %[v]\n"              \: [v] "+Q" (v->counter)                     \: [i] "r" (i));                         \
}ATOMIC_OP(andnot, stclr)
ATOMIC_OP(or, stset)
ATOMIC_OP(xor, steor)
ATOMIC_OP(add, stadd)

三、总结

本篇文章首先根据一个真实的事例引出原子操作要解决的问题,然后对linux提供的原子操作的众多接口进行了解释说明,最后对arm架构上的两种原子操作的实现方式原理LL/SC、LSE进行了剖析。经过上面的学习,大家应该已经了解原子变量的使用场景以及内部的实现机理。

参考: https://jishuzhan.net/article/1763876122459639809

《奔跑吧,linux内核-卷一基础架构》

《奔跑吧,linux内核-卷二调试与案例分析》

下篇文章,将经典自旋锁进行解读,敬请期待

一个专注于“嵌入式知识分享”、“DIY嵌入式产品”的技术开发人员,关注我,一起共创嵌入式联盟。

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

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

相关文章

多表执行嵌套查询,减少笛卡尔积,防止内存溢出

问题&#xff1a;当涉及四个表的查询时&#xff0c;会产生大量的笛卡尔积导致内存溢出。 解决办法 &#xff1a;可以使用嵌套查询将多表的联合查询拆分为单个表的查询&#xff0c;使用resultmap中的association&#xff08;适合一对一&#xff09; 或 collection&#xff08;一…

医院消防设施设备管理系统

医院为人员密集场所&#xff0c;且多为各类病患及其陪护人员&#xff0c;一旦发生火灾&#xff0c;人员疏散逃生困难&#xff0c;容易造成较严重的生命与财产损失。为规范医院的消防设施设备管理&#xff0c;通过凡尔码系统对医院消防设施设备进行信息化管理&#xff0c;提高医…

MapReduce学习

目录 7.3 MapReduce工作流程 7.3.1 工作流程概述 7.3.2 MapReduce各个执行阶段 7.3.3 Shuffle过程详解 1. Shuffle过程简介&#xff08;过程分为Map端的操作和Reduce端的操作&#xff09; 2、Map端的Shuffle过程&#xff1a; 3、在Reduce端的Shuffle过程 7.4 实例分析&am…

使用supportFragmentManager管理多个fragment切换

android studio创建的项目就没有一个简单点的框架&#xff0c;生成的代码都是繁琐而复杂&#xff0c;并且不实用。 国内的页面一般都是TAB页面的比较多&#xff0c;老外更喜欢侧边菜单。 如果我们使用一个activity来创建程序&#xff0c;来用占位符管理多个fragment切换&…

五、Spring IoCDI ★ ✔

5. Spring IoC&DI 1. IoC & DI ⼊⻔1.1 Spring 是什么&#xff1f;★ &#xff08;Spring 是包含了众多⼯具⽅法的 IoC 容器&#xff09;1.1.1 什么是容器&#xff1f;1.1.2 什么是 IoC&#xff1f;★ &#xff08;IoC: Inversion of Control (控制反转)&#xff09;总…

Python逻辑控制语句 之 判断语句--if、if else 和逻辑运算符结合

逻辑运算符&#xff1a; and or not 1.案例一 需求&#xff1a; 1. 获取⽤户输⼊的⽤户名和密码 2. 判断⽤户名是 admin 并且密码是 123456 时, 在控制台输出: 登录成功! 3. 否则在控制台输出: 登录信息错误! # 需求&#xff1a; # 1. 获取用户输入的用户名和密码 # 2. 判断…

【折腾笔记】兰空图床使用Redis做缓存

前言 最近发现我部署在群晖NAS上的兰空图床程序在高并发的情况下会导致图片加载缓慢或出现图片加载失败的情况&#xff0c;于是我查阅了官方文档资料并进行了一系列的测试&#xff0c;发现兰空图床如果开启了原图保护功能&#xff0c;会非常的吃CPU的性能&#xff0c;尤其是在…

【Python游戏】猫和老鼠

本文收录于 《一起学Python趣味编程》专栏,从零基础开始,分享一些Python编程知识,欢迎关注,谢谢! 文章目录 一、前言二、代码示例三、知识点梳理四、总结一、前言 本文介绍如何使用Python的海龟画图工具turtle,开发猫和老鼠游戏。 什么是Python? Python是由荷兰人吉多范…

【限免】线性调频信号的脉冲压缩及二维分离SAR成像算法【附MATLAB代码】

文章来源&#xff1a;微信公众号&#xff1a;EW Frontier QQ交流群&#xff1a;949444104 程序一 对线性调频信号进行仿真&#xff0c;输出其时频域的相关信息&#xff0c;并模拟回波信号&#xff0c; 对其进行脉冲压缩和加窗处理。 实验记录&#xff1a; 1.线性调频信号时…

从0构建一个录制UI测试工具

很多UI自动化测试工具都具备录制UI自动化测试的能力&#xff0c;例如playwright&#xff0c;可以通过playwright vscode插件完成录制&#xff0c;如下图所示&#xff0c;当选择录制脚本时&#xff0c;会打开一个浏览器&#xff0c;在浏览器中输入被测应用url&#xff0c;用户在…

C++:enum枚举共用体union

enum枚举 C继承C的枚举用法 (1)典型枚举类型定义&#xff0c;枚举变量定义和使用 (2)枚举类型中的枚举值常量不能和其他外部常量名称冲突&#xff1a; 举例1宏定义&#xff0c;举例2另一个枚举 // 定义一个名为Color的枚举类型 enum Color {RED, // 红色&#xff0c;默认值…

昇思25天学习打卡营第11天|SSD目标检测

1. 学习内容复盘 模型简介 SSD&#xff0c;全称Single Shot MultiBox Detector&#xff0c;是Wei Liu在ECCV 2016上提出的一种目标检测算法。使用Nvidia Titan X在VOC 2007测试集上&#xff0c;SSD对于输入尺寸300x300的网络&#xff0c;达到74.3%mAP(mean Average Precision)…

JAVA毕业设计145—基于Java+Springboot+vue+uniapp的驾校预约小程序(源代码+数据库+15000字论文)

毕设所有选题&#xff1a; https://blog.csdn.net/2303_76227485/article/details/131104075 基于JavaSpringbootvueuniapp的驾校预约小程序(源代码数据库15000字论文)145 一、系统介绍 本项目前后端分离&#xff0c;分为用户、教练、管理员三种角色 1、用户&#xff1a; …

ModuleNotFoundError: No module named ‘_sysconfigdata_x86_64_conda_linux_gnu‘

ModuleNotFoundError: No module named _sysconfigdata_x86_64_conda_linux_gnu 1.软件环境⚙️2.问题描述&#x1f50d;3.解决方法&#x1f421;4.结果预览&#x1f914; 1.软件环境⚙️ Ubuntu 20.04 Python 3.7.0 2.问题描述&#x1f50d; 今天发现更新conda之后&#xff0…

Redisson(分布式锁、限流)

注意Redisson是基于Redis的&#xff0c;所以必须先引入Redis配置&#xff08;参考SpringBoot集成Redis文章&#xff09; 1. 集成Redisson 引入依赖 <!-- 二选一,区别是第一个自动配置&#xff0c;第二个还需要手动配置也就是第二步自定义配置&#xff0c;注意版本号&…

Windows怎么实现虚拟IP

在做高可用架构时&#xff0c;往往需要用到虚拟IP&#xff0c;在linux上面有keepalived来实现虚拟ip的设置。在windows上面该怎么弄&#xff0c;keepalived好像也没有windows版本&#xff0c;我推荐一款浮动IP软件PanguVip&#xff0c;它可以实现windows上面虚拟ip的漂移。设置…

UE5材质之HLSL:深度

UE4/5的Custom节点&#xff1a;在VScode使用HLSL&#xff08;新手入门用&#xff09;_vscode写hlsl-CSDN博客 效果&#xff1a; 材质节点&#xff1a; 自定义节点代码&#xff1a; float3 rayStepViewDir*-1; float4 inputTexTexture2DSample(TexObject,TexObjectSampler,uv)…

JavaSE主要内容(全套超完整)

一、为什么选择Java&#xff08;Java的优势&#xff09; 1、应用面广&#xff1a; 相较于其他语言&#xff0c;Java的应用面可谓是非常广&#xff0c;这得益于他的跨平台性和其性能的稳定性。他在服务器后端&#xff0c;Android应用开发&#xff0c;大数据开发&#xf…

Jmeter性能场景设计

为什么会有性能场景设计呢&#xff1f; 相信有部分同学对场景设计优点模糊&#xff0c;前面博文提到的是场景提取 场景设计&#xff1a;在压测的过程中怎么设置线程数、Ramp-Up时间(秒)、循环次数等等 一、 性能场景分类 场景的概念&#xff1a; a. 单场景 b. 混合场景 c. 容…

开源项目-商城管理系统

哈喽,大家好,今天主要给大家带来一个开源项目-商城管理系统 商城管理系统分前后端两部分。前端主要有商品展示,我的订单,个人中心等内容;后端的主要功能包括产品管理,门店管理,会员管理,订单管理等模块 移动端页面