OpenHarmony(鸿蒙南向开发)——轻量系统内核(LiteOS-M)【内核通信机制】

往期知识点记录:

事件

基本概念

事件(Event)是一种任务间的通信机制,可用于任务间的同步操作。事件的特点是:

  • 任务间的事件同步,可以一对多,也可以多对多。一对多表示一个任务可以等待多个事件,多对多表示多个任务可以等待多个事件。但是一次写事件最多触发一个任务从阻塞中醒来。

  • 事件读超时机制。

  • 只做任务间同步,不传输具体数据。

提供了事件初始化、事件读写、事件清零、事件销毁等接口。

运行机制

事件控制块

由事件初始化函数配置的一个结构体,在事件读写等操作时作为参数传入,用于标识不同的事件,控制块数据结构如下:

typedef struct tagEvent {
    UINT32 uwEventID;        /* 事件集合,表示已经处理(写入和清零)的事件集合 */
    LOS_DL_LIST stEventList; /* 等待特定事件的任务链表 */
} EVENT_CB_S, *PEVENT_CB_S;

事件运作原理

事件初始化:创建一个事件控制块,该控制块维护一个已处理的事件集合,以及等待特定事件的任务链表。

写事件:会向事件控制块写入指定的事件,事件控制块更新事件集合,并遍历任务链表,根据任务等待具体条件满足情况决定是否唤醒相关任务。

读事件:如果读取的事件已存在时,会直接同步返回。其他情况会根据超时时间以及事件触发情况,来决定返回时机:等待的事件条件在超时时间耗尽之前到达,阻塞任务会被直接唤醒,否则超时时间耗尽该任务才会被唤醒。

读事件条件满足与否取决于入参eventMask和mode,eventMask即需要关注的事件类型掩码。mode是具体处理方式,分以下三种情况:

  • LOS_WAITMODE_AND:逻辑与,基于接口传入的事件类型掩码eventMask,只有这些事件都已经发生才能读取成功,否则该任务将阻塞等待或者返回错误码。

  • LOS_WAITMODE_OR:逻辑或,基于接口传入的事件类型掩码eventMask,只要这些事件中有任一种事件发生就可以读取成功,否则该任务将阻塞等待或者返回错误码。

  • LOS_WAITMODE_CLR:这是一种附加读取模式,需要与所有事件模式或任一事件模式结合使用(LOS_WAITMODE_AND | LOS_WAITMODE_CLR或 LOS_WAITMODE_OR | LOS_WAITMODE_CLR)。在这种模式下,当设置的所有事件模式或任一事件模式读取成功后,会自动清除事件控制块中对应的事件类型位。

事件清零:根据指定掩码,去对事件控制块的事件集合进行清零操作。当掩码为0时,表示将事件集合全部清零。当掩码为0xffff时,表示不清除任何事件,保持事件集合原状。

事件销毁:销毁指定的事件控制块。

图1 轻量系统事件运作原理图

接口说明

功能分类 接口名 描述
事件检测 LOS_EventPoll 根据eventID,eventMask(事件掩码),mode(事件读取模式),检查用户期待的事件是否发生。 须知: 当mode含LOS_WAITMODE_CLR,且用户期待的事件发生时,此时eventID中满足要求的事件会被清零,这种情况下eventID既是入参也是出参。其他情况eventID只作为入参。
初始化 LOS_EventInit 事件控制块初始化。
事件读 LOS_EventRead 读事件(等待事件),任务会根据timeOut(单位:tick)进行阻塞等待; 未读取到事件时,返回值为0; 正常读取到事件时,返回正值(事件发生的集合); 其他情况返回特定错误码。
事件写 LOS_EventWrite 写一个特定的事件到事件控制块。
事件清除 LOS_EventClear 根据events掩码,清除事件控制块中的事件。
事件销毁 LOS_EventDestroy 事件控制块销毁。

开发流程

事件的典型开发流程:

  1. 初始化事件控制块

  2. 阻塞读事件控制块

  3. 写入相关事件

  4. 阻塞任务被唤醒,读取事件并检查是否满足要求

  5. 处理事件控制块

  6. 事件控制块销毁

说明:

  • 进行事件读写操作时,事件的第25bit(0x02U << 24)为保留bit位,不可以进行位设置。

  • 对同一事件反复写入,算作一次写入。

编程实例

实例描述

示例中,任务ExampleEvent创建一个任务EventReadTask,EventReadTask读事件阻塞,ExampleEvent向该任务写事件。可以通过示例日志中打印的先后顺序理解事件操作时伴随的任务切换。

  1. 在任务ExampleEvent创建任务EventReadTask,其中任务EventReadTask优先级高于ExampleEvent。

  2. 在任务EventReadTask中读事件0x00000001,阻塞,发生任务切换,执行任务ExampleEvent。

  3. 在任务ExampleEvent写事件0x00000001,发生任务切换,执行任务EventReadTask。

  4. EventReadTask得以执行,直到任务结束。

  5. ExampleEvent得以执行,直到任务结束。

示例代码

示例代码如下:

本演示代码在 ./kernel/liteos_m/testsuites/src/osTest.c 中编译验证,在TestTaskEntry中调用验证入口函数ExampleEvent。

#include "los_event.h"
#include "los_task.h"

/* 事件控制结构体 */
EVENT_CB_S g_exampleEvent;

/* 等待的事件类型 */
#define EVENT_WAIT 0x00000001

/* 等待超时时间 */
#define EVENT_TIMEOUT 100

/* 用例任务入口函数 */
VOID EventReadTask(VOID)
{
    UINT32 ret;
    UINT32 event;

    /* 超时等待方式读事件,超时时间为100 ticks, 若100 ticks后未读取到指定事件,读事件超时,任务直接唤醒 */
    printf("Example_Event wait event 0x%x \n", EVENT_WAIT);

    event = LOS_EventRead(&g_exampleEvent, EVENT_WAIT, LOS_WAITMODE_AND, EVENT_TIMEOUT);
    if (event == EVENT_WAIT) {
        printf("Example_Event, read event :0x%x\n", event);
    } else {
        printf("Example_Event, read event timeout\n");
    }
}

UINT32 ExampleEvent(VOID)
{
    UINT32 ret;
    UINT32 taskId;
    TSK_INIT_PARAM_S taskParam = { 0 };

    /* 事件初始化 */
    ret = LOS_EventInit(&g_exampleEvent);
    if (ret != LOS_OK) {
        printf("init event failed .\n");
        return LOS_NOK;
    }

    /* 创建任务 */
    taskParam.pfnTaskEntry = (TSK_ENTRY_FUNC)EventReadTask;
    taskParam.pcName       = "EventReadTask";
    taskParam.uwStackSize  = LOSCFG_BASE_CORE_TSK_DEFAULT_STACK_SIZE;
    taskParam.usTaskPrio   = 3;
    ret = LOS_TaskCreate(&taskId, &taskParam);
    if (ret != LOS_OK) {
        printf("task create failed.\n");
        return LOS_NOK;
    }

    /* 写事件 */
    printf("Example_TaskEntry write event.\n");

    ret = LOS_EventWrite(&g_exampleEvent, EVENT_WAIT);
    if (ret != LOS_OK) {
        printf("event write failed.\n");
        return LOS_NOK;
    }

    /* 清标志位 */
    printf("EventMask:%d\n", g_exampleEvent.uwEventID);
    LOS_EventClear(&g_exampleEvent, ~g_exampleEvent.uwEventID);
    printf("EventMask:%d\n", g_exampleEvent.uwEventID);

    /* 删除事件 */
    ret = LOS_EventDestroy(&g_exampleEvent);
    if (ret != LOS_OK) {
        printf("destory event failed .\n");
        return LOS_NOK;
    }

    return LOS_OK;
}

结果验证

编译运行得到的结果为:

Example_Event wait event 0x1
Example_TaskEntry write event.
Example_Event, read event :0x1
EventMask:1
EventMask:0

互斥锁

基本概念

互斥锁又称互斥型信号量,是一种特殊的二值性信号量,用于实现对共享资源的独占式处理。

任意时刻互斥锁的状态只有两种,开锁或闭锁。当任务持有互斥锁时,该互斥锁处于闭锁状态,这个任务获得该互斥锁的所有权。当该任务释放互斥锁时,该互斥锁被开锁,任务失去该互斥锁的所有权。当一个任务持有互斥锁时,其他任务将不能再对该互斥锁进行开锁或持有。

多任务环境下往往存在多个任务竞争同一共享资源的应用场景,互斥锁可被用于对共享资源的保护从而实现独占式访问。另外互斥锁可以解决信号量存在的优先级翻转问题。

运行机制

多任务环境下会存在多个任务访问同一公共资源的场景,而有些公共资源是非共享的,需要任务进行独占式处理。互斥锁怎样来避免这种冲突呢?

用互斥锁处理非共享资源的同步访问时,如果有任务访问该资源,则互斥锁为加锁状态。此时其他任务如果想访问这个公共资源则会被阻塞,直到互斥锁被持有该锁的任务释放后,其他任务才能重新访问该公共资源,此时互斥锁再次上锁,如此确保同一时刻只有一个任务正在访问这个公共资源,保证了公共资源操作的完整性。

图1 轻量系统互斥锁运作示意图

接口说明

表1 互斥锁模块接口

功能分类 接口描述
互斥锁的创建和删除 LOS_MuxCreate:创建互斥锁。
LOS_MuxDelete:删除指定的互斥锁。
互斥锁的申请和释放 LOS_MuxPend:申请指定的互斥锁。 LOS_MuxPost:释放指定的互斥锁。

开发流程

互斥锁典型场景的开发流程:

  1. 创建互斥锁LOS_MuxCreate。

  2. 申请互斥锁LOS_MuxPend。 申请模式有三种:无阻塞模式、永久阻塞模式、定时阻塞模式。

  • 无阻塞模式:任务需要申请互斥锁,若该互斥锁当前没有任务持有,或者持有该互斥锁的任务和申请该互斥锁的任务为同一个任务,则申请成功。否则直接返回并继续运行当前任务,不会产生阻塞。
  • 永久阻塞模式:任务需要申请互斥锁,若该互斥锁当前没有被占用,则申请成功。否则,该任务进入阻塞态,系统切换到就绪任务中优先级高者继续执行。任务进入阻塞态后,直到有其他任务释放该互斥锁,阻塞任务才会重新得以执行。
  • 定时阻塞模式:任务需要申请互斥锁,若该互斥锁当前没有被占用,则申请成功。否则该任务进入阻塞态,系统切换到就绪任务中优先级高者继续执行。任务进入阻塞态后,指定时间超时前有其他任务释放该互斥锁,或者用户指定时间超时后,阻塞任务才会重新得以执行。
  1. 释放互斥锁LOS_MuxPost。

    • 如果有任务阻塞于指定互斥锁,则唤醒被阻塞任务中优先级高的,该任务进入就绪态,并进行任务调度;
    • 如果没有任务阻塞于指定互斥锁,则互斥锁释放成功。
  2. 删除互斥锁LOS_MuxDelete。

说明:

  • 互斥锁支持嵌套,即申请该互斥锁的任务与已经持有该互斥锁的任务为同一个任务时会认为申请成功,按申请次数对应的去释放该锁即可。

  • 互斥锁不能在中断服务程序中使用。

  • LiteOS-M内核作为实时操作系统需要保证任务调度的实时性,尽量避免任务的长时间阻塞,因此在获得互斥锁之后,应该尽快释放互斥锁。

  • 持有互斥锁的过程中,不得再调用LOS_TaskPriSet等接口更改持有互斥锁任务的优先级。

编程实例

实例描述

本实例实现如下流程。

  1. 任务ExampleMutex创建一个互斥锁,锁任务调度,创建两个任务ExampleMutexTask1、ExampleMutexTask2。ExampleMutexTask2优先级高于ExampleMutexTask1,解锁任务调度。

  2. ExampleMutexTask2被调度,以永久阻塞模式申请互斥锁,并成功获取到该互斥锁,然后任务休眠100Tick,ExampleMutexTask2挂起,ExampleMutexTask1被唤醒。

  3. ExampleMutexTask1以定时阻塞模式申请互斥锁,等待时间为10Tick,因互斥锁仍被ExampleMutexTask2持有,ExampleMutexTask1挂起。10Tick超时时间到达后,ExampleMutexTask1被唤醒,以永久阻塞模式申请互斥锁,因互斥锁仍被ExampleMutexTask2持有,ExampleMutexTask1挂起。

  4. 100Tick休眠时间到达后,ExampleMutexTask2被唤醒, 释放互斥锁,唤醒ExampleMutexTask1。ExampleMutexTask1成功获取到互斥锁后,释放并删除互斥锁。

示例代码

示例代码如下:

本演示代码在 ./kernel/liteos_m/testsuites/src/osTest.c 中编译验证,在TestTaskEntry中调用验证入口函数ExampleMutex。

#include "los_mux.h"

/* 互斥锁句柄 */
UINT32 g_testMux;

VOID ExampleMutexTask1(VOID)
{
    UINT32 ret;

    printf("task1 try to get  mutex, wait 10 ticks.\n");
    /* 申请互斥锁 */
    ret = LOS_MuxPend(g_testMux, 10);
    if (ret == LOS_OK) {
        printf("task1 get mutex g_testMux.\n");
        /* 释放互斥锁,这个分支正常不应该进来 */
        LOS_MuxPost(g_testMux);
        LOS_MuxDelete(g_testMux);
        return;
    }

    if (ret == LOS_ERRNO_MUX_TIMEOUT ) {
        printf("task1 timeout and try to get mutex, wait forever.\n");
        /* 申请互斥锁 */
        ret = LOS_MuxPend(g_testMux, LOS_WAIT_FOREVER);
        if (ret == LOS_OK) {
            printf("task1 wait forever, get mutex g_testMux.\n");
            /* 释放互斥锁 */
            LOS_MuxPost(g_testMux);
            /* 删除互斥锁 */
            LOS_MuxDelete(g_testMux);
            printf("task1 post and delete mutex g_testMux.\n");
            return;
        }
    }

    return;
}

VOID ExampleMutexTask2(VOID)
{
    printf("task2 try to get  mutex, wait forever.\n");
    /* 申请互斥锁 */
    (VOID)LOS_MuxPend(g_testMux, LOS_WAIT_FOREVER);
    printf("task2 get mutex g_testMux and suspend 100 ticks.\n");

    /* 任务休眠100Ticks */
    LOS_TaskDelay(100);

    printf("task2 resumed and post the g_testMux\n");
    /* 释放互斥锁 */
    LOS_MuxPost(g_testMux);
    return;
}

UINT32 ExampleMutex(VOID)
{
    UINT32 ret;
    TSK_INIT_PARAM_S task1 = { 0 };
    TSK_INIT_PARAM_S task2 = { 0 };
    UINT32 taskId01;
    UINT32 taskId02;

    /* 创建互斥锁 */
    LOS_MuxCreate(&g_testMux);

    /* 锁任务调度 */
    LOS_TaskLock();

    /* 创建任务1 */
    task1.pfnTaskEntry = (TSK_ENTRY_FUNC)ExampleMutexTask1;
    task1.pcName       = "MutexTsk1";
    task1.uwStackSize  = LOSCFG_BASE_CORE_TSK_DEFAULT_STACK_SIZE;
    task1.usTaskPrio   = 5;
    ret = LOS_TaskCreate(&taskId01, &task1);
    if (ret != LOS_OK) {
        printf("task1 create failed.\n");
        return LOS_NOK;
    }

    /* 创建任务2 */
    task2.pfnTaskEntry = (TSK_ENTRY_FUNC)ExampleMutexTask2;
    task2.pcName       = "MutexTsk2";
    task2.uwStackSize  = LOSCFG_BASE_CORE_TSK_DEFAULT_STACK_SIZE;
    task2.usTaskPrio   = 4;
    ret = LOS_TaskCreate(&taskId02, &task2);
    if (ret != LOS_OK) {
        printf("task2 create failed.\n");
        return LOS_NOK;
    }

    /* 解锁任务调度 */
    LOS_TaskUnlock();

    return LOS_OK;
}

结果验证

编译运行得到的结果为:

task2 try to get  mutex, wait forever.
task2 get mutex g_testMux and suspend 100 ticks.
task1 try to get  mutex, wait 10 ticks.
task1 timeout and try to get mutex, wait forever.
task2 resumed and post the g_testMux
task1 wait forever, get mutex g_testMux.
task1 post and delete mutex g_testMux.

消息队列

基本概念

消息队列又称队列,是一种任务间通信的机制。消息队列接收来自任务或中断的不固定长度消息,并根据不同的接口确定传递的消息是否存放在队列空间中。

任务能够从队列里面读取消息,当队列中的消息为空时,挂起读取任务;当队列中有新消息时,挂起的读取任务被唤醒并处理新消息。任务也能够往队列里写入消息,当队列已经写满消息时,挂起写入任务;当队列中有空闲消息节点时,挂起的写入任务被唤醒并写入消息。

可以通过调整读队列和写队列的超时时间来调整读写接口的阻塞模式,如果将读队列和写队列的超时时间设置为0,就不会挂起任务,接口会直接返回,这就是非阻塞模式。反之,如果将读队列和写队列的超时时间设置为大于0的时间,就会以阻塞模式运行。

消息队列提供了异步处理机制,允许将一个消息放入队列,但不立即处理。同时队列还有缓冲消息的作用,可以使用队列实现任务异步通信,队列具有如下特性:

  • 消息以先进先出的方式排队,支持异步读写。
  • 读队列和写队列都支持超时机制。
  • 每读取一条消息,就会将该消息节点设置为空闲。
  • 发送消息类型由通信双方约定,可以允许不同长度(不超过队列的消息节点大小)的消息。
  • 一个任务能够从任意一个消息队列接收和发送消息。
  • 多个任务能够从同一个消息队列接收和发送消息。
  • 创建普通队列时所需的队列空间,由系统自行动态申请内存。
  • 创建静态队列时所需的队列空间,由用户传入。这块空间在队列删除之后也由用户去释放。

运行机制

队列控制块

队列会在初始化时给分配一个属于自己的控制块,控制块包含了队列的名称、状态等信息。删除队列时会释放该控制块。

队列控制块数据结构如下:

typedef struct 
{
    UINT8       *queue;                          		/* 队列消息内存空间的指针 */
    UINT8 		*queueName								/* 队列名称 */
    UINT16      queueState;                      		/* 队列状态 */
    UINT16      queueLen;                        		/* 队列中消息节点个数,即队列长度 */
    UINT16      queueSize;                       		/* 消息节点大小 */
    UINT16      queueID;                         		/* 队列ID */
    UINT16      queueHead;                       		/* 消息头节点位置(数组下标)*/
    UINT16      queueTail;                       		/* 消息尾节点位置(数组下标)*/
    UINT16      readWriteableCnt[OS_READWRITE_LEN]; 	/* 数组下标0的元素表示队列中可读消息数,                              
                                                    		数组下标1的元素表示队列中可写消息数 */
    LOS_DL_LIST readWriteList[OS_READWRITE_LEN];    	/* 读取或写入消息的任务等待链表, 
                                                       		下标0:读取链表,下标1:写入链表 */
    LOS_DL_LIST memList;                         		/* 内存块链表 */
} 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_QueueCreateStatic:创建一个消息队列,由用户传入队列空间。 LOS_QueueDelete:根据队列ID删除一个指定队列,静态消息队列删除后,队列空间需要用例自行处理。
读/写队列(不带拷贝) LOS_QueueRead:读取指定队列头节点中的数据(队列节点中的数据实际上是一个地址)。 LOS_QueueWrite:向指定队列尾节点中写入入参bufferAddr的值(即buffer的地址)。 LOS_QueueWriteHead:向指定队列头节点中写入入参bufferAddr的值(即buffer的地址)。
读/写队列(带拷贝) LOS_QueueReadCopy:读取指定队列头节点中的数据。 LOS_QueueWriteCopy:向指定队列尾节点中写入入参bufferAddr中保存的数据。 LOS_QueueWriteHeadCopy:向指定队列头节点中写入入参bufferAddr中保存的数据。
获取队列信息 LOS_QueueInfoGet:获取指定队列的信息,包括队列ID、队列长度、消息节点大小、头节点、尾节点、可读节点数量、可写节点数量、等待读操作的任务、等待写操作的任务。

开发流程

  1. 用LOS_QueueCreate创建队列。创建成功后,可以得到队列ID。
  2. 通过LOS_QueueWrite或者LOS_QueueWriteCopy写队列。
  3. 通过LOS_QueueRead或者LOS_QueueReadCopy读队列。
  4. 通过LOS_QueueInfoGet获取队列信息。
  5. 通过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_QueueReadCopy接口的读取长度如果小于消息实际长度,消息将被截断。

  • 鉴于LOS_QueueWrite和LOS_QueueWriteHead和LOS_QueueRead这组接口实际操作的是数据地址,也就意味着实际写和读的消息长度仅仅是一个指针数据,因此用户使用这组接口之前,需确保创建队列时的消息节点大小,为一个指针的长度,避免不必要的浪费和读取失败。

编程实例

实例描述

创建一个队列,两个任务。任务1调用写队列接口发送消息,任务2通过读队列接口接收消息。

  1. 通过LOS_TaskCreate创建任务1和任务2。
  2. 通过LOS_QueueCreate创建一个消息队列。
  3. 在任务1 SendEntry中发送消息。
  4. 在任务2 RecvEntry中接收消息。
  5. 通过LOS_QueueDelete删除队列。

示例代码

示例代码如下:

本演示代码在 ./kernel/liteos_m/testsuites/src/osTest.c 中编译验证,在TestTaskEntry中调用验证入口函数ExampleQueue。

#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) {
        printf("send message failure, error: %x\n", ret);
    }
}

VOID RecvEntry(VOID)
{
    UINT32 ret = 0;
    CHAR readBuf[BUFFER_LEN] = {0};
    UINT32 readLen = BUFFER_LEN;

    /* 休眠1s */
    usleep(1000000);
    ret = LOS_QueueReadCopy(g_queue, readBuf, &readLen, 0);
    if (ret != LOS_OK) {
        printf("recv message failure, error: %x\n", ret);
    }

    printf("recv message: %s.\n", readBuf);

    ret = LOS_QueueDelete(g_queue);
    if (ret != LOS_OK) {
        printf("delete the queue failure, error: %x\n", ret);
    }

    printf("delete the queue success.\n");
}

UINT32 ExampleQueue(VOID)
{
    printf("start queue example.\n");
    UINT32 ret = 0;
    UINT32 task1;
    UINT32 task2;
    TSK_INIT_PARAM_S taskParam1 = { 0 };
    TSK_INIT_PARAM_S taskParam2 = { 0 };

    LOS_TaskLock();

    taskParam1.pfnTaskEntry = (TSK_ENTRY_FUNC)SendEntry;
    taskParam1.usTaskPrio = 9;
    taskParam1.uwStackSize = LOSCFG_BASE_CORE_TSK_DEFAULT_STACK_SIZE;
    taskParam1.pcName = "SendQueue";
    ret = LOS_TaskCreate(&task1, &taskParam1);
    if(ret != LOS_OK) {
        printf("create task1 failed, error: %x\n", ret);
        return ret;
    }

    taskParam2.pfnTaskEntry = (TSK_ENTRY_FUNC)RecvEntry;
    taskParam2.usTaskPrio = 10;
    taskParam2.uwStackSize = LOSCFG_BASE_CORE_TSK_DEFAULT_STACK_SIZE;
    taskParam2.pcName = "RecvQueue";
    ret = LOS_TaskCreate(&task2, &taskParam2);
    if(ret != LOS_OK) {
        printf("create task2 failed, error: %x\n", ret);
        return ret;
    }

    ret = LOS_QueueCreate("queue", 5, &g_queue, 0, 50);
    if(ret != LOS_OK) {
        printf("create queue failure, error: %x\n", ret);
    }

    printf("create the queue success.\n");
    LOS_TaskUnlock();
    return ret;
}

结果验证

编译运行得到的结果为:

start queue example.
create the queue success.
recv message: test message.
delete the queue success.

信号量

基本概念

信号量(Semaphore)是一种实现任务间通信的机制,可以实现任务间同步或共享资源的互斥访问。

一个信号量的数据结构中,通常有一个计数值,用于对有效资源数的计数,表示剩下的可被使用的共享资源数,其值的含义分两种情况:

  • 0,表示该信号量当前不可获取,因此可能存在正在等待该信号量的任务。

  • 正值,表示该信号量当前可被获取。

信号量可用于同步或者互斥。以同步为目的的信号量和以互斥为目的的信号量在使用上有如下不同:

  • 用作互斥时,初始信号量计数值不为0,表示可用的共享资源个数。在需要使用共享资源前,先获取信号量,然后使用一个共享资源,使用完毕后释放信号量。这样在共享资源被取完,即信号量计数减至0时,其他需要获取信号量的任务将被阻塞,从而保证了共享资源的互斥访问。另外,当共享资源数为1时,建议使用二值信号量,一种类似于互斥锁的机制。

  • 用作同步时,初始信号量计数值为0。任务1因获取不到信号量而阻塞,直到任务2或者某中断释放信号量,任务1才得以进入Ready或Running态,从而达到了任务间的同步。

运行机制

信号量控制块

/**
 * 信号量控制块数据结构
 */
typedef struct {
    UINT16            semStat;          /* 信号量状态 */
    UINT16            semType;          /* 信号量类型 */
    UINT16            semCount;         /* 信号量计数 */
    UINT16            semId;            /* 信号量索引号 */
    LOS_DL_LIST       semList;          /* 用于插入阻塞于信号量的任务 */
} LosSemCB;

信号量运作原理

信号量初始化,为配置的N个信号量申请内存(N值可以由用户自行配置,通过LOSCFG_BASE_IPC_SEM_LIMIT宏实现,按产品实际需要设定),并把所有信号量初始化成未使用,加入到未使用链表中供系统使用。

信号量创建,从未使用的信号量链表中获取一个信号量,并设定初值。

信号量申请,若其计数器值大于0,则直接减1返回成功。否则任务阻塞,等待其它任务释放该信号量,等待的超时时间可设定。当任务被一个信号量阻塞时,将该任务挂到信号量等待任务队列的队尾。

信号量释放,若没有任务等待该信号量,则直接将计数器加1返回。否则唤醒该信号量等待任务队列上的第一个任务。

信号量删除,将正在使用的信号量置为未使用信号量,并挂回到未使用链表。

信号量允许多个任务在同一时刻访问共享资源,但会限制同一时刻访问此资源的最大任务数目。当访问资源的任务数达到该资源允许的最大数量时,会阻塞其他试图获取该资源的任务,直到有任务释放该信号量。

图1 轻量系统信号量运作示意图

接口说明

功能分类 接口描述
创建/删除信号量 LOS_SemCreate:创建信号量,返回信号量ID。 LOS_BinarySemCreate:创建二值信号量,其计数值最大为1。 LOS_SemDelete:删除指定的信号量。
申请/释放信号量 LOS_SemPend:申请指定的信号量,并设置超时时间。 LOS_SemPost:释放指定的信号量。

开发流程

  1. 创建信号量LOS_SemCreate,若要创建二值信号量则调用LOS_BinarySemCreate。

  2. 申请信号量LOS_SemPend。

  3. 释放信号量LOS_SemPost。

  4. 删除信号量LOS_SemDelete。

说明: 由于中断不能被阻塞,因此不能在中断中使用阻塞模式申请信号量。

编程实例

实例描述

本实例实现如下功能:

  1. 测试任务ExampleSem创建一个信号量,锁任务调度。创建两个任务ExampleSemTask1和ExampleSemTask2, ExampleSemTask2优先级高于ExampleSemTask1。两个任务中申请同一信号量,解锁任务调度后两任务阻塞,测试任务ExampleSem释放信号量。

  2. ExampleSemTask2得到信号量,被调度,然后任务休眠20Tick,ExampleSemTask2延迟,ExampleSemTask1被唤醒。

  3. ExampleSemTask1定时阻塞模式申请信号量,等待时间为10Tick,因信号量仍被ExampleSemTask2持有,ExampleSemTask1挂起,10Tick后仍未得到信号量,ExampleSemTask1被唤醒,试图以永久阻塞模式申请信号量,ExampleSemTask1挂起。

  4. 20Tick后ExampleSemTask2唤醒, 释放信号量后,ExampleSemTask1得到信号量被调度运行,最后释放信号量。

  5. ExampleSemTask1执行完,400Tick后任务ExampleSem被唤醒,执行删除信号量。

示例代码

示例代码如下:

本演示代码在 ./kernel/liteos_m/testsuites/src/osTest.c 中编译验证,在TestTaskEntry中调用验证入口函数ExampleSem。

#include "los_sem.h"

/* 信号量结构体id */
static UINT32 g_semId;

VOID ExampleSemTask1(VOID)
{
    UINT32 ret;

    printf("ExampleSemTask1 try get sem g_semId, timeout 10 ticks.\n");
    /* 定时阻塞模式申请信号量,定时时间为10ticks */
    ret = LOS_SemPend(g_semId, 10);
    /* 申请到信号量 */
    if (ret == LOS_OK) {
         LOS_SemPost(g_semId);
         return;
    }

    /* 定时时间到,未申请到信号量 */
    if (ret == LOS_ERRNO_SEM_TIMEOUT) {
        printf("ExampleSemTask1 timeout and try get sem g_semId wait forever.\n");
        /*永久阻塞模式申请信号量*/
        ret = LOS_SemPend(g_semId, LOS_WAIT_FOREVER);
        printf("ExampleSemTask1 wait_forever and get sem g_semId.\n");
        if (ret == LOS_OK) {
            LOS_SemPost(g_semId);
            return;
        }
    }
}

VOID ExampleSemTask2(VOID)
{
    UINT32 ret;
    printf("ExampleSemTask2 try get sem g_semId wait forever.\n");

    /* 永久阻塞模式申请信号量 */
    ret = LOS_SemPend(g_semId, LOS_WAIT_FOREVER);
    if (ret == LOS_OK) {
        printf("ExampleSemTask2 get sem g_semId and then delay 20 ticks.\n");
    }

    /* 任务休眠20 ticks */
    LOS_TaskDelay(20);
    printf("ExampleSemTask2 post sem g_semId.\n");

    /* 释放信号量 */
    LOS_SemPost(g_semId);
    return;
}

UINT32 ExampleSem(VOID)
{
    UINT32 ret;
    TSK_INIT_PARAM_S task1 = { 0 };
    TSK_INIT_PARAM_S task2 = { 0 };
    UINT32 taskId1;
    UINT32 taskId2;

   /* 创建信号量 */
    LOS_SemCreate(0, &g_semId);

    /* 锁任务调度 */
    LOS_TaskLock();

    /* 创建任务1 */
    task1.pfnTaskEntry = (TSK_ENTRY_FUNC)ExampleSemTask1;
    task1.pcName       = "TestTask1";
    task1.uwStackSize  = LOSCFG_BASE_CORE_TSK_DEFAULT_STACK_SIZE;
    task1.usTaskPrio   = 5;
    ret = LOS_TaskCreate(&taskId1, &task1);
    if (ret != LOS_OK) {
        printf("task1 create failed.\n");
        return LOS_NOK;
    }

    /* 创建任务2 */
    task2.pfnTaskEntry = (TSK_ENTRY_FUNC)ExampleSemTask2;
    task2.pcName       = "TestTask2";
    task2.uwStackSize  = LOSCFG_BASE_CORE_TSK_DEFAULT_STACK_SIZE;
    task2.usTaskPrio   = 4;
    ret = LOS_TaskCreate(&taskId2, &task2);
    if (ret != LOS_OK) {
        printf("task2 create failed.\n");
        return LOS_NOK;
    }

    /* 解锁任务调度 */
    LOS_TaskUnlock();

    ret = LOS_SemPost(g_semId);

    /* 任务休眠400 ticks */
    LOS_TaskDelay(400);

    /* 删除信号量 */
    LOS_SemDelete(g_semId);
    return LOS_OK;
}

结果验证

编译运行得到的结果为:

ExampleSemTask2 try get sem g_semId wait forever.
ExampleSemTask1 try get sem g_semId, timeout 10 ticks.
ExampleSemTask2 get sem g_semId and then delay 20 ticks.
ExampleSemTask1 timeout and try get sem g_semId wait forever.
ExampleSemTask2 post sem g_semId.
ExampleSemTask1 wait_forever and get sem g_semId.

最后

经常有很多小伙伴抱怨说:不知道学习鸿蒙开发哪些技术?不知道需要重点掌握哪些鸿蒙应用开发知识点?

为了能够帮助到大家能够有规划的学习,这里特别整理了一套纯血版鸿蒙(HarmonyOS Next)全栈开发技术的学习路线,包含了鸿蒙开发必掌握的核心知识要点,内容有(ArkTS、ArkUI开发组件、Stage模型、多端部署、分布式应用开发、WebGL、元服务、OpenHarmony多媒体技术、Napi组件、OpenHarmony内核、OpenHarmony驱动开发、系统定制移植等等)鸿蒙(HarmonyOS NEXT)技术知识点。

《鸿蒙 (Harmony OS)开发学习手册》(共计892页):https://gitcode.com/HarmonyOS_MN/733GH/overview

如何快速入门?

1.基本概念

2.构建第一个ArkTS应用

3.......

鸿蒙开发面试真题(含参考答案):

《OpenHarmony源码解析》:

  • 搭建开发环境
  • Windows 开发环境的搭建
  • Ubuntu 开发环境搭建
  • Linux 与 Windows 之间的文件共享
  • ......
  • 系统架构分析
  • 构建子系统
  • 启动流程
  • 子系统
  • 分布式任务调度子系统
  • 分布式通信子系统
  • 驱动子系统
  • ......

OpenHarmony 设备开发学习手册 :https://gitcode.com/HarmonyOS_MN/733GH/overview


相关推荐
vfbox33987 小时前
C-DTL698电表 转 IEC61850 协议项目案例
物联网·iec61850·vfbox·协议转换工业网关·c-dtl698
dawn9 小时前
鸿蒙ArkTS中的获取网络数据
华为·harmonyos
桃花键神9 小时前
鸿蒙5.0时代:原生鸿蒙应用市场引领开发者服务新篇章
华为·harmonyos
鸿蒙自习室9 小时前
鸿蒙多线程开发——并发模型对比(Actor与内存共享)
华为·harmonyos
易云码9 小时前
信息安全建设方案,网络安全等保测评方案,等保技术解决方案,等保总体实施方案(Word原件)
数据库·物联网·安全·web安全·低代码
Qingniu0110 小时前
【青牛科技】应用方案 | RTC实时时钟芯片D8563和D1302
科技·单片机·嵌入式硬件·实时音视频·安防·工控·储能
JavaPub-rodert10 小时前
鸿蒙生态崛起:开发者的机遇与挑战
华为·harmonyos
深圳市青牛科技实业有限公司11 小时前
【青牛科技】应用方案|D2587A高压大电流DC-DC
人工智能·科技·单片机·嵌入式硬件·机器人·安防监控
Mr.谢尔比12 小时前
电赛入门之软件stm32keil+cubemx
stm32·单片机·嵌入式硬件·mcu·信息与通信·信号处理
LightningJie12 小时前
STM32中ARR(自动重装寄存器)为什么要减1
stm32·单片机·嵌入式硬件