【IPC工作方式】线程间通信详解:互斥量、信号量与RTT操作系统内核源码解析
IPC工作方式分析
实际应用中,需要在线程与线程间,线程与中断之间进行一定的数据通信,
主要分为两种:
因此,ipc大致可以分为两种:一种是线程间通信;一种是线程间同步。
- 线程间通信:邮箱,消息队列。
- 线程间同步:信号量、互斥量、事件集。
邮箱:
邮箱一次仅可传输4个字节数据内容(规定),在32位系统中,4个字节相当于一个指针的大小。
在需要传输较多数据时,创建结构体,使用指针指向的地址的方式传输。
发送和接受都支持阻塞机制。
线程和终端都可以发送邮件,但是中断服务函数不允许任何阻塞,中断发送邮件必须选择非阻塞方式。
终端服务函数不能接收邮件,因为中断不可阻塞。
多个线程可以向同一个邮箱发送和接收邮件。
一个线程可以从任何一个邮箱中接收和发送邮件。
支持FIFO,PRIO两种阻塞唤醒方式。
邮箱中有entery表示邮箱可用邮件数,in_offset,out_offset指针指向邮件存放地址偏移量,指针自加偏移4个字节。初始化为0,线程发送一个邮件到邮箱,entery + 1, in_offset +1, 若邮箱满了则in_offset清0,线程从邮箱接收一个邮件,entery – 1, offset + 1,邮箱空了则in_offset清零。
若邮箱中有邮件,接收线程接收时,其实是复制邮箱中的4字节地址的内容到接收地址中。
如果邮箱发送的是地址,则注定这个地址不能是局部变量的地址,所以发送的邮件不要用局部变量,局部变量如果发送了,那在接受时存放的是这个局部变量的指针,但是可能在接收完这个局部变量时就已经给释放掉了,这个时候就会存在野指针。
邮箱一般传地址的多,所以最好定义全局变量。消息传数据多,不限制局部变量或全局变量,但是也要放心传地址的情况。
消息队列:
消息队列是邮箱的
支持发送紧急消息,将消息放在队列的头部,(正常是放在尾部)。
当空闲消息链表上有空闲消息块时,线程或中断将消息复制到消息块上,然后将该消息块挂载在消息队列的尾部。如果无空闲消息块,要发送消息的线程和线程会收到错误码。
信号量:
信号量是实现线程间通信的机制,实现线程间同步或者临界资源的互斥访问
(裸机逻辑:在中断中进行标记,在退出中断之后进行轮询处理)
RTT信号量控制块:
信号量属于内核对象,也在自身结构中包含内核对象类型的成员,通过该成员将信号量挂到系统对象容器里面。信号量由IPC容器管理。
信号量创建函数(create属于动态创建信号量方式)
RT提供两种信号量创建的方式,对应的初始化函数调用好像也有两种方式,一种是动态创建和初始化,一种是静态创建和初始化。
信号量的名称
信号量的值:二值信号量和计数信号量
信号量唤醒模式:RT_IPC_FLAG_PRIO与RT_IPC_FLAG_FIFO(在rtdef.h中定义)
RT_IPC_FLAG_PRIO:优先级flag创建IPC对象
RT_IPC_FLAG_FIFO:先进先出flag创建IPC对象。
静态创建信号量方式:rt_sem_create()
·动态创建方式需要为信号量分配内存空间,信号量也属于内核对象,所以使用rt_object_create()函数进行。
·然后进行初始化,继续使用内核对象初始化函数进行初始化。
·内核对象初始化即将内核对象添加到阻塞列表中。
信号量获取函数
rt提供多种信号量获取方式,基本最后都回归到rt_sem_take函数,只是针对了不同方式进行了相关的封装
下图是rt_sem_take函数源码
大致分为三个主要步骤:
- 获取信号量时信号量有效,直接进行sem->value自减
- 获取信号量时信号量不可用,但是阻塞时间为零
- 获取信号量时信号量不可用,但是设定了阻塞时间
- 设定了阻塞时间的信号量阻塞应该是进行忙等待,将对应的线程添加到阻塞列表,开启线程定时器并进行开启线程定时器。
信号量释放函数
信号量释放过程源码如上图所示:
- 第一部分,阻塞队列中有阻塞线程,需要将其从链表中移除,然后标记任务切换的标记值,
- 第二部分,如果没有阻塞线程,释放信号量的过程即自加信号量的值,在进行一轮判断,如果信号量值溢出,则报错。如果没有溢出则自加
- 判断是否需要进行任务切换。
从阻塞列表中移除调用rt_susp_list_dequeue()函数。
获取调度锁 -> 获取阻塞链表中的线程 -> 恢复线程 -> 解锁调度锁 -> 日志记录。
信号量删除(动态:rt_sem_delet())
检查参数有效性
确定没有在中断服务函数中执行
-
恢复所有因信号量阻塞的线程
调用rt_susp_list_resume_all(任务上下文,或者在这里可以称之为线程上下文)
调用rt_susp_list_resume_all_irq(中断上下文,在恢复线程中同时考虑中断安全性问题)
-
删除信号量的内核对象
静态方式和动态方式的信号量删除
信号量控制函数
各函数间的调用关系总览:
互斥量
互斥量存在主要解决竞争互斥中可能存在的优先级翻转现象。
互斥量的使用场景,更适合于
信号量和互斥量的动态创建和分配,都基于开启RT_USING_HEAP宏
互斥量和信号量一样,都有两种各自不同的创建方式,分别为动态方式和静态方式。
互斥量控制块
rt_mutex结构体
父对象
优先级上限
优先级
持有互斥量的线程
互斥量当前的拥有者
线程所在的对象链表
互斥量的动态创建方式
- 按照内核对象的通用方式动态分配内核对象的空间
- 按照内核对象的通用方式进行互斥量的初始化
- 初始化互斥量的相应字段
- 互斥量的链表的初始化
- 互斥量的锁初始化
静态创建方式
省去了rt_object_allocate()的动态分配空间。然后使用rt_object_init()初始化互斥量的结构体
从第二步骤开始,动态方式创建和静态方式创建的初始化方式几乎没有区别。
互斥量删除方式也对应两种
针对动态创建的互斥量,rt_mutex_create()对应rt_mutex_delete()
互斥量获取函数
主要流程主要有以下几个情形:
- 互斥量的持有者就是当前线程,并且当先互斥量的累计值没有超过最大值,这里直接自加
- 互斥量的持有者不是当前线程,但是互斥量暂时没有持有者,这里只需要按照初始化的过程将互斥量的持有者相关设置为当前线程。
- 互斥量的持有者不是当前线程,也就是说当前互斥量的持有者是其他线程。
再分为两种情况:
a)设置阻塞时间为0,则直接返回错误值。
b)设置了阻塞时间大于0的情况,
将当前线程添加到阻塞队列
设置当前线程的阻塞对象为互斥量
获取当先线程的优先级
判断是否出现优先级翻转现象,
RTT的线程优先级是数值越小,优先级越高
如果当前线程比互斥量的优先级高,则需要更新当前互斥量的优先级。
如果当前视图获取互斥量的线程的优先级大于互斥量持有者优先级,那么发生了优先级翻转现象,则需要进行优先级更新。
需要设定阻塞时间的倒计时实现:设置线程定时器进行倒计时,开始定时器进行倒计时。
然后继续执行后续的逻辑:
1)能够成功获得互斥量
2)互斥量被获取,阻塞线程获取到互斥量之后从阻塞列表中移除,并且标记需要后续进行更新。
更新互斥量的优先级更新为当前线程的优先级
根据之前的标记需要更新互斥量的持有者的优先级。
互斥量的释放
首先互斥量的释放必须是当前持有信号量的线程释放
然后将信号量的持有者的值进行自减
如果已经没有线程继续持有互斥量
调用_check_and_update_prio()判断是否需要进行线程优先级更新
该函数更新线程优先级的过程即为当前持有互斥量的线程最高级并修改
线程优先级更新完成之后,因为当前互斥量没有被持有,需要唤醒线程的挂起链表
将待唤醒的线程从挂起链表中移除,并且添加到就绪列表
设置新的互斥量持有者,将线程的对象添加到互斥量的获取列表
互斥量的删除函数
依旧是根据创建互斥量的方式,对应着有两种不同的互斥量删除方式,大致方式是相同的,一种是静态方式,一种是动态方式。
互斥量控制函数
暂时没有控制功能实现
互斥量的优先级天花板获取
互斥量的优先级天花板设置
从互斥量的挂起链表中恢复一个线程
作者:Stay_Hun_forward