Linux信号量机制分析

253次阅读  |  发布于3年以前

背景

说明:

  1. Kernel版本:4.14
  2. ARM64处理器,Contex-A53,双核
  3. 使用工具:Source Insight 3.5, Visio

1 . 概述

本文将分析信号量与读写信号量的机制,开始吧。

2 . 信号量

2.1 流程分析

信号量的实现很简单,先看一下数据结构:

struct semaphore {
    raw_spinlock_t      lock;       //自旋锁,用于count值的互斥访问
    unsigned int        count;      //计数值,能同时允许访问的数量,也就是上文中的N把锁
    struct list_head    wait_list;      //不能立即获取到信号量的访问者,都会加入到等待列表中
};

struct semaphore_waiter {
    struct list_head list;      //用于添加到信号量的等待列表中
    struct task_struct *task;   //用于指向等待的进程,在实际实现中,指向current
    bool up;                    //用于标识是否已经释放
};

流程如下:

2.2 信号量缺点

  1. 在保护临界区的时候,无法进行优先级反转的处理;
  2. 系统无法对其进行跟踪断言处理,比如死锁检测等;
  3. 信号量的调试变得更加麻烦;

因此,在Mutex能满足要求的情况下,优先使用Mutex

2.3 其他接口

信号量提供了多种不同的信号量获取的接口,介绍如下:

/* 未获取信号量时,进程轻度睡眠:TASK_INTERRUPTIBLE */
int down_interruptible(struct semaphore *sem)
/* 未获取到信号量时,进程中度睡眠:TASK_KILLABLE */
int down_killable(struct semaphore *sem)
/* 非等待的方式去获取信号量 */
int down_trylock(struct semaphore *sem)
/* 获取信号量,并指定等待时间 */
int down_timeout(struct semaphore *sem, long timeout)

3 . 读写信号量

《linux spinlock/rwlock/seqlock原理剖析(基于ARM64)》文章中,我们分析过读写自旋锁,读写信号量的功能类似,它能有效提高并发性,我们先明确下它的特点:

3.1 数据结构

读写信号量的数据结构与信号量的结构比较相似:

struct rw_semaphore {
    atomic_long_t count;        //用于表示读写信号量的计数
    struct list_head wait_list;     //等待列表,用于管理在该信号量上睡眠的任务
    raw_spinlock_t wait_lock;   //锁,用于保护count值的操作
#ifdef CONFIG_RWSEM_SPIN_ON_OWNER
    struct optimistic_spin_queue osq; /* spinner MCS lock */    //MCS锁,参考上一篇文章Mutex中的介绍
    /*
     * Write owner. Used as a speculative check to see
     * if the owner is running on the cpu.
     */
    struct task_struct *owner;      //当写者成功获取锁时,owner会指向锁的持有者
#endif
#ifdef CONFIG_DEBUG_LOCK_ALLOC
    struct lockdep_map  dep_map;
#endif
};

在获取释放读锁和写锁的全过程中,count值伴随着上述这几个宏定义的加减操作,用于标识不同的状态,可以罗列如下:

  1. 0xFFFF000X = RWSEM_WAITING_BIAS + X * RWSEM_ACTIVE_READ_BIAS,表示活跃的读者和正在申请读锁的读者总共有X个,并且还有一个写者在睡眠等待;
  2. 0xFFFF000X = RWSEM_ACTIVE_WRITE_BIAS + (X - 1)* RWSEM_ACTIVE_READ_BIAS,表示有一个写者在尝试获取锁,活跃的读者和正在申请读锁的读者总共有X-1个;
  1. 0xFFFF0001 = RWSEM_ACTIVE_WRITE_BIAS,有一个活跃的写者,或者写者正在尝试获取锁,没有读者干扰;
  2. 0xFFFF0001 = RWSEM_ACTIVE_READ_BIAS + RWSEM_WAITING_BIAS,有个写者正在睡眠等待,还有一个活跃或尝试获取锁的读者;

3.1 读信号量

3.1.1 读者获取锁

总结一下:读者获取锁的时候,如果没有写者持有,那就可以支持多个读者直接获取;而如果此时写者持有了锁,读者获取失败,它将把自己添加到等待列表中,(这个等待列表中可能已经存放了其他来获取锁的读者或者写者),在将读者真正睡眠等待前,还会再一次判断此时是否有写者释放了该锁,释放了的话,那就需要对睡眠等待在该锁的任务进行唤醒操作了

3.1.2 读者释放锁

3.2 写信号量

3.2.1 写者获取锁

总结写者获取锁时,只要锁被其他读者或者写者持有了,则获取锁失败,然后进行失败情况处理。在失败情况下,它本身会尝试进行optimistic spin去尝试获取锁,如果获取成功了,那就是皆大欢喜了,否则还是需要进入慢速路径。慢速路径中去判断等待列表中是否有任务在睡眠等待,并且会再次尝试去查看是否已经有写者释放了锁,写者释放了锁,并且只有读者在睡眠等待,那么此时应该优先让这些先等待的任务唤醒

3.2.2 写者释放锁

3.3 总结

理解读写信号量有几个关键点:

  1. 读写信号量的特性可以与读写自旋锁进行类比(读者与读者并发、读者与写者互斥、写者与写者互斥),区别在于读写信号量可能会发生睡眠,进而带来进程切换的开销;
  2. 为了优化读写信号量的性能,引入了MCS锁机制,进一步减少切换开销。第一个写者获取了锁后,第二个写者去获取时自旋等待,而读者去获取时则会进入睡眠;
  3. 读写信号量的count值很关键,代表着读写信号量不同状态的切换,因此也决定了执行流程;
  4. 读者或写者释放锁的时候,去唤醒等待列表中的任务,需要分情况处理。等待列表中可能存放的是读者与写者的组合,如果第一个任务是写者,则直接唤醒该写者,否则将唤醒排在前边的连续几个读者;

参考

Real-world Concurrency

Copyright© 2013-2020

All Rights Reserved 京ICP备2023019179号-8