14

Linux学习第27节,内核中的原子操作

 3 years ago
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.
neoserver,ios ssh client

Linux学习第27节,内核中的原子操作

发表于 2019-03-01 20:03:53   |   已被 访问: 623 次   |   分类于:   Linux笔记   |   暂无评论

前面20多节的文章在分析 Linux 内核设计与C语言代码实现时,常会遇到全局变量。全局变量显然属于多个函数的共享资源,因此若想安全的使用它,必须做好同步。事实上,Linux 内核也确实提供了一些用于同步共享资源的接口,不过之前的文章都对此避而不谈,接下来几节将尝试学习一下 Linux 内核同步方法。

48e1b32ecdf6a714812b4a49a5c82d44.png

在讨论 Linux 内核同步方法之前,先来了解一下原子操作,因为原子操作是内核其他同步方法的基石。那么什么是原子操作呢?相信大家都知道,原子是组成物体的不可再分割的微粒,那与之对应,原子操作就是不能再被分割的指令。原子操作的意义是什么呢?请考虑这个问题:

假设在某个C语言程序开发中,定义了一个全局变量 i,这时有两个线程对 i 执行“加一”的操作(即执行 i++)。

假设 i 的初值为 0,我们自然期望两个线程是下面这种执行流程:

a702c4dd4211970b9d0da196c5f0b767.png

但是若两个线程没有对全局变量 i 做任何同步操作,实际上可能是下面这样的执行流程:
b2b3db3be964de0f5edca037671464f6.png

线程1和线程2可能都在全局变量 i 的值增加之前读取到了它的初值,这就可能导致出现不期望的结果:当两个线程执行完毕后,全局变量 i 的值本来应该是 2 的,结果却为 1 了。

不过,如果使用了原子操作,上面这种竞争情况就不会出现了,整个过程只有可能是下面这两种情况:

f11a57f89572f8cacb5e4736a041e14d.png

最后必定会得到预期结果(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) }
721f8a1e9987ec6ea71e0948293ca151.png

定义好原子变量后,若是想使用它,可以使用 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)
3dcdb3f8f5f0b900ac7be4991d65fe19.png

其实就是直接赋值和直接读取而已,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));
}
a7ceac94f85bb67be337600714fee5eb.png

显然,这段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)))
d9ab4bb62bdd96d01ab5a9fbb1409ad8.png

看到这里,读者可能有些疑惑,位操作不存在发生矛盾的可能性吧?那原子位操作存在的意义是什么呢?原子操作意味着指令会完整的执行,或者完全不执行。

假设有两个原子位操作,第一个操作是将 a 的 bit 3 置零,第二个操作是将 a 的 bit 3 置一。那么显然,在第一个操作完成之后,第二个操作进行之前,a 的 bit 3 必定为零,当第二个操作完成后,a 的 bit 3 必定为一。也就是说,所有的中间结果都是可预知的,都是正确无误的。

如果对变量 a 的 bit 3 先置零,再置一的两个操作不是原子操作,那么 a 的 bit 3 最后可能的确等于一了,但是中间可能根本没有被置零过,因为两个操作可能同时发生,导致 a 的 bit 3 置零失败了。这在操作硬件寄存器的时候,是绝对不能容忍的。

本节先介绍了Linux 内核 C语言开发中,共享资源的竞争问题,接着讨论了内核中关于原子整数操作和原子位操作的设计与实现。


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK