Linux内核设计与实现 总结笔记(第十章)内核同步方法

一、原子操作

 原子操作可以保证指令以原子的方式执行----执行过程不被打断。

1.1 原子整数操作

针对整数的原子操作只能对atomic_t类型的数据进行处理。

  • 首先,让原子函数只接收atomic_t类型的操作数,可以确保原子操作只与这种特殊类型数据一起使用,并且不会被其他人使用。
  • 使用atomic_t类型确保编译器不对,相应的值进行访问优化。
  • 在不同体系结构上实现原子操作的时候,使用atomic_t可以屏蔽其间的差异

atomic_t类型定义在文件<linux/types.h> 中

typedef struct {
    volatile int counter;
} atomic_t;

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

定义一个atomic_t类型的数据方法很平常,还可以在定义时给它设定初值:

atomic_t v;                                        /* 定义v */
atomic_t u = ATOMIC_INIT(0);           /* 定义u并把它初始化为0 */

atomic_set(&v, 4);                                /* v = 4 */
atomic_add(2, &v);                                /* v = v + 2 = 6 */
atomic_inc(&v);                                   /* v = v + 1 = 7 */

如果需要atomic_t转换成int,则需要atomic_read()来完成。

还可以用原子整数操作原子的执行一个操作并检查结果。

printk("%d
", atomic_read(&v));    /* 会打印"7" */
int atomic_dec_and_test(atomic_t *v)

某种特定的体系结构上实现的所有操作可以在文件<asm/atomic.h>中找到

ATOMIC_INIT(int i);
/* 在声明一个atomic_t变量时,将它初始化为i */

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

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减i */

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

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

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

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减i,且返回结果 */

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

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

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

int atomic_inc_and_test(atomic_t *v)
/* 原子地给v加1,如果结果等于0,返回真,否则返回假 */
原子整数操作列表

在编写代码时,能使用原子操作时,就尽量不要使用复杂的加锁机制。

1.2 64位原子操作

随着64位操作系统的普及,因为atomic_t便令无法在体系结构之间改变。所以atomic_t类型即便在64位下也是32位的,需要使用64位的原子变量

atomic64_t类型,其功能和32位原子操作无异,不同的只有整型变量大小从32位变成了64位,atomic64_t类型其实是对长整型的一个简单封装类。

ATOMIC64_INIT(int i);
/* 在声明一个atomic_t变量时,将它初始化为i */

int atomic64_read(atomic_t *v);
/* 原子地读取整数变量v */

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

void atomic64_add(int i, atomic_t *v)
/* 原子地给v加i */

void atomic64_sub(int i, atomic_t *v)
/* 原子地从v减i */

void atomic64_inc(atomic_t *v)
/* 原子地给v加1 */

void atomic64_dec(atomic_t *v)
/* 原子地从v减1 */

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

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

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

int atomic64_sub_return(int i,atomic_t *v)
/* 原子地从v减i,且返回结果 */

int atomic64_inc_return(int i, atomic_t *v)
/* 原子地给v加1,且返回结果 */

int atomic64_dec_return(int i, atomic_t *v)
/* 原子地给v减1,且返回结果 */

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

int atomic64_inc_and_test(atomic_t *v)
/* 原子地给v加1,如果结果等于0,返回真,否则返回假 */
原子整型操作

1.3 原子位操作

定义在文件<asm/bitops.h>中,例子:

unsigned long word = 0;

set_bit(0, &word);                    /* 第0位被设置(原子地) */
set_bit(1, &work);                    /* 第1位被设置(原子地) */
printk("%u1
", word);            /* 打印3 */
clear_bit(1, &word);                    /* 清空第1位 */
change_bit(0, &word);                /* 反转第0位的值,这里它被清空 */

/* 原子地设置第0位并且返回设置前的值(0) */
if(test_and_set_bit(0, &word) {
    /* 永远不为真 */
}

/* 下面的语句是合法的,你可以把原子位指令与一般的C语言混在一起 */
word = 7;
例子

标准原子位操作列表:

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)
/* 原子地返回addr所指对象的第nr位 */
原子位操作的列表

内核还提供了一组与上述操作对应的非原子位函数,非原子位函数与原子位函数的操作完全相同。

但是前者不保证原子性,其名字前缀多两个下划线。 比如test_bit()和__test_bit()

内核还提供了两个例程用来指定的地址开始搜索第一个被设置的位。

int find_first_bit(unsigned long *addr, unsigned int size)
int find_first_zero_bit(unsigned long *addr, unsgined int size)
/* 第一参数是一个指针,第二个参数是要搜索的总位数 */
额外

二、自旋锁

Linux内核中最常见的锁是自旋锁。自旋锁最多只能被一个可执行线程持有。

自旋锁的要点:被争用的自旋锁使得请求它的线程在等待锁重新可用时自选(特别浪费处理器时间),所以自旋锁不应该被长时间持有。

这也是自旋锁的初衷:在短时间内进行轻量级加锁。持有自旋锁的时间最好小于完成两次上下文切换的耗时。

2.1 自旋锁的方法

相关的体系结构代码在文件<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_irq()和spin_unlock_irq()会更好点,

DEFINE_SPINLOCK(mr_lock);

spin_lock_irq(&mr_lock);
/* 关键节 */
spin_unlock_irq(&mr_lock);
不提倡使用spin_lock_irq

2.2 其他针对自旋锁的操作

可以使用spin_lock_init()方法初始化动态创建的自旋锁,spin_try_lock()试图获得某个特定自旋锁,如果已被争用,那么该防范会立刻返回一个非0值,而不会等待自旋锁释放。

如果成功获得了这个锁,返回0。同理spin_is_lock(),用于检查特定锁是否被占用。占用非0,否则返回0

spin_lock()
/* 获取指定的自旋锁 */

spin_lock_irq()
/* 进制本地中断并获取指定的锁 */

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

spin_unlock()
/* 释放指定的锁 */

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

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

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

spin_is_locked()
/* 如果指定的锁当前正在被获取,则返回非0,否则返回0 */
自旋锁方法列表

2.3 自旋锁和下半部

与下半部配合使用时,必须小心使用锁机制。函数spin_lock_bh()用于获取指定锁,同时会禁止所有下半部的执行。

相应的spin_unlock_bh()函数执行相反的操作

三、读--写自旋锁

有时锁的用途可以明确的分为读取和写入两个场景。

当对某个数据结构的操作可以划分为 读/写或者消费者/生产者两种类别时,类似读/写锁这样的机制就很有帮助了。

/* 读/写自旋锁的初始化 */
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_irqrestore()
/* 释放指定的读锁并将本地中断恢复到指定的前状态 */

write_lock()
/* 获得指定的写锁 */

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

write_lock_irqsave()
/* 储存本地中断的当前状态,进制本地中断并获得指定写锁 */

write_unlock()
/* 释放指定的写锁 */

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

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

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

rwlock_init()
/* 初始化指定的rwlock_t */
读-写自旋锁方法列表

读写锁会照顾读比照顾写要多一点,所以大量读者必定会使挂起的写着处于饥饿状态

四、信号量

Linux信号量是一种睡眠锁,如果有一个任务试图获得一个不可用的信号量时,信号量会将其推进一个等待队列,让后让其睡眠。

这时处理器能重获自由,从而去执行其他代码。当持有信号量可用后,处于等待队列中的那个任务将被唤醒。

信号量比自旋锁提供了更好的处理器利用率,因为没有把时间花费在忙等待上,但是,信号量比自旋锁有更大的开销。

  • 由于争用信号量的进程在等待锁重新变为可用时会睡眠,所以信号量适用于锁会被长时间持有的情况
  • 相反,如果锁短时间持有,信号量就不太适宜了。因为睡眠、维护等待队列以及唤醒所花费的开销可能比锁被占用的全部时间还要长
  • 由于执行线程在锁被争用时会睡眠,所以只能在进程上下文中才能获取信号量锁,因为在中断上下文中是不能进行调度的
  • 你可以在持有信号量时去睡眠,因为当其他进程试图获得统一信号量时不会因此死锁
  • 在你占用信号量的同时不能占用自旋锁。因为你等地啊信号量时可能睡眠,自旋锁不允许睡眠

如果需要在自旋锁和信号量中做选择,应该根据锁被持有的时间长短做判断。

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

信号量同时允许的持有者数量可以在声明信号量时指定。这个值称为使用者数量(usage count)或简单地叫数量(count)。

通常情况下,信号量和自旋锁一样,一个时刻允许一个持有者。这样的的信号量被称为二值信号量或者称为互斥信号量。

如果初始化时为大于1的非0值,信号量被称为计数信号量,允许一个时刻至多有count个锁持有者。

信号量有两个down()和up()操作,down操作通过信号量计数减1来请求获得一个信号量,如果结果是0或大于0,获得信号量。否则就进入等待队列。

up()操作用来释放信号量,也被称作提升,因为会增加信号量计数值。如果在该信号量上的等待队列不为空,那么等待队列的任务会被唤醒同时获得信号量。

4.2 创建和初始化信号量

在头文件<asm/semaphore.h>中,可以通过静态地声明信号量

struct semaphore name;
sema_init(&name, count);
/* name是信号量变量名,count是信号量的使用数量 */

static DECLARE_MUTEX(name);
/* 更为普通的信号量的创建 */

sema_inti(sem, count);
/* 动态创建,sem是指针,count是信号量的使用者数量 */

init_MUTEX(sem);
创建和初始化信号量

4.3 使用信号量 

函数down_interruptible()试图获取指定的信号量,如果信号量不可用,它将把调用进程置成TASK_INTERRUPTIBLE状态----进入睡眠。

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值 */

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

五、读--写信号量

定义在文件<linux/rwsem.h>中,通过以下语句可以创建静态声明

static DECLARE_RWSEM(name);
/* 静态创建,name是新信号量名 */

init_rwsem(struct rw_semaphore *sem)
/* 动态创建, */
静态动态创建

所有的读写信号量都是互斥信号量,在引用计数等于1,它们只对写着互斥,不对读者。例如:

static DECLARE_RWSEM(mr_rwsem);

/* 试图获取信号量用于读... */
down_read(&mr_rwsem);

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

/* 释放信号量 */
up_read(&mr_rwsem);
/* ... */

/* 试图获取信号量用于写 ... */
down_write(&mr_rwsem);

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

/* 释放信号量 */
up_write(&mr_sem);
up和down例子

六、互斥体

多数时候信号量只使用计数1,信号量适合用于哪些较复杂的、未明情况下的互斥访问。

为了找到要给更简单睡眠锁,内核引入了互斥体(mutex),指的是可以睡眠的强制互斥锁。

DEFINE_MUTEX(name);
/* 静态定义mutex */

mutex_init(&mutex);
/* 动态初始化mutex */

/* 互斥锁锁定和解锁并不难 */
mutex_lock(&mutex);
/* 临界区 */
mutex_unlock(&mutex);
静态和动态mutex

下面是基本的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更要个的要求了:

  • 任何时刻中只有一个任务可以持有mutex,也就是说,mutex的使用计数永远是1
  • 给mutex上锁者必须负责给其再解锁----你不能在一个上下文锁定一个mutex,而在领域给上下文中给它解锁。这个限制使得mutex不适合内核同用户空间复杂的同步场景。最常使用的方式是:在同一上下文中上锁和解锁。
  • 递归地上锁和解锁是不允许的。也就是说,你不能递归的持有同一个锁,同样你也不能再去解锁一个已经被解开的mutex
  • 当持有一个mutex时,进程不可以退出
  • mutex不能在中断或者下半部中使用,即使使用mutex_trylock()也不行
  • mutex只能通过官方API管理:它只能使用上节中描述的方法初始化,不可被拷贝、手动初始化或者重复初始化。

打开内核配置选项CONFIG_DEBUG_MUTEXES后,就会有多种检测来确保这些约束得以遵守。

6.1 信号量和互斥体

在信号量和互斥体的选中中,首选mutex。除非mutex的某个约束妨碍你使用,否则相比信号量要有限使用mutex。

6.2 自旋锁和互斥体

在中断上下文中只能使用自旋锁,而在任务睡眠时只能使用互斥体

需求 建议的加锁方法
低开销加锁 优先自旋锁
短期锁定 优先自旋锁
长期加锁 优先互斥体
中断上下文中加锁 使用自旋锁
持有锁需要睡眠 使用互斥体

 

七、完成变量

如果内核中一个任务需要发出信号通知另一个任务发生了某个特定事件,利用完成变量。

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

DECLARE_COMPLETION(mr_comp);
/* 静态创建 */

init_completion(&mr_comp);
/* 动态创建 */
静态动态创建

在一个指定的完成变量上,需要等待的任务调用wait_for_completion()来等待特定事件。

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

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

complete(struct completion *)
/* 发信号唤醒任何等待任务 */
完成变量方法

使用完成变量的例子可以参考kernel/sched.c和kernel/fork.c。

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

初始化完成后,初始化函数调用completion()唤醒在等待的内核任务。

 

八、BLK:大内核锁

BKL(大内核锁)是一个全局自旋锁,使用它主要是为了方便实现从Linux最初的SMP过度到细粒度加锁机制。

  • 持有BKL的惹怒我仍然可以睡眠。因为当任务无法被调度时,所加锁会自动被丢弃;当任务被调度时,锁将会被重新获得。当然, 这并不是说,当任务持有BKL时,睡眠安全的,仅仅是可以这样做,因为睡眠不会死锁。
  • BKL是一种递归锁。要给进程可以多次请求一个锁,并不会像自旋锁那样死锁现象。
  • BKL只可以用在进程上下文中。和自旋锁不同,你不能在中断上下文中申请BLK
  • 新的用户不允许使用BLK,随着内核不断演进,越来越少的依赖BLK了。

BKL的使用方式和自旋锁类似,函数lock_kernel()请求锁,unlock_kernel()释放锁。

函数kernel_locker()检测锁当前是否被持有,有返回非0值,否则返回0。头文件在<linux/smp_lock.h>中。

lock_kernel();
/* 临界区,对所有其他的BLK用户进行同步······
 * 注意,你可以安全地在此睡眠,锁会悄无声息的被释放
 * 当你的任务被重新调度时,锁又会被悄无声息地获取
 * 这意味着你不会处于死锁状态,但是,如果你需要锁保护这里的数据
 * 你还是不需要睡眠
 */
unlock_kernel();
简单用法

BLK函数列表

lock_kernel()
/* 获得BKL */

unlock_kernel()
/* 释放BKL */

kernel_locked()
/* 如果锁被持有返回非0值,否则返回0 */
BKL函数列表

九、顺序锁

2.6版本引入的新型锁,简称seq锁。实现这种锁主要依靠一个序列计数器。当有疑义的数据被写入时,会得到一个锁,并且序列值会增加。

seqlock_t mr_seq_lock = DEFINE_SEQLOCK(mr_sq_lock);
/* 定义一个seq锁 */

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锁比较理想:

  • 数据存在很多读者
  • 数据写者很少
  • 写者很少,但希望优先于读,而且不允许读者让写者饥饿
  • 数据很简单,如简单数据结构

jiffies使用的就是seq锁。

u64 get_jiffies_64(void)
{
    unsigned long seq;
    u64 ret;
    do {
        seq = read_seqbegin(&xtime_lock);
        ret = jiffies_64;
    } while (read_seqretry(&xtime_lock, seq));
    return ret;
}
get_jiffies_64

如果想要进一步了解jiffies和内核时间管理,在kernel/timer.c与kernel/time/tick-common.c文件

 

十、禁止抢占

由于内核是抢占性的,内核中的进程在任何时刻都可能停下来以便另一个具有更高优先权的进程运行。

如果一个自旋锁被持有,内核便不能进行抢占。因为内核抢占和SMP面对相同的并发问题,并且内核已经是SMP安全的,所以,这种简单的变化使得内核也是抢占安全的。

有些时候我们不需要自旋锁,但是任然需要关闭内核抢占。为了解决这个问题,可以通过preemt_disable()禁止内核抢占。每次调用都必须有一个相应的preemt_enable()调用。

当最后一次preemt_enable()被调用后,内核抢占才重新启用。例如:

preempt_disable()
/* 抢占被禁止 ... */
preempt_enable();
简单用例

抢占计数存放着被持有锁的数量和preempt_disable()的调用次数,如果计数是0,那么内核可以进行抢占。如果为1或更大的值,那么,内核就不会进行抢占。

他是一种对原子操作和睡眠很有效的调试方法。

preempt_disable()
/* 增加抢占计数值,从而进制内核抢占 */

preempt_enable()
/* 减少抢占计数,并当该值降为0时检查和执行被挂起的需调度的任务 */

preempt_enable_no_resched()
/* 激活内核抢占但不再检查任何被挂起的需调度任务 */

preempt_count()
/* 返回抢占计数 */
内核抢占的相关函数

为了更简洁的方法解决每个处理器上的数据访问问题,可以通过get_cpu()获得处理器编号。这个函数在返回当前处理器号前首先会关闭内核抢占。

int cpu;
/* 禁止内核抢占,并将CPU设置为当前处理器 */
cpu = get_cpu();
/* 对每个处理器的数据进行操作 */
/* 再给与内核抢占性,"CPU"可改变故它不再有效 */
put_cpu();
get_cpu

十一、顺序和屏障

当处理多处理器之间或硬件设备之间的同步问题时,有时需要在你的程序代码中以指定的顺序发出读内存(读入)和写内存(存储)指令。

编译器和处理器为了提高效率,可能对读和写重新排序,这样无疑使问题复杂化了。

同样也可以指示编译器不要对给定点周围的指令序列进行重新排序,这些确保顺序的指令称作屏障。

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

wmb()方法提供了一个"写"内存屏障,和rmb()类似,区别是它针对存储而非载入

mb()方法及提供了读屏障也提供写屏障。

read_barrier_depends()是rmb()的变种,它提供了读屏障,但是仅仅针对后续读操作所依靠的哪些载入。

barrier()方法可以防止编译器跨屏障对载入或存储操作进行优化。

rmb()
/* 阻止跨越屏障的载入动作发生重排序 */

read_barrier_depends()
/* 组织跨越屏障的具有数据依赖关系的载入动作重排序 */

wmb()
/* 组织跨越屏障的存储动作发生重排序 */

mb()
/* 阻止跨越屏障的载入和存储动作重新排序 */

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

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

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

smp_mb()
/* 在SMP上提供mb()功能,在UP上提供barrier()功能 */

barrier()
/* 组织编译器跨屏障对载入或存储操作进行优化 */
内存和编译器屏障方法

 

原文地址:https://www.cnblogs.com/ch122633/p/11029008.html