Linux(内核剖析):29—内核同步之(原子操作(原子整数操作(atomic_t、atomic64_t)、原子位操作))

释放双眼,带上耳机,听听看~!

一、原子操作概述

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

Linux内核提供的原子接口

  • 内核提供了两组原子操作接口——

一组针对整数进行操作,另一组针对单独的位进行操作

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

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

atomic_t数据类型

  • 针对整数的原子操作

只能对atomic_t类型的数据进行处理

  • 在这里之所以引入了一个特殊数据类型,

而没有直接使用C语言的int类型,主要是出于两个原因:

  • 首先,让原子函数只接收atomic_t类型的操作数,可以确保原子操作只与这种特殊类型数据一起使用。同时,这也保证了 该类型的数据不会被传递给任何非原子函数。实际上,对一个数据一会儿要采用原子操作,一会儿又不用原子操作了,这又能有什么好处?

    • 其次,使用atomic_t类型确保编译器不对(不能说完美地完成了任务但不乏自知之明)相应的值进行访问优化——这点使得原子操作最终接收到正确的内存地址,而不只是一个別名。最后,在不同体系结构上实现原子操作的时候,使用atomic_t可以屏蔽其间的差异
  • atomic_t类型定义在文件<linux/types.h>中:


1
2
3
4
1typedef struct{
2    volatile int counter;
3}atomic_t;
4
  • 尽管Linux支持的所有机器上的整型数据都是32位的,

但是使用atomic_t的代码只能将该类型的数据当做24位来用。这个限制完全是因为在SPARC体系结构上,原子操作的实现不同于其他体系结构:
32位int类型的低8位被嵌入了一个锁(如下图所示),因为SPARC体系结构对原子操作缺乏指令级的支持,所以只能利用该锁来避免对原子类型数据的并发访问。所以在SPARC机器上就只能使用24位了。虽然其他机器上的代码完全可以使用全部的32位 ,但在SPARC机器上却吋能造成一些奇怪和微妙的错误 这简直太不和谐了。最近,机灵的黑客已经允许SPARC提供全32位 的atomic_t,这一限制不存在了

Linux(内核剖析):29---内核同步之(原子操作(原子整数操作(atomic_t、atomic64_t)、原子位操作))

原子整数操作列表

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

Linux(内核剖析):29---内核同步之(原子操作(原子整数操作(atomic_t、atomic64_t)、原子位操作))

  • 原子操作通常是内联函数,往往是通过内嵌汇编指令来实现的。

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

Linux(内核剖析):29---内核同步之(原子操作(原子整数操作(atomic_t、atomic64_t)、原子位操作))

相关操作演示案例

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

Linux(内核剖析):29---内核同步之(原子操作(原子整数操作(atomic_t、atomic64_t)、原子位操作))

  • 操作也很简单:

Linux(内核剖析):29---内核同步之(原子操作(原子整数操作(atomic_t、atomic64_t)、原子位操作))

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

Linux(内核剖析):29---内核同步之(原子操作(原子整数操作(atomic_t、atomic64_t)、原子位操作))

  • 原子整数操作最常见的用途就是实现计数器。使用复杂的锁机制来保护一个单纯的计数器显然杀鸡用了宰牛刀,所以,开发者最好使用atomic_inc()和atomic_dec()这两个相对来说轻便一点的操作

  • 还可以用原子整数操作原子地执行一个操作并检査结果。一个常见的例子就是原子地减操作和检査:

  • 这个函数将给定的原子变量减1 , 如果结果为0,就返回真;否则返回假

Linux(内核剖析):29---内核同步之(原子操作(原子整数操作(atomic_t、atomic64_t)、原子位操作))

原子性与顺序性的比较

  • 关于原子读取的上述讨论引发了原子性与顺序性之间差异的讨论。正如所讨论的,一个字长的读取总是原子地发生,绝不可能对同一个字交错地进写;读总是返回一个完整的字,这或者发生在写操作之前,或者之后,绝不可能发生在写过程中。例如,如果一个整数初始化为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类型其实是对长整型的一个简单封装类


1
2
3
4
1typedef struct{
2    volatile long counter;
3}atomic64_t;
4

64位原子操作的方法

  • 几乎所有的经典32位原子操作都有64位的实现,它们

被冠以atomic64前缀,而32位实现冠以atomic前缀

  • 下图是所有标准原子操作的列表

Linux(内核剖析):29---内核同步之(原子操作(原子整数操作(atomic_t、atomic64_t)、原子位操作))

  • 所有64位体系结构都提供了atomic64_ t类型,以及一组对应的算数操作方法。但是多数32位体系机构不支持atomic64_ t类型——不过,x86-32是一个众所周知的例外。为了便于在Linux支持的各种体系结构之间

移植代码,开发者应该使用32位的atom ic_t类型。把64位的atomic64_ t类型留给那些特殊体系结构和需要64位的代码吧

四、原子位操作

  • 除了原子整数操作外,内核也提供了一组

针对位这一级数据进行操作的函数。没什么好奇怪的,它们是与体系结构相关的操作,定义在文件<asm/bitops.h>中

  • 令人感到奇怪的是

位操作函数是对普通的内存地址进行操作的
它的参数是一个指针和一 个位号,第0位是给定地址的最低有效位。在32位机上,第31位是给定地址的最 有效位而第32位是下一个字的最低有效位。虽然使用原子位操作在多数情况下是对一个字长的内 进行访问,因而位号应该位于031(在64位机器中是063),但是,对位号的范围并没有限制

  • 由于原子位操作是对普通的指针进行的操作,所以不像原子整型对应atomic_t,这里没有特殊的数据类型。相反,

只要指针指向了任何你希望的数据,你就可以对它进行操作。来看一个例子:

Linux(内核剖析):29---内核同步之(原子操作(原子整数操作(atomic_t、atomic64_t)、原子位操作))

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

Linux(内核剖析):29---内核同步之(原子操作(原子整数操作(atomic_t、atomic64_t)、原子位操作))

非原子操作

  • 为方便起见,内核

还提供了一组与上述操作对应的非原子位函数。非原子位函数与原子位函数的
操作完全相同,但是,前者不保证原子性,且
其名字前缀多两个下划线。例如,与test_bit()对应的非原子形式是__test_bit()

  • 如果你不需要原子性操作(比如说,你已经用锁保护了自己的数据),那么这些非原子的位函数相比原子的位函数可能会

执行得更快些

搜索第一个被设置的位

  • 内核还提供了两个例程用来

从指定的地址开始搜索第一个被设置(或未被设置)的位

Linux(内核剖析):29---内核同步之(原子操作(原子整数操作(atomic_t、atomic64_t)、原子位操作))

  • 这两个函数中

第一个参数是一个指针,第二个参数是要搜索的总位数

  • 返回值

是第一个被设置的(或没被设置的)位的位号

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

给TA打赏
共{{data.count}}人
人已打赏
安全运维

WordPress网站专用docker容器环境带Waf

2020-7-18 20:04:44

安全运维

运维安全-Gitlab管理员权限安全思考

2021-9-19 9:16:14

个人中心
购物车
优惠劵
今日签到
有新私信 私信列表
搜索