OpenHarmony设备开发 小型系统内核(LiteOS-A) 内核通信机制(下)
版本:V3.2Beta
消息队列
基本概念
队列又称消息队列,是一种常用于任务间通信的数据结构。队列接收来自任务或中断的不固定长度消息,并根据不同的接口确定传递的消息是否存放在队列空间中。
任务能够从队列里面读取消息,当队列中的消息为空时,挂起读取任务;当队列中有新消息时,挂起的读取任务被唤醒并处理新消息。任务也能够往队列里写入消息,当队列已经写满消息时,挂起写入任务;当队列中有空闲消息节点时,挂起的写入任务被唤醒并写入消息。
可以通过调整读队列和写队列的超时时间来调整读写接口的阻塞模式,如果将读队列和写队列的超时时间设置为0,就不会挂起任务,接口会直接返回,这就是非阻塞模式。反之,如果将读队列和写队列的超时时间设置为大于0的时间,就会以阻塞模式运行。
消息队列提供了异步处理机制,允许将一个消息放入队列,但不立即处理。同时队列还有缓冲消息的作用,可以使用队列实现任务异步通信,队列具有如下特性:
- 消息以先进先出的方式排队,支持异步读写。
- 读队列和写队列都支持超时机制。
- 每读取一条消息,就会将该消息节点设置为空闲。
- 发送消息类型由通信双方约定,可以允许不同长度(不超过队列的消息节点大小)的消息。
- 一个任务能够从任意一个消息队列接收和发送消息。
- 多个任务能够从同一个消息队列接收和发送消息。
- 创建队列时所需的队列空间,接口内系统自行动态申请内存。
运行机制
队列控制块
/**
* 队列控制块数据结构
*/
typedef struct {
UINT8 *queueHandle; /**< Pointer to a queue handle */
UINT16 queueState; /**< Queue state */
UINT16 queueLen; /**< Queue length */
UINT16 queueSize; /**< Node size */
UINT32 queueID; /**< queueID */
UINT16 queueHead; /**< Node head */
UINT16 queueTail; /**< Node tail */
UINT16 readWriteableCnt[OS_QUEUE_N_RW]; /**< Count of readable or writable resources, 0:readable, 1:writable */
LOS_DL_LIST readWriteList[OS_QUEUE_N_RW]; /**< the linked list to be read or written, 0:readlist, 1:writelist */
LOS_DL_LIST memList; /**< Pointer to the memory linked list */
} LosQueueCB;
每个队列控制块中都含有队列状态,表示该队列的使用情况:
- OS_QUEUE_UNUSED:队列未被使用。
- OS_QUEUE_INUSED:队列被使用中。
队列运作原理
- 创建队列时,创建队列成功会返回队列ID。
- 在队列控制块中维护着一个消息头节点位置Head和一个消息尾节点位置Tail,用于表示当前队列中消息的存储情况。Head表示队列中被占用的消息节点的起始位置。Tail表示被占用的消息节点的结束位置,也是空闲消息节点的起始位置。队列刚创建时,Head和Tail均指向队列起始位置。
- 写队列时,根据readWriteableCnt[1]判断队列是否可以写入,不能对已满(readWriteableCnt[1]为0)队列进行写操作。写队列支持两种写入方式:向队列尾节点写入,也可以向队列头节点写入。尾节点写入时,根据Tail找到起始空闲消息节点作为数据写入对象,如果Tail已经指向队列尾部则采用回卷方式。头节点写入时,将Head的前一个节点作为数据写入对象,如果Head指向队列起始位置则采用回卷方式。
- 读队列时,根据readWriteableCnt[0]判断队列是否有消息需要读取,对全部空闲(readWriteableCnt[0]为0)队列进行读操作会引起任务挂起。如果队列可以读取消息,则根据Head找到最先写入队列的消息节点进行读取。如果Head已经指向队列尾部则采用回卷方式。
- 删除队列时,根据队列ID找到对应队列,把队列状态置为未使用,把队列控制块置为初始状态,并释放队列所占内存。
图1队列读写数据操作示意图
上图对读写队列做了示意,图中只画了尾节点写入方式,没有画头节点写入,但是两者是类似的。
开发指导
接口说明
功能分类 | 接口描述 |
创建/删除消息队列 | - LOS_QueueCreate:创建一个消息队列,由系统动态申请队列空间 - LOS_QueueDelete:根据队列ID删除一个指定队列 |
读/写队列(不带拷贝) | - LOS_QueueRead:读取指定队列头节点中的数据(队列节点中的数据实际上是一个地址) - LOS_QueueWrite:向指定队列尾节点中写入入参bufferAddr的值(即buffer的地址) - LOS_QueueWriteHead:向指定队列头节点中写入入参bufferAddr的值(即buffer的地址) |
读/写队列(带拷贝) | - LOS_QueueReadCopy:读取指定队列头节点中的数据 - LOS_QueueWriteCopy:向指定队列尾节点中写入入参bufferAddr中保存的数据 - LOS_QueueWriteHeadCopy:向指定队列头节点中写入入参bufferAddr中保存的数据 |
获取队列信息 | LOS_QueueInfoGet:获取指定队列的信息,包括队列ID、队列长度、消息节点大小、头节点、尾节点、可读节点数量、可写节点数量、等待读操作的任务、等待写操作的任务 |
开发流程
- 用LOS_QueueCreate创建队列。创建成功后,可以得到队列ID。
- 通过LOS_QueueWrite或者LOS_QueueWriteCopy写队列。
- 通过LOS_QueueRead或者LOS_QueueReadCopy读队列。
- 通过LOS_QueueInfoGet获取队列信息。
- 通过LOS_QueueDelete删除队列。
说明:
- 系统支持的最大队列数是指:整个系统的队列资源总个数,而非用户能使用的个数。例如:系统软件定时器多占用一个队列资源,那么用户能使用的队列资源就会减少一个。
- 创建队列时传入的队列名和flags暂时未使用,作为以后的预留参数。
- 队列接口函数中的入参timeOut是相对时间。
- LOS_QueueReadCopy和LOS_QueueWriteCopy及LOS_QueueWriteHeadCopy是一组接口,LOS_QueueRead和LOS_QueueWrite及LOS_QueueWriteHead是一组接口,每组接口需要配套使用。
- 鉴于LOS_QueueWrite和LOS_QueueWriteHead和LOS_QueueRead这组接口实际操作的是数据地址,用户必须保证调用LOS_QueueRead获取到的指针所指向的内存区域在读队列期间没有被异常修改或释放,否则可能导致不可预知的后果。
- LOS_QueueRead和LOS_QueueReadCopy接口的读取长度如果小于消息实际长度,消息将被截断。
- 鉴于LOS_QueueWrite和LOS_QueueWriteHead和LOS_QueueRead这组接口实际操作的是数据地址,也就意味着实际写和读的消息长度仅仅是一个指针数据,因此用户使用这组接口之前,需确保创建队列时的消息节点大小,为一个指针的长度,避免不必要的浪费和读取失败。
编程实例
实例描述
创建一个队列,两个任务。任务1调用写队列接口发送消息,任务2通过读队列接口接收消息。
- 通过LOS_TaskCreate创建任务1和任务2。
- 通过LOS_QueueCreate创建一个消息队列。
- 在任务1 SendEntry中发送消息。
- 在任务2 RecvEntry中接收消息。
- 通过LOS_QueueDelete删除队列。
编程示例
本演示代码在./kernel/liteos_a/testsuites/kernel/src/osTest.c中编译验证,在TestTaskEntry中调用验证入口函数ExampleQueue,
为方便用户观察,建议调用ExampleQueue前先调用 LOS_Msleep(5000) 进行短时间延时,避免其他打印过多。
示例代码如下:
#include "los_task.h"
#include "los_queue.h"
static UINT32 g_queue;
#define BUFFER_LEN 50
VOID SendEntry(VOID)
{
UINT32 ret = 0;
CHAR abuf[] = "test message";
UINT32 len = sizeof(abuf);
ret = LOS_QueueWriteCopy(g_queue, abuf, len, 0);
if(ret != LOS_OK) {
dprintf("send message failure, error: %x\n", ret);
}
}
VOID RecvEntry(VOID)
{
UINT32 ret = 0;
CHAR readBuf[BUFFER_LEN] = {0};
UINT32 readLen = BUFFER_LEN;
LOS_Msleep(1000);
ret = LOS_QueueReadCopy(g_queue, readBuf, &readLen, 0);
if(ret != LOS_OK) {
dprintf("recv message failure, error: %x\n", ret);
}
dprintf("recv message: %s\n", readBuf);
ret = LOS_QueueDelete(g_queue);
if(ret != LOS_OK) {
dprintf("delete the queue failure, error: %x\n", ret);
}
dprintf("delete the queue success!\n");
}
UINT32 ExampleQueue(VOID)
{
dprintf("start queue example\n");
UINT32 ret = 0;
UINT32 task1, task2;
TSK_INIT_PARAM_S initParam = {0};
ret = LOS_QueueCreate("queue", 5, &g_queue, 0, 50);
if(ret != LOS_OK) {
dprintf("create queue failure, error: %x\n", ret);
}
dprintf("create the queue success!\n");
initParam.pfnTaskEntry = (TSK_ENTRY_FUNC)SendEntry;
initParam.usTaskPrio = 9;
initParam.uwStackSize = LOSCFG_BASE_CORE_TSK_DEFAULT_STACK_SIZE;
initParam.pcName = "SendQueue";
LOS_TaskLock();
ret = LOS_TaskCreate(&task1, &initParam);
if(ret != LOS_OK) {
dprintf("create task1 failed, error: %x\n", ret);
LOS_QueueDelete(g_queue);
return ret;
}
initParam.pcName = "RecvQueue";
initParam.pfnTaskEntry = (TSK_ENTRY_FUNC)RecvEntry;
ret = LOS_TaskCreate(&task2, &initParam);
if(ret != LOS_OK) {
dprintf("create task2 failed, error: %x\n", ret);
LOS_QueueDelete(g_queue);
return ret;
}
LOS_TaskUnlock();
LOS_Msleep(5000);
return ret;
}
结果验证
编译运行得到的结果为:
start queue example
create the queue success!
recv message: test message
delete the queue success!
读写锁
基本概念
读写锁与互斥锁类似,可用来同步同一进程中的各个任务,但与互斥锁不同的是,其允许多个读操作并发重入,而写操作互斥。
相对于互斥锁的开锁或闭锁状态,读写锁有三种状态:读模式下的锁,写模式下的锁,无锁。
读写锁的使用规则:
- 保护区无写模式下的锁,任何任务均可以为其增加读模式下的锁。
- 保护区处于无锁状态下,才可增加写模式下的锁。
多任务环境下往往存在多个任务访问同一共享资源的应用场景,读模式下的锁以共享状态对保护区访问,而写模式下的锁可被用于对共享资源的保护从而实现独占式访问。
这种共享-独占的方式非常适合多任务中读数据频率远大于写数据频率的应用中,提高应用多任务并发度。
运行机制
相较于互斥锁,读写锁如何实现读模式下的锁及写模式下的锁来控制多任务的读写访问呢?
- 若A任务首次获取了写模式下的锁,有其他任务来获取或尝试获取读模式下的锁,均无法再上锁。
- 若A任务获取了读模式下的锁,当有任务来获取或尝试获取读模式下的锁时,读写锁计数均加一。
开发指导
接口说明
表1 读写锁模块接口
功能分类 | 接口描述 |
读写锁的创建和删除 | - LOS_RwlockInit:创建读写锁 - LOS_RwlockDestroy:删除指定的读写锁 |
读模式下的锁的申请 | - LOS_RwlockRdLock:申请指定的读模式下的锁 - LOS_RwlockTryRdLock:尝试申请指定的读模式下的锁 |
写模式下的锁的申请 | - LOS_RwlockWrLock:申请指定的写模式下的锁 - LOS_RwlockTryWrLock:尝试申请指定的写模式下的锁 |
读写锁的释放 | LOS_RwlockUnLock:释放指定读写锁 |
读写锁有效性判断 | LOS_RwlockIsValid:判断读写锁有效性 |
开发流程
读写锁典型场景的开发流程:
- 创建读写锁LOS_RwlockInit。
- 申请读模式下的锁LOS_RwlockRdLock或写模式下的锁LOS_RwlockWrLock。
申请读模式下的锁:
- 若无人持有锁,读任务可获得锁。
- 若有人持有锁,读任务可获得锁,读取顺序按照任务优先级。
- 若有人(非自己)持有写模式下的锁,则当前任务无法获得锁,直到写模式下的锁释放。
申请写模式下的锁:
- 若该锁当前没有任务持有,或者持有该读模式下的锁的任务和申请该锁的任务为同一个任务,则申请成功,可立即获得写模式下的锁。
- 若该锁当前已经存在读模式下的锁,且读取任务优先级较高,则当前任务挂起,直到读模式下的锁释放。
3.申请读模式下的锁和写模式下的锁均有三种:无阻塞模式、永久阻塞模式、定时阻塞模式,区别在于挂起任务的时间。
4.释放读写锁LOS_RwlockUnLock。
- 如果有任务阻塞于指定读写锁,则唤醒被阻塞任务中优先级高的,该任务进入就绪态,并进行任务调度;
- 如果没有任务阻塞于指定读写锁,则读写锁释放成功。
- 删除读写锁LOS_RwlockDestroy。
说明:
- 读写锁不能在中断服务程序中使用。
- LiteOS-A内核作为实时操作系统需要保证任务调度的实时性,尽量避免任务的长时间阻塞,因此在获得读写锁之后,应该尽快释放该锁。
- 持有读写锁的过程中,不得再调用LOS_TaskPriSet等接口更改持有读写锁任务的优先级
用户态快速互斥锁
基本概念
Futex(Fast userspace mutex,用户态快速互斥锁)是内核提供的一种系统调用能力,通常作为基础组件与用户态的相关锁逻辑结合组成用户态锁,是一种用户态与内核态共同作用的锁,例如用户态mutex锁、barrier与cond同步锁、读写锁。其用户态部分负责锁逻辑,内核态部分负责锁调度。
当用户态线程请求锁时,先在用户态进行锁状态的判断维护,若此时不产生锁的竞争,则直接在用户态进行上锁返回;反之,则需要进行线程的挂起操作,通过Futex系统调用请求内核介入来挂起线程,并维护阻塞队列。
当用户态线程释放锁时,先在用户态进行锁状态的判断维护,若此时没有其他线程被该锁阻塞,则直接在用户态进行解锁返回;反之,则需要进行阻塞线程的唤醒操作,通过Futex系统调用请求内核介入来唤醒阻塞队列中的线程。
运行机制
当用户态产生锁的竞争或释放需要进行相关线程的调度操作时,会触发Futex系统调用进入内核,此时会将用户态锁的地址传入内核,并在内核的Futex中以锁地址来区分用户态的每一把锁,因为用户态可用虚拟地址空间为1GiB,为了便于查找、管理,内核Futex采用哈希桶来存放用户态传入的锁。
当前哈希桶共有80个,
0-63号桶用于存放私有锁(以虚拟地址进行哈希),64-79号桶用于存放共享锁(以物理地址进行哈希),私有/共享属性通过用户态锁的初始化以及Futex系统调用入参确定。
Futex设计图
图1
如图1,每个futex哈希桶中存放被futex_list串联起来的哈希值相同的futex node,每个futex node对应一个被挂起的task,node中key值唯一标识一把用户态锁,具有相同key值的node被queue_list串联起来表示被同一把锁阻塞的task队列。
Futex有以下三种操作:
Futex模块接口
功能分类 | 接口名称 | 描述 |
设置线程等待 | OsFutexWait | 向Futex表中插入代表被阻塞的线程的node |
唤醒被阻塞线程 | OsFutexWake | 唤醒一个被指定锁阻塞的线程 |
调整锁的地址 | OsFutexRequeue | 调整指定锁在Futex表中的位置 |
说明:
Futex系统调用通常与用户态逻辑共同组成用户态锁,故推荐使用用户态POSIX接口的锁。
信号
基本概念
信号(signal)是一种常用的进程间异步通信机制,用软件的方式模拟中断信号,当一个进程需要传递信息给另一个进程时,则会发送一个信号给内核,再由内核将信号传递至指定进程,而指定进程不必进行等待信号的动作。
运行机制
信号的运作流程分为三个部分,如表1:
表1 信号的运作流程及相关接口(用户态接口)
功能分类 | 接口名称 | 描述 |
注册信号回调函数 | signal | 注册信号总入口及注册和去注册某信号的回调函数。 |
注册信号回调函数 | sigaction | 功能同signal,但增加了信号发送相关的配置选项,目前仅支持SIGINFO结构体中的部分参数。 |
发送信号 | kill pthread_kill raise alarm abort | 发送信号给某个进程或进程内发送消息给某线程,为某进程下的线程设置信号标志位。 |
触发回调 | 无 | 由系统调用与中断触发,内核态与用户态切换前会先进入用户态指定函数并处理完相应回调函数,再回到原用户态程序继续运行。 |
说明:
信号机制为提供给用户态程序进程间通信的能力,故推荐使用上表1列出的用户态POSIX相关接口。
注册回调函数:
void *signal(int sig, void (*func)(int))(int);
- 31 号信号,该信号用来注册该进程的回调函数处理入口,不可重复注册。
- 0-30 号信号,该信号段用来注册与去注册回调函数。
注册回调函数:
int sigaction(int, const struct sigaction *__restrict, struct sigaction *__restrict); 支持信号注册的配置修改和配置获取,目前仅支持SIGINFO的选项,SIGINFO内容见sigtimedwait接口内描述。
发送信号:
- 进程接收信号存在默认行为,单不支持POSIX标准所给出的STOP及CONTINUE、COREDUMP功能。
- 进程无法屏蔽SIGSTOP、SIGKILL、SIGCONT信号。
- 某进程后被杀死后,若其父进程不回收该进程,其转为僵尸进程。
- 进程接收到某信号后,直到该进程被调度后才会执行信号回调。
- 进程结束后会发送SIGCHLD信号给父进程,该发送动作无法取消。
- 无法通过信号唤醒处于DELAY状态的进程。
文章转载自:
博主您好!您的文章写得太细致了!我想问一个很小但是一直不懂的问题,就是这一份代码要如何编译呢?已经思考一天了但是一直找不到头文件,谢谢博主!