• Linux之并发竞争管理


    Linux 系统是个多任务操作系统,会存在多个任务同时访问同一片内存区域,这些任务可能会相互覆盖这段内存中的数据,造成内存数据混乱。我们需要对共享数据进行相应的保护处理。

    产生并发的主要原因有:

    ①多线程并发访问, Linux 是多任务(线程)的系统,多线程访问是最基本的原因

    ②抢占式并发访问,Linux 内核支持抢占,调度程序可以在任意时刻抢占正在运行的线程,从而运行其他的线程。

    ③中断程序并发访问,硬件中断的权利可以是很大的。

    ④SMP(多核)核间并发访问,多核 CPU 存在核间并发访问。

    并发:多个执行单元同时进行或多个执行单元微观串行执行,宏观并行执行。微观串行,宏观并行:理解成把时间轴放大以后,各个任务是串行执行,然后每个任务执行一定的时间片,执行完后由调度系统调度到另一个任务去执行。因为CPU的速度很快,所以宏观看来是并行执行。 

    竞争:并发的执行单元对共享资源(硬件资源和软件上的全局变量)的访问而导致的竞争状态。

    临界资源: 多个进程访问的资源,共享数据段

    临界区:多个进程访问的代码段

     

    一、原子操作

    原子操作是指不能再进一步分割的操作。一般原子操作用于整形变量或者位操作。

    Linux 内核定义了叫做 atomic_t 的结构体来完成整形数据的原子操作,在使用中用原子变量来代替整形变量,此结构体定义在 include/linux/types.h 文件中

    1. typedef struct {
    2. int counter;
    3. } atomic_t;

    如果要使用原子操作 API 函数,首先要先定义一个 atomic_t 的变量,

    1. atomic_t a; //定义 a
    2. atomic_t b = ATOMIC_INIT(0); //定义原子变量 b 并赋初值为 0

    原子变量有了,接下来就是对原子变量进行操作,读、写、增加、减少。

    原子操作API函数

     如果使用 64 位的 SOC 的话,就使用64 位的原子变量

    1. typedef struct {
    2. long long counter;
    3. } atomic64_t;

     相应的操作函数把“atomic_”前缀换为“atomic64_”,将 int 换为 long long即可。

    原子位操作:即位操作,在Linux 内核也提供了一系列的原子位操作 API 函数,原子位操作不像原子整形变量那样有个 atomic_t 的数据结构,原子位操作是直接对内存进行操作

     

    1. atomic_t lock; /* 原子变量 */
    2. /* 初始化原子变量 */
    3. atomic_set(&lock, 1); /* 原子变量初始值为1 */
    4. atomic_inc(&lock); /*释放原子变量*/
    5. /* 通过判断原子变量的值来检查共享资源有没有被别的应用使用:1 */
    6. //若为1 则1-1为0 ,返回真,取反为假
    7. //若为0 ,则0-1不为0,返回假,取反为真,执行下面
    8. if (!atomic_dec_and_test(&gpioled.lock)) {
    9. atomic_inc(&gpioled.lock); /* 小于0的话就加1,使其原子变量等于0 */
    10. return -EBUSY; /* 共享资源被使用,返回忙 */
    11. }
    12. /* 通过判断原子变量的值来检查共享资源有没有被别的应用使用:2 */
    13. if(atomic_read(&lock) <= 0)
    14. {
    15. return -EBUSY;
    16. }
    17. else
    18. {
    19. atomic_dec(&lock);
    20. }

    二、自旋锁 

    原子操作只能对整形变量或者位进行保护,不能运用到太复杂的环境中。如若共享数据为设备结构体,对于结构体中成员变量的操作要保证原子性,在线程 A 对结构体变量使用期间,应该禁止其他的线程来访问此结构体变量,此时可以使用自旋锁解决。

    当一个线程要访问某个共享资源的时候首先要先获取相应的锁, 锁只能被一个线程持有只要此线程不释放持有的锁,那么其他的线程就不能获取此锁。对于自旋锁而言,如果自旋锁正在被线程 A 持有,线程 B 想要获取自旋锁,那么线程 B 就会处于忙循环-旋转-等待状态,线程 B 不会进入休眠状态或者说去做其他的处理,直到线程A释放自旋锁,线程B才可以访问共享资源。由于等待自旋锁会浪费处理器时间,降低系统性能,所以自旋锁的持有时间不能太长,所以自旋锁适用于短时期的轻量级加锁
     

    Linux 内核使用结构体 spinlock_t 表示自旋锁

    1. typedef struct spinlock {
    2. union {
    3. struct raw_spinlock rlock;
    4. #ifdef CONFIG_DEBUG_LOCK_ALLOC
    5. # define LOCK_PADSIZE (offsetof(struct raw_spinlock, dep_map))
    6. struct {
    7. u8 __padding[LOCK_PADSIZE];
    8. struct lockdep_map dep_map;
    9. };
    10. #endif
    11. };
    12. } spinlock_t;

    在使用自旋锁之前,肯定要先定义一个自旋锁变量

    spinlock_t lock; //定义自旋锁

    定义好自旋锁变量以后就可以使用相应的 API 函数来操作自旋锁,加锁或者释放锁。

    自旋锁API函数


     

    自旋锁API函数适用于SMP或支持抢占的单CPU下线程之间的并发访问,用于线程与线程之间,被自旋锁保护的临界区一定不能调用任何能够引起睡眠和阻塞的API 函数,否则可能会导致死锁现象自旋锁会自动禁止抢占,当线程 A得到锁以后会暂时禁止内核抢占

    如果线程 A 在持有锁期间进入了休眠状态,线程 A 会自动放弃 CPU 使用权。线程 B 开始运行,线程 B 也想要获取锁,但是此时锁被 A 线程持有,而且内核抢占还被禁止。线程 B 无法被调度出去,那么线程 A 就无法运行,锁也就无法释放,产生死锁现象。

    如果在自旋锁加锁的临界区中使用中断,并且中断也会去访问共享资源,也可能会发送死锁现象。



    在中断里面是可以使用自旋锁,但是在中断里面使用自旋锁的时候,在获取锁之前一定要先禁止本地中断(也就是本 CPU 中断,对于多核 SOC来说会有多个 CPU 核),否则可能导致锁死现象的发生。

    获取锁之前关闭本地中断,相关自旋锁API

     使用 spin_lock_irq/spin_unlock_irq 的时候需要用户能够确定加锁之前的中断状态,但实际上内核很庞大,很难确定某个时刻的中断状态,因此不推荐使用。建议使用 spin_lock_irqsave/ spin_unlock_irqrestore,这一组函数会保存中断状态,在释放锁的时候会恢复中断状态

     一般在线程中使用 spin_lock_irqsave/spin_unlock_irqrestore

    中断中使用spin_lock/spin_unlock

     

    1. DEFINE_SPINLOCK(lock) /* 定义并初始化一个锁 */
    2. /* 线程 A */
    3. void functionA ()
    4. {
    5. unsigned long flags; /* 中断状态 */
    6. spin_lock_irqsave(&lock, flags) /* 获取锁 */
    7. /* 临界区 */
    8. spin_unlock_irqrestore(&lock, flags) /* 释放锁 */
    9. }
    10. /* 中断服务函数 */
    11. void irq()
    12. {
    13. spin_lock(&lock) /* 获取锁 */
    14. /* 临界区 */
    15. spin_unlock(&lock) /* 释放锁 */
    16. }

     注意自旋锁使用事项:

    锁的持有时间不能太长,一定要短,否则的话会降低系统性能。如果临界区比较大,运行时间比较长的话要选择其他的并发处理方式:信号量、互斥体。

    ②自旋锁保护的临界区内不能调用任何可能导致线程休眠的 API 函数,否则可能导致死锁。

    ③不能递归申请自旋锁,否则可能导致死锁。

    三、信号量

    Linux 内核提供了信号量机制,信号量常常用于控制对共享资源的访问。它是一个计数器,常用于实现进程间的互斥与同步,而不是用于存储进程间通信数据。

    信号量的特点

    ①信号量可以使等待资源线程进入休眠状态,适用于占用资源比较久的场合

    ②信号量会引起休眠,中断不能休眠,所以信号量不能用于中断

    ③如果共享资源的持有时间比较短,不适合使用信号量,因为频繁的休眠、切换线程引起的开销要远大于信号量带来的优势,此时使用自旋锁。

    示例:假设一个房子有 10 把钥匙,这 10 把钥匙就相当于信号量值为10。可以通过信号量来控制访问共享资源的访问数量。如果要想进房间,需要先获取一把钥匙,信号量值减 1,直到 10 把钥匙被拿走,信号量值为 0,这时不允许任何人进入房间。如果有人从房间出来,并归还钥匙,信号量值加 1,现在可以允许进去一个人。

    通过信号量控制访问资源的线程数,在初始化的时将信号量值设置的大于 1信号量是计数型信号量,计数型信号量不能用于互斥访问,因为它允许多个线程同时访问共享资源

    如果要互斥的访问共享资源,则信号量的值小于等于1,此时信号量称为二值信号量
     

    Linux 内核使用 semaphore 结构体表示信号量
     

    1. struct semaphore {
    2. raw_spinlock_t lock;
    3. unsigned int count;
    4. struct list_head wait_list;
    5. };

    使用信号量①先定义信号量②初始化信号量

    1. struct semaphore sem; /* 定义信号量 */
    2. sema_init(&sem, 1); /* 初始化信号量 */
    3. down(&sem); /* 申请信号量 */
    4. /* 临界区 */
    5. up(&sem); /* 释放信号量 */

    信号量常用API

    四、互斥体 

    将信号量的值设置为 1 就可以使用信号量进行互斥访问。但是 Linux 提供了专门的互斥体mutex。互斥访问表示一次只有一个线程可以访问共享资源,不能递归申请互斥体。在 Linux 驱动的时遇到需要互斥访问的地方一般使用 mutex。

    Linux 内核使用 mutex 结构体表示互斥体

    1. struct mutex {
    2. /* 1: unlocked, 0: locked, negative: locked, possible waiters */
    3. atomic_t count;
    4. spinlock_t wait_lock;
    5. };

    互斥体使用注意事项:

    ①mutex 可以导致休眠,因此不能在中断中使用 mutex,中断中只能使用自旋锁
    ②mutex 保护的临界区可以调用引起阻塞的 API 函数(信号量也可以)

    ③因为一次只有一个线程可以持有 mutex,所以,必须由 mutex 的持有者释放 mutex。
    且 mutex 不能递归上锁和解锁

    互斥体常用API

     

    1. struct mutex lock; /* 定义一个互斥体 */
    2. mutex_init(&lock); /* 初始化互斥体 */
    3. mutex_lock(&lock); /* 上锁 */
    4. /* 临界区 */
    5. mutex_unlock(&lock); /* 解锁 */

     



     

  • 相关阅读:
    CSS魔法!如何将任意CSS类型转换为数值?
    云服务器ECS价格表出炉_2024年最新价格表——阿里云
    公众号题库搜题对接
    【项目实战】从零开始设计并实现一个接口异常链路分析器
    POI:接收上传上来的excel,解析并导入到数据库
    MS COCO数据集介绍以及pycocotools使用
    基于Java+SpringBoot+vue+elementui药品商城采购系统详细设计实现
    FPGA - 科学设计复位信号(XILINX)
    【拼多多研究报告】一个被忽视的社交电商搅局者
    (附源码)小程序 记账微信小程序 毕业设计 180815
  • 原文地址:https://blog.csdn.net/qq_53144843/article/details/126714857