一、为何会有rw spin lock?

在有了强大的spin lock之后,为何还会有rw spin lock呢?无他,仅仅是为了增加内核的并发,从而增加性能而已。spin lock严格的限制只有一个thread可以进入临界区,但是实际中,有些对共享资源的访问可以严格区分读和写的,这时候,其实多个读的thread进入临界区是OK的,使用spin lock则限制一个读thread进入,从而导致性能的下降。

本文主要描述RW spin lock的工作原理及其实现。需要说明的是Linux内核同步机制之(四):spin lock是本文的基础,请先阅读该文档以便保证阅读的畅顺。

二、工作原理

1、应用举例

我们来看一个rw spinlock在文件系统中的例子:

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、基本的策略

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

我们首先看看加锁的逻辑:

(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可以进入临界区,听起来也是不公平的)直到所有的read thread释放锁(离开临界区),这时候write thread们可以参与到临界区的竞争中,如果获取到锁,那么该write thread可以进入。

三、实现

1、通用代码文件的整理

rw spin lock的头文件的结构和spin lock是一样的。include/linux/rwlock_types.h文件中定义了通用rw spin lock的基本的数据结构(例如rwlock_t)和如何初始化的接口(DEFINE_RWLOCK)。include/linux/rwlock.h。这个头文件定义了通用rw spin lock的接口函数声明,例如read_lock、write_lock、read_unlock、write_unlock等。include/linux/rwlock_api_smp.h文件定义了SMP上的rw spin lock模块的接口声明。

需要特别说明的是:用户不需要include上面的头文件,基本上普通spinlock和rw spinlock使用统一的头文件接口,用户只需要include一个include/linux/spinlock.h文件就OK了。

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

typedef struct {
    arch_rwlock_t raw_lock;
} rwlock_t;

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

3、API

我们整理RW spinlock的接口API如下表:

接口API描述 rw spinlock API
定义rw spin lock并初始化 DEFINE_RWLOCK
动态初始化rw spin lock rwlock_init
获取指定的rw spin lock read_lock
write_lock
获取指定的rw spin lock同时disable本CPU中断 read_lock_irq
write_lock_irq
保存本CPU当前的irq状态,disable本CPU中断并获取指定的rw spin lock read_lock_irqsave
write_lock_irqsave
获取指定的rw spin lock同时disable本CPU的bottom half read_lock_bh
write_lock_bh
释放指定的spin lock read_unlock
write_unlock
释放指定的rw spin lock同时enable本CPU中断 read_unlock_irq
write_unlock_irq
释放指定的rw spin lock同时恢复本CPU的中断状态 read_unlock_irqrestore
write_unlock_irqrestore
获取指定的rw spin lock同时enable本CPU的bottom half read_unlock_bh
write_unlock_bh
尝试去获取rw spin lock,如果失败,不会spin,而是返回非零值 read_trylock
write_trylock

在具体的实现面,如何将archtecture independent的代码转到具体平台的代码的思路是和spin lock一样的,这里不再赘述。

2、ARM上的实现

对于arm平台,rw spin lock的代码位于arch/arm/include/asm/spinlock.h和spinlock_type.h(其实普通spin lock的代码也是在这两个文件中),和通用代码类似,spinlock_type.h定义ARM相关的rw spin lock定义以及初始化相关的宏;spinlock.h中包括了各种具体的实现。我们先看arch_rwlock_t的定义:

typedef struct {
    u32 lock;
} arch_rwlock_t;

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

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的操作:

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的操作:

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在临界区),那么就不执行。OK,最后我们来看read unlock的函数:

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
}

最后,总结一下:

32个bit的lock,0~30的bit用来记录进入临界区的read thread的数目,第31个bit用来记录write thread的数目,由于只允许一个write thread进入临界区,因此1个bit就OK了。在这样的设计下,read thread的数目最大就是2的30次幂减去1的数值,超过这个数值就溢出了,当然这个数值在目前的系统中已经足够的大了,姑且认为它是安全的吧。

四、后记

read/write spinlock对于read thread和write thread采用相同的优先级,read
thread必须等待write thread完成离开临界区才可以进入,而write thread需要等到所有的read
thread完成操作离开临界区才能进入。正如我们前面所说,这看起来对write thread有些不公平,但这就是read/write
spinlock的特点。此外,在内核中,已经不鼓励对read/write
spinlock的使用了,RCU是更好的选择。如何解决read/write spinlock优先级问题?RCU又是什么呢?我们下回分解。

Linux内核同步 - Read/Write spin lock的更多相关文章

  1. Linux内核同步机制之(五):Read Write spin lock【转】

    一.为何会有rw spin lock? 在有了强大的spin lock之后,为何还会有rw spin lock呢?无他,仅仅是为了增加内核的并发,从而增加性能而已.spin lock严格的限制只有一个 ...

  2. Linux内核同步机制之(四):spin lock【转】

    转自:http://www.wowotech.net/kernel_synchronization/spinlock.html 一.前言 在linux kernel的实现中,经常会遇到这样的场景:共享 ...

  3. Linux内核同步机制--转发自蜗窝科技

    Linux内核同步机制之(一):原子操作 http://www.wowotech.net/linux_kenrel/atomic.html 一.源由 我们的程序逻辑经常遇到这样的操作序列: 1.读一个 ...

  4. Linux内核同步

    Linux内核剖析 之 内核同步 主要内容 1.内核请求何时以交错(interleave)的方式执行以及交错程度如何. 2.内核所实现的基本同步机制. 3.通常情况下如何使用内核提供的同步机制. 内核 ...

  5. Linux内核同步 - spin_lock

    一.前言 在linux kernel的实现中,经常会遇到这样的场景:共享数据被中断上下文和进程上下文访问,该如何保护呢?如果只有进程上下文的访问,那么可以考虑使用semaphore或者mutex的锁机 ...

  6. [内核同步]浅析Linux内核同步机制

    转自:http://blog.csdn.net/fzubbsc/article/details/37736683?utm_source=tuicool&utm_medium=referral ...

  7. Linux内核同步机制

    http://blog.csdn.net/bullbat/article/details/7376424 Linux内核同步控制方法有很多,信号量.锁.原子量.RCU等等,不同的实现方法应用于不同的环 ...

  8. Linux内核同步机制之completion【转】

    Linux内核同步机制之completion 内核编程中常见的一种模式是,在当前线程之外初始化某个活动,然后等待该活动的结束.这个活动可能是,创建一个新的内核线程或者新的用户空间进程.对一个已有进程的 ...

  9. 浅析Linux内核同步机制

    非常早之前就接触过同步这个概念了,可是一直都非常模糊.没有深入地学习了解过,最近有时间了,就花时间研习了一下<linux内核标准教程>和<深入linux设备驱动程序内核机制>这 ...

随机推荐

  1. 如何在脚本中执行SQL语句并获得结果输出?

    这里需要用到的工具叫做sqlcmd.exe, 它随SQL server的安装而安装. 该可执行程序的位置在: C:\Program Files\Microsoft SQL Server\xxx\Too ...

  2. Android中XML解析-PULL解析

    前面写了两篇XML解析的Dom和SAX方式,Dom比较符合思维方式,SAX事件驱动注重效率,除了这两种方式以外也可以使用Android内置的Pull解析器解析XML文件. Pull解析器的运行方式与 ...

  3. Strategy 策略模式 MD

    Markdown版本笔记 我的GitHub首页 我的博客 我的微信 我的邮箱 MyAndroidBlogs baiqiantao baiqiantao bqt20094 baiqiantao@sina ...

  4. Spring Security OAuth2 Demo

    Spring Security OAuth2 Demo 项目使用的是MySql存储, 需要先创建以下表结构: CREATE SCHEMA IF NOT EXISTS `alan-oauth` DEFA ...

  5. 宿主机为linux、windows分别实现VMware三种方式上网(转)

    一.VMware三种方式工作原理1 Host-only连接方式  让虚机具有与宿主机不同的各自独立IP地址,但与宿主机位于不同网段,同时为宿主主机新增一个IP地址,且保证该IP地址与各虚机IP地址位于 ...

  6. 《TCP/IP具体解释卷2:实现》笔记--接口层

    接口层包含在本地网上发送和接收分组的硬件与软件. 我们用设备驱动程序来表示与硬件及网络接口通信的软件,网络接口是指在一个特定网络上硬件与设备驱动器之间的接口. Net/3接口层试图在网络协议和连接到一 ...

  7. 机器视觉之 ICP算法和RANSAC算法

    临时研究了下机器视觉两个基本算法的算法原理 ,可能有理解错误的地方,希望发现了告诉我一下 主要是了解思想,就不写具体的计算公式之类的了 (一) ICP算法(Iterative Closest Poin ...

  8. C#应用视频教程3.4 Halcon+C#测试

    有了前面的基础后,我们来测试一下如何把程序做的更通用,首先是把初始化的方法修改一下,在初始化的时候传递过去HTuple这个对象(改成了全局的变量,以便于不同的方法调用)   其次需要有相机打开/相机关 ...

  9. 把普通java项目转换成maven项目

    我使用的是eclipse,右键项目,Configure->Convert to Maven Project 然后就是jar包的引入了,如果jar包比较简单,建议从maven中拉取,这样便于后期升 ...

  10. Python编程-基础知识-条件判断

    1. 简单的if/else条件判断 judge_flow.py name = input("Please input name: ") if name == 'master': p ...