Linux 内核学习(2) - 原子操作和内存屏障
原子操作
-
原子操作是指,不能被本CPU和其它CPU所中断的执行指令或指令流。
-
原子操作的实现
- CPU基本的原子操作
- LOCK前缀
- 自动带LOCK的指令
-
CPU平台本身可以保证的原子操作
- 读/写一个字节
- 读/写一个对齐的16 bit数据
- 读/写一个对齐的32 bit数据
- 读/写一个对齐的64 bit数据(64位CPU)
- 另外
- 在32-bit的数据总线上访问一块cache禁止的16位数据( ≥ \ge ≥Pentium)
- 在一个cache line中访问不对齐的16,32,64位数据( ≥ \ge ≥ P6 family)
-
LOCK前缀
- 指令前带LOCK前缀迫使CPU做独占的内存访问
- XCHG自动带有LOCK前缀
- 代码示例
cmpxchg(ptr, old, new)
: 比较*ptr
和old
的值,如果相等则*ptr=new
。在任何时候该函数返回ptr
的初始值。因此,如果想知道ptr
的值是否被new
替换,只需要检查返回值是否与old
相等- 参见下面的代码1
-
LOCK在不同CPU版本的实现
- 在较老的CPU上,产生LOCK信号以用来锁总线
- 在 ≥ \ge ≥ P6 family,锁cache line
-
自动带LOCK前缀的指令
- xchg
- 事务内存 (TSX on intel)
- 可以自定义一块transaction region, 然后可以再区域结束的时候原子提交。如果成功提交,则其它CPU可以看到该区域的所有更改;如果提交失败则撤销该区域的所有更改操作,进而转到该程序提供的abort代码
-
Linux Kernel中的原子操作
atomic_t
的数据bit
操作- 其它指令
xchg()
交换cmpxchg()
比较并交换cpmxchg_double()
比较交换再加倍
代码1 tools/arch/x86/include/asm/cmpxchg.h
#define __cmpxchg(ptr, old, new, size)
__raw_cmpxchg((ptr), (old), (new), (size), LOCK_PREFIX)
#define cmpxchg(ptr, old, new)
__cmpxchg(ptr, old, new, sizeof(*(ptr)))
/*
* Atomic compare and exchange. Compare OLD with MEM, if identical,
* store NEW in MEM. Return the initial value in MEM. Success is
* indicated by comparing RETURN with OLD.
*/
#define __raw_cmpxchg(ptr, old, new, size, lock)
({
__typeof__(*(ptr)) __ret;
__typeof__(*(ptr)) __old = (old);
__typeof__(*(ptr)) __new = (new);
switch (size) {
case __X86_CASE_B:
{
volatile u8 *__ptr = (volatile u8 *)(ptr);
asm volatile(lock "cmpxchgb %2,%1" // 这里的汇编代码前面加了lock
: "=a" (__ret), "+m" (*__ptr)
: "q" (__new), "0" (__old)
: "memory");
break;
}
代码2 atomic_t的定义
typedef struct {
int counter;
} atomic_t;
1 atomic_t
- 定义:参见代码2
- 所有的操作都定义在
include/linux/atomic.h
中以及include/asm/atomic.h
中 - 常见atomic操作
ATOMIC_INIT(i)
atomic_set(v, i)
atomic_read(v)
atomic_add(i, v)
加v
atomic_sub(i, v)
减v
atomic_inc(v)
加1atomic_dec(v)
减1atomic_inc_return(v)
加1并返回atomic_dec_return(v)
atomic_add_return(i, v)
atomic_sub_return(i, v)
atomic_inc_add_test(v)
加1测试新值是否为0atomic_dec_add_test(v)
减1测试新值是否为0atomic_xchg(v, new)
exchangeatomic_cmpxchg(v, old, new)
if *v==old, then *v = newatomic_add_negtive(i, v)
测试新值是否为负数atomic_add_unless(v, a, u)
if v!=u then v+=a; return 非0; else return 0
代码3 atomic使用示例
#include <linux/module.h>
#include <linux/kthread.h>
#include <linux/delay.h>
#include <linux/slab.h>
#include <linux/srcu.h>
#include <linux/rwlock.h>
#include <linux/atomic.h>
MODULE_AUTHOR("OneGhost");
MODULE_LICENSE("GPL");
MODULE_DESCRIPTION("The module is only used for test");
#define MAX_KTHREAD 10
static atomic_t my_count;
static void show_my_data(void) {
printk("count %d\n", atomic_read(&my_count));
}
static struct task_struct *threads[MAX_KTHREAD];
static int thread_do(void *data) {
long i = (long)data;
printk("thread %ld is running...\n", i);
atomic_inc(&my_count); // +1
atomic_add(10, &my_count); // +10
while (!kthread_should_stop()) {
msleep(10);
}
return 0;
}
static int create_threads(void) {
int i;
for (i = 0;i < MAX_KTHREAD; i++) {
struct task_struct *thread;
thread = kthread_run(thread_do, (void *)(long)i, "thread-%d", i);
if (IS_ERR(thread)) return -1;
threads[i] = thread;
}
return 0;
}
static void cleanup_threads(void) {
int i;
for (i=0;i < MAX_KTHREAD; i++) {
if (threads[i])
kthread_stop(threads[i]);
}
}
static __init int minit(void) // 如果加上__init,则只会调用一次
{
printk("call %s.\n", __FUNCTION__);
atomic_set(&my_count, 0);
if (create_threads()) {
cleanup_threads();
return -1;
}
return 0;
}
static __exit void mexit(void) // __exit同理
{
printk("call %s.\n", __FUNCTION__);
cleanup_threads();
show_my_data();
}
module_init(minit)
module_exit(mexit)
2 bitops
-
原子性的位操作
-
尽量不要用,开销比较大
-
常用的原子位操作
set_bit(nr, *addr)
设置成1clear_bit(nr, *addr)
设置成0test_bit(nr, *addr)
是否被设置(为1)change_bit(nr, *addr)
取反test_and_set_bit(nr, addr)
测试旧值是否已经设置(为1)test_and_clear_bit(nr, *addr)
test_and_change_bit(nr, *addr)
-
非原子的位操作
__set_bit(nr, *addr)
__clear_bit(nr, *addr)
test_bit(nr, *addr)
__change_bit(nr, *addr)
__test_and_set_bit(nr, addr)
测试旧值是否已经设置
例子5
#include <linux/module.h>
#include <linux/kthread.h>
#include <linux/delay.h>
#include <linux/bitops.h>
MODULE_AUTHOR("OneGhost");
MODULE_LICENSE("GPL");
MODULE_DESCRIPTION("The module is only used for test");
#define MAX_KTHREAD 10
static ulong my_bits;
static void show_my_data(void) {
int i, bits = sizeof(my_bits) * 8;
for (i = 0; i < bits; ++i) {
int set = test_bit(i, &my_bits);
if (set) {
printk("bit %d is set\n", i);
}
}
}
static struct task_struct *threads[MAX_KTHREAD];
static int thread_do(void *data) {
long i = (long)data;
printk("thread %ld is running...\n", i);
set_bit(i, &my_bits); // 将第i位设置成1
while (!kthread_should_stop()) {
msleep(10);
}
return 0;
}
static int create_threads(void) {
int i;
for (i = 0;i < MAX_KTHREAD; i++) {
struct task_struct *thread;
thread = kthread_run(thread_do, (void *)(long)i, "thread-%d", i);
if (IS_ERR(thread)) return -1;
threads[i] = thread;
}
return 0;
}
static void cleanup_threads(void) {
int i;
for (i=0;i < MAX_KTHREAD; i++) {
if (threads[i])
kthread_stop(threads[i]);
}
}
static __init int minit(void) // 如果加上__init,则只会调用一次
{
printk("call %s.\n", __FUNCTION__);
if (create_threads()) {
cleanup_threads();
return -1;
}
return 0;
}
static __exit void mexit(void) // __exit同理
{
printk("call %s.\n", __FUNCTION__);
cleanup_threads();
show_my_data();
}
module_init(minit)
module_exit(mexit)
运行结果
[ 9580.920234] call minit.
[ 9580.920693] thread 0 is running...
[ 9580.921048] thread 1 is running...
[ 9580.921641] thread 2 is running...
[ 9580.922600] thread 3 is running...
[ 9580.922867] thread 4 is running...
[ 9580.923319] thread 5 is running...
[ 9580.923806] thread 6 is running...
[ 9580.924053] thread 7 is running...
[ 9580.924489] thread 8 is running...
[ 9580.924964] thread 9 is running...
[ 9587.008868] call mexit.
[ 9587.146254] bit 0 is set
[ 9587.146262] bit 1 is set
[ 9587.146268] bit 2 is set
[ 9587.146275] bit 3 is set
[ 9587.146282] bit 4 is set
[ 9587.146288] bit 5 is set
[ 9587.146295] bit 6 is set
[ 9587.146301] bit 7 is set
[ 9587.146308] bit 8 is set
[ 9587.146314] bit 9 is set
内存屏障
-
Memory Barrier,简而言之就是强制规定内存访问次序
-
产生原因
- 编译器的优化
- CPU的乱序和推测执行
-
消除编译器导致的乱序
asm volatile("":::"memory")
- C语言中的
volatile
关键字
-
从x86的内存序谈起
- 相同类型的操作(read vs read; write vs write) 不能乱序
- write不能乱序到read前面
- read可以乱序到write的前面
- CPU内部访问内存可以允许bypass
- write全局可见
-
在x86上阻止读写之间的乱序:
- mfence:This serializing operation guarantees that every load and store instruction that precedes the MFENCE instruction in program order becomes globally visible before any load or store instruction that follows the MFENCE instruction.
-
与其他平台相比,特别是alpha,x86是一个相对来说比较strong order的平台,但linux kernel服务于各种平台,所以有各种各样的memory barrier
-
在写kernel代码的时候,需要假想任意的乱序都有可能存在
-
主要的内存屏障:
barrier()
用来防止编译器导致的乱序smp_rmb()
所有位于此函数之前的读操作不能越过此函数(必须先完成之前的读操作才能继续执行)smp_wmb()
所有位于此函数之前的写操作不能越过此函数smp_mb()
所有位于此函数之前的任意读写操作不能越过此函数smp_read_barrier_depends()
读依赖屏障- read a; read buffer[a]; 在有些平台上会出现a先执行后面语句的情况
- forces subsequent operations that depend on prior operations to be ordered. This primitive is a no-op on all platforms except Alpha.
-
使用规则
- 读写屏障只有成对使用才有意义 (
smp_rmb() +smp_wmb()
或smp_rmb()+smp_mb()
) - 读依赖屏障后面要跟
smp_wmb()
或者smp_mb()
smp_mb()
后面要跟smp_mb()
- 读写屏障只有成对使用才有意义 (
-
隐含的屏障
- 所有的
smp_xx()
屏障(除了读依赖屏障)都隐含编译屏障 - 一般来说,对于有更新操作且返回旧值或新值的原子操作都包含全屏障
- 例如:
xchg()
cmpxchg()
test_and_set_bit()
- 例如:
- 所有的
-
内存屏障对比锁
lock()
和unlock()
不隐含全屏障,它允许上下两端的代码进入临界区但不能穿透临界区。但是不能保证临界区外的操作- 不能穿透是指:临界区后面的代码不能穿到lock()前面;临界区前的代码不能穿到unlock()后面
-
什么时候需要内存屏障
- 在多核(smp)之间有竞争的时候(在设计lockless算法的时候,用内存屏障代替锁提高并发性能),才需要内存屏障
- 在CPU和device之前的做I/O的时候
代码6 barrier()的定义 arch/x86/include/asm/barrier.h
#define barrier() __asm__ __volatile__("": : :"memory")
代码7 smp相关函数定义 arch/x86/include/asm/barrier.h
#ifdef CONFIG_X86_32
#define mb() asm volatile(ALTERNATIVE("lock; addl $0,-4(%%esp)", "mfence", \
X86_FEATURE_XMM2) ::: "memory", "cc")
#define rmb() asm volatile(ALTERNATIVE("lock; addl $0,-4(%%esp)", "lfence", \
X86_FEATURE_XMM2) ::: "memory", "cc")
#define wmb() asm volatile(ALTERNATIVE("lock; addl $0,-4(%%esp)", "sfence", \
X86_FEATURE_XMM2) ::: "memory", "cc")
#else
#define mb() asm volatile("mfence":::"memory")
#define rmb() asm volatile("lfence":::"memory")
#define wmb() asm volatile("sfence" ::: "memory")
#endif
- 使用内存屏障的例子
- KVM的内存虚拟化中,有一个lockless的算法:
walk_shadow_page_lockless_begin()
与walk_shadow_page_lockless_end()
之前可以访问页表- 在释放页表时,如果vcpu处于GUEST状态,需要发送中断(IPI)给这个VCPU
- 所以基本思想是,在做lockless访问的时候,将这个CPU的中断禁止,然后将其状态改为处于GUEST的状态
- KVM的内存虚拟化中,有一个lockless的算法:
代码8 kvm使用内存屏障的例子 arch/x86/kvm/mmu/mmu.c
static void walk_shadow_page_lockless_begin(struct kvm_vcpu *vcpu)
{
/*
* Prevent page table teardown by making any free-er wait during
* kvm_flush_remote_tlbs() IPI to all active vcpus.
*/
local_irq_disable();
/*
* Make sure a following spte read is not reordered ahead of the write
* to vcpu->mode.
*/
smp_store_mb(vcpu->mode, READING_SHADOW_PAGE_TABLES);
}
static void walk_shadow_page_lockless_end(struct kvm_vcpu *vcpu)
{
/*
* Make sure the write to vcpu->mode is not reordered in front of
* reads to sptes. If it does, kvm_mmu_commit_zap_page() can see us
* OUTSIDE_GUEST_MODE and proceed to free the shadow page table.
*/
smp_store_release(&vcpu->mode, OUTSIDE_GUEST_MODE);
local_irq_enable();
}
使用内存屏障的原因:
CPU0 CPU1
Read page table A
IRQ disable
free page table A and see VCPU isn't in guest mode
Set vcpu 0 in guest mode
!!! accessing A is invalid