Linux内核9、10-内核同步

Linux内核第9、10章

共享资源防止并发访问,因为多个执行线程同时访问和操作数据会导致在各线程之间数据相互覆盖,造成被访问数据不一致状态。

临界区和竞争条件:

所谓临界区(临界段)就是访问和操作共享数据的代码段。整个临界区原子执行,如同不可分割的指令一样。

如果两个执行线程有可能处于同一个临界区中同时执行,那么这就是程序的一个bug。如果这种情况发生了就称之为竞争条件(race condition)。避免并发和防止竞争条件称为同步。

多数处理器都提供了指令来原子地读变量、增加变量、然后写变量。两个原子操作交错执行不可能发生,因为处理器会从物理上确保这种不可能。

加锁:Linux各种锁机制之间的区别在于:当锁已经被其它线程持有,因而不可用时的行为表现----一些锁被争用时会简单地执行忙等待,而另一些会使当前任务睡眠直到锁可用为止。

内核中造成并发的原因:

-中断,中断几乎可以在任何时刻异步发生,也就可能随时打断当前正在执行的代码。

-软中断和tasklet,内核能在任何时刻唤醒或调度软中断和tasklet,打断当前正在执行的代码。

-内核抢占,内核具有抢占性,内核中的任务可能会被另一任务抢占。

-睡眠及与用户空间的同步,在内核执行的进程可能会睡眠,这就会唤醒调度程序,从而导致调度一个新的用户进程执行。

-对称多处理,两个或多个处理器可以同时执行代码。

在中断处理程序中能避免并发访问的安全代码称作中断安全代码(interrupt-safe),在对称多处理的机器中能避免并发访问的安全代码称为SMP安全代码(SMP-safe),在内核抢占时能避免并发访问的安全代码称为抢占安全代码(preempt-safe)。

死锁:

死锁产生需要一些条件:要有一个或多个执行线程和一个或多个资源,每个线程都在等待其中的一个资源,但所有的资源都已经被占用了。所有线程都在互相等待,但它们永远不会释放已经占有的资源。于是任何线程都无法继续。

锁的争用:也称争用,是指当锁正在被占用时,有其他线程试图获得该锁。说一个锁处于高度争用状态,就是指有多个其它线程在等待获得该锁。由于锁的作用是使程序以串行方式对资源进行访问,所以使用锁无疑会降低系统的性能。

第10章:

原子操作:

同步方法中的原子操作,是其它同步方法的基石。原子操作可以保证指令以原子的方式执行---执行过程不被打断。

内核提供了两组原子操作接口---一组针对整数进行操作,另一组针对单独的位进行操作。

原子整数操作:

针对整数的原子操作只能对atomic_t类型的数据进行处理。一可确保原子操作只与这种特殊类型数据一起使用,同时也保证该类型的数据不会被传递给任何非原子函数。

使用atomic_t类型确保编译器不对相应的值进行访问优化---这点使得原子操作最终接收到正确的内存地址,而不只是一个别名。最后,在不同体系结构上实现原子操作时,使用atomic_t可以屏蔽其间的差异。atomic_t定义在<linux/types.h>中:

typedef struct{

  volatile int counter;

}atomic_t;

尽管Linux支持的所有机器上的整型数据都是32位的,但是使用atomic_t的代码只能将该类型的数据当做24位来用。这个限制完全是因为SPARC体系结构上,原子操作的实现不同于其它体系结构:32位int类型的低8位被嵌入了一个锁,因为SPARC体系结构对原子操作缺乏指令级支持,所以只能利用该锁来避免对原子类型数据的并发访问。

使用原子整型操作需要的声明都在<asm/atomic.h>文件中。

定义一个atomic_t类型数据:atomic_t v;

可以在声明时赋初值:atmoic_t v=ATOMIC_INIT(0);  ATOMIC_INIT(int i);

原子地读取整数变量v:int atomic_read(atomic_t*v)

原子地设置v值为i:void atomic_set(atomic_t*v, int i);

原子地给v加i:void atomic_add(int i, atomic_t*v)

原子地从v减i:void atomic_sub(int i, atomic_t*v)

原子地给v加1:void atomic_inc(atomic_t*v)

原子地从v减1:void atomic_dec(atomic_t*v)

原子地从v减i,如果结果等于0,返回真;否则返回假:int atomic_sub_and_test(int i, atomic_t*v)

原子地给v加i,如果结果是负数,返回真;否则返回假:int atomic_add_negative(int i, atomic_t*v)

原子地给v加i,且返回结果:int atomic_add_return(int i, atomic_t*v)

原子地从v减i,且返回结果:int atomic_sub_return(int i, atomic_t*v)    ??

原子地给v加1,且返回结果:int atomic_inc_return(int i, atomic_t*v)    ??

原子地从v减1,且返回结果:int atomic_dec_return(int i, atomic_t*v)

原子地从v减1,如果结果等于0,返回真;否则返回假:int atomic_dec_and_test(atomic_t*v)

原子地给v加1,如果结果等于0,返回真;否则返回假:int atomic_inc_and_test(atomic_t*v)

原子操作通常是内联函数,往往是通过内嵌汇编指令来实现的。如果某个函数本来就是原子的,那么它往往会被定义成一个宏。

对多数体系结构来讲,原子操作与更复杂的同步方法相比较,给系统带来的开销小,对高速缓存行(cache-line)的影响也小。

原子操作只保证原子性,顺序性通过屏障(barrier)指令来实施,后面讨论。

64位原子操作:

64位的atomic64_t,atomic_t即使在64位体系结构中也是32位的,所以使用atomic64_t来表示64位的原子变量:

typedef struct{

  volatile long counter;

}atomic64_t;

其接口函数与atomic_t接口相似,只是将所有的atomic_t换位atomic64_t以及函数名称中的atomic换位atomic64。

原子位操作:

除了原子整数操作外,内核也提供了一组针对位这一级数据进行操作的函数。定义在文件<asm/bitops.h>中。

位操作函数是对普通的内存地址进行操作的。它的参数有一个指针和一个位号,第0位是给定地址的最低有效位。虽然使用原子操作在多数情况下是对一个字长的内存进行访问,因而位号应该位于0~31(64位机器上是0~63),但是,对位号的范围并没有限制。

原子操作是对普通的指针进行的操作,不像原子整数操作对应的atomic_t。

原子位操作:

原子地设置addr所指对象的第nr位:  void set_bit(int nr, void*addr)

原子地清空addr所指对象的第nr位:  void clear_bit(int nr, void*addr)

原子地翻转addr所指对象的第nr位:  void change_bit(int nr, void*addr)

原子地设置addr所指对象的第nr位,并返回原先的值:  int test_and_set_bit(int nr, void*addr)

原子地清空addr所指对象的第nr位,并返回原先的值:  int test_and_clear_bit(int nr, void*addr)

原子地翻转addr所指对象的第nr位,并返回原先的值:  int test_and_change_bit(int nr, void*addr)

原子地返回addr所指对象的第nr位:  int test_bit(int nr, void*addr)

自旋锁:

因为临界区可能会非常复杂,如跨越多个函数。此时简单的原子操作对此无能为力,需要更复杂的同步方法----锁来保护。

Linux内核中最常见的锁是自旋锁(spin lock)。自旋锁最多只能被一个可执行线程持有。如果一个执行线程试图获得一个被已经持有(争用)的自旋锁,那么该线程就会一直进行忙循环---旋转-等待锁重新可用。要是锁未被争用,请求锁的执行线程便能立刻得到它,继续执行。

自旋锁自旋期间占用CPU时间,所以自旋锁不应该被长时间持有。这也是使用自旋锁的初衷:在短期内进行轻量级加锁。

还可以采取另外一种方法处理锁的争用:让请求线程睡眠,直到锁重新可用时再唤醒它。此时处理器不用忙循环,但是会有等待线程的上下文切换(换出和换入)操作耗时。下面将讨论的信号量便是采用这种机制,再发生争用时,等待的线程能投入睡眠,而不是旋转。

Linux内核实现的自旋锁不可递归,因为同一执行线程再次试图获取自旋锁时会陷入忙循环。

自旋锁方法定义在<asm/spinlock.h>中,接口文件为<linux/spinlock.h>,基本使用方法如下:

DEFINE_SPINLOCK(mr_lock);

spin_lock(&mr_lock);

/*..临界区..*/

spin_unlock(&mr_lock);

内核提供的禁止中断同时请求锁的接口:

DEFINE_SPINLOCK(mr_lock);

unsigned long flags;

spin_lock_irqsave(&mr_lock,flags);

/*..临界区..*/

spin_unlock_irqrestore(&mr_lock,flags);

函数spin_lock_irqsave()函数保存中断的当前状态,并禁止本地中断,然后再去获取指定的锁。反过来spin_unlock_irqrestore()对指定的锁解锁,然后让中断恢复到加锁前的状态。

自旋锁方法:

spin_lock()  获取指定的自旋锁

spin_lock_irq()  禁止本地中断并获取指定的锁

spin_lock_irqsave()  保存本地中断的当前状态,禁止本地中断,并获取指定的锁

spin_unlock()  释放指定的锁

spin_unlock_irq()  释放指定的锁,并激活本地中断

spin_unlock_irqrestore()  释放指定的锁,并让本地中断恢复到以前状态

spin_lock_init()  动态初始化指定的spinlock_t

spin_try_lock()  试图获取指定的锁,如果未获取,则返回非0,获取则返回0

spin_is_locked()  如果指定的锁当前已被占用,则返回非0,否则返回0

读-写自旋锁:

Linux内核提供了专门的读-写自旋锁,这种自旋锁为读和写分别提供了不同的锁。一个或多个读任务可以并发地持有读者锁;相反,用于写的锁最多只能被一个写任务持有,而且此时不能有并发的读操作。有时把读/写锁叫做共享/排斥锁,或者并发/排斥锁,因为这种锁以共享(对读者而言)和排斥(对写者而言)的形式获得使用。

读/写自旋锁的使用方法:

DEFINE_RWLOCK(mr_rwlock);

read_lock(&mr_rwlock);

/*...临界区(只读)..*/

read_unlock(&mr_rwlock);

write_lock(&mr_rwlock);

/*...临界区(读写)..*/

write_unlock(&mr_rwlock);

不能把一个读锁升级为写锁:

read_lock(&mr_rwlock);

write_lock(&mr_rwlock);

上述两个函数带来死锁,写锁会等待所有读锁被释放,也包括其本身的读锁。。。

read_lock()  获得指定的读锁

read_lock_irq()  禁止本地中断并获得指定读锁

read_lock_irqsave()  存储本地中断的当前状态,禁止本地中断并获得指定读锁

read_unlock()  释放指定的读锁

read_unlock_irq()  释放指定的读锁并激活本地中断

read_unlock_irqrestore()  释放指定的读锁并将本地中断恢复到指定的前状态

write_lock()  获得指定的写锁

write_lock_irq()  禁止本地中断并获得指定写锁

write_unlock()  释放指定的写锁

write_unlock_irq()  释放指定的写锁并激活本地中断

write_unlock_irqrestore()  释放指定的写锁并将本地中断恢复到指定的前状态

write_trylock()  试图获得指定的写锁;如果写锁不可用,返回非0值

rwlock_init()  初始化指定的rwlock_t

自旋锁提供了一种快速简单的锁实现方式。如果加锁时间不长并且代码不会睡眠(比如中断处理程序),利用自旋锁是最佳选择。如果加锁时间可能很长或者代码在持有时有可能睡眠。那么最好使用信号量来完成加锁功能。

信号量:

Linux中的信号量是一种睡眠锁。如果有一个任务试图获得一个不可用(已经被占用)的信号量时,信号量会将其推进一个等待队列,然后让其睡眠。这时处理器能重获自由,从而去执行其它代码。当持有的信号量可用(被释放)后,处于等待队列中的那个任务将被唤醒,并获得该信号量。

-由于争用信号量的进程在等待锁重新变为可用时会睡眠,所以信号量适用于锁会被长时间持有的情况。

-相反,锁被短时间持有时,使用信号量就不太适宜了。因为睡眠、维护等待队列以及唤醒所花费的开销可能比锁被占用的全部时间还要长。

-由于执行线程在锁被争用时会睡眠,所以只能在进程上下文中才能获取信号量锁,因为在中断上下文中是不能进行调度的。

-可以在持有信号量时去睡眠(也可能并不需要睡眠),因为当其它进程试图获得同一信号量时不会因此而死锁(因为该进程也只是去睡眠而已)。

-在占用信号量的同时不能占用自旋锁。因为在等待信号量时可能会睡眠,而持有自旋锁时是不允许睡眠的。

信号量不同于自旋锁,它不会禁止内核抢占,所以持有信号量的代码可以被抢占。

计数信号量和二值信号量:

信号量同时允许任意数量的锁持有者,信号量同时允许的持有者数量可以在声明信号量时指定。这个值称为使用者数量或简单地叫数量。通常情况下,信号量和自旋锁一样,在一个时刻仅允许有一个锁持有者,这时计数等于1,这样的信号量被称为二值信号量或者互斥信号量。另一方面,初始化时也可以把数量设置为大于1的非0值,这时信号量称为计数信号量,它允许在一个时刻至多有count个锁持有者。计数信号量不能用来进行强制互斥,因为它允许多个执行线程同时访问临界区。

信号量支持两个原子操作P()和V(),前者叫做测试操作,后者叫做增加操作。后来的系统把这两种操作分别叫做down()和up(),down()操作通过对信号量计数减1来请求获得一个信号量。如果结果是0或大于0,获得信号量锁,任务就可以进入临界区。如果结果是负数,任务会被放入等待队列,处理器执行其它任务。当临界区中的操作完成后,up()操作用来释放信号量,该操作也被称为是提升信号量,因为它会增加信号量的计数值。如果在该信号量上的等待队列不为空,那么处于队列中等待的任务在被唤醒的同时会获得该信号量。

信号量操作:

信号量接口定义在<asm/semaphore.h>中。struct semaphore类型用来表示信号量。

struct semaphore name;

sema_init(&name,count);

name是信号量的变量名,count是信号量的使用数量。

创建更为普通的互斥信号量可以使用以下快捷方式:

static DECLARE_MUTEX(name);

init_MUTEX(name);

函数down_interruptible()试图获取指定的信号量,如果信号量不可用,它将把调用进程置成TASK_INTERRUPTIBLE状态---进入睡眠,这种进程状态可被唤醒,如果进程在等待获取信号量的时候接收到了信号,那么该进程就会被唤醒,而函数down_interruptible()会返回-EINTR。另一个函数down()会让进程在TASK_UNINTERRUPTIBLE状态下睡眠,这样一来进程在等待信号量时就不再响应信号了。down_trylock()函数,可以尝试以阻塞(意味着可以马上获得返回值)方式来获取指定的信号量。在信号量已被占用时,它立刻返回非0值;否则返回0而且持有信号量锁。

释放指定的信号量时,使用up()函数。

方法和描述:

sema_init(struct semaphore*,int)  以指定的计数值初始化动态创建的信号量

init_MUTEX(struct semaphore*)  以计数值1初始化动态创建的信号量

init_MUTEX_LOCKED(struct semaphore*)  以计数值0初始化动态创建的信号量(初始为加锁状态)

down_interruptible(struct semaphore*)  以试图获得指定的信号量,如果信号量已被争用,则进入可中断睡眠状态

down(struct semaphore*)  以试图获得指定的信号量,如果信号量已被争用,则进入不可中断睡眠状态

down_trylock(struct semaphore*)  以试图获得指定的信号量,如果信号量已被争用,则立刻返回非0值;未被争用则返回0并且执行线程获得该信号量锁

up(struct semaphore*)  以释放指定的信号量,如果睡眠队列不空,则唤醒其中一个任务

读-写信号量:

与自旋锁一样,信号量也有区分读-写访问的可能。读-写信号量在内核中是由rw_semaphore结构表示的,定义在文件<linux/rwsem.h>中。

创建静态声明的读-写信号量:

static DECLARE_RWSEM(name);  //name是信号量名

初始化:

init_rwsem(struct rw_semaphore*sem);

所有的读-写信号量都是互斥信号量,它的引用计数等于1,只对写者互斥不对读者。只要没有写者,并发持有读锁的读者数不限,而只有唯一的写者可以获得写锁。所有读-写锁的睡眠都不会被信号打断,所以它只有一个版本的down()操作。

static DECLARE_RWSEM(name);

down_read(&name);

//临界区(只读)

up_read(&name);

//释放信号量

down_write(&name);

//临界区(只写)

up_write(&name);

读写信号量也提供了down_read_trylock()和down_write_trylock()操作,阻塞版本,立刻获得返回值。

读写信号量相比读写自旋锁多一种特有的操作:downgrade_write(),这个函数可以动态地将获取的写锁转换为读锁。

互斥体:

以上所述,内核中允许睡眠的锁是信号量。互斥体所指的是任何可以水面的强制互斥锁,比如使用计数是1地信号量。在最新的Linux内核中,互斥体也用于一种实现互斥的特定睡眠锁。也就是说,互斥体是一种互斥信号。

mutex在内核中对应的数据结构是mutex,其行为和使用计数为1地信号量类似,但操作接口更简单,实现也更高效,而且使用限制更强。

静态定义mutex:

DEFINE_MUTEX(name);

动态初始化mutex:

mutex_init(&mutex);

对互斥锁锁定和解锁:

mutex_lock(&mutex);

//临界区

mutex_unlock(&mutex);

方法和描述:

mutex_lock(struct mutex*)  为指定的mutex上锁,如果锁不可用则睡眠

mutex_unlock(struct mutex*)  为指定的mutex解锁

mutex_trylock(struct mutex*)  试图获取指定的mutex,如果成功则返回1;否则锁被获取,返回值是0

mutex_is_locked(struct mutex*)  如果锁已被争用,则返回1;否则返回0

-任何时刻中只有一个任务可以持有mutex,也就是说,mutex的使用计数永远是1;

-给mutex上锁者必须负责给其再解锁。不能在一个上下文中锁定一个mutex,而在另一个上下文中给它解锁。这个限制使得mutex不适合内核同用户空间复杂的同步场景。通常在一个上下文中上锁和解锁。

-递归地上锁和解锁是不允许的;

-当持有一个mutex时,进程不可以退出;

-mutex不能在中断或者下半部中使用,即使mutex_trylock()也不行;

-mutex只能通过官方API管理:它只能使用上面描述的方法初始化,不可拷贝、手动初始化或者重新初始化。

信号量和互斥体:

互斥体功能类似与二值信号量。一般优先使用互斥体,偏底层代码中使用信号量。

完成变量:

如果在内核中一个任务需要发出信号通知另一任务发生了某个特定事件,利用完成变量(completion variable)是使两个任务得以同步的简单方法。

如果一个任务要执行一些工作时,另一个任务就会在完成变量上等待。当这个任务完成工作后,会使用完成变量去唤醒在等待的任务。

完成变量由结构completion表示,定义在<linux/completion.h>中。

通过以下宏静态地创建完成变量并初始化它:

DECLARE_COMPLETION(mr_comp);

通过init_completion()动态创建并初始化完成变量。在一个指定的完成变量上,需要等待的任务调用wait_for_comletion()来等待特定事件。当特定事件发生后,产生事件的任务调用complete()来发送信号唤醒正在等待的任务。

方法和描述:

init_completion(struct completion*)  初始化指定的动态创建的完成变量

wait_for_completion(struct completion*)  等待指定的完成变量接收信号

complete(struct completion*)  发信号唤醒任何等待任务

完成变量的通常用法是:将完成变量作为数据结构中的一项动态创建,而完成数据结构初始化工作的内核代码将调用wait_for_completion()进行等待。初始化完成后,初始化函数调用completion()唤醒在等待的内核任务。

大内核锁BLK:

顺序锁:

顺序锁,简称seq锁,这种锁提供了一种很简单的机制,用于读写共享数据。实现这种数据主要依靠一个序列计数器。当有疑义的数据被写入时,会得到一个锁,并且序列值会增加。在读取数据之前和之后,序列号都被读取。如果读取的序列号值相同,说明在读操作进行的过程中没有被写操作打断过。此外,如果读取的值是偶数,那么就表明写操作没有发生(要明白因为锁的初值是0,所以写锁会使值为奇数,释放的时候变成偶数。

定义一个seq锁:

seqlock_t mr_seq_lock=DEFINE_SEQLOCK(mr_seq_lock);

写锁的方法:

write_seqlock(&mr_seq_lock);

//写锁被获取

write_sequnlock(&mr_seq_lock);

读时(与自旋锁有很大不同):

unsigned long seq;

do{

  seq=read_seqbegin(&mr_seq_lock);

  //读这里的数据

}while(read_seqretry(&mr_seq_lock,seq));

在多个读者和少数写者共享一把锁的时候,seq锁有助于提供一种非常轻量级和具有可扩展性的外观。但是seq锁对写者更有利。只要没有其它写者,写锁总能被成功获得,读者不会影响写锁,这点和读-写锁及信号量一样。另外,挂起的写者会不断地使得读操作循环,直到不再有任何写者持有锁为止。

seq锁适合以下需求:

-数据存在很多读者;

-数据写者很少;

-虽然写者很少,但是希望写者优先于读,而且不允许读者让写者及饥饿;

-数据很简单,如简单结构,甚至是简单的整型---在某些场合,是不能使用原子量的;

顺序锁总结:seq锁分为读操作和写操作,其对读写的安排通过一个序列计数器实现。序列计数器初始为0,写操作前后都会使序列计数器增加1。写操作是互斥的,存在一个写者占用seq锁时,其它试图获取seq锁的写者以自旋方式等待。读操作不会对seq锁的序列计数器进行增加操作,只是访问序列计数器的值;读操作通过do-while循环来读取直到读取到满足条件的情况,其满足的条件是在读临界区域前、后seq锁序列计数器的值相等且为偶数(为偶数时没有写者占用锁),否则(存在某个写者获取了该锁)就继续循环。。所以seq锁对写者响应会更快,读者获取不到锁时会循环操作,所以seq锁更适合写者较少的情况,这时读者的循环次数少效率更高。

禁止抢占:

由于内核是抢占性的,内核中的进程在任何时刻都可能停下来以便另一个具有更高优先权的进程运行。这意味着一个任务与被抢占的任务可能会在同一个临界区内运行。为了避免这种情况,内核抢占代码使用自旋锁作为非抢占区域的标记。如果一个自旋锁被持有,内核便不能进行抢占。

因为内核抢占和SMP面对相同的并发问题,并且内核已经是SMP安全的了(多处理器并发安全),所以这种简单的变化使得内核也是抢占安全的(preempt-safe)。

实际中,某些情况不需要自旋锁,但是仍然需要关闭内核抢占。比如每个处理器上的数据,如果数据对每个处理器是唯一的,那么这样的数据可能就不需要使用锁来保护,因为数据只能被一个处理器访问。如果自旋锁没有被持有,内核又是抢占式的,那么一个新的调度任务就可能访问同一个变量。(如任务A写处理器1和2上的x变量,B也写处理器1和2上的x变量,可能发生异步情况使得数据被覆盖)

通过preempt_disable()禁止内核抢占。这时一个可以嵌套调用的函数,可以调用任意次。每次调用都必须有一个相应的preempt_enable()调用。当最后一次preempt_enable()调用后,内核抢占才重新启用。

preempt_disable();

//抢占被禁止

preempt_enable();

抢占计数存放着被持有锁的数量和preempt_disable()的调用次数,如果计数是0,那么内核可以进行抢占;如果为1或更大的值。那么内核就不会进行抢占。函数preempt_count()返回这个值。preempt_enable_no_resched()激活内核抢占但不再检查任何被挂起的需调度任务。

顺序和屏障:

当处理多处理器之间或硬件设备之间的同步问题时,有时需要在你的程序代码中以指定的顺序发出读内容(读入)和写内存(存储)指令。在和硬件交互时,时常需要确保一个给定的读操作发生在其它写操作之前。另外,在多处理器上,可能需要按写数据的顺序读数据(通常确保后来以同样的顺序进行读取)。但是编译器和处理器为了提高效率,可能对读和写重新排序,这样使得问题复杂化了。不过,所有可能重新排序和写的处理器提供了机器指令来确保顺序要求。同样也可以指示编译器不要对给定点周围的指令序列进行重新排序。这些确保顺序的指令称作屏障(barriers)。

rmb()方法提供了一个“读”内存屏障,它确保跨越rmb()的载入动作不会发生重排序。也就是说,在rmb()之前的载入操作不会重新排序在该操作之后,同理,在rmb()之后的载入操作不会被重新排序在该调用之前。

wmb()方法提供了一个“写”屏障,这个函数的功能和rmb()类似,区别仅仅是它是针对存储而非载入---它确保跨屏障的存储不发生重新排序。

mb()方法既提供了读屏障也提供了写屏障。载入和存储动作都不会跨越屏障重新排序。

read_barrier_depends()是rmb()的变种,它提供了一个读屏障,但是仅仅是针对后续读操作所依赖的那些载入。因为屏障后的读操作依赖于屏障前的读操作,因此该屏障确保屏障前的读操作在屏障后的读操作之前完成。

smp_rmb(),在SMP上提供rmb()功能,在UP上提供barrier()功能。

smp_read_barrier_depends(),在SMP上提供read_barrier_depends()功能,在UP上提供barrier()功能。

smp_wmb(),在SMP上提供wmb()功能,在UP上提供barrier()功能。

smp(),在SMP上提供mb()功能,在UP上提供barrier()功能。

barrier(),阻止编译器跨屏障对载入或存储操作进行优化。

原文地址:https://www.cnblogs.com/cjj-ggboy/p/12285463.html