【精通内核】Linux内核自旋锁实现原理与源码解析

 

前言

📫作者简介:小明java问道之路,专注于研究计算机底层/Java/Liunx 内核,就职于大型金融公司后端高级工程师,擅长交易领域的高安全/可用/并发/性能的架构设计📫 

🏆 CSDN专家博主/Java优质创作者/CSDN内容合伙人、InfoQ签约作者、阿里云签约专家博主、华为云专家、51CTO专家/TOP红人 🏆

🔥如果此文还不错的话,还请👍关注、点赞、收藏三连支持👍一下博主~ 

本文目录

本文导读

一、Linux内核自旋锁原理

二、Linux内核源码实现

三、Linux内核自旋读写锁源码解析

1、Linux内核自旋锁读锁

2、Linux内核自旋锁写锁

总结


本文导读

对于一些数据结构,例如阻塞队列来说本身队列就是非线程安全,所以我们就需要实现同步的需求(信号量),这时就需要自旋锁这种机制,同时自旋锁也是与 CPU 核数相关的,下面我们了解其内核源码实现。

一、Linux内核自旋锁原理

前面我们看到了自旋锁的使用,因为对于一些结构,例如线程阻塞队列来说,本身队列就是非程安全的,而我们又需要通过该队列来实现其他的同步结构,如信号量,这时就需要自旋锁来提供种机制,同时读者应注意,在单核 CPU 中不需要使用自旋锁,而在 MP 对多处理器中才需使用它,也就是说单核 CPU 不需要自旋锁。下面直接查看源码实现。

自旋锁结构体 
typedef struct {
    // 自旋锁为无符号的volatile 整型变量 
    volatile unsigned int lock;
}spinlock_t;

// 宏定义创建一个值为1的自旋锁
#define SPIN_LOCK_UNLOCKED(spinlock_()(1}

// 宏定义自旋锁初始化
#define spin_lock_init(x) 
do {
    *(x) = SPIN_LOCK_UNLOCKED;
} while(0)

//  宏定义判断自旋锁是否被锁定,通过将 lock 变量转为 volatile signed char 看看是否小于或等于0
#define spin_is_locked(x) (*(volatile signed char *)(&(x)->lock) <= 0)

// 宏定义等待自旋锁释放,这里的 barrier()为前述内容曾讨论过的编译器屏障
#define spin_unlock_wait(x) 
do{
    barrier();
} while(spin_is_locked(x))

二、Linux内核源码实现

自旋锁就是一个无符号整型值的 lock 变量,通过 volatile 的修饰,可以让它避免被编译器优化。接下来,查看获取自旋锁的代码实现。

// 宏定义获取自旋锁内联汇编代码
#define spin_lock_string
    "1:"
    "lock ; decb %0"  // 对lock 变量自减	
    "js 2f"	          // 如果小于O,则跳转到前面标号2处	
    LOCK_SECTION_START("")
    "2:"
    "rep;nop"	    // 执行空操作	
    "cmpb $0, %0"	// 比较lock的值是否为0	
    "jle 2b"	    // 如果非 0,则继续跳到标号2处循环判断	
    "jmp 1b"	    // 如果等于0,则跳到标号1处重新开始尝试获取锁	
    LOCK_SECTION_END

// 直接上锁,如果上锁失败,则进入前面的 spin_lock_string中,自旋等待 
static inline void_raw_spin_lock(spinlock_t *lock) {
    __asm___volatile__(
        spin_lock_string
        :"=m" (lock->lock): : "memory");
}

// 下面为自旋锁释放锁的代码实现。
#define spin_unlock_string
    "movb $1,%0"	// 将立即数 1 设置为 lock的值	
        :"=m" (lock->lock)::"memory"

static inline void_raw_spin_unlock(spinlock_t *lock) {
    __asm___volatile__(
        spin_unlock_string);
}

三、Linux内核自旋读写锁源码解析

从上述内容中可以看到,自旋锁在 Linx 中的实现就是不停地对 lock 变量的减 1,然后在循环等待它变为1时,再次尝试获取锁;而释放锁较为简单,只需要将其设置为!即可。我们知道在读存在的情况下,使用同一把锁会导致粒度较大,这时通常将其实现为读写锁,同样,内核中也存在一个读写自旋锁。现在查看它的实现原理。

// 上述的自旋锁都是互斥自旋锁,这里定义了一个读写自旋锁,即支持多个读共享,写者互斥 
typedef struct{
    volatile unsigned int lock;
}rwlock_t;   // 和互斥自旋锁一样的结构

// 宏定义获得一个初始化的读写自旋锁
#define RW_LOCK UNLOCKED(rwlock_t){RWLOCKBIAS}
// 宏定义RW_LOCK_BIAS 为0x0100 0000。我们以int 类型的lock的第7位作为写锁标志位

// 宏定义初始化的读写自旋锁
#define rwlock_init(x)	
do { 
    *(x) = RW_LOCK_UNLOCKED; 
} while(0)	

// 宏定义判断读写自旋锁是否上锁了,也就是说看看lock是否为RW_LOCK_BIAS
#define rwlock_is_locked(x) ((x)->lock != RW LOCK_BIAS)

1、Linux内核自旋锁读锁

// 获取读锁
static inline void_raw_read_lock(rwlock_t*rw){
    __build_read_lock(rw, "___read_lock_failed");
}

#define_build_read_lock(rw,helper) 
do{
    build_read_lock_ptr(rw,helper);
} while (0)

#define_build_read_lock_ptr(rw, helper)
asm volatile(
    LOCK "subl $1,(%0)"	//对rw的指针的lock变量原子性减1	
    // 如果小于 0,则表示有别的线程获取写锁,因为读写锁的初始值为0x0100 0000,
    // 只有获取写锁后,才会将其减 0x0100 0000,结果为0,减1后自然变为负数。
    // 这时向前跳到标号2处,执行 helper,即执行_read_lock_failed
    "js 2f"
    "1:"
    LOCK SECTION_START("")
    "2: call " helper ""
    "jmp 1b"
    LOCK SECTION END
    ::"a"(rw):"memory")

// 上读锁失败后的处理逻辑_read_lock_failed 代码实现 
asm(
    "_read_lock_failed:"
    // 原子性增加 eax 中也就是 lock 变量的值
    // 因为上面的内联汇编已经将 lock 地址放入eax 中,即"a"(rw)操作
    LOCK "incl (%eax)"
    "1: rep; nop"
    "cmpl	$1,(%eax)" // 通过cmpl 和js指令看看lock的值是否大于或等于1,如果不是,则跳到标号1处循环比较
    "js 1b"
    LOCK "decl	(%eax)” // 最后原子性减 lock 的值,如果小于 0,则继续执行_read_lock_failed,否则视为获得自旋读锁
    "js _read_lock_failed""ret"
);

2、Linux内核自旋锁写锁

// 获取写锁
static inline void_raw_write_lock(rwlock_t*rw) {
    __build_write_lock(rw, "__write_lock_failed");
}

#define__build_write_lock(rw, helper)
do {
    __build_write_lock_ptr(rw, helper);
} while (0)

#define_build_write lock_ptr(rwhelper)
    //原子性对lock变量执行宏定义RW_LOCK_BIAS=0x01000000相减运算 
    asm volatile(LOCK"subl $"RWLOCKBIAS_STR",(%O)"
        //如果不为0,则跳到标号2处,执行__write_lock_failed 代码
        "jnz 2r"
        "1:"
        LOCK SECTION_START()
        "2: call " helper ""
        "jmp 1b"
        LOCK SECTION_END
        ::"a"(rw) : "memory")

// 获取写锁失败,回调_write_lock_failed 
asm(
    "write_lock_failed:"  // 原子性加locker 的值
    LOCK "addIS" $"RW_LOCK_BIAS_STR",(%eax)"
    "1: rep; nop"
    // 通过cmpl 和jne指令看看locker的值是否为RWLOCKBIAS,如果不相等,则一直重试
    "cmpl $" RW_LOCK_BIAS_STR",(%eax)"
    "jne 1b"
    // 如果相等,则尝试原子性减操作
    LOCK "subl$RWLOCK_BIAS_STR",(%eax)
    // 如果依然不为0,则继续重试
    "jnz__write_lock_failed"
    "ret"

// 宏定义释放读锁,直接原子性自增即可
#define _raw_read_unlock(rw) asm volatile("lock; incl %0":"=m" ((rw)->lock):: "memory")

// 宏定义释放写锁,直接原子性加RWLOCK_BIAS即可
#define_raw_write_unlock(rw) asm volatile("lock;addl $"R V_LOCK_BIAS_STR ",%O":"=m" ((rw)->lock)::"memory")

总结

不管是普通自旋锁还是读写自旋锁都是通过原子性命令来加减操作的;而对于读写锁而言,可通过将 lock 变量变为 0x01000000 值,然后后将第六位作为写锁标志位来使用。

发表回复

您的电子邮箱地址不会被公开。 必填项已用*标注