Linux同步互斥5

Linux同步互斥5(基于Linux6.6)---R/W lockspin lock


一、为何会有rw spin lock?

1.1、为什么需要 rwspinlock

  1. 读多写少的场景
    在许多应用程序中,尤其是在内核中,读操作往往比写操作频繁。比如,很多数据结构的查询操作要比修改操作多得多。传统的自旋锁(spinlock)在这种情况下可能并不是最优选择,因为当一个线程持有锁时,其他线程即使只是读取数据,也需要等待锁释放,这会造成不必要的性能瓶颈。

    rwspinlock 通过引入读锁和写锁的区分,能够在多个线程同时进行读操作时,允许它们并发执行,从而提高了并发度和性能。

  2. 提高并发性
    rwspinlock 允许多个读者同时持有锁,这使得在没有写者时,读者可以并行执行,充分利用多核处理器的优势。然而,只有一个写者可以持有写锁,而且在有写者持有写锁的情况下,所有的读者和写者都需要等待。

  3. 降低锁争用
    传统的自旋锁在存在多个读者时,每个读者都会争抢同一个锁,这样会增加锁的争用和上下文切换的成本。而在 rwspinlock 中,多个读者可以共享锁,只会在有写操作时才发生争用,这样减少了无谓的锁竞争。

1.2、rwspinlock 的基本工作原理

rwspinlock 提供两种不同的锁机制:

  1. 读锁(read lock)
    允许多个线程同时获得读锁,只要没有线程持有写锁。读锁是共享的,多个读者可以并发访问共享资源。

  2. 写锁(write lock)
    只允许一个线程获得写锁,并且在写锁持有期间,不允许任何读锁或写锁。写锁是独占的,意味着一旦一个线程获得写锁,其他所有线程(无论是读锁还是写锁)都必须等待该写锁释放。

1.3、使用 rwspinlock 的优点

  1. 并发读性能
    在多个线程频繁读取数据的场景中,rwspinlock 允许多个读线程同时持有锁,而传统的自旋锁则会导致所有读线程等待,从而减少了性能。

  2. 写时互斥
    在数据结构需要修改的场景,rwspinlock 能够保证写操作的互斥性,防止读和写操作的冲突。只有一个写线程可以持有写锁,其他线程(无论是读还是写)必须等待。

  3. 减少锁竞争
    对于读多写少的场景,rwspinlock 能够显著减少锁竞争,因为多个读线程可以共享同一把锁,而只有在写线程竞争时才需要排队。

1.4、读写自旋锁的实现原理

与普通自旋锁相比,rwspinlock 实现了读锁和写锁的分离。它通过内部的状态标志来区分读锁和写锁的持有者。

1. 读锁的获取
  • 如果当前没有线程持有写锁,rwspinlock 允许多个线程同时获取读锁。
  • 在获取读锁时,线程会通过自旋来检查是否有写锁被持有。如果没有写锁,线程就可以成功获取读锁并进入临界区。
2. 写锁的获取
  • 写锁是独占的。只有在没有任何线程持有读锁或写锁时,线程才能获取写锁。
  • 获取写锁时,线程会自旋,直到没有其他读线程或写线程在访问资源。
3. 锁的释放
  • 释放读锁时,不会影响其他读线程的获取。
  • 释放写锁时,所有等待的线程(无论是读锁还是写锁)都能根据优先级获取锁。

1.5、rwspinlock 与普通自旋锁的比较

特性普通自旋锁 (spinlock)读写自旋锁 (rwspinlock)
锁的类型独占锁(只允许一个线程持有)读写分离(读锁共享,写锁独占)
并发读取不允许并发读取允许多个读线程并发获取锁
并发写入不允许并发写入只允许一个写线程获得锁
性能读和写都受到限制读多写少时性能优越
使用场景一般适用于读写较为均衡的情况适用于读操作远多于写操作的场景

 

二、工作原理

2.1、应用举例

rw spinlock在文件系统中的例子:

fs/filesystems.c

static struct file_system_type *file_systems;
static DEFINE_RWLOCK(file_systems_lock);

 Linux内核支持多种文件系统类型,例如EXT4,YAFFS2等,每种文件系统都用struct file_system_type来表示。内核中所有支持的文件系统用一个链表来管理,file_systems指向这个链表的第一个node。访问这个链表的时候,需要用file_systems_lock来保护,场景包括:

(1)register_filesystem和unregister_filesystem分别用来向系统注册和注销一个文件系统。

(2)fs_index或者fs_name等函数会遍历该链表,找到对应的struct file_system_type的名字或者index。

这些操作可以分成两类,第一类就是需要对链表进行更新的动作,例如向链表中增加一个file system type(注册)或者减少一个(注销)。另外一类就是仅仅对链表进行遍历的操作,并不修改链表的内容。在不修改链表的内容的前提下,多个thread进入这个临界区是OK的,都能返回正确的结果。但是对于第一类操作则不然,这样的更新链表的操作是排他的,只能是同时有一个thread在临界区中。

2.2、基本的策略

使用普通的spin lock可以完成上一节中描述的临界区的保护,但是,由于spin lock的特定就是只允许一个thread进入,因此这时候就禁止了多个读thread进入临界区,而实际上多个read thread可以同时进入的,但现在也只能是不停的spin,cpu强大的运算能力无法发挥出来,如果使用不断retry检查spin lock的状态的话(而不是使用类似ARM上的WFE这样的指令),对系统的功耗也是影响很大的。因此,必须有新的策略来应对:

lock的逻辑如下:

(1)假设临界区内没有任何的thread,这时候任何read thread或者write thread可以进入,但是只能是其一。

(2)假设临界区内有一个read thread,这时候新来的read thread可以任意进入,但是write thread不可以进入

(3)假设临界区内有一个write thread,这时候任何的read thread或者write thread都不可以进入

(4)假设临界区内有一个或者多个read thread,write thread当然不可以进入临界区,但是该write thread也无法阻止后续read thread的进入,他要一直等到临界区一个read thread也没有的时候,才可以进入,多么可怜的write thread。

unlock的逻辑如下:

(1)在write thread离开临界区的时候,由于write thread是排他的,因此临界区有且只有一个write thread,这时候,如果write thread执行unlock操作,释放掉锁,那些处于spin的各个thread(read或者write)可以竞争上岗。

(2)在read thread离开临界区的时候,需要根据情况来决定是否让其他处于spin的write thread们参与竞争。如果临界区仍然有read thread,那么write thread还是需要spin直到所有的read thread释放锁,这时候write thread们可以参与到临界区的竞争中,如果获取到锁,那么该write thread可以进入。

三、实现

3.1、数据结构。rwlock_t数据结构定义如下:

 include/linux/rwlock_types.h

typedef struct {
	arch_rwlock_t raw_lock;
#ifdef CONFIG_DEBUG_SPINLOCK
	unsigned int magic, owner_cpu;
	void *owner;
#endif
#ifdef CONFIG_DEBUG_LOCK_ALLOC
	struct lockdep_map dep_map;
#endif
} rwlock_t;

rwlock_t依赖arch对rw spinlock相关的定义。

3.2、API

以下是常用的 rwspinlock API 接口及其功能。

Linux rwspinlock API

函数描述
rwlock_t my_rwlock;定义一个读写自旋锁变量。通常 rwlock_t 类型用于声明锁。
rwlock_init(&rwlock);初始化 rwspinlock。用于在使用前初始化锁。
read_lock(&rwlock);获取读锁。多个线程可以同时获取读锁,适用于读取共享资源的场景。
read_unlock(&rwlock);释放读锁。释放锁后,其他线程可以继续获取读锁。
write_lock(&rwlock);获取写锁。只有一个线程可以获取写锁,适用于修改共享资源的场景。
write_unlock(&rwlock);释放写锁。释放锁后,其他线程可以获取读锁或写锁。
read_trylock(&rwlock);尝试获取读锁。如果当前没有写锁,则返回成功;否则返回失败(不会阻塞)。
write_trylock(&rwlock);尝试获取写锁。如果当前没有读锁和写锁,则返回成功;否则返回失败(不会阻塞)。

详细描述

1. rwlock_t my_rwlock;

定义一个 rwspinlock 类型的变量,rwlock_t 是 Linux 内核中定义的读写自旋锁类型。

rwlock_t my_rwlock;
2. rwlock_init(&rwlock);

初始化读写自旋锁。在使用锁之前,需要初始化它。

rwlock_init(&my_rwlock);
3. read_lock(&rwlock);

获取读锁。多个线程可以同时获取读锁,只要没有线程持有写锁。这适用于多线程并发读取共享数据的场景。

read_lock(&my_rwlock);
4. read_unlock(&rwlock);

释放读锁。释放锁后,其他线程可以继续获取读锁或写锁。

read_unlock(&my_rwlock);
5. write_lock(&rwlock);

获取写锁。只有一个线程可以获取写锁,且在写锁持有期间,其他线程(无论是读锁还是写锁)都需要等待。这适用于修改共享资源的场景。

write_lock(&my_rwlock);
6. write_unlock(&rwlock);

释放写锁。释放锁后,其他线程可以获取读锁或写锁。

write_unlock(&my_rwlock);
7. read_trylock(&rwlock);

尝试获取读锁。如果当前没有写锁持有,调用该函数会成功获取读锁。如果有写锁持有,则返回失败。该函数不会阻塞线程。

if (read_trylock(&my_rwlock) == 0) {
    // 成功获取读锁
    // 执行读操作
    read_unlock(&my_rwlock);  // 释放读锁
} else {
    // 获取读锁失败
}
8. write_trylock(&rwlock);

尝试获取写锁。如果当前没有其他读锁或写锁持有,调用该函数会成功获取写锁。如果有其他线程持有锁,则返回失败。该函数不会阻塞线程。

if (write_trylock(&my_rwlock) == 0) {
    // 成功获取写锁
    // 执行写操作
    write_unlock(&my_rwlock);  // 释放写锁
} else {
    // 获取写锁失败
}

3.3、ARM上的实现

spinlock_type.h定义ARM相关的rw spin lock定义以及初始化相关的宏;spinlock.h中包括了各种具体的实现。我们先看arch_rwlock_t的定义:

arch/arm/include/asm/spinlock_types.h

typedef struct {
	u32 lock;
} arch_rwlock_t;

就是一个32-bit的整数。从定义就可以看出rw spinlock不是ticket-based spin lock。我们再看看arch_write_lock的实现:

arch/arm/include/asm/spinlock.h

static inline void arch_write_lock(arch_rwlock_t *rw)
{
    unsigned long tmp;

    prefetchw(&rw->lock); -------知道后面需要访问这个内存,先通知hw进行preloading cache
    __asm__ __volatile__(
"1:    ldrex    %0, [%1]\n" -----获取lock的值并保存在tmp中
"    teq    %0, #0\n" --------判断是否等于0
    WFE("ne") ----------如果tmp不等于0,那么说明有read 或者write的thread持有锁,那么还是静静的等待吧。其他thread会在unlock的时候Send Event来唤醒该CPU的
"    strexeq    %0, %2, [%1]\n" ----如果tmp等于0,将0x80000000这个值赋给lock
"    teq    %0, #0\n" --------是否str成功,如果有其他thread在上面的过程插入进来就会失败
"    bne    1b" ---------如果不成功,那么需要重新来过,否则持有锁,进入临界区
    : "=&r" (tmp) ----%0
    : "r" (&rw->lock), "r" (0x80000000)-------%1和%2
    : "cc");

    smp_mb(); -------memory barrier的操作
}

对于write lock,只要临界区有一个thread进行读或者写的操作(具体判断是针对32bit的lock进行,覆盖了writer和reader thread),该thread都会进入spin状态。如果临界区没有任何的读写thread,那么writer进入临界区,并设定lock=0x80000000。再来看看write unlock的操作:

arch/arm/include/asm/spinlock.h

static inline void arch_write_unlock(arch_rwlock_t *rw)
{
    smp_mb(); -------memory barrier的操作

    __asm__ __volatile__(
    "str    %1, [%0]\n"-----------恢复0值
    :
    : "r" (&rw->lock), "r" (0) --------%0和%1
    : "cc");

    dsb_sev();-------memory barrier的操作加上send event,wakeup其他 thread(那些cpu处于WFE状态)
}

write unlock看起来很简单,就是一个lock=0x0的操作。了解了write相关的操作后,再来看看read的操作:

arch/arm/include/asm/spinlock.h

static inline void arch_read_lock(arch_rwlock_t *rw)
{
    unsigned long tmp, tmp2;

    prefetchw(&rw->lock);
    __asm__ __volatile__(
"1:    ldrex    %0, [%2]\n"--------获取lock的值并保存在tmp中
"    adds    %0, %0, #1\n"--------tmp = tmp + 1
"    strexpl    %1, %0, [%2]\n"----如果tmp结果非负值,那么就执行该指令,将tmp值存入lock
    WFE("mi")---------如果tmp是负值,说明有write thread,那么就进入wait for event状态
"    rsbpls    %0, %1, #0\n"-----判断strexpl指令是否成功执行
"    bmi    1b"----------如果不成功,那么需要重新来过,否则持有锁,进入临界区
    : "=&r" (tmp), "=&r" (tmp2)----------%0和%1
    : "r" (&rw->lock)---------------%2
    : "cc");

    smp_mb();
}

上面的代码比较简单,需要说明的是adds指令更新了状态寄存器(指令中s那个字符就是这个意思),strexpl会根据adds指令的执行结果来判断是否执行。pl的意思就是positive or zero,也就是说,如果结果是正数或者0(没有thread在临界区或者临界区内有若干read thread),该指令都会执行,如果是负数(有write thread在临界区),那么就不执行。最后看read unlock的函数:

arch/arm/include/asm/spinlock.h

static inline void arch_read_unlock(arch_rwlock_t *rw)
{
    unsigned long tmp, tmp2;

    smp_mb();

    prefetchw(&rw->lock);
    __asm__ __volatile__(
"1:    ldrex    %0, [%2]\n"--------获取lock的值并保存在tmp中
"    sub    %0, %0, #1\n"--------tmp = tmp - 1
"    strex    %1, %0, [%2]\n"------将tmp值存入lock中
"    teq    %1, #0\n"------是否str成功,如果有其他thread在上面的过程插入进来就会失败
"    bne    1b"-------如果不成功,那么需要重新来过,否则离开临界区
    : "=&r" (tmp), "=&r" (tmp2)------------%0和%1
    : "r" (&rw->lock)-----------------%2
    : "cc");

    if (tmp == 0)
        dsb_sev();-----如果read thread已经等于0,说明是最后一个离开临界区的reader,那么调用sev去唤醒WFE的cpu core
}

最后,总结一下:

rwspinlock

32个bit的lock,0~30的bit用来记录进入临界区的read thread的数目,第31个bit用来记录write thread的数目,由于只允许一个write thread进入临界区,因此1个bit就OK了

四、ARM举例说明

在 ARM 架构上,Linux 内核实现的读写自旋锁(rwspinlock)机制与 x86 或其他架构相似,主要用于管理对共享资源的并发访问。读写自旋锁允许多个读者并发访问共享资源,同时确保写者的独占性。以下是一个在 ARM 架构上实现 rwspinlock 的简化示例,展示了如何在 ARM 体系结构下使用 rwspinlock_t 进行读写锁操作。

1. 定义读写自旋锁

首先,需要定义一个 rwspinlock_t 类型的锁变量。在 ARM 架构中,它与其他架构相同,使用内核提供的读写自旋锁 API。

#include <linux/rwlock.h>
#include <linux/kernel.h>
#include <linux/init.h>
#include <linux/module.h>
#include <linux/spinlock.h>
#include <linux/sched.h>

rwlock_t my_rwlock;  // 定义一个读写自旋锁

2. 锁的初始化

需要在合适的位置初始化这个锁,通常在模块加载时进行初始化。

static int __init rwlock_example_init(void)
{
    // 初始化读写自旋锁
    rwlock_init(&my_rwlock);
    printk(KERN_INFO "Read-Write Lock initialized.\n");
    return 0;
}

3. 读锁操作

读锁允许多个线程并发访问资源,只要没有线程持有写锁。以下是如何获取和释放读锁的示例:

void read_data(void)
{
    // 获取读锁
    read_lock(&my_rwlock);
    
    // 这里可以执行读取共享资源的操作
    printk(KERN_INFO "Reading data...\n");

    // 释放读锁
    read_unlock(&my_rwlock);
}

4. 写锁操作

写锁是独占的,任何一个线程获取写锁后,其他线程(无论是读锁还是写锁)都需要等待该写锁释放。以下是如何获取和释放写锁的示例:

void write_data(void)
{
    // 获取写锁
    write_lock(&my_rwlock);
    
    // 执行写操作
    printk(KERN_INFO "Writing data...\n");

    // 释放写锁
    write_unlock(&my_rwlock);
}

5. 模块的加载与卸载

在模块加载时,你可以调用 rwlock_example_init() 初始化锁,并在模块卸载时释放资源。

static void __exit rwlock_example_exit(void)
{
    printk(KERN_INFO "Module exiting...\n");
}

module_init(rwlock_example_init);
module_exit(rwlock_example_exit);

MODULE_LICENSE("GPL");
MODULE_AUTHOR("Your Name");
MODULE_DESCRIPTION("A simple example of using rwspinlock in ARM architecture.");

6. ARM 相关特性

ARM 体系结构下,rwspinlock_t 的实现并没有特别复杂的区别,主要依赖于底层的自旋锁机制。在 ARM 平台上,rwspinlock_t 实现了与其他架构相同的基本原理:通过自旋等待锁的释放。

ARM 的自旋锁依赖于原子操作,确保线程在获取锁时不会干扰其他线程。Linux 内核在 ARM 上通过内存屏障和原子操作来确保读写锁的正确性和性能。

ARM 架构的优化可能会体现在自旋锁的实现中,例如在 __spin_lock__rwlock 内部会使用 ARM 的原子指令来确保正确性。这些原子操作的具体实现细节通常是透明的,开发者通常不需要关心。

7. 完整示例代码

#include <linux/rwlock.h>
#include <linux/kernel.h>
#include <linux/init.h>
#include <linux/module.h>
#include <linux/spinlock.h>
#include <linux/sched.h>

rwlock_t my_rwlock;  // 定义一个读写自旋锁

static int __init rwlock_example_init(void)
{
    // 初始化读写自旋锁
    rwlock_init(&my_rwlock);
    printk(KERN_INFO "Read-Write Lock initialized.\n");

    // 模拟读取和写入
    read_data();
    write_data();
    
    return 0;
}

void read_data(void)
{
    // 获取读锁
    read_lock(&my_rwlock);
    
    // 执行读取操作
    printk(KERN_INFO "Reading data...\n");

    // 释放读锁
    read_unlock(&my_rwlock);
}

void write_data(void)
{
    // 获取写锁
    write_lock(&my_rwlock);
    
    // 执行写操作
    printk(KERN_INFO "Writing data...\n");

    // 释放写锁
    write_unlock(&my_rwlock);
}

static void __exit rwlock_example_exit(void)
{
    printk(KERN_INFO "Module exiting...\n");
}

module_init(rwlock_example_init);
module_exit(rwlock_example_exit);

MODULE_LICENSE("GPL");
MODULE_AUTHOR("Your Name");
MODULE_DESCRIPTION("A simple example of using rwspinlock in ARM architecture.");

8. 测试和验证

  1. 将上述代码编译并插入到 Linux 内核模块中。
  2. 查看 /var/log/kern.log 或使用 dmesg 命令查看内核日志,确认读锁和写锁的操作是否正确执行。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值