OpenHarmony内核编程实战

在正式开始之前,对于刚接触OpenHarmony的伙伴们,面对大篇幅的源码可能无从下手,不知道怎么去编码写程序,下面用一个简单的例子带伙伴们入门。

▍任务

编写程序,让开发板在串口调试工具中输出”Hello,OpenHarmony“。

▍操作

在源码的根目录中有名为”applications“的文件,他存放着应用程序样例,下面是他的目录结构:

我们要编写的程序样例就在源码根目录下的:applications/sample/wifi-iot/app/

下面将具体演示如何编写程序样例。

1.新建样例目录

applications/sample/wifi-iot/app/hello_demo

2.新建源文件和gn文件

applications/sample/wifi-iot/app/hello_demo/hello.c

applications/sample/wifi-iot/app/hello_demo/BUILD.gn

3.编写源文件

hello.c

#include <stdio.h>
#include "ohos_init.h"void hello(void){printf("Hello,OpenHarmony!");
}SYS_RUN(hello);

第一次操作的伙伴们可能会在引入”ohos_init.h“库时报错,面对这个问题我们只需要修改我们的include path即可,一般我们直接在目录下的  .vscode/c_cpp_properties.json文件中直接修改includePath

笔者的代码版本是OpenHarmony3.2Release版,不同版本的源码可能库所存放的路径不同,那么怎么去找到对应的库呢,对于不熟悉源码结构的伙伴们学习起来很不友好。

对于在纯Windows环境开发的伙伴们,笔者推荐使用everything这款工具,它可以快速查找主机中的文件,比在资源管理器的搜索快上不少。

everything似乎不能找到我WSL中的Ubuntu中的文件,因此对于Windows + Linux环境下的伙伴们,这款工具又不那么适用。那就可以根据Linux的查询指令来定位文件所在目录,下面提供查询案例防止有不熟悉Linux的伙伴们。我们使用locate指令来查找文件。

首先安装locate

sudo apt install mlocate

更新mlocate.db

sudo updatedb

查询文件目录

locate ohos\_init.h  

找到我们源码根目录下 include路径下的ohos_init.h文件

4.编写gn文件

static_library("sayHello"){sources = ["hello.c"]include_dirs = ["//commonlibrary/utils_lite/include"]
}

static_library表示我们编写的静态模块,名为"sayHello", sources表示我们要编译的源码,include_dirs表示我们引入的库,这里的双斜杠就代表我们的源码根目录,”/commonlibrary/utils_lite/include“就是我们ohos_init.h的所在目录

5.编写app下的gn文件

在app的目录下也有一个gn文件,我们只需要去修改他即可

这表示我们的程序将会执行hello_demo样例中的sayHello模块

6.编译,烧录,串口调试

这一步就属于基础操作了,不做过多赘述,

7.观察控制台的输出

至此编码完成了编码入门,下面就具体介绍OpenHarmony的内核编程。

内核

▍内核介绍

什么是内核?

或者说内核在一个操作系统中起到一个什么样的作用?相信初次接触这个词的伙伴们也会有同样的疑问。不过不用担心,笔者会尽可能地通俗地介绍内核的相关知识,以便大家能够更好地去体会内核编程。

我们先来看一张图,这是OpenHarmony官网发布的技术架构图

我们可以看到最底层叫做内核层,有Linux,LiteOS等。内核在整个架构,或者操作系统中起到一个核心作用,他负责管理计算机系统内的资源和硬件设备,提供给顶层的应用层一个统一规范的接口,从而使得整个系统能够完成应用与硬件的交互。

具体点来说,内核可以做以下相关的工作:

1.进程管理
2.内存管理
3.文件资源管理
4.网络通信管理
5.设备驱动管理

当然不局限于这些,这里只是给出具体的例子供伙伴们理解,如果实在难以理解,那么笔者再举一个例子,进程。可能你没听过进程,但你一定打开过任务管理器。

这些都是进程,一个进程又由多个线程组成。那么CPU,内存,硬盘,网络这些硬件层面资源是怎么合理分配到我们软件的各个进程中呢?这就是内核帮助我们完成的事情,我们并不关心我们设备上的应用在哪里执行,如何分配资源,内核会完成这些事情。我们日常与软件交互,而内核会帮助我们完成软件和硬件的交互。

▍OpenHarmony内核

明白了什么是内核后,我们来看看OpenHarmony的内核是怎么样设计的吧。

OpenHarmony采用的是多内核设计 有基于Linux内核的标准系统,有基于LiteOS-A的小型系统,也有基于LiteOS-M的轻量系统。他们分别适配不同的设备,比如说智能手表就是轻量级别的,智能汽车就是标准级别的等等。本篇并不介绍标准系统和小型系统,轻量系统更加适合初学者。

▍LiteOS-M内核

下面是一张LiteOS-M的架构图

下面重点介绍KAL抽象层 和 基础内核的操作

KAL抽象层

相信大家还是会有疑惑,什么是KAL抽象层?

Kernel Abstraction Layer

在刚刚的内核中我们提到了,内核主要完成的是软件与硬件的交互,他会给应用层提供统一的规范接口,而KAL抽象层正是内核对应用层提供的接口集合。应用程序可以通过KAL抽象层完成对硬件的控制交互。

抽象层是因为他隐藏了与硬件接口具体的交互逻辑,开发人员只需要关心如何操作硬件,而无需关心硬件底层的细节,大大提高了可移植性和维护性。

以笔者的角度去看,KAL简单来说就是一堆接口,帮助你去操控硬件。CMSIS与POSIX就是具有统一规范的一些接口。通过他们我们就可以去控制一些基础的内核,线程,软件定时器,互斥锁,信号量等等。概念就先简单介绍这么多,感兴趣的伙伴们可以上官网查看更多的关于OpenHarmony内核的信息。下面笔者会带着大家编码操作,从实际去体会内核编程。

内核编程

▍线程管理

在管理线程前,我们需要了解线程,线程是调度的基本单位,具有独立的栈空间和寄存器上下文,相比与进程,他是轻量的。举一个实际的例子,动物园卖票。

对于动物园卖票这件事本身而言是一个进程,而每一个买票的人可以看作一个线程,在多个售票口处,我们并发执行,并行计算,共同消费动物园的门票,像享受共同的内存资源空间一样。为什么要线程管理呢?你我都希望买到票,但是票有限,我们都不希望看到售票厅一篇混乱,因此对线程进行管理是非常重要的一件事情。

任务

创建一个线程,每间隔0.1秒,输出“Hello,OpenHarmony”,1秒后终止线程。

操作

回忆第一个hello.c的例子

我们要编写的程序样例就在源码根目录下的:applications/sample/wifi-iot/app/

下面将具体演示如何编写程序样例。

1.新建样例目录

applications/sample/wifi-iot/app/thread_demo

2.新建源文件和gn文件

applications/sample/wifi-iot/app/thread_demo/singleThread.c

applications/sample/wifi-iot/app/thread_demo/BUILD.gn

3.编写源码

注意:我们需要使用到cmsis_os2.h这个库,请伙伴们按照笔者介绍的方法把includePath修改好。

问题一:怎么创建线程?

typedef struct {  /\*\* Thread name \*/  const char                   \*name;  /\*\* Thread attribute bits \*/  uint32\_t                 attr\_bits;  /\*\* Memory for the thread control block \*/  void                      \*cb\_mem;  /\*\* Size of the memory for the thread control block \*/  uint32\_t                   cb\_size;  /\*\* Memory for the thread stack \*/  void                   \*stack\_mem;  /\*\* Size of the thread stack \*/  uint32\_t                stack\_size;  /\*\* Thread priority \*/  osPriority\_t              priority;  /\*\* TrustZone module of the thread \*/  TZ\_ModuleId\_t            tz\_module;  /\*\* Reserved \*/  uint32\_t                  reserved;  
} osThreadAttr\_t;  

这是线程的结构体,它具有以下属性:

  • name:线程的名称。
  • attr_bits:线程属性位。
  • cb_mem:线程控制块的内存地址。
  • cb_size:线程控制块的内存大小。
  • stack_mem:线程栈的内存地址。
  • stack_size:线程栈的大小。
  • priority:线程的优先级。
  • tz_module:线程所属的TrustZone模块。
  • reserved:保留字段。

问题二:怎么把线程启动起来呢?

osThreadId\_t osThreadNew (osThreadFunc\_t func, void \*argument, const osThreadAttr\_t \*attr);  

这是创建线程的接口函数,他有三个参数,一个返回值,我们来逐个解析

func:是线程的回调函数,你创建的这个线程会执行这段函数的内容。

arguments:线程回调函数的参数。

attr:线程的属性,也就是我们之前创建的线程

返回值:线程的id 如果id不为空则说明成功。

问题三:怎么终止线程呢?

osStatus\_t osThreadTerminate (osThreadId\_t thread\_id);  

显然我们只要传入线程的id就会让该线程终止,返回值是一个状态码,下面给出全部的状态码

typedef enum {  /\*\* Operation completed successfully \*/  osOK                      =  0,  /\*\* Unspecified error \*/  osError                   = \-1,  /\*\* Timeout \*/  osErrorTimeout            = \-2,  /\*\* Resource error \*/  osErrorResource           = \-3,  /\*\* Incorrect parameter \*/  osErrorParameter          = \-4,  /\*\* Insufficient memory \*/  osErrorNoMemory           = \-5,  /\*\* Service interruption \*/  osErrorISR                = \-6,  /\*\* Reserved. It is used to prevent the compiler from optimizing enumerations. \*/  osStatusReserved          = 0x7FFFFFFF  
} osStatus\_t;  

回调函数怎么写?当然是结合我们的任务,每间隔0.1秒,输出“Hello,OpenHarmony”,1秒后终止。讲到这里,代码的整体逻辑是不是就清晰了很多,直接上完整代码。

#include <stdio.h>  
#include "ohos\_init.h"  
// CMSIS  
#include "cmsis\_os2.h"  
// POSIX  
#include <unistd.h>  // 线程回调函数  
void printThread(void \*args){  (void)args;  while(1){  printf("Hello,OpenHarmony!\\r\\n");  // 休眠0.1秒  osDelay(10);  }  
}  void threadTest(void){  // 创建线程  osThreadAttr\_t attr;  attr.name = "mainThread";  // 线程  attr.cb\_mem = NULL;  attr.cb\_size = 0U;  attr.stack\_mem = NULL;  attr.stack\_size = 1024;  attr.priority = osPriorityNormal;  // 将线程启动  osThreadId\_t tid = osThreadNew((osThreadFunc\_t)printThread, NULL, &attr);  if(tid == NULL){  printf("\[Thread Test\] Failed to create printThread!\\r\\n");  }  // 休眠5秒  osDelay(500);  // 终止线程  osStatus\_t status = osThreadTerminate(tid);  printf("\[Thread Test\] printThread stop, status = %d.\\r\\n", status);  }  APP\_FEATURE\_INIT(threadTest);  

4.编写gn文件

static\_library("thread\_demo"){  sources = \[  "singleThread.c"  \]  include\_dirs = \[  "//commonlibrary/utils\_lite/include",  "//device/soc/hisilicon/hi3861v100/hi3861\_adapter/kal/cmsis"  \]  
}  

5.编写app下的gn文件

注意的是,这次的写法与上次不同,是因为笔者的样例文件名和静态模块的名字是一样的就可以简写。

执行效果

▍多线程的封装

在处理业务的时候,我们一般是多线程的背景,下面笔者将创建线程函数封装起来,方便大家创建多线程
osThreadId_t newThread(char *name, osThreadFunc_t func, void *arg){// 定义线程和属性osThreadAttr_t attr = {name, 0, NULL, 0, NULL, 1024, osPriorityNormal, 0, 0};// 创建线程osThreadId_t tid = osThreadNew(func, arg, &attr);if(tid == NULL){printf("[newThread] osThreadNew(%s) failed.\r\n", name);}return tid;
}

线程部分先体会到这里,想要探索更过线程相关的API,笔者这里提供了API网站,供大家参考学习。

CMSIS_OS2 Thread API:https://arm-software.github.io/CMSIS_5/RTOS2/html/os2MigrationFunctions.html#mig_threadMgmt

软件定时器

下面我们介绍软件定时器,老样子我们先来介绍以下软件定时器。软件定时器是一种在软件层面上实现的计时器机制,用于在特定的时间间隔内执行特定的任务或触发特定的事件。它不依赖于硬件定时器,而是通过软件编程的方式实现。举一个例子,手机应用。

当你使用手机上的某个应用时,你可能会注意到,如果你在一段时间内没有进行任何操作,应用程序会自动断开连接并要求你重新登录。这是为了保护你的账号安全并释放服务器资源。类似的设定都是有软件定时器实现的,下面进行实际操作,让大家体会一下软件定时器。

任务

创建一个软件定时器,用来模拟上述手机应用的例子。为了方便理解,假设从此刻开始,我们不对手机做任何操作,也就是说,我们的回调函数只需要单纯的计算应用不被操作的时常即可。

操作

1.新建样例目录

applications/sample/wifi-iot/app/thread_demo

2.新建源文件和gn文件

applications/sample/wifi-iot/app/thread_demo/singleThread.c

applications/sample/wifi-iot/app/thread_demo/BUILD.gn

3.编写源码

创建软件定时器

osTimerId\_t osTimerNew (osTimerFunc\_t func, osTimerType\_t type, void \*argument, const osTimerAttr\_t \*attr);  

func: 软件定时器的回调函数

type:软件定时器的种类

argument:软件定时器回调函数的参数

attr:软件定时器的属性

返回值:返回软件定时器的id, id为空则说明软件定时器失败

typedef enum {  /\*\* One-shot timer \*/  osTimerOnce               = 0,  /\*\* Repeating timer \*/  osTimerPeriodic           = 1  
} osTimerType\_t;  

软件定时器的种类有两个,分为一次性定时器和周期性定时器,一次性在执行完回调函数后就会停止计数,而周期性定时器会重复触发,每次触发重新计时。根据不同的需求我们可以选择使用不同的软件定时器。

启动软件定时器

osStatus\_t osTimerStart (osTimerId\_t timer\_id, uint32\_t ticks);  

timer_id:软件定时器的参数,指定要启动哪个软件定时器

ticks:等待多少个ticks执行回调函数,在Hi3861中 100个ticks为1秒

返回值:软件定时器的状态码,在线程部分已经展示给大家了全部的状态码

停止定时器

osStatus\_t osTimerStop (osTimerId\_t timer\_id);  

这个函数很简单,只需要传软件定时器的id,即可停止软件计时器,并且返回他的状态码

删除定时器

osStatus\_t osTimerDelete (osTimerId\_t timer\_id);  

删除和停止类似,就不多说明了。

下面是源代码

#include <stdio.h>  
#include "ohos\_init.h"  
// CMSIS  
#include "cmsis\_os2.h"  
// POSIX  
#include <unistd.h>  // 为操作软件的时间  
static int times = 0;  // 软件定时器回调函数  
void timerFunction(void){  times++;  printf("\[Timer Test\] Timer is Running, times = %d.\\r\\n", times);  
}  // 主函数  
void timerMain(void){  // 创建软件定时器  osTimerId\_t tid = osTimerNew(timerFunction, osTimerPeriodic, NULL, NULL);  if(tid == NULL){  printf("\[Timer Test\] Failed to create a timer!\\r\\n");  return;  } else {  printf("\[Timer Test\] Create a timer success!\\r\\n");  }  // 启动软件定时器,每1秒执行一次回调函数  osStatus\_t status = osTimerStart(tid, 100);  // 当超过三个周期位操作软件时,关闭软件  while(times <= 3){  osDelay(100);  }  // 停止软件定时器  status = osTimerStop(tid);  // 删除软件定时器  status = osTimerDelete(tid);  printf("\[Timer Test\] Time Out!\\r\\n");  
}  void TimerTest(void){  // 创建测试线程  osThreadAttr\_t attr;  attr.name = "timerMain";  attr.attr\_bits = 0U;  attr.cb\_mem = NULL;  attr.cb\_size = 0U;  attr.stack\_mem = NULL;  attr.stack\_size = 0U;  attr.priority = osPriorityNormal;  // 启动测试线程  osThreadId\_t tid = osThreadNew((osThreadFunc\_t)timerMain, NULL, &attr);  if(tid == NULL){  printf("\[Timer Test\] Failed to created timerMain!\\r\\n");  }  
}  APP\_FEATURE\_INIT(TimerTest);  

4.编写gn文件

static\_library("timer\_demo"){  sources = \[  "timer.c"  \]  include\_dirs = \[  "//commonlibrary/utils\_lite/include",  "//device/soc/hisilicon/hi3861v100/hi3861\_adapter/kal/cmsis"  \]  
}  

5.编写app下的gn文件

执行效果

软件定时器的API相对较少,这里还是提供所有的软件定时器API

CMSIS_OS2 Timer API:https://arm-software.github.io/CMSIS_5/RTOS2/html/os2MigrationFunctions.html#mig_timer

▍互斥锁

线程的状态

在介绍互斥锁之前,我们有必要去了解一下线程的状态,或者说线程的生命周期。避免伙伴们因为不够熟悉线程而对这个互斥锁的概念感到困难。

首先介绍一下线程的几个状态,他们分别有:

  • 创建

创建线程,在OpenHarmony的源码中,线程的属性被封装成了一个名为”osThreadAttr_t“的结构体

typedef struct {const char *name;uint32_t attr_bits;void *cb_mem;uint32_t cb_size;void *stack_mem;uint32_t stack_size;osPriority_t priority;TZ_ModuleId_t tz_module;uint32_t reserved;
} osThreadAttr_t;
  • name:线程的名称。
  • attr_bits:线程属性位。
  • cb_mem:线程控制块的内存地址。
  • cb_size:线程控制块的内存大小。
  • stack_mem:线程栈的内存地址。
  • stack_size:线程栈的大小。
  • priority:线程的优先级。
  • tz_module:线程所属的TrustZone模块。
  • reserved:保留字段。

当我们创建一个线程的时候,系统就会为该线程分配所需要的资源,将线程加入到系统的线程调度队列中,此时线程已经处在就绪状态了。

  • 就绪

线程一旦被创建,就会进入就绪状态,他表示我们完成的线程的创建(线程相关属性的初始化),但是并未运行,线程正在等待操作系统调度程序,将其调度运行起来。

  • 运行

之前我们介绍了一个关于线程的API,他可以将就绪状态的线程加入到活跃线程组

osThreadId\_t osThreadNew (osThreadFunc\_t func, void \*argument, const osThreadAttr\_t \*attr);  

此时的线程将会占用部分cpu资源执行他的程序,直到程序结束,或者线程被阻塞,cpu被抢占等。

  • 阻塞

线程阻塞,可以理解为线程无法继续往下执行,但时线程执行的程序不会直接退出,他会进入到等待的状态,直到相关资源被释放,线程可以继续执行。

  • 终止

上篇我们介绍了线程终止的API

sStatus\_t status = osThreadTerminate(osThreadId\_t tid);  

此时线程完成了自己的代码逻辑,我们方可主动调用该API彻底删除这个线程。

当然如果具体细分,其实不止这五个状态,但是了解了这五个状态就足够了。下面我们来聊聊什么是互斥锁。

互斥锁简介

互斥锁的应用场景时处理多线程,资源访问的问题。这里还是给大家举一个例子:动物园卖票。

在我们的程序设计中,往往会有共有资源,每个线程都可以进来访问这些资源。这里的共有资源就是我们的门票,一个售票口就是一个线程,当有人来窗口买票,我们的门票就会减少一张。当然一个动物园的流量时巨大的,我们不能只设立一个售票口,这样的效率是很低的。京东淘宝抢购秒杀也一样,我们必须设立多个窗口,在同一时刻为多个人处理业务。多线程解决了效率问题但也带来了安全隐患。

我们假设一个这样的场景,动物园仅剩一张门票,但是有两个在不同的窗口同时付了钱,当售票员为他们拿票的时候就会发现,少了一张票,两个人都付了钱都想要那张票,就陷入了一个死局,无奈动物园只能让他们都进去。但是在程序中体现的可能就是一个bug,动物园的门票剩余数为:-1。

if(count > 0){  count--;  
}  

售票的逻辑很简单,只要票数大于零,还有票能卖,那就在此基础上减掉一。问题就在于,两个线程同时在count = 1的时候通过了if判断,都执行的count–;那么就会出现了count = -1,也就是票数为负 的bug结果。

互斥锁的出现就很好地解决了一个问题,他能够阻止上面两个线程同时访问资源的同步行为,也就是说当一个线程进入这个if语句后,别的线程都不能进入。形象起来说,就像对这段代码加了锁,只有有钥匙的线程才能够访问它。

互斥锁API
通过对几个API的介绍,让大家知道怎么为一段代码加上互斥锁

1.创建互斥锁

与线程的定义一样,互斥锁也封装成了一个结构体

typedef struct {  /\*\* Mutex name \*/  const char                   \*name;  /\*\* Reserved attribute bits \*/  uint32\_t                 attr\_bits;  /\*\* Memory for the mutex control block \*/  void                      \*cb\_mem;  /\*\* Size of the memory for the mutex control block \*/  uint32\_t                   cb\_size;  
} osMutexAttr\_t;  
  • name:互斥锁的名称
  • attr_bits:保留的属性位
  • cb_mem:互斥锁控制块的内存
  • cb_size:互斥锁控制块内存的大小

2.获取互斥锁的id

osMutexId\_t osMutexNew(const osMutexAttr\_t \*attr);  

将我们上面定义的互斥锁属性传入函数,返回互斥锁的id

3.线程获取互斥锁

osStatus\_t osMutexAcquire(osMutexId\_t mutex\_id, uint32\_t timeout);  

传入互斥锁id,设置我们的延迟时间,当线程获取到我们的互斥锁时,返回的状态是osOK。

下面是全部的状态码

typedef enum {  /\*\* Operation completed successfully \*/  osOK                      =  0,  /\*\* Unspecified error \*/  osError                   = \-1,  /\*\* Timeout \*/  osErrorTimeout            = \-2,  /\*\* Resource error \*/  osErrorResource           = \-3,  /\*\* Incorrect parameter \*/  osErrorParameter          = \-4,  /\*\* Insufficient memory \*/  osErrorNoMemory           = \-5,  /\*\* Service interruption \*/  osErrorISR                = \-6,  /\*\* Reserved. It is used to prevent the compiler from optimizing enumerations. \*/  osStatusReserved          = 0x7FFFFFFF  
} osStatus\_t;  

4.释放锁

osStatus\_t osMutexRelease(osMutexId\_t mutex\_id);  

当我们的线程执行完业务后需要把锁释放出来,让别的线程获取锁,执行业务。当然这个过程是线程之间的竞争,一个线程可能一直得不到锁,一个线程也可能刚释放锁又获得锁,我们可以添加休眠操作,提高锁在各个线程间的分配。

其他API请参考:

Mutex API:https://arm-software.github.io/CMSIS_5/RTOS2/html/os2MigrationFunctions.html#mig_mutex

操作

1.新建样例目录

applications/sample/wifi-iot/app/mutex_demo

2.新建源文件和gn文件

applications/sample/wifi-iot/app/mutex_demo/mutex.c

applications/sample/wifi-iot/app/mutex_demo/BUILD.gn

3.源码编写

因为已经介绍了主要的API,这里就直接给伙伴们上源码了。

#include <stdio.h>  
#include <unistd.h>  
#include "ohos\_init.h"  
#include "cmsis\_os2.h"  // 模拟动物园门票数  
static int count = 100;  // 售票业务线程  
void outThread(void \*args){  // 获取互斥锁  osMutexId\_t \*mid = (osMutexId\_t \*)args;  // 每个线程都在不停地买票  while(1){  // 获取锁,进入业务流程  if(osMutexAcquire(\*mid, 100) == osOK){  if(count > 0){  count--;  // 设置提示信息  printf("\[Mutex Test\] Thread %s get a value, the less is %d.\\r\\n", osThreadGetName(osThreadGetId()), count);  } else {  // 告知这些线程已经没有门票卖了,线程结束  printf("\[Mutex Test\] The value is out!\\r\\n");  osThreadTerminate(osThreadGetId());  }  }  // 释放锁  osMutexRelease(\*mid);  osDelay(5);  }  
}  
// 创建线程封装  
osThreadId\_t createThreads(char \*name, osThreadFunc\_t func, void \*args){  osThreadAttr\_t attr = {  name, 0, NULL, 0, NULL, 1024, osPriorityNormal, 0, 0  };  osThreadId\_t tid = osThreadNew(func, args, &attr);  return tid;  
}  // 主函数实现多线程的创建,执行买票业务  
void mutexMain(void){  // 创建互斥锁  osMutexAttr\_t attr = {0};  // 获取互斥锁的id  osMutexId\_t mid = osMutexNew(&attr);  if(mid == NULL){  printf("\[Mutex Test\] Failed to create a mutex!\\r\\n");  }  // 创建多线程  osThreadId\_t tid1 = createThreads("Thread\_1", (osThreadFunc\_t)outThread, &mid);  osThreadId\_t tid2 = createThreads("Thread\_2", (osThreadFunc\_t)outThread, &mid);  osThreadId\_t tid3 = createThreads("Thread\_3", (osThreadFunc\_t)outThread, &mid);  osDelay(1000);  }  // 测试线程  
void MainTest(void){  osThreadId\_t tid = createThreads("MainTest", (osThreadFunc\_t)mutexMain, NULL);  
}  APP\_FEATURE\_INIT(MainTest);  

4.编写gn文件

static\_library("mutex\_demo"){  sources = \[  "mutex.c"  \]  include\_dirs = \[  "//commonlibrary/utils\_lite/include",  "//device/soc/hisilicon/hi3861v100/hi3861\_adapter/kal/cmsis"  \]  
}  

5.编写app目录下的gn文件

结果展示

可能有的伙伴们看到这里不太清晰,会觉得这段代码真的上锁了吗

 if(osMutexAcquire(\*mid, 100) == osOK){  if(count > 0){  count--;  printf("\[Mutex Test\] Thread %s get a value, the less is %d.\\r\\n", osThreadGetName(osThreadGetId()), count);  } else {  printf("\[Mutex Test\] The value is out!\\r\\n");  osThreadTerminate(osThreadGetId());  }  }  

那么我们可以不使用互斥锁再次执行这段代码

结果展示如下:

注:这里笔者还另外多加了3个线程,一共六个线程,可以看出来控制台的输出很混乱,当一个线程在执行输出指令时,另一个线程也插了进来执行输出指令所造成的,再看票数,也是出现了明显的问题。因此互斥锁在处理多线程问题时,起到了非常重要的作用。

可能有伙伴好奇,怎么没有负数票的出现,笔者作为学习者,代码能力也有限,可能写出来的案例并不是非常精确,仅供参考。

▍信号量

对大部分初学者而言,这又是一个新名词,什么是信号量?其实他跟我们上篇介绍的互斥锁很像。互斥锁是在多线程中允许一个线程访问资源,信号量是在多线程中允许多个线程访问资源。

初学者一定会感到困惑,为了解决多线程访问资源的风险我们限制只能有一个线程在某一时刻访问资源,现在这个信号量怎么有允许多个线程访问资源呢。笔者刚开始也比较困惑,结合一些案例理解后,也是明白了这样的设计初衷。实际上,信号量,互斥锁本就是两种不同的多形成同步运行机制,在特定的应用场景下,有特定的需求,而信号量,互斥锁可以满足不同的需求,具体是什么需求呢,举个例子给大家。

卖票,我们的确需要互斥锁解决多线程可能带来的错误,那么如果是验票呢,为了提高效率,我们开设多个入口同时验票且不会发生冲突,信号量就做到了限制线程数量访问资源的作用。如果我们不限制并发的数量,我们的程序占用资源可能会非常大,甚至崩溃,就像检票的入口没有被明确入口数量一样,门口的人们会乱成一片。

信号量API

1.创建信号量

osSemaphoreId\_t osSemaphoreNew(uint32\_t max\_count, uint32\_t initial\_count, const osSemaphoreAttr\_t \*attr);  

参数解释:最大容量量,初始容纳量,信号量属性

最大容纳量说明了,我们的资源最大能被多少线程访问

初始容纳量说明了,我们当前实际能有多少线程访问资源,因为一个信号对应一个线程的许可。

返回值:信号量的id

2.获取信号量

osStatus\_t osSemaphoreAcquire(osSemaphoreId\_t semaphore\_id, uint32\_t timeout);  

参数解释:信号量的id,等待时长

返回值:状态码 (介绍很多遍了,就不说明了)

我们往往会在timoeout处设置为 oswaitForever

#define osWaitForever         0xFFFFFFFFU  

这样我们的线程就会一直等,直到有信号量空出来被他获取,才执行后续的代码。

3.释放信号量

osStatus\_t osSemaphoreRelease(osSemaphoreId\_t semaphore\_id);  

很简单,传入信号量的id,就可以释放一个信号量出来。

其他的API请参考:

Semaphore API:https://arm-software.github.io/CMSIS\_5/RTOS2/html/os2MigrationFunctions.html#mig\_sem
任务

有4个售票窗,2个检票口,每次会有4个人来买票,然后去检票,用互斥锁控制购票,信号量控制检票。

操作

1.新建样例目录

applications/sample/wifi-iot/app/semaphore\_demo

2.新建源文件和gn文件

applications/sample/wifi-iot/app/semaphore_demo/semaphore.c

applications/sample/wifi-iot/app/semaphore_demo/BUILD.gn

3.源码编写

直接上源码了

#include <stdio.h>  
#include <unistd.h>  
#include "ohos\_init.h"  
#include "cmsis\_os2.h"  // 售票口 4  
#define OUT\_NUM 4  
// 检票口 2  
#define IN\_NUM 2  // 信号量  
osSemaphoreId\_t sid;  // 待检票人数  
static int people = 0;  // 售票业务  
void outThread(void \*args){  // 获取互斥锁  osMutexId\_t \*mid = (osMutexId\_t \*)args;  while(1){  if(osMutexAcquire(\*mid, 100) == osOK){  // 卖一张票,带检票的人数就会加一位  people++;  printf("\[SEMAPHORE TEST\] out, people: %d.\\r\\n", people);  }  osMutexRelease(\*mid);  osDelay(50);  }  
}  // 检票业务  
void inThread(void \*args){  // 获取信号量  osSemaphoreAcquire(sid, osWaitForever);  while(1){  if(people > 0){  people--;  printf("\[SEMAPHORE TEST\] in, people: %d.\\r\\n", people);  }  osSemaphoreRelease(sid);  osDelay(100);  }  
}  // 创建线程封装  
osThreadId\_t createThreads(char \*name, osThreadFunc\_t func, void \*args){  osThreadAttr\_t attr = {  name, 0, NULL, 0, NULL, 1024 \* 2, osPriorityNormal, 0, 0  };  osThreadId\_t tid = osThreadNew(func, args, &attr);  return tid;  
}  // 主线程  
void SemaphoreMain(void){  // 创建信号量  sid = osSemaphoreNew(IN\_NUM, IN\_NUM, NULL);  // 创建互斥锁  osMutexAttr\_t attr = {0};  // 获取互斥锁的id  osMutexId\_t mid = osMutexNew(&attr);  // 创建售票线程  for(int i = 0; i < OUT\_NUM; i++){  createThreads("", (osThreadFunc\_t)outThread, &mid);  }  // 创建检票线程  for(int i = 0; i < IN\_NUM; i++){  createThreads("", (osThreadFunc\_t)inThread, NULL);  }  
}  // 测试函数  
void MainTest(){  createThreads("MainTest", (osThreadFunc\_t)SemaphoreMain, NULL);  
}  APP\_FEATURE\_INIT(MainTest);  

4.编写gn文件

static\_library("semaphore\_demo"){  sources = \[  "semaphore.c"     \]  include\_dirs = \[  "//utils/native/lite/include",  \]  
}  

5.编写app目录下的gn文件

结果展示

大家可以加长检票业务的休眠时间,我们的检票口是两个,in的业务一定是两个一起执行的。

总之信号量和互斥锁是多线程管理中的重点,大家一定要好好体会他们的作用和区别。

▍消息队列

本篇的最后我们来介绍消息队列。队列相信大部分朋友都不陌生,是一种基本且常用的数据结构,这里笔者就不介绍队列的相关信息了。那么什么是消息队列呢?有什么应用场景呢。

消息队列也是多线程,高并发中的处理方式,大家可以理解为“同步入队,异步出队”。老样子从一个案例解释,网购秒杀。

在网购秒杀时,会有上万甚至上百万的流量涌入服务器,下单即可看作一个请求,向服务器请求获取某个商品。服务器处理,生成买家的订单号。当然强大服务器的也无法在同一时刻支持如此多的请求,并且商品的数量也不足被所有人购买,这个时候,我们的消息队列就会同步接受大家的请求,所有的请求都会被压进一个队列中,服务器从队列中依次获取消息,确保不会因为资源被占用而导致系统崩溃。

消息队列API

1.创建消息队列

osMessageQueueId\_t osMessageQueueNew (uint32\_t msg\_count, uint32\_t msg\_size, const osMessageQueueAttr\_t \*attr);  

参数说明:

  • msg_count:消息队列中的消息数量。
  • msg_size:消息队列中每个消息的大小,通常我们的消息会用一个结构体来自定义消息的内容
  • attr:指向消息队列属性。

该函数的返回值是osMessageQueueId_t类型,表示消息队列的ID。如果创建消息队列失败,函数将返回NULL。

2.向消息队列加入消息

osStatus\_t osMessageQueuePut (osMessageQueueId\_t mq\_id, const void \*msg\_ptr, uint8\_t msg\_prio, uint32\_t timeout);  

参数说明:

  • mq_id:消息队列的ID,通过调用osMessageQueueNew函数获得。
  • msg_ptr:指向要放入消息队列的消息缓冲区的指针,也就是我们将结构体的指针转递给函数
  • msg_prio:消息的优先级。
  • timeout:延时,使用osWaitForever,线程就会一直等待直到队列中有空余的位置。

该函数的返回值是osStatus_t类型,表示函数执行的结果。

3.从消息队列中接受消息

osStatus\_t osMessageQueueGet (osMessageQueueId\_t mq\_id, void \*msg\_ptr, uint8\_t \*msg\_prio, uint32\_t timeout);  

参数说明:

  • mq_id:消息队列的ID,通过调用osMessageQueueNew函数获得。
  • msg_ptr:指向存储从消息队列中获取的消息的缓冲区的指针。
  • msg_prio:指向存储从消息队列中获取的消息的优先级的缓冲区的指针。
  • timeout:延时,使用osWaitForever,线程就会一直等待直到队列中有消息了。

该函数的返回值是osStatus_t类型,表示函数执行的结果。

4.删除消息队列

osStatus\_t osMessageQueueDelete (osMessageQueueId\_t mq\_id);  

参数说明:

  • mq_id:消息队列的ID,通过调用osMessageQueueNew函数获得。

该函数的返回值是osStatus_t类型,表示函数执行的结果。

其他的API请参考:

MessageQueue API:https://arm-software.github.io/CMSIS\_5/RTOS2/html/os2MigrationFunctions.html#mig\_msgQueue
任务

模拟抢购秒杀,假设我们有10个线程,15个大小的消息队列,5件商品。

操作

1.新建样例目录

applications/sample/wifi-iot/app/queue_demo

2.新建源文件和gn文件

applications/sample/wifi-iot/app/queue_demo/queue.c

applications/sample/wifi-iot/app/queue_demo/BUILD.gn

3.编写源码

直接上源码

#include <stdio.h>  
#include <unistd.h>  
#include "ohos\_init.h"  
#include "cmsis\_os2.h"  // 定义消息队列的大小  
#define QUEUE\_SIZE 15  // 定义请求数量  
#define REQ\_SIZE 10  // 定义消息的结构  
typedef struct{  osThreadId\_t tid;  
} message\_queue;  // 创建消息队列id  
osMessageQueueId\_t qid;  // 模拟发送业务  
void sendThread(void){  // 定义一个消息结构  message\_queue sentry;  sentry.tid = osThreadGetId();  osDelay(100);  // 消息入队  osMessageQueuePut(qid, (const void\*)&sentry, 0, osWaitForever);  // 设置提示信息  printf("\[MESSAGEQUEUE TEST\] %d send a message.\\r\\n", sentry.tid);  
}  // 模拟处理业务  
void receiverThread(void){  // 定义一个消息结构  message\_queue rentry;  int less = 5;  while(less > 0){  osMessageQueueGet(qid, (void \*)&rentry, NULL, osWaitForever);  less--;  printf("\[MESSAGEQUEUE TEST\] %d get a product, less = %d.\\r\\n", rentry.tid, less);  osDelay(5);  }  printf("\[MESSAGEQUEUE TEST\] over!\\r\\n");  
}  // 创建线程封装  
osThreadId\_t createThreads(char \*name, osThreadFunc\_t func, void \*args){  osThreadAttr\_t attr = {  name, 0, NULL, 0, NULL, 1024 \* 2, osPriorityNormal, 0, 0  };  osThreadId\_t tid = osThreadNew(func, args, &attr);  return tid;  
}  // 主线程  
void MessageQueueMain(void){  // 创建一个消息队列  qid = osMessageQueueNew(QUEUE\_SIZE, sizeof(message\_queue), NULL);  // 创建发送线程  for(int i = 0; i < REQ\_SIZE; i++){  createThreads("", (osThreadFunc\_t)sendThread, NULL);  }  osDelay(5);  // 创建接收线程  createThreads("", (osThreadFunc\_t)receiverThread, NULL);  osDelay(500);  // 删除消息队列  osMessageQueueDelete(qid);  
}  // 测试函数  
void MainTest(){  createThreads("MainTest", (osThreadFunc\_t)MessageQueueMain, NULL);  
}  APP\_FEATURE\_INIT(MainTest);  

4.编写gn

static\_library("queue\_demo"){  sources = \[  "queue.c",  \]  include\_dirs = \[  "//utils/native/lite/include",  \]  
}  

5.编写app目录下的gn

结果展示

因为线程创建是循环创建的,先创建的线程就优先发送了请求,可以看的出来,前五个线程抢到了商品。如果线程可以同时发送请求,争抢入队的时机,模拟将会更加准确一些,这里只是简单的模拟。

为了能让大家更好的学习鸿蒙(HarmonyOS NEXT)开发技术,这边特意整理了《鸿蒙开发学习手册》(共计890页),希望对大家有所帮助:https://qr21.cn/FV7h05

《鸿蒙开发学习手册》:

如何快速入门:https://qr21.cn/FV7h05

  1. 基本概念
  2. 构建第一个ArkTS应用
  3. ……

开发基础知识:https://qr21.cn/FV7h05

  1. 应用基础知识
  2. 配置文件
  3. 应用数据管理
  4. 应用安全管理
  5. 应用隐私保护
  6. 三方应用调用管控机制
  7. 资源分类与访问
  8. 学习ArkTS语言
  9. ……

基于ArkTS 开发:https://qr21.cn/FV7h05

  1. Ability开发
  2. UI开发
  3. 公共事件与通知
  4. 窗口管理
  5. 媒体
  6. 安全
  7. 网络与链接
  8. 电话服务
  9. 数据管理
  10. 后台任务(Background Task)管理
  11. 设备管理
  12. 设备使用信息统计
  13. DFX
  14. 国际化开发
  15. 折叠屏系列
  16. ……

鸿蒙开发面试真题(含参考答案):https://qr18.cn/F781PH

鸿蒙开发面试大盘集篇(共计319页):https://qr18.cn/F781PH

1.项目开发必备面试题
2.性能优化方向
3.架构方向
4.鸿蒙开发系统底层方向
5.鸿蒙音视频开发方向
6.鸿蒙车载开发方向
7.鸿蒙南向开发方向

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.rhkb.cn/news/287508.html

如若内容造成侵权/违法违规/事实不符,请联系长河编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

java Web会议信息管理系统 用eclipse定制开发mysql数据库BS模式java编程jdbc

一、源码特点 jsp 会议信息管理系统是一套完善的web设计系统&#xff0c;对理解JSP java SERLVET mvc编程开发语言有帮助&#xff0c;系统具有完整的源代码和数据库&#xff0c;系统主要采用B/S模式开发。开发环境为TOMCAT7.0,eclipse开发&#xff0c;数据库为Mysql5.0&am…

嵌入式|蓝桥杯STM32G431(HAL库开发)——CT117E学习笔记11:数字电位器MCP4017

系列文章目录 嵌入式|蓝桥杯STM32G431&#xff08;HAL库开发&#xff09;——CT117E学习笔记01&#xff1a;赛事介绍与硬件平台 嵌入式|蓝桥杯STM32G431&#xff08;HAL库开发&#xff09;——CT117E学习笔记02&#xff1a;开发环境安装 嵌入式|蓝桥杯STM32G431&#xff08;…

一周是一年的2%

今天读到阮一峰的293期周刊&#xff0c;其中有句话很让我震动——“一周是一年的2%”。 过去的时间里&#xff0c;我都没有在意时间的流逝&#xff0c;过的好的时候就觉得一周过的好快&#xff0c;周三一过这周也就过去了&#xff0c;过的不好的时候就感觉很漫长。 确实&…

C# wpf 嵌入hwnd窗口

WPF Hwnd窗口互操作系列 第一章 嵌入Hwnd窗口&#xff08;本章&#xff09; 第二章 嵌入WinForm控件 第三章 嵌入WPF控件 文章目录 WPF Hwnd窗口互操作系列前言一、如何实现1、继承HwndHost2、实现抽象方法3、xaml中使用HwndHost控件 二、具体实现1、Win32窗口2、HwndSource窗…

【Python】搭建 Python 环境

目 录 一.安装 Python二.安装 PyCharm 要想能够进行 Python 开发&#xff0c;就需要搭建好 Python 的环境 需要安装的环境主要是两个部分&#xff1a; 运行环境: Python开发环境: PyCharm 一.安装 Python (1) 找到官方网站 (2) 找到下载页面 选择 “Download for Windows”…

PHPCMS v9城市分站插件

PHPCMS自带的有多站点功能&#xff0c;但是用过的朋友都知道&#xff0c;自带的多站点功能有很多的不方便之处&#xff0c;例如站点栏目没法公用&#xff0c;每个站点都需要创建模型、每个站点都需要单独添加内容&#xff0c;还有站点必须静态化。如果你内容很多这些功能当然无…

Unity VisionOS开发流程

Unity开发环境 Unity Pro, Unity Enterprise and Unity Industry 国际版 Mac Unity Editor(Apple silicon) visionOS Build Support (experimental) 实验版 Unity 2022.3.11f1 NOTE: 国际版与国服版Pro账通用&#xff0c;需要激活Pro的许可证。官方模板v0.6.2,非Pro版本会打…

安科瑞智慧安全用电综合解决方案

概述 智慧用电管理云平台是智慧城市建设的延伸成果&#xff0c;将电力物联网技术与云平台的大数据分析功能相结合&#xff0c;实现用电信息的可视化管理&#xff0c;可帮助用户实现安全用电&#xff0c;节约用电&#xff0c;可靠用电。平台支持web&#xff0c;app&#xff0c;微…

【回眸】Tessy 单元测试软件使用指南(三)怎么打桩和指针赋值和测试

目录 前言 Tessy 如何进行打桩操作 普通桩 高级桩 手写桩 Tessy单元测试之指针相关测试注意事项 有类型的指针&#xff08;非函数指针&#xff09;&#xff1a; 有类型的函数指针&#xff1a; void 类型的指针&#xff1a; 结语 前言 进行单元测试之后&#xff0c;但凡…

zookeeper面试题

文章目录 ZooKeeper 是什么&#xff1f;ZooKeeper 提供什么&#xff1f;1. 文件系统2. 通知机制 ZooKeeper 文件系统四种类型的 znode1. PERSISTENT (持久化目录节点)2. PERSISTENT_SEQUENTIAL (持久化顺序编号目录节点)3. EPHEMERAL (临时目录节点)4. EPHEMERAL_SEQUENTIAL (临…

C语言------指针(2)

前面已经向大家介绍了指针的一些基本内容&#xff0c;接下来&#xff0c;就在再我来先大家讲解一下指针的其他内容。 1. 数组名的理解 int arr[10] { 1,2,3,4,5,6,7,8,9,10 }; 在学习数组的过程中&#xff0c;我们肯定会写过以上代码&#xff0c;我们知道 int 是该数组的数…

【C语言】C语言运算符优先级详解

文章目录 &#x1f4dd;前言&#x1f309;运算符优先级简述 &#x1f320;逻辑与和逻辑或&#x1f309;赋值和逗号运算符 &#x1f320;位运算&#x1f309;条件表达式&#x1f309;位运算与算术运算结合&#x1f309;混合使用条件表达式和赋值运算符&#x1f309; 逗号运算符的…

UE5、CesiumForUnreal实现海量POI撒点显示与聚合功能

1.实现目标 POI是UE+GIS三维场景中经常需要展示的要素,在UE中常用的表示POI方法有两种。一种是Mesh,即空间的方式;另一种是Widget,即屏幕上的方式,本文这里使用的是Widget屏幕展示的形式来表示POI。 本文这里使用的POI点位数量共3.3w+,采用直接网格聚合算法,并进行性能优…

Chrome/Edge 使用 Markdown Viewer 查看 Markdown 格式文件

Chrome/Edge 使用 Markdown Viewer 查看 Markdown 格式文件 0. 引言1. 安装 Markdown Viewer 插件2. 使用 Markdown Viewer 阅读 Markdown 格式文件 0. 引言 大部分程序员都喜欢 Markdown 格式的文件&#xff0c;这时给一些没有在电脑上安装 Markdown 编辑器的同事分享资料时&…

苹果与百度合作,将在iPhone 16中使用生成式AI

3月25日&#xff0c;《科创板日报》消息&#xff0c;苹果将与百度进行技术合作&#xff0c;为今年即将发布的iPhone16、Mac系统和iOS 18提供生成式AI&#xff08;AIGC&#xff09;功能。 据悉&#xff0c;苹果曾与阿里巴巴以及另外一家国产大模型厂商进行了技术合作洽谈。最终…

[自研开源] 数据集成之分批传输 v0.7

开源地址&#xff1a;gitee | github 详细介绍&#xff1a;MyData 基于 Web API 的数据集成平台 部署文档&#xff1a;用 Docker 部署 MyData 使用手册&#xff1a;MyData 使用手册 试用体验&#xff1a;https://demo.mydata.work 交流Q群&#xff1a;430089673 介绍 本篇基于…

Hana数据库 No columns were bound prior to calling SQLFetch or SQLFetchScroll

在php调用hana数据库的一个sql时报错了&#xff0c;查表结构的sql&#xff1a; select * from sys.table_columns where table_name VBAP SQLSTATE[SL009]: <<Unknown error>>: 0 [unixODBC][Driver Manager]No columns were bound prior to calling SQLFetch …

55、Qt/事件机制相关学习20240326

一、代码实现设置闹钟&#xff0c;到时间后语音提醒用户。示意图如下&#xff1a; 代码&#xff1a; #include "widget.h" #include "ui_widget.h"Widget::Widget(QWidget *parent): QWidget(parent), ui(new Ui::Widget), speecher(new QTextToSpeech(t…

canvas跟随鼠标画有透明度的矩形边框

提示&#xff1a;canvas跟随鼠标画有透明度的矩形边框 文章目录 前言一、跟随鼠标画有透明度的矩形边框总结 前言 一、跟随鼠标画有透明度的矩形边框 test.html <!DOCTYPE html> <html lang"en"> <head><meta charset"UTF-8">&…

【笔记】OpenHarmony设备开发:搭建开发环境(Ubuntu 20.04,VirtualBox 7.0.14)

参考&#xff1a;搭建开发环境&#xff08;HarmonyOS Device&#xff09; Note&#xff1a;Windows系统虚拟机中Ubuntu系统安装完成后&#xff0c;根据指导完成Ubuntu20.04基础环境配置&#xff08;HarmonyOS Connect 开发工具系列课&#xff09; 系统要求 Windows系统要求&…