RT-Thread|RT-Thread记录(六、IPC机制之信号量、互斥量和事件集)

上文说到 RT-Thread 对临界区的处理方式有多种,其中已经分析了关闭调度器和屏蔽中断的方式, 本文就来学学另外的线程同步方式。


目录
  • 前言
  • 一、IPC机制
  • 二、信号量
    • 2.1 信号量控制块
    • 2.2 信号量操作
      • 2.2.1 创建和删除
      • 2.2.2 初始化和脱离
      • 2.2.3 获取信号量
      • 2.2.4 释放信号量
    • 2.3 示例(典型停车场模型)
  • 三、互斥量
    • 3.1 优先级翻转
    • 3.2 优先级继承
    • 3.3 互斥量控制块
    • 3.4 互斥量操作
      • 3.2.1 创建和删除
      • 3.2.2 初始化和脱离
      • 3.2.3 获取互斥量
      • 3.2.4 释放互斥量
    • 3.5 示例(优先级继承)
  • 四、事件集
    • 4.1 事件集控制块
    • 4.2 事件集操作
      • 4.2.1 创建和删除
      • 4.2.2 初始化和脱离
      • 4.2.3 发送事件
      • 4.2.4 接收事件
    • 4.3 示例(逻辑与和逻辑或)
  • 结语

前言 在我们专栏前面的文章中,已经学习过 RT-Thread 线程操作函数、软件定时器、临界区的保护,我们都进行了一些底层的分析,能让我们更加理解 RT-Thread 的内核,但是也不要忽略了上层的函数使用 要理解 RT-Thread 面向对象的思想,对所有的这些线程啊,定时器,包括要介绍的信号量,邮箱这些,都是以 对象 来操作,直白的说来就是 对于所有这些对象,都是以结构体的形式来表示,然后通过对这个对象结构体的操作来进行的。
本文所要介绍的内容属于 IPC机制,这些内容相对来说比较简单,我们重点在于学会如何使用以及了解他们的使用场合。
本 RT-Thread 专栏记录的开发环境:
RT-Thread记录(一、RT-Thread 版本、RT-Thread Studio开发环境 及 配合CubeMX开发快速上手)

一、IPC机制 在嵌入式操作系统中,运行代码主要包括线程 和 ISR,在他们的运行过程中,因为应用或者多线程模型带来的需求,有时候需要同步,有时候需要互斥,有时候也需要彼此交换数据。操作系统必须提供相应的机制来完成这些功能,这些机制统称为 线程间通信(IPC机制)。
本文所要介绍的就是关于线程同步的信号量、互斥量、事件 也属于 IPC机制。
RT-Thread 中的 IPC机制包括信号量、互斥量、事件、邮箱、消息队列。对于学习 RT-Thread ,这些IPC机制我们必须要学会灵活的使用。
为什么要说一下这个IPC机制?
我们前面说到过,RT-Thread 面向对象的思想,所有的这些 IPC 机制都被当成一个对象,都有一个结构体控制块,我们用信号量结构体来看一看:
RT-Thread|RT-Thread记录(六、IPC机制之信号量、互斥量和事件集)
文章图片

Kernel object有哪些,我们可以从基础内核对象结构体定义下面的代码找到:
RT-Thread|RT-Thread记录(六、IPC机制之信号量、互斥量和事件集)
文章图片

本节说明了 RT-Thread 的 IPC 机制,同时通过 信号量的结构体控制块再一次的认识了 RT-Thread 面向对象的设计思想。
在我的 FreeRTOS 专栏中,对于FreeRTOS 的信号量,互斥量,事件集做过说明和测试。在这个部分,实际上 RT-Thread 与 FreeRTOS 是类似的,都是一样的思想。所以如果属熟悉FreeRTOS的话,这部分是简单的,我们要做的就是记录一下 对象的控制块,和操作函数,加以简单的示例测试。
二、信号量 信号量官方的说明是:信号量是一种轻型的用于解决线程间同步问题的内核对象,线程可以获取或释放它,从而达到同步或互斥的目的。
信号量非常灵活,可以使用的场合也很多:
  • 比如 一个典型的应用场合就是停车位模型,总共有多少个车位,就是多少个信号量,入口进入一辆车信号量-1,出口离开一辆车信号量+1。
  • 比如 两个线程之间的同步,信号量的值初始化成 0,而尝试获得该信号量的线程,一定需要等待另一个释放信号量的线程先执行完。
在 FreeRTOS 中存在二值信号量,但是 RT-Thread 中已经没有了,官方有说明:
RT-Thread|RT-Thread记录(六、IPC机制之信号量、互斥量和事件集)
文章图片

信号量记住一句话基本就可以,释放一次信号量就+1,获取一次就-1,如果信号量数据为0,那么尝试获取的线程就会挂机,直到有线程释放信号量使得信号量大于0。
2.1 信号量控制块 老规矩用源码,解释看注释(使用起来也方便复制 ~ ~!):
#ifdef RT_USING_SEMAPHORE /** * Semaphore structure * value 信号量的值,直接表明目前信号量的数量 */ struct rt_semaphore { struct rt_ipc_object parent; /**< inherit from ipc_object */rt_uint16_tvalue; /**< value of semaphore. */ rt_uint16_treserved; /**< reserved field */ }; /* rt_sem_t 是指向 semaphore 结构体的指针类型 */ typedef struct rt_semaphore *rt_sem_t; #endif

2.2 信号量操作 2.2.1 创建和删除
同以前的线程那些一样,动态的方式,先定义一个信号量结构体的指针变量,接收创建好的句柄。
创建信号量:
/* 参数的含义: 1、name信号量名称 2、value信号量初始值 3、flag信号量标志,它可以取如下数值: RT_IPC_FLAG_FIFO 或 RT_IPC_FLAG_PRIO 返回值: 信号量创建成功,返回信号量的控制块指针 信号量创建失败,返回RT_BULL */ rt_sem_t rt_sem_create(const char *name, rt_uint32_t value, rt_uint8_t flag)

对于最后的参数 flag,决定了当信号量不可用时(就是当信号量为0的时候),多个线程等待的排队方式。只有RT_IPC_FLAG_FIFO (先进先出)或 RT_IPC_FLAG_PRIO(优先级等待)两种 flag。
关于用哪一个,要看具体的情况,官方有特意说明:
RT-Thread|RT-Thread记录(六、IPC机制之信号量、互斥量和事件集)
文章图片

删除信号量:
/* 参数: semrt_sem_create() 创建的信号量对象,信号量句柄 返回值: RT_EOK删除成功 */ rt_err_t rt_sem_delete(rt_sem_t sem)

2.2.2 初始化和脱离
静态的方式,先定义一个信号量结构体,然后对他进行初始化。
初始化信号量:
/** 参数的含义: 1、sem信号量对象的句柄,就是开始定义的信号量结构体变量 2、name信号量名称 3、value信号量初始值 4、flag信号量标志,它可以取如下数值: RT_IPC_FLAG_FIFO 或 RT_IPC_FLAG_PRIO 返回值: RT_EOK初始化成功 */ rt_err_t rt_sem_init(rt_sem_tsem, const char *name, rt_uint32_t value, rt_uint8_tflag)

脱离信号量:
/* 参数: sem信号量对象的句柄 返回值: RT_EOK脱离成功 */ rt_err_t rt_sem_detach(rt_sem_t sem);

2.2.3 获取信号量
当信号量值大于零时,线程将获得信号量,并且相应的信号量值会减 1。
/** 参数: 1、sem信号量对象的句柄 2、time指定的等待时间,单位是操作系统时钟节拍(OS Tick) 返回值: RT_EOK成功获得信号量 -RT_ETIMEOUT超时依然未获得信号量 -RT_ERROR其他错误 */ rt_err_t rt_sem_take(rt_sem_t sem, rt_int32_t time)

注意!要等待的时间是系统时钟节拍(OS Tick)。
无等待获取信号量:
//就是上面获取的等待时间为0的方式 rt_err_t rt_sem_trytake(rt_sem_t sem) { return rt_sem_take(sem, 0); }

当线程申请的信号量资源实例为0时,直接返回 - RT_ETIMEOUT。
2.2.4 释放信号量
释放信号量可以使得该信号量+1,如果有线程在等待这个信号量,可以唤醒这个线程。
/** 参数: sem信号量对象的句柄 返回值: RT_EOK成功释放信号量 */ rt_err_t rt_sem_release(rt_sem_t sem)

2.3 示例(典型停车场模型) 前面说到过,信号量非常灵活,可以使用的场合也很多,官方也有很多例子,我们这里做个典型的示例
— 停车场模型(前面用截图做解释,后面会附带源码)。
示例中,我们使用两个不同的按键来模拟车辆的进出,但是考虑到我们还没有学设备和驱动,没有添加按键驱动,所以我们用古老的方式来实现按键操作:
按键key3,代表车辆离开:
RT-Thread|RT-Thread记录(六、IPC机制之信号量、互斥量和事件集)
文章图片

按键key2,代表车辆进入:
RT-Thread|RT-Thread记录(六、IPC机制之信号量、互斥量和事件集)
文章图片

信号量的创建,初始10个车位:
RT-Thread|RT-Thread记录(六、IPC机制之信号量、互斥量和事件集)
文章图片

当然不要忘了,车辆进入和车辆离开(两个按键)是需要两个线程的。
我们来看看测试效果,说明如图:
RT-Thread|RT-Thread记录(六、IPC机制之信号量、互斥量和事件集)
文章图片

注意上图测试最后的细节,虽然 one car get out! 但是打印出来的停车位还是0,可以这么理解,key3_thread_entry线程释放了信号量以后还没来得及打印,等待信号量的线程key2_thread_entry就获取到了信号量。
具体的分析需要看rt_sem_release函数源码,里面会判断是否需要值+1,以及是否需要调度:
RT-Thread|RT-Thread记录(六、IPC机制之信号量、互斥量和事件集)
文章图片

附上上面测试代码:
/* * Copyright (c) 2006-2022, RT-Thread Development Team * * SPDX-License-Identifier: Apache-2.0 * * Change Logs: * DateAuthorNotes * 2022-02-16RT-Threadfirst version */#include #include "main.h" #include "usart.h" #include "gpio.h"#define DBG_TAG "main" #define DBG_LVL DBG_LOG #include static struct rt_thread led1_thread; //led1线程 static char led1_thread_stack[256]; static rt_thread_t led2_thread = RT_NULL; //led2线程static rt_thread_t key2_thread = RT_NULL; //static rt_thread_t key3_thread = RT_NULL; //rt_sem_t mysem; static void led1_thread_entry(void *par){ while(1){ LED1_ON; rt_thread_mdelay(1000); LED1_OFF; rt_thread_mdelay(1000); } }static void led2_thread_entry(void *par){ while(1){ LED2_ON; rt_thread_mdelay(500); LED2_OFF; rt_thread_mdelay(500); } }static void key2_thread_entry(void *par){ static rt_err_t result; while(1){ if(key2_read == 0){ rt_thread_mdelay(10); //去抖动 if(key2_read == 0){ result = rt_sem_take(mysem, 1000); if (result != RT_EOK) { rt_kprintf("the is no parking spaces now...\r\n"); } else { rt_kprintf("one car get in!,we have %d parking spaces now...\r\n",mysem->value); } while(key2_read == 0){rt_thread_mdelay(10); } } } rt_thread_mdelay(1); } }static void key3_thread_entry(void *par){ while(1){ if(key3_read == 0){ rt_thread_mdelay(10); //去抖动 if(key3_read == 0){ if(mysem->value < 10){ rt_sem_release(mysem); rt_kprintf("one car get out!,we have %d parking spaces now...\r\n",mysem->value); } while(key3_read == 0){rt_thread_mdelay(10); } //去抖动 } } rt_thread_mdelay(1); } } int main(void) { MX_GPIO_Init(); MX_USART1_UART_Init(); rt_err_t rst2; rst2 = rt_thread_init(&led1_thread, "led1_blink ", led1_thread_entry, RT_NULL, &led1_thread_stack[0], sizeof(led1_thread_stack), RT_THREAD_PRIORITY_MAX -1, 50); if(rst2 == RT_EOK){ rt_thread_startup(&led1_thread); }mysem = rt_sem_create("my_sem1", 10, RT_IPC_FLAG_FIFO); if(RT_NULL == mysem){ LOG_E("create sem failed!...\n"); } else LOG_D("we have 10 parking spaces now...\n"); key2_thread = rt_thread_create("key2_control", key2_thread_entry, RT_NULL, 512, RT_THREAD_PRIORITY_MAX -2, 50); /* 如果获得线程控制块,启动这个线程 */ if (key2_thread != RT_NULL) rt_thread_startup(key2_thread); key3_thread = rt_thread_create("key3_control", key3_thread_entry, RT_NULL, 512, RT_THREAD_PRIORITY_MAX -2, 50); /* 如果获得线程控制块,启动这个线程 */ if (key3_thread != RT_NULL) rt_thread_startup(key3_thread); return RT_EOK; }void led2_Blink(){ led2_thread = rt_thread_create("led2_blink", led2_thread_entry, RT_NULL, 256, RT_THREAD_PRIORITY_MAX -1, 50); /* 如果获得线程控制块,启动这个线程 */ if (led2_thread != RT_NULL) rt_thread_startup(led2_thread); }MSH_CMD_EXPORT(led2_Blink, Led2 sample);

三、互斥量 互斥量是一种特殊的二值信号量。互斥量的状态只有两种,开锁或闭锁(两种状态值)。
互斥量支持递归,持有该互斥量的线程也能够再次获得这个锁而不被挂起。自己能够再次获得互斥量。
互斥量可以解决优先级翻转问题,它能够实现优先级继承。
互斥量互斥量不能在中断服务例程中使用。
3.1 优先级翻转 优先级翻转,我在以前 FreeRTOS 专栏写过:
RT-Thread|RT-Thread记录(六、IPC机制之信号量、互斥量和事件集)
文章图片

再用官方的图加深理解:
RT-Thread|RT-Thread记录(六、IPC机制之信号量、互斥量和事件集)
文章图片

3.2 优先级继承 优先级继承,我在以前 FreeRTOS 专栏也写过:
RT-Thread|RT-Thread记录(六、IPC机制之信号量、互斥量和事件集)
文章图片

再用官方的图加深理解:
RT-Thread|RT-Thread记录(六、IPC机制之信号量、互斥量和事件集)
文章图片

需要切记的是互斥量不能在中断服务例程中使用。
3.3 互斥量控制块
#ifdef RT_USING_MUTEX /** * Mutual exclusion (mutex) structure * parent继承ipc类 * value互斥量的值 * original_priority持有线程的原始优先级 * hold持有线程的持有次数,可以多次获得 * *owner当前拥有互斥量的线程 */ struct rt_mutex { struct rt_ipc_object parent; /**< inherit from ipc_object */ rt_uint16_tvalue; /**< value of mutex */ rt_uint8_toriginal_priority; /**< priority of last thread hold the mutex */ rt_uint8_thold; /**< numbers of thread hold the mutex */struct rt_thread*owner; /**< current owner of mutex */ }; /* rt_mutext_t 为指向互斥量结构体的指针类型*/ typedef struct rt_mutex *rt_mutex_t; #endif

3.4 互斥量操作 3.2.1 创建和删除
先定义一个指向互斥量结构体的指针变量,接收创建好的句柄。
创建互斥量:
/** 参数的含义: 1、name互斥量名称 2、flag该标志已经作废,无论用户选择 RT_IPC_FLAG_PRIO 还是 RT_IPC_FLAG_FIFO, 内核均按照 RT_IPC_FLAG_PRIO 处理 返回值: 互斥量创建成功,返回互斥量的控制块指针 互斥量创建失败,返回RT_BULL */ rt_mutex_t rt_mutex_create(const char *name, rt_uint8_t flag)

删除互斥量:
/** 参数: mutex 互斥量对象的句柄 返回值: RT_EOK删除成 */ rt_err_t rt_mutex_delete(rt_mutex_t mutex)

3.2.2 初始化和脱离
静态的方式,先定义一个互斥量结构体,然后对他进行初始化。
初始化互斥量:
/** 参数的含义: 1、mutex 互斥量对象的句柄,指向互斥量对象的内存块,开始定义的结构体 2、name互斥量名称 3、flag该标志已经作废,按照 RT_IPC_FLAG_PRIO (优先级)处理 返回值: RT_EOK初始化成功 */ rt_err_t rt_mutex_init(rt_mutex_t mutex, const char *name, rt_uint8_t flag)

脱离互斥量:
/** 参数: mutex 互斥量对象的句柄 返回值: RT_EOK成功 */ rt_err_t rt_mutex_detach(rt_mutex_t mutex)

3.2.3 获取互斥量
一个时刻一个互斥量只能被一个线程持有。
如果互斥量没有被其他线程控制,那么申请该互斥量的线程将成功获得该互斥量。如果互斥量已经被当前线程线程控制,则该互斥量的持有计数加 1,当前线程也不会挂起等待。
/** 参数: 1、mutex 互斥量对象的句柄 2、time指定的等待时间,单位是操作系统时钟节拍(OS Tick) 返回值: RT_EOK成功获得互斥量 -RT_ETIMEOUT超时依然未获得互斥量 -RT_ERROR获取失败 */ rt_err_t rt_mutex_take(rt_mutex_t mutex, rt_int32_t time)

3.2.4 释放互斥量
在获得互斥量后,应该尽可能的快释放互斥量。
/** 参数: mutex互斥量对象的句 返回值: RT_EOK成功 */ rt_err_t rt_mutex_release(rt_mutex_t mutex)

3.5 示例(优先级继承) 互斥量做一个简单的示例,但是即便简单,也能体现出优先级继承这个机制。
示例中,我们使用两个按键,key2按键,按一次获取互斥量,再按一次释放互斥量,打印自己初始优先级,当前优先级,互斥量占有线程优先级这几个量。key3按键,按一次,获取互斥量,立马就释放,也打印几个优先级。
互斥量的创建,和两个线程的优先级:
RT-Thread|RT-Thread记录(六、IPC机制之信号量、互斥量和事件集)
文章图片

key2操作:
RT-Thread|RT-Thread记录(六、IPC机制之信号量、互斥量和事件集)
文章图片

key3操作:
RT-Thread|RT-Thread记录(六、IPC机制之信号量、互斥量和事件集)
文章图片

测试结果说明图:
RT-Thread|RT-Thread记录(六、IPC机制之信号量、互斥量和事件集)
文章图片

示例中为了更好的演示并没有快进快出,实际使用还是需要快进快出,除非你自己就是有这种特出需求。
还有一个细节,就是 RT-Thread 中对象的 名字,只能显示8个字符长度,长了会截断,并不影响使用。
四、事件集 事件集这部分与 FreeRTOS 基本一样。
事件集主要用于线程间的同步,它的特点是可以实现一对多,多对多的同步。即一个线程与多个事件的关系可设置为:其中任意一个事件唤醒线程,或几个事件都到达后才唤醒线程进行后续的处理;同样,事件也可以是多个线程同步多个事件。
RT-Thread 定义的事件集有以下特点:
  • 事件只与线程相关,事件间相互独立:每个线程可拥有 32 个事件标志,采用一个 32 bit 无符号整型数进行记录,每一个 bit 代表一个事件;
  • 事件仅用于同步,不提供数据传输功能;
  • 事件无排队性,即多次向线程发送同一事件 (如果线程还未来得及读走),其效果等同于只发送一次。
4.1 事件集控制块
#ifdef RT_USING_EVENT /** * flag defintions in event * 逻辑与 * 逻辑或 * 清除标志位 */ #define RT_EVENT_FLAG_AND0x01/**< logic and */ #define RT_EVENT_FLAG_OR0x02/**< logic or */ #define RT_EVENT_FLAG_CLEAR0x04/**< clear flag *//* * event structure * set:事件集合,每一 bit 表示 1 个事件,bit 位的值可以标记某事件是否发生 */ struct rt_event { struct rt_ipc_object parent; /**< inherit from ipc_object */rt_uint32_tset; /**< event set */ }; /* rt_event_t 是指向事件结构体的指针类型*/ typedef struct rt_event *rt_event_t; #endif

4.2 事件集操作 4.2.1 创建和删除
先定义一个指向事件集结构体的指针变量,接收创建好的句柄。
创建事件集:
/** 参数的含义: 1、name事件集的名称 2、flag事件集的标志,它可以取如下数值: RT_IPC_FLAG_FIFO 或 RT_IPC_FLAG_PRIO理 返回值: 事件集创建成功,返回事件集的控制块指针 事件集创建失败,返回RT_BULL */ rt_event_t rt_event_create(const char *name, rt_uint8_t flag)

flag 使用哪一个,解释和信号量一样,可参考信号量创建部分说明。
删除事件集:
/** 参数: event 事件集对象的句柄 返回值: RT_EOK成功 */ rt_err_t rt_event_delete(rt_event_t event)

4.2.2 初始化和脱离
静态的方式,先定义一个事件集结构体,然后对他进行初始化。
初始化事件集:
/** 参数的含义: 1、event 事件集对象的句柄 2、name事件集的名称 3、flag事件集的标志,它可以取如下数值: RT_IPC_FLAG_FIFO 或 RT_IPC_FLAG_PRIO 返回值: RT_EOK初始化成功 */ rt_err_t rt_event_init(rt_event_t event, const char *name, rt_uint8_t flag)

脱离事件集:
/** 参数: event 事件集对象的句柄 返回值: RT_EOK成功 */ rt_err_t rt_event_detach(rt_event_t event)

4.2.3 发送事件
发送事件函数可以发送事件集中的一个或多个事件。
/** 参数的含义: 1、event 事件集对象的句柄 2、set发送的一个或多个事件的标志值 返回值: RT_EOK成功 */ rt_err_t rt_event_send(rt_event_t event, rt_uint32_t set)

4.2.4 接收事件
内核使用 32 位的无符号整数来标识事件集,它的每一位代表一个事件,因此一个事件集对象可同时等待接收 32 个事件,内核可以通过指定选择参数 “逻辑与” 或“逻辑或”来选择如何激活线程。
/** 参数的含义: 1、event事件集对象的句柄 2、set接收线程感的事件 3、option接收选项,可取的值为 #define RT_EVENT_FLAG_AND0x01逻辑与 #define RT_EVENT_FLAG_OR0x02逻辑或 #define RT_EVENT_FLAG_CLEAR0x04选择清除重置事件标志位 4、timeout指定超时时间 5、recved指向接收到的事件,如果不在意,可以使用 NULL 返回值: RT_EOK成功 -RT_ETIMEOUT超时 -RT_ERROR错误 */ rt_err_t rt_event_recv(rt_event_tevent, rt_uint32_tset, rt_uint8_toption, rt_int32_ttimeout, rt_uint32_t *recved)

4.3 示例(逻辑与和逻辑或) 事件集通过示例可以很好的理解怎么使用,我们示例中,用按钮发送事件,其他线程接收事件,进行对应的处理。
按键操作:
RT-Thread|RT-Thread记录(六、IPC机制之信号量、互斥量和事件集)
文章图片

线程逻辑或处理:
RT-Thread|RT-Thread记录(六、IPC机制之信号量、互斥量和事件集)
文章图片

逻辑或测试结果:
RT-Thread|RT-Thread记录(六、IPC机制之信号量、互斥量和事件集)
文章图片

线程逻辑与处理:
RT-Thread|RT-Thread记录(六、IPC机制之信号量、互斥量和事件集)
文章图片

逻辑与测试结果:
RT-Thread|RT-Thread记录(六、IPC机制之信号量、互斥量和事件集)
文章图片

结语 前面说过,RT-Thread 的这些机制与 FreeRTOS 大体上类似,如果对 FreeRTOS 这部分感兴趣的,可以看一下 FreeRTOS 这部分的博文:
FreeRTOS记录(七、FreeRTOS信号量、事件标志组、邮箱和消息队列、任务通知的关系)
本文虽然只是介绍了信号量、互斥量和事件集这几个比较简单的线程同步操作,但是最终完成了后发现内容还是很多的。
洋洋洒洒这么多字,最终看下来自己还是挺满意的,希望我把该表述的都表达清楚了,希望大家多多提意见,让博主能给大家带来更好的文章。
那么下一篇的 RT-Thread 记录,就要来说说与线程通讯 有关的 邮箱、消息队列和信号内容了。
【RT-Thread|RT-Thread记录(六、IPC机制之信号量、互斥量和事件集)】谢谢!

    推荐阅读