内存屏障和中断屏蔽
参考资料:《宋宝华 Linux设备驱动开发详解》
内存屏障:
内存屏障(Memory Barrier)是一种硬件或软件机制,用于协调并发访问共享资源时的数据一致性。它可以控制处理器和缓存对内存操作的顺序和时序,从而确保共享变量的读写操作按照预期方式进行。
barrier() 函数通常用于创建一个同步点(barrier),在该同步点处,多个线程或进程需要等待其他所有线程或进程都完成某个阶段的工作后才能继续执行后续的操作。
在多线程编程中,barrier() 函数通常与多线程同步和并发控制相关。它可以用于确保所有线程都完成了一定的任务后再进行下一步操作,从而避免竞态条件和数据不一致性问题。
#define barrier() \ __asm__ __volatile__("": : :"memory") barrier_data 是宏的名称,接受一个指针作为参数。 __asm__ __volatile__ 是内联汇编的标记,用于告诉编译器后面的内容是内联汇编代码,并且禁止编译器对其进行优化。 "" 表示内联汇编没有任何操作数。 :: 表示没有输入和输出操作数,即这个内联汇编没有读取或写入任何寄存器或内存位置。 :"memory" 是一个内存约束,告诉编译器内存会被修改,因此需要在内存上刷新相关操作并防止优化。 #define example_macro(arg1, arg2) \ __asm__ __volatile__ ( \ "add %0, %1, %2" \ : "=r" (arg1) \ : "r" (arg2), "r" (3) \ ) example_macro 是宏的名称,接受两个参数 arg1 和 arg2。 __asm__ __volatile__ 是内联汇编的标记,告诉编译器接下来的内容是内联汇编代码,并且禁止编译器对其进行优化。 "add %0, %1, %2" 是实际的汇编指令,将寄存器 %1 和立即数 3 相加,结果存储在寄存器 %0 中。 : "=r" (arg1) 表示输出操作数,使用寄存器(r)来存储 arg1 的值,并将其约束为输出。 : "r" (arg2), "r" (3) 表示输入操作数,使用寄存器(r)来存储 arg2 和立即数 3 的值,并将其约束为输入。 "=r" (arg1) 表示将计算结果存储到一个通用寄存器中,并将其约束为输出。这里的 "=" 符号表示这是一个输出操作数,而 "r" 则表示将结果存储到一个通用寄存器中。
处理器为了解决多核间一个核的内存行为对另外一个核可见的问题,引入了一些内存屏障的指令。比如,ARM处理器的屏障指令包括:
DMB(Data Memory Barrier):
DMB指令用于强制数据内存操作的顺序性。
DMB确保在DMB指令之前的所有数据访问操作都在DMB执行完成之前完成,而在DMB之后的数据访问操作将在DMB执行完成之后进行。
例如:DMB SY表示强制数据内存操作的顺序性。
DSB(Data Synchronization Barrier):
DSB指令用于确保数据同步操作的顺序性。
DSB确保在DSB指令之前的所有数据访问和同步操作都在DSB执行完成之前完成,而在DSB之后的数据访问和同步操作将在DSB执行完成之后进行。
例如:DSB SY表示强制数据同步操作的顺序性。
ISB(Instruction Synchronization Barrier):
ISB指令用于确保指令同步操作的顺序性。
ISB确保在ISB指令之前的所有指令都在ISB执行完成之前完成,而在ISB之后的指令将在ISB执行完成之后进行。
例如:ISB表示强制指令同步操作的顺序性。
Linux内核的自旋锁、互斥体等互斥逻辑,都要用到上述指令;请求获得锁时,调用屏障指令,在解锁时,也要调用屏障指令
在多线程或多核处理器系统中,为了避免竞态条件(Race Condition)等问题,常常需要使用内存屏障来保证数据的一致性。内存屏障通常分为以下几种:
- 读屏障(Read Barrier): 确保所有之前的读操作已经完成,防止后续的读操作获取到脏数据。
- 写屏障(Write Barrier): 确保所有之前的写操作已经完成,防止后续的写操作干扰到其他线程或进程。
- 全屏障(Full Barrier):确保所有之前的读写操作已经完成,防止后续的读写操作出现异常或冲突。
Linux内核定义了读屏障rmb()、写屏障wmb()、读写屏障mb()、以及作用与寄存器读写的__iormb()、__iowmb等屏障API。读写寄存器的readb_relaxed()和readb()、writel_relaxed()和writel()等API区别就在是否有屏蔽上
#define readb(c) ({u8 __v = readb_relaxed(c); _iormb(); __v;}) #define readw(c) ({u16 __v = readw_relaxed(c); _iormb(); __v;}) #define readl(c) ({u32 __v = readl_relaxed(c); _iormb(); __v;}) #define writeb(c) ({ __iowmb(); writeb_relaxed(v,c); }) #define writew(c) ({ __iowmb(); writew_relaxed(v,c); }) #define writel(c) ({ __iowmb(); writel_relaxed(v,c); })
例如通过writel_relaxed()写完DMA的开始地址、结束地址、大小之后,一定要调用writel()来启动DMA
writel_relaxed(DMA_SRC_REG, src_addr); writel_relaxed(DMA_DST_REG, dst_addr); writel_relaxed(DMA_SIZE_REG, size); writel(DMA_ENABLE, 1);
中断屏蔽:
在单CPU范围内避免竞态的一种简单有效的方法就是在进入临界区之前屏蔽系统的中断,但是在驱动编程中不推荐,CPU一般都具备中断屏蔽和打开中断的功能,这项功能可以保证正在运行的内核执行路径不被中断处理程序所抢占,防止某些竞态条件的发生。具体而言,中断屏蔽将使得中断与进程之间的并发不再发生。而且,由于Linux内核的进程调度等操作都依赖中断来实现,内核抢占进程之间的并发也得以避免了
中断屏蔽的使用方法为:
local_irq_disable() // 屏蔽中断 ... critical section // 临界区 ... local_irq_enable() // 开中断
底层逻辑原理是让CPU本身不响应中断,比如,对于ARM处理器而言,其底层的实现是屏蔽ARM CPSR的1位:
static inline void arch_local_irq_disable(void) { asm volatile("cpsid i":::"memory","cc"); }
由于Linux的异步IO、进程调度等很多重要的操作都依赖中断,所以屏蔽中断之后,需要尽快完成临界区的代码
local_irq_disable()和local_irq_enable()都只能禁止和使能本CPU中断,因此,并不能解决SMP多CPU引发的竞态。因此,单独使用中断屏蔽不是一个避免竞态的好方法,它适合与自旋锁联合使用
与local_irq_disable()不同的是,local_irq_save(flags)除了进行禁止中断的操作之外,还保存目前CPU的中断位信息。local_restore_save(flags)进行的local_irq_save(flags)进行相反的操作。对于ARM处理器而言,其实就是保存和恢复CPSR