Linux 内核学习(2) - 原子操作和内存屏障

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): 比较*ptrold的值,如果相等则*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) 加1
    • atomic_dec(v) 减1
    • atomic_inc_return(v) 加1并返回
    • atomic_dec_return(v)
    • atomic_add_return(i, v)
    • atomic_sub_return(i, v)
    • atomic_inc_add_test(v) 加1测试新值是否为0
    • atomic_dec_add_test(v) 减1测试新值是否为0
    • atomic_xchg(v, new) exchange
    • atomic_cmpxchg(v, old, new) if *v==old, then *v = new
    • atomic_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) 设置成1
    • clear_bit(nr, *addr) 设置成0
    • test_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的状态

代码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
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值