• RT-Thread信号量和互斥量


    目录

    信号量

    信号量基本概念

    信号量基本概念

    信号量的特性

    二值信号量的运作机制

    计数型信号量的运作机制

     信号量相关接口

    信号量控制块、

    创建信号量

    删除信号量

    初始化信号量

    脱离信号量

    释放信号量

    获取信号量

    无等待获取信号量

    使用场合

    线程同步

     中断与线程的同步

    资源计数

    信号量实验的代码和流程分析

    个人总结:


    信号量

    信号量是一种轻型的用于解决线程间同步问题的内核对象,线程可以获取或释放它,从而达到同步或互斥的目的。信号量就像一把钥匙,把一段临界区给锁住,只允许有钥匙的线程进行访问,线程拿到了钥匙,才允许它进入临界区;而离开后把钥匙传递给排队在后面的等待线程,让后续线程依次进入临界区。

    信号量工作示意图,每个信号量对象都有一个信号量值和一个线程等待队列,信号量的值对应了信号量对象的实例数目、资源数目,假如信号量值为5,则表示共有5个信号量实例(资源)可以被使用,当信号量实例数目为0时,再申请该信号量的线程就会被挂在该信号量的等待队列上,等待可用的信号量实例(资源)。

    信号量基本概念

    同志们,回想一下,在裸机编程中这样使用过一个变量,用于标记某个事件是否发生,或者标志一下某个东西是否正在被使用,如果是被占用了的或者没发生的,我们就不对它进行操作。

    信号量基本概念

    信号量(semaphore)是一种实现线程间通信的机制,实现线程之间同步或临界资源的互斥访问,常用于协助一组相互竞争的线程来访问临界资源。在多线程系统中,各线程之间需要同步或互斥实现临界资源的保护,信号量功能可以为用户提供这方面支持。

    通常一个信号量的计数值用于对应有效的资源数,表示剩下的可能被占用的资源互斥数。其值的含义分为两种情况:

    • 0:表示没有积累下来的release释放信号量操作,且有可能有在此信号量上阻塞的线程。
    • 正值:表示有一个或者多个release释放信号量的操作。

    以同步为目的的信号量和以互斥为目的的信号量在使用有如下不同:

    • 用作互斥时,信号量创建之后可用信号量个数应该是满的,线程在需要使用临界资源时,先获取信号量,使其变空,这样其他线程需要使用临界资源时就会因为无法获取信号量而进入阻塞,从而保证了临界资源的安全。但是这样子有一个缺点就是有可能产生优先级反转,优先级反转的危害在互斥量中会详细说明。
    • 用作同步时,信号量在创建后被置为空,线程1取信号量而阻塞,线程2在某种条件发生后,释放信号量,于是线程1得以进入就绪态,如果线程1的优先级是最高的,那么就会立即切换线程,从而达到了两个线程的同步。同样的,在中断服务函数中释放信号量,也能达到线程与中断的同步。

    在操作系统中,我们使用信号量的目的是为了给临界资源建立一个标志,信号量表示了该临界资源被占用的情况。这样,当一个线程在访问临界资源的时候,就会先对这个资源信息进行查询,从而在了解资源被占用的情况之后,再做处理,从而使得临界资源得到有效的保护。

    还记得我们经常说的中断要快进快出吗,在裸机开发中我们经常是在中断中做一个标志,然后在退出的时候进行轮询处理,这个就是类似我们使用信号量进行同步的,当标记发生了,我们再做其他事情。在RT-Thread我们使用信号量用作同步,线程与线程的同步,中断与线程的同步,可以大大提高效率。

    信号量还有计数型信号量,计数型信号量是允许多个线程对其进行操作,但限制了线程的数量。比如有一个停车场,里面只有100个车位,那么能停的车只有100辆,也就相当于我们的信号量有100个,假如一开始停车场的车位还有100个,那么每进去一辆车就要消耗一个停车位,车位的数量就要减一,对应的,我们的信号量在使用之后也需要减1,当停车场停满了100辆车的时候,此时的停车位为0,再来的车就停不进去了,否则将造成交通事故,也相当于我们的信号量为0,后面的线程对这个停车场资源的访问也无法进行,当有车从停车场离开的时候,车位又空余出来了,那么后面的车就能停进去了,在我们的信号量的操作也是一样的,当我们释放了这个资源,后面的线程才能对这个资源进行访问。

    信号量的特性

    信号量的常规操作

    信号量这个名字很恰当:

    • 信号:起通知作用
    • 量:用来表示资源的数量
    • 支持的动作:“give”给出资源,计数值加1:“take”获得资源,计数值减1

    信号量的典型应用场景是:

    • 计数:生产者“give”信号量,让计数值加1,消费者先“take”信号量,就是获得信号量,让计数值减1
    • 资源管理:想要访问资源需要先“take”信号量,让计数值减1,用完资源后“give”信号量,让计数值加1

    信号量的give、take双方并不需要相同,可以用于生产者-消费者场合:

    • 生产者为线程A、B,消费者为线程C、D
    • 一开始信号量的计数值为0,如果线程C、D想获得信号量,会有两种结果:
      • 阻塞:买不到东西咱就等等吧,可以定个闹钟(超时时间)
      • 即刻返回失败,不等
    • 线程A、B可以生产资源,就是让信号量的计数值加1,并且把等待这个资源的线程唤醒
    • 唤醒谁?有两种办法。创建信号量时,可以指定一个参数flag
      • RT_IPC_FLAG_PRIO:表示先唤醒优先级最高的等待线程
      • RT_IPC_FLAG_FIFO:表示先唤醒等待时间最长的等待线程

      信号量和消息队列的对比

        消息队列       信号量

    可以容纳多个数据,创建队列时有2部分内存:

    队列结构体,存储数据的空间

    只有计数值,无法容纳其他数据。创建信号量时,只需要分配信号量结构体
    生产者:没有空间存入数据时可以阻塞生产者:不阻塞,只要计数值没超过0xffff都会成功
    消费者:没有数据时可以阻塞消费者:没有资源时可以阻塞

    二值信号量的应用场景

    只有0和1两种情况的信号量称之为二值信号量

    在线程系统中,我们经常会使用这个二值信号量,比如,某个线程需要等待一个标记,那么线程可以在轮询中查询这个标记有没有被置位,这样子做,就会很浪费CPU的资源,其实根本不需要在轮询中查询这个标志,只需要使用二值信号量即可,当二值信号量没有的时候,线程进入阻塞态等待二值信号量的到来即可,当得到了这个信号量标记之后,在进行线程的处理即可,这样子就不会消耗太多资源,而且实时响应也是最快的。

    二值信号量的运作机制

    创建二值信号量,为创建的信号量对象分配内存,并把可用信号量初始化为用户定义的个数,二值信号量的最大可用信号量个数为1.

    信号量获取,从创建的信号量资源中获取一个信号量,获取成功返回正确,否则线程会等待它线程释放该信号量,超时时间由用户设定,当线程获取信号量失败时,线程将进入阻塞态,系统将线程挂到该信号量的阻塞列表中。

    在二值信号量无效的时候,假如此时有线程获取该信号量的话,那么线程将进入阻塞态。

    加入某个时间中断/线程释放了信号量,其过程具体见下图,那么由于获取无效信号量而进入阻塞态的线程将获得信号量并且恢复为就绪态,其过程如下图

    计数型信号量的运作机制

    计数型信号量和二值信号量是差不多的,一样用于资源保护,不过计数信号量则允许多个线程获取信号量访问共享资源,但会限制线程的最大数目。访问的线程数目到达信号量可支持的最大数目时,会阻塞其他试图获取该信号量的线程,直到有线程释放了信号。这就是计数型信号量的运作机制,虽然计数信号零允许多个线程访问同一个资源,但是也有限定,比如某个资源限定只能有3个线程访问,那么第4个线程访问的时候,会因为获取不到信号量而进入阻塞态,等到有线程(比如线程1) 释放掉该资源的时候,第4个线程才能获取到信号量从而进行资源的访问,其运作机制具体见计数信号量的运作示意图。

     信号量相关接口

    信号量控制块、

    1. struct rt_semaphore
    2. {
    3. struct rt_ipc_object parent; /**< inherit from ipc_object */
    4. rt_uint16_t value; /**< value of semaphore. */
    5. };
    6. typedef struct rt_semaphore *rt_sem_t;

    rt_semaphore对象从rt_ipc_object中派生,由IPC容器管理,信号量的最大值是65535.

    创建信号量

    rt_sem_t rt_sem_create(const char *name, rt_uint32_t value, rt_uint8_t flag)

    当创建一个信号量时,内核首先创建一个信号量控制块,然后对该控制块进行基本的初始化工作,创建信号量使用上面的函数接口。

    当调用这个函数时,系统将先分配一个semaphore对象,并初始化这个对象,然后初始化IPC对象以及与semaphore相关的部分,在创建信号量指定的参数中,信号量标志参数决定了当信号量不可用的时候,多个线程的等待方式,当选择FIFO方式时,那么等待线程队列将按照先进先出的方式排队,先进入的线程将先获得等待的信号量,当选择PRIO(优先级等待)的方式时,等待线程队列将按照优先级进行排队,优先级高的等待线程将优先获得等待的信号量。

    函数参数

    参数描述

    name

    value

    flag

    信号量名称

    信号量初始值

    信号量标志,取值可以用如下类型

    1. #define RT_IPC_FLAG_FIFO 0x00 /*IPC参数采用FIFO方式*/
    2. #define RT_IPC_FLAG_PRIO 0x01 /*IPC参数采用优先级方式*/

    删除信号量

    调用这个函数时,系统将删除这个信号量。如果删除信号量时,有线程正在等待该信号量,那么删除操作会先唤醒等待在该信号量上的线程上(等待线程的返回值是-RT_ERROR),然后再释放信号量的内存资源。

    1. rt_err_t rt_sem_delete(rt_sem_t sem)

    初始化信号量

    对于静态信号量对象,它的内存空间在编译时就被编译器分配出来,放在数据段或ZI段上,此时使用信号量就不再需要使用rt_sem_create接口来创建它,而只需要在使用前对它进行初始化即可。初始化信号量对象可用下面的接口:

    1. rt_err_t rt_sem_init(rt_sem_t sem,
    2. const char *name,
    3. rt_uint32_t value,
    4. rt_uint8_t flag)

    当调用这个函数时,系统将首先对这个semaphore对象进行初始化,然后初始化IPC对象以及与semaphore相关的部分。区别和动态创建信号量的区别就是内存是否是动态分配的。

    脱离信号量

    脱离信号量就是让信号量对象从内核对象管理器中移除掉。脱离信号量使用下面的函数接口:

    1. rt_err_t rt_sem_detach(rt_sem_t sem)

    使用该函数后,内核先唤醒所有挂在该信号量的等待队列上的线程,然后将该信号量从内核对象管理器中删除。原来挂起在信号量上的等待线程将获得-RT_ERROR的返回值。

    释放信号量

    当线程释放完资源的访问后,应尽快的释放它持有的信号量,使得其他线程能获得该信号量。释放信号量使用下面的函数接口:

    1. rt_err_t rt_sem_release(rt_sem_t sem)

    当信号量的值等于0时,并且有线程等待这个信号量时,将唤醒等待在该信号量线程队列中的第一个线程,由他获取信号量。否则将信号量的值加一。

    获取信号量

    线程通过获取信号量来获得信号量资源实例,当信号量值大于0时,线程将获得信号量,并且相应的信号量值都会减1,获取信号量使用下面的函数接口:

    1. rt_err_t rt_sem_take(rt_sem_t sem, rt_int32_t time)

    在调用这个函数时,如果信号量的值等于0,那么说明当前信号量的资源实例不可用,申请该信号量的线程将根据time参数的情况选择直接返回、或挂起等待一段时间,或永久等待直到其他线程或中断释放该信号量。如果在参数time指定的时间内依然得不到信号量,线程将超时返回,返回值是-RT_ETIMEOUT。

    无等待获取信号量

    当用户不想在申请的信号量上挂起线程等待的时候,可以使用无等待的方式获取信号量,无等待获取信号量使用下面的函数接口:本质上还是调用上面的函数接口,只是把time直接设置为0而已。

    1. rt_err_t rt_sem_trytake(rt_sem_t sem)
    2. {
    3. return rt_sem_take(sem, 0);
    4. }

    使用场合

    信号量是一种非常灵活的同步方式,可以运用在多种场合中。形成锁,同步,资源计数等关系,也能方便的用于线程与线程,中断与线程的同步中。

    线程同步

    线程同步是信号量最简单的一类应用。例如,两个线程用来进行任务间的执行控制转移,信号量的值初始化成具备0个信号量资源实例,而等待线程先直接在这个信号量上进行等待。当信号线程完成它处理的工作时,释放这个信号量,以把等待在这个信号量上的线程唤醒,让它执行下一部分工作。这类场合也可以看成把信号量用于工作完成的标志;信号线程完成他自己的工作,然后通知等待线程继续下一部分工作。

    锁,单一的锁常用于多个线程间对同一临界区的访问。信号量在作为锁来使用时,通常应将信号量资源实例初始成1,代表系统默认有一个资源可用。当线程需要访问临界资源时,它需要先获得这个资源锁。当这个线程成功获得资源锁时,其他打算访问临界区的线程将被挂在该信号量上,这是因为其他线程在试图获取这个锁时,它将会释放信号量并把锁解开,而挂起在锁上的第一个等待线程将被唤醒从而获得临界区的访问权。

    因为信号量的值始终在1和0之间变动,所以这类锁也叫做坐二值信号量。如锁所示:

     中断与线程的同步

    信号量也能够方便的应用于中断与线程间的同步,例如一个中断触发,中断服务例程需要通知线程进行相应的数据处理。这个时候可以设置信号量的初始值是0,线程在试图持有这个信号量时,由于信号量的初始值是0,线程直接在这个信号量上挂起直到信号量被释放。当中断触发时,先进行与硬件相关的动作,例如从硬件的I/O口中直接读取相应的数据,并确认中断以清除中断源,而后释放一个信号量来唤醒相应的线程以做后续的处理。例如finsh shell线程的处理方式,如图finsh shell的中断、线程间同步所示:

    警告:中断与线程间的互斥不能采用信号量(锁)的方式,而应采用中断锁。

    资源计数

    资源计数适合于线程间速度不匹配的场合,这个时候信号量可以作为前一线工作完成的计数,而当调度到后一线程时,它可以以一种连续的方式一次处理数个事件。例如生产者与消费者的问题中,生产者可以对信号进行多次释放,而后消费者被调度到时能够一次处理多个资源。

    注:一般资源计数类型多是混合方式的线程间同步,因为对于单个的资源处理依然存在线程的多重访问,这就需要对一个单独的资源进行访问、处理,并进行锁方式的互斥操作。

    信号量实验的代码和流程分析

    1. /**
    2. 信号量例程
    3. 程序内容:创建一个动态信号量,初始化两个线程,线程1count每计数10
    4. 发送一个信号量,线程2在接收信号量后,对number进行加1操作
    5. */
    6. #include <rtthread.h>
    7. #define THREAD_PRIORITY 20
    8. #define THREAD_STACK_SIZE 512
    9. #define THREAD_TIMESLICE 30
    10. #define s 1
    11. /*计数型信号量实验*/
    12. #ifdef s
    13. static rt_sem_t dynamic_sem = RT_NULL;
    14. ALIGN(RT_ALIGN_SIZE)
    15. static rt_uint8_t thread1_stack[512];
    16. static struct rt_thread thread1;
    17. /*释放信号量,每10次释放一次信号量,计数100后线程执行完成退出*/
    18. static void thread1_entry(void *parameter)
    19. {
    20. static rt_uint8_t count = 0;
    21. while(1)
    22. {
    23. if(count<100)
    24. {
    25. count++;
    26. }
    27. else
    28. return;
    29. if(count%10 == 0) /*10次释放一次信号量*/
    30. {
    31. rt_kprintf("thread1 release a dynamic semaphore.\n");
    32. rt_sem_release(dynamic_sem);
    33. }
    34. }
    35. }
    36. ALIGN(RT_ALIGN_SIZE)
    37. static rt_uint8_t thread2_stack[512];
    38. static struct rt_thread thread2;
    39. /*获取信号量*/
    40. static void thread2_entry(void *parameter)
    41. {
    42. static rt_err_t result;
    43. static rt_uint8_t number = 0;
    44. while(1)
    45. {
    46. result = rt_sem_take(dynamic_sem,RT_WAITING_FOREVER); /*获取信号量*/
    47. if(result == RT_EOK)
    48. {
    49. number++;
    50. rt_kprintf("thread2 take a dynamic semaphore, number = %d\n",number);
    51. }
    52. else
    53. {
    54. rt_kprintf("thread2 take a dynamic semaphore failed\n");
    55. rt_sem_delete(dynamic_sem);
    56. return;
    57. }
    58. }
    59. }
    60. int sem_sample(void)
    61. {
    62. rt_err_t result;
    63. /*动态创建信号量*/
    64. dynamic_sem = rt_sem_create("dynamic_sem",
    65. 0,
    66. RT_IPC_FLAG_FIFO
    67. );
    68. if(dynamic_sem != RT_NULL)
    69. {
    70. rt_kprintf("信号量创建成功!\n");
    71. }
    72. /*创建释放信号量线程1*/
    73. result = rt_thread_init(&thread1,
    74. "thread1",
    75. thread1_entry,
    76. RT_NULL,
    77. &thread1_stack[0],
    78. sizeof(thread1_stack),
    79. THREAD_PRIORITY,
    80. THREAD_TIMESLICE
    81. );
    82. if(result == RT_EOK)
    83. rt_thread_startup(&thread1); /*启动线程1*/
    84. /*创建获取信号量线程2*/
    85. result = rt_thread_init(&thread2,
    86. "thread2",
    87. thread2_entry,
    88. RT_NULL,
    89. &thread2_stack[0],
    90. sizeof(thread2_stack),
    91. THREAD_PRIORITY,
    92. THREAD_TIMESLICE
    93. );
    94. if(result == RT_EOK)
    95. rt_thread_startup(&thread2); /*启动线程2*/
    96. return 0;
    97. }
    98. #else
    99. /*二值信号量同步实验*/
    100. static rt_thread_t receive_thread = RT_NULL;
    101. static rt_thread_t send_thread = RT_NULL;
    102. static rt_sem_t test_sem = RT_NULL;
    103. rt_uint8_t ucvalue[2] = {0x00,0x00};
    104. static void receive_thread_entry(void *parameter)
    105. {
    106. while(1)
    107. {
    108. rt_sem_take(test_sem,RT_WAITING_FOREVER);
    109. if(ucvalue[0] == ucvalue[1])
    110. {
    111. rt_kprintf("同步成功!\n");
    112. rt_kprintf("ucvalue[0] = %d,ucvalue[1] = %d\n",ucvalue[0],ucvalue[1]);
    113. }
    114. else
    115. rt_kprintf("同步失败!\n");
    116. rt_sem_release(test_sem);
    117. rt_thread_delay(100);
    118. }
    119. }
    120. static void send_thread_entry(void *parameter)
    121. {
    122. while(1)
    123. {
    124. rt_sem_take(test_sem,RT_WAITING_FOREVER);
    125. ucvalue[0]++;
    126. rt_kprintf("ucvalue[0] = %d\n",ucvalue[0]);
    127. rt_thread_mdelay(100);
    128. ucvalue[1]++;
    129. rt_kprintf("ucvalue[1] = %d\n",ucvalue[1]);
    130. rt_sem_release(test_sem);
    131. rt_thread_yield();
    132. }
    133. }
    134. int sem_sample(void)
    135. {
    136. test_sem = rt_sem_create("test_sem",1,RT_IPC_FLAG_FIFO);
    137. if(test_sem!= RT_NULL)
    138. rt_kprintf("信号量创建成功!\n");
    139. receive_thread = rt_thread_create("receive_thread",
    140. receive_thread_entry,
    141. RT_NULL,
    142. THREAD_STACK_SIZE,
    143. THREAD_PRIORITY,
    144. THREAD_TIMESLICE
    145. );
    146. if(receive_thread!=RT_NULL)
    147. rt_thread_startup(receive_thread);
    148. send_thread = rt_thread_create("send_thread",
    149. send_thread_entry,
    150. RT_NULL,
    151. THREAD_STACK_SIZE,
    152. THREAD_PRIORITY,
    153. THREAD_TIMESLICE
    154. );
    155. if(send_thread != RT_NULL)
    156. rt_thread_startup(send_thread);
    157. return 0;
    158. }
    159. #endif
    160. MSH_CMD_EXPORT(sem_sample,sem_sample);

    个人总结:

    信号量是一种轻型的用于解决线程间同步问题的内核对象,线程可以获取或释放它,从而达到同步或互斥的目的。信号量就像一把钥匙,把一段临界区给锁住,只允许有钥匙的线程进行访问,线程拿到了钥匙,才允许它进入临界区,而离开后把钥匙传递给排队在后面的等待线程,让后续线程一次进入临界区。

  • 相关阅读:
    微服务-sentinel详解
    【python】(十六)python内置库——logging
    口袋参谋:找关键词的三种方法!
    openwrt (一):特殊的WiFi驱动移植方法
    JVM 类加载机制
    android 性能优化之内存泄漏
    KeepAlived搭建高可用的HAproxy负载均衡集群系统
    使用 NestJs 进行错误处理
    mysql主从复制与读写分离
    G1D13-Apt论文阅读&fraud&git&KGbook&rce33-36&php环境搭建
  • 原文地址:https://blog.csdn.net/qq_43460068/article/details/127112499