基于 OpenHarmony 5.0 的星闪轻量型设备应用开发——Ch2 OpenHarmony LiteOS-M 内核应用开发
写在前面: 此篇是系列文章《基于 OpenHarmony5.0 的星闪轻量型设备应用开发》的第 2 章。本篇介绍了如何在 OpenHarmony 5.0 框架下,针对 WS63 进行 LiteOS-M 内核应用工程的开发。
为了方便读者学习,需要OpenHarmony 5.0 WS63 SDK 的小伙伴可以访问润和软件官方网站咨询获取。
2.1 OpenHarmony LiteOS-M 内核简介
2.2.1 OpenHarmony 的多内核结构设计
OpenHarmony 操作系统整体遵从分层设计, 自顶向下分别为应用层、框架层、系统服务层以及内核层。
作为一个分布式操作系统框架,OpenHarmony 同样有内核层的概念。内核层是 OpenHarmony 的最底层,提供了包括任务调度、线程互斥等在内的各种基本操作系统内核功能。
OpenHarmony 的内核层由内核子系统、驱动子系统组成。其中内核子系统采用多核(Linux 内核或者 LiteOS)设计,支持针对不同资源受限设备选用合适的 OS 内核。例如,OpenHarmony 在轻量系统和小型系统上使用 LiteOS 内核,它是面向 IoT 领域的实时操作系统内核,同时具备 RTOS 的轻快还有 Linux 的用的特点。按其系统体量细分,LiteOS 内核又可分为 LiteOS-A 以及 LiteOS-M 这两个内核,其中后者主要针对轻量系统(mini system),具有小体积、低功耗、高性能的特点。
图 2.1-1 OpenHarmony LiteOS-M 内核架构图
2.1.2 OSAL
OSAL(Operating System Abstraction Layer,操作系统抽象层),其核心目标是屏蔽不同操作系统之间的差异,为上层应用程序提供统一的编程接口。在实际开发中,存在着众多类型的操作系统,如常见的实时操作系统(RTOS)FreeRTOS、uC/OS,以及通用操作系统 Linux、Windows 等。这些操作系统在任务管理、内存管理、文件系统等方面的实现方式和接口差异较大。OSAL 的作用就是将这些差异屏蔽起来,为上层应用提供统一的操作接口。例如,不同的 RTOS 对于任务创建的函数名、参数列表可能都不一样,通过 OSAL 可以提供一个统一的函数,让开发者无需关心底层具体使用的是哪种 RTOS。
2.1.3 WS63 的应用开发介绍
润和软件出品的 WS63 模组基于海思 Hi3863 解决方案,该模组内置了高性能 RISC-V 架构的 32 位 MCU,在 OpenHarmony 的设备分级中属于“轻量型”。
由于 OpenHarmony 官方发布的源码仅仅提供了不同内核的框架支持,如果针对某款芯片做应用开发,需要将与芯片有关的 SDK 移植到 OpenHarmony 源码框架下才行。润和软件已经完成了 WS63 SDK 的 OpenHarmony 移植工作,并将其源码开源供开发者使用。
WS63 SDK 存放在 OpenHarmony 源码目录的 device/soc/hisilicon/ws63v100/sdk 目录中,以下将该目录简称为“SDK 目录”。
2.2 LiteOS-M 内核线程功能
2.2.1 内核线程及其调度管理
(1)任务与线程
从系统的角度来看,**任务(Task)**是竞争系统资源的最小单元,OpenHarmony LiteOS-M 的任务模块可以给用户提供多个任务,实现任务间的切换。
在操作系统领域中,与任务类似的概念又被称为线程(Thread),且在 OpenHarmony 源代码中相关 API 名称及结构体均使用 Thread 一词,故而本文中不对任务、线程两个名字做严格区分,且更多以“线程”代之。
(2)线程调度
LiteOS-M 的任务模块采用抢占式线程调度机制:
- 共分为 32 个任务优先级(0…31),其中数字越小优先级越高,0 代表最高优先级,31 代表最低优先级
- 高优先级线程可以打断低优先级线程,低优先级线程必须在高优先级线程阻塞或者结束后才能得到调度运行
- 相同优先级线程支持使用时间片轮转调度和 FIFO 调度方式
(3)线程状态
在系统初始化完成之后,创建的线程就可以在系统中竞争一定的资源,此时将由内核进行统一调度。线程通常可分以下几种状态:
- 初始化(Init):该线程正在被创建。
- 就绪(Ready):该线程在就绪队列中,等待被 CPU 执行。
- 运行(Running):该线程正在被执行。
- 阻塞(Blocked):该线程被阻塞挂起。这一状态包括:pend(因为锁、事件、信号量等阻塞)、suspend(主动 pend)、delay(延时阻塞)、pendtime(因为锁、事件、信号量时间等超时等待)。
- 退出(Exit):该线程运行结束,等待父线程回收其控制块资源。
(4)线程状态迁移
图 2.2-1 线程状态迁移
- 创建(Init) → 就绪(Ready):
线程创建拿到控制块后为创建状态,处于线程初始化阶段,当线程初始化完成将线程插入调度队列,此时线程进入就绪状态。
- 就绪(Ready) → 运行(Running):
线程创建后进入就绪态,发生线程切换时,就绪列表中最高优先级的线程被执行,从而进入运行态,但此刻该线程会从就绪列表中删除。
- 运行(Running) → 阻塞(Blocked):
正在运行的线程发生阻塞(挂起、延时、读信号量等)时,该线程会从就绪列表中删除,线程状态由运行态变成阻塞态,然后发生线程切换,运行就绪列表中剩余最高优先级线程。
- 阻塞(Blocked) → 就绪(Ready) / 阻塞(Blocked) → 运行(Running):
阻塞的线程被恢复后(线程恢复、延时时间超时、读信号量超时或读到信号量等),此时被恢复的线程会被加入就绪列表,从而由阻塞态变成就绪态;此时如果被恢复线程的优先级高于正在运行线程的优先级,则会发生线程切换,将该线程由就绪态变成运行态。
- 就绪(Ready) → 阻塞(Blocked):
线程也有可能在就绪态时被阻塞(挂起),此时线程状态会由就绪态转变为阻塞态,该线程从就绪列表中删除,不会参与线程调度,直到该线程被恢复。
- 运行(Running) → 就绪(Ready):
有更高优先级线程创建或者恢复后,会发生线程调度,此刻就绪列表中最高优先级线程变为运行态,那么原先运行的线程由运行态变为就绪态,并加入就绪列表中。
- 运行(Running) → 退出(Exit):
运行中的线程运行结束,线程状态由运行态变为退出态。
- 阻塞(Blocked) → 退出(Exit):
阻塞的线程调用删除接口,线程状态由阻塞态变为退出态。
线程创建后,用户态可以执行线程调度、挂起、/恢复、延时等操作,同时也可以设置线程优先级和调度策略,获取线程优先级和调度策略。这些操作均可以通过对应的内核线程接口函数来完成(见 2.2.3 内核线程接口介绍)。
2.2.2 内核线程开发的基本步骤
OpenHarmony 为内核线程的相关操作提供了诸多接口函数,通过调用对应的接口函数,即可实现不同的功能。一般的开发步骤可以总结如下:
- 编写线程入口函数:根据实际需求,编写独立 / 协同的任务(线程)入口函数,并定义函数参数
- 预设置线程的参数属性:为不同线程设置各自的名称、控制块信息(可选)、栈区信息(可选)、不同的优先级别
- 新建内核线程:调用内核功能接口,创建线程,并接收返回的线程 id
- 视需求进行内核线程调度、管理:
- 查询线程运行状态
- 挂起、恢复线程
- 等待线程执行完毕或者提前结束线程
2.2.3 内核线程接口介绍
针对 WS63 的 LiteOS-M 内核线程的接口均在 SDK 目录/kernel/osal/include/schedule/osal_task.h
文件中声明,在使用的时候需要包含这个头文件。
(1)创建内核线程
- 原型:
osal_task *osal_kthread_create(osal_kthread_handler handler, void *data, const char *name, unsigned int stack_size);
- 参数:
- handler 是一个 osal_kthread_handler 类型的函数指针,该指针指向的是线程功能函数的入口,即该线程如果被创建成功,将会执行由 handler 指向的函数功能。因此,在创建内核线程之前,首先要定义该线程的入口函数。
【说明】线程的入口函数有统一的函数原型。在
SDK目录/kernel/osal/include/schedule/osal_task.h
文件中可以找到 osal_kthread_handler 的定义(如代码 2.2-1
所示),它是一个函数指针类型定义,描述了线程入口函数的参数、返回值信息。线程入口函数内容与常规代码并没有显著区别。通常需要注意几点:
- 使用强制类型转换处理 param 参数,如果不使用它,用 (void) 进行修饰
- 使用 osal_msleep 代替 sleep 等方法进行延时
- 灵活使用事件机制与信号量机制
代码 2.2-2 是一个线程入口函数定义的示例。
// 代码 2.2-1
/// Entry point of a thread.
typedef struct {void *task;
} osal_task;
typedef int (*osal_kthread_handler)(void *data);
// 代码 2.2-2int Task1 (void *param)
{// 告知编译器不去检查 param 是否被使用(void)param;// 写个死循环while(1){// 每隔 1s 发送一行内容osal_printk("Hello!\r\n");osal_msleep(1000); // osal_msleep 使用 1ms 基准延时,因此参数指定为 1000 时将延时 1s}
}
- data 为 void * 空指针类型,实际上代表一个万能数据类型,可以通过强制类型转换将其他类型的值作为参数传递给线程。
- name 为一个 const char 类型数据,用于指定线程的名称。
- stack_size 是一个 unsigned int 型的数据,用于指定线程运行所需要的栈空间大小。(这里强调一下,不同的线程需要的栈空间大小不同,如果指定的栈空间过小,会导致线程运行出错)
- 返回值:
- 如果成功创建了线程,则返回指向该线程的 osal_task 类型的指针,其定义见代码 2.2-1 。
- 如果未成功创建,则返回值为 NULL。
- 调用:以下是一个调用的示例代码,注意该函数在调用前,应确保已经对线程的入口函数进行了定义。
// 代码 2.2-3// 调用前,已经定义了 Task1 // 创建线程
osal_task * task1 = osal_kthread_create((osal_kthread_handler)Task1, NULL, "Task1", 0x1000);// 对 task1 的值进行判断以确定线程是否创建成功,后续也可使用该值对线程进行调度、管理
if(task1 != NULL)
{osal_printk("Task1 created with handler: %p\r\n", task1);
}
else
{osal_printk("Task1 failed\r\n");
}
(2)设置优先级
- 原型:
int osal_kthread_set_priority(osal_task *task, unsigned int priority);
- 参数:
- task 是一个 osal_task * 类型的数据,即通过 osal_kthread_create 函数创建线程时得到的返回值。
- 参数 priority 为 unsigned int 类型,用于设置线程的优先级。在 osal_task.h 文件中对 LiteOS 的线程优先级有如下的宏定义:
// 代码 2.2-4
/// Priority values.
#define OSAL_TASK_PRIORITY_ABOVE_HIGH 2
#define OSAL_TASK_PRIORITY_HIGH 3
#define OSAL_TASK_PRIORITY_BELOW_HIGH 4
#define OSAL_TASK_PRIORITY_ABOVE_MIDDLE 5
#define OSAL_TASK_PRIORITY_MIDDLE 6
#define OSAL_TASK_PRIORITY_BELOW_MIDDLE 7
#define OSAL_TASK_PRIORITY_ABOVE_LOW 8
#define OSAL_TASK_PRIORITY_LOW 10
#define OSAL_TASK_PRIORITY_BELOW_LOW 11
【说明】虽然在 SDK 注释中表明只能使用以下预定义的宏
OSAL_TASK_PRIORITY_HIGH
OSAL_TASK_PRIORITY_MIDDLE
OSAL_TASK_PRIORITY_LOW但在实际的使用中,依然可以通过序号的方式对线程指定优先级的大小。
- 返回值:
- 线程创建成功返回 OSAL_SUCCESS,对应整型数据 0
- 当线程创建失败时返回 OSAL_FAILURE,对应整型数据 -1
- 调用:线程需要先被创建才能指定优先级,以下代码是一个调用的示例。
// 代码 2.2-5
// 在之前已经创建了 task1// 设置线程 task1 优先级为 HIGH
osal_kthread_set_priority(task1, OSAL_TASK_PRIORITY_HIGH);
(3)挂起指定的线程
- 原型:
void osal_kthread_suspend(osal_task *task);
- 参数:task 是一个 osal_task * 类型的数据,即通过 osal_kthread_create 函数创建线程时得到的返回值。
- 返回值:无返回值
- 调用:该函数用于对指定的线程执行“挂起”操作,一旦该函数被执行,则指定线程将会从线程调度队列中被删除。具体调用示例参见代码 2.2-6。
(4)将挂起的线程恢复到调度队列中
- 原型:
void osal_kthread_resume(osal_task *task);
- 参数:task 是一个 osal_task * 类型的数据,即通过 osal_kthread_create 函数创建线程时得到的返回值。
- 返回值:无返回值
- 调用:该函数用于对被“挂起”的内核线程执行恢复操作。注意:这个函数应当在 osal_kthead_suspend 函数执行之后再调用。具体调用示例参见代码 2.2-6。
(5)锁定线程调度
- 原型:
void osal_kthread_lock(void);
- 参数:无
- 返回值:无返回值
- 调用:该函数用于对系统线程的调度进行锁定,具体调用示例参见代码 2.2-6。
(6)解锁线程调度
- 原型:
void osal_kthread_unlock(void);
- 参数:无
- 返回值:无返回值
- 调用:该函数用于回复系统线程的调度,具体调用示例参见代码 2.2-6。
(7)销毁指定的线程
- 原型:
void osal_kthread_destroy(osal_task *task, unsigned int stop_flag);
- 参数:
- task 是一个 osal_task * 类型的数据,即通过 osal_kthread_create 函数创建线程时得到的返回值。
- stop_flag 是一个无符号的整型数据,用于指示当前需要销毁的线程是否存在,如果该值为 0,表示线程不存在,通常情况该值不能为 0.
- 返回值:无返回值
- 调用:该函数用于对将已经创建的线程所占用的空间进行回收,特别强调的是,这个函数执行时并不能对线程进行终止,它仅仅是将线程占用的空间回收了,换句话说,如果在销毁线程前没有对其进行任务的终止,那将会带来严重的后果,所以该函数慎用。具体调用示例参见代码 2.2-6。
2.2.4 内核线程功能开发实现
基于 OpenHarmony 的应用开发需要依次通过“创建工程”、“编写功能代码”、“编写和修改配置文件”、“编译”和“烧写”等步骤,本书所有示例工程均遵循上述步骤。
本示例工程演示如何创建两个任务线程,其中任务 1 正常运行时,每隔 1s 在控制台打印一行内容;任务 2 则负责处理任务 1 的调度,每隔 5s 对任务 1 进行挂起、恢复的操作。
(1)创建工程
OpenHarmony 的工程目录一般存放在 application/sample/wifi-iot/app
目录下,以下将该目录统称为“工程目录”。本章所有示例工程都建在工程目录下新建的 kernel 文件夹下, 本章称该目录为 “内核开发工程目录”。
在内核开发工程目录下新建 “kernel_01_task_demo” 文件夹,并在该文件夹中新建“kernel_01_task_demo.c”文件和“BUILD.gn”文件。
(2)编写功能代码
kernel_01_task_demo.c 文件内容如下:
// 代码 2.2-6// 必要头文件
#include "osal_debug.h" // 用于支持 osal_printk
#include "common_def.h" // 常用的类型定义
#include "soc_osal.h" // osal 内核所有头文件引用合集
#include "app_init.h" // 用于支持 app_run 方法// 任务 1 的线程入口函数
int Task1 (void*param)
{// 并不需要使用参数,告知编译器不要检查此参数是否使用(void)param;// 每 1s 输出一行内容while(1){osal_printk("[1] Hello, this is Task 1.\r\n");osal_msleep(1000); // 延迟 1s}
}// 任务 2 的线程入口函数
int Task2 (void*param)
{// 从线程参数中取出所需要的值,任务 1 的线程 handlerosal_task * task1 =(osal_task *) param;while(1){osal_printk("[2] Hello, this is Task 2.\r\n");// 每过 5s 切换任务 1 的运行状态osal_msleep(5000);osal_kthread_suspend(task1);osal_printk("[2] Task 1 suspended.\r\n");osal_msleep(5000);osal_kthread_resume(task1);osal_printk("[2] Task 1 resumed.\r\n");}
}// 系统入口函数
void TaskDemoEntry (void)
{osal_printk("\r\n========= Hello, Task Demo! =========\r\n");osal_kthread_lock();// 创建任务 1 线程osal_task * task1 = osal_kthread_create((osal_kthread_handler)Task1, NULL, "Task1", 0x1000);if(task1 != NULL){osal_printk("Task1 created with handler: %p\r\n", task1);}else{osal_printk("Task1 failed\r\n");}// 创建任务 2 线程osal_task * task2 = osal_kthread_create((osal_kthread_handler)Task2, (void *)Task1, "Task2", 0x1000);if(task2!= NULL){osal_printk("Task2 created with handler: %p\r\n", task2);}else{osal_printk("Task2 failed\r\n");}osal_kthread_unlock();
}app_run(TaskDemoEntry);
其中 osal_msleep
用于在系统层次实现 1ms 级别的延时,由内核进行实现,在延时期间不会造成其他任务的阻塞。例如 osal_msleep(1000)
将实现一个 1s 的延时。
(3)编写配置文件
接着是编译配置 BUILD.gn
文件,内容如下:
# 代码 2.2-7static_library("kernel_01_TaskDemo") {sources = [# 添加参与编译的 .c"kernel_01_task_demo.c",]# 在参与编译的源码文件中,引用了一批头文件,
# 这些头文件所在的路径需要填写到 include_dirs 数组中,
# 以下字符串中的 “//” 代表 SDK 的根目录。include_dirs = [# 添加头文件搜索路径"//commonlibrary/utils_lite/include","//device/soc/hisilicon/ws63v100/sdk/kernel/osal/include/debug","//device/soc/hisilicon/ws63v100/sdk/include","//device/soc/hisilicon/ws63v100/sdk/kernel/osal/include","//device/soc/hisilicon/ws63v100/sdk/middleware/utils/app_init"]
}
【说明】基于 OpenHarmony 的 WS63 工程编译采用的是 GN+Ninja
工具链进行配置,编译。每个独立的应用工程都需要包含一个 BUILD.gn 文件。BUILD.gn文件由三部分内容(目标、源文件、头文件路径)构成,其中:
- static_library 中指定业务模块的编译结果,为静态库文件 libXXXX.a(其中 XXXX 表示static_library 中指定的名称,本例中 XXXX 为 kernel_01_TaskDemo),开发者根据实际情况完成填写。
- sources 中指定静态库 .a 所依赖的 .c 文件及其路径,若路径中包含"//“则表示绝对路径(此处为代码根路径),若不包含”//"则表示相对路径。
- include_dirs 中指定 source 所需要依赖的 .h 文件路径。
(4)修改工程配置文件
一个基于 OpenHarmony 的 WS63 工程的配置文件有三个,以下分别对其修改方法进行介绍。
1) 修改“工程目录”下的 BUILD.gn
文件,修改后的代码如下所示:
# 代码 2.2-8 import("//build/lite/config/component/lite_component.gni")lite_component("app") {features = [ # "startup" "kernel/kernel_01_task_demo:kernel_01_TaskDemo",]
}
【说明】
- lite_component(“app”) 编译生成的组件的名字叫 “app”。
- features 是一个数组,其中包含的是待编译组件“app”所拥有的功能特性,换句话说就是“app”组件中都有哪些功能。如果需要多个功能,那么各个功能以数组元素的方式进行组织;如果只需要一个功能,那么将数组中无需编译的功能注释即可,注释符号是“#”。
- kernel/kernel_01_task_demo 表示待编译的工程在“工程目录”的存放路径。
- kernel_01_TaskDemo 是在“工程目录/kernel/kernel_01_task_demo/BUILD.gn”中所指定的 static_library 的名字。这个名字必须一致,且注意大小写敏感。
2)修改 SDK 目录/build/config/target_config/ws63/config.py 文件,'ws63-liteos-app'
中添加 "kernel_01_TaskDemo",
,具体添加位置如代码 2.2-9 所示:
# 代码 2.2-9'ws63-liteos-app': {# 以上代码省略'ram_component': [# 这里部分组件名称省略'xo_trim_port',"mqtt","coap","kernel_01_TaskDemo" # 添加 kernel_01_TaskDemo 到这里],# 以下代码省略
},
3)修改 SDK 目录/libs_url/ws63/cmake/ohos.cmake 文件,在 COMPONENT_LIST 项中添加 kernel_01_TaskDemo,具体添加位置如代码 2.2-10 所示:
# 代码 2.2-10# 以上代码省略
elseif(${TARGET_COMMAND} MATCHES "ws63-liteos-app")
set(COMPONENT_LIST "begetutil" "hilog_lite_static" "samgr_adapter" "bootstrap" "fsmanager_static" "hal_update_static" "hilog_static" "inithook" "samgr_source""broadcast" "hal_file_static" "init_log" "native_file" "udidcomm""cjson_static" "hal_sys_param" "hichainsdk" "hota" "init_utils" "param_client_lite""hiview_lite_static" "hal_sysparam" "hievent_lite_static" "huks_3.0_sdk" "samgr" "blackbox_lite" "hal_iothardware" "wifiservice""hidumper_mini" "kernel_01_TaskDemo")
endif()
# 以下代码省略
(5)编译工程
在 VS Code 工具中,打开内置终端工具,会自动进入当前OpenHarmony 的源码目录下,直接运行编译命令 hb build -f
,等待编译完成即可。
【说明】在执行 hb build -f 命令后,整个工程项目会被重新编译。每次执行该命令,都会在 out
目录的制品文件目录下将之前执行该命令时生成的 build.log
文件以创建文件时的时间戳重新命名,不仅如此,如果编译出现错误,也会创建对应的 error.log
文件。如此,随着编译次数的增多,会导致该目录下的这些日志文件以及编译过程中产生的临时文件越来越多。改进的方法是将命令改为:
rm -rf out && hb set -p nearlink_dk_3863 && hb build -f
该命令是三条独立命令的联合执行写法:
- 首先执行 rm -rf out ,用于将 out 目录整个删除。
- 接着执行 hb set -p nearlink_dk_3863 ,用于重新配置编译生成的制品目录。
- 最后执行 hb build -f ,对这个工程进行重新编译。
(6)烧写
参考“第 1 章 1.4 镜像文件的烧写”一节的内容完成烧写。
【说明】如果已经对 BurnTool 工具进行过配置,那么这些配置会保存下来,无需每次使用时再重新配置。
(7)通过串口调试助手查看程序运行结果
由于内核应用开发中很多功能是通过执行 osal_printk 方法,将运行过程的部分情况通过串口打印输出的,因此,本章所有示例工程的运行结构均需要通过 Windows 上可以运行的串口调试助手工具来查看。
下图是本示例工程的运行结果,从图中可以看到:
- 首先进入系统入口函数 TaskDemoEntry 执行了打印 “========= Hello, Task Demo! =========”。
- 接着两个 task 分别被创建,并分别返回了指向 task 的指针的值:0xa34338 和 0xa4bea8。
- 后面 task1 和 task2 的功能相继被调用。
图 2.2-2 线程测试实例串口消息内容
2.3 LiteOS-M 内核定时器功能
2.3.1 内核定时器原理及其功能
(1)定时器简介及原理
定时器(Timer)是一种在特定时间间隔后执行特定任务的机制或设备。然而硬件定时器受硬件的限制,数量上不足以满足实际需求,为提供更多的定时器,LiteOS-M 内核提供了软件定时器功能。这扩展了定时器的数量,允许创建更多的定时业务。
在 OpenHarmony 系统中,内核定时器是基于系统 Tick 时钟中断实现的软件定时器(定时精度与系统 Tick 时钟的周期有关)。当定时器到达设定的 Tick 计数值时,会触发用户定义的回调函数。这种机制使得用户可以通过编写回调函数来处理定时任务。
软件定时器的实现主要有以下几点:
软件定时器是系统资源,在模块初始化的时候已分配了一块连续的内存,系统支持的最大定时器个数由 los_config.h 中的 LOSCFG_BASE_CORE_SWTMR_LIMIT 宏配置。
图 2.3-1 LOSCFG_BASE_CORE_SWTMR_LIMIT 宏配置
- 软件定时器使用了系统的一个队列和一个任务资源,其触发遵循队列规则,先进先出。定时短的定时器总是比定时长的靠近队列头,满足优先被触发的准则。
- 软件定时器以 Tick 为基本计时单位,当用户创建并启动一个定时器时,LiteOS-M 内核会根据当前系统 Tick 时间及用户设置的定时间隔确定该定时器的到期 Tick 时间,并将该定时器控制结构挂入计时全局链表。
- 当 Tick 中断到来时,在 Tick 中断处理函数中扫描软件定时器的计时全局链表,看是否有定时器超时,若有则将超时的定时器记录下来。
- Tick 中断处理函数结束后,软件定时器任务(优先级为最高)被唤醒,在该任务中调用之前记录下来的定时器的超时回调函数。
(2)定时器功能
LiteOS-M 内核提供软件定时器功能, 有:
- 静态裁剪:能通过宏关闭软件定时器功能
- 软件定时器创建
- 软件定时器启动
- 软件定时器停止
- 软件定时器删除
- 软件定时器剩余 Tick 数获取
通过以上功能,开发者可以灵活地创建、管理、启动和停止软件定时器,以满足不同的定时需求。
2.3.2 内核定时器开发的具体步骤
OpenHarmony 为 LiteOS-M 内核定时器的相关操作提供了诸多接口函数,通过调用对应的接口函数,即可实现不同的功能。⼀般的开发步骤可以总结如下:
- 编写定时器回调函数:根据实际需求, 编写回调函数,并定义函数参数。
- 创建内核定时器:调用内核功能接口, 创建定时器, 并进行设定类型、属性等初始化操作。
- 视需求进行内核定时器功能的使用, 如开启、停止定时器等。
2.3.3 内核定时器接口介绍
针对 WS63 的 LiteOS-M 内核定时器的接口均在 SDK 目录/kernel/osal/include/time/osal_timer.h
文件中声明。
(1)创建内核定时器
- 原型:
int osal_timer_init(osal_timer *timer);
- 参数:
- 参数 timer 为 osal_time 结构体类型指针,该结构体类型定义同样在 osal_timer.h 中,具体内容如下:
// 代码 2.3-1typedef struct {void *timer;void (*handler)(unsigned long);unsigned long data; // data for handlerunsigned int interval; // timer timing duration, unit: ms.
} osal_timer;
其中:
1)timer 成员表示定时器句柄,实际指向一个内部结构体,这个成员必须被初始化成 NULL。
2)handler 成员为定时器回调函数指针,需要指向定时器回调函数。
3)data 成员为用于传递给定时器回调的用户参数。
4)interval 成员为定时器触发延时,单位为毫秒(ms)。
建议在初始化 osal_timer 结构体时,先将结构体整体赋初值 0,再手动进行必要的修改,如设置 handler 回调函数指针,设置延时值 interval 等。
- 返回值:
- 创建成功, 返回 OSAL_SUCCESS,
- 创建失败,返回 OSAL_FAILURE 。
- 调用:以下是一个完整的创建内核定时器的示例代码:
// 代码 2.3-2// 创建一个定时器
static osal_timer timer1 = {0};
// 定时器回调函数
void Timer1(unsigned long data)
{(void)data;osal_printk("T1: timer 1 report!\r\n");
}timer1.handler = Timer1; // 指向定时器回调函数
timer1.interval = 2000;// 检测定时器是否创建成功
if(OSAL_SUCCESS != osal_timer_init(&timer1))
{osal_printk("Failed to initialize timer 1!\r\n");
}
【说明】osal_timer_init 函数被调用后,会分配一段内存空间存放定时器对象资源,该指针被存放在 timer
成员中。当不再需要用到定时器时,需要调用 osal_timer_destroy 函数对定时器资源进行销毁。
(2)启动内核定时器
- 原型:
int osal_timer_start(osal_timer *timer);
- 参数:
- timer 为 osal_timer 结构体类型指针,同 osal_timer_init 函数的参数用法。
- 返回值:
- 创建成功,返回 OSAL_SUCCESS,
- 创建失败,返回 OSAL_FAILURE 。
- 调用:在编写好定时器回调函数和初始化函数后, 就可以使调用 osal_timer_start 启动定时器。
// 代码 2.3-3// 首先创建定时器 timer1
// 启动定时器
osal_timer_start(&timer1);
(3)停止内核定时器
- 原型:
int osal_timer_stop(osal_timer *timer);
- 参数:
- timer 为 osal_timer 结构体类型指针,同 osal_timer_init 函数的参数用法。
- 返回值:
- 创建成功,返回 OSAL_SUCCESS,
- 创建失败,返回 OSAL_FAILURE 。
- 调用:启动了内核定时器后可通过 osal_timer_stop停止定时器
// 代码 2.3-4// 首先创建定时器 timer1
// 启动定时器
// 停止定时器
osal_timer_stop(&timer1);
(4)销毁(释放)定时器
- 原型:
int osal_timer_destroy(osal_timer *timer);
- 参数:
- timer 为 osal_timer 结构体类型指针,同 osal_timer_init 函数的参数用法。
- 返回值:
- 创建成功,返回 OSAL_SUCCESS,
- 创建失败,返回 OSAL_FAILURE 。
- 调用:创建好内核定时器后可通过 osal_timer_destroy销毁(释放)定时器
// 代码 2.3-5// 首先创建定时器 timer1
// 停止定时器
osal_timer_destroy(&timer1);
更多关于定时器的功能接口,以及和它们相关的结构体、宏定义等内容,可以从 device/soc/hisilicon/ws63v100/sdk/kernel/osal/include/time/osal_timer.h
文件中进行检索。
2.3.4 内核定时器功能开发实现
以下的工程实例中,创建了两个定时器,并分别为其设置了定时周期以及回调函数。通过完成该实例代码的编写、编译后,将生成的镜像文件烧写至 WS63 开发板,可以通过串口调试助手查看这两个定时器的运行情况。
(1)创建工程
在内核开发工程目录下新建 “kernel_02_timer_demo” 文件夹,并在该文件夹中新建“kernel_02_timer_demo.c”文件和“BUILD.gn”文件。
(2)编写功能代码
kernel_02_timer_demo.c 的代码如下:
// 代码 2.3-6#include "osal_debug.h"
#include "common_def.h"
#include "soc_osal.h"
#include "app_init.h"#define THREAD_STACK_SIZE 0x1000
#define THREAD1_PRIO OSAL_TASK_PRIORITY_MIDDLE
#define TIMER1_INTERVAL 2000 // timer1 的定时间隔为 2s
#define TIMER2_INTERVAL 1000 // timer2 的定时间隔是 1sstatic osal_timer timer1 = {0}, timer2 = {0}; // 初始化两个定时器结构体变量/* Task1 为内核线程* 该线程的功能是对定时器 timer1 和 timer2 进行控制* 包括:对两个定时器的启动、停止和销毁。
*/
int Task1(void * data)
{(void)data;// 等待 2sosal_msleep(2000);// 启动两个定时器osal_printk("starting timers ...\r\n");osal_timer_start(&timer1);osal_timer_start(&timer2);// 等待 10sosal_msleep(10000);// 停止两个定时器osal_timer_stop(&timer1);osal_timer_stop(&timer2);osal_printk("timers stopped.\r\n");// 销毁释放定时器资源osal_timer_destroy(&timer1);osal_timer_destroy(&timer2);
}/* Timer1 是 timer1 定时时间到时的回调函数,* 功能是打印一句话“T1: timer 1 report!”到调试串口输出显示。
*/
void Timer1(unsigned long data)
{(void)data;osal_printk("T1: timer 1 report!\r\n");
}/* Timer2 是 timer2 定时时间到时的回调函数,* 功能是打印一句话“T2: timer 2 report!”到调试串口输出显示,* 并再次启动 timer2
*/
void Timer2(unsigned long data)
{(void)data;osal_printk("T2: timer 2 report!\r\n");// 再次启动定时器 2 实现周期性定时osal_timer_start(&timer2);
}void kernel_02_timer_demo(void)
{osal_printk("\r\n========= Hello, Event Demo! =========\r\n");osal_printk("Enter kernel_02_timer_example()!\r\n");osal_kthread_lock();osal_task * task1 = osal_kthread_create(Task1, NULL, "Task1", THREAD_STACK_SIZE);if(task1 != NULL)osal_kthread_set_priority(task1, THREAD1_PRIO);elseosal_printk("Failed to create thread 1!\r\n");timer1.handler = Timer1; // 为 timer1 指定定时结束的回调函数timer1.interval = TIMER1_INTERVAL; // 为 timer1 指定定时间隔if(OSAL_SUCCESS != osal_timer_init(&timer1)) // 创建 timer1{osal_printk("Failed to initialize timer 1!\r\n");}timer2.handler = Timer2; // 为 timer2 指定定时结束的回调函数timer2.interval = TIMER2_INTERVAL; // 为 timer2 指定定时间隔if(OSAL_SUCCESS != osal_timer_init(&timer2)) // 创建 timer2{osal_printk("Failed to initialize timer 2!\r\n");}osal_kthread_unlock();
}
// 程序入口
app_run(kernel_02_timer_demo);
(3)编写配置文件
接着是编译配置 BUILD.gn
文件,内容如下:
# 代码 2.3-7static_library("kernel_02_TimerDemo") {sources = [# 添加参与编译的 .c"kernel_02_timer_demo.c"]include_dirs = [# 添加头文件搜索路径"//commonlibrary/utils_lite/include","//device/soc/hisilicon/ws63v100/sdk/kernel/osal/include/debug","//device/soc/hisilicon/ws63v100/sdk/include","//device/soc/hisilicon/ws63v100/sdk/kernel/osal/include","//device/soc/hisilicon/ws63v100/sdk/middleware/utils/app_init","//device/soc/hisilicon/ws63v100/sdk/kernel/osal/include/time"]
}
(4)修改工程配置文件
【说明】基于 OpenHarmony 的 WS63 工程在完成应用开发后,都要对“第 2 章 2.2.4
内核线程功能开发实现(4)修改工程配置文件”中所提到的三个文件做修改,为了节省篇幅,以下在涉及本次示例工程以及后续的示例工程配置文件修改时,都假设您已经学习完毕“第
2 章 2.2”小节的内容,并已经完成了该小节的所有操作的基础上进行说明。
1) 修改“工程目录”下的 BUILD.gn 文件,在 features
数组中添加 "kernel/kernel_02_timer_demo:kernel_02_TimerDemo",
2)修改 SDK 目录/build/config/target_config/ws63/config.py 文件,'ws63-liteos-app'
中'ram_component': []'
添加 "kernel_02_TimerDemo",
3)修改 SDK 目录/libs_url/ws63/cmake/ohos.cmake 文件,在COMPONENT_LIST
项中添加 "kernel_02_TimerDemo"
(5)编译工程
在 VS Code 工具中,打开内置终端工具,进入当前OpenHarmony 的源码目录下,输入命令 rm -rf out && hb set -p nearlink_dk_3863 && hb build -f
,等待编译完成。
(6)烧写
参考“第 1 章 1.4 镜像文件的烧写”一节的内容完成烧写。
【说明】如果已经对 BurnTool 工具进行过配置,那么这些配置会保存下来,无需每次使用时再重新配置。
(7)通过串口调试助手查看程序运行结果
打开串口调试助手,设置正确的 COM 编号,波特率为 115200,其他选择默认,并打开串口。
按下 WS63 开发板上的 RST 按键,观察串口输出端口返回的打印信息,可以看到定时器 time1 和 timer2 按照既定的功能,在定时结束后执行了回调函数功能。具体效果如下图所示。
图 2.3-2 定时器测试实例串⼝消息内容
2.4 OpenHarmony 内核事件功能
2.4.1 内核事件与跨线程同步
(1)内核事件概述
事件是一种高效的任务线程间通信机制,主要用于实现跨线程同步操作。事件具有以下特点:
- 支持一对多和多对多的线程间同步,即一个线程可以等待多个事件,多个线程也可以等待多个事件。但每次写事件仅能触发一个线程从阻塞状态唤醒。
- 具备事件读超时机制,确保线程在指定时间内未收到事件时能够正确处理。
- 事件仅用于跨线程同步,不涉及具体数据的传输。
(2)内核事件工作机制
图 2.4-1 内核事件工作机制原理图
- 事件集合用
void *
类型指针的event
来表示,表示所要创建的内核事件标志。任务线程通过创建事件控制块来实现对事件的触发和等待操作,线程通过“逻辑与”或“逻辑或”与一个事件或多个事件建立关联,形成一个事件集合(事件组),事件的“逻辑或”也称为独立型同步,事件的“逻辑与”也称为关联型同步。事件控制块如下:
// 代码 2.4-1typedef struct {void *event; //表示所要创建的内核事件标志
} osal_event;
- `event` 成员表示所要创建的内核事件标志
- 在读事件操作(获取事件)时,可以在
mode
参数(后文函数接口中会再次提及)中设置读取模式,来选择用户感兴趣的事件,读取模式如下:- 所有事件,
OSAL_WAITMODE_AND
(逻辑与):任务等待所有预期事件发生,读取掩码中所有事件类型,只有读取的所有事件类型都发生了,才能读取成功。 - 任一事件,
OSAL_WAITMODE_OR
(逻辑或):任务会等待其预期的任何一个事件发生,读取掩码中任一事件类型,读取的事件中任意一种事件类型发生了,就可以读取成功了。 - 清除事件,
OSAL_WAITMODE_CLR
:事件标志在事件被读取后立即被清除
- 所有事件,
(3)事件与信号量的区别
事件能够在一定程度上代替信号量,用于任务与任务,中断与任务间的同步,但与信号量不同的是:
- 事件的发送操作是不可累计的,而信号量的释放动作时可以累计的。
- 事件接收任务可以等待多种事件,信号量只能识别单一同步动作,而不能同时等待多个事件的同步。
- 各个事件可以分别发送或一起发送给事件对象。
2.4.2 内核事件开发的具体步骤
OpenHarmony 为内核事件的相关操作提供了诸多接口函数,通过调用对应的接口函数,即可实现不同的内核事件功能。⼀般的开发步骤可以总结如下:
- 创建内核线程:编写线程入口函数、新建并初始化新建内核线程等操作。有关线程操作详见 2.2 章节。
- 创建内核事件:调用内核功能接口, 创建事件标志对象
- 视需求通过调度和管理事件, 如设置事件标记等操作来实现跨线程同步通信。
2.4.3 内核事件接口介绍
针对 WS63 的 LiteOS-M 内核事件的接口均在 SDK 目录/kernel/osal/include/event/osal_event.h
文件中声明。
(1)创建内核事件
- 原型:
int osal_event_init(osal_event *event_obj)
- 参数:event_obj 为 osal_event 结构体类型指针,该结构体定义见代码 2.4-1。
- 返回值:
- 创建成功,返回 OSAL_SUCCESS,
- 创建失败,返回 OSAL_FAILURE 。
- 调用:以下代码演示了如何调用 osal_event_init 进行内核事件的创建。
// 代码 2.4-2// 创建事件标志
osal_event event1_create;
// 检测事件是否创建成功
if(osal_event_init(&event1_create) == OSAL_SUCCESS)
{osal_printk("Event Create is OK!\r\n");
}
elseosal_printk("Event failed to Create!\r\n");
(2)写入事件控制块
当完成了内核事件标志初始化后,可以通过调用 osal_event_write
函数将由传入的事件掩码指定的事件写入由 osal_event
指向的事件控制块中。
- 原型:
int osal_event_write(osal_event *event_obj, unsigned int mask);
- 参数:
- event_obj 为 osal_event 结构体类型指针,该结构体定义见代码 2.4-1。
- mask 是一个无符号的整数据,表示为用户期望发生的事件的掩码
- 返回值:
- 创建成功,返回 OSAL_SUCCESS,
- 创建失败,返回 OSAL_FAILURE 。
- 调用:可如下代码所示写入事件控制块。
// 代码 2.4-3// 写入事件控制块
osal_event_write(&event1_create, 0x00000001U);
(3)读取事件
在 OSAL 的内核机制中,设置了可以通过指定事件控制块、事件掩码、读取模式和超时信息来阻塞或调度任务以读取事件的接口。
- 原型:
int osal_event_read(osal_event *event_obj, unsigned int mask, unsigned int timeout_ms, unsigned int mode);
- 参数:
- event_obj 为 osal_event 结构体类型指针,该结构体定义见代码 2.4-1。
- mask 是一个无符号的整数据,表示为用户期望发生的事件的掩码。
- timeout_ms 是一个无符号的整型数据,百世事件读取的超时时间间隔,单位是毫秒。
- mode 是一个无符号的整型数据,表示事件读取模式,其中:
- OSAL_WAITMODE_AND 表示任务等待所有预期事件发生
- OSAL_WAITMODE_OR 表示任务会等待其预期的任何一个事件发生
- OSAL_WAITMODE_CLR 表示事件标志在事件被读取后立即被清除
- 返回值:
- 创建成功,返回 OSAL_SUCCESS,
- 创建失败,返回 OSAL_FAILURE 。
- 调用:不能在中断服务中调用该函数,不建议在软件定时器回调中使用此 API,并且在 liteos 上禁止使用事件掩码的第 25 位。
(4)清除指定任务中的事件
- 原型:
int osal_event_clear(osal_event *event_obj, unsigned int mask);
- 参数:
- event_obj 为 osal_event 结构体类型指针,该结构体定义见代码 2.4-1。
- mask 是一个无符号的整数据,表示为用户期望发生的事件的掩码
- 返回值:
- 创建成功,返回 OSAL_SUCCESS,
- 创建失败,返回 OSAL_FAILURE 。
- 调用:该函数被执行后,将会清除掉指向事件的指针的值为 0 。具体调用见代码 2.4-4。
(5)销毁指定事件
- 原型:
int osal_event_destroy(osal_event *event_obj);
- 参数:event_obj 为 osal_event 结构体类型指针,该结构体定义见代码 2.4-1。
- 返回值:
- 创建成功,返回 OSAL_SUCCESS,
- 创建失败,返回 OSAL_FAILURE 。
- 调用:该函数用于销毁一个事件,并且可能释放与该事件相关的内存。具体调用见代码 2.4-4。
2.4.4内核事件功能开发实现
以下的示例工程中演示了如何创建两个任务,任务 1 每隔 1 秒钟设置一个事件标志位,总共设置 3 个事件标志位;任务 2 阻塞等待是否接收到 3 个事件标志位,如果接收到了,打印输出。
(1)创建工程
在内核开发工程目录下新建 “kernel_03_event_demo” 文件夹,并在该文件夹中新建“kernel_03_event_demo.c”文件和“BUILD.gn”文件。
(2)编写功能代码
// 代码 2.4-4#include "osal_debug.h"
#include "common_def.h"
#include "soc_osal.h"
#include "app_init.h"#define THREAD_STACK_SIZE 0x1000
#define THREAD1_PRIO OSAL_TASK_PRIORITY_HIGH
#define THREAD2_PRIO OSAL_TASK_PRIORITY_MIDDLEuint32_t event1_Flags = 0x00000001U; // 事件掩码 每一位代表一个事件
uint32_t event2_Flags = 0x00000002U; // 事件掩码 每一位代表一个事件
uint32_t event3_Flags = 0x00000004U; // 事件掩码 每一位代表一个事件// 创建事件标志
static osal_event event_create;
//任务1线程 用于发送事件
int Task1(void*param)
{// 并不需要使用参数,告知编译器不要检查此参数是否使用(void)param;while (1){osal_printk("enter Task 1.......\r\n");osal_event_write(&event_create, event1_Flags); // 设置事件标记 osal_printk("send eventFlag1.......\r\n");osal_msleep(1000); // 1秒osal_event_write(&event_create, event2_Flags); // 设置事件标记 osal_printk("send eventFlag2.......\r\n");osal_msleep(1000); // 1秒osal_event_write(&event_create, event3_Flags); // 设置事件标记 osal_printk("send eventFlag3.......\r\n");osal_msleep(1000); // 1秒}
}// 任务2线程 用于接受事件
int Task2(void*param)
{// 并不需要使用参数,告知编译器不要检查此参数是否使用(void)param;uint32_t flags = 0;while (1){// 永远等待事件标记触发,当接收到 event1_Flags 和 event2_Flags // 和 event3_Flags时才会执行osal_printk函数// OSAL_WAITMODE_AND :全部事件标志位接收到 // OSAL_WAITMODE_OR: 任意一个事件标志位接收到// 当只有一个事件的时候,事件的类型选择哪个都可以flags = osal_event_read(&event_create, event1_Flags | event2_Flags | event3_Flags, OSAL_EVENT_FOREVER, OSAL_WAITMODE_AND);osal_printk("enter Task 2.......\r\n");osal_printk("receive event is OK!\r\n\r\n"); // 事件已经标记//清除事件标志 osal_event_clear(&event_create,event1_Flags | event2_Flags | event3_Flags); }
}
//系统入口函数
static void kernel_03_event_demo(void)
{osal_printk("\r\n========= Hello, Event Demo! =========\r\n");osal_printk("Enter kernel_03_event_demo()!\r\n");osal_kthread_lock();// 检测事件是否创建成功if(osal_event_init(&event_create) == OSAL_SUCCESS){osal_printk("Event Create is OK!\r\n");}elseosal_printk("Event failed to Create!\r\n");osal_task * task1 = osal_kthread_create(Task1, NULL, "Task1", THREAD_STACK_SIZE);if(task1 != NULL)osal_kthread_set_priority(task1, THREAD1_PRIO);elseosal_printk("Failed to create thread 1!\r\n");osal_task * task2 = osal_kthread_create(Task2, NULL, "Task2", THREAD_STACK_SIZE);if(task2 != NULL)osal_kthread_set_priority(task2, THREAD2_PRIO);elseosal_printk("Failed to create thread 2!\r\n");osal_kthread_unlock();}
app_run(kernel_03_event_demo);
(3)编写配置文件
接着是编译配置 BUILD.gn
文件,内容如下:
# 代码 2.4-5static_library("kernel_03_EventDemo") {sources = [# 添加参与编译的 .c"kernel_03_event_demo.c"]include_dirs = [# 添加头文件搜索路径"//commonlibrary/utils_lite/include","//device/soc/hisilicon/ws63v100/sdk/kernel/osal/include/debug","//device/soc/hisilicon/ws63v100/sdk/include","//device/soc/hisilicon/ws63v100/sdk/kernel/osal/include","//device/soc/hisilicon/ws63v100/sdk/middleware/utils/app_init","//device/soc/hisilicon/ws63v100/sdk/kernel/osal/include/event"]
}
(4)修改工程配置文件
1) 修改“工程目录”下的 BUILD.gn 文件,在 features
数组中添加 "kernel/kernel_03_event_demo:kernel_03_EventDemo",
2)修改 SDK 目录/build/config/target_config/ws63/config.py 文件,在'ws63-liteos-app'
中'ram_component': []'
添加 "kernel_03_EventDemo",
3)修改 SDK 目录/libs_url/ws63/cmake/ohos.cmake 文件,在 COMPONENT_LIST
项中添加 "kernel_03_EventDemo"
(5)编译工程
在 VS Code 工具中,打开内置终端工具,进入当前OpenHarmony 的源码目录下,输入命令 rm -rf out && hb set -p nearlink_dk_3863 && hb build -f
,等待编译完成。
(6)烧写
参考“第 1 章 1.4 镜像文件的烧写”一节的内容完成烧写。
(7)通过串口调试助手查看程序运行结果
图 2.4-2 内核事件示例运行效果
2.5 OpenHarmony 互斥锁功能
2.5.1 互斥锁及其运行机制
(1)互斥锁简介
互斥锁是一种特殊的二值性信号量,用于实现对共享资源的独占式处理。互斥锁的状态只有两种,开锁或闭锁。当有线程持有时,互斥锁处于闭锁状态,此线程获得该互斥锁的所有权。当释放它时,该互斥锁被开锁,线程失去该互斥锁的所有权。当一个线程持有互斥锁时,其他线程将不能再对该互斥锁进行开锁或持有。
(2)互斥锁的作用
多线程环境下会存在多个线程访问同一公共资源的场景,而有些公共资源是非共享的,需要任务进行独占式处理,此时互斥锁可被用于对共享资源的保护从而实现独占式访问。另外互斥锁可以解决信号量存在的优先级翻转问题。
(3)互斥锁的运行机制
用互斥锁处理非共享资源的同步访问时,如果有线程访问该资源,则互斥锁为加锁状态。此时其他线程如果想访问这个公共资源则会被阻塞,直到互斥锁被持有该锁的线程释放后,其他线程才能重新访问该公共资源,此时互斥锁再次上锁,如此确保同一时刻只有一个任务正在访问这个公共资源,保证了公共资源操作的完整性。
互斥锁的运行机制如下图:
图 2.5-1 互斥锁运行机制
(4)申请互斥锁的模式
申请互斥锁可分为无阻塞模式、永久阻塞模式以及定时阻塞模式共三种模式。具体详见 2.5.3 互斥锁接口介绍。
2.5.2 互斥锁使用的基本步骤
OpenHarmony 为内核互斥锁的使用提供了一些接口函数,通过调用对应的接口函数,即可实现不同的功能。一般的使用互斥锁的步骤可以总结如下:
- 创建内核互斥锁:调用内核功能接口, 创建内核互斥锁,并记录互斥锁 ID。
- 视需求调度和管理互斥锁, 如申请、释放互斥锁等操作来实现对共享资源的独占式处理。
2.5.3 互斥锁接口介绍
针对 WS63 的 LiteOS-M 内核互斥锁的接口均在 SDK 目录/kernel/osal/include/lock/osal_mutex.h
文件中声明。
(1)创建互斥锁
可调用 OSAL 提供的 osal_mutex_init
函数创建互斥锁。
- 原型:
int osal_mutex_init(osal_mutex *mutex);
- 参数:mutex 是 osal_mutex 结构体类型的指针,该类型的定义同样在 osal_mutex.h 文件中,具体如下:
// 代码 2.5-1typedef struct {void *mutex;
} osal_mutex;
- 返回值:
- 创建成功,返回 OSAL_SUCCESS。
- 创建失败,返回 OSAL_FAILURE。
- 调用:以下代码是创建一个互斥锁的示例。
// 代码 2.5-2// 定义互斥锁结构体变量
osal_mutex mutex;
// 创建互斥锁
if(osal_mutex_init(&mutex) == OSAL_SUCCESS)
{osal_printk("Create mutex is OK!\r\n");
}
(2)获取并持有互斥锁
- 原型:
int osal_mutex_lock(osal_mutex *mutex);
- 参数:mutex 是一个 osal_mutex 结构体类型的指针,即通过 osal_mutex_init 函数创建互斥锁时得到的互斥锁 ID。
- 返回值:
- 执行成功,将返回 TRUE
- 执行失败,将返回 FALSE
- 调用:获取互斥锁和释放互斥锁需要配对使用,具体调用示例参见代码 2.5-4 。
(3)释放互斥锁
- 原型:
void osal_mutex_unlock(osal_mutex *mutex);
- 参数:mutex 是一个 osal_mutex 结构体类型的指针,即通过 osal_mutex_init 函数创建互斥锁时得到的互斥锁 ID。
- 返回值:无。
- 调用:以下代码演示如何在线程中获取持有互斥锁,执行操作后再释放互斥锁 :
// 代码 2.5-3// 定义互斥锁变量
// 创建互斥锁// 请求互斥锁
osal_mutex_lock(&mutex);// 操作共享数据,如读数据
// ...// 释放互斥锁
osal_mutex_unlock(&mutex);
(4)销毁互斥锁
- 原型:
void osal_mutex_destroy(osal_mutex *mutex);
- 参数:mutex 是一个 osal_mutex 结构体类型的指针,即通过 osal_mutex_init 函数创建互斥锁时得到的互斥锁 ID。
- 返回值:无。
- 调用:当任务中不再需要互斥锁时,可以调用该函数将互斥锁释放掉,回收内存空间。
2.5.4 内核互斥锁功能开发实现
以下示例演示了如何创建两个线程,线程 1 向全局数组中存入数据,此时在线程 2 中不能读数据;线程 2 读全局数组中的数据,此时在线程 1 中不能写数据,以此演示互斥锁的作用。
(1)创建工程
在内核开发工程目录下新建 “kernel_04_mutex_demo” 文件夹,并在该文件夹中新建“kernel_04_mutex_demo.c”文件和“BUILD.gn”文件。
(2)编写功能代码
kernel_04_mutex_demo.c 文件内容如下:
// 代码 2.5-4#include "osal_debug.h"
#include "common_def.h"
#include "soc_osal.h"
#include "app_init.h"osal_task * task1;
osal_task * task2;
osal_mutex mutex; // 创建互斥锁变量
uint8_t buff[20] = {0}; // 任务1 线程写数据
int Task1(void *argument)
{(void)argument;osal_printk("enter Task 1.......\r\n");while (1){// 请求互斥锁osal_mutex_lock(&mutex);// 操作共享数据 写数据osal_printk("write Buff Data: \r\n");for (uint8_t i = 0; i < sizeof(buff); i++){buff[i] = i;}osal_printk("\r\n");// 释放互斥锁osal_mutex_unlock(&mutex);osal_msleep(100);}
}// 任务2 线程读数据
int Task2(void *argument)
{(void)argument;osal_printk("enter Task 2.......\r\n");while (1){// 请求互斥锁osal_mutex_lock(&mutex);// 操作共享数据 读数据osal_printk("read Buff Data: \r\n");for (uint8_t i = 0; i < sizeof(buff); i++){osal_printk("%d \r\n", buff[i]);}osal_printk("\r\n");// 释放互斥锁osal_mutex_unlock(&mutex);osal_msleep(100);}
}// 系统入口函数
static void kernel_04_mutex_demo(void)
{osal_printk("\r\n========= Hello, Mutex Demo! =========\r\n");osal_printk("Enter kernel_04_mutex_demo()!\r\n");osal_kthread_lock();// 创建互斥锁if(osal_mutex_init(&mutex) == OSAL_SUCCESS){osal_printk("Create mutex is OK!\r\n");}// 创建任务1 线程osal_task * task1 = osal_kthread_create((osal_kthread_handler)Task1, NULL, "Task1", 0x1000);if (task1 != NULL){osal_printk("Create task1 is OK!\r\n");}// 创建任务2 线程osal_task * task2 = osal_kthread_create((osal_kthread_handler)Task2, NULL, "Task2", 0x1000);if (task2 != NULL){osal_printk("Create task2 is OK!\r\n");}osal_kthread_unlock();
}app_run(kernel_04_mutex_demo);
(3)编写配置文件
接着是编译配置 BUILD.gn
文件,内容如下:
# 代码 2.5-5static_library("kernel_04_MutexDemo") {sources = ["kernel_04_mutex_demo.c"]include_dirs = ["//commonlibrary/utils_lite/include","//device/soc/hisilicon/ws63v100/sdk/kernel/osal/include/debug","//device/soc/hisilicon/ws63v100/sdk/include","//device/soc/hisilicon/ws63v100/sdk/kernel/osal/include","//device/soc/hisilicon/ws63v100/sdk/middleware/utils/app_init"]
}
(4)修改工程配置文件
1) 修改“工程目录”下的 BUILD.gn 文件,在 features
数组中添加 "kernel/kernel_04_mutex_demo:kernel_04_MutexDemo",
2)修改 SDK 目录/build/config/target_config/ws63/config.py 文件,在'ws63-liteos-app'
中'ram_component': []'
添加 "kernel_04_MutexDemo",
3)修改 SDK 目录/libs_url/ws63/cmake/ohos.cmake 文件,在 COMPONENT_LIST
项中添加 "kernel_04_MutexDemo"
(5)编译工程
在 VS Code 工具中,打开内置终端工具,进入当前OpenHarmony 的源码目录下,输入命令 rm -rf out && hb set -p nearlink_dk_3863 && hb build -f
,等待编译完成。
(6)烧写
参考“第 1 章 1.4 镜像文件的烧写”一节的内容完成烧写。
(7)通过串口调试助手查看程序运行结果
图 2.5-2 互斥锁测试实例串口消息
2.6 OpenHarmony 的信号量
2.6.1 信号量及其运行机制
(1)信号量
信号量(Semaphore)是一种实现任务间通信的机制,可以实现任务间同步或共享资源的互斥访问。一个信号量的数据结构中,通常有一个计数值用于对有效资源数的计数,表示剩下的可被使用的共享资源数,其值的含义分以下两种情况:
- 信号量为负值,表示该信号量当前不可获取,因此可能存在正在等待该信号量的任务
- 信号量为正值,表示该信号量当前可被获取
(2)信号量的运行机制
当某一任务申请信号量时,若申请成功,则信号量的计数值会减一;若申请失败,则挂起在该信号量的等待任务队列上,直到有任务释放该信号量,等待任务队列中的任务就会被唤醒并开始执行。
信号量的运行机制如下图所示:
图 2.6-1 信号量运行机制
由图可知,该信号量设置的信号量最大值为四,n个线程依次申请信号量时,只有前四个线程任务申请成功并执行,线程五之后的任务都挂起在该信号量的等待任务队列上,直到线程一任务释放信号量,线程五任务被唤醒并执行,依此类推。
(3)信号量的应用场景
信号量还有一些应用场景,可以用作多种场合中,比如可以实现互斥锁、同步、资源技术等功能,也能方便用于任务与任务,中断与任务的同步中。
- 互斥型信号量
互斥是信号量的一个重要使用场景。当信号量用作互斥时,信号量在创建的时候将最大值设置为1,可以实现两个任务之间的资源进行互斥。如果想要使用临界资源时,先申请信号量,使其变为0,这样其他任务就会因为无法申请到信号量而阻塞,从而保证了临界资源的安全,在使用完临界资源之后,进行释放信号量。
- 同步信号量
信号量用做同步时,信号量在创建时初始值设置为0,任务1要申请信号量而阻塞,任务2可以释放信号量,于是任务1得以进入等待态和运行态,从而达到了两个任务间的同步。
- 计数型信号量
信号量用做资源计数时,信号量的作用是一个特殊的计数器,可以递增或递减,但是此值不能为负值,典型的应用场景是生产者与消费者的场景中。
2.6.2 信号量使用的基本步骤
OpenHarmony 为内核信号量的使用提供了一些接口函数,通过调用对应的接口函数,即可实现不同的功能。一般的使用信号量的步骤可以总结如下:
- 预设置信号量的参数属性
- 新建信号量
- 视需求进行内核信号量调度、管理
2.6.3 信号量接口介绍
针对 WS63 的 LiteOS-M 内核信号量的接口均在 SDK 目录/kernel/osal/include/semaphore/osal_semaphore.h
文件中声明。
(1)创建内核信号量
- 原型:
int osal_sem_init(osal_semaphore *sem, int val);
- 参数:
- sem 是一个 osal_semaphore 结构体类型指针,表示信号量,该结构体类型的定义同样在 osal_semaphore.h 中,具体如下:
// 代码 2.6-1typedef struct {void *sem;
} osal_semaphore;
- val 是一个 int 类型的数据,表示初始可用信号量的数量
- 返回值:
- 创建成功,返回 OSAL_SUCCESS,
- 创建失败,返回 OSAL_FAILURE 。
- 调用:
// 代码 2.6-2// 创建信号量
osal_semaphore Sem1;
//检测是否创建成功
if((osal_sem_init(&Sem1, 1) == 0)
{osal_printk("Sem1 Create is OK!\r\n");
}
elseosal_printk("Sem1 failed to Create!\r\n");
(2)创建二进制信号量
如果工作模式为互斥型信号量,也可调用 OSAL 提供的 osal_sem_binary_sem_init
函数, 用于创建一个特殊的二进制信号量,并配置相关参数。
- 原型:
int osal_sem_binary_sem_init(osal_semaphore *sem, int val);
- 参数:
- sem 是一个 osal_semaphore 结构体类型指针,表示信号量
- val 是一个 int 类型的数据,表示初始可用信号量的数量,取值范围是 0 或 1
- 返回值:
- 创建成功,返回 OSAL_SUCCESS,
- 创建失败,返回 OSAL_FAILURE 。
- 调用:以下示例代码演示如何创建一个特殊的二进制信号量来实现互斥型信号量的功能。
// 代码 2.6-3// 创建信号量
osal_semaphore Sem1;
//检测是否创建成功
if((osal_sem_binary_sem_init(&Sem1, 1) == 0)
{osal_printk("Sem1 Create is OK!\r\n");
}
elseosal_printk("Sem1 failed to Create!\r\n");
(3)申请信号量(阻塞式)
- 原型:
int osal_sem_down(osal_semaphore *sem);
- 参数:sem 是一个 osal_semaphore 结构体类型指针,表示信号量。
- 返回值:
- 创建成功,返回 OSAL_SUCCESS,
- 创建失败,返回 OSAL_FAILURE 。
- 调用:参考代码 2.6-4 。
(4)申请信号量(带超时)。
- 原型:
int osal_sem_down_timeout(osal_semaphore *sem, unsigned int timeout);
- 参数:
- sem 是一个 osal_semaphore 结构体类型指针,表示信号量。
- timeout 为 unsigned int 型数据, 可设置超时时间,单位为毫秒, 最大可为 OSAL_SEM_WAIT_FOREVER。
- 返回值:
- 创建成功,返回 OSAL_SUCCESS,
- 创建失败,返回 OSAL_FAILURE 。
- 调用:参考代码 2.6-4 。
(5)释放信号量。
- 原型:
void osal_sem_up(osal_semaphore *sem);
- 参数:sem 是一个 osal_semaphore 结构体类型指针,表示信号量。
- 返回值:无
- 调用:参考代码 2.6-4 。
(6)销毁信号量并释放信号量资源
- 原型:
void osal_sem_destroy(osal_semaphore *sem);
- 参数: sem 是一个 osal_semaphore 结构体类型指针,表示信号量。
- 返回值:无
- 调用:参考代码 2.6-4 。
2.6.4 互斥型信号量开发实现
以下示例演示了通过创建两个任务线程,其中线程 1 率先获取并持有信号量,10s 后再释放信号量。线程 2 将同时尝试获取并持有信号量,2s 后释放。以此演示内核互斥信号量的用法。
(1)创建工程
在内核开发工程目录下新建 “kernel_05_mutex_semaphore_demo” 文件夹,并在该文件夹中新建“kernel_05_mutex_semaphore_demo.c”文件和“BUILD.gn”文件。
(2)编写功能代码
kernel_05_mutex_semaphore_demo.c 文件内容如下:
// 代码 2.6-4#include "osal_debug.h"
#include "common_def.h"
#include "soc_osal.h"
#include "app_init.h"#define THREAD_STACK_SIZE 0x1000
#define THREAD1_PRIO OSAL_TASK_PRIORITY_HIGH
#define THREAD2_PRIO OSAL_TASK_PRIORITY_MIDDLE// 创建信号量
static osal_semaphore Sem1;void Task1(void * param)
{// 并不需要使用参数,告知编译器不要检查此参数是否使用(void)param;// 获取信号量(信号量 -1),并检测是否获取成功if(osal_sem_down(&Sem1)==OSAL_SUCCESS){osal_printk("Task1: Lock held.\r\n");}osal_msleep(10000); // 等待10sosal_sem_up(&Sem1);// 释放信号量(信号量 +1)osal_printk("Task1: Lock released.\r\n");
}void Task2(void * param)
{// 并不需要使用参数,告知编译器不要检查此参数是否使用(void)param;// 获取信号量(信号量 -1),并检测是否获取成功if(osal_sem_down_timeout(&Sem1, OSAL_SEM_WAIT_FOREVER)==OSAL_SUCCESS){osal_printk("Task2: Lock held.\r\n");}osal_msleep(2000); // 等待2s// 释放信号量(信号量 +1)osal_sem_up(&Sem1);osal_printk("Task2: Lock released.\r\n");
}static void SemaphoreDemo(void)
{osal_printk("\r\n========= Hello, Semaphore Demo! =========\r\n");osal_printk("Enter kernel_05_mutex_semaphore_example()!\r\n");osal_kthread_lock();// 检测二进制信号量是否创建成功if(osal_sem_binary_sem_init(&Sem1, 1) == OSAL_SUCCESS){osal_printk("Sem1 Create is OK!\r\n");}elseosal_printk("Sem1 failed to Create!\r\n");osal_task * task1 = osal_kthread_create(Task1, NULL, "Task1", THREAD_STACK_SIZE);if(task1 != NULL)osal_kthread_set_priority(task1, THREAD1_PRIO);elseosal_printk("Failed to create thread 1!\r\n");osal_task * task2 = osal_kthread_create(Task2, NULL, "Task2", THREAD_STACK_SIZE);if(task2 != NULL)osal_kthread_set_priority(task2, THREAD2_PRIO);elseosal_printk("Failed to create thread 2!\r\n");osal_kthread_unlock();}
app_run(SemaphoreDemo);
(3)编写配置文件
接着是编译配置 BUILD.gn 文件,内容如下:
# 代码 2.6-5static_library("kernel_05_MutexSemaphoreDemo") {sources = ["kernel_05_mutex_semaphore_demo.c"]include_dirs = ["//commonlibrary/utils_lite/include","//device/soc/hisilicon/ws63v100/sdk/kernel/osal/include/debug","//device/soc/hisilicon/ws63v100/sdk/include","//device/soc/hisilicon/ws63v100/sdk/kernel/osal/include","//device/soc/hisilicon/ws63v100/sdk/middleware/utils/app_init","//device/soc/hisilicon/ws63v100/sdk/kernel/osal/include/semaphore"]
}
(4)修改工程配置文件
1) 修改“工程目录”下的 BUILD.gn 文件,在 features
数组中添加 "kernel/kernel_05_mutex_semaphore_demo:kernel_05_MutexSemaphoreDemo",
2)修改 SDK 目录/build/config/target_config/ws63/config.py 文件,在'ws63-liteos-app'
中'ram_component': []'
添加 "kernel_05_MutexSemaphoreDemo",
3)修改 SDK 目录/libs_url/ws63/cmake/ohos.cmake 文件,在 COMPONENT_LIST
项中添加 "kernel_05_MutexSemaphoreDemo"
(5)编译工程
在 VS Code 工具中,打开内置终端工具,进入当前OpenHarmony 的源码目录下,输入命令 rm -rf out && hb set -p nearlink_dk_3863 && hb build -f
,等待编译完成。
(6)烧写
参考“第 1 章 1.4 镜像文件的烧写”一节的内容完成烧写。
(7)通过串口调试助手查看程序运行结果
图 2.6-2 互斥型信号量实例串口消息
由输出可知,在线程 1 获取并持有信号量的 10s 期间,线程 2 一直无法获取到信号量,直到线程 1 将其释放,线程 2 才成功获取。
2.6.5 同步型信号量开发实现
以下示例演示了同步型信号量的相关开发方法。
(1)创建工程
在内核开发工程目录下新建 “kernel_06_sync_semaphore_demo” 文件夹,并在该文件夹中新建“kernel_06_sync_semaphore_demo.c”文件和“BUILD.gn”文件。
(2)编写功能代码
kernel_06_sync_semaphore_demo.c 文件内容如下:
// 代码 2.6-6#include "osal_debug.h"
#include "common_def.h"
#include "soc_osal.h"
#include "app_init.h"#define THREAD_STACK_SIZE 0x1000
#define THREAD_PRIO OSAL_TASK_PRIORITY_MIDDLE
#define SEM_COUNT_INITIAL 0// 创建信号量
static osal_semaphore sem;int Task1(void * param)
{// 并不需要使用参数,告知编译器不要检查此参数是否使用(void)param;// 等待 2sosal_msleep(2000);// 先输出 5 行内容for(int i = 0; i < 5; i ++){osal_printk("[1] task 1 report!\r\n");osal_msleep(1000);}// 获取信号量(信号量 -1),这将导致线程挂起,直到另一个线程释放信号量osal_sem_down(&sem);// 再输出 5 行内容,此时线程 1 和线程 2 同步for(int i = 0; i < 5; i ++){osal_printk("[1] task 1 report!\r\n");osal_msleep(1000);}
}
int Task2(void * param)
{// 并不需要使用参数,告知编译器不要检查此参数是否使用(void)param;// 等待 2.3s,让两个线程错开一定时间osal_msleep(2300);// 先输出 5 行内容,此时线程 1 和线程 2 并未实现同步for(int i = 0; i < 5; i ++){osal_printk("[2] task 2 report!\r\n");osal_msleep(1000);}// 等待1秒osal_msleep(1000);// 释放信号量(信号量 +1),这会解除 Thread1 的挂起osal_sem_up(&sem);// 再输出 5 行内容,此时线程 1 和线程 2 同步for(int i = 0; i < 5; i ++){osal_printk("[2] task 2 report!\r\n");osal_msleep(1000);}
}void SemaphoreExample(void)
{osal_printk("\r\n========= Hello, Sync Semaphore Demo! =========\r\n");osal_printk("Enter kernel_06_sync_semaphore_example()!\r\n");osal_kthread_lock();osal_sem_init(&sem, SEM_COUNT_INITIAL);osal_task * task1 = osal_kthread_create(Task1, NULL, "Task1", THREAD_STACK_SIZE);if(task1 != NULL)osal_kthread_set_priority(task1, THREAD_PRIO);elseosal_printk("Failed to create task 1!\r\n");osal_task * task2 = osal_kthread_create(Task2, NULL, "Task2", THREAD_STACK_SIZE);if(task2 != NULL)osal_kthread_set_priority(task2, THREAD_PRIO);elseosal_printk("Failed to create task 2!\r\n");osal_kthread_unlock();
}app_run(SemaphoreExample);
(3)编写配置文件
接着是编译配置 BUILD.gn 文件,内容如下:
# 代码 2.6-7static_library("kernel_06_SyncSemaphoreDemo") {sources = ["kernel_06_sync_semaphore_demo.c"]include_dirs = ["//commonlibrary/utils_lite/include","//device/soc/hisilicon/ws63v100/sdk/kernel/osal/include/debug","//device/soc/hisilicon/ws63v100/sdk/include","//device/soc/hisilicon/ws63v100/sdk/kernel/osal/include","//device/soc/hisilicon/ws63v100/sdk/middleware/utils/app_init","//device/soc/hisilicon/ws63v100/sdk/kernel/osal/include/semaphore"]
}
(4)修改工程配置文件
1) 修改“工程目录”下的 BUILD.gn 文件,在 features
数组中添加 "kernel/kernel_06_sync_semaphore_demo:kernel_06_SyncSemaphoreDemo",
2)修改 SDK 目录/build/config/target_config/ws63/config.py 文件,在'ws63-liteos-app'
中'ram_component': []'
添加 "kernel_06_SyncSemaphoreDemo",
3)修改 SDK 目录/libs_url/ws63/cmake/ohos.cmake 文件,在 COMPONENT_LIST
项中添加 "kernel_06_SyncSemaphoreDemo"
(5)编译工程
在 VS Code 工具中,打开内置终端工具,进入当前OpenHarmony 的源码目录下,输入命令 rm -rf out && hb set -p nearlink_dk_3863 && hb build -f
,等待编译完成。
(6)烧写
参考“第 1 章 1.4 镜像文件的烧写”一节的内容完成烧写。
(7)通过串口调试助手查看程序运行结果
图 2.6-3 同步型信号量测试实例串口消息
整个程序的功能是创建 Task1 线程,线程将等待 2s,随后每隔 1s 输出一行消息,输出 5 次后等待 Task2 进行同步。创建 Task2 线程,线程将等待 2.3s(以确保两个线程初始处于不同步状态),随后同样每隔 1s 输出一行消息,5 次后等待一段时间,然后与 Task1 进行同步。两个线程在同步之后会再次输出 5 行消息,间隔 1s,此时两个线程的消息应同时打印出来。从串口的输出信息可以看到,在同步操作执行之前,两个线程存在时间差,在同步之后,两个线程的节奏保持一致,输出的时间值完全相同。
2.6.6 计数型信号开发实现
在以下示例工程中,创建两个任务线程,用计数型信号量模拟客人用餐的座位情况。其中线程 1 模拟客人入座用餐,线程 2 模拟客人结束用餐离开。
(1)创建工程
在内核开发工程目录下新建 “kernel_07_count_semaphore_demo” 文件夹,并在该文件夹中新建“kernel_07_count_semaphore_demo.c”文件和“BUILD.gn”文件。
(2)编写功能代码
kernel_07_count_semaphore_demo.c 文件内容如下:
// 代码 2.6-8#include "osal_debug.h"
#include "common_def.h"
#include "soc_osal.h"
#include "app_init.h"//宏定义:定义本例程创建信号量的个数
#define SEM_COUNT_MAX 5// 创建信号量
static osal_semaphore sem;int Task1(void * param)
{// 并不需要使用参数,告知编译器不要检查此参数是否使用(void)param;osal_msleep(1000);//请求信号量osal_sem_down(&sem);// 请求信号量osal_sem_down(&sem);osal_printk("[person 1] and [person 2] are already in seats.\r\n");for(int i = 2; i < 10; i ++){//请求信号量if(osal_sem_down(&sem)==OSAL_SUCCESS){osal_printk("Task1: Lock held.\r\n"); //请求信号量}}osal_printk("No more guests!\r\n");
}int Task2(void * param)
{// 并不需要使用参数,告知编译器不要检查此参数是否使用(void)param;osal_msleep(1000);int i = 0;int a=0;for(a=0;a<10;a++){osal_msleep(1000);osal_printk("[person %d] left seat\r\n", ++ i);osal_sem_up(&sem);osal_msleep(10);}osal_printk("Closing!\r\n");
}void SemaphoreDemo(void)
{osal_printk("\r\n========= Hello,Count Semaphore Demo! =========\r\n");osal_printk("Enter kernel_07_count_semaphore_example()!\r\n");osal_kthread_lock();if(osal_sem_init(&sem,SEM_COUNT_MAX) != OSAL_SUCCESS){osal_printk("Failed to create semaphore!\r\n");return;}// 创建线程1osal_task * task1 = osal_kthread_create((osal_kthread_handler)Task1, NULL, "Task1", 0x1000);if(task1 != NULL){osal_printk("Task1 created with handle: %p\r\n, task1");}// 创建线程2osal_task * task2 = osal_kthread_create((osal_kthread_handler)Task2, NULL, "Task2", 0x1000);if(task2 != NULL){osal_printk("Task2 created with handle: %p\r\n, task2");}osal_kthread_unlock();
}app_run(SemaphoreDemo);
(3)编写配置文件
接着是编译配置 BUILD.gn
文件,内容如下:
# 代码 2.6-9static_library("kernel_07_CountSemaphoreDemo") {sources = ["kernel_07_count_semaphore_demo.c"]include_dirs = ["//commonlibrary/utils_lite/include","//device/soc/hisilicon/ws63v100/sdk/kernel/osal/include/debug","//device/soc/hisilicon/ws63v100/sdk/include","//device/soc/hisilicon/ws63v100/sdk/kernel/osal/include","//device/soc/hisilicon/ws63v100/sdk/middleware/utils/app_init","//device/soc/hisilicon/ws63v100/sdk/kernel/osal/include/semaphore","//device/soc/hisilicon/ws63v100/sdk/kernel/osal/include/time"]
}
(4)修改工程配置文件
1) 修改“工程目录”下的 BUILD.gn 文件,在 features
数组中添加 "kernel/kernel_07_count_semaphore_demo:kernel_07_CountSemaphoreDemo",
2)修改 SDK 目录/build/config/target_config/ws63/config.py 文件,在'ws63-liteos-app'
中'ram_component': []'
添加 "kernel_07_CountSemaphoreDemo"
3)修改 SDK 目录/libs_url/ws63/cmake/ohos.cmake 文件,在 COMPONENT_LIST
项中添加 "kernel_07_CountSemaphoreDemo"
(5)编译工程
在 VS Code 工具中,打开内置终端工具,进入当前OpenHarmony 的源码目录下,输入命令 rm -rf out && hb set -p nearlink_dk_3863 && hb build -f
,等待编译完成。
(6)烧写
参考“第 1 章 1.4 镜像文件的烧写”一节的内容完成烧写。
(7)通过串口调试助手查看程序运行结果
图 2.6-4 计数型信号量实例串口消息
可以看到,当目前空余位置(信号量最大计数 - 当前计数)不为 0 时,新来的客人可直接入座(获取并持有信号量),当同时有 5 个客人入座时,剩下的客人需要等待前面的客人就餐完毕(释放信号量)。
2.7 OpenHarmony 的消息队列
2.7.1 内核消息队列及其处理机制
(1)消息队列
**消息队列(message queue)**又称队列,是一种常用于任务间通信的数据结构。队列接收来自任务或中断的不固定长度消息,并根据不同的接口确定传递的消息是否存放在队列空间中。
任务能够从队列里面读取消息,当队列中的消息为空时,挂起读取任务;当队列中有新消息时,挂起的读取任务被唤醒并处理新消息。任务也能够往队列里写入消息,当队列已经写满消息时,挂起写入任务;当队列中有空闲消息节点时,挂起的写入任务被唤醒并写入消息。
(2)阻塞模式
可以通过调整读队列和写队列的超时时间来调整读写接口的阻塞模式,如果将读队列和写队列的超时时间设置为 0,就不会挂起任务,接口会直接返回,这就是非阻塞模式。反之,如果将都队列和写队列的超时时间设置为大于 0 的时间,就会以阻塞模式运行。
(3)处理机制
消息队列提供了异步处理机制,允许将一个消息放入队列,但不立即处理。同时队列还有缓冲消息的作用,可以使用队列实现任务异步通信,队列具有如下特性:
- 消息以先进先出的方式排队,支持异步读写。
- 读队列和写队列都支持超时机制。
- 每读一条消息,就会将该消息节点设置为空闲。
- 发送的消息类型由通信双方约定,可以允许不同长度(不超过列队的消息节点大小)的消息。
- 一个任务能够从任意一个消息列队接受和发送消息。
- 多个队列能够从同一个消息队列接受和发送消息。
- 创建对联时所需的队列空间,接口内系统自行动态申请内存。
(4)运行机制
图 2.7-1 消息队列运行机制
2.7.2 消息队列使用的基本步骤
OpenHarmony 为消息队列的相关操作提供了诸多接口函数,通过调用对应的接口函数,即可实现队列消息的收发功能。一般的使用步骤可以总结如下:
- 配置消息队列的参数属性。
- 创建消息队列:调用内核功能接口, 创建消息队列。
- 视需求进行内核消息队列的使用,如发送消息、接收消息。
2.7.3 内核消息队列接口介绍
针对 WS63 的 LiteOS-M 内核消息队列接口均在 SDK 目录/kernel/osal/include/msgqueue/osal_msgqueue.h
文件中声明。
(1)设置消息队列的参数属性
osMessageQueueAttr_t
结构体类型用于描述消息队列的属性,如名称、属性位、控制块的内存等。
//代码 2.7-1/// Attributes structure for message queue.
typedef struct {const char *name; ///< name of the message queueuint32_t attr_bits; ///< attribute bitsvoid *cb_mem; ///< memory for control blockuint32_t cb_size; ///< size of provided memory for control blockvoid *mq_mem; ///< memory for data storageuint32_t mq_size; ///< size of provided memory for data storage
} osMessageQueueAttr_t;
(2)创建消息队列
可以调用 OSAL 提供的 osal_msg_queue_create
函数创建消息队列。
- 原型:
// 代码 2.7-2 int osal_msg_queue_create(const char *name, unsigned short queue_len, unsigned long *queue_id, unsigned int flags,unsigned short max_msgsize);
- 参数:
- name 是一个字符串常量,表示显示的消息队列名称。
- queue_line 是一个无符号短整型数据,表示消息队列的长度大小。
- queue_id 是一个无符号长整型指针,表示消息队列句柄。
- flag 是无符号整型数据,表示消息队列的模式。
- max_msgsize 是无符号短整型数据,表示消息队列的节点大小。
- 返回值:
- 创建成功,返回OSAL_SUCCESS,
- 创建失败,返回OSAL_FAILURE。
- 调用:具体参见代码 2.7-5 。
(3)向消息队列写入数据
在消息队列被创建后,就可以向其中写入消息来实现任务间的消息共享。
- 原型:
int osal_msg_queue_write_copy(unsigned long queue_id, void *buffer_addr, unsigned int buffer_size,unsigned int timeout);
- 参数:
- queue_id 是无符号长整型数据,是只通过 osal_msg_queue_create 所创建的消息队列 id。
- buffer_addr 是 void 类型的指针,在使用时可以指向要写入消息的地址
- buffer_size 是无符号整型数据,表示要写入消息的大小
- timeout 是无符号整型数据,表示消息写入时间的限定
- 返回值:
- 执行成功,返回OSAL_SUCCESS,
- 执行失败,返回OSAL_FAILURE。
调用:具体参见代码 2.7-5 。
(4)从消息队列中读出消息
- 原型:
// 代码 2.7-4int osal_msg_queue_read_copy(unsigned long queue_id, void *buffer_addr, unsigned int *buffer_size,unsigned int timeout);
- 参数:
- queue_id 是无符号长整型数据,是只通过 osal_msg_queue_create 所创建的消息队列 id。
- buffer_addr 是 void 类型的指针,在使用时可以指向要读出后消息的存放地址。
- buffer_size 是无符号整型数据,表示待读入消息的长度。
- timeout 是无符号整型数据,表示消息读入时间的限定。
- 返回值:
- 执行成功,返回OSAL_SUCCESS,
- 执行失败,返回OSAL_FAILURE。
调用:具体参见代码 2.7-5 。
2.9.4 消息队列功能开发实现
以下示例中创建两个任务线程,其中任务1用于发送消息,并在控制台打印发送状态,任务2用于接收消息队列,并在控制台打印接收到的消息。
(1)创建工程
在内核开发工程目录下新建 “kernel_08_message_queuedemo” 文件夹,并在该文件夹中新建“kernel08_message_queue_demo.c”文件和“BUILD.gn”文件。
(2)编写功能代码
kernel_08_message_queue_demo.c 文件内容如下:
// 代码 2.7-5#include "osal_debug.h"
#include "common_def.h"
#include "soc_osal.h"
#include "app_init.h"#define MSG_QUEUE_SIZE 18 // 每数据最大18字节
#define MSG_MAX_LEN 18 // 可存储18条数据static unsigned long g_msg_queue;
uint8_t abuf[] = "test is message x";// 任务1 线程 发送消息
void Task1(void *argument)
{(void)argument;uint32_t i = 0;uint32_t uwlen = sizeof(abuf);while (i < 5){osal_printk("\r\nenter Task 1.......\r\n");abuf[uwlen - 2] = '0' + i;i ++;if(OSAL_SUCCESS == osal_msg_queue_write_copy(g_msg_queue, abuf, sizeof(abuf), OSAL_WAIT_FOREVER)){osal_printk("osal_msg_queue_write_copy is ok.\r\n");}osal_msleep(100);}
}// 任务 2 线程 接收消息
void Task2(void *argument)
{(void)argument;uint8_t msg[2024];while (1){uint32_t msg_rev_size = 2024;osal_printk("\r\nenter Task 2.......\r\n");if(OSAL_SUCCESS != osal_msg_queue_read_copy(g_msg_queue, msg, &msg_rev_size, OSAL_WAIT_FOREVER)){osal_printk("osal_msg_queue_read_copy failed.\r\n");break;}osal_printk("osal_msg_queue_read_copy is ok.\r\n");osal_printk("recv message:%s\r\n", (char *)msg);osal_msleep(100);}
}// 系统入口函数
static void kernel_08_message_queue_demo(void)
{osal_printk("\r\n========= Hello, Message Queue Demo! =========\r\n");osal_printk("[demo] Enter kernel_08_message_queue_demo()!\r\n");osal_kthread_lock();// 创建消息队列if(OSAL_SUCCESS == osal_msg_queue_create(NULL,MSG_QUEUE_SIZE, &g_msg_queue, 0, MSG_MAX_LEN)){osal_printk("Create MsgQueue is OK!\r\n");}// 创建任务1 线程osal_task * task1 = osal_kthread_create((osal_kthread_handler)Task1, NULL, "Task1", 0x1000);if (task1 != NULL){osal_printk("Create task1 is OK!\r\n");}// 创建任务2 线程osal_task * task2 = osal_kthread_create((osal_kthread_handler)Task2, NULL, "Task2", 0x1000);if (task2 != NULL){osal_printk("Create task2 is OK!\r\n");}osal_kthread_unlock();
}
app_run(kernel_08_message_queue_demo);
(3)编写配置文件
接着是编译配置 BUILD.gn 文件,内容如下:
# 代码 2.7-6static_library("kernel_08_MessageQueueDemo") {sources = [# 添加参与编译的 .c"kernel_08_message_queue_demo.c"]include_dirs = [# 添加头文件搜索路径"//commonlibrary/utils_lite/include","//device/soc/hisilicon/ws63v100/sdk/kernel/osal/include/debug","//device/soc/hisilicon/ws63v100/sdk/include","//device/soc/hisilicon/ws63v100/sdk/kernel/osal/include","//device/soc/hisilicon/ws63v100/sdk/middleware/utils/app_init"]
}
(4)修改工程配置文件
1) 修改“工程目录”下的 BUILD.gn 文件,在 features
数组中添加 "kernel/kernel_08_message_queue_demo:kernel_08_MessageQueueDemo",
2)修改 SDK 目录/build/config/target_config/ws63/config.py 文件,在'ws63-liteos-app'
中'ram_component': []'
添加 "kernel_08_MessageQueueDemo",
3)修改 SDK 目录/libs_url/ws63/cmake/ohos.cmake 文件,在 COMPONENT_LIST
项中添加 "kernel_08_MessageQueueDemo"
(5)编译工程
在 VS Code 工具中,打开内置终端工具,进入当前OpenHarmony 的源码目录下,输入命令 rm -rf out && hb set -p nearlink_dk_3863 && hb build -f
,等待编译完成。
(6)烧写
参考“第 1 章 1.4 镜像文件的烧写”一节的内容完成烧写。
(7)通过串口调试助手查看程序运行结果
图 2.7-2 消息队列测试实例串口消息内容
相关文章:
基于 OpenHarmony 5.0 的星闪轻量型设备应用开发——Ch2 OpenHarmony LiteOS-M 内核应用开发
写在前面: 此篇是系列文章《基于 OpenHarmony5.0 的星闪轻量型设备应用开发》的第 2 章。本篇介绍了如何在 OpenHarmony 5.0 框架下,针对 WS63 进行 LiteOS-M 内核应用工程的开发。 为了方便读者学习,需要OpenHarmony 5.0 WS63 SDK 的小伙伴可…...
2025年4月9日-华为暑期实习-第二题-200分
📌 点击直达笔试专栏 👉《大厂笔试突围》 💻 春秋招笔试突围在线OJ 👉 笔试突围OJ 02. 智能导航系统 问题描述 K小姐生活在一个被称为"未来之城"的智能城市,这个城市拥有高效的无人驾驶运输网络。城市内的智能车辆可以在不同的交通枢纽之间穿行,每个枢…...
抖音视频下载工具
抖音视频下载工具 功能介绍 这是一个基于Python开发的抖音视频下载工具,可以方便地下载抖音平台上的视频内容。 主要特点 支持无水印视频下载自动提取视频标题作为文件名显示下载进度条支持自动重试机制支持调试模式 使用要求 Python 3.10Chrome浏览器必要的P…...
基于大模型预测儿童急性淋巴细胞白血病诱导达完全缓解患者综合治疗方案研究报告
目录 一、引言 1.1 研究背景与意义 1.2 研究目的 1.3 国内外研究现状 二、儿童急性淋巴细胞白血病及大模型相关理论基础 2.1 儿童急性淋巴细胞白血病概述 2.2 大模型技术原理及特点 三、大模型在术前评估中的应用 3.1 患者基本信息与病情数据收集 3.2 大模型对病情严…...
项目合同从专家到小白
文章目录 按项目范围划分项目总承包合同项目单项承包合同项目分包合同 按项目付款方式划分总价合同固定总价合同总价加激励费用合同(FPIF)总价加经济价格调整合同订购单 \ 单边合同 成本补偿合同工料合同(混合型) 基础概念目标成本…...
【windows10】基于SSH反向隧道公网ip端口实现远程桌面
【windows10】基于SSH反向隧道公网ip端口实现远程桌面 1.背景2.SSH反向隧道3.远程连接电脑 1.背景 Windows 10远程桌面协议的简称是RDP(Remote Desktop Protocol)。 RDP是一种网络协议,允许用户远程访问和操作另一台计算机。 远程桌面功…...
学习海康VisionMaster之四边形查找
一:进一步学习了 今天学习下VisionMaster中的四边形查找,这个还是拟合直线的衍生应用,可以同时测量四条直线并且输出交点或者判定是否有交点 二:开始学习 1:什么是四边形查找? 按照传统的算法,…...
菊风RTC 2.0 开发者文档正式发布,解锁音视频新体验!
重磅发布! 开发者们,菊风实时音视频2.0文档已正式发布上线,为您提供更清晰、更高效的开发支持!让菊风实时音视频2.0为您的音视频应用加速~ 菊风实时音视频2.0聚焦性能升级、体验升级、录制服务升级,助力视频通话、语…...
用Python和OpenCV开启图像处理魔法之旅
你是否曾好奇计算机是如何“看懂”这个世界的?从人脸识别到自动驾驶,计算机视觉技术正日益渗透到我们的生活中。而 OpenCV (Open Source Computer Vision Library),作为一个强大的开源计算机视觉库,正是我们探索这个奇妙世界的强大…...
初识MySQL · 复合查询(内外连接)
目录 前言: 基本查询回顾 笛卡尔积和子查询 笛卡尔积 内外连接 子查询 单行子查询 多行子查询 多列子查询 from中使用子查询 合并查询 前言: 在前文我们学习了MySQL的基本查询,就是简单的套用了select语句,最多不过是…...
Devops系列之对接Gerrit的设计与实现(三)-- Java编程实现
一、背景 上文讲述了如何使用shell命令实现创建gerrit项目,本文介绍如何使用java语言编程实现。 二、java语言实现 1、引入jar包 <dependency><groupId>com.urswolfer.gerrit.client.rest</groupId><artifactId>gerrit-rest-java-client…...
深入理解全排列算法:DFS与回溯的完美结合
全排列问题是算法中的经典问题,其目标是将一组数字的所有可能排列组合列举出来。本文将详细解析如何通过深度优先搜索(DFS)和回溯法高效生成全排列,并通过模拟递归过程帮助读者彻底掌握其核心思想。 问题描述 给定一个正整数 n&a…...
服务器(一种管理计算资源的计算机)
服务器是在网络环境中提供计算能力并运行软件应用程序的特定IT设备,它在网络中为其他客户机(如个人计算机、智能手机、ATM机等终端设备)提供计算或者应用服务, 一般来说服务器都具备承担响应服务请求、承担服务、保障服务的能力。服务器相比普…...
时态--02--一般过去时
提示:文章写完后,目录可以自动生成,如何生成可参考右边的帮助文档 文章目录 一般过去时1.肯定句am/is — wasare — were 2.否定句3.⼀般疑问句4.特殊疑问句5.there be 过去式 practice过去分词 一般过去时 1.肯定句 am/is — was are — wer…...
WSA(Windows Subsystem for Android)安装LSPosed和应用教程
windows安卓子系统WSA的Lsposed和shamiko的安装教程 WSA(Windows Subsystem for Android)安装LSPosed和应用教程 一、环境准备 在开始之前,请确保: 已经安装好WSA(Windows Subsystem for Android)已经安装好ADB工具下载好LSPosed和Shamiko框架安装包 二、连接WSA 首先需要…...
Opencv计算机视觉编程攻略-第十三节 跟踪视频中的物品
这是opencv系列的最后一节,主要学习视频序列,上一节介绍了读取、处理和存储视频的工具,本文将介绍几种跟踪图像序列中运动物体的算法。可见运动或表观运动,是物体以不同的速度在不同的方向上移动,或者是因为相机在移动…...
10 个最新 CSS 功能已在所有主流浏览器中得到支持
前言 CSS 不断发展,新功能使我们的工作更快、更简洁、更强大。得益于最新的浏览器改进(Baseline 2024),许多新功能现在可在所有主要引擎上使用。以下是您可以立即开始使用的10 CSS新功能。 1. Scrollbar-Gutter 和 Scrollbar-Co…...
[特殊字符] 企业级Docker私有仓库实战:3步搭建Harbor安全仓库,镜像管理从此高效无忧
本文提供 一站式Docker私有仓库部署指南,聚焦企业级镜像管理需求,深入解析Harbor私有仓库的搭建、运维与安全加固全流程。内容涵盖 轻量级Registry快速部署与 Harbor企业级方案对比,手把手演示SSL证书配置、多租户权限控制、镜像漏洞扫描等核…...
一个基于Django的进销存管理系统Demo实现
第一步:创建 Django 项目 bash 复制 django-admin startproject inventory_system cd inventory_system python manage.py startapp erp 第二步:定义数据模型(models.py) python 复制 from django.db import models from d…...
wsl2+ubuntu22.04安装blender教程(详细教程)
本章教程介绍,如何在Windows操作系统上通过wsl2+ubuntu安装blender并运行教程。Blender 是一款免费、开源的 3D 创作套件,广泛应用于建模、动画、渲染、视频编辑、特效制作等领域。它由全球开发者社区共同维护,支持跨平台(Windows、macOS、Linux),功能强大且完全…...
netty中的ChannelPipeline详解
Netty中的ChannelPipeline是事件处理链的核心组件,负责将多个ChannelHandler组织成有序的责任链,实现网络事件(如数据读写、连接状态变化)的动态编排和传播。以下从核心机制、执行逻辑到应用场景进行详细解析: 1. 核心结构与组成 双向链表结构 组成单元:ChannelPipeline…...
使用多进程和 Socket 接收解析数据并推送到 Kafka 的高性能架构
使用多进程和 Socket 接收解析数据并推送到 Kafka 的高性能架构 在现代应用程序中,实时数据处理和高并发性能是至关重要的。本文将介绍如何使用 Python 的多进程和 Socket 技术来接收和解析数据,并将处理后的数据推送到 Kafka,从而实现高效的…...
WinForm真入门(14)——ListView控件详解
一、ListView 控件核心概念与功能 ListView 是 WinForm 中用于展示结构化数据的多功能列表控件,支持多列、多视图模式及复杂交互,常用于文件资源管理器、数据报表等场景。 核心特点: 支持 5种视图模式:Details&…...
FastAPI用户认证系统开发指南:从零构建安全API
前言 在现代Web应用开发中,用户认证系统是必不可少的功能。本文将带你使用FastAPI框架构建一个完整的用户认证系统,包含注册、登录、信息更新和删除等功能。我们将采用JWT(JSON Web Token)进行身份验证,并使用SQLite作…...
【BUG】阿里云服务器数据库远程连接报错
当你遇到 ERROR 2003 (HY000): Cant connect to MySQL server on 47.100.xxx.xx (10061) 错误,这个错误代码 10061 通常意味着客户端无法连接到指定的 MySQL 服务器,原因可能有多种,下面为你分析可能的原因及对应的解决办法。 1. 网络连接问…...
【前端】【React】性能优化三件套useCallback,useMemo,React.memo
一、总览:性能优化三件套 useCallback(fn, deps):缓存函数,避免每次渲染都新建函数。useMemo(fn, deps):缓存值(计算结果),避免重复执行计算。React.memo(Component):缓存组件的渲染…...
Vue3性能优化终极指南:编译策略、运行时调优与全链路监控
一、Vue3性能优化体系框架 1.1 性能优化全景图谱 1.2 关键性能指标定义表 指标测量方式优化目标核心影响因子FCPLighthouse<1.5s资源加载速度LCPPerformance API<2.5s关键资源大小TTIWebPageTest<3.5s主线程阻塞时间Memory UsageChrome DevTools<50MB对象引用策略…...
FISCO BCOS技术架构解析:从多群组设计到性能优化实践
目录 FISCO BCOS整体架构设计 多群组架构与数据隔离机制 交易流程与执行机制 安全架构与隐私保护 性能优化与压测实践 应用案例与生态工具 FISCO BCOS作为中国领先的金融级开源联盟链平台,自2017年由金链盟开源工作组推出以来,已在政务、金融、医疗、版权等众多领域实现…...
Ceph异地数据同步之- S3对象异地同步复制
#作者:闫乾苓 文章目录 关键组件说明数据流说明部署步骤配置主区域配置次要区域S3对象文件同步测试 关键组件说明 在Ceph RGW的多站点复制架构中,Realm、Zonegroup 和 Zone 是关键的组织结构,用于管理多站点的配置和数据同步 Realm(领域)&a…...
iOS按键精灵辅助工具在游戏开发中的创新应用
一、iOS自动化测试辅助工具 在移动游戏开发领域,iOS按键精灵类辅助工具不同于传统的安卓自动化方案,iOS环境下的自动化测试面临更严峻的技术挑战,但通过创新方法仍可实现精准控制。 # 基于图像识别的智能定位算法示例 def find_button(butt…...
3D案例丨多个3D工业相机拼接检测 开启360°新视界
在高速生产线上,经常需要在极短的时间内对工件进行全方位的外观检测,如:线缆直径和直线度检测、锂电池外观缺陷检测、铁轨截面尺寸检测等。 这需要传感器完整还原被测物的截面面轮廓形状,并获取精准的截面轮廓数据。但单一相机的…...
打分函数分类
在分子对接中,打分函数用于评估配体与受体结合的亲和力。不同类型的打分函数有各自的优势和应用场景。常见的打分函数主要分为以下几类: 1. 基于物理(力场)的打分函数 (Force/physics-field-based scoring functions) 这种打分…...
实践 DevOps 项目:使用 Terraform、Helm、SonarQube 和 GitLab CI/CD 在 AWS EKS 上实践全栈部署
在当今快节奏的软件开发领域,自动化至关重要。在本文中,我将向您展示如何构建一个全面的 DevOps 流水线,该流水线能够: 使用 Terraform 预置完整的 AWS 基础设施。部署一个包含私有子网和公共子网、RDS PostgreSQL 以及完整配置的…...
EFT干扰和共模干扰
EFT干扰本质上属于共模干扰的一种具体表现形式,但严格来说不能简单等同于共模干扰。以下从原理、特征及区别角度展开分析: 1. EFT干扰的原理 定义:EFT(Electrical Fast Transient,电快速瞬变脉冲群)干扰是…...
android 下提示 SQLITECIPHER driver not loaded
问题描述: 在android下出现 SQLITECIPHER driver not loaded 错误 解决办法: 在QT的Android目录下面放入 libplugins_sqldrivers_sqlitecipher_arm64-v8a.so...
[D1,2]回溯刷题
文章目录 组合 组合 回溯的基础结构 #组合总和 注意startIndex的更新是用i来更新的,不然会产生重复的组合...
使用 VBA 宏创建一个选择全部word图片快捷指令,进行图片格式编辑
使用 VBA 宏批量选择图片 ✅ 第一步:创建 .dotm 加载项文件 1、使用环境 office word 365,文件格式为.docx 图片格式为.PNG 2、创建 .dotm 加载项文件 打开 Word,新建一个空白文档。 按下 Alt F11 打开 VBA 编辑器。 点击菜单栏ÿ…...
SQL 关键字
SQL 包含许多关键字,这些关键字用于执行各种数据库操作。以下是主要的 SQL 关键字分类: 数据查询语言 (DQL) SELECT - 从数据库中选择数据 FROM - 指定要查询的表 WHERE - 指定查询条件 GROUP BY - 对结果集进行分组 HAVING - 对分组结果进行过滤 …...
从PPT到PNG:Python实现的高效PPT转图工具
从PPT到PNG:Python实现的高效PPT转图工具 在日常工作中,PPT(PowerPoint)文件是我们常用的演示工具。然而,有时候我们需要将PPT的内容提取为图片格式(如PNG)以便于展示或保存。手动将每一页PPT保…...
TCP和UDP协议
前言 TCP(传输控制协议)和UDP(用户数据报协议)是两种主要的传输层协议;它们在连接方式、可靠性、效率等方面有显著区别。 关键对比 差异总结 可靠性: TCP通过确认应答、重传等机制确保数据可靠传输&#…...
高并发内存池(三):PageCache(页缓存)的实现
前言: 在前两期内容中,我们深入探讨了内存管理机制中在 ThreadCache 和 CentralCache两个层级进行内存申请的具体实现。这两层缓存作为高效的内存分配策略,能够快速响应线程的内存需求,减少锁竞争,提升程序性能。 本期…...
使用pybind11开发可供python使用的c++扩展模块
在做紫微斗数程序的时候用到了padas库,不过也只用了它下面几个功能: 1、读入csv文件,构造DataFrame; 2、通过行列标题查找数据; 3、通过行标题读取一行数据。 用这几个功能却导入了pandas、numpy、dateutil、pytz等一堆库,多少有点划不来,于是想用c++开发一个实现这几…...
系统与网络安全------网络通信原理(5)
资料整理于网络资料、书本资料、AI,仅供个人学习参考。 传输层解析 传输层 传输层的作用 IP层提供点到点的连接传输层提供端到端的连接 端口到端口的连接(不同端口号,代表不同的应用程序) TCP协议概述 TCP(Transm…...
JavaScript防抖与节流
目录 防抖(Debounce) 一、防抖的定义 二、防抖的实现原理 三、防抖的代码实现 四、代码解析 五、使用示例 1. 输入框实时搜索(延迟执行模式) 2. 按钮防重复点击(立即执行模式) 六、总结 节流&…...
Java网络编程实战(多人聊天室-CS模式)
一、C/S模式核心原理 1.1 基本架构 C/S(Client/Server)模式采用客户端-服务器架构: 服务器端:持续运行,负责消息路由和广播客户端:用户交互界面,连接服务器进行通信通信协议:TCP&…...
Vue3.5 + Vite6.x 项目的完整 Stylelint 配置方案,支持 .vue/.html 内联样式、Less/SCSS/CSS 等多种文件类
Vue3.5 Vite6.x 项目的完整 Stylelint 配置方案,支持 .vue/.html 内联样式、Less/SCSS/CSS 等多种文件类型 一、完整依赖安装 npm install --save-dev stylelint stylelint-config-standard postcss-html # 解析 Vue/HTML 文件中的样式postcss-scss …...
23种设计模式Java版(带脑图,带示例源码)
设计模式 1、创建型 1.1、单例模式(Singleton pattern) 确保一个类只有一个实例,并提供该实例的全局访问点。 1.2、工厂方法(Factory Method) 它定义了一个创建对象的接口,但由子类决定要实例化哪个类。工厂方法把实例化操作推迟到子类。 1.3、抽象…...
mapbox高阶,使用graphology、graphology-shortest-path前端插件和本地geojson数据纯前端实现路径规划
👨⚕️ 主页: gis分享者 👨⚕️ 感谢各位大佬 点赞👍 收藏⭐ 留言📝 加关注✅! 👨⚕️ 收录于专栏:mapbox 从入门到精通 文章目录 一、🍀前言1.1 ☘️mapboxgl.Map 地图对象1.2 ☘️mapboxgl.Map style属性1.3 ☘️graphology 插件1.3.1 ☘️概念1.3.2 ☘…...
【已解决】vscode升级后连接远程异常:“远程主机可能不符合XXX的先决条件”解决方法
vscode提示升级,每次都升了,突然某次关闭后无法连接远程,查询资料是因为从VS Code 1.86.1版本开始(2024年1月)要求glibc版本>2.28。 命令“ ldd --version”可查看glibc版本为2.27: rootXXXXXXX:~$ ld…...
Springboot整合JAVAFX
Springboot整合JAVAFX 实体与VO设计 pom.xml文件如下: <?xml version"1.0" encoding"UTF-8"?> <project xmlns"http://maven.apache.org/POM/4.0.0"xmlns:xsi"http://www.w3.org/2001/XMLSchema-instance"xs…...