

Linux学习第27节,内核中的原子操作
source link: https://blog.popkx.com/linux-learn-section-27-atomic-operations-in-the-kernel/
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

Linux学习第27节,内核中的原子操作
前面20多节的文章在分析 Linux 内核设计与C语言代码实现时,常会遇到全局变量。全局变量显然属于多个函数的共享资源,因此若想安全的使用它,必须做好同步。事实上,Linux 内核也确实提供了一些用于同步共享资源的接口,不过之前的文章都对此避而不谈,接下来几节将尝试学习一下 Linux 内核同步方法。
在讨论 Linux 内核同步方法之前,先来了解一下原子操作,因为原子操作是内核其他同步方法的基石。那么什么是原子操作呢?相信大家都知道,原子是组成物体的不可再分割的微粒,那与之对应,原子操作就是不能再被分割的指令。原子操作的意义是什么呢?请考虑这个问题:
假设在某个C语言程序开发中,定义了一个全局变量 i,这时有两个线程对 i 执行“加一”的操作(即执行 i++)。
假设 i 的初值为 0,我们自然期望两个线程是下面这种执行流程:
但是若两个线程没有对全局变量 i 做任何同步操作,实际上可能是下面这样的执行流程:
线程1和线程2可能都在全局变量 i 的值增加之前读取到了它的初值,这就可能导致出现不期望的结果:当两个线程执行完毕后,全局变量 i 的值本来应该是 2 的,结果却为 1 了。
不过,如果使用了原子操作,上面这种竞争情况就不会出现了,整个过程只有可能是下面这两种情况:
最后必定会得到预期结果(i==2),因为两个原子操作绝对不可能并发的访问同一个变量,也即不会出现上面那种“竞争”现象。
Linux 内核提供了两组原子操作接口,一组是针对整数的,一组是针对单个位操作的。
原子整数操作
Linux 内核为整数原子操作专门定义了 atiomic_t 类型,它是因平台而异的,在x86平台下,它的C语言代码定义如下,请看:
typedef struct {
int counter;
} atomic_t;
显然,atomic_t 其实就是个只有一个 int 成员的结构体,Linux 内核这么定义整数原子操作的数据类型,主要就是为了区分非原子操作类型。
定义一个 atomic_t 类型的数据就很简单了,直接将 atomic_t 当作C语言中一个普通的结构体就可以了,例如:
atomic_t a;
atomic_t b = ATOMIC_INIT(0);
上面定义了原子类型 b,并对其赋了初值 0,ATMOIC_INIT 是一个宏,在 x86 平台,它的 C语言代码如下:
#define ATOMIC_INIT(i) { (i) }
定义好原子变量后,若是想使用它,可以使用 Linux 内核提供的这几个接口,请看下面的C语言示范代码:
// a = 4
atomic_set(&a, 4);
// a = a+2
atomic_add(2, &a);
// a ++
atomic_inc(&a);
// 读取
int a = atomic_read(v);
在 x86 平台,atomic_set() 和 atomic_read() 的C语言定义很简单,请看:
#define atomic_set(v, i) (((v)->counter) = (i))
#define atomic_read(v) ((v)->counter)
其实就是直接赋值和直接读取而已,x86 架构在物理上保证了这两个操作的原子性。atomic_inc() 和 atomic_add() 函数的C语言定义稍微复杂一点,请看:
static inline void atomic_inc(atomic_t *v)
{
asm volatile(LOCK_PREFIX "incl %0"
: "+m" (v->counter));
}
static inline void atomic_add(int i, atomic_t *v)
{
asm volatile(LOCK_PREFIX "addl %1,%0"
: "+m" (v->counter)
: "ir" (i));
}
显然,这段C语言代码是使用内嵌汇编完成的,与 atomic_add() 函数对应的atomic_sub() 函数的C语言定义也是类似的。原子整数操作最常见的用途就是实现计数器,因为如果使用其他复杂的方法去保护一个单纯的计数器,明显就是高射炮打蚊子,大材小用了。
原子位操作
再来看看 Linux 内核关于原子位操作的设计与实现,内核没有为原子位操作定义新的专用的数据类型,最常用的几个操作是 set_bit(),clear_bit(),以及 test_bit() 函数,它们的C语言定义如下:
static inline void set_bit(int nr, volatile void *addr)
{
asm volatile(LOCK_PREFIX "bts %1,%0" : ADDR : "Ir" (nr) : "memory");
}
static inline void clear_bit(int nr, volatile void *addr)
{
asm volatile(LOCK_PREFIX "btr %1,%0" : ADDR : "Ir" (nr));
}
#define test_bit(nr, addr) \
(__builtin_constant_p((nr)) \
? constant_test_bit((nr), (addr)) \
: variable_test_bit((nr), (addr)))
看到这里,读者可能有些疑惑,位操作不存在发生矛盾的可能性吧?那原子位操作存在的意义是什么呢?原子操作意味着指令会完整的执行,或者完全不执行。
假设有两个原子位操作,第一个操作是将 a 的 bit 3 置零,第二个操作是将 a 的 bit 3 置一。那么显然,在第一个操作完成之后,第二个操作进行之前,a 的 bit 3 必定为零,当第二个操作完成后,a 的 bit 3 必定为一。也就是说,所有的中间结果都是可预知的,都是正确无误的。
如果对变量 a 的 bit 3 先置零,再置一的两个操作不是原子操作,那么 a 的 bit 3 最后可能的确等于一了,但是中间可能根本没有被置零过,因为两个操作可能同时发生,导致 a 的 bit 3 置零失败了。这在操作硬件寄存器的时候,是绝对不能容忍的。
本节先介绍了Linux 内核 C语言开发中,共享资源的竞争问题,接着讨论了内核中关于原子整数操作和原子位操作的设计与实现。
Recommend
-
65
-
61
前言 对于Java多线程,接触最多的莫过于使用synchronized,这个简单易懂,但是这synchronized并非性能最优的。今天我就简单介绍一下几种锁。可能我...
-
49
一:原子操作CAS(compare-and-swap) 原子操作分三步:读取addr的值,和old进行比较,如果相等,则将new赋值给*addr,他能保证这三步一起执行完成,叫原子操作也就是说它不能再分了,当有一个CPU在访问这块内容addr时,其他CPU就...
-
88
原子操作 对于一个Go程序来说,GO语言运行时系统中的调度器会恰当的安排其中所有的goroutine的运行。不过,在同一时刻,只会有少数的goroutine真正处于运行状态。为了公平起见,调度器会频繁的切换这些goroutine。这个中断的...
-
33
概念 原子操作,意思就是执行的过程不能背终端的操作。在针对某个值的原子操作执行过程中,cpu不会再去执行其他针对这个值得操作。在底层, 这会由CPU提供芯片级别的支持 ,所以绝对有效。即使在拥...
-
36
这个系列的文章里介绍了很多并发编程里经常用到的技术,除了 Context 、计时器、互斥锁还有通道外还有一种技术-- 原子操作 在一些同步算法中会被用到。今天的文章里我们会简单了解一下 Go...
-
22
前言 所谓原子操作,就是要么不做,要么全做。在很多场景中,都有对原子操作的需求。在翻aep的spec文档时...
-
24
上一节较为详细的介绍了 linux 内核中链表的设计与实现,能够看出,内核实际上是将链表“塞入”数据结构的。事实上,为了方便的操作这些链表,linux内核实现了一系列方法,本节将了解此。
-
12
stdatomic 已经是 C11 的标准,并且成为了 C++ 标准的一部分。
-
7
32位系统下,Go标准库中atomic原子操作int64有崩溃bug 发表于 2021-03-12 | 分类于 Go| 字数统计: 714下面这个demo,在32位系统(我测试的运行环境:32位arm linux)会崩...
About Joyk
Aggregate valuable and interesting links.
Joyk means Joy of geeK