Linux(内核剖析):29---内核同步之(原子操作(原子整数操作(atomic_t、atomic64_t)、原子位操作))
发布日期:2021-06-29 22:33:21 浏览次数:2 分类:技术文章

本文共 3409 字,大约阅读时间需要 11 分钟。

一、原子操作概述

  • 原子操作可以保证指令以原子的方式执行——执行过程不被打断。众所周知,原子原本指的是不可分割的微粒,所以原子操作也就是不能够被分割的指令

Linux内核提供的原子接口

  • 内核提供了两组原子操作接口——一组针对整数进行操作,另一组针对单独的位进行操作
  • 在Linux支持的所有体系结构上都实现了这两组接口。大多数体系结构会提供支持原子操作的简单算术指令。而有些体系结构确实缺少简单的原子操作指令,但是也为单步执行提供了锁内存总线的指令,这就确保了其他改变内存的操作不能同时发生

二、原子整数操作(32位)

atomic_t数据类型

  • 针对整数的原子操作只能对atomic_t类型的数据进行处理
  • 在这里之所以引入了一个特殊数据类型,而没有直接使用C语言的int类型,主要是出于两个原因:
    • 首先,让原子函数只接收atomic_t类型的操作数,可以确保原子操作只与这种特殊类型数据一起使用。同时,这也保证了 该类型的数据不会被传递给任何非原子函数。实际上,对一个数据一会儿要采用原子操作,一会儿又不用原子操作了,这又能有什么好处?
    • 其次,使用atomic_t类型确保编译器不对(不能说完美地完成了任务但不乏自知之明)相应的值进行访问优化——这点使得原子操作最终接收到正确的内存地址,而不只是一个別名。最后,在不同体系结构上实现原子操作的时候,使用atomic_t可以屏蔽其间的差异
  • atomic_t类型定义在文件<linux/types.h>中:
typedef struct{    volatile int counter;}atomic_t;
  • 尽管Linux支持的所有机器上的整型数据都是32位的,但是使用atomic_t的代码只能将该类型的数据当做24位来用。这个限制完全是因为在SPARC体系结构上,原子操作的实现不同于其他体系结构:32位int类型的低8位被嵌入了一个锁(如下图所示),因为SPARC体系结构对原子操作缺乏指令级的支持,所以只能利用该锁来避免对原子类型数据的并发访问。所以在SPARC机器上就只能使用24位了。虽然其他机器上的代码完全可以使用全部的32位 ,但在SPARC机器上却吋能造成一些奇怪和微妙的错误 这简直太不和谐了。最近,机灵的黑客已经允许SPARC提供全32位 的atomic_t,这一限制不存在了

原子整数操作列表

  • 下标列出了所 有的标准原子整数操作(所有体系结构都包含这些操作)。某种特定的体系结构上实现的所有操作可以在文件<asm/atomic.h>中找到

  • 原子操作通常是内联函数,往往是通过内嵌汇编指令来实现的。如果某个函数本来就是原子的,那么它往往会被定义成一个宏。例如,在大部分体系结构上,读取一个字本身就是一种原子操作,也就是说,在对一个字进行写入操作期间不可能完成对该字的读取。这样,把 atomic_ read()定义成一个宏,只须返回atomic_t类型的整数值就可以了

相关操作演示案例

  • 定义一个atomic_t类型的数据方法很平常,还可以在定义时给它设定初值:

  • 操作也很简单:

  • 如果需要将atomic_t转为int型,可以使用atomic_read()来完成:

  • 原子整数操作最常见的用途就是实现计数器。使用复杂的锁机制来保护一个单纯的计数器显然杀鸡用了宰牛刀,所以,开发者最好使用atomic_inc()和atomic_dec()这两个相对来说轻便一点的操作
  • 还可以用原子整数操作原子地执行一个操作并检査结果。一个常见的例子就是原子地减操作和检査:
    • 这个函数将给定的原子变量减1 , 如果结果为0,就返回真;否则返回假

原子性与顺序性的比较

  • 关于原子读取的上述讨论引发了原子性与顺序性之间差异的讨论。正如所讨论的,一个字长的读取总是原子地发生,绝不可能对同一个字交错地进写;读总是返回一个完整的字,这或者发生在写操作之前,或者之后,绝不可能发生在写过程中。例如,如果一个整数初始化为42,然后又置为365,那么读取这个整数肯定会返回42或者365,而绝不会是二者的混合。这就是我们所谓的原子性
  • 也许代码比这有更多的要求。或许要求读必须在待定的写之前发生——这种需求其实不属于原子性要求,而是顺序要求。原子性确保指令执行期间不被打断,要么全部执行完,要么根本不执行。另一方面,顺序性确保即使两条或多条指令出现在独立的执行线程中,甚至独立的处理器上,它们本该的执行顺序却依然要保持
  • 在本小节讨论的原子操作只保证原子性。顺序性通过屏障(barrier) 指令来实施,这将在后面文章介绍
  • 在编写代码的时候,能使用原子操作时,就尽量不要使用复杂的加锁机制。对多数体系结构来讲,原子操作与更复杂的同步方法相比较,给系统带来的开销小,对高速缓存行(cache-line)的影响也小。但是,对于那些有高性能要求的代码,对多种同步方法进行测试比较,不失为一种明智的做法

三、64位原子操作

atomic64_t

  • 随着64位体系结构越来越普及,内核开发者确实在考虑原子变量除32位atomic_t类型外, 应引入64位的atomic64_ t
  • 因为移植性原因,atomic_t变量大小无法在体系结构之间改变。所以,atomic_t类型即便在64位体系结构下也是32位的,若要使用64位的原子变量,则要使用atomic64_ t类型——其功能和其32位的兄弟无异,使用方法完全相同,不同的只有整型变量大小32位变成了64位
  • 与atomic_t一样,atomic64_2类型其实是对长整型的一个简单封装类
typedef struct{    volatile long counter;}atomic64_t;

64位原子操作的方法

  • 几乎所有的经典32位原子操作都有64位的实现,它们被冠以atomic64前缀,而32位实现冠以atomic前缀
  • 下图是所有标准原子操作的列表

  • 所有64位体系结构都提供了atomic64_ t类型,以及一组对应的算数操作方法。但是多数32位体系机构不支持atomic64_ t类型——不过,x86-32是一个众所周知的例外。为了便于在Linux支持的各种体系结构之间移植代码,开发者应该使用32位的atom ic_t类型。把64位的atomic64_ t类型留给那些特殊体系结构和需要64位的代码吧

四、原子位操作

  • 除了原子整数操作外,内核也提供了一组针对位这一级数据进行操作的函数。没什么好奇怪的,它们是与体系结构相关的操作,定义在文件<asm/bitops.h>中
  • 令人感到奇怪的是位操作函数是对普通的内存地址进行操作的它的参数是一个指针和一 个位号,第0位是给定地址的最低有效位。在32位机上,第31位是给定地址的最 有效位而第32位是下一个字的最低有效位。虽然使用原子位操作在多数情况下是对一个字长的内 进行访问,因而位号应该位于0~31(在64位机器中是0~63),但是,对位号的范围并没有限制
  • 由于原子位操作是对普通的指针进行的操作,所以不像原子整型对应atomic_t,这里没有特殊的数据类型。相反,只要指针指向了任何你希望的数据,你就可以对它进行操作。来看一个例子:

  • 下表给出了标准原子位操作列表

非原子操作

  • 为方便起见,内核还提供了一组与上述操作对应的非原子位函数。非原子位函数与原子位函数的操作完全相同,但是,前者不保证原子性,且其名字前缀多两个下划线。例如,与test_bit()对应的非原子形式是__test_bit()
  • 如果你不需要原子性操作(比如说,你已经用锁保护了自己的数据),那么这些非原子的位函数相比原子的位函数可能会执行得更快些

搜索第一个被设置的位

  • 内核还提供了两个例程用来从指定的地址开始搜索第一个被设置(或未被设置)的位

  • 这两个函数中第一个参数是一个指针,第二个参数是要搜索的总位数
  • 返回值是第一个被设置的(或没被设置的)位的位号
  • 如果你的搜索范围仅限于一个字,使用_ffs()和ffe()这两个函数更好,它们只需要给定一个要搜索的地址做参数
  • 与原子整数操作不同,代码一般无法选择是否使用位操作,它们是唯一的、具有可移植性的设置特定位方法,需要选择的是使用原子位操作还是非原子位操作。如果你的代码本身已经避免了竞争条件,你可以使用非原子位操作,通常这样执行得更快,当然,这还要取决于具体的体系结构

转载地址:https://dongshao.blog.csdn.net/article/details/104074027 如侵犯您的版权,请留言回复原文章的地址,我们会给您删除此文章,给您带来不便请您谅解!

上一篇:C++(数据结构与算法):49---平衡搜索树总体概述
下一篇:Linux(内核剖析):28---内核同步之(临界区、竞争条件、同步、锁、常见的内核并发、SMNP和UP配置选项、锁的争用和扩展性(锁粒度))

发表评论

最新留言

路过,博主的博客真漂亮。。
[***.116.15.85]2024年04月13日 05时39分55秒

关于作者

    喝酒易醉,品茶养心,人生如梦,品茶悟道,何以解忧?唯有杜康!
-- 愿君每日到此一游!

推荐文章