【Linux 内核】整理内核特殊宏、原语等

目录

一、ACCESS_ONCE、READ_ONCE、WRITE_ONCE

1、volatile和barrier()

2、barrier()和mb()

3、ACCESS_ONCE()


一、ACCESS_ONCE、READ_ONCE、WRITE_ONCE

原文:内存模型与同步原语 - 1 内存屏障-阿里云开发者社区

1、volatile和barrier()

        volatile 关键词用于告知编译器,其修饰的变量的值很可能被程序之外的因素(如该变量存储于硬件寄存器 IO 映射的内存)改变,因而防止编译器对该变量进行缓存优化;对于 volatile 修饰的变量,编译器不能对该变量进行缓存,当每次使用该变量的值时,编译器必须从内存重新读取该变量的值。虽然 barrier() 和 volatile 都有抑制编译器优化的效果,但是两者还是存在着细微的差别由于 volatile 是修饰一个变量的,那么 volatile 就会一直伴随着这个变量,也就是说这个变量再也不能使用寄存器对其进行缓存,今后访问这个变量时每次都需要从内存重新读取该变量的值。

编译器的优化:

int a = 1;

void foo(void) {
    while (a) ;
}

void bar(void) {a = 0;}

        假设程序中有两个线程,一个线程执行 foo()函数,另一个线程执行 bar() 函数,两个线程会并行访问全局变量 a在默认不开启任何优化选项时,编译器输出的 foo() 函数为

foo:
.L2:
        movl    a(%rip), %eax   // %eax = a
        testl   %eax, %eax      // test if a == 0
        jne     .L2             // reenter the loop if a != 0
        popq    %rbp
        .cfi_def_cfa 7, 8
        ret                     // exit and return if a == 0

        可以看到在未开启优化的时候,编译器的输出是符合程序的设计意图的再来看看开启编译优化 (gcc -O2) 时,编译器输出的 foo() 函数

foo:
.LFB0:
        movl    a(%rip), %eax   // %eax = a
        testl   %eax, %eax      // test if %eax == 0
        jne     .L4
        rep ret                 // exit and return if %eax == 0
.L4:
        jmp     .L4             // infinite loop

        可以看到此时 foo()函数会陷入无限循环。

barrier()、volatile编译器进行的优化对比:

#define barrier()__asm__ __volatile__("": : :"memory")
int a = 1;

void foo(void) {while (a) barrier();}

此时开启编译优化 (gcc -O2) 时,编译器输出的 foo() 函数为

foo:
.LFB0:
        .cfi_startproc
        jmp     .L6
        .p2align 4,,10
        .p2align 3
.L5:
.L6:
        movl    a(%rip), %eax
        testl   %eax, %eax
        jne     .L5
        rep ret
        .cfi_endproc

可以看到加上 barrier() 之后,恢复为从内存读取变量的值。同样的例子,再来看一下使用 barrier() 的效果:

void foo(void) {while (a) {
                b = a;
                barrier()};}

此时编译器 gcc -O2 的编译输出为

foo:
        jmp     .L9
.L7:
        movl    %eax, b(%rip)   // b=a if a != 0
.L9:
        movl    a(%rip), %eax   // %eax = a
        testl   %eax, %eax      // test if %eax == 0
        jne     .L7
        rep ret                 // exit and return if a == 0

可以看到每次循环执行一次读内存的操作,同时在执行 b=a 时复用寄存器中缓存的 a 值。

2、barrier()和mb()

#define barrier()__asm__ __volatile__("": : :"memory")
#define mb() 	asm volatile("mfence":::"memory")
#define rmb()	asm volatile("lfence":::"memory")
#define wmb()	asm volatile("sfence" ::: "memory")


  1. __volatile__: 告诉编译barrier()周围的指令不要被优化;
  2. memory:是告诉编译器汇编代码会使内存里面的值更改,编译器应使用内存里的新值而非寄存器里保存的老值;
  3. sfence, 在写指令之后插入写屏障,将处于store buffers中的条目写回到主内存;
  4. lfence,清空invalidate queue后,再从主内存加载数据;
  5. mfence, 是一种全能型的屏障,具备ifence和sfence的能力。

3、ACCESS_ONCE()

#define ACCESS_ONCE(x) (*(volatile typeof(x) *)&(x))

这里虽然还是使用 volatile 关键字来抑制编译器的优化,但是之前是直接将整个变量声明为 volatile,而这里 ACCESS_ONCE() 则是通过指针的方式,将变量临时地转换为 volatile 变量,相当于只有调用 ACCESS_ONCE() 的时候才会抑制编译器优化,而在其他地方访问变量的时候,编译器优化是正常开启的

4、READ_ONCE()/WRITE_ONCE()

#define __READ_ONCE(x, check) \
({                                    \
    union { typeof(x) __val; char __c[1]; } __u; \
    if (check)                        \
        __read_once_size(&(x), __u.__c, sizeof(x)); \
    else                                \
        __read_once_size_nocheck(&(x), __u.__c, sizeof(x)); \
    smp_read_barrier_depends(); /* Enforce dependency ordering from x */ \
    __u.__val;                        \
})
#define READ_ONCE(x) __READ_ONCE(x, 1)
#define __READ_ONCE_SIZE    \
({                                    \
    switch (size) {            \
    case 1: *(__u8 *)res = *(volatile __u8 *)p; break;   \
    case 2: *(__u16 *)res = *(volatile __u16 *)p; break; \
    case 4: *(__u32 *)res = *(volatile __u32 *)p; break; \
    case 8: *(__u64 *)res = *(volatile __u64 *)p; break; \
    default:                        \
        barrier();                    \
        __builtin_memcpy((void *)res, (const void *)p, size); \
        barrier();                    \
    }                                \
})

static __always_inline
void __read_once_size(const volatile void *p, void *res, int size)
{
    __READ_ONCE_SIZE;
}

规避的原理非常简单,就是将 non-scalar 类型强制转换为 scalar 类型,之后再按照ACCESS_ONCE() 中那样将转换后的 scalar 类型的变量临时转换为 volatile 的进行访问 ,WRITE_ONCE()同样如此。

4、__read_mostly

vmlinux.lds.S:

 

vmlinux.lds.h:

 

在vmlinux的链接脚本中定义了.data..read_mostly,将锁有这种属性的数据放入到.data中,并且这种数据的起始地址和结束地址都按照L1 cache对齐(__cache_aligned结束地址没有按L1 cache对齐),这样在同一缓存行的数据都是mosly read的数据,这样会减少常读数据和常写数据混合在同一cache,由于常写数据导致的缓存invalid,但这避免不了缓存行full后的缓存弹出策略和正常读写内存的内存恰好命中该缓存行的情况,read_mostly对常读数据有一定的性能优化但不多。网上有很多文章说read_mostly的数据会常驻cache中,这样不是很合理,L1缓存是稀缺资源内核中有很多变量的定义为这种属性,如果这些变量常驻缓存会造成缓存资源变少从而导致其他数据缓存miss的情况增加。 

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值