24.linux内核(线程/进程)同步——内存屏障

1.linux内核(线程/进程)同步——内存屏障
2.linux内核(线程/进程)同步——原子操作
3.linux内核(线程/进程)同步——自旋锁
4.linux内核(线程/进程)同步——信号量
5.linux内核(线程/进程)同步——互斥锁
6.linux内核(线程/进程)同步——完成量

多核同步需要关注的第一件事就是自己的值被有序确切的写入了内存。
由于编译器优化,CPU cache机制以及CPU乱序执行的原因,在多核CPU多线程条件下,线程或者进程交换数据未必是预期中的结果。这个时候就需要使用内存屏障来解决这个问题。

gcc编译选项

我们使用gcc的gcc-objdump -d来反汇编代码,查看编译结果。通常情况下gcc会默认连接启动文件crtl.ocrti.ocrtend.o,这些会干扰我们查看汇编代码。使用-nostartfiles编译选项禁止连接启动文件。另外还会连接标准库,使用-nostdlib编译选项禁用。最后差不多是下面这个样子。

tergets := main_1

objs := $(addsuffix ".o", $(tergets))
srcs := $(addsuffix ".c", $(tergets))

all:
	arm-linux-gcc -nostartfiles -nostdlib $(srcs) -o $(objs) -O2
	arm-linux-objcopy $(objs) $(tergets)
	arm-linux-objdump $(tergets) -d

clean:
	rm -f $(tergets) $(objs)

编译器优化

看两个编译器优化导致的代码优化问题。

#include <stdio.h>

int main()
{
	int i = 10;
	i++;
	i = 9;
	return i;
}
main_1:     文件格式 elf32-littlearm


Disassembly of section .text:

000100b8 <main>:
   100b8:	e3a0000b 	mov	r0, #9	;返回值9
   100bc:	e12fff1e 	bx	lr		;返回

可以看到i++i = 9被直接无视了,汇编代码直接返回了9。如果是在设置外设寄存器的值,这个时候就会莫名其妙的设置不成功,因为设置值的代码被优化掉了,压根没执行。volatile关键字可以阻止编译器删减代码,下面使用volatile关键字阻止编译优化。

#include <stdio.h>

int main()
{
	volatile int i = 10;
	i++;
	i = 9;
	return i;
}
main_1:     文件格式 elf32-littlearm


Disassembly of section .text:

000100b8 <main>:
   100b8:	e24dd008 	sub	sp, sp, #8		;栈指针向下移动8字节,说明栈向下生长,先放值,后增长
   100bc:	e3a0300a 	mov	r3, #10			;立即数10放入r3
   100c0:	e58d3004 	str	r3, [sp, #4]	;将本地变量10放到栈里面(i)
   100c4:	e59d3004 	ldr	r3, [sp, #4]	;取本地变量10放入r3(i)
   100c8:	e3a02009 	mov	r2, #9			;将9放入r2
   100cc:	e2833001 	add	r3, r3, #1		;10++
   100d0:	e58d3004 	str	r3, [sp, #4]	;吧11放入栈(i)
   100d4:	e58d2004 	str	r2, [sp, #4]	;吧9放入栈(i)
   100d8:	e59d0004 	ldr	r0, [sp, #4]	;吧栈内容(i)放入r0准备返回
   100dc:	e28dd008 	add	sp, sp, #8		;清空栈
   100e0:	e12fff1e 	bx	lr				;返回

可以看到volatile 关键字加上就老实了。常见的writel()函数精妙就在于volatile 关键字。

static inline void __raw_writel(u32 b, volatile void __iomem *addr)
{
	*(volatile u32 __force *) addr = b;
}
#define writel(b,addr) __raw_writel(__cpu_to_le32(b),addr)

另外i++i=9明显被掉换了位置,我们插入一个内存屏障阻止编译器换位置。

#include <stdio.h>

int main()
{
	volatile int i = 10;
	i++;
	__asm__ __volatile__ ("" : : : "memory");
	i = 9;
	return i;
}
main_1:     文件格式 elf32-littlearm


Disassembly of section .text:

000100b8 <main>:
   100b8:	e24dd008 	sub	sp, sp, #8
   100bc:	e3a0300a 	mov	r3, #10
   100c0:	e58d3004 	str	r3, [sp, #4]
   100c4:	e59d3004 	ldr	r3, [sp, #4]
   100c8:	e2833001 	add	r3, r3, #1
   100cc:	e58d3004 	str	r3, [sp, #4]
   100d0:	e3a03009 	mov	r3, #9
   100d4:	e58d3004 	str	r3, [sp, #4]
   100d8:	e59d0004 	ldr	r0, [sp, #4]
   100dc:	e28dd008 	add	sp, sp, #8
   100e0:	e12fff1e 	bx	lr

使用__asm__ __volatile__ ("" : : : "memory");内存屏障可以阻止编译器改变代码的执行顺序。这里的__volatile__volatile关键字是一个意思,防止编译器删除这段看似无用的代码。

内核已经封装好了相关的函数,可以直接用
compiler-gcc.h

#define barrier() __asm__ __volatile__("": : :"memory")

CPU乱序执行

高级CPU都具有乱序执行的能力,以提高流水线效率与cache命中率。但是有时候我们不希望我们代码的顺序被打乱了,这个时候就需要使用DMB DSB ISB 这三条汇编指令,指令含义如下:

指令 含义
DMB 数据内存屏障,在他前面的内存访问执行完毕后才会执行后面的内存访问
DSB 数据同步屏障,比DMB严格,除了等待内存访问完毕,还会等待所有缓存、跳转预测和TLB维护完成
ISB 最严格,除了DSB以外还会清空流水线

内核已经封装好了相关的函数,可以直接用
compiler-gcc.h

#define barrier() __asm__ __volatile__("": : :"memory")

barriers.h

#define nop() __asm__ __volatile__("mov\tr0,r0\t@ nop\n\t");

#define isb() __asm__ __volatile__ ("isb" : : : "memory")
#define dsb() __asm__ __volatile__ ("dsb" : : : "memory")
#define dmb() __asm__ __volatile__ ("dmb" : : : "memory")

#define mb()		do { dsb(); outer_sync(); } while (0)
#define rmb()		dsb()
#define wmb()		mb()

#define smp_mb()	barrier()
#define smp_rmb()	barrier()
#define smp_wmb()	barrier()

猜你喜欢

转载自blog.csdn.net/qq_16054639/article/details/107513334