Kernel Exploring
  • 前言
  • 支持
  • 老司机带你探索内核编译系统
    • 编译出你的第一个内核
    • 内核编译中的小目标
    • 可能是kbuild中最直接的小目标 – help
    • 使用了一个kbuild函数的目标 – cscope
    • 内核中单个.o文件的编译过程
    • 根目录vmlinux的编译过程
    • 启动镜像bzImage的前世今生
    • setup.bin的诞生记
    • 真假vmlinux–由vmlinux.bin揭开的秘密
    • bzImage的全貌
    • kbuild系统浅析
  • 启动时的小秘密
    • INIT_CALLS的秘密
    • 内核参数
  • 内核加载全流程
    • bootloader如何加载bzImage
    • 内核压缩与解压
    • 内核加载的几个阶段
    • 保护模式内核代码赏析
  • 内存管理
    • 内核页表成长记
      • 未解压时的内核页表
      • 内核早期的页表
      • cleanup_highmap之后的页表
      • 映射完整物理地址
      • 启用init_level4_pgt
    • 自底而上话内存
      • e820从硬件获取内存分布
      • 原始内存分配器--memblock
      • 页分配器
        • 寻找页结构体的位置
        • 眼花的页结构体
        • Node-Zone-Page
        • 传说的伙伴系统
        • Compound Page
        • GFP的功效
        • 页分配器的用户们
      • slub分配器
        • slub的理念
        • 图解slub
      • 内存管理的不同粒度
      • 挑战和进化
        • 扩展性的设计和实现
        • 减少竞争 per_cpu_pageset
        • 海量内存
        • 延迟初始化
        • 内存热插拔
        • 连续内存分配器
    • 虚拟内存空间
      • 页表和缺页中断
      • 虚拟地址空间的管家--vma
      • 匿名反向映射的前世今生
      • 图解匿名反向映射
      • THP和mapcount之间的恩恩怨怨
      • 透明大页的玄机
      • NUMA策略
      • numa balance
      • 老版vma
    • 内存的回收再利用
      • 水线
      • Big Picture
      • 手动触发回收
      • Page Fram Reclaim Algorithm
      • swapfile原理使用和演进
    • 内存隔离
      • memcg初始化
      • 限制memcg大小
      • 对memcg记账
    • 通用
      • 常用全局变量
      • 常用转换
    • 测试
      • 功能测试
      • 性能测试
  • 中断和异常
    • 从IDT开始
    • 中断?异常?有什么区别
    • 系统调用的实现
    • 异常向量表的设置
    • 中断向量和中断函数
    • APIC
    • 时钟中断
    • 软中断
    • 中断、软中断、抢占和多处理器
  • 设备模型
    • 总线
    • 驱动
    • 设备
    • 绑定
  • nvdimm初探
    • 使用手册
    • 上帝视角
    • nvdimm_bus
    • nvdimm
    • nd_region
    • nd_namespace_X
    • nd_dax
      • dev_dax
  • KVM
    • 内存虚拟化
      • Qemu内存模型
      • KVM内存管理
  • cgroup
    • 使用cgroup控制进程cpu和内存
    • cgroup文件系统
    • cgroup层次结构
    • cgroup和进程的关联
    • cgroup数据统计
  • 同步机制
    • 内存屏障
    • RCU
  • Trace/Profie/Debug
    • ftrace的使用
    • 探秘ftrace
    • 内核热补丁的黑科技
    • eBPF初探
    • TraceEvent
    • Drgn
  • 内核中的数据结构
    • 双链表
    • 优先级队列
    • 哈希表
    • xarray
    • B树
    • Maple Tree
    • Interval Tree
  • Tools
  • Good To Read
    • 内核自带文档
    • 内存相关
    • 下载社区邮件
Powered by GitBook
On this page
  • 为什么需要内存屏障
  • 内存屏障的种类
  • Address Dependency Barrier(Historical)
  • Control Dependency
  • load-load
  • load-store
  • SMP Barrier Pairing
  • 内核中显式屏障
  • Compiler Barrier
  • CPU memory barrier
  • 内核中隐含屏障
  • 哪里需要用到内存屏障?
  • 内存模型模拟
  • 一些例子
  • Double-checked locking
  • 参考资料
  1. 同步机制

内存屏障

Previous同步机制NextRCU

Last updated 4 months ago

这部分的内容,主要来自于的学习笔记。

为什么需要内存屏障

因为下面几个原因,导致需要内存屏障:

  • 编译器优化代码

  • CPU乱序执行

  • 内存一致性

  • 内存合并操作

  • 内存预读取

  • 分支预测

PS: 在的GUARANTEES部分,提到了编译器和CPU会保证顺序的情况--访问的变量有前后依赖的情况。

而内存屏障提供了一种方法,让代码的执行顺序能按照我们写的来。

PS: 实际上应该是内存屏障和编译屏障一起提供保障。

内存屏障的种类

通常我们看到的种类有:

  • Write memory barrier

  • Read memory barrier

  • General memory barrier

这几个的定义可以用一句话概括:

all the LOAD/STORE operations specified before the barrier will appear to happen before all the LOAD/STORE operations specified after the barrier with respect to the other components of the system.

以及隐含变体:

  • ACQUIRE operation

  • RELEASE operation

这两个我感觉以RCU里面的subscribe/publish来理解可能容易点。

RELEASE对应的是publish,而ACQUIRE对应的是subscribe。之前我一直认为RELEASE是释放的意思,但感觉这里解释为发布更为合适。

Address Dependency Barrier(Historical)

我们先看一个例子:

        CPU 1                 CPU 2
        ===============       ===============
        { A == 1, B == 2, C == 3, P == &A, Q == &C }
        B = 4;
        <write barrier>
        WRITE_ONCE(P, &B);
                              Q = READ_ONCE_OLD(P);
                              D = *Q;

这里面有个很神奇的情况,就是从CPU2的角度看P已经赋值为&B,但是B还是2。(这种情况会发生在split cache的机器上)

具体解决方法是使用READ_ONCE(),因为当前的READ_ONCE()里隐藏了address dependency。

        CPU 1                 CPU 2
        ===============       ===============
        { A == 1, B == 2, C == 3, P == &A, Q == &C }
        B = 4;
        <write barrier>
        WRITE_ONCE(P, &B);
                              Q = READ_ONCE(P);
                              <implicit address-dependency barrier>
                              D = *Q;

这里需要关注的是,如果没有隐藏的address dependency,会影响rcu的功能。 我们可以把P看作一个全局指针,B是一个新的版本。当CPU2认为P已经更新到B这个版本时,如果看到的B里内容不是最新的,那就有问题了。

Control Dependency

这部分主要是因为编译器会对if这样的判断语句做优化,导致代码不按照我们的预期执行。

从形式上看,又可以分成两类:

  • load-load control dependency

  • load-store control dependency

load-load

比如这样的情况

        q = READ_ONCE(a);
        <implicit address-dependency barrier>
        if (q) {
                /* BUG: No address dependency!!! */
                p = READ_ONCE(b);
        }

两个READ_ONCE之间没有地址上的依赖,一个是从a读,一个是从b读,所以CPU可以打乱两者的顺序。 所以正确的写法是

        q = READ_ONCE(a);
        if (q) {
                <read barrier>
                p = READ_ONCE(b);
        }

PS: 如果是单线程纯内存访问,不加barrier可能也没问题。最后还是要判断q是不是非空,才会赋值到p。但是如果是访问设备寄存器的话,就必须加barrier了。

load-store

写的情况稍微好些,因为之间有一定的地址依赖:

        q = READ_ONCE(a);
        if (q) {
                WRITE_ONCE(b, 1);
        }

PS: 其实我好像没有看出来有依赖,但意思就是能。而且必须是先读后写。

另外重要的是READ_ONCE和WRITE_ONCE是必须要的。

后面还有几个编译器能预测出if结果的例子,这里就不展开了。

SMP Barrier Pairing

这一部分主要关注的是多个CPU访问同一段内存的情况,这个问题是由内存一致性引入的。

为了解决这个问题,通常需要内存屏障成对出现。

例如:

        CPU 1                   CPU 2
        ======================= =======================
                { A = 0, B = 9 }
        STORE A=1
        <write barrier>
        STORE B=2
                                LOAD B
                                <read barrier>
                                LOAD A

CPU1/CPU2中各自要加上屏障,才能保证CPU2上读到B==2后,A等于1。

内核中显式屏障

这个又分成两种:

  • compiler barrier

  • cpu memory barrier

Compiler Barrier

  • barrier()

  • READ_ONCE()/WRITE_ONCE()

不过READ_ONCE()/WRITE_ONCE()还有点cpu memory barrier的作用。

比如这个例子:

        a[0] = READ_ONCE(x);
        a[1] = READ_ONCE(x);

另外还有个例子:

作者说下面的代码

        while (tmp = a)
                do_something_with(tmp);

会被编译器优化成

        if (tmp = a)
                for (;;)
                        do_something_with(tmp);

所以以后像while/if这种判断里面,最好是不要做赋值操作了。

或者也会因为寄存器不够用,将上面的代码优化成:

        while (a)
                do_something_with(a);

这样如果有另一个线程在do_something_with()前更改了a,那就不是我们想要的行为了。

所以这种情况需要写成:

        while (tmp = READ_ONCE(a))
                do_something_with(tmp);

CPU memory barrier

内核中其中基本的内存屏障:

Type
Mandatory
SMP Conditional

General

mb()

smp_mb()

Write

wmb()

smp_wmb()

Read

rmb()

smp_rmb()

Address Dependency

READ_ONCE()

除了Address Dependency, 都隐含了编译器屏障。

内核中隐含屏障

除了显式内存屏障,内核中有些接口隐含调用了内存屏障:

  • 锁

  • 睡眠/唤醒

  • 调度

哪里需要用到内存屏障?

代码执行顺序重排,在下面几个情况会产生问题:

  • interprocessor interaction

  • atomic operation

  • accessing device

  • interrupt

内存模型模拟

一些例子

Double-checked locking

内核文档 Documentation/litmus-tests/locking/DCL-broken.litmus 写了一个很有意思的问题。

对应还有解释在tools/memory-model/Documentation/locking.txt。

下面这段代码是有问题的

	void CPU0(void)
	{
		r0 = READ_ONCE(flag);                 --------+
		if (r0 == 0) {                                |
			spin_lock(&lck);                      |
			r1 = READ_ONCE(flag);                 |
			if (r1 == 0) {                        |
				WRITE_ONCE(data, 1);  --+     |
				WRITE_ONCE(flag, 1);  --+     |
			}                                     |
			spin_unlock(&lck);                    |
		}                                             |
		r2 = READ_ONCE(data);                 --------+
	}
	/* CPU1() is the exactly the same as CPU0(). */

因为在上面标出来的两对代码可能会被重排,所以需要用smp_load_acquire()/smp_store_release()修复。

参考资料

说是可以防止a的值不会比a[0]的旧,而仅仅是编译器屏障,是不能保证cpu不会乱序执行的。

在中,提到了一个内存模型的模拟器herd7,以及内核中相关验证代码在tools/memory-model目录。

另外还找到简述了herd7的使用。

1
1
1
1
一篇文章
Memory Barriers