首页 > 系统相关 >Linux 内存屏障

Linux 内存屏障

时间:2024-08-11 14:56:06浏览次数:16  
标签:fp void smp 屏障 内存 Linux

一.Linux 内存屏障概念

Linux 内存屏障是一种同步原语,用于确保在多处理器系统或单处理器的乱序执行环境中,内存操作按照特定顺序执行。它们在并发编程、设备驱动程序开发和底层系统编程中扮演着重要角色。以下是关于 Linux 内存屏障的详细解释:

1. 基本概念:
   • 内存屏障是一种同步机制,用于控制内存访问的顺序。
   • 它们防止编译器优化和处理器重排序导致的不一致性。

2. 主要类型:
   a) 读屏障(Read Barrier):
      - 确保在屏障之前的所有读操作都在屏障之后的读操作之前完成。
      - Linux 中使用 rmb() 或 smp_rmb()。

   b) 写屏障(Write Barrier):
      - 确保在屏障之前的所有写操作都在屏障之后的写操作之前完成。
      - Linux 中使用 wmb() 或 smp_wmb()。

   c) 通用屏障(General Barrier):
      - 同时作为读屏障和写屏障。
      - Linux 中使用 mb() 或 smp_mb()。

3. 使用场景:
   • 多处理器系统中的共享内存同步。
   • 设备驱动程序中的硬件寄存器访问。
   • 无锁编程中的数据一致性保证。

4. 实现细节:
   • 在不同架构上,内存屏障的实现可能不同。
   • 可能涉及特殊的硬件指令或内存栅栏操作。

5. 性能考虑:
   • 过度使用内存屏障可能导致性能下降。
   • 应谨慎使用,只在必要时才引入。

6. 编译器屏障:
   • asm volatile("" ::: "memory") 用于防止编译器重排序。
   • 不同于完整的内存屏障,它只影响编译器优化。

7. 特定架构的屏障:
   • x86: lfence(读屏障), sfence(写屏障), mfence(通用屏障)
   • ARM: dmb, dsb, isb

8. 原子操作和内存屏障:
   • 某些原子操作隐含了内存屏障效果。
   • 例如,atomic_inc() 通常包含了一个完整的内存屏障。

9. 顺序一致性:
   • 内存屏障帮助实现顺序一致性模型。
   • 确保多个 CPU 对内存操作有一致的视图。

10. 弱内存模型:
    • 在弱内存模型架构(如 ARM)上,内存屏障更为重要。
    • 用于强制执行特定的内存操作顺序。

11. 内核中的使用:
    • 在驱动程序、并发数据结构和低级同步原语中广泛使用。
    • 通常与 spinlock、RCU 等机制结合使用。

12. 编程注意事项:
    • 正确使用内存屏障需要深入理解硬件架构和内存模型。
    • 错误使用可能导致难以发现的并发 bug。

13. 调试和验证:
    • 使用内存模型检查工具(如 LKMM)验证正确性。
    • 在多种硬件平台上进行测试非常重要。

14. 新的趋势:
    • C11/C++11 引入了标准化的原子操作和内存序列。
    • Linux 内核逐渐采用这些新标准。

内存屏障是一个复杂但重要的话题,尤其在系统级编程中。正确使用内存屏障可以确保并发程序的正确性和可移植性,但也需要谨慎以避免不必要的性能开销。深入理解内存屏障需要扎实的计算机体系结构知识和并发编程经验。

二.Linux 内存屏障举例说明

当然,我来通过几个具体的例子来说明内存屏障的使用和重要性:

例1: 生产者-消费者模式

考虑一个简单的生产者-消费者场景,其中一个线程(生产者)写入数据,另一个线程(消费者)读取数据:

```c
struct data {
    int flag;
    int value;
};

struct data shared;

// 生产者
void producer() {
    shared.value = 42;
    smp_wmb();  // 写内存屏障
    shared.flag = 1;
}

// 消费者
void consumer() {
    while (shared.flag == 0) {
        // 等待
    }
    smp_rmb();  // 读内存屏障
    int value = shared.value;
}
```

在这个例子中:
- `smp_wmb()` 确保 `value` 的写入在 `flag` 的写入之前完成。
- `smp_rmb()` 确保在读取 `value` 之前, `flag` 的读取已经完成。

如果没有这些内存屏障,处理器或编译器可能会重排序这些操作,导致消费者可能读取到未初始化或部分初始化的数据。

例2: 设备驱动程序

在设备驱动程序中,内存屏障常用于确保对硬件寄存器的访问顺序:

```c
void write_to_device(void __iomem *base, u32 value) {
    writel(value, base + DATA_REG);
    wmb();  // 写内存屏障
    writel(1, base + CONTROL_REG);
}

u32 read_from_device(void __iomem *base) {
    writel(1, base + REQUEST_REG);
    mb();  // 全内存屏障
    return readl(base + DATA_REG);
}
```

在 `write_to_device` 函数中,`wmb()` 确保数据写入在控制寄存器设置之前完成。
在 `read_from_device` 函数中,`mb()` 确保请求发出后,再读取数据寄存器。

例3: 自旋锁实现

内存屏障在锁的实现中也很重要。这是一个简化的自旋锁实现示例:

```c
typedef struct {
    int lock;
} spinlock_t;

void spin_lock(spinlock_t *lock) {
    while (atomic_cmpxchg(&lock->lock, 0, 1) != 0) {
        // 自旋等待
    }
    smp_mb();  // 全内存屏障
}

void spin_unlock(spinlock_t *lock) {
    smp_mb();  // 全内存屏障
    atomic_set(&lock->lock, 0);
}
```

这里的 `smp_mb()` 确保:
- 在获得锁之后,所有后续的内存访问不会被重排到锁操作之前。
- 在释放锁之前,所有之前的内存访问都已完成。

例4: RCU (Read-Copy-Update) 机制

RCU 是 Linux 内核中广泛使用的同步机制,它也依赖于内存屏障:

```c
struct foo {
    int a;
    int b;
};

struct foo *gp;

void update_foo(void) {
    struct foo *new_fp = kmalloc(sizeof(*new_fp), GFP_KERNEL);
    struct foo *old_fp;

    new_fp->a = 1;
    new_fp->b = 2;
    
    old_fp = rcu_dereference(gp);
    rcu_assign_pointer(gp, new_fp);  // 包含内存屏障
    
    synchronize_rcu();
    kfree(old_fp);
}

void read_foo(void) {
    struct foo *fp;
    
    rcu_read_lock();
    fp = rcu_dereference(gp);  // 包含内存屏障
    if (fp) {
        int a = fp->a;
        int b = fp->b;
        // 使用 a 和 b
    }
    rcu_read_unlock();
}
```

在这个例子中:
- `rcu_assign_pointer` 包含一个内存屏障,确保新结构的所有字段都已初始化。
- `rcu_dereference` 在某些架构上包含一个内存屏障,确保后续的读操作不会被重排到引用操作之前。

这些例子展示了内存屏障在不同场景下的应用。它们确保了在多处理器系统或支持乱序执行的处理器上,内存操作按预期顺序执行,从而保证程序的正确性。正确使用内存屏障对于编写健壮的并发代码和底层系统软件至关重要。
 

标签:fp,void,smp,屏障,内存,Linux
From: https://blog.csdn.net/zhangyihu321/article/details/141100432

相关文章

  • linux 使用iio 通过I2C 方式采集实例
    IIO(IndustrialI/O)子系统通过I2C方式采集数据的实例。这个例子包括驱动程序和用户空间应用程序。首先,让我们创建一个简单的IIO驱动程序,它通过I2C接口与ADC(模数转换器)通信,并通过PCI总线连接到系统。1.驱动程序(my_iio_driver.c):```c#include<linux/module.h>......
  • Linux C++ 多线程编程
    LinuxC++多线程编程参考教程:c++:互斥锁/多线程的创建和unique_lock<mutex>的使用_mutex头文件vc++-CSDN博客1.编写unique_mutex1.1创建文件夹通过终端创建一个名为unique_mutex的文件夹以保存我们的VSCode项目,在/unique_mutex目录下打开vscode。rosnoetic@rosnoetic-Virt......
  • Linux常用命令(图文并茂+超详细!)
    ......
  • Linux5:Shell编程——函数、重定向
    目录前言一、函数1.函数结构2.函数实例3.函数传参二、重定向1.输出重定向2.输入重定向3.同时使用4.重定向深入了解 5.垃圾桶总结前言    Shell编程将会在本章完结 一、函数1.函数结构#!/bin/sh#函数functionfun1(){echo"thisisaf......
  • Linux源码下载渠道是什么
    Linux源码可以从多个渠道下载,以下是几个主要的下载途径:1.Linux官方网站官方网站地址:https://www.kernel.org/在Linux官方网站上,你可以找到最新版本的Linux内核源码包以及之前版本的源码包。网站主页通常会显示最新版本的Linux内核,点击该版本号即可进入下载页面。在下载页面上,......
  • linux系统CENTOS 7安装docker
    前言:使用阿里云镜像,在CENTOS7版本上安装docker容器,方便使用docker容器安装其他软件。前置准备如果已经安装了docker,先将其卸载。yumremovedocker安装docker安装docker依赖的软件包。sudoyuminstall-yyum-utilsdevice-mapper-persistent-datalvm2添加阿里......
  • 枚举、typedef、位运算、堆内存-malloc 函数
    目录枚举定义枚举值枚举类型枚举的优点枚举的注意事项示例程序总结typedef基本用法复杂数据类型的重命名位运算位移操作总结堆内存malloc 函数free 函数常见问题枚举定义在C语言中,枚举(enum)是一种数据类型,它允许定义一组具名的常量。使用枚举可以使代码......
  • Linux基于Redis实现短地址服务
    一、应用场景为什么要使用短地址服务,具体使用的业务场景如下:URL压缩,把原始长地址压缩成短地址,便于文本长度限制的场景使用(短信、社交网络、网络营销)    —营销短信有字数限制,链接太长会影响短信内容的条数(涉及到费用问题)。    — 相对于长链接,短链接更......
  • Linux:@2024-08-10 最新的Openssl-3.3.1 Openssh-9.8p1 Centos7上的编译后二进制 一键
     附件:Portable_Openssl-Openssh9.8p1-bin-el7.v1.2.1.tgz.zip特点:适用于centos7.x 已经编译为二进制对老版本的关键二进制文件sshd、sftp、scp、openssl进行了备份升级前,自动打开一个端口为2222的老版本的sshd服务,你可以连接那个2222的服务,以防死翘翘。对sshd_config进......
  • 【JVM】Java跨平台性质及Java虚拟机内存结构
    目录Java为什么可以跨平台Java虚拟机的内存结构简单聊聊~Java为什么可以跨平台Java编写的代码可以做到一次编译,多平台运行。这是为什么呢?我们在使用Java之前先要去按照对应操作系统版本的JDK,JDK中包含了Java编译器,Java虚拟机,一些类库等。在编写完代码之后,代码通过编译......