Linux 内核中的内存屏障

来源:github.com/torvalds/li…

1 抽象内存模型

1.1 指令重排

每个 CPU 运行一个程序,程序的执行产生内存访问操作。在这个抽象 CPU 中,内存 操作的顺序是松散的,CPU 假定进程间不依靠内存直接通信,在不改变程序执行 结果 的推测下由自己方便的顺序执行内存访问操作。

例如,考虑下面的执行过程:

{ A = 1 b = 2}

CPU 1

CPU 2

A=3;

x=B;

B=4;

y=A;

这有 24 中内存访问操作的组合,每种组合都有可能出现:

STORE A=3,    STORE B=4,      y=LOAD A->3,    x=LOAD B->4
STORE A=3,    STORE B=4,      x=LOAD B->4,    y=LOAD A->3
STORE A=3,    y=LOAD A->3,    STORE B=4,      x=LOAD B->4
STORE A=3,    y=LOAD A->3,    x=LOAD B->2,    STORE B=4
STORE A=3,    x=LOAD B->2,    STORE B=4,      y=LOAD A->3
STORE A=3,    x=LOAD B->2,    y=LOAD A->3,    STORE B=4
STORE B=4,    STORE A=3,      y=LOAD A->3,    x=LOAD B->4
STORE B=4, ...
...

从而产生 4 种结果:

x == 2, y == 1
x == 2, y == 3
x == 4, y == 1
x == 4, y == 3

更残酷的是,一个 CPU 已经提交的 store 操作,另一个 CPU 可能不会感知到,从而 load 操作取到旧的值。

比如:

{A = 1, B = 2, C = 3, P = &A, Q = &C}

CPU 1

CPU 2

B=4;

Q=p;

P=&B;

D=*Q;

可以产生 4 种结果:

(Q == &A) and (D == 1)
(Q == &B) and (D == 2)
(Q == &B) and (D == 4)

1.2 设备操作

一些设备将自己的控制接口映射成一个内存地址,访问这些地址的指令顺序是极重要 的。比如一个拥有一系列内部寄存器的网卡,可以通过一个地址寄存器 (A) 和一个数 据寄存器 (D) 访问它们。如果要访问内部寄存器 5 ,则使用下面的代码:

*A = 5;
x = *D;

但这个代码可能生成以下两种执行顺序:

STORE *A = 5, x = LOAD *D
x = LOAD *D, STORE *A = 5

1.3 合并内存访问

CPU 还可能将内存操作合并。比如

X = *A; Y = *(A + 4);

可能会生成下面任何一种执行顺序:

X = LOAD *A; Y = LOAD *(A + 4);
Y = LOAD *(A + 4); X = LOAD *A;
{X, Y} = LOAD {*A, *(A + 4) };

*A = X; *(A + 4) = Y;

则可能生成下面任何一种执行:

STORE *A = X; STORE *(A + 4) = Y;
STORE *(A + 4) = Y; STORE *A = X;
STORE {*A, *(A + 4) } = {X, Y};

1.4 最小保证

可以期望 CPU 提供了一些最小保证,不满足最小保证的 CPU 都是假的 CPU。

有依赖关系的内存访问操作是有顺序的。也就是说:

Q = READ_ONCE(P); smp_read_barrier_depends(); D = READ_ONCE(*Q);

总是在 CPU 中以这样的顺序执行:

Q = LOAD P, D = LOAD *Q

smp_read_barrier_depends() 只在 DEC Alpha 中有用,READ_ONCE 的作用在 这里 提到。

在一个 CPU 中的覆盖 load-store 操作是有顺序的。比如

a = READ_ONCE(*X); WRITE_ONCE(*X, b);

总是以这样的顺序执行:

a = LOAD *X, STORE *X = b

WRITE_ONCE(*X, c); d = READ_ONCE(*X);

总是以下面的顺序执行:

STORE *X = c, d = LOAD *X

最小保证不适用于位图。假设我们有一个长度为 8 的位图,CPU 1 要将 1 位置 1, CPU 2 要将 2 位 置 1:

{ A = 0 }

CPU 1

CPU 2

A = A OR (1 << 1)

A = A OR (1 << 2)

可能有三种个结果:

A = 2
A = 4
A = 6

2 内存屏障

正如之前看到的,内存访问操作的顺序是随机的,这会造成 CPU 间通信或者 I/O 问 题。需要一种介入保证指令的顺序以获得期望结果。内存屏障就是这样一种介入。

2.1 4+2种内存屏障

写屏障

写屏障保证任何出现在写屏障之前的 STORE 操作先于出现在写屏障之后的任何 STORE 操作执行。

写屏障一般与读屏障或者数据依赖屏障配合使用。

数据依赖屏障

数据依赖屏障是一个弱的读屏障。用于两个读操作之间,且第二个读操作依赖第一个 读操作(比如第一个读操作获得第二个读操作所用的地址)。读屏障用于保证第二个读 操作的目标已经被第一个读操作获得。

数据依赖屏障只能用于确实存在数据依赖的地方。

读屏障

读屏障保证任何出现在读屏障之前的 LOAD 操作先于出现在读屏障之后的任何 LOAD 操作执行。

读屏障一般和写屏障配合使用。

一般内存屏障

一般内存屏障保证所有出现在屏障之前的内存访问 (LOAD 和 STORE) 先于出现在屏障 之后的内存访问执行。

此外还有两种不常见的内存屏障

ACQUIRE 操作

保证出现在 ACQUIRE 之后的操作确实在 ACQUIRE 之后执行。而出现在 ACQUIRE 之前 的内存操作可能执行于 ACQUIRE 之后。

ACQUIRE 和 RELEASE 配合使用。

RELEASE 操作

保证出现在 RELEASE 之前的操作确实在 RELEASE 之前执行。而出现在 RELEASE 之后 的内存操作可能执行于 RELEASE 之前。

2.2 数据依赖屏障

数据依赖屏障需要并不总是那么明显。举个例子。

{ A = 1, B = 2, C = 3, P = &A, Q = &C }

CPU 1

CPU 2

B=4

WRITE_ONCE(P, &B)

Q=READ_ONCE(P)

D=*Q

这个例子中,D 要么是 &A, 要么是 &B:

(Q == &A) implies (D == 1)
(Q == &B) implies (D == 4)

但是,CPU 2 察觉到的 P 的更新可能比先于 B 的更新被察觉,这导致下面的结果:

(Q == &B) and (D == 2)

现实世界中有 CPU 是这么表现的,比如 DEC Alpha。要获得想要的结果,需要一个数 据依赖屏障:

CPU 1

CPU 2

B=4

WRITE_ONCE(P, &B)

Q=READ_ONCE(P)

D=*Q

数据依赖凭证保证了只会出现两种可预期的结果。

数据依赖屏障也可以序列化依赖前一指令的写操作:

{ A = 1, B = 2, C = 3, P = &A, Q = &C }

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
Linux内核链表是一个非常基础的数据结构,在Linux内核被广泛使用。Linux内核链表通过指针连接节点,每个节点都包含下一个节点的指针,从而形成链表。 在多核处理器架构下,由于不同核心的缓存可能不一致,当多个核心同时对链表进行修改时,可能会出现数据不一致的情况。为了解决这个问题,Linux内核采用了一种叫做“cache一致性”的技术,即每次修改链表节点时,都需要将该节点所在的缓存行标记为无效,这样其他核心访问该节点时,就会重新从内存读取数据,保证数据一致性。 而内存屏障则是保证代码执行顺序的关键技术。在多核处理器架构下,由于不同核心的指令可能乱序执行,因此需要内存屏障来确保指令的执行顺序。内存屏障分为读屏障、写屏障和全屏障三种。 读屏障(rmb)用于确保所有先于读屏障的读操作都完成后,才能执行读屏障之后的操作;写屏障(wmb)用于确保所有先于写屏障的写操作都完成后,才能执行写屏障之后的操作;全屏障(mb)则是同时执行读屏障和写屏障的作用。 在Linux内核链表内存屏障被广泛应用。例如,在向链表添加节点时,需要先将新节点的指针指向下一个节点,再将上一个节点的指针指向新节点。此时就需要使用内存屏障来确保指针的修改顺序正确。具体来说,需要在修改新节点指针之后、修改上一个节点指针之前,加入写屏障;在读取新节点指针之前、读取上一个节点指针之后,加入读屏障。这样就可以确保指针的修改顺序正确,从而避免了数据不一致的情况。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值