解密Linux内核神器:内存屏障的秘密功效与应用方法(下)

简介: 解密Linux内核神器:内存屏障的秘密功效与应用方法(下)

四、内存一致性模型

内存一致性模型(Memory Consistency Model)是用来描述多线程对共享存储器的访问行为,在不同的内存一致性模型里,多线程对共享存储器的访问行为有非常大的差别。这些差别会严重影响程序的执行逻辑,甚至会造成软件逻辑问题。

不同的处理器架构,使用了不同的内存一致性模型,目前有多种内存一致性模型,从上到下模型的限制由强到弱:

  • 顺序一致性(Sequential Consistency)模型
  • 完全存储定序(Total Store Order)模型
  • 部分存储定序(Part Store Order)模型
  • 宽松存储(Relax Memory Order)模型

注意,这里说的内存模型是针对可以同时执行多线程的平台,如果只能同时执行一个线程,也就是系统中一共只有一个CPU核,那么它一定是满足顺序一致性模型的。

对于内存的访问,我们只关心两种类型的指令的顺序,一种是读取,一种是写入。对于读取和加载指令来说,它们两两一起,一共有四种组合:

  1. LoadLoad:前一条指令是读取,后一条指令也是读取。
  2. LoadStore:前一条指令是读取,后一条指令是写入。
  3. StoreLoad:前一条指令是写入,后一条指令是读取。
  4. StoreStore:前一条指令是写入,后一条指令也是写入。

顺序一致性模型

顺序存储模型是最简单的存储模型,也称为强定序模型。CPU会按照代码来执行所有的读取与写入指令,即按照它们在程序中出现的次序来执行。同时,从主存储器和系统中其它CPU的角度来看,感知到数据变化的顺序也完全是按照指令执行的次序。也可以理解为,在程序看来,CPU不会对指令进行任何重排序的操作。在这种模型下执行的程序是完全不需要内存屏障的。但是,带来的问题就是性能会比较差,现在已经没有符合这种内存一致性模型的系统了。

为了提高系统的性能,不同架构都会或多或少的对这种强一致性模型进行了放松,允许对某些指令组合进行重排序。注意,这里处理器对读取或写入操作的放松,是以两个操作之间不存在数据依赖性为前提的,处理器不会对存在数据依赖性的两个内存操作做重排序。

完全存储定序模型

这种内存一致性模型允许对StoreLoad指令组合进行重排序,如果第一条指令是写入,第二条指令是读取,那么有可能在程序看来,读取指令先于写入指令执行。但是,对于其它另外三种指令组合还是可以保证按照顺序执行。

这种模型就相当于前面提到的,在CPU和缓存中间加入了存储缓冲,而且这个缓冲还是一个满足先入先出(FIFO)的队列。先入先出队列就保证了对StoreStore这种指令组合也能保证按照顺序被感知。

我们非常熟悉的X86架构就是使用的这种内存一致性模型。

部分存储定序模型

这种内存一致性模型除了允许对StoreLoad指令组合进行重排序外,还允许对StoreStore指令组合进行重排序。但是,对于其它另外两种指令组合还是可以保证按照顺序执行。

这种模型就相当于也在CPU和缓存中间加入了存储缓冲,但是这个缓冲不是先入先出的。

宽松存储模型

这种内存一致性模型允许对上面说的四种指令组合都进行重排序。

这种模型就相当于前面说的,既有存储缓冲,又有无效队列的情况。

这种内存模型下其实还有一个细微的差别,就是所谓的数据依赖性的问题。例如下面的程序,假设变量A初始值是0:

CPU 0

CPU 1

A = 1;

Q = P;

<write barrier>

B = *Q;

P = &A;


五、内存屏障的使用规则

前面提到过了,读、写内存屏障应该配对使用,或者换做通用内存屏障也需要成对的使用,否则起不到想要的效果。

配对使用场景

首先,来看最常用的组合,一个CPU上执行两个写入操作,中间用写内存屏障分割,另一个CPU上执行两个读取操作,中间用读内存屏障分割:

640.png

注意,在这种场景下写入变量的顺序和读取变量的顺序刚好要是相反的。加了这一对读、写内存屏障后,可以保证,在两个CPU都执行完上面的代码后,如果Y的值等于2,那么X的值一定等于1。Y的值等于2,意味着在CPU0上对B赋值2的语句已经执行过了,由于有写内存屏障的存在,也就意味着对A赋值1的语句在之前肯定也被执行过了。在CPU1上,由于有读内存屏障的存在,表示读取变量A值的语句一定在读取变量B值的语句之后被执行,也就可以保证,这时候变量A的值一定已经被赋值成了1。

第二种场景,两个CPU上都执行一个读取操作,接着一个写入操作,中间用通用内存屏障分割:

640.png

假设变量A和B的初始值都是0,当两个CPU都执行完上面的代码后,如果有Y等于2,那么X一定等于0。如果Y等于2,也就意味着在CPU1上对A赋值1的语句,一定在CPU0上读取变量A值的语句之后被执行。同时,由于对称性,如果有X等于1,那么X一定等于0。

最后一种场景,一个 CPU 执行一个读取操作,后面跟一个通用内存屏障,再后面是一个写入操作;而另外一个CPU执行一对由

写内存屏障分开的写入操作:

640.png

这种情况下,如果X的值等于1,那么必然有B的值等于1。如果X的值等于1,就意味着在这之前CPU1上已经执行过了对A赋值1的语句,由于写内存屏障的存在,也就能够保证在CPU1上已经执行过了对B赋值2的语句,而在CPU0上由于有通用内存屏障的存在,那么对B赋值1的语句一定会在对X赋值的语句之后执行。也就是说,可以保证在CPU0上对B赋值1的语句,一定会在CPU1上对B赋值2的语句之后被执行。

为什么在单核系统上没有乱序的问题

还要说明一下,无论如何,即使某一个体系的内存模型再弱,有一些基本规则还是必须要遵守的(当然,对于编译器优化来说也要遵循这些规则。):

单个CPU总是按照编程顺序来感知它自己的内存访问。

仅仅在操作不同地址时,CPU才对给定的存储操作进行“真”重新排序。

还有,如果程序一直可以保证只在单个CPU核上执行,也就不存在所谓的缓存一致性问题。因此,仅仅在两个CPU之间或者CPU与设备之间存在需要交互的可能性时,才需要内存屏障。任何代码只要能够保证没有这样的交互就不需要使用内存屏障。也就是说,如果当前系统中只有一个CPU核,并且程序没有和设备打交道,即使是多线程的,也不需要使用内存屏障。

第一个规则从直观上说感觉和前面讲的重排序有点矛盾,不是说可以按照任何次序执行嘛,怎么又可以保证按照编程顺序来感知了。我们还是用前面的例子来说,代码如下:

thread0(void)
{
   A = 1;
   B = 2;
}

thread1(void)
{
   while (B != 2)
       continue;
   assert(A != 0);
}

这段程序在没有编译器优化重排序的情况下,在单CPU系统上其实是可以正确运行的。CPU在执行的时候是不知道所谓的线程的,线程是操作系统的概念,CPU执行的时候只能感知到的是一个指令序列。对于上面的例子,这个指令序列应该是这样的:

Store A = 1;
Store B = 2;

......

Back:
Load B;
Test B != 2;
Jump Back If True;
Load A;
Test A != 0;

中间省略的是一些可能的线程切换的代码。然后,CPU会对这组指令序列进行重排序优化。但是,前面说了重排序优化是有前提条件的,因此无论如何不会将前面的将变量A赋值为1的语句重排序到后面读取变量A的语句之后,当然也不会将变量B赋值为2的语句重排序到后面读取变量B的语句之后,从而保证了这个两线程程序的正确运行。所以,前面的第一个规则其实和所谓的指令重排序是不矛盾的。

因此,在单CPU核的系统下,硬件保证程序至少看上去是按照指令的顺序被执行的,唯一可能更改指令顺序的就是编译器了,这时候所有内存屏障都将退化成编译器屏障。

但是,不使用内存屏障不代表不使用相应的同步机制。如果某个操作不是原子的,那么多线程访问它即使在单CPU的系统上也会出现问题,只不过这个问题不是因为重排序引起的。

六、有什么优化方法?

6.1硬件上的优化

store buffer

有一种可以阻止CPU进入阻塞状态的方法,就是在CPU和cache之间加入一个store buffer的硬件结构,如下图:

640.png

入了这个硬件结构后,但CPU0需要往某个地址中写入一个数据时,它不需要去关心其他的CPU的local cache中有没有这个地址的数据,它只需要把它需要写的值直接存放到store buffer中,然后发出invalidate的信号,等到成功invalidate其他CPU中该地址的数据后,再把CPU0存放在store buffer中的数据推到CPU0的local cache中。每一个CPU core都拥有自己私有的store buffer,一个CPU只能访问自己私有的那个store buffer。

store buffer的弊端

先看看下面的代码,思考一下会不会出现什么问题

a = 1;
b = a + 1;
assert(b = 2);

我们假设变量a的数据存放在CPU1的local cache line中,变量b在CPU0的cache line中,如果我们单纯使用CPU来运行上面这段程序,应该是正常运行的,但是如果加入了store buffer这个硬件结构,就会出现assert失败的问题,到底是为什么呢?我们来走一走程序的流程。

CPU0执行a=1这条命令,由于CPU0本地没有数据,会发出read invalidate消息从CPU1那获得数据,并发出invalidate命令

  • CPU0把要写入的数据放到store buffer中
  • CPU1接收到了CPU0发来的read invalidate信号,将其local cache line 中的数据发送给CPU0,并把自己本地cache line中的数据设置为无效
  • CPU0执行b=a+1
  • CPU0接收到CPU1返回回来的a的值,等于0,b=0+1=1
  • CPU0将store buffer中的a=1的值推入到cache中,造成了数据不一致性

store forwarding

硬件上出现一种新的设计,为了解决优化上面所说的store buffer的弊端, 具体结构如下图:

640.png

当CPU执行load操作的时候,不但要看cache,还要看store buffer中是否有内容,如果store buffer有该数据,那么就采用store buffer中的值。

但是这样,就能保证程序的正确运行,就能保证数据的一致性了吗?并不是!!

6.2软件上的优化

我们先来分析分析一个例子,看看下面的例子有可能会出现什么问题

void foo(void)
{
a = 1;
b = 1;
}

void bar(void)
{
while (b == 0) continue;
assert(a == 1);
}

同样,我们假设a和b都是初始化为0,CPU0执行foo函数,CPU1执行bar函数,a变量在CPU1的cache中,b在CPU0的cache中

CPU0执行a=1的操作,由于a不在local cache中,CPU0会将a的值写入到store buffer中,然后发出read invalidate信号从其他CPU获取a 的值

CPU1执行while(b == 0),由于b不在local cache中,CPU发送一个read message到总线上,看看是否可以从其他CPU的local cache中或者memory中获取数据

CPU0继续执行b=1,由于b就在自己的local cache中,所以CPU0可以直接把b=1直接写入到local cache

CPU0收到read message,将最新的b=1的值回送到CPU1,同时把存放b数据的cache line在状态设置为sharded

CPU1收到来自CPU0的read response消息,将b变量的最新值1写入自己的cachelline,并设置为sharded。

由于b的值为1,CPU1跳出while循坏,继续执行

CPU1执行assert(a==1),由于CPU1的local cache中a的值还是旧的值为0,assert(a==1)失败。

这个时候,CPU1收到了CPU 0的read invalidate消息(执行了a=1),清空了自己local cache中a的值,但是已经太晚了

CPU0收到CPU1的invalidate ack消息后,将store buffer中的a的最新值写入到cache line,然并卵,CPU1已经assertion fali了。

这个时候,就需要加入一些memory barrier的操作了。说了这么久,终于说到了内存屏障了,我们把代码修改成下面这样:

void foo(void)
{
a = 1;
smp_mb();
b = 1;
}

void bar(void)
{
while (b == 0) continue;
assert(a == 1);
}

可以看到我们在foo函数中增加了一个smp_mb()的操作,smp_mb有什么作用呢?

它主要是为了让数据在local cache中的操作顺序服务program order的顺序,那么它又是怎么让store buffer中存储的数据按照program order的顺序写入到cache中的呢?我们看看数据读写流程跟上面没有加smp_mb的时候有什么区别

  • CPU0执行a=1的操作,由于a不在local cache中,CPU0会将a的值写入到store buffer中,然后发出read invalidate信号从其他CPU获取a 的值
  • CPU1执行while(b == 0),由于b不在local cache中,CPU发送一个read message到总线上,看看是否可以从其他CPU的local cache中或者memory中获取数据
  • CPU0在执行b=1前,我们先执行了smp_mb操作,给store buffer中的所有数据项做了一个标记marked,这里也就是给a做了个标记
  • CPU0继续执行b=1,虽然b就在自己的local cache中,但是由于store buffer中有marked entry,所遇CPU0并没有把b的值直接写到cache中,而是把它写到了store buffer中
  • CPU0收到read message,将最新的b=1的值回送到CPU1,同时把存放b数据的cache line在状态设置为sharded
  • CPU1收到来自CPU0的read response消息,将b变量的值0写入自己的cachelline(因为b的最新值1还存放在store buffer中),并设置为sharded。
  • 完成了bus transaction之后,CPU1可以load b到寄存器中了,当然,这个时候b的指还是0
  • CPU1收到了来自CPU0的read invalidate消息(执行了a=1),清空了自己local cache中a的值
  • CPU0将存放在store buffer中的a=1的值写入cache line中,并设置为modified
  • 由于store buffer中唯一一个marked的entry已经写入到cache line中了,所以b也可以进入cache line。不过需要注意的是,当前b对应的cache line状态还是sharded(因为在CPU1中的cache line中还保留有b的数据)
  • CPU0发送invalidate消息,CPU1清空自己的b cacheline,CPU0将b cacheline设置为exclusive

你以为这样就完了吗????NONONONO!!!!太天真了

6.3Invalidate Queues

不幸的是:每个CPU的store buffer不能实现地太大,其存储队列的数目也不会太多。当CPU以中等的频率执行store操作的时候(假设所有的store操作都导致了cache miss),store buffer会很快的呗填满。在这种情况下,CPU只能又进入阻塞状态,直到cacheline完成invalidation和ack的交互后,可以将store buffer的entry写入cacheline,从而让新的store让出空间之后,CPU才可以继续被执行。

这种情况也可能发生在调用了memory barrier指令之后,因为一旦store buffer中的某个entry被标记了,那么随后的store都必须等待invalidation完成,因此不管是否cache miss,这些store都必须进入store buffer。

这个时候,invalidate queues就出现了,它可也缓解这个情况。store buffer之所以很容易被填满,主要是因为其他CPU在回应invalidate acknowledge比较慢,如果能加快这个过程,让store buffer中的内容尽快写入到cacheline,那么就不会那么容易被填满了。

而invalidate acknowledge不能尽快回复的主要原因,是因为invalidate cacheline的操作没有那么块完成,特别是在cache比较繁忙的时候,如果再收到其他CPU发来的invalidate请求,只有在完成了invalidate操作后,本CPU才会发送invalidate acknowledge。

然而,CPU其实不需要完成invalidate就可以回送acknowledgement消息,这样就不会阻止发送invalidate的那个CPU进去阻塞状态。CPU可以将这些接收到的invalidate message存放到invalidate queues中,然后直接回应acknowledge,表示自己已经收到请求,随后会慢慢处理,当时前提是必须在发送invalidate message的CPU发送任何关于某变量对应cacheline的操作到bus之前完成。结构如下图:

640.png

七、关于内存屏障的一些补充

在实际的应用程序开发中,开发者可能完全不知道内存屏障就写出了正确的多线程程序,

这主要是因为各种同步机制中已隐含了内存屏障(但和实际的内存屏障有细微差别),使得不直接使用内存屏障也不会存在任何问题。但如果你希望编写诸如无锁数据结构,那么内存屏障意义重大。

在Linux内核中,除了前面说到的编译器屏障—barrier()和ACESS_ONCE(),还有CPU内存屏障:

通用屏障,保证读写操作有序,包括mb()和smp_mb(); 写操作屏障,仅保证写操作有序,包括wmb()和smp_wmb(); 读操作屏障,仅保证读操作有序,包括rmb()和smp_rmb();

注意,所有的CPU内存屏障(除了数据依赖屏障外)都隐含了编译器屏障(也就是使用CPU内存屏障后就无需再额外添加编译器屏障了)。

这里的smp开通的内存屏障会根据配置在单处理器上直接使用编译器屏障,而在SMP上才使用CPU内存屏障(即mb()、wmb()、rmb())。

还需要注意一点是,CPU内存屏障中某些类型的屏障需要成对使用,否则会出错,

详细来说就是:一个写操作屏障需要和读操作(或者数据依赖)屏障一起使用(当然,通用屏障也是可以的),反之亦然。

通常,我们是希望在写屏障之前出现的STORE操作总是匹配度屏障或者数据依赖屏障之后出现的LOAD操作。以之前的代码示例为例:

// thread 1
x = 42;
smb_wmb();
ok = 1;

// thread 2
while(!ok);
smb_rmb();
do(x);

我们这么做,是希望在thread2执行到do(x)时(在ok验证的确=1时),x = 42的确是有效的(写屏障之前出现的STORE操作),此时do(x),的确是在执行do(42)(读屏障之后出现的LOAD操作)

利用内存屏障实现无锁环形缓冲区

最后,以一个使用内存屏障实现的无锁环形缓冲区(只有一个读线程和一个写线程时)来结束本文。

本代码源于内核FIFO的一个实现,内容如下(略去了非关键代码):

代码来源:linux-2.6.32.63\kernel\kfifo.c

unsigned int __kfifo_put(struct kfifo *fifo,
           const unsigned char *buffer, unsigned int len)
{
   unsigned int l;

   len = min(len, fifo->size - fifo->in + fifo->out);

   /*
    * Ensure that we sample the fifo->out index -before- we
    * start putting bytes into the kfifo.
        * 通过内存屏障确保先读取fifo->out后,才将buffer中数据拷贝到
        * 当前fifo中
    */

   smp_mb();

   /* first put the data starting from fifo->in to buffer end */
       /* 将数据拷贝至fifo->in到fifo结尾的一段内存中 */
   l = min(len, fifo->size - (fifo->in & (fifo->size - 1)));
   memcpy(fifo->buffer + (fifo->in & (fifo->size - 1)), buffer, l);

       /* then put the rest (if any) at the beginning of the buffer */
       /* 如果存在剩余未拷贝完的数据(此时len – l > 0)则拷贝至
        * fifo的开始部分
        */
   memcpy(fifo->buffer, buffer + l, len - l);

   /*
    * Ensure that we add the bytes to the kfifo -before-
    * we update the fifo->in index.
    */
       /*
        * 通过写操作屏障确保数据拷贝完成后才更新fifo->in
        */

   smp_wmb();

   fifo->in += len;

   return len;


unsigned int __kfifo_get(struct kfifo *fifo,
            unsigned char *buffer, unsigned int len)
{
   unsigned int l;

   len = min(len, fifo->in - fifo->out);

   /*
    * Ensure that we sample the fifo->in index -before- we
    * start removing bytes from the kfifo.
    */
       /*
        * 通过读操作屏障确保先读取fifo->in后,才执行另一个读操作:
        * 将fifo中数据拷贝到buffer中去
        */

   smp_rmb();

   /* first get the data from fifo->out until the end of the buffer */
       /* 从fifo->out开始拷贝数据到buffer中 */
   l = min(len, fifo->size - (fifo->out & (fifo->size - 1)));
   memcpy(buffer, fifo->buffer + (fifo->out & (fifo->size - 1)), l);

   /* then get the rest (if any) from the beginning of the buffer */
       /* 如果需要数据长度大于fifo->out到fifo结尾长度,
        * 则从fifo开始部分拷贝(此时len – l > 0)
        */
   memcpy(buffer + l, fifo->buffer, len - l);

   /*
    * Ensure that we remove the bytes from the kfifo -before-
    * we update the fifo->out index.
    */
       /* 通过内存屏障确保数据拷贝完后,才更新fifo->out */

   smp_mb();

   fifo->out += len;

   return len;
}

这里__kfifo_put被一个线程用于向fifo中写入数据,另外一个线程可以安全地调用__kfifo_get,从此fifo中读取数据。代码中in和out的索引用于指定环形缓冲区实际的头和尾。

具体的in和out所指向的缓冲区的位置通过与操作来求取(例如:fifo->in & (fifo->size -1)),这样相比取余操作来求取下表的做法效率要高不少。使用与操作求取下表的前提是环形缓冲区的大小必须是2的N次方,

换而言之,就是说环形缓冲区的大小为一个仅有一个1的二进制数,那么index & (size – 1)则为求取的下标(这不难理解)。

640.png

索引in和out被两个线程访问。in和out指明了缓冲区中实际数据的边界,也就是in和out同缓冲区数据存在访问上的顺序关系,由于不适用同步机制,那么保证顺序关系就需要使用到内存屏障了。索引in和out都分别只被一个线程修改,而被两个线程读取。__kfifo_put先通过in和out来确定可以向缓冲区中写入数据量的多少,这时,out索引应该先被读取,才能真正将用户buffer中的数据写入缓冲区,因此这里使用到了smp_mb(),对应的,__kfifo_get也使用smp_mb()来确保修改out索引之前缓冲区中数据已被成功读取并写入用户buffer中了。(我认为在__kfifo_put中添加的这个smp_mb()是没有必要的。

理由如下,kfifo仅支持一写一读,这是前提。在这个前提下,in和out两个变量是有着依赖关系的,这的确没错,并且我们可以看到在put中,in一定会是最新的,因为put修改的是in的值,而在get中,out一定会是最新的,因为get修改out的值。这里的smp_mb()显然是希望在运行时,遵循out先load新值,in再load新值。的确,这样做是没错,但这是否有必要呢?out一定要是最新值吗?out如果不是最新值会有什么问题?如果out不是最新值,实际上并不会有什么问题,仅仅是在put时,fifo的实际可写入空间要大于put计算出来的空间(因为out是旧值,导致len在计算时偏小),这并不影响程序执行的正确性。

从最新linux-3.16-rc3 kernel的代码:lib\kfifo.c的实现:

__kfifo_in中也可以看出memcpy(fifo->data + off, src, l);
memcpy(fifo->data, src + l, len – l);

之前的那次smb_mb()已经被省去了,当然更新in之前的smb_wmb()还是在kfifo_copy_in中被保留了。之所以省去这次smb_mb()的调用,我想除了省去调用不影响程序正确性外,是否还有对于性能影响的考虑,尽量减少不必要的mb调用)。

对于in索引,在__kfifo_put中,通过smp_wmb()保证先向缓冲区写入数据后才修改in索引, 由于这里只需要保证写入操作有序,所以选用写操作屏障,在__kfifo_get中,通过smp_rmb()保证先读取了in索引(这时in索引用于确定缓冲区中实际存在多少可读数据)才开始读取缓冲区中数据(并写入用户buffer中),由于这里指需要保证读取操作有序,故选用读操作屏障。

什么时候需要注意考虑内存屏障(补充)

从上面的介绍我们已经可以看出,在SMP环境下,内存屏障是如此的重要,

在多线程并发执行的程序中,一个数据读取与写入的乱序访问,就有可能导致逻辑上错误,而显然这不是我们希望看到的。

作为系统程序的实现者,我们涉及到内存屏障的场景主要集中在无锁编程时的原子操作。

执行这些操作的地方,就是我们需要考虑内存屏障的地方。

从我自己的经验来看,使用原子操作,一般有如下三种方式:

  • (1). 直接对int32、int64进行赋值;
  • (2). 使用gcc内建的原子操作内存访问接口;
  • (3). 调用第三方atomic库:libatomic实现内存原子操作。

对于第一类原子操作方式,显然内存屏障是需要我们考虑的,例如kernel中kfifo的实现,就必须要显示的考虑在数据写入和读取时插入必要的内存屏障,以保证程序执行的顺序与我们设定的顺序一致。,对于使用gcc内建的原子操作访问接口,基本上大多数gcc内建的原子操作都自带内存屏障,他可以确保在执行原子内存访问相关的操作时,执行顺序不被打断。

当然,其中也有几个并未实现full barrier,具体情况可以参考gcc文档对对应接口的说明。同时,gcc还提供了对内存屏障的封装接口:__sync_synchronize (…),这可以作为应用程序使用内存屏障的接口(不用写汇编语句)。,对于使用libatomic库进行原子操作,原子访问的程序。Libatomic在接口上对于内存屏障的设置粒度更新,他几乎是对每一个原子操作的接口针对不同平台都有对应的不同内存屏障的绑定。

接口实现上分别添加了_release/_acquire/_full等各个后缀,分别代表的该接口的内存屏障类型,具体说明可参见libatomic的README说明。如果是调用最顶层的接口,已AO_compare_and_swap为例,最终会根据平台的特性以及宏定义情况调用到:AO_compare_and_swap_full或者AO_compare_and_swap_release或者AO_compare_and_swap_release等。

我们可以重点关注libatomic在x86_64上的实现,libatomic中,在x86_64架构下,也提供了应用层的内存屏障接口:AO_nop_full404 Not Found,对于第一类原子操作方式,显然内存屏障是需要我们考虑的,例如kernel中kfifo的实现,就必须要显示的考虑在数据写入和读取时插入必要的内存屏障,以保证程序执行的顺序与我们设定的顺序一致。

对于使用gcc内建的原子操作访问接口,基本上大多数gcc内建的原子操作都自带内存屏障,他可以确保在执行原子内存访问相关的操作时,执行顺序不被打断。

当然,其中也有几个并未实现full barrier,具体情况可以参考gcc文档对对应接口的说明。同时,gcc还提供了对内存屏障的封装接口:__sync_synchronize (…),这可以作为应用程序使用内存屏障的接口(不用写汇编语句)。,对于使用libatomic库进行原子操作,原子访问的程序。Libatomic在接口上对于内存屏障的设置粒度更新,他几乎是对每一个原子操作的接口针对不同平台都有对应的不同内存屏障的绑定。

接口实现上分别添加了_release/_acquire/_full等各个后缀,分别代表的该接口的内存屏障类型,具体说明可参见libatomic的README说明。如果是调用最顶层的接口,已AO_compare_and_swap为例,最终会根据平台的特性以及宏定义情况调用到:AO_compare_and_swap_full或者AO_compare_and_swap_release或者AO_compare_and_swap_release等。我们可以重点关注libatomic在x86_64上的实现,libatomic中,在x86_64架构下,也提供了应用层的内存屏障接口:AO_nop_full。

综合上述三点,总结下来便是:

如果你在程序中是裸着写内存,读内存,则需要显式的使用内存屏障确保你程序的正确性,gcc内建不提供简单的封装了内存屏障的内存读写。

因此,如果只是使用gcc内建函数,你仍然存在裸读,裸写,此时你还是必须显式使用内存屏障。如果你通过libatomic进行内存访问,在x86_64架构下,使用AO_load/AO_store,你可以不再显式的使用内存屏障(但从实际使用的情况来看,libatomic这类接口的效率不是很高)。


相关文章
|
1天前
|
Linux 数据库
Linux内核中的锁机制:保障并发操作的数据一致性####
【10月更文挑战第29天】 在多线程编程中,确保数据一致性和防止竞争条件是至关重要的。本文将深入探讨Linux操作系统中实现的几种关键锁机制,包括自旋锁、互斥锁和读写锁等。通过分析这些锁的设计原理和使用场景,帮助读者理解如何在实际应用中选择合适的锁机制以优化系统性能和稳定性。 ####
14 6
|
2天前
|
机器学习/深度学习 负载均衡 算法
深入探索Linux内核调度机制的优化策略###
本文旨在为读者揭开Linux操作系统中至关重要的一环——CPU调度机制的神秘面纱。通过深入浅出地解析其工作原理,并探讨一系列创新优化策略,本文不仅增强了技术爱好者的理论知识,更为系统管理员和软件开发者提供了实用的性能调优指南,旨在促进系统的高效运行与资源利用最大化。 ###
|
4天前
|
算法 Linux 开发者
深入探究Linux内核中的内存管理机制
本文旨在对Linux操作系统的内存管理机制进行深入分析,探讨其如何通过高效的内存分配和回收策略来优化系统性能。文章将详细介绍Linux内核中内存管理的关键技术点,包括物理内存与虚拟内存的映射、页面置换算法、以及内存碎片的处理方法等。通过对这些技术点的解析,本文旨在为读者提供一个清晰的Linux内存管理框架,帮助理解其在现代计算环境中的重要性和应用。
|
1天前
|
监控 网络协议 算法
Linux内核优化:提升系统性能与稳定性的策略####
本文深入探讨了Linux操作系统内核的优化策略,旨在通过一系列技术手段和最佳实践,显著提升系统的性能、响应速度及稳定性。文章首先概述了Linux内核的核心组件及其在系统中的作用,随后详细阐述了内存管理、进程调度、文件系统优化、网络栈调整及并发控制等关键领域的优化方法。通过实际案例分析,展示了这些优化措施如何有效减少延迟、提高吞吐量,并增强系统的整体健壮性。最终,文章强调了持续监控、定期更新及合理配置对于维持Linux系统长期高效运行的重要性。 ####
|
2天前
|
缓存 网络协议 Linux
Linux操作系统内核
Linux操作系统内核 1、进程管理: 进程调度 进程创建与销毁 进程间通信 2、内存管理: 内存分配与回收 虚拟内存管理 缓存管理 3、驱动管理: 设备驱动程序接口 硬件抽象层 中断处理 4、文件和网络管理: 文件系统管理 网络协议栈 网络安全及防火墙管理
19 4
|
18小时前
|
缓存 算法 Linux
深入理解Linux内核调度器:公平性与性能的平衡####
真知灼见 本文将带你深入了解Linux操作系统的核心组件之一——完全公平调度器(CFS),通过剖析其设计原理、工作机制以及在实际系统中的应用效果,揭示它是如何在众多进程间实现资源分配的公平性与高效性的。不同于传统的摘要概述,本文旨在通过直观且富有洞察力的视角,让读者仿佛亲身体验到CFS在复杂系统环境中游刃有余地进行任务调度的过程。 ####
16 6
|
21小时前
|
开发框架 监控 .NET
【Azure App Service】部署在App Service上的.NET应用内存消耗不能超过2GB的情况分析
x64 dotnet runtime is not installed on the app service by default. Since we had the app service running in x64, it was proxying the request to a 32 bit dotnet process which was throwing an OutOfMemoryException with requests >100MB. It worked on the IaaS servers because we had the x64 runtime install
|
4天前
|
人工智能 算法 大数据
Linux内核中的调度算法演变:从O(1)到CFS的优化之旅###
本文深入探讨了Linux操作系统内核中进程调度算法的发展历程,聚焦于O(1)调度器向完全公平调度器(CFS)的转变。不同于传统摘要对研究背景、方法、结果和结论的概述,本文创新性地采用“技术演进时间线”的形式,简明扼要地勾勒出这一转变背后的关键技术里程碑,旨在为读者提供一个清晰的历史脉络,引领其深入了解Linux调度机制的革新之路。 ###
|
5天前
|
缓存 监控 Linux
|
8天前
|
Linux Shell 数据安全/隐私保护
下一篇
无影云桌面