• Linux 并发与竞争(二)


    Linux 并发与竞争(二)

    原子操作,自旋锁,读写锁,顺序锁相关内容在上节内容中,看这里

    Linux并发与竞争(一)

    1. 前言

    协调好 Linux 并发导致的竞争问题,除了可以使用原子操作,自旋锁(含包含读写锁,顺序锁)之外还可以使用信号量,互斥体。有这么多的机制可用,它们各有特点并不是相互取代的关系,这些里面因该没有一种机制是通用的,所以这些机制都要了解(如同学习编程语言,语法都要会,根据逻辑要求使用不同的语法,这就没有学习哪些语法就够了的说法),不用考虑学习哪种机制更好,根据场景结合这些机制的特点去使用这些机制。

    2. 信号量

    信号量(Semaphore),可以用来保证两个或多个关键代码段,资源不被并发调用,只要是用来协调资源竞争的机制都是围绕这个出发点的,所以主要是理解不同机制实现资源协调的原理。

    工作原理:

    信号量的工作机制可以直接理解成计数器,信号量会有一个 大于 0 的初值,任何进程申请使用信号量,信号量个数会减一,任何进程离开临界区释放信号量,信号量个数会加一,当计数器减到 0 的时候就说明没有资源了。信号量为 0,其他进程要想访问就必须等待,等待别的进程可以进入睡眠(直到信号量值大于 0 时进程被唤醒,访问该资源)。

    思考:

    工作原理部分说明了什么信息呢?说明了一个进程只能申请一个信号量,信号量为 0 说明没有资源了,信号量为正说明还有多少资源,那信号量为负值时表示什么?可以表示还有几个进程正在等待资源(还差几个资源才满足访问)。所以假设计数值为 n,可以得到以下 3 种结论:

    (1) n > 0,当前有可用资源,可用资源数量为 n。
    (2) n = 0,资源都被占用,可用资源数量为 0,有 n 个进程正在占用资源。
    (3) n < 0,资源都被占用,并且还有 n 个进程正在排队。

    队列:

    信号量为 0 时,其他进程要想访问就必须等待,等待也讲究先来后到,所以信号量除了一个用于统计信号量个数的变量还需要一个队列(阻塞队列),让等待资源的进程排好队并睡眠,资源空闲时按顺序唤醒进程去访问资源。

    特性:

    (1) 中断不能休眠,信号量不能用于中断中。

    (2) 信号量会使等待线程进入休眠状态,因此适用于那些占用资源比较久的场合。

    (3) 如果共享资源的持有时间比较短,就不适合使用信号量,因为频繁的休眠、切换线程会引起极大的开销。

    如果信号量值的大于 1,那么这个信号量属于计数型信号量,如果要互斥的访问共享资源那么信号量的值就不能大于 1,此时的信号量属于二值信号量,这两种信号量原理相同。

    生产者/消费者问题

    系统中有一组生产者进程和一组消费者进程,生产者进程每次生产一个产品放入缓冲区,消费者进程每次从缓冲区中取出一个产品并使用。(这里的 产品 理解为某种数据)生产者,消费者共享一个初始为空,大小为 n 的缓冲区。只有缓冲区没满时,生产者才能把产品放入缓冲区,否则必须等待。只有缓冲区不空时,消费者才能从中取出产品,否则必须等待。缓冲区是临界资源,在临界资源都被占用后,各进程必须互斥地访问。

    2.1 方法浅析

    Linux 内核定义了 semaphore 的结构体来表示信号量,使用之前先定义类型,类型定义在 “include\linux\semaphore.h” 文件中。

    /* Please don't access any members of this structure directly */
    struct semaphore {
        raw_spinlock_t        lock;
        unsigned int        count;
        struct list_head    wait_list;
    };
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    可以看到结构体包含一个计数器 count(用于标记资源的数量),以及一个等待列表(也可以叫队列,用来存放等待资源的线程信息),是不是和上面表述的一样。

    2.2 API 函数

    下面是一些常用的信号量(对象)操作函数,注意 DEFINE_SEMAPHORE() 宏定义默认定义的是二值信号量,如果定义计数信号量,则自己直接定义结构体,再使用 sema_init() 函数去初始化结构体,并指定信号数量。

    这些函数定义在 “kernel\locking\semaphore.c” 文件中。

    /*
    定义一个信号量,默认设置信号量的值为 1,
    也就是说默认定义二值信号量,如果想使用计数信号量请用
    sema_init 函数初始化时定义一个大于 1 的值
    */
    DEFINE_SEMAPHORE(name);
    /*初始化信号量 sem,设置信号量值为 val*/
    void sema_init(struct semaphore *sem, int val);
    /*获取信号量,因为会导致休眠,因此不能在中断中使用*/
    void down(struct semaphore *sem);
    /*
    尝试获取信号量,如果能获取到信号量就获
    取,并且返回 0。如果不能就返回非 0,并且
    不会进入休眠
    */
    int down_trylock(struct semaphore *sem);
    /*
    获取信号量,和 down 类似,只是使用 down 进
    入休眠状态的线程不能被信号打断。而使用此
    函数进入休眠以后是可以被信号打断的
    */
    int down_interruptible(struct semaphore *sem);
    /*释放信号量*/
    void up(struct semaphore *sem);
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24

    本质上 DEFINE_SEMAPHORE() 宏定义也是定义结构体,看 Linux 源码这样定义。

    #define DEFINE_SEMAPHORE(_name, _n) \
        struct semaphore _name = __SEMAPHORE_INITIALIZER(_name, _n)
    
    • 1
    • 2

    下面再看一个信号量定义和使用实例。

    struct semaphore sem; /* 定义信号量 */
    sema_init(&sem, 1);   /* 初始化信号量 */
    down(&sem);           /* 申请信号量 */
    
    /*访问/操作资源临界区*/
    
    up(&sem);             /* 释放信号量 */
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    3. 互斥体

    互斥体用于保护要互斥访问的共享资源,互斥访问二值信号量(信号量值为 1)也可以实现,而互斥体加一个计数器可以模仿信号量的效果,它们各有特点并不是相互取代的关系。

    区别与联系:

    Mutex 相比信号量增加了所有权的概念,被锁住的 Mutex 只能由给它上锁的线程才能解开(解铃还需系铃人),Mutex 的功能也就因而限制在了构造临界区上。

    二值信号量则可以由任一线程解开,是解决生产者-消费者问题的工具。比如某进程读取磁盘并进入睡眠,等待中断读取盘块结束之后来唤醒它。这就是可以用二值信号量的一个情景,这是 Mutex 解决不了的。

    特性:

    (1) 中断不能休眠,互斥体不能用于中断中。

    (2) 只有 Mutex 的持有者才能释放 Mutex。

    (3) Mutex 保护的临界区可以调用引起阻塞的 API 函数。

    (4) 因为一次只有一个线程可以持有 Mutex,并且由于 (2) 的原因,因此 Mutex 不能递归上锁和解锁。

    3.1 方法浅析

    Linux 内核定义了 mutex 的结构体来表示互斥体,使用之前先定义类型,类型定义在 “include\linux\mutex.h” 文件中。

    struct mutex {
        atomic_long_t       owner;
        raw_spinlock_t      wait_lock;
    #ifdef CONFIG_MUTEX_SPIN_ON_OWNER
        struct optimistic_spin_queue osq; /* Spinner MCS lock */
    #endif
        struct list_head    wait_list;
    #ifdef CONFIG_DEBUG_MUTEXES
        void            *magic;
    #endif
    #ifdef CONFIG_DEBUG_LOCK_ALLOC
        struct lockdep_map  dep_map;
    #endif
    };
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14

    从上面的结构体可以看到,互斥体包含 owner 变量,这个就是用来记录互斥体当前属于谁(线程),所以只有 mutex 的持有者才能释放 mutex。

    3.2 API 函数

    下面是一些常用的互斥体(对象)操作函数,这些函数定义在 “tools\perf\util\mutex.c” 文件中。

    /*定义并初始化一个 mutex 变量*/
    DEFINE_MUTEX(name);
    /*初始化 mutex*/
    void mutex_init(mutex *lock);
    /*获取 mutex,也就是给 mutex 上锁,如果获取不到就进休眠*/
    void mutex_lock(struct mutex *lock);
    /*释放 mutex,也就给 mutex 解锁*/
    void mutex_unlock(struct mutex *lock);
    /*尝试获取 mutex,如果成功就返回 1,如果失败就返回 0*/
    int mutex_trylock(struct mutex *lock);
    /*判断 mutex 是否被获取,如果是的话就返回1,否则返回 0*/
    int mutex_is_locked(struct mutex *lock);
    /*使用此函数获取信号量失败进入休眠以后可以被信号打断*/
    int mutex_lock_interruptible(struct mutex *lock);
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14

    下面看一个互斥体定义实例。

    struct mutex lock; /* 定义互斥体 */
    mutex_init(&lock); /* 初始化互斥体 */
    mutex_lock(&lock); /* 申请互斥体 */
    
    /*访问/操作资源临界区*/
    
    mutex_unlock(&lock); /* 释放互斥体 */
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    自此在 Linux 中常用的资源竞争处理工具就这些了,原子操作,自旋锁,读写锁,顺序锁相关内容在上节内容中,在这里:

    Linux并发与竞争(一)

  • 相关阅读:
    go gin r.Any Query PostForm Param获取请求参数
    CGLIB源码易懂解析
    less与sass(scss)的区别
    使用Docker Compose运行Elasticsearch
    CentOS 7 无界面版本设置静态IP步骤
    Halcon 小笔记 C# 图片是否有效
    【Java面试】ConcurrentHashMap再JDK7和8中的区别以及ConcurrentHashMap底层实现
    Stable Diffusion|Ai赋能电商 Inpaint Anything
    代码随想录训练营day53
    [国产MCU]-W801开发实例-MQTT客户端通信
  • 原文地址:https://blog.csdn.net/jf_52001760/article/details/134255553