鸿蒙小型系统内核Liteos-a开发指南

鸿蒙小型系统内核Liteos-a开发指南

文章目录

1内核概述

1.1简介

OpenHarmony 轻量级内核是基于IoT领域轻量级物联网操作系统Huawei LiteOS内核演进发展的新一代内核,包含LiteOS-M和LiteOS-A两类内核。LiteOS-M内核主要应用于轻量系统,面向的MCU(微控制单元)一般是百K级内存,可支持MPU隔离,业界类似的内核有FreeRTOS或ThreadX等;LiteOS-A内核主要应用于小型系统,面向设备一般是M级内存,可支持MMU隔离,业界类似的内核有Zircon或Darwin等。本开发指南适用于LiteOS-A内核。

为适应IoT产业的高速发展,OpenHarmony 轻量级内核不断优化和扩展,能够带给应用开发者友好的开发体验和统一开放的生态系统能力。轻量级内核LiteOS-A重要的新特性如下:

1)新增了丰富的内核机制

新增虚拟内存、系统调用、多核、轻量级IPC(Inter-Process Communication,进程间通信)、DAC(Discretionary Access Control,自主访问控制)等机制,丰富了内核能力;为了更好的兼容软件和开发者体验,新增支持多进程,使得应用之间内存隔离、相互不影响,提升系统的健壮性。

2)引入统一驱动框架HDF(Hardware Driver Foundation)

引入统一驱动框架HDF,统一驱动标准,为设备厂商提供了更统一的接入方式,使驱动更加容易移植,力求做到一次开发,多系统部署。

3)支持1200+标准POSIX接口

更加全面的支持POSIX标准接口,使得应用软件易于开发和移植,给应用开发者提供了更友好的开发体验。

4)内核和硬件高解耦

轻量级内核与硬件高度解耦,新增单板,内核代码不用修改。

1.2 内核架构

轻量级内核主要由基础内核、扩展组件、HDF框架、POSIX接口组成。轻量级内核的文件系统、网络协议等扩展功能(没有像微内核那样运行在用户态)运行在内核地址空间,主要考虑组件之间直接函数调用比进程间通信或远程过程调用要快得多。

在这里插入图片描述

图 1 OpenHarmony LiteOS-A内核架构图

·基础内核主要包括内核的基础机制,如调度、内存管理、中断异常等;

·扩展组件主要包括文件系统、网络协议和安全等扩展功能;

·HDF框架是外设驱动统一标准框架;

·POSIX接口(可移植性操作系统接口)是为兼容POSIX标准的应用方便移植到OpenHarmony;

1.2.1基础内核

基础内核组件实现精简,主要包括内核的基础机制,如调度、内存管理、中断异常、内核通信等;

进程管理:支持进程和线程,基于Task实现进程,进程独立4GiB地址空间

多核调度:支持任务和中断亲核性设置,支持绑核运行

实时调度:支持高优先级抢占,同优先级时间片轮转

虚拟内存:支持缺页异常,内核空间静态映射到0-1GiB地址,用户空间映射到1-4GiB地址

内核通信:事件、信号量、互斥锁、队列

时间管理:软件定时器、系统时钟

1.2.2文件系统

轻量级内核支持FAT,JFFS2,NFS,ramfs,procfs等众多文件系统,并对外提供完整的POSIX标准的操作接口;内部使用VFS层作为统一的适配层框架,方便移植新的文件系统,各个文件系统也能自动利用VFS层提供的丰富的功能。

主要特性有:

1)完整的POSIX接口支持

2)文件级缓存(pagecache)

3)磁盘级缓存(bcache)

4)目录缓存(pathcache)

5)DAC能力

6)支持嵌套挂载及文件系统堆叠等

7)支持特性的裁剪和资源占用的灵活配置。

1.2.3网络协议

轻量级内核网络协议基于开源LWIP构建,对LWIP的RAM占用进行优化,同时提高LWIP的传输性能。

协议: IP、IPv6、 ICMP、 ND、MLD、 UDP、 TCP、IGMP、ARP、PPPoS、PPPoE;

API:socket API;

扩展特性:多网络接口IP转发、TCP拥塞控制、RTT估计和快速恢复/快速重传;

应用程序:HTTP(S)服务、SNTP客户端、SMTP(S)客户端、ping工具、NetBIOS名称服务、mDNS响应程序、MQTT客户端、TFTP服务、DHCP客户端、DNS客户端、AutoIP/APIPA(零配置)、SNMP代理;

1.2.4HDP框架

轻量级内核集成HDF框架,HDF框架旨在为开发者提供更精准、更高效的开发环境,力求做到一次开发,多系统部署。

  1. 支持多内核平台
  2. 支持用户态驱动
  3. 可配置组件化驱动模型
  4. 基于消息的驱动接口模型
  5. 基于对象的驱动、设备管理
  6. HDI(Hardware Driver Interface)统一硬件接口
  7. 支持电源管理、PnP
1.2.5扩展组件

对内核功能进行扩展,可选但很重要的机制。

  1. 动态链接:支持标准ELF链接执行、加载地址随机化
  2. 进程通信:支持轻量级LiteIPC,同时也支持标准的Mqueue、Pipe、Fifo、Signal等机制
  3. 系统调用:支持170+系统调用,同时有支持VDSO机制
  4. 权限管理:支持进程粒度的特权划分和管控,UGO三种权限配置

2内核启动

2.1内核态启动

2.1.1内核启动流程

内核启动流程包含汇编启动阶段和C语言启动阶段2部分,如图所示。汇编启动阶段完成CPU初始设置,关闭dcache/icache,使能FPU及neon,设置MMU建立虚实地址映射,设置系统栈,清理bss段,调用C语言main函数等。C语言启动阶段包含OsMain函数及开始调度等,其中如图所示,OsMain函数用于内核基础初始化和架构、板级初始化等,其整体由内核启动框架主导初始化流程,图中右边区域为启动框架中可接受外部模块注册启动的阶段,各个阶段的说明如下表1所示。

在这里插入图片描述

图 2内核启动流程图
表 1 启动框架层级
层级说明
LOS_INIT_LEVEL_EARLIEST最早期初始化 说明:不依赖架构,单板以及后续模块会对其有依赖的纯软件模块初始化 例如:Trace模块
LOS_INIT_LEVEL_ARCH_EARLY架构早期初始化 说明:架构相关,后续模块会对其有依赖的模块初始化,如启动过程中非必需的功能,建议放到LOS_INIT_LEVEL_ARCH层
LOS_INIT_LEVEL_PLATFORM_EARLY平台早期初始化 说明:单板平台、驱动相关,后续模块会对其有依赖的模块初始化,如启动过程中必需的功能,建议放到LOS_INIT_LEVEL_PLATFORM层 例如:uart模块
LOS_INIT_LEVEL_KMOD_PREVM内存初始化前的内核模块初始化 说明:在内存初始化之前需要使能的模块初始化
LOS_INIT_LEVEL_VM_COMPLETE基础内存就绪后的初始化 说明:此时内存初始化完毕,需要进行使能且不依赖进程间通讯机制与系统进程的模块初始化 例如:共享内存功能
LOS_INIT_LEVEL_ARCH架构后期初始化 说明:架构拓展功能相关,后续模块会对其有依赖的模块初始化
LOS_INIT_LEVEL_PLATFORM平台后期初始化 说明:单板平台、驱动相关,后续模块会对其有依赖的模块初始化 例如:驱动内核抽象层初始化(mmc、mtd)
LOS_INIT_LEVEL_KMOD_BASIC内核基础模块初始化 说明:内核可拆卸的基础模块初始化 例如:VFS初始化
LOS_INIT_LEVEL_KMOD_EXTENDED内核扩展模块初始化 说明:内核可拆卸的扩展模块初始化 例如:系统调用初始化、ProcFS初始化、Futex初始化、HiLog初始化、HiEvent初始化、LiteIPC初始化
LOS_INIT_LEVEL_KMOD_TASK内核任务创建 说明:进行内核任务的创建(内核任务,软件定时器任务) 例如:资源回收系统常驻任务的创建、SystemInit任务创建、CPU占用率统计任务创建
2.1.2编程样例

实例描述:新增一个内核模块,需要在内核初始化时进行该模块的初始化,则通过内核启动框架将该模块的初始化函数注册进内核启动流程中。

示例代码:

/\* 内核启动框架头文件 \*/

\#include "los_init.h"

......

/\* 新增模块的初始化函数 \*/

unsigned int OsSampleModInit(void)

{

PRINTK("OsSampleModInit SUCCESS!\\n");

......

}

......

/\* 在启动框架的目标层级中注册新增模块 \*/

LOS_MODULE_INIT(OsSampleModInit, LOS_INIT_LEVEL_KMOD_EXTENDED);

结果验证:

main core booting up...

OsSampleModInit SUCCESS!

releasing 1 secondary cores

cpu 1 entering scheduler

cpu 0 entering scheduler

根据上述系统启动阶段的打印可知,内核在启动时进行了该注册模块的初始化函数调用,完成该模块的初始化操作。

说明: 启动框架中同一层级内的注册模块不能有依赖关系,建议新增模块按照上述启动阶段进行模块初始化的拆分,按需注册启动。 可通过查看系统编译生成文件OHOS_Image.map中.rodata.init.kernel.*段内的符号表来了解当前已注册进内核启动框架中的各个模块初始化入口,以及检查新注册的模块初始化入口是否生效。

2.2用户态启动

2.2.1用户态根进程启动

根进程是系统第一个用户态进程,进程ID为1,它是所有用户态进程的祖先。

在这里插入图片描述

图 3进程树示意图
2.2.1.1根进程的启动过程

使用链接脚本将如下init启动代码放置到系统镜像指定位置。

\#define LITE_USER_SEC_ENTRY \__attribute__((section(".user.entry")))

LITE_USER_SEC_ENTRY VOID OsUserInit(VOID \*args)

{

\#ifdef LOSCFG_KERNEL_DYNLOAD

sys_call3(__NR_execve, (UINTPTR)g_initPath, 0, 0);

\#endif

while (true) {

}

}

系统启动阶段,OsUserInitProcess启动init进程。具体过程如下:

·由内核OsLoadUserInit加载上述代码。

·创建新的进程空间,启动/bin/init进程。

2.2.1.2根进程的职责

启动关键系统程序或服务,如交互进程shell。

监控回收孤儿进程,清理子进程中的僵尸进程。

说明: 在OpenHarmony 中init进程通过读取/etc/init.cfg,根据配置执行指定命令,或启动指定进程(详见:init启动引导)。

2.2.2 用户态程序运行

用户态程序常见编译方式有如下两种:

1利用框架编译用户态进程。

2手动编译

实例:

clang --target=arm-liteos --sysroot=prebuilts/lite/sysroot -o helloworld helloworld.c

clang:参考LLVM安装指导安装LLVM编译器。

–target:–target=arm-liteos,指定编译平台为arm-liteos。

–sysroot:–sysroot=${YOUR_ROOT_PATH}/prebuilts/lite/sysroot,指定头文件以及依赖标准库搜索路径为prebuilts下的指定路径。

用户态程序启动有如下常见方式:

1)shell命令启动进程。

OHOS \$ exec helloworld

OHOS \$ ./helloworld

OHOS \$ /bin/helloworld

2)通过POSIX接口启动新进程。

Fork方法创建一个新的进程,exec类接口执行一个全新的进程。

3基础内核

3.1中断及异常处理

3.1.1基本概念

中断是指出现需要时,CPU暂停执行当前程序,转而执行新程序的过程。即在程序运行过程中,出现了一个必须由CPU立即处理的事务。此时,CPU暂时中止当前程序的执行转而处理这个事务,这个过程就叫做中断。通过中断机制,可以使CPU避免把大量时间耗费在等待、查询外设状态的操作上,大大提高系统实时性以及执行效率。

异常处理是操作系统对运行期间发生的异常情况(芯片硬件异常)进行处理的一系列动作,例如虚拟内存缺页异常、打印异常发生时函数的调用栈信息、CPU现场信息、任务的堆栈情况等。

3.1.2运行机制

外设可以在没有CPU介入的情况下完成一定的工作,但某些情况下也需要CPU为其执行一定的工作。通过中断机制,在外设不需要CPU介入时,CPU可以执行其它任务,而当外设需要CPU时,产生一个中断信号,该信号连接至中断控制器。中断控制器是一方面接收其它外设中断引脚的输入,另一方面它会发出中断信号给CPU。可以通过对中断控制器编程来打开和关闭中断源、设置中断源的优先级和触发方式。常用的中断控制器有VIC(Vector Interrupt Controller)和GIC(General Interrupt Controller)。在ARM Cortex-A7中使用的中断控制器是GIC。CPU收到中断控制器发送的中断信号后,中断当前任务来响应中断请求。

异常处理就是可以打断CPU正常运行流程的一些事情,如未定义指令异常、试图修改只读的数据异常、不对齐的地址访问异常等。当异常发生时,CPU暂停当前的程序,先处理异常事件,然后再继续执行被异常打断的程序。

以ARMv7-a架构为例,中断和异常处理的入口为中断向量表,中断向量表包含各个中断和异常处理的入口函数。

在这里插入图片描述

图 4 中断向量表
3.1.3开发指导
3.1.3.1 接口说明

异常处理为内部机制,不对外提供接口,中断模块提供对外接口如下:

表2 对外接口说明表
功能分类接口名称描述
创建和删除中断LOS_HwiCreate中断创建,注册中断号、中断触发模式、中断优先级、中断处理程序。中断被触发时,会调用该中断处理程序
LOS_HwiDelete删除中断
打开和关闭所有中断LOS_IntUnLock打开当前处理器所有中断响应
LOS_IntLock关闭当前处理器所有中断响应
LOS_IntRestore恢复到使用LOS_IntLock关闭所有中断之前的状态
获取系统支持的最大中断数LOS_GetSystemHwiMaximum获取系统支持的最大中断数
3.1.3.2开发流程

1)调用中断创建接口LOS_HwiCreate创建中断。

2)调用LOS_HwiDelete接口删除指定中断,此接口根据实际情况使用,判断是否需要删除中断。

3.1.3.3编程实例

本实例实现创建中断与删除中断。

代码实现如下,演示如何创建中断和删除中断,当指定的中断号HWI_NUM_TEST产生中断时,会调用中断处理函数:

\#include "los_hwi.h"

/\*中断处理函数\*/

STATIC VOID HwiUsrIrq(VOID)

{

printf("in the func HwiUsrIrq \\n");

}

static UINT32 Example_Interrupt(VOID)

{

UINT32 ret;

HWI_HANDLE_T hwiNum = 7;

HWI_PRIOR_T hwiPrio = 3;

HWI_MODE_T mode = 0;

HWI_ARG_T arg = 0;

/\*创建中断\*/

ret = LOS_HwiCreate(hwiNum, hwiPrio, mode, (HWI_PROC_FUNC)HwiUsrIrq, (HwiIrqParam \*)arg);

if(ret == LOS_OK){

printf("Hwi create success!\\n");

} else {

printf("Hwi create failed!\\n");

return LOS_NOK;

}

/\* 延时50个Ticks, 当有硬件中断发生时,会调用函数HwiUsrIrq\*/

LOS_TaskDelay(50);

/\*删除中断\*/

ret = LOS_HwiDelete(hwiNum, (HwiIrqParam \*)arg);

if(ret == LOS_OK){

printf("Hwi delete success!\\n");

} else {

printf("Hwi delete failed!\\n");

return LOS_NOK;

}

return LOS_OK;

}
3.1.3.4编程验证

编译运行得到的结果为:

Hwi create success!

Hwi delete success!

3.2进程管理

3.2.1进程
3.2.1.1 基本概念

进程是系统资源管理的最小单元。OpenHarmony LiteOS-A内核提供的进程模块主要用于实现用户态进程的隔离,内核态被视为一个进程空间,不存在其它进程(KIdle除外,KIdle进程是系统提供的空闲进程,和KProcess共享一个进程空间)。

1)OpenHarmony 的进程模块主要为用户提供多个进程,实现了进程之间的切换和通信,帮助用户管理业务程序流程。

OpenHarmony 的进程采用抢占式调度机制,采用高优先级优先+同优先级时间片轮转的调度算法。

2)OpenHarmony 的进程一共有32个优先级(0-31),用户进程可配置的优先级有22个(10-31),最高优先级为10,最低优先级为31。

3)高优先级的进程可抢占低优先级进程,低优先级进程必须在高优先级进程阻塞或结束后才能得到调度。

4)每一个用户态进程均拥有自己独立的进程空间,相互之间不可见,实现进程间隔离。

5)用户态根进程init由内核态创建,其它用户态子进程均由init进程fork而来。

3.2.1.1.1进程状态说明
  1. 初始化(Init):进程正在被创建。

    就绪(Ready):进程在就绪列表中,等待CPU调度。

    运行(Running):进程正在运行。

    阻塞(Pending):进程被阻塞挂起。本进程内所有的线程均被阻塞时,进程被阻塞挂起。

    僵尸(Zombies):进程运行结束,等待父进程回收其控制块资源。

3.2.1.1.2进程状态迁移说明

Init→Ready:进程创建或fork时,拿到该进程控制块后进入Init状态,处于进程初始化阶段,当进程初始化完成将进程插入调度队列,此时进程进入就绪状态。

Ready→Running:进程创建后进入就绪态,发生进程切换时,就绪列表中最高优先级的进程被执行,从而进入运行态。若此时该进程中已无其它线程处于就绪态,则进程从就绪列表删除,只处于运行态;若此时该进程中还有其它线程处于就绪态,则该进程依旧在就绪队列,此时进程的就绪态和运行态共存,但对外呈现的进程状态为运行态。

Running→Pending:进程在最后一个线程转为阻塞态时, 进程内所有的线程均处于阻塞态,此时进程同步进入阻塞态,然后发生进程切换。

Pending→Ready:阻塞进程内的任意线程恢复就绪态时,进程被加入到就绪队列,同步转为就绪态。

Ready→Pending:

进程内的最后一个就绪态线程转为阻塞态时,进程从就绪列表中删除,进程由就绪态转为阻塞态。

Running→Ready:进程由运行态转为就绪态的情况有以下两种:

1)有更高优先级的进程创建或者恢复后,会发生进程调度,此刻就绪列表中最高优先级进程变为运行态,那么原先运行的进程由运行态变为就绪态。

2)若进程的调度策略为LOS_SCHED_RR,且存在同一优先级的另一个进程处于就绪态,则该进程的时间片消耗光之后,该进程由运行态转为就绪态,另一个同优先级的进程由就绪态转为运行态。

Running→Zombies:当进程的主线程或所有线程运行结束后,进程由运行态转为僵尸态,等待父进程回收资源。

3.2.1.2运行机制

OpenHarmony 提供的进程模块主要用于实现用户态进程的隔离,支持用户态进程的创建、退出、资源回收、设置/获取调度参数、获取进程ID、设置/获取进程组ID等功能。

用户态进程通过fork父进程而来,fork进程时会将父进程的进程虚拟内存空间clone到子进程,子进程实际运行时通过写时复制机制将父进程的内容按需复制到子进程的虚拟内存空间。

进程只是资源管理单元,实际运行是由进程内的各个线程完成的,不同进程内的线程相互切换时会进行进程空间的切换。

在这里插入图片描述

图 5 进程管理示意图
3.2.1.3开发指导
表3 进程管理模块接口
功能分类接口名称描述
进程调度参数控制LOS_GetProcessScheduler获取指定进程的调度策略
LOS_SetProcessScheduler设置指定进程的调度参数,包括优先级和调度策略
LOS_GetProcessPriority获取指定进程的优先级
LOS_SetProcessPriority设置指定进程的优先级
等待回收子进程LOS_Wait等待子进程结束并回收子进程
进程组LOS_GetProcessGroupID获取指定进程的进程组ID
LOS_GetCurrProcessGroupID获取当前进程的进程组ID
获取进程IDLOS_GetCurrProcessID获取当前进程的进程ID
用户及用户组LOS_GetUserID获取当前进程的用户ID
LOS_GetGroupID获取当前进程的用户组ID
LOS_CheckInGroups检查指定用户组ID是否在当前进程的用户组内
系统支持的最大进程数LOS_GetSystemProcessMaximum获取系统支持的最大进程数目

注:不支持内核态进程创建,内核态不涉及进程相关开发。

idle线程的数量跟随CPU核心数,每个CPU均有一个相应的idle线程。

不支持创建除KProcess和KIdle进程之外的其它内核态进程。

用户态进程通过系统调用进入内核态后创建的线程属于KProcess, 不属于当前用户态进程。

3.2.2线程
3.2.2.1基本概念

从系统的角度看,任务Task是竞争系统资源的最小运行单元。任务可以使用或等待CPU、使用内存空间等系统资源,并独立于其它任务运行。

OpenHarmony 内核中使用一个任务表示一个线程

OpenHarmony 内核中同优先级进程内的任务统一调度、运行。

OpenHarmony 内核中的任务采用抢占式调度机制,同时支持时间片轮转调度和FIFO调度方式

OpenHarmony 内核的任务一共有32个优先级(0-31),最高优先级为0,最低优先级为31。

当前进程内, 高优先级的任务可抢占低优先级任务,低优先级任务必须在高优先级任务阻塞或结束后才能得到调度。

3.2.2.1.1任务状态说明
  1. 初始化(Init):任务正在被创建。

    就绪(Ready):任务在就绪列表中,等待CPU调度。

    运行(Running):任务正在运行。

    阻塞(Blocked):任务被阻塞挂起。Blocked状态包括:pending(因为锁、事件、信号量等阻塞)、suspended(主动pend)、delay(延时阻塞)、pendtime(因为锁、事件、信号量时间等超时等待)。

    退出(Exit):任务运行结束,等待父任务回收其控制块资源。

在这里插入图片描述

图 6 任务状态迁移示意图
3.2.2.1.2任务状态迁移说明

Init→Ready:任务创建拿到控制块后为初始化阶段(Init状态),当任务初始化完成将任务插入调度队列,此时任务进入就绪状态。

Ready→Running:任务创建后进入就绪态,发生任务切换时,就绪列表中最高优先级的任务被执行,从而进入运行态,此刻该任务从就绪列表中删除。

Running→Blocked:正在运行的任务发生阻塞(挂起、延时、读信号量等)时,任务状态由运行态变成阻塞态,然后发生任务切换,运行就绪列表中剩余最高优先级任务。

Blocked→Ready :阻塞的任务被恢复后(任务恢复、延时时间超时、读信号量超时或读到信号量等),此时被恢复的任务会被加入就绪列表,从而由阻塞态变成就绪态。

Ready→Blocked:任务也有可能在就绪态时被阻塞(挂起),此时任务状态会由就绪态转变为阻塞态,该任务从就绪列表中删除,不会参与任务调度,直到该任务被恢复。

Running→Ready:有更高优先级任务创建或者恢复后,会发生任务调度,此刻就绪列表中最高优先级任务变为运行态,那么原先运行的任务由运行态变为就绪态,并加入就绪列表中。

Running→Exit:运行中的任务运行结束,任务状态由运行态变为退出态。若为设置了分离属性(LOS_TASK_STATUS_DETACHED)的任务,运行结束后将直接销毁。

3.2.2.2运行机制

OpenHarmony 任务管理模块提供任务创建、任务延时、任务挂起和任务恢复、锁任务调度和解锁任务调度、根据ID查询任务控制块信息功能。

用户创建任务时,系统会将任务栈进行初始化,预置上下文。此外,系统还会将“任务入口函数”地址放在相应位置。这样在任务第一次启动进入运行态时,将会执行任务入口函数。

3.2.2.3开发指导
3.2.2.3.1接口说明
表4接口说明表
功能分类接口名称描述
任务的创建和删除LOS_TaskCreateOnly创建任务,并使该任务进入Init状态,不执行任务调度
LOS_TaskCreate创建任务,并使该任务进入Ready状态,并调度
LOS_TaskDelete删除指定的任务
任务状态控制LOS_TaskResume恢复挂起的任务
LOS_TaskSuspend挂起指定的任务
LOS_TaskDelay任务延时等待
LOS_TaskYield显式放权,调整调用任务优先级的任务调度顺序
任务调度的控制LOS_TaskLock锁任务调度
LOS_TaskUnlock解锁任务调度
任务优先级的控制LOS_CurTaskPriSet设置当前任务的优先级
LOS_TaskPriSet设置指定任务的优先级
LOS_TaskPriGet获取指定任务的优先级
任务信息获取LOS_CurTaskIDGet获取当前任务的ID
LOS_TaskInfoGet获取指定任务的信息
任务绑核操作LOS_TaskCpuAffiSet绑定指定任务到指定cpu上运行,仅在多核下使用
LOS_TaskCpuAffiGet获取指定任务的绑核信息,仅在多核下使用
任务调度参数的控制LOS_GetTaskScheduler获取指定任务的调度策略
LOS_SetTaskScheduler设置指定任务的调度参数,包括优先级和调度策略
系统支持的最大任务数LOS_GetSystemTaskMaximum获取系统支持的最大任务数目
3.2.2.3.2开发流程

任务的典型开发流程:

1.通过LOS_TaskCreate创建一个任务。

1)指定任务的执行入口函数

2)指定任务名

3)指定任务的栈大小

4)指定任务的优先级

5)指定任务的属性,是否支持LOS_TASK_STATUS_DETACHED属性

6)多核运行时,可以选择设置任务的绑核属性

2.任务参与调度运行,执行用户指定的业务代码。

3.任务执行结束,如果任务设置了LOS_TASK_STATUS_DETACHED属性,则任务运行结束后自动回收任务资源,如果未设置LOS_TASK_STATUS_DETACHED属性,则需要调用LOS_TaskDelete接口回收任务资源。

注:内核态具有最高权限,可以操作任意进程内的任务。

用户态进程通过系统调用进入内核态后创建的任务属于KProcess, 不属于当前用户态进程。

3.2.2.3.3编程实例
UINT32 g_taskLoID;

UINT32 g_taskHiID;

\#define TSK_PRIOR_HI 4

\#define TSK_PRIOR_LO 5

UINT32 ExampleTaskHi(VOID)

{

UINT32 ret;

PRINTK("Enter TaskHi Handler.\\n");

/\* 延时2个Tick,延时后该任务会挂起,执行剩余任务中最高优先级的任务(g_taskLoID任务) \*/

ret = LOS_TaskDelay(2);

if (ret != LOS_OK) {

PRINTK("Delay Task Failed.\\n");

return LOS_NOK;

}

/\* 2个Tick时间到了后,该任务恢复,继续执行 \*/

PRINTK("TaskHi LOS_TaskDelay Done.\\n");

/\* 挂起自身任务 \*/

ret = LOS_TaskSuspend(g_taskHiID);

if (ret != LOS_OK) {

PRINTK("Suspend TaskHi Failed.\\n");

return LOS_NOK;

}

PRINTK("TaskHi LOS_TaskResume Success.\\n");

return LOS_OK;

}

/\* 低优先级任务入口函数 \*/

UINT32 ExampleTaskLo(VOID)

{

UINT32 ret;

PRINTK("Enter TaskLo Handler.\\n");

/\* 延时2个Tick,延时后该任务会挂起,执行剩余任务中就高优先级的任务(背景任务) \*/

ret = LOS_TaskDelay(2);

if (ret != LOS_OK) {

PRINTK("Delay TaskLo Failed.\\n");

return LOS_NOK;

}

PRINTK("TaskHi LOS_TaskSuspend Success.\\n");

/\* 恢复被挂起的任务g_taskHiID \*/

ret = LOS_TaskResume(g_taskHiID);

if (ret != LOS_OK) {

PRINTK("Resume TaskHi Failed.\\n");

return LOS_NOK;

}

PRINTK("TaskHi LOS_TaskDelete Success.\\n");

return LOS_OK;

}

/\* 任务测试入口函数,在里面创建优先级不一样的两个任务 \*/

UINT32 ExampleTaskCaseEntry(VOID)

{

UINT32 ret;

TSK_INIT_PARAM_S initParam = {0};

/\* 锁任务调度 \*/

LOS_TaskLock();

PRINTK("LOS_TaskLock() Success!\\n");

initParam.pfnTaskEntry = (TSK_ENTRY_FUNC)ExampleTaskHi;

initParam.usTaskPrio = TSK_PRIOR_HI;

initParam.pcName = "HIGH_NAME";

initParam.uwStackSize = LOS_TASK_MIN_STACK_SIZE;

initParam.uwResved = LOS_TASK_STATUS_DETACHED;

/\* 创建高优先级任务,由于锁任务调度,任务创建成功后不会马上执行 \*/

ret = LOS_TaskCreate(&g_taskHiID, \&initParam);

if (ret != LOS_OK) {

LOS_TaskUnlock();

PRINTK("ExampleTaskHi create Failed! ret=%d\\n", ret);

return LOS_NOK;

}

PRINTK("ExampleTaskHi create Success!\\n");

initParam.pfnTaskEntry = (TSK_ENTRY_FUNC)ExampleTaskLo;

initParam.usTaskPrio = TSK_PRIOR_LO;

initParam.pcName = "LOW_NAME";

initParam.uwStackSize = LOS_TASK_MIN_STACK_SIZE;

initParam.uwResved = LOS_TASK_STATUS_DETACHED;

/\* 创建低优先级任务,由于锁任务调度,任务创建成功后不会马上执行 \*/

ret = LOS_TaskCreate(&g_taskLoID, \&initParam);

if (ret!= LOS_OK) {

LOS_TaskUnlock();

PRINTK("ExampleTaskLo create Failed!\\n");

return LOS_NOK;

}

PRINTK("ExampleTaskLo create Success!\\n");

/\* 解锁任务调度,此时会发生任务调度,执行就绪列表中最高优先级任务 \*/

LOS_TaskUnlock();

while(1){};

return LOS_OK;

}

编译运行得到的结果为:

LOS_TaskLock() Success!

ExampleTaskHi create Success!

ExampleTaskLo create Success!

Enter TaskHi Handler.

Enter TaskLo Handler.

TaskHi LOS_TaskDelay Done.

TaskHi LOS_TaskSuspend Success.

TaskHi LOS_TaskResume Success.

TaskHi LOS_TaskDelete Success.
3.2.3调度器
3.2.3.1 基本概念

OpenHarmony LiteOS-A内核 了高优先级优先+同优先级时间片轮转的抢占式调度机制,系统从启动开始基于real time的时间轴向前运行,使得该调度算法具有很好的实时性。

OpenHarmony 的调度算法将tickless机制天然嵌入到调度算法中,一方面使得系统具有更低的功耗,另一方面也使得tick中断按需响应,减少无用的tick中断响应,进一步提高系统的实时性。

OpenHarmony 的进程调度策略支持SCHED_RR,线程调度策略支持SCHED_RR和SCHED_FIFO。

OpenHarmony 调度的最小单元为线程。

3.2.3.2运行机制

OpenHarmony 采用进程优先级队列+线程优先级队列的方式,进程优先级范围为0-31,共有32个进程优先级桶队列,每个桶队列对应一个线程优先级桶队列;线程优先级范围也为0-31,一个线程优先级桶队列也有32个优先级队列。

在这里插入图片描述

图 7调度优先级桶队列示意图

OpenHarmony 在系统启动内核初始化之后开始调度,运行过程中创建的进程或线程会被加入到调度队列,系统根据进程和线程的优先级及线程的时间片消耗情况选择最优的线程进行调度运行,线程一旦调度到就会从调度队列上删除,线程在运行过程中发生阻塞,会被加入到对应的阻塞队列中并触发一次调度,调度其它线程运行。如果调度队列上没有可以调度的线程,则系统就会选择KIdle进程的线程进行调度运行。

在这里插入图片描述

图 8 调度流程示意图
3.2.3.3 接口说明
表5接口说明表
功能分类接口名称描述
触发系统调度LOS_Schedule触发系统调度

注:系统启动初始化阶段,不允许触发调度。

3.3内存管理

3.3.1 堆内存管理
3.3.1.1基本概念

内存管理模块管理系统的内存资源,它是操作系统的核心模块之一,主要包括内存的初始化、分配以及释放。OpenHarmony LiteOS-A的堆内存管理提供内存初始化、分配、释放等功能。在系统运行过程中,堆内存管理模块通过对内存的申请/释放来管理用户和OS对内存的使用,使内存的利用率和使用效率达到最优,同时最大限度地解决系统的内存碎片问题。

3.3.1.2运行机制

堆内存管理,即在内存资源充足的情况下,根据用户需求,从系统配置的一块比较大的连续内存(内存池,也是堆内存)中分配任意大小的内存块。当用户不需要该内存块时,又可以释放回系统供下一次使用。与静态内存相比,动态内存管理的优点是按需分配,缺点是内存池中容易出现碎片。OpenHarmony LiteOS-A堆内存在TLSF算法的基础上,对区间的划分进行了优化,获得更优的性能,降低了碎片率。动态内存核心算法框图如下:

在这里插入图片描述

图 9动态内存核心算法

根据空闲内存块的大小,使用多个空闲链表来管理。根据内存空闲块大小分为两个部分:[4, 127]和[27, 231],如上图size class所示:

1)对[4,127]区间的内存进行等分,如上图下半部分所示,分为31个小区间,每个小区间对应内存块大小为4字节的倍数。每个小区间对应一个空闲内存链表和用于标记对应空闲内存链表是否为空的一个比特位,值为1时,空闲链表非空。[4,127]区间的31个小区间内存对应31个比特位进行标记链表是否为空。

2)大于127字节的空闲内存块,按照2的次幂区间大小进行空闲链表管理。总共分为24个小区间,每个小区间又等分为8个二级小区间,见上图上半部分的Size Class和Size SubClass部分。每个二级小区间对应一个空闲链表和用于标记对应空闲内存链表是否为空的一个比特位。总共24*8=192个二级小区间,对应192个空闲链表和192个比特位进行标记链表是否为空。

例如,当有40字节的空闲内存需要插入空闲链表时,对应小区间[40,43],第10个空闲链表,位图标记的第10比特位。把40字节的空闲内存挂载第10个空闲链表上,并判断是否需要更新位图标记。当需要申请40字节的内存时,根据位图标记获取存在满足申请大小的内存块的空闲链表,从空闲链表上获取空闲内存节点。如果分配的节点大于需要申请的内存大小,进行分割节点操作,剩余的节点重新挂载到相应的空闲链表上。当有580字节的空闲内存需要插入空闲链表时,对应二级小区间[2^9,2^9+2^6],第31+2*8=47个空闲链表,并使用位图的第47个比特位来标记链表是否为空。把580字节的空闲内存挂载第47个空闲链表上,并判断是否需要更新位图标记。当需要申请580字节的内存时,根据位图标记获取存在满足申请大小的内存块的空闲链表,从空闲链表上获取空闲内存节点。如果分配的节点大于需要申请的内存大小,进行分割节点操作,剩余的节点重新挂载到相应的空闲链表上。如果对应的空闲链表为空,则向更大的内存区间去查询是否有满足条件的空闲链表,实际计算时,会一次性查找到满足申请大小的空闲链表。内存管理结构如下图所示:
在这里插入图片描述

图 10 动态内存管理结构图

1)内存池池头部分:内存池池头部分包含内存池信息、位图标记数组和空闲链表数组。内存池信息包含内存池起始地址及堆区域总大小,内存池属性。位图标记数组有7个32位无符号整数组成,每个比特位标记对应的空闲链表是否挂载空闲内存块节点。空闲内存链表包含223个空闲内存头节点信息,每个空闲内存头节点信息维护内存节点头和空闲链表中的前驱、后继空闲内存节点。

2)内存池节点部分:包含3种类型节点:未使用空闲内存节点,已使用内存节点和尾节点。每个内存节点维护一个前序指针,指向内存池中上一个内存节点,还维护内存节点的大小和使用标记。空闲内存节点和已使用内存节点后面的内存区域是数据域,尾节点没有数据域。

3.3.1.3开发指导

1)使用场景

堆内存管理的主要工作是动态分配并管理用户申请到的内存区间,主要用于用户需要使用大小不等的内存块的场景,当用户需要使用内存时,可以通过操作系统的动态内存申请函数索取指定大小的内存块。一旦使用完毕,通过内存释放函数释放所占用内存,使之可以重复使用。

2)接口说明

OpenHarmony LiteOS-A的堆内存管理主要为用户提供以下功能,接口详细信息可以查看API参考。

表6 堆内存管理接口说明表
功能分类接口名称描述
初始化和删除内存池LOS_MemInit初始化一块指定的动态内存池,大小为size
LOS_MemDeInit删除指定内存池,仅打开LOSCFG_MEM_MUL_POOL时有效
申请、释放动态内存LOS_MemAlloc从指定动态内存池中申请size长度的内存
LOS_MemFree释放从指定动态内存中申请的内存
LOS_MemRealloc按size大小重新分配内存块,并将原内存块内容拷贝到新内存块。如果新内存块申请成功,则释放原内存块
LOS_MemAllocAlign从指定动态内存池中申请长度为size且地址按boundary字节对齐的内存
获取内存池信息LOS_MemPoolSizeGet获取指定动态内存池的总大小
LOS_MemTotalUsedGet获取指定动态内存池的总使用量大小
LOS_MemInfoGet获取指定内存池的内存结构信息,包括空闲内存大小、已使用内存大小、空闲内存块数量、已使用的内存块数量、最大的空闲内存块大小
LOS_MemPoolList打印系统中已初始化的所有内存池,包括内存池的起始地址、内存池大小、空闲内存总大小、已使用内存总大小、最大的空闲内存块大小、空闲内存块数量、已使用的内存块数量。仅打开LOSCFG_MEM_MUL_POOL时有效
获取内存块信息LOS_MemFreeNodeShow打印指定内存池的空闲内存块的大小及数量
检查指定内存池的完整性LOS_MemIntegrityCheck对指定内存池做完整性检查,仅打开LOSCFG_BASE_MEM_NODE_INTEGRITY_CHECK时有效

3)开发流程

本节介绍使用动态内存的典型场景开发流程。

1)初始化LOS_MemInit:初始一个内存池后生成一个内存池控制头、尾节点EndNode,剩余的内存被标记为FreeNode内存节点。注:EndNode作为内存池末尾的节点,size为0。

2)申请任意大小的动态内存LOS_MemAlloc:判断动态内存池中是否存在大于申请量大小的空闲内存块空间,若存在,则划出一块内存块,以指针形式返回,若不存在,返回NULL。如果空闲内存块大于申请量,需要对内存块进行分割,剩余的部分作为空闲内存块挂载到空闲内存链表上。

3)释放动态内存LOS_MemFree:回收内存块,供下一次使用。调用LOS_MemFree释放内存块,则会回收内存块,并且将其标记为FreeNode。在回收内存块时,相邻的FreeNode会自动合并。

4)编程实例

本实例执行以下步骤:

1初始化一个动态内存池。

2从动态内存池中申请一个内存块。

3在内存块中存放一个数据。

4打印出内存块中的数据。

5释放该内存块。

示例代码如下:

\#include "los_memory.h"

\#define TEST_POOL_SIZE (2\*1024\*1024)

\__attribute__((aligned(4))) UINT8 g_testPool[TEST_POOL_SIZE];

VOID Example_DynMem(VOID)

{

UINT32 \*mem = NULL;

UINT32 ret;

/\*初始化内存池\*/

ret = LOS_MemInit(g_testPool, TEST_POOL_SIZE);

if (LOS_OK == ret) {

printf("Mem init success!\\n");

} else {

printf("Mem init failed!\\n");

return;

}

/\*分配内存\*/

mem = (UINT32 \*)LOS_MemAlloc(g_testPool, 4);

if (NULL == mem) {

printf("Mem alloc failed!\\n");

return;

}

printf("Mem alloc success!\\n");

/\*赋值\*/

\*mem = 828;

printf("\*mem = %d\\n", \*mem);

/\*释放内存\*/

ret = LOS_MemFree(g_testPool, mem);

if (LOS_OK == ret) {

printf("Mem free success!\\n");

} else {

printf("Mem free failed!\\n");

}

return;

}

UINT32 ExampleDynMemEntry(VOID)

{

UINT32 ret;

TSK_INIT_PARAM_S initParam = {0};

initParam.pfnTaskEntry = (TSK_ENTRY_FUNC)Example_DynMem;

initParam.usTaskPrio = 10;

initParam.pcName = "Example_DynMem";

initParam.uwStackSize = LOSCFG_BASE_CORE_TSK_DEFAULT_STACK_SIZE;

initParam.uwResved = LOS_TASK_STATUS_DETACHED;

/\* 创建高优先级任务,由于锁任务调度,任务创建成功后不会马上执行 \*/

ret = LOS_TaskCreate(&g_taskHiID, \&initParam);

if (ret != LOS_OK) {

LOS_TaskUnlock();

PRINTK("Example_DynMem create Failed! ret=%d\\n", ret);

return LOS_NOK;

}

PRINTK("Example_DynMem create Success!\\n");

while(1){};

return LOS_OK;

}

输出结果如下:

Mem init success!

Mem alloc success!

\*mem = 828

Mem free success!
3.3.2 物理内存管理
3.3.2.1基本概念

物理内存是计算机上最重要的资源之一,指的是实际的内存设备提供的、可以通过CPU总线直接进行寻址的内存空间,其主要作用是为操作系统及程序提供临时存储空间。LiteOS-A内核管理物理内存是通过分页实现的,除了内核堆占用的一部分内存外,其余可用内存均以4KiB为单位划分成页帧,内存分配和内存回收便是以页帧为单位进行操作。内核采用伙伴算法管理空闲页面,可以降低一定的内存碎片率,提高内存分配和释放的效率,但是一个很小的块往往也会阻塞一个大块的合并,导致不能分配较大的内存块。

3.3.2.2运行机制

如下图所示,LiteOS-A内核的物理内存使用分布视图,主要由内核镜像、内核堆及物理页组成。内核堆部分见堆内存管理一节。

在这里插入图片描述

图 11 物理内存使用分布图

伙伴算法把所有空闲页帧分成9个内存块组,每组中内存块包含2的幂次方个页帧,例如:第0组的内存块包含2的0次方个页帧,即1个页帧;第8组的内存块包含2的8次方个页帧,即256个页帧。相同大小的内存块挂在同一个链表上进行管理。

  1. 申请内存

系统申请12KiB内存,即3个页帧时,9个内存块组中索引为3的链表挂着一块大小为8个页帧的内存块满足要求,分配出12KiB内存后还剩余20KiB内存,即5个页帧,将5个页帧分成2的幂次方之和,即4跟1,尝试查找伙伴进行合并。4个页帧的内存块没有伙伴则直接插到索引为2的链表上,继续查找1个页帧的内存块是否有伙伴,索引为0的链表上此时有1个,如果两个内存块地址连续则进行合并,并将内存块挂到索引为1的链表上,否则不做处理。

在这里插入图片描述

图12 内存申请示意图
  1. 释放内存

系统释放12KiB内存,即3个页帧,将3个页帧分成2的幂次方之和,即2跟1,尝试查找伙伴进行合并,索引为1的链表上有1个内存块,若地址连续则合并,并将合并后的内存块挂到索引为2的链表上,索引为0的链表上此时也有1个,如果地址连续则进行合并,并将合并后的内存块挂到索引为1的链表上,此时继续判断是否有伙伴,重复上述操作。

在这里插入图片描述

图13 内存释放示意图
3.3.2.3接口说明
表7 物理内存管理模块接口
功能分类接口名称描述
申请物理内存LOS_PhysPageAlloc申请一个物理页
LOS_PhysPagesAlloc申请物理页并挂在对应的链表上
LOS_PhysPagesAllocContiguous申请多页地址连续的物理内存
释放物理内存LOS_PhysPageFree释放一个物理页
LOS_PhysPagesFree释放挂在链表上的物理页
LOS_PhysPagesFreeContiguous释放多页地址连续的物理内存
查询地址LOS_VmPageGet根据物理地址获取其对应的物理页结构体指针
LOS_PaddrToKVaddr根据物理地址获取其对应的内核虚拟地址
3.3.2.4编程实例

编程示例主要是调用申请、释放接口对内存进行操作,包括申请一个页以及多个页的示例。

\#include "los_vm_phys.h"

\#define PHYS_PAGE_SIZE 0x4000

// 申请一个页

VOID OsPhysPagesAllocTest3(VOID)

{

PADDR_T newPaddr;

VOID \*kvaddr = NULL;

LosVmPage \*newPage = NULL;

newPage = LOS_PhysPageAlloc();

if (newPage == NULL) {

printf("LOS_PhysPageAlloc fail\\n");

return;

}

printf("LOS_PhysPageAlloc success\\n");

newPaddr = VM_PAGE_TO_PHYS(newPage);

kvaddr = OsVmPageToVaddr(newPage);

// Handle the physical memory

// Free the physical memory

LOS_PhysPageFree(newPage);

}

// 申请多个页,不要求连续

VOID OsPhysPagesAllocTest2(VOID)

{

UINT32 sizeCount;

UINT32 count;

UINT32 size = PHYS_PAGE_SIZE;

LosVmPage \*vmPageArray[PHYS_PAGE_SIZE \>\> PAGE_SHIFT] = { NULL };

UINT32 i = 0;

LosVmPage \*vmPage = NULL;

PADDR_T pa;

size = LOS_Align(size, PAGE_SIZE);

if (size == 0) {

return;

}

sizeCount = size \>\> PAGE_SHIFT;

LOS_DL_LIST_HEAD(pageList);

count = LOS_PhysPagesAlloc(sizeCount, \&pageList);

if (count \< sizeCount) {

printf("failed to allocate enough pages (ask %zu, got %zu)\\n", sizeCount, count);

goto ERROR;

}

printf("LOS_PhysPagesAlloc success\\n");

while ((vmPage = LOS_ListRemoveHeadType(&pageList, LosVmPage, node))) {

pa = vmPage-\>physAddr;

vmPageArray[i++] = vmPage;

// Handle the physical memory

}

// Free the physical memory

for (i = 0; i \< sizeCount; ++i) {

LOS_PhysPageFree(vmPageArray[i]);

}

return;

ERROR:

(VOID)LOS_PhysPagesFree(&pageList);

}

// 申请多个连续页

VOID OsPhysPagesAllocTest1(VOID)

{

VOID \*ptr = NULL;

LosVmPage \*page = NULL;

UINT32 size = PHYS_PAGE_SIZE;

ptr = LOS_PhysPagesAllocContiguous(ROUNDUP(size, PAGE_SIZE) \>\> PAGE_SHIFT);

if (ptr == NULL) {

printf("LOS_PhysPagesAllocContiguous fail\\n");

return;

}

printf("LOS_PhysPagesAllocContiguous success\\n");

// Handle the physical memory

// Free the physical memory

page = OsVmVaddrToPage((VOID \*)ptr);

LOS_PhysPagesFreeContiguous((VOID \*)ptr, size \>\> PAGE_SHIFT);

}

UINT32 ExamplePhyMemCaseEntry(VOID)

{

OsPhysPagesAllocTest1();

OsPhysPagesAllocTest2();

OsPhysPagesAllocTest3();

return LOS_OK;

}

编译运行得到的结果为:

LOS_PhysPagesAllocContiguous success

LOS_PhysPagesAlloc success

LOS_PhysPageAlloc success
3.3.3虚拟内存管理
3.3.3.1基本概念

虚拟内存管理是计算机系统管理内存的一种技术。每个进程都有连续的虚拟地址空间,虚拟地址空间的大小由CPU的位数决定,32位的硬件平台可以提供的最大的寻址空间为0-4GiB。整个4GiB空间分成两部分,LiteOS-A内核占据3GiB的高地址空间,1GiB的低地址空间留给进程使用。各个进程空间的虚拟地址空间是独立的,代码、数据互不影响。

系统将虚拟内存分割为称为虚拟页的内存块,大小一般为4KiB或64KiB,LiteOS-A内核默认的页的大小是4KiB,根据需要可以对MMU(Memory Management Units)进行配置。虚拟内存管理操作的最小单位就是一个页,LiteOS-A内核中一个虚拟地址区间region包含地址连续的多个虚拟页,也可只有一个页。同样,物理内存也会按照页大小进行分割,分割后的每个内存块称为页帧。虚拟地址空间划分:内核态占高地址3GiB(0x40000000 ~ 0xFFFFFFFF),用户态占低地址1GiB(0x01000000 ~ 0x3F000000),具体见下表,详细可以查看或配置los_vm_zone.h。

表 8 内核态地址规划
Zone名称描述属性
DMA zone供IO设备的DMA使用。Uncache
Normal zone加载内核代码段、数据段、堆和栈的地址区间。Cache
high mem zone可以分配连续的虚拟内存,但其所映射的物理内存不一定连续。Cache
表 9 用户态虚地址规划
Zone名称描述属性
代码段用户态代码段地址区间。Cache
用户态堆地址区间。Cache
用户态栈地址区间。Cache
共享库用于加载用户态共享库的地址区间,包括mmap所映射的区间。Cache
3.3.3.2 运行机制

虚拟内存管理中,虚拟地址空间是连续的,但是其映射的物理内存并不一定是连续的,如下图所示。可执行程序加载运行,CPU访问虚拟地址空间的代码或数据时存在两种情况:

1)CPU访问的虚拟地址所在的页,如V0,已经与具体的物理页P0做映射,CPU通过找到进程对应的页表条目(详见虚实映射),根据页表条目中的物理地址信息访问物理内存中的内容并返回。

2)CPU访问的虚拟地址所在的页,如V2,没有与具体的物理页做映射,系统会触发缺页异常,系统申请一个物理页,并把相应的信息拷贝到物理页中,并且把物理页的起始地址更新到页表条目中。此时CPU重新执行访问虚拟内存的指令便能够访问到具体的代码或数据。

在这里插入图片描述

图 14 内存映射示意图
3.3.3.3接口说明
表10 虚拟内存管理模块接口
功能分类接口名称描述
获取进程空间系列接口LOS_CurrSpaceGet获取当前进程空间结构体指针
LOS_SpaceGet获取虚拟地址对应的进程空间结构体指针
LOS_GetKVmSpace获取内核进程空间结构体指针
LOS_GetVmallocSpace获取vmalloc空间结构体指针
LOS_GetVmSpaceList获取进程空间链表指针
虚拟地址区间region相关的操作LOS_RegionFind根据起始地址在进程空间内查找是否存在虚拟地址区间
LOS_RegionRangeFind根据地址区间在进程空间内查找是否存在虚拟地址区间
LOS_IsRegionFileValid判断虚拟地址区间region是否与文件关联映射
LOS_RegionAlloc申请空闲的虚拟地址区间
LOS_RegionFree释放进程空间内特定的region
LOS_RegionEndAddr获取指定地址区间region的结束地址
LOS_RegionSize获取region的大小
LOS_IsRegionTypeFile判断是否为文件内存映射
LOS_IsRegionPermUserReadOnly判断地址区间是否是用户空间只读属性
LOS_IsRegionFlagPrivateOnly判断地址区间是否是具有私有属性
LOS_SetRegionTypeFile设置文件内存映射属性
LOS_IsRegionTypeDev判断是否为设备内存映射
LOS_SetRegionTypeDev设置设备内存映射属性
LOS_IsRegionTypeAnon判断是否为匿名映射
LOS_SetRegionTypeAnon设置匿名映射属性
地址校验LOS_IsUserAddress判断地址是否在用户态空间
LOS_IsUserAddressRange判断地址区间是否在用户态空间
LOS_IsKernelAddress判断地址是否在内核空间
LOS_IsKernelAddressRange判断地址区间是否在内核空间
LOS_IsRangeInSpace判断地址区间是否在进程空间内
vmalloc操作LOS_VMallocvmalloc申请内存
LOS_VFreevmalloc释放内存
LOS_IsVmallocAddress判断地址是否是通过vmalloc申请的
内存申请系列接口LOS_KernelMalloc申请小于16KiB的内存则通过堆内存池获取,否则申请多个连续物理页
LOS_KernelMallocAlign申请具有对齐属性的内存,申请规则:申请小于16KiB的内存则通过堆内存池获取,否则申请多个连续物理页
LOS_KernelFree释放内核堆内存
LOS_KernelRealloc重新分配内核内存空间
其他LOS_PaddrQuery根据虚拟地址获取对应的物理地址
LOS_VmSpaceFree释放进程空间,包括虚拟内存区间、页表等信息
LOS_VmSpaceReserve在进程空间中预留一块内存空间
LOS_VaddrToPaddrMmap将指定长度的物理地址区间与虚拟地址区间做映射,需提前申请物理地址区间
LOS_UserSpaceVmAlloc根据地址、大小、权限等信息在用户进程空间内申请地址区间region
3.3.3.4 开发流程

虚拟内存相关接口的使用:

1)根据进程空间获取的系列接口可以得到进程空间结构体,进而可以读取结构体相应信息。

2)对虚拟地址区间做相关操作:

1通过LOS_RegionAlloc申请虚拟地址区间;

2通过LOS_RegionFind、LOS_RegionRangeFind可以查询是否存在相应的地址区间;

3通过LOS_RegionFree释放虚拟地址区间。

3)vmalloc接口及内存申请系列接口可以在内核中根据需要申请内存。

注:内存申请系列接口申请的内存要求物理内存是连续的,当系统内存无法满足大块连续内存的申请条件时会申请失败,一般适用于小块内存的申请;vmalloc相关接口申请的内存可以获得不连续的物理内存,但其是以页(当前系统一个页为4096字节)为单位的,当需要申请以页为整数倍的内存时可以通过vmalloc申请,例如文件系统中文件读取需要较大的缓存,便可以通过vmalloc相关接口申请内存。

3.3.4虚拟映射
3.3.4.1基本概念

虚实映射是指系统通过内存管理单元(MMU,Memory Management Unit)将进程空间的虚拟地址与实际的物理地址做映射,并指定相应的访问权限、缓存属性等。程序执行时,CPU访问的是虚拟内存,通过MMU页表条目找到对应的物理内存,并做相应的代码执行或数据读写操作。MMU的映射由页表(Page Table)来描述,其中保存虚拟地址和物理地址的映射关系以及访问权限等。每个进程在创建的时候都会创建一个页表,页表由一个个页表条目(Page Table Entry, PTE)构成,每个页表条目描述虚拟地址区间与物理地址区间的映射关系。MMU中有一块页表缓存,称为快表(TLB, Translation Lookaside Buffers),做地址转换时,MMU首先在TLB中查找,如果找到对应的页表条目可直接进行转换,提高了查询效率。CPU访问内存或外设的示意图如下:

在这里插入图片描述

图 15 CPU访问内存或外设的示意图
3.3.4.2运行机制

虚实映射其实就是一个建立页表的过程。MMU有多级页表,LiteOS-A内核采用二级页表描述进程空间。每个一级页表条目描述符占用4个字节,可表示1MiB的内存空间的映射关系,即1GiB用户空间(LiteOS-A内核中用户空间占用1GiB)的虚拟内存空间需要1024个。系统创建用户进程时,在内存中申请一块4KiB大小的内存块作为一级页表的存储区域,二级页表根据当前进程的需要做动态的内存申请。

用户程序加载启动时,会将代码段、数据段映射进虚拟内存空间(详细可参考动态加载与链接),此时并没有物理页做实际的映射;

程序执行时,如下图粗箭头所示,CPU访问虚拟地址,通过MMU查找是否有对应的物理内存,若该虚拟地址无对应的物理地址则触发缺页异常,内核申请物理内存并将虚实映射关系及对应的属性配置信息写进页表,并把页表条目缓存至TLB,接着CPU可直接通过转换关系访问实际的物理内存;

若CPU访问已缓存至TLB的页表条目,无需再访问保存在内存中的页表,可加快查找速度。

在这里插入图片描述

图17 CPU访问内存示意图
3.3.4.3接口说明
表 11 虚实映射模块接口
功能分类接口名称描述
MMU相关操作LOS_ArchMmuQuery获取进程空间虚拟地址对应的物理地址以及映射属性。
LOS_ArchMmuMap映射进程空间虚拟地址区间与物理地址区间。
LOS_ArchMmuUnmap解除进程空间虚拟地址区间与物理地址区间的映射关系。
LOS_ArchMmuChangeProt修改进程空间虚拟地址区间的映射属性。
LOS_ArchMmuMove将进程空间一个虚拟地址区间的映射关系转移至另一块未使用的虚拟地址区间重新做映射。
3.3.4.4开发流程

虚实映射相关接口的使用:

1)通过LOS_ArchMmuMap映射一块物理内存。

2)对映射的地址区间做相关操作:

1通过LOS_ArchMmuQuery可以查询相应虚拟地址区间映射的物理地址区间及映射属性;

2通过LOS_ArchMmuChangeProt修改映射属性;

3通过LOS_ArchMmuMove做虚拟地址区间的重映射。

3)通过LOS_ArchMmuUnmap解除映射关系。

注:上述接口的使用都是基于MMU初始化完成以及相关进程页表的建立,MMU在系统启动阶段已完成初始化,进程创建的时候会建立页表,开发者无需介入操作。

3.4内核通信机制

3.4.1事件
3.4.1.1基本概念

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

多任务环境下,任务之间往往需要同步操作,一个等待即是一个同步。事件可以提供一对多、多对多的同步操作。

一对多同步模型:一个任务等待多个事件的触发。可以是任意一个事件发生时唤醒任务处理事件,也可以是几个事件都发生后才唤醒任务处理事件。

多对多同步模型:多个任务等待多个事件的触发。

OpenHarmony LiteOS-A的事件模块提供的事件,具有如下特点:

1任务通过创建事件控制块来触发事件或等待事件。

2事件间相互独立,内部实现为一个32位无符号整型,每一位标识一种事件类型。第25位不可用,因此最多可支持31种事件类型。

3事件仅用于任务间的同步,不提供数据传输功能。

4多次向事件控制块写入同一事件类型,在被清零前等效于只写入一次。

5多个任务可以对同一事件进行读写操作。

6支持事件读写超时机制。

3.4.1.2运行机制

事件控制块

/\*\*

\* 事件控制块数据结构

\*/

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时,表示不清除任何事件,保持事件集合原状。

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

在这里插入图片描述

图 18 事件运作原理图
3.4.1.3接口说明

OpenHarmony LiteOS-A内核的事件模块提供下面几种功能。

表12 事件模块接口
功能分类接口名称描述
初始化事件LOS_EventInit初始化一个事件控制块
读/写事件LOS_EventRead读取指定事件类型,超时时间为相对时间:单位为Tick
LOS_EventWrite写指定的事件类型
清除事件LOS_EventClear清除指定的事件类型
校验事件掩码LOS_EventPoll根据用户传入的事件ID、事件掩码及读取模式,返回用户传入的事件是否符合预期
销毁事件LOS_EventDestroy销毁指定的事件控制块
3.4.1.4开发流程

事件的典型开发流程:

1初始化事件控制块

2阻塞读事件控制块

3写入相关事件

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

5处理事件控制块

6事件控制块销毁

注:1进行事件读写操作时,事件的第25位为保留位,不可以进行位设置。

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

3.4.1.5编程实例

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

在任务Example_TaskEntry创建任务Example_Event,其中任务Example_Event优先级高于Example_TaskEntry。

在任务Example_Event中读事件0x00000001,阻塞,发生任务切换,执行任务Example_TaskEntry。

在任务Example_TaskEntry向任务Example_Event写事件0x00000001,发生任务切换,执行任务Example_Event。

Example_Event得以执行,直到任务结束。

Example_TaskEntry得以执行,直到任务结束。:

\#include "los_event.h"

\#include "los_task.h"

\#include "securec.h"

/\* 任务ID \*/

UINT32 g_testTaskId;

/\* 事件控制结构体 \*/

EVENT_CB_S g_exampleEvent;

/\* 等待的事件类型 \*/

\#define EVENT_WAIT 0x00000001

/\* 用例任务入口函数 \*/

VOID Example_Event(VOID)

{

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, 100);

if (event == EVENT_WAIT) {

printf("Example_Event,read event :0x%x\\n", event);

} else {

printf("Example_Event,read event timeout\\n");

}

}

UINT32 Example_EventEntry(VOID)

{

UINT32 ret;

TSK_INIT_PARAM_S task1;

/\* 事件初始化 \*/

ret = LOS_EventInit(&g_exampleEvent);

if (ret != LOS_OK) {

printf("init event failed .\\n");

return -1;

}

/\* 创建任务 \*/

(VOID)memset_s(&task1, sizeof(TSK_INIT_PARAM_S), 0, sizeof(TSK_INIT_PARAM_S));

task1.pfnTaskEntry = (TSK_ENTRY_FUNC)Example_Event;

task1.pcName = "EventTsk1";

task1.uwStackSize = LOSCFG_BASE_CORE_TSK_DEFAULT_STACK_SIZE;

task1.usTaskPrio = 5;

ret = LOS_TaskCreate(&g_testTaskId, \&task1);

if (ret != LOS_OK) {

printf("task create failed.\\n");

return LOS_NOK;

}

/\* 写g_testTaskId 等待事件 \*/

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_TaskDelete(g_testTaskId);

if (ret != LOS_OK) {

printf("task delete 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
3.4.2信号量
3.4.2.1 基本概念

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

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

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

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

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

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

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

3.4.2.2运行机制

信号量控制块

/**

* 信号量控制块数据结构

*/

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返回。否则唤醒该信号量等待任务队列上的第一个任务。

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

运行示意图如下图所示:

在这里插入图片描述

图 19 信号量运作示意图
3.4.2.3接口说明
表 13 信号量模块接口
功能分类接口名称描述
创建/删除信号量LOS_SemCreate创建信号量,返回信号量ID
LOS_BinarySemCreate创建二值信号量,其计数值最大为1
LOS_SemDelete删除指定的信号量
申请/释放信号量LOS_SemPend申请指定的信号量,并设置超时时间
LOS_SemPost释放指定的信号量
3.4.2.4 开发流程

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

2申请信号量LOS_SemPend。

3释放信号量LOS_SemPost。

4删除信号量LOS_SemDelete。

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

3.4.2.5 编程实例

本实例实现如下功能:

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

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

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

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

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

\#include "los_sem.h"

\#include "securec.h"

/\* 任务ID \*/

static UINT32 g_testTaskId01;

static UINT32 g_testTaskId02;

/\* 测试任务优先级 \*/

\#define TASK_PRIO_TEST 5

/\* 信号量结构体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;

TSK_INIT_PARAM_S task2;

/\* 创建信号量 \*/

LOS_SemCreate(0, \&g_semId);

/\* 锁任务调度 \*/

LOS_TaskLock();

/\* 创建任务1 \*/

(VOID)memset_s(&task1, sizeof(TSK_INIT_PARAM_S), 0, sizeof(TSK_INIT_PARAM_S));

task1.pfnTaskEntry = (TSK_ENTRY_FUNC)ExampleSemTask1;

task1.pcName = "TestTask1";

task1.uwStackSize = LOSCFG_BASE_CORE_TSK_DEFAULT_STACK_SIZE;

task1.usTaskPrio = TASK_PRIO_TEST;

ret = LOS_TaskCreate(&g_testTaskId01, \&task1);

if (ret != LOS_OK) {

printf("task1 create failed .\\n");

return LOS_NOK;

}

/\* 创建任务2 \*/

(VOID)memset_s(&task2, sizeof(TSK_INIT_PARAM_S), 0, sizeof(TSK_INIT_PARAM_S));

task2.pfnTaskEntry = (TSK_ENTRY_FUNC)ExampleSemTask2;

task2.pcName = "TestTask2";

task2.uwStackSize = LOSCFG_BASE_CORE_TSK_DEFAULT_STACK_SIZE;

task2.usTaskPrio = (TASK_PRIO_TEST - 1);

ret = LOS_TaskCreate(&g_testTaskId02, \&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.

ExampleSemTask2 get sem g_semId and then delay 20 ticks.

ExampleSemTask1 try get sem g_semId, timeout 10 ticks.

ExampleSemTask1 timeout and try get sem g_semId wait forever.

ExampleSemTask2 post sem g_semId.

ExampleSemTask1 wait_forever and get sem g_semId.
3.4.3互斥锁
3.4.3.1基本概念

互斥锁又称互斥型信号量,用于实现对共享资源的独占式处理。当有任务持有时,这个任务获得该互斥锁的所有权。当该任务释放它时,任务失去该互斥锁的所有权。当一个任务持有互斥锁时,其他任务将不能再持有该互斥锁。多任务环境下往往存在多个任务竞争同一共享资源的应用场景,互斥锁可被用于对共享资源的保护从而实现独占式访问。

互斥锁属性包含3个属性:协议属性、优先级上限属性和类型属性。协议属性用于处理不同优先级的任务申请互斥锁,协议属性包含如下三种:

LOS_MUX_PRIO_NONE:不对申请互斥锁的任务的优先级进行继承或保护操作。

LOS_MUX_PRIO_INHERIT:先级继承属性,默认设置为该属性,对申请互斥锁的任务的优先级进行继承。在互斥锁设置为本协议属性情况下,申请互斥锁时,如果高优先级任务阻塞于互斥锁,则把持有互斥锁任务的优先级备份到任务控制块的优先级位图中,然后把任务优先级设置为和高优先级任务相同的优先级;持有互斥锁的任务释放互斥锁时,从任务控制块的优先级位图恢复任务优先级。

S_MUX_PRIO_PROTECT:优先级保护属性,对申请互斥锁的任务的优先级进行保护。在互斥锁设置为本协议属性情况下,申请互斥锁时,如果任务优先级小于互斥锁优先级上限,则把任务优先级备份到任务控制块的优先级位图中,然后把任务优先级设置为互斥锁优先级上限属性值;释放互斥锁时,从任务控制块的优先级位图恢复任务优先级。

互斥锁的类型属性用于标记是否检测死锁,是否支持递归持有,类型属性包含如下三种:LOS_MUX_NORMAL:

普通互斥锁,不会检测死锁。如果任务试图对一个互斥锁重复持有,将会引起这个线程的死锁。如果试图释放一个由别的任务持有的互斥锁,或者如果一个任务试图重复释放互斥锁都会引发不可预料的结果。

LOS_MUX_RECURSIVE:

递归互斥锁,默认设置为该属性。在互斥锁设置为本类型属性情况下,允许同一个任务对互斥锁进行多次持有锁,持有锁次数和释放锁次数相同,其他任务才能持有该互斥锁。如果试图持有已经被其他任务持有的互斥锁,或者如果试图释放已经被释放的互斥锁,会返回错误码。

LOS_MUX_ERRORCHECK:

错误检测互斥锁,会自动检测死锁。在互斥锁设置为本类型属性情况下,如果任务试图对一个互斥锁重复持有,或者试图释放一个由别的任务持有的互斥锁,或者如果一个任务试图释放已经被释放的互斥锁,都会返回错误码。

3.4.3.2 运行机制

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

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

在这里插入图片描述

图 20 互斥锁运作示意图
3.4.3.3接口说明
表 14 互斥锁模块接口
功能分类接口名称描述
初始化和销毁互斥锁LOS_MuxInit互斥锁初始化
LOS_MuxDestroy销毁指定的互斥锁
互斥锁的申请和释放LOS_MuxLock申请指定的互斥锁
LOS_MuxTrylock尝试申请指定的互斥锁,不阻塞
LOS_MuxUnlock释放指定的互斥锁
校验互斥锁LOS_MuxIsValid判断互斥锁释放有效
初始化和销毁互斥锁属性LOS_MuxAttrInit互斥锁属性初始化
LOS_MuxAttrDestroy销毁指定的互斥锁属性
设置和获取互斥锁属性LOS_MuxAttrGetType获取指定互斥锁属性的类型属性
LOS_MuxAttrSetType设置指定互斥锁属性的类型属性
LOS_MuxAttrGetProtocol获取指定互斥锁属性的协议属性
LOS_MuxAttrSetProtocol设置指定互斥锁属性的协议属性
LOS_MuxAttrGetPrioceiling获取指定互斥锁属性的优先级上限属性
LOS_MuxAttrSetPrioceiling设置指定互斥锁属性的优先级上限属性
LOS_MuxGetPrioceiling获取互斥锁优先级上限属性
LOS_MuxSetPrioceiling设置互斥锁优先级上限属性
3.4.3.4开发流程

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

1初始化互斥锁LOS_MuxInit。

2申请互斥锁LOS_MuxLock。

申请模式有三种:无阻塞模式、永久阻塞模式、定时阻塞模式。

1)无阻塞模式:任务需要申请互斥锁,若该互斥锁当前没有任务持有,或者持有该互斥锁的任务和申请该互斥锁的任务为同一个任务,则申请成功;

2)永久阻塞模式:任务需要申请互斥锁,若该互斥锁当前没有被占用,则申请成功。否则,该任务进入阻塞态,系统切换到就绪任务中优先级高者继续执行。任务进入阻塞态后,直到有其他任务释放该互斥锁,阻塞任务才会重新得以执行;

3)定时阻塞模式:任务需要申请互斥锁,若该互斥锁当前没有被占用,则申请成功。否则该任务进入阻塞态,系统切换到就绪任务中优先级高者继续执行。任务进入阻塞态后,指定时间超时前有其他任务释放该互斥锁,或者用 户指定时间超时后,阻塞任务才会重新得以执行。

3释放互斥锁LOS_MuxUnlock。

如果有任务阻塞于指定互斥锁,则唤醒被阻塞任务中优先级高的,该任务进入就绪态,并进行任务调度;

如果没有任务阻塞于指定互斥锁,则互斥锁释放成功。

4销毁互斥锁LOS_MuxDestroy。

注:两个任务不能对同一把互斥锁加锁。如果某任务对已被持有的互斥锁加锁,则该任务会被挂起,直到持有该锁的任务对互斥锁解锁,才能执行对这把互斥锁的加锁操作。

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

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

3.4.3.5编程实例

本实例实现如下流程:

任务Example_TaskEntry创建一个互斥锁,锁任务调度,创建两个任务Example_MutexTask1、Example_MutexTask2。Example_MutexTask2优先级高于Example_MutexTask1,解锁任务调度。

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

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

100Tick休眠时间到达后,Example_MutexTask2被唤醒, 释放互斥锁,唤醒Example_MutexTask1。Example_MutexTask1成功获取到互斥锁后,释放,删除互斥锁。

\#include \<string.h\>

\#include "los_mux.h"

/\* 互斥锁 \*/

LosMux g_testMux;

/\* 任务ID \*/

UINT32 g_testTaskId01;

UINT32 g_testTaskId02;

VOID Example_MutexTask1(VOID)

{

UINT32 ret;

printf("task1 try to get mutex, wait 10 ticks.\\n");

/\* 申请互斥锁 \*/

ret = LOS_MuxLock(&g_testMux, 10);

if (ret == LOS_OK) {

printf("task1 get mutex g_testMux.\\n");

/\* 释放互斥锁 \*/

LOS_MuxUnlock(&g_testMux);

return;

}

if (ret == LOS_ETIMEDOUT ) {

printf("task1 timeout and try to get mutex, wait forever.\\n");

/\* 申请互斥锁 \*/

ret = LOS_MuxLock(&g_testMux, LOS_WAIT_FOREVER);

if (ret == LOS_OK) {

printf("task1 wait forever, get mutex g_testMux.\\n");

/\* 释放互斥锁 \*/

LOS_MuxUnlock(&g_testMux);

/\* 删除互斥锁 \*/

LOS_MuxDestroy(&g_testMux);

printf("task1 post and delete mutex g_testMux.\\n");

return;

}

}

return;

}

VOID Example_MutexTask2(VOID)

{

printf("task2 try to get mutex, wait forever.\\n");

/\* 申请互斥锁 \*/

(VOID)LOS_MuxLock(&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_MuxUnlock(&g_testMux);

return;

}

UINT32 Example_MutexEntry(VOID)

{

UINT32 ret;

TSK_INIT_PARAM_S task1;

TSK_INIT_PARAM_S task2;

/\* 初始化互斥锁 \*/

LOS_MuxInit(&g_testMux, NULL);

/\* 锁任务调度 \*/

LOS_TaskLock();

/\* 创建任务1 \*/

memset(&task1, 0, sizeof(TSK_INIT_PARAM_S));

task1.pfnTaskEntry = (TSK_ENTRY_FUNC)Example_MutexTask1;

task1.pcName = "MutexTsk1";

task1.uwStackSize = LOSCFG_BASE_CORE_TSK_DEFAULT_STACK_SIZE;

task1.usTaskPrio = 5;

ret = LOS_TaskCreate(&g_testTaskId01, \&task1);

if (ret != LOS_OK) {

printf("task1 create failed.\\n");

return LOS_NOK;

}

/\* 创建任务2 \*/

memset(&task2, 0, sizeof(TSK_INIT_PARAM_S));

task2.pfnTaskEntry = (TSK_ENTRY_FUNC)Example_MutexTask2;

task2.pcName = "MutexTsk2";

task2.uwStackSize = LOSCFG_BASE_CORE_TSK_DEFAULT_STACK_SIZE;

task2.usTaskPrio = 4;

ret = LOS_TaskCreate(&g_testTaskId02, \&task2);

if (ret != LOS_OK) {

printf("task2 create failed.\\n");

return LOS_NOK;

}

/\* 解锁任务调度 \*/

LOS_TaskUnlock();

return LOS_OK;

}

编译运行得到的结果为:

task1 try to get mutex, wait 10 ticks.

task2 try to get mutex, wait forever.

task2 get mutex g_testMux and suspend 100 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.
3.4.4消息队列
3.4.4.1基本概念

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

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

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

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

1消息以先进先出的方式排队,支持异步读写。

2读队列和写队列都支持超时机制。

3每读取一条消息,就会将该消息节点设置为空闲。

4发送消息类型由通信双方约定,可以允许不同长度(不超过队列的消息节点大小)的消息。

5一个任务能够从任意一个消息队列接收和发送消息。

6多个任务能够从同一个消息队列接收和发送消息。

7创建队列时所需的队列空间,接口内系统自行动态申请内存。

3.4.4.2运行机制

队列控制块

/\*\*

\* 队列控制块数据结构

\*/

typedef struct {

UINT8 \*queueHandle; /\*\*\< Pointer to a queue handle \*/

UINT16 queueState; /\*\*\< Queue state \*/

UINT16 queueLen; /\*\*\< Queue length \*/

UINT16 queueSize; /\*\*\< Node size \*/

UINT32 queueID; /\*\*\< queueID \*/

UINT16 queueHead; /\*\*\< Node head \*/

UINT16 queueTail; /\*\*\< Node tail \*/

UINT16 readWriteableCnt[OS_QUEUE_N_RW]; /\*\*\< Count of readable or writable resources, 0:readable, 1:writable \*/

LOS_DL_LIST readWriteList[OS_QUEUE_N_RW]; /\*\*\< the linked list to be read or written, 0:readlist, 1:writelist \*/

LOS_DL_LIST memList; /\*\*\< Pointer to the memory linked list \*/

} LosQueueCB;

每个队列控制块中都含有队列状态,表示该队列的使用情况:

OS_QUEUE_UNUSED:队列未被使用。

OS_QUEUE_INUSED:队列被使用中。

队列运作原理

创建队列时,创建队列成功会返回队列ID。

在队列控制块中维护着一个消息头节点位置Head和一个消息尾节点位置Tail,用于表示当前队列中消息的存储情况。Head表示队列中被占用的消息节点的起始位置。Tail表示被占用的消息节点的结束位置,也是空闲消息节点的起始位置。队列刚创建时,Head和Tail均指向队列起始位置。

写队列时,根据readWriteableCnt[1]判断队列是否可以写入,不能对已满(readWriteableCnt[1]为0)队列进行写操作。写队列支持两种写入方式:向队列尾节点写入,也可以向队列头节点写入。尾节点写入时,根据Tail找到起始空闲消息节点作为数据写入对象,如果Tail已经指向队列尾部则采用回卷方式。头节点写入时,将Head的前一个节点作为数据写入对象,如果Head指向队列起始位置则采用回卷方式。

读队列时,根据readWriteableCnt[0]判断队列是否有消息需要读取,对全部空闲(readWriteableCnt[0]为0)队列进行读操作会引起任务挂起。如果队列可以读取消息,则根据Head找到最先写入队列的消息节点进行读取。如果Head已经指向队列尾部则采用回卷方式。

删除队列时,根据队列ID找到对应队列,把队列状态置为未使用,把队列控制块置为初始状态,并释放队列所占内存。

在这里插入图片描述

图21 队列读写数据操作示意图
3.4.4.3接口说明
表 15 接口说明表
功能分类接口名称描述
创建/删除消息队列LOS_QueueCreate创建一个消息队列,由系统动态申请队列空间
LOS_QueueDelete根据队列ID删除一个指定队列
读/写队列(不带拷贝)LOS_QueueRead读取指定队列头节点中的数据(队列节点中的数据实际上是一个地址)
LOS_QueueWrite向指定队列尾节点中写入入参bufferAddr的值(即buffer的地址)
LOS_QueueWriteHead向指定队列头节点中写入入参bufferAddr的值(即buffer的地址)
读/写队列(带拷贝)LOS_QueueReadCopy读取指定队列头节点中的数据
LOS_QueueWriteCopy向指定队列尾节点中写入入参bufferAddr中保存的数据
LOS_QueueWriteHeadCopy向指定队列头节点中写入入参bufferAddr中保存的数据
获取队列信息LOS_QueueInfoGet获取指定队列的信息,包括队列ID、队列长度、消息节点大小、头节点、尾节点、可读节点数量、可写节点数量、等待读操作的任务、等待写操作的任务
3.4.4.4开发流程

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

3.4.4.5编程实例

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

1通过LOS_TaskCreate创建任务1和任务2。

2通过LOS_QueueCreate创建一个消息队列。

3在任务1 SendEntry中发送消息。

4在任务2 RecvEntry中接收消息。

5通过LOS_QueueDelete删除队列。

\#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, task2;

TSK_INIT_PARAM_S initParam = {0};

initParam.pfnTaskEntry = (TSK_ENTRY_FUNC)SendEntry;

initParam.usTaskPrio = 9;

initParam.uwStackSize = LOSCFG_BASE_CORE_TSK_DEFAULT_STACK_SIZE;

initParam.pcName = "SendQueue";

LOS_TaskLock();

ret = LOS_TaskCreate(&task1, \&initParam);

if(ret != LOS_OK) {

printf("create task1 failed, error: %x\\n", ret);

return ret;

}

initParam.pcName = "RecvQueue";

initParam.pfnTaskEntry = (TSK_ENTRY_FUNC)RecvEntry;

ret = LOS_TaskCreate(&task2, \&initParam);

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 test example

create the queue success!

recv message: test message

delete the queue success!
3.4.5读写锁
3.4.5.1 基本概念

读写锁与互斥锁类似,可用来同步同一进程中的各个任务,但与互斥锁不同的是,其允许多个读操作并发重入,而写操作互斥。

相对于互斥锁的开锁或闭锁状态,读写锁有三种状态:读模式下的锁,写模式下的锁,无锁。

读写锁的使用规则:

1保护区无写模式下的锁,任何任务均可以为其增加读模式下的锁。

2保护区处于无锁状态下,才可增加写模式下的锁。

多任务环境下往往存在多个任务访问同一共享资源的应用场景,读模式下的锁以共享状态对保护区访问,而写模式下的锁可被用于对共享资源的保护从而实现独占式访问。

这种共享-独占的方式非常适合多任务中读数据频率远大于写数据频率的应用中,提高应用多任务并发度。

3.4.5.2运行机制

相较于互斥锁,读写锁如何实现读模式下的锁及写模式下的锁来控制多任务的读写访问呢?

若A任务首次获取了写模式下的锁,有其他任务来获取或尝试获取读模式下的锁,均无法再上锁。

若A任务获取了读模式下的锁,当有任务来获取或尝试获取读模式下的锁时,读写锁计数均加一。

3.4.5.3接口说明
表16 接口说明表
功能分类接口名称描述
读写锁的创建和删除LOS_RwlockInit创建读写锁
LOS_RwlockDestroy删除指定的读写锁
读模式下的锁的申请LOS_RwlockRdLock申请指定的读模式下的锁
LOS_RwlockTryRdLock尝试申请指定的读模式下的锁
写模式下的锁的申请LOS_RwlockWrLock申请指定的写模式下的锁
LOS_RwlockTryWrLock尝试申请指定的写模式下的锁
读写锁的释放LOS_RwlockUnLock释放指定读写锁
读写锁有效性判断LOS_RwlockIsValid判断读写锁有效性
3.4.5.4开发流程

1读写锁典型场景的开发流程:

1创建读写锁LOS_RwlockInit。

2申请读模式下的锁LOS_RwlockRdLock或写模式下的锁LOS_RwlockWrLock。

2申请读模式下的锁:

1)若无人持有锁,读任务可获得锁。

2)若有人持有锁,读任务可获得锁,读取顺序按照任务优先级。

3)若有人(非自己)持有写模式下的锁,则当前任务无法获得锁,直到写模式下的锁释放。

申请写模式下的锁:

1)若该锁当前没有任务持有,或者持有该读模式下的锁的任务和申请该锁的任务为同一个任务,则申请成功,可立即获得写模式下的锁。

2)若该锁当前已经存在读模式下的锁,且读取任务优先级较高,则当前任务挂起,直到读模式下的锁释放。

3申请读模式下的锁和写模式下的锁均有三种:无阻塞模式、永久阻塞模式、定时阻塞模式,区别在于挂起任务的时间。

4释放读写锁LOS_RwlockUnLock。

1)如果有任务阻塞于指定读写锁,则唤醒被阻塞任务中优先级高的,该任务进入就绪态,并进行任务调度;

2)如果没有任务阻塞于指定读写锁,则读写锁释放成功。

3)删除读写锁LOS_RwlockDestroy。

注:

读写锁不能在中断服务程序中使用。

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

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

3.4.6 用户态快速互斥锁
3.4.6.1 基本概念

Futex(Fast userspace mutex,用户态快速互斥锁)是内核提供的一种系统调用能力,通常作为基础组件与用户态的相关锁逻辑结合组成用户态锁,是一种用户态与内核态共同作用的锁,例如用户态mutex锁、barrier与cond同步锁、读写锁。其用户态部分负责锁逻辑,内核态部分负责锁调度。

当用户态线程请求锁时,先在用户态进行锁状态的判断维护,若此时不产生锁的竞争,则直接在用户态进行上锁返回;反之,则需要进行线程的挂起操作,通过Futex系统调用请求内核介入来挂起线程,并维护阻塞队列。

当用户态线程释放锁时,先在用户态进行锁状态的判断维护,若此时没有其他线程被该锁阻塞,则直接在用户态进行解锁返回;反之,则需要进行阻塞线程的唤醒操作,通过Futex系统调用请求内核介入来唤醒阻塞队列中的线程。

3.4.6.2 运行机制

当用户态产生锁的竞争或释放需要进行相关线程的调度操作时,会触发Futex系统调用进入内核,此时会将用户态锁的地址传入内核,并在内核的Futex中以锁地址来区分用户态的每一把锁,因为用户态可用虚拟地址空间为1GiB,为了便于查找、管理,内核Futex采用哈希桶来存放用户态传入的锁。

当前哈希桶共有80个,0~63号桶用于存放私有锁(以虚拟地址进行哈希),64~79号桶用于存放共享锁(以物理地址进行哈希),私有/共享属性通过用户态锁的初始化以及Futex系统调用入参确定。

在这里插入图片描述

图 22 Futex设计图

如图22,每个futex哈希桶中存放被futex_list串联起来的哈希值相同的futex node,每个futex node对应一个被挂起的task,node中key值唯一标识一把用户态锁,具有相同key值的node被queue_list串联起来表示被同一把锁阻塞的task队列。

3.4.6.3接口说明
表 17 Futex模块接口
功能分类接口名称描述
设置线程等待OsFutexWait向Futex表中插入代表被阻塞的线程的node
唤醒被阻塞线程OsFutexWake唤醒一个被指定锁阻塞的线程
调整锁的地址OsFutexRequeue调整指定锁在Futex表中的位置

注:Futex系统调用通常与用户态逻辑共同组成用户态锁,故推荐使用用户态POSIX接口的锁。

3.4.7信号
3.4.7.1基本概念

信号(signal)是一种常用的进程间异步通信机制,用软件的方式模拟中断信号,当一个进程需要传递信息给另一个进程时,则会发送一个信号给内核,再由内核将信号传递至指定进程,而指定进程不必进行等待信号的动作。

3.4.7.2运行机制

信号的运作流程分为三个部分,如表1:

表 18 信号的运作流程及相关接口(用户态接口)
功能分类接口名称描述
注册信号回调函数signal注册信号总入口及注册和去注册某信号的回调函数。
sigaction功能同signal,但增加了信号发送相关的配置选项,目前仅支持SIGINFO结构体中的部分参数。
发送信号kill发送信号给某个进程或进程内发送消息给某线程,为某进程下的线程设置信号标志位。
pthread_kill
raise
alarm
abort
触发回调由系统调用与中断触发,内核态与用户态切换前会先进入用户态指定函数并处理完相应回调函数,再回到原用户态程序继续运行。

注:

信号机制为提供给用户态程序进程间通信的能力,故推荐使用上表1列出的用户态POSIX相关接口。 注册回调函数:

void \*signal(int sig, void (\*func)(int))(int);

a. 31 号信号,该信号用来注册该进程的回调函数处理入口,不可重复注册。 b. 0-30 号信号,该信号段用来注册与去注册回调函数。 注册回调函数:

int sigaction(int, const struct sigaction \*__restrict, struct sigaction \*__restrict);

支持信号注册的配置修改和配置获取,目前仅支持SIGINFO的选项,SIGINFO内容见sigtimedwait接口内描述。 发送信号: a. 进程接收信号存在默认行为,单不支持POSIX标准所给出的STOP及COTINUE、COREDUMP功能。 b. 进程无法屏蔽SIGSTOP、SIGKILL、SIGCONT信号。 c. 某进程后被杀死后,若其父进程不回收该进程,其转为僵尸进程。 d. 进程接收到某信号后,直到该进程被调度后才会执行信号回调。 e. 进程结束后会发送SIGCHLD信号给父进程,该发送动作无法取消。 f. 无法通过信号唤醒处于DELAY状态的进程。

3.5 时间管理

3.5.1基本概念

时间管理以系统时钟为基础。时间管理提供给应用程序所有和时间有关的服务。系统时钟是由定时/计数器产生的输出脉冲触发中断而产生的,一般定义为整数或长整数。输出脉冲的周期叫做一个“时钟滴答”。系统时钟也称为时标或者Tick。一个Tick的时长可以静态配置。用户是以秒、毫秒为单位计时,而操作系统时钟计时是以Tick为单位的,当用户需要对系统操作时,例如任务挂起、延时等,输入秒为单位的数值,此时需要时间管理模块对二者进行转换。

Tick与秒之间的对应关系可以配置。

Cycle:系统最小的计时单位。Cycle的时长由系统主频决定,系统主频就是每秒钟的Cycle数。

Tick:Tick是操作系统的基本时间单位,对应的时长由系统主频及每秒Tick数决定,由用户配置。

OpenHarmony系统的时间管理模块提供时间转换、统计、延迟功能以满足用户对时间相关需求的实现。

用户需要了解当前系统运行的时间以及Tick与秒、毫秒之间的转换关系时,需要使用到时间管理模块的接口。

3.5.2接口说明

OpenHarmony LiteOS-A内核的时间管理提供下面几种功能,接口详细信息可以查看API参考。

表 19 时间管理相关接口说明
功能分类接口名称描述
时间转换LOS_MS2Tick毫秒转换成Tick
LOS_Tick2MSTick转换成毫秒
时间统计LOS_TickCountGet获取当前Tick数
LOS_CyclePerTickGet每个Tick的cycle数
3.5.3开发流程

1调用时间转换接口;

2获取系统Tick数完成时间统计等。

3.5.4编程实例

前置条件:

1)配置好LOSCFG_BASE_CORE_TICK_PER_SECOND,即系统每秒的Tick数。

2)配置好OS_SYS_CLOCK 系统时钟频率,单位:Hz。

时间转换:

VOID Example_TransformTime(VOID)

{

UINT32 uwMs;

UINT32 uwTick;

uwTick = LOS_MS2Tick(10000); //10000 ms数转换为Tick数

PRINTK("uwTick = %d \\n",uwTick);

uwMs= LOS_Tick2MS(100); //100 Tick数转换为ms数

PRINTK("uwMs = %d \\n",uwMs);

}

时间统计和时间延迟:

VOID Example_GetTime(VOID)

{

UINT32 uwcyclePerTick;

UINT64 uwTickCount;

uwcyclePerTick = LOS_CyclePerTickGet(); //每个Tick多少Cycle数

if(0 != uwcyclePerTick)

{

PRINTK("LOS_CyclePerTickGet = %d \\n", uwcyclePerTick);

}

uwTickCount = LOS_TickCountGet(); //获取Tick数

if(0 != uwTickCount)

{

PRINTK("LOS_TickCountGet = %d \\n", (UINT32)uwTickCount);

}

LOS_TaskDelay(200);//延迟200 Tick

uwTickCount = LOS_TickCountGet();

if(0 != uwTickCount)

{

PRINTK("LOS_TickCountGet after delay = %d \\n", (UINT32)uwTickCount);

}

}

编译运行的结果如下:

时间转换:

uwTick = 10000

uwMs = 100

时间统计和时间延迟:

LOS_CyclePerTickGet = 49500

LOS_TickCountGet = 5042

LOS_TickCountGet after delay = 5242

3.6软件定时器

3.6.1基本概念

软件定时器,是基于系统Tick时钟中断且由软件来模拟的定时器,当经过设定的Tick时钟计数值后会触发用户定义的回调函数。定时精度与系统Tick时钟的周期有关。硬件定时器受硬件的限制,数量上不足以满足用户的实际需求,因此为了满足用户需求,提供更多的定时器,Huawei LiteOS操作系统提供软件定时器功能。软件定时器扩展了定时器的数量,允许创建更多的定时业务。、

软件定时器功能上支持:

1静态裁剪:能通过宏关闭软件定时器功能。

2软件定时器创建。

3软件定时器启动。

4软件定时器停止。

5软件定时器删除。

6软件定时器剩余Tick数获取。

3.6.2运行机制

软件定时器是系统资源,在模块初始化的时候已经分配了一块连续的内存,系统支持的最大定时器个数由los_config.h中的LOSCFG_BASE_CORE_SWTMR_LIMIT宏配置。软件定时器使用了系统的一个队列和一个任务资源,软件定时器的触发遵循队列规则,先进先出。同一时刻设置的定时时间短的定时器总是比定时时间长的靠近队列头,满足优先被触发的准则。软件定时器以Tick为基本计时单位,当用户创建并启动一个软件定时器时,OpenHarmony系统会根据当前系统Tick时间及用户设置的定时间隔确定该定时器的到期Tick时间,并将该定时器控制结构挂入计时全局链表。

当Tick中断到来时,在Tick中断处理函数中扫描软件定时器的计时全局链表,看是否有定时器超时,若有则将超时的定时器记录下来。

Tick中断处理函数结束后,软件定时器任务(优先级为最高)被唤醒,在该任务中调用之前记录下来的定时器的超时回调函数。

定时器状态

OS_SWTMR_STATUS_UNUSED(未使用):系统在定时器模块初始化的时候将系统中所有定时器资源初始化成该状态。

OS_SWTMR_STATUS_CREATED(创建未启动/停止):在未使用状态下调用LOS_SwtmrCreate接口或者启动后调用LOS_SwtmrStop接口后,定时器将变成该状态。

OS_SWTMR_STATUS_TICKING(计数):在定时器创建后调用LOS_SwtmrStart接口,定时器将变成该状态,表示定时器运行时的状态。

定时器模式

OpenHarmony系统的软件定时器提供三类定时器机制:

第一类是单次触发定时器,这类定时器在启动后只会触发一次定时器事件,然后定时器自动删除。

第二类是周期触发定时器,这类定时器会周期性的触发定时器事件,直到用户手动停止定时器,否则将永远持续执行下去。

第三类也是单次触发定时器,但与第一类不同之处在于这类定时器超时后不会自动删除,需要调用定时器删除接口删除定时器。

3.6.3接口说明

OpenHarmony LiteOS-A内核的软件定时器模块提供下面几种功能,接口详细信息可以查看API参考。

表 20 软件定时器接口说明
功能分类接口名称描述
创建、删除定时器LOS_SwtmrCreate创建软件定时器
LOS_SwtmrDelete删除软件定时器
启动、停止定时器LOS_SwtmrStart启动软件定时器
LOS_SwtmrStop停止软件定时器
获得软件定时剩余Tick数LOS_SwtmrTimeGet获得软件定时器剩余Tick数
3.6.4开发流程

软件定时器的典型开发流程:

1配置软件定时器。

1)确认配置项LOSCFG_BASE_CORE_SWTMR和LOSCFG_BASE_IPC_QUEUE为打开状态;

2)配置LOSCFG_BASE_CORE_SWTMR_LIMIT最大支持的软件定时器数;

3)配置OS_SWTMR_HANDLE_QUEUE_SIZE软件定时器队列最大长度;

2创建定时器LOS_SwtmrCreate。

1)创建一个指定计时时长、指定超时处理函数、指定触发模式的软件定时器;

2)返回函数运行结果,成功或失败;

3启动定时器LOS_SwtmrStart。

4获得软件定时器剩余Tick数LOS_SwtmrTimeGet。

5停止定时器LOS_SwtmrStop。

6删除定时器LOS_SwtmrDelete。

注:

软件定时器的回调函数中不要做过多操作,不要使用可能引起任务挂起或者阻塞的接口或操作。

软件定时器使用了系统的一个队列和一个任务资源,软件定时器任务的优先级设定为0,且不允许修改 。

系统可配置的软件定时器资源个数是指:整个系统可使用的软件定时器资源总个数,而并非是用户可使用的软件定时器资源个数。例如:系统软件定时器多占用一个软件定时器资源数,那么用户能使用的软件定时器资源就会减少一个。

创建单次软件定时器,该定时器超时执行完回调函数后,系统会自动删除该软件定时器,并回收资源。

创建单次不自删除属性的定时器,用户需要调用定时器删除接口删除定时器,回收定时器资源,避免资源泄露。

3.6.5编程实例

前置条件

1在los_config.h中,将LOSCFG_BASE_CORE_SWTMR配置项打开。

2配置好LOSCFG_BASE_CORE_SWTMR_LIMIT最大支持的软件定时器数。

3配置好OS_SWTMR_HANDLE_QUEUE_SIZE软件定时器队列最大长度。

\#include "los_swtmr.h"

void Timer1_Callback(uint32_t arg);

void Timer2_Callback(uint32_t arg);

UINT32 g_timercount1 = 0;

UINT32 g_timercount2 = 0;

void Timer1_Callback(uint32_t arg) // 回调函数1

{

unsigned long tick_last1;

g_timercount1++;

tick_last1=(UINT32)LOS_TickCountGet(); // 获取当前Tick数

PRINTK("g_timercount1=%d\\n",g_timercount1);

PRINTK("tick_last1=%d\\n",tick_last1);

}

void Timer2_Callback(uint32_t arg) // 回调函数2

{

unsigned long tick_last2;

tick_last2=(UINT32)LOS_TickCountGet();

g_timercount2 ++;

PRINTK("g_timercount2=%d\\n",g_timercount2);

PRINTK("tick_last2=%d\\n",tick_last2);

}

void Timer_example(void)

{

UINT16 id1;

UINT16 id2; // timer id

UINT32 uwTick;

/\* 创建单次软件定时器,Tick数为1000,启动到1000Tick数时执行回调函数1 \*/

LOS_SwtmrCreate (1000, LOS_SWTMR_MODE_ONCE, Timer1_Callback, \&id1, 1);

/\* 创建周期性软件定时器,每100Tick数执行回调函数2 \*/

LOS_SwtmrCreate(100, LOS_SWTMR_MODE_PERIOD, Timer2_Callback, \&id2, 1);

PRINTK("create Timer1 success\\n");

LOS_SwtmrStart (id1); //启动单次软件定时器

dprintf("start Timer1 success\\n");

LOS_TaskDelay(200); // 延时200Tick数

LOS_SwtmrTimeGet(id1, \&uwTick); // 获得单次软件定时器剩余Tick数

PRINTK("uwTick =%d\\n", uwTick);

LOS_SwtmrStop(id1); // 停止软件定时器

PRINTK("stop Timer1 success\\n");

LOS_SwtmrStart(id1);

LOS_TaskDelay(1000);

LOS_SwtmrDelete(id1); // 删除软件定时器

PRINTK("delete Timer1 success\\n");

LOS_SwtmrStart(id2); // 启动周期性软件定时器

PRINTK("start Timer2\\n");

LOS_TaskDelay(1000);

LOS_SwtmrStop(id2);

LOS_SwtmrDelete(id2);

}

运行结果

create Timer1 success
start Timer1 success
uwTick =800
stop Timer1 success
g_timercount1=1
tick_last1=1201
delete Timer1 success
start Timer2
g_timercount2 =1
tick_last1=1301
g_timercount2 =2
tick_last1=1401
g_timercount2 =3
tick_last1=1501
g_timercount2 =4
tick_last1=1601
g_timercount2 =5
tick_last1=1701
g_timercount2 =6
tick_last1=1801
g_timercount2 =7
tick_last1=1901
g_timercount2 =8
tick_last1=2001
g_timercount2 =9
tick_last1=2101
g_timercount2 =10
tick_last1=2201

3.7原子操作

3.7.1基本概念

在支持多任务的操作系统中,修改一块内存区域的数据需要“读取-修改-写入”三个步骤。然而同一内存区域的数据可能同时被多个任务访问,如果在修改数据的过程中被其他任务打断,就会造成该操作的执行结果无法预知。

使用开关中断的方法固然可以保证多任务执行结果符合预期,但是显然这种方法会影响系统性能。

ARMv6架构引入了LDREX和STREX指令,以支持对共享存储器更缜密的非阻塞同步。由此实现的原子操作能确保对同一数据的“读取-修改-写入”操作在它的执行期间不会被打断,即操作的原子性。

3.7.2运行机制

OpenHarmony系统通过对ARMv6架构中的LDREX和STREX进行封装,向用户提供了一套原子性的操作接口。

1、LDREX Rx, [Ry]

读取内存中的值,并标记对该段内存的独占访问:

1)读取寄存器Ry指向的4字节内存数据,保存到Rx寄存器中。

2)对Ry指向的内存区域添加独占访问标记。

2、STREX Rf, Rx, [Ry]

检查内存是否有独占访问标记,如果有则更新内存值并清空标记,否则不更新内存:

(1)有独占访问标记

1)将寄存器Rx中的值更新到寄存器Ry指向的内存。

2)标志寄存器Rf置为0。

(2)没有独占访问标记

1)不更新内存。

2)标志寄存器Rf置为1。

3、判断标志寄存器

1)标志寄存器为0时,退出循环,原子操作结束。

2)标志寄存器为1时,继续循环,重新进行原子操作。

3.7.3接口说明

OpenHarmony LiteOS-A内核的原子操作模块提供下面几种功能,接口详细信息可以查看API参考。

表 21 原子操作接口说明
功能分类接口名称描述
LOS_AtomicRead读取32bit原子数据
LOS_Atomic64Read读取64bit原子数据
LOS_AtomicSet设置32bit原子数据
LOS_Atomic64Set设置64bit原子数据
LOS_AtomicAdd对32bit原子数据做加法
LOS_Atomic64Add对64bit原子数据做加法
LOS_AtomicInc对32bit原子数据做加1
LOS_Atomic64Inc对64bit原子数据做加1
LOS_AtomicIncRet对32bit原子数据做加1并返回
LOS_Atomic64IncRet对64bit原子数据做加1并返回
LOS_AtomicSub对32bit原子数据做减法
LOS_Atomic64Sub对64bit原子数据做减法
LOS_AtomicDec对32bit原子数据做减1
LOS_Atomic64Dec对64bit原子数据做减1
LOS_AtomicDecRet对32bit原子数据做减1并返回
LOS_Atomic64DecRet对64bit原子数据做减1并返回
交换LOS_AtomicXchgByte交换8bit内存数据
LOS_AtomicXchg16bits交换16bit内存数据
LOS_AtomicXchg32bits交换32bit内存数据
LOS_AtomicXchg64bits交换64bit内存数据
先比较后交换LOS_AtomicCmpXchgByte比较相同后交换8bit内存数据
LOS_AtomicCmpXchg16bits比较相同后交换16bit内存数据
LOS_AtomicCmpXchg32bits比较相同后交换32bit内存数据
LOS_AtomicCmpXchg64bits比较相同后交换64bit内存数据
3.7.4编程实例

有多个任务对同一个内存数据进行加减或交换等操作时,使用原子操作保证结果的可预知性。

原子操作接口仅支持整型数据。

实例描述

调用原子操作相关接口,观察结果:

1创建两个任务

1)任务一用LOS_AtomicInc对全局变量加100次。

2)任务二用LOS_AtomicDec对全局变量减100次。

2子任务结束后在主任务中打印全局变量的值。

\#include "los_hwi.h"

\#include "los_atomic.h"

\#include "los_task.h"

UINT32 g_testTaskId01;

UINT32 g_testTaskId02;

Atomic g_sum;

Atomic g_count;

UINT32 Example_Atomic01(VOID)

{

int i = 0;

for(i = 0; i \< 100; ++i) {

LOS_AtomicInc(&g_sum);

}

LOS_AtomicInc(&g_count);

return LOS_OK;

}

UINT32 Example_Atomic02(VOID)

{

int i = 0;

for(i = 0; i \< 100; ++i) {

LOS_AtomicDec(&g_sum);

}

LOS_AtomicInc(&g_count);

return LOS_OK;

}

UINT32 Example_AtomicTaskEntry(VOID)

{

TSK_INIT_PARAM_S stTask1={0};

stTask1.pfnTaskEntry = (TSK_ENTRY_FUNC)Example_Atomic01;

stTask1.pcName = "TestAtomicTsk1";

stTask1.uwStackSize = LOSCFG_BASE_CORE_TSK_DEFAULT_STACK_SIZE;

stTask1.usTaskPrio = 4;

stTask1.uwResved = LOS_TASK_STATUS_DETACHED;

TSK_INIT_PARAM_S stTask2={0};

stTask2.pfnTaskEntry = (TSK_ENTRY_FUNC)Example_Atomic02;

stTask2.pcName = "TestAtomicTsk2";

stTask2.uwStackSize = LOSCFG_BASE_CORE_TSK_DEFAULT_STACK_SIZE;

stTask2.usTaskPrio = 4;

stTask2.uwResved = LOS_TASK_STATUS_DETACHED;

LOS_TaskLock();

LOS_TaskCreate(&g_testTaskId01, \&stTask1);

LOS_TaskCreate(&g_testTaskId02, \&stTask2);

LOS_TaskUnlock();

while(LOS_AtomicRead(&g_count) != 2);

PRINTK("g_sum = %d\\n", g_sum);

return LOS_OK;

}

结果验证

g_sum = 0

4扩展组件

4.1 系统调用

4.1.1 基础概念

OpenHarmony LiteOS-A实现了用户态与内核态的区分隔离,用户态程序不能直接访问内核资源,而系统调用则为用户态程序提供了一种访问内核资源、与内核进行交互的通道。

4.1.2 运行机制

如图23所示,用户程序通过调用System API(系统API,通常是系统提供的POSIX接口)进行内核资源访问与交互请求,POSIX接口内部会触发SVC/SWI异常,完成系统从用户态到内核态的切换,然后对接到内核的Syscall Handler(系统调用统一处理接口)进行参数解析,最终分发至具体的内核处理函数。

在这里插入图片描述

图 23 系统调用示意图

Syscall Handler的具体实现在kernel/liteos_a/syscall/los_syscall.c中OsArmA32SyscallHandle函数,在进入系统软中断异常时会调用此函数,并且按照kernel/liteos_a/syscall/syscall_lookup.h中的清单进行系统调用的入参解析,执行各系统调用最终对应的内核处理函数。

注:

系统调用提供基础的用户态程序与内核的交互功能,不建议开发者直接使用系统调用接口,推荐使用内核提供的对外POSIX接口,若需要新增系统调用接口,详见开发指导。

内核向用户态提供的系统调用接口清单详见kernel/liteos_a/syscall/syscall_lookup.h,内核相应的系统调用对接函数清单详见kernel/liteos_a/syscall/los_syscall.h。

4.1.3 开发指导

新增系统调用的典型开发流程如下:

1在LibC库中确定并添加新增的系统调用号。

2在LibC库中新增用户态的函数接口声明及实现。

3在内核系统调用头文件中确定并添加新增的系统调用号及对应内核处理函数的声明。

4在内核中新增该系统调用对应的内核处理函数。

编程实例:

在LibC库syscall.h.in中新增系统调用号

如下所示,其中__NR_new_syscall_sample为新增系统调用号:

...

/\* 当前现有的系统调用清单 \*/

/\* OHOS customized syscalls, not compatible with ARM EABI \*/

\#define \__NR_OHOS_BEGIN 500

\#define \__NR_pthread_set_detach (__NR_OHOS_BEGIN + 0)

\#define \__NR_pthread_join (__NR_OHOS_BEGIN + 1)

\#define \__NR_pthread_deatch (__NR_OHOS_BEGIN + 2)

\#define \__NR_creat_user_thread (__NR_OHOS_BEGIN + 3)

\#define \__NR_processcreat (__NR_OHOS_BEGIN + 4)

\#define \__NR_processtart (__NR_OHOS_BEGIN + 5)

\#define \__NR_printf (__NR_OHOS_BEGIN + 6)

\#define \__NR_dumpmemory (__NR_OHOS_BEGIN + 13)

\#define \__NR_mkfifo (__NR_OHOS_BEGIN + 14)

\#define \__NR_mqclose (__NR_OHOS_BEGIN + 15)

\#define \__NR_realpath (__NR_OHOS_BEGIN + 16)

\#define \__NR_format (__NR_OHOS_BEGIN + 17)

\#define \__NR_shellexec (__NR_OHOS_BEGIN + 18)

\#define \__NR_ohoscapget (__NR_OHOS_BEGIN + 19)

\#define \__NR_ohoscapset (__NR_OHOS_BEGIN + 20)

\#define \__NR_new_syscall_sample (__NR_OHOS_BEGIN + 21) /\* 新增的系统调用号 \__NR_new_syscall_sample:521 \*/

\#define \__NR_syscallend (__NR_OHOS_BEGIN + 22)

...

在LibC库中新增用户态接口的声明与实现

\#include "stdio_impl.h"

\#include "syscall.h"

...

/\* 新增系统调用用户态的接口实现 \*/

void newSyscallSample(int num)

{

printf("user mode: num = %d\\n", num);

\__syscall(SYS_new_syscall_sample, num);

return;

}

在内核系统调用头文件中新增系统调用号

如下所示,在third_party/musl/porting/liteos_a/kernel/include/bits/syscall.h文件中,__NR_new_syscall_sample为新增系统调用号。

...

/\* 当前现有的系统调用清单 \*/

/\* OHOS customized syscalls, not compatible with ARM EABI \*/

\#define \__NR_OHOS_BEGIN 500

\#define \__NR_pthread_set_detach (__NR_OHOS_BEGIN + 0)

\#define \__NR_pthread_join (__NR_OHOS_BEGIN + 1)

\#define \__NR_pthread_deatch (__NR_OHOS_BEGIN + 2)

\#define \__NR_creat_user_thread (__NR_OHOS_BEGIN + 3)

\#define \__NR_processcreat (__NR_OHOS_BEGIN + 4)

\#define \__NR_processtart (__NR_OHOS_BEGIN + 5)

\#define \__NR_printf (__NR_OHOS_BEGIN + 6)

\#define \__NR_dumpmemory (__NR_OHOS_BEGIN + 13)

\#define \__NR_mkfifo (__NR_OHOS_BEGIN + 14)

\#define \__NR_mqclose (__NR_OHOS_BEGIN + 15)

\#define \__NR_realpath (__NR_OHOS_BEGIN + 16)

\#define \__NR_format (__NR_OHOS_BEGIN + 17)

\#define \__NR_shellexec (__NR_OHOS_BEGIN + 18)

\#define \__NR_ohoscapget (__NR_OHOS_BEGIN + 19)

\#define \__NR_ohoscapset (__NR_OHOS_BEGIN + 20)

\#define \__NR_new_syscall_sample (__NR_OHOS_BEGIN + 21) /\* 新增的系统调用号 \__NR_new_syscall_sample:521 \*/

\#define \__NR_syscallend (__NR_OHOS_BEGIN + 22)

...

在kernel/liteos_a/syscall/syscall_lookup.h中,增加一行SYSCALL_HAND_DEF(__NR_new_syscall_sample, SysNewSyscallSample, void,ARG_NUM_1):

...

/\* 当前现有的系统调用清单 \*/

SYSCALL_HAND_DEF(__NR_chown, SysChown, int, ARG_NUM_3)

SYSCALL_HAND_DEF(__NR_chown32, SysChown, int, ARG_NUM_3)

\#ifdef LOSCFG_SECURITY_CAPABILITY

SYSCALL_HAND_DEF(__NR_ohoscapget, SysCapGet, UINT32, ARG_NUM_2)

SYSCALL_HAND_DEF(__NR_ohoscapset, SysCapSet, UINT32, ARG_NUM_1)

\#endif

/\* 新增系统调用 \*/

SYSCALL_HAND_DEF(__NR_new_syscall_sample, SysNewSyscallSample, void, ARG_NUM_1)

...

在内核中新增内核该系统调用对应的处理函数

如下所示,在kernel/liteos_a/syscall/los_syscall.h中,SysNewSyscallSample为新增系统调用的内核处理函数声明:

...

/\* 当前现有的系统调用内核处理函数声明清单 \*/

extern int SysClockSettime64(clockid_t clockID, const struct timespec64 \*tp);

extern int SysClockGettime64(clockid_t clockID, struct timespec64 \*tp);

extern int SysClockGetres64(clockid_t clockID, struct timespec64 \*tp);

extern int SysClockNanoSleep64(clockid_t clk, int flags, const struct timespec64 \*req, struct timespec64 \*rem);

extern int SysTimerGettime64(timer_t timerID, struct itimerspec64 \*value);

extern int SysTimerSettime64(timer_t timerID, int flags, const struct itimerspec64 \*value, struct itimerspec64 \*oldValue);

/\* 新增的系统调用内核处理函数声明 \*/

extern void SysNewSyscallSample(int num);

...

新增的系统调用的内核处理函数实现如下:

include "los_printf.h"

...

/\* 新增系统调用内核处理函数的实现 \*/

void SysNewSyscallSample(int num)

{

PRINTK("kernel mode: num = %d\\n", num);

return;

}

结果验证:

用户态程序调用newSyscallSample(10)接口,得到输出结果如下:

/\* 用户态接口与内核态接口均有输出,证明系统调用已使能 \*/

user mode: num = 10

kernel mode: num = 10

4.2 动态加载与链接

4.2.1 基本概念

OpenHarmony系统的动态加载与链接机制主要是由内核加载器以及动态链接器构成,内核加载器用于加载应用程序以及动态链接器,动态链接器用于加载应用程序所依赖的共享库,并对应用程序和共享库进行符号重定位。与静态链接相比,动态链接是将应用程序与动态库推迟到运行时再进行链接的一种机制。

动态链接的优势:

多个应用程序可以共享一份代码,最小加载单元为页,相对静态链接可以节约磁盘和内存空间。

共享库升级时,理论上将旧版本的共享库覆盖即可(共享库中的接口向下兼容),无需重新链接。

加载地址可以进行随机化处理,防止攻击,保证安全性。

4.2.2 运行机制

内核将应用程序ELF文件的PT_LOAD段信息映射至进程空间。对于ET_EXEC类型的文件,根据PT_LOAD段中p_vaddr进行固定地址映射;对于ET_DYN类型(位置无关的可执行程序,通过编译选项“-fPIE”得到)的文件,内核通过mmap接口选择base基址进行映射(load_addr = base + p_vaddr)。

若应用程序是静态链接的(静态链接不支持编译选项“-fPIE”),设置堆栈信息后跳转至应用程序ELF文件中e_entry指定的地址并运行;若程序是动态链接的,应用程序ELF文件中会有PT_INTERP段,保存动态链接器的路径信息(ET_DYN类型)。musl的动态链接器是libc-musl.so的一部分,libc-musl.so的入口即动态链接器的入口。内核通过mmap接口选择base基址进行映射,设置堆栈信息后跳转至base + e_entry(该e_entry为动态链接器的入口)地址并运行动态链接器。

动态链接器自举并查找应用程序依赖的所有共享库并对导入符号进行重定位,最后跳转至应用程序的e_entry(或base + e_entry),开始运行应用程序。

在这里插入图片描述

图 24 动态加载流程

在这里插入图片描述

图 25 程序执行流程

1加载器与链接器调用mmap映射PT_LOAD段;

2内核调用map_pages接口查找并映射pagecache已有的缓存;

3程序执行时,内存若无所需代码或数据时触发缺页中断,将elf文件内容读入内存,并将该内存块加入pagecache;

4将已读入文件内容的内存块与虚拟地址区间做映射;

5程序继续执行;

至此,程序将在不断地缺页中断中执行。

4.2.3 接口说明
LOS_DoExecveFile

函数原型

INT32 LOS_DoExecveFile(const CHAR \*fileName, CHAR \* const \*argv, CHAR \* const \*envp)

**函数功能:**根据fileName执行一个新的用户程序。

参数说明:

表 22 参数说明表
参数描述
fileName二进制可执行文件名,可以是路径名。
argv程序执行所需的参数序列,以NULL结尾。无需参数时填入NULL。
envp程序执行所需的新的环境变量序列,以NULL结尾。无需新的环境变量时填入NULL。

LOS_DoExecveFile接口一般由用户通过execve系列接口利用系统调用机制调用创建新的进程,内核不能直接调用该接口启动新进程。

4.3 虚拟动态共享库

4.3.1 基本概念

VDSO(Virtual Dynamic Shared Object,虚拟动态共享库)相对于普通的动态共享库,区别在于其so文件不保存在文件系统中,存在于系统镜像中,由内核在运行时确定并提供给应用程序,故称为虚拟动态共享库。

OpenHarmony系统通过VDSO机制实现上层用户态程序可以快速读取内核相关数据的一种通道方法,可用于实现部分系统调用的加速,也可用于实现非系统敏感数据(硬件配置、软件配置)的快速读取。

4.3.2 运行机制

VDSO其核心思想就是内核看护一段内存,并将这段内存映射(只读)进用户态应用程序的地址空间,应用程序通过链接vdso.so后,将某些系统调用替换为直接读取这段已映射的内存从而避免系统调用达到加速的效果。

VDSO总体可分为数据页与代码页两部分:

1)数据页提供内核映射给用户进程的内核时数据;

2)代码页提供屏蔽系统调用的主要逻辑;

在这里插入图片描述

图 26 VDSO系统设计

如图所示,当前VDSO机制有以下几个主要步骤:

① 内核初始化时进行VDSO数据页的创建

② 内核初始化时进行VDSO代码页的创建;

③ 根据系统时钟中断不断将内核一些数据刷新进VDSO的数据页;

④ 用户进程创建时将代码页映射进用户空间;

⑤ 用户程序在动态链接时对VDSO的符号进行绑定;

⑥ 当用户程序进行特定系统调用时(例如clock_gettime(CLOCK_REALTIME_COARSE, &ts)),VDSO代码页会将其拦截;

⑦ VDSO代码页将正常系统调用转为直接读取映射好的VDSO数据页;

⑧ 从VDSO数据页中将数据传回VDSO代码页;

⑨ 将从VDSO数据页获取到的数据作为结果返回给用户程序;

注:

当前VDSO机制支持LibC库clock_gettime接口的CLOCK_REALTIME_COARSE与CLOCK_MONOTONIC_COARSE功能,clock_gettime接口的使用方法详见POSIX标准。用户调用C库接口clock_gettime(CLOCK_REALTIME_COARSE, &ts)或者clock_gettime(CLOCK_MONOTONIC_COARSE, &ts)即可使用VDSO机制。

使用VDSO机制得到的时间精度会与系统tick中断的精度保持一致,适用于对时间没有高精度要求且短时间内会高频触发clock_gettime或gettimeofday系统调用的场景,若有高精度要求,不建议采用VDSO机制。

4.4 轻量级进程间通信

4.4.1 基本概念

LiteIPC是OpenHarmony LiteOS-A内核提供的一种新型IPC(Inter-Process Communication,即进程间通信)机制,不同于传统的System V IPC机制,LiteIPC主要是为RPC(Remote Procedure Call,即远程过程调用)而设计的,而且是通过设备文件的方式对上层提供接口的,而非传统的API函数方式。

LiteIPC中有两个主要概念,一个是ServiceManager,另一个是Service。整个系统只能有一个ServiceManager,而Service可以有多个。ServiceManager有两个主要功能:一是负责Service的注册和注销,二是负责管理Service的访问权限(只有有权限的任务(Task)可以向对应的Service发送IPC消息)。

4.4.2 运行机制

首先将需要接收IPC消息的任务通过ServiceManager注册成为一个Service,然后通过ServiceManager为该Service任务配置访问权限,即指定哪些任务可以向该Service任务发送IPC消息。LiteIPC的核心思想就是在内核态为每个Service任务维护一个IPC消息队列,该消息队列通过LiteIPC设备文件向上层用户态程序分别提供代表收取IPC消息的读操作和代表发送IPC消息的写操作。

4.4.3 接口说明
表 23 LiteIPC模块接口(仅LiteOS-A内部使用)
功能分类接口名称描述
模块初始化OsLiteIpcInit初始化LiteIPC模块
IPC消息内存池LiteIpcPoolInit初始化进程的IPC消息内存池
LiteIpcPoolReInit重新初始化进程的IPC消息内存池
LiteIpcPoolDelete释放进程的IPC消息内存池
Service管理LiteIpcRemoveServiceHandle删除指定的Service

注:LiteIPC模块接口都只在LiteOS-A内部使用。

4.5 文件系统

文件系统(File System,或者简称FS),是操作系统中输入输出的一种主要形式,主要负责和内外部的存储设备交互。

文件系统对上通过C库提供的POSIX标准的操作接口,具体可以参考C库的API文档说明。对下,通过内核态的VFS虚拟层,屏蔽了各个具体文件系统的差异。基本架构如下:

在这里插入图片描述

图 27 文件系统的总体结构
4.5.1 虚拟文件系统
4.5.1.1 基本概念

VFS(Virtual File System)是文件系统的虚拟层,它不是一个实际的文件系统,而是一个异构文件系统之上的软件粘合层,为用户提供统一的类Unix文件操作接口。由于不同类型的文件系统接口不统一,若系统中有多个文件系统类型,访问不同的文件系统就需要使用不同的非标准接口。而通过在系统中添加VFS层,提供统一的抽象接口,屏蔽了底层异构类型的文件系统的差异,使得访问文件系统的系统调用不用关心底层的存储介质和文件系统类型,提高开发效率。

OpenHarmony内核中,VFS框架是通过在内存中的树结构来实现的,树的每个结点都是一个Vnode结构体,父子结点的关系以PathCache结构体保存。VFS最主要的两个功能是:查找节点与统一调用(标准)。

4.5.1.2 运行机制

当前,VFS层主要通过函数指针,实现对不同文件系统类型调用不同接口实现标准接口功能;通过Vnode与PathCache机制,提升路径搜索以及文件访问的性能;通过挂载点管理进行分区管理;通过FD管理进行进程间FD隔离等。下面将对这些机制进行简要说明。

文件系统操作函数指针:VFS层通过函数指针的形式,将统一调用按照不同的文件系统类型,分发到不同文件系统中进行底层操作。各文件系统的各自实现一套Vnode操作、挂载点操作以及文件操作接口,并以函数指针结构体的形式存储于对应Vnode、挂载点、File结构体中,实现VFS层对下访问。

Vnode:Vnode是具体文件或目录在VFS层的抽象封装,它屏蔽了不同文件系统的差异,实现资源的统一管理。Vnode节点主要有以下几种类型:

1挂载点:挂载具体文件系统,如/、/storage

2设备节点:/dev目录下的节点,对应于一个设备,如/dev/mmcblk0

3文件/目录节点:对应于具体文件系统中的文件/目录,如/bin/init

Vnode通过哈希以及LRU机制进行管理。当系统启动后,对文件或目录的访问会优先从哈希链表中查找Vnode缓存,若缓存没有命中,则并从对应文件系统中搜索目标文件或目录,创建并缓存对应的Vnode。当Vnode缓存数量达到上限时,将淘汰长时间未访问的Vnode,其中挂载点Vnode与设备节点Vnode不参与淘汰。当前系统中Vnode的规格默认为512,该规格可以通过LOSCFG_MAX_VNODE_SIZE进行配置。Vnode数量过大,会造成较大的内存占用;Vnode数量过少,则会造成搜索性能下降。下图展示了Vnode的创建流程。

在这里插入图片描述

图 28 Vnode创建流程

PathCache:PathCache是路径缓存,它通过哈希表存储,利用父节点Vnode的地址和子节点的文件名,可以从PathCache中快速查找到子节点对应的Vnode。下图展示了文件/目录的查找流程。

在这里插入图片描述

图 29 文件查找流程

PageCache:PageCache是内核中文件的缓存。当前PageCache仅支持缓存二进制文件,在初次访问文件时通过mmap映射到内存中,下次再访问时,直接从PageCache中读取,可以提升对同一个文件的读写速度。另外基于PageCache可实现以文件为基底的进程间通信。

fd管理:Fd(File Descriptor)是描述一个打开的文件/目录的描述符。当前OpenHarmony内核中,fd总规格为896,分为三种类型:

1普通文件描述符,系统总规格为512。

2Socket描述符,系统总规格为128。

3消息队列描述符,系统总规格为256。

当前OpenHarmony内核中,对不同进程中的fd进行隔离,即进程只能访问本进程的fd,所有进程的fd映射到全局fd表中进行统一分配管理。进程的文件描述符最多有256个。

挂载点管理:当前OpenHarmony内核中,对系统中所有挂载点通过链表进行统一管理。挂载点结构体中,记录了该挂载分区内的所有Vnode。当分区卸载时,会释放分区内的所有Vnode。

4.5.1.3 接口说明

当前文件系统支持的接口如下表所示,表格中的“×”代表对应文件系统不支持该接口。

表 24 支持接口列表
分类接口名称功能FATJFFS2NFSTMPFSPROCFS
文件操作open打开文件
read/pread/readv/preadv读取文件
write/pwrite/writev/pwritev写入文件
lseek设置文件偏移×
close关闭文件
unlink删除文件×
fstat查询文件信息
fallocate预分配大小××××
truncate文件截断××
link创建硬链接××××
symlink创建软链接×××
readlink读取软链接×××
dup复制文件句柄
fsync文件内容刷入设备××××
ioctl设备控制××××
fcntl文件控制操作
目录操作mkdir创建目录×
opendir打开目录
readdir读取目录
closedir关闭目录
telldir获取目录偏移
seekdir设置目录偏移
rewinddir重置目录偏移×
scandir读取目录数据
rmdir删除目录×
chdir切换当前路径
getcwd获取当前路径
realpath相对/绝对路径转换
rename文件/目录重命名×
chmod修改文件/目录属性×××
chown修改文件/目录所有者×××
stat/lstat查询文件/目录信息
access查询文件/目录访问权限
分区操作mount挂载分区
umount卸载分区×
statfs查询挂载分区信息
format格式化分区××××
sync分区内容刷入设备××××
4.5.1.4 编程实例

文件系统的主要开发流程包括挂载/卸载分区,以及系列目录/文件操作。

\#include \<stdio.h\>

\#include \<string.h\>

\#include "sys/stat.h"

\#include "fcntl.h"

\#include "unistd.h"

\#define LOS_OK 0

\#define LOS_NOK -1

int main(void)

{

int ret;

int fd = -1;

ssize_t len;

off_t off;

char mntName[20] = "/storage";

char devName[20] = "/dev/mmcblk0p0";

char dirName[20] = "/storage/test";

char fileName[20] = "/storage/test/file.txt";

char writeBuf[20] = "Hello OpenHarmony!";

char readBuf[20] = {0};

/\* 创建目录“/storage” \*/

ret = mkdir(mntName, 0777);

if (ret != LOS_OK) {

printf("mkdir failed.\\n");

return LOS_NOK;

}

/\* 挂载设备“/dev/mmcblk0p0”到“/storage” \*/

ret = mount(devName, mntName, "vfat", 0, 0);

if (ret != LOS_OK) {

printf("mount failed.\\n");

return LOS_NOK;

}

/\* 创建目录“/storage/test” \*/

ret = mkdir(dirName, 0777);

if (ret != LOS_OK) {

printf("mkdir failed.\\n");

return LOS_NOK;

}

/\* 创建可读写文件“/storage/test/file.txt” \*/

fd = open(fileName, O_RDWR \| O_CREAT, 0777);

if (fd \< 0) {

printf("open file failed.\\n");

return LOS_NOK;

}

/\* 将writeBuf中的内容写入文件 \*/

len = write(fd, writeBuf, strlen(writeBuf));

if (len != strlen(writeBuf)) {

printf("write file failed.\\n");

return LOS_NOK;

}

/\* 将文件内容刷入存储设备中 \*/

ret = fsync(fd);

if (ret != LOS_OK) {

printf("fsync failed.\\n");

return LOS_NOK;

}

/\* 将读写指针偏移至文件头 \*/

off = lseek(fd, 0, SEEK_SET);

if (off != 0) {

printf("lseek failed.\\n");

return LOS_NOK;

}

/\* 将文件内容读出至readBuf中,读取长度为readBuf大小 \*/

len = read(fd, readBuf, sizeof(readBuf));

if (len != strlen(readBuf)) {

printf("read file failed.\\n");

return LOS_NOK;

}

printf("%s\\n", readBuf);

/\* 关闭文件 \*/

ret = close(fd);

if (ret != LOS_OK) {

printf("close failed.\\n");

return LOS_NOK;

}

/\* 删除文件“/storage/test/file.txt” \*/

ret = unlink(fileName);

if (ret != LOS_OK) {

printf("unlink failed.\\n");

return LOS_NOK;

}

/\* 删除目录“/storage/test” \*/

ret = rmdir(dirName);

if (ret != LOS_OK) {

printf("rmdir failed.\\n");

return LOS_NOK;

}

/\* 卸载分区“/storage” \*/

ret = umount(mntName);

if (ret != LOS_OK) {

printf("umount failed.\\n");

return LOS_NOK;

}

/\* 删除目录“/storage” \*/

ret = rmdir(mntName);

if (ret != LOS_OK) {

printf("rmdir failed.\\n");

return LOS_NOK;

}

return LOS_OK;

}

编译运行得到的结果为:Hello OpenHarmony!

4.5.2 支持的文件系统

. FAT、JFFS2、NFS、Ramfs、Procfs

4.5.2.1 FAT

1)基本概念

FAT文件系统是File Allocation Table(文件配置表)的简称,主要包括DBR区、FAT区、DATA区三个区域。其中,FAT区各个表项记录存储设备中对应簇的信息,包括簇是否被使用、文件下一个簇的编号、是否文件结尾等。FAT文件系统有FAT12、FAT16、FAT32等多种格式,其中,12、16、32表示对应格式中FAT表项的字节数,它们同时也限制了文件系统中的最大文件大小。FAT文件系统支持多种介质,特别在可移动存储介质(U盘、SD卡、移动硬盘等)上广泛使用,使嵌入式设备和Windows、Linux等桌面系统保持很好的兼容性,方便用户管理操作文件。

OpenHarmony内核支持FAT12、FAT16与FAT32三种格式的FAT文件系统,具有代码量小、资源占用小、可裁切、支持多种物理介质等特性,并且与Windows、Linux等系统保持兼容,支持多设备、多分区识别等功能。OpenHarmony内核支持硬盘多分区,可以在主分区以及逻辑分区上创建FAT文件系统。

2)运行机制

FAT文件系统设计与物理布局的相关文档在互联网上非常丰富,请开发者自行搜索查看。

OpenHarmony LiteOS-A内核通过Bcache提升FAT文件系统性能,Bcache是block cache的简称。当发生读写时,Bcache会缓存读写扇区附近的扇区,以减少I/O次数,提高性能。Bcache的基本缓存单位为block,每个block大小一致(默认有28个block,每个block缓存64个扇区的数据)。当Bcache脏块率(脏扇区数/总扇区数)达到阈值时,会触发写回;如果脏块率未达到阈值,则不会将缓存数据写回磁盘。如果需要保证数据写回,开发者应当调用sync和fsync触发写回。FAT文件系统的部分接口也会触发写回操作(如close、umount等),但开发者不应当基于这些接口触发写回。

3)开发流程

基本使用流程为挂载→操作→卸载。

SD卡或MMC的设备名为mmcblk[x]p[y],文件系统类型为“vfat”。

示例:

mount("/dev/mmcblk0p0", "/mnt", "vfat", 0, NULL);

FAT文件系统中,单个文件不能大于4 GiB。

当有两个SD卡插槽时,卡0和卡1不固定,先插上的为卡0,后插上的为卡1。

当多分区功能打开,存在多分区的情况下,卡0注册的设备节点/dev/mmcblk0(主设备)和/dev/mmcblk0p0(次设备)是同一个设备,禁止对主设备进行操作。

为避免SD卡使用异常或内存泄漏,SD卡使用过程中拔卡,用户必须先关闭正处于打开状态的文件和目录,并且卸载挂载节点。

在format操作之前,需要首先umount挂载点。

当Bcache功能生效时,需要注意:

当mount函数的入参为MS_NOSYNC时,FAT不会主动将cache的内容写回存储器件。FAT的如下接口(open、close、 unlink、rename、mkdir、rmdir、truncate)不会自动进行sync操作,速度可以提升,但是需要上层主动调用sync来进行数据同步,否则可能会数据丢失。

Bcache有定时写回功能。在menuconfig中开启LOSCFG_FS_FAT_CACHE_SYNC_THREAD选项,打开后系统会创建一个任务定时写回Bcache中的数据,默认每隔5秒检查Bcache中脏数据块比例,超过80%时进行sync操作,将Bcache中的脏数据全部写回磁盘。任务优先级、刷新时间间隔以及脏数据块比例的阈值可分别通过接口LOS_SetSyncThreadPrio、 LOS_SetSyncThreadInterval和LOS_SetDirtyRatioThreshold设置。

当前cache的默认大小为28个块,每个块64个扇区。

4.5.2.2 JFFS2

1)基本概念

JFFS2是Journalling Flash File System Version 2(日志文件系统)的缩写,是针对MTD设备的日志型文件系统。

OpenHarmony内核的JFFS2主要应用于NOR FLASH闪存,其特点是:可读写、支持数据压缩、提供了崩溃/掉电安全保护、提供“写平衡”支持等。闪存与磁盘介质有许多差异,直接将磁盘文件系统运行在闪存设备上,会导致性能和安全问题。为解决这一问题,需要实现一个特别针对闪存的文件系统,JFFS2就是这样一种文件系统。

2)运行机制

关于JFFS2文件系统的在存储设备上的实际物理布局,及文件系统本身的规格说明,请参考JFFS2的官方规格说明文档。

这里仅列举几个对开发者和使用者会有一定影响的JFFS2的重要机制/特征:

Mount机制及速度问题:按照JFFS2的设计,所有的文件会按照一定的规则,切分成大小不等的节点,依次存储到flash设备上。在mount流程中,需要获取到所有的这些节点信息并缓存到内存里。因此,mount速度和flash设备的大小和文件数量的多少成线性比例关系。这是JFFS2的原生设计问题,对于mount速度非常介意的用户,可以在内核编译时开启“Enable JFFS2 SUMMARY”选项,可以极大提升mount的速度。这个选项的原理是将mount需要的信息提前存储到flash上,在mount时读取并解析这块内容,使得mount的速度变得相对恒定。这个实际是空间换时间的做法,会消耗8%左右的额外空间。

写平衡的支持:由于flash设备的物理属性,读写都只能基于某个特定大小的“块”进行,为了防止某些特定的块磨损过于严重,在JFFS2中需要对写入的块进行“平衡”的管理,保证所有的块的写入次数都是相对平均的,进而保证flash设备的整体寿命。

GC(garbage collection)机制:在JFFS2里发生删除动作,实际的物理空间并不会立即释放,而是由独立的GC线程来做空间整理和搬移等GC动作,和所有的GC机制一样,在JFFS2里的GC会对瞬时的读写性能有一定影响。另外,为了有空间能被用来做空间整理,JFFS2会对每个分区预留3块左右的空间,这个空间是用户不可见的。

压缩机制:当前使用的JFFS2,底层会自动的在每次读/写时进行解压/压缩动作,实际IO的大小和用户请求读写的大小并不会一样。特别在写入时,不能通过写入大小来和flash剩余空间的大小来预估写入一定会成功或者失败。

硬链接机制:JFFS2支持硬链接,底层实际占用的物理空间是一份,对于同一个文件的多个硬连接,并不会增加空间的占用;反之,只有当删除了所有的硬链接时,实际物理空间才会被释放。

3)基础开发

对于基于JFFS2和nor flash的开发,总体而言,与其他文件系统非常相似,因为都有VFS层来屏蔽了具体文件系统的差异,对外接口体现也都是标准的POSIX接口。

对于整个裸nor flash设备而言,没有集中的地方来管理和记录分区的信息。因此,需要通过其他的配置方式来传递这部分信息(当前使用的方式是在烧写镜像的时候,使用bootargs参数配置的),然后在代码中调用相应的接口来添加分区,再进行挂载动作。

制作JFFS2文件系统镜像使用mkfs.jffs2工具,制作镜像默认命令如下。页大小默认为4KiB,eraseblock大小默认64KiB。若实际参数与下面不同时,修改相应参数。

./mkfs.jffs2 -d rootfs/ -o rootfs.jffs2

表 25 指令含义表(更详细的介绍可以通过mkfs.jffs2 --help来查看)
指令含义
-s页大小,不指定默认为4KiB
-eeraseblock大小,不指定默认为64KiB
-p镜像大小。在镜像文件后面,用0xFF填充至指定大小,不指定则用0xFF填充至eraseblock对齐。
-d要制作成文件系统镜像的源目录
-o要制成的镜像名称

·挂载JFFS2分区

调用int mount(const char *source, const char *target, const char *filesystemtype, unsigned long mountflags, const void *data)函数实现设备节点和挂载点的挂载。

该函数有五个参数,第一个参数const char *source,表示设备节点,第二个参数const char *target表示挂载点。第三个参数 const char *filesystemtype,表示文件系统类型。

最后两个参数unsigned long mountflags和const void *data表示挂载标志和数据,默认为0和NULL;这一操作也可以在Shell中使用mount命令实现,最后两个参数不需要用户给出。

运行命令:

OHOS \# mount /dev/spinorblk1 /jffs1 jffs2

将从串口得到如下回应信息,表明挂载成功。

OHOS \# mount /dev/spinorblk1 /jffs1 jffs2

mount OK

挂载成功后,用户就能对norflash进行读写操作。

卸载JFFS2分区

调用int umount(const char *target)函数卸载分区,只需要正确给出挂载点即可。

运行命令:

OHOS \# umount /jffs1

将从串口得到如下回应信息,表明卸载成功。

OHOS \# umount /jffs1

umount ok
4.5.2.3 NFS

1)基础概念

NFS是Network File System(网络文件系统)的缩写。它最大的功能是可以通过网络,让不同的机器、不同的操作系统彼此分享其他用户的文件。因此,用户可以简单地将它看做是一个文件系统服务,在一定程度上相当于Windows环境下的共享文件夹。

2)运行机制

OpenHarmony LiteOS-A内核的NFS文件系统指的是NFS的客户端,NFS客户端能够将远程的NFS服务端分享的目录挂载到本地的机器中,运行程序和共享文件,但不占用当前系统的存储空间,在本地端的机器看起来,远程服务端的目录就好像是自己的一个磁盘一样。

3)基础开发

搭建NFS服务器

这里以Ubuntu操作系统为例,说明服务器端设置步骤。

安装NFS服务器软件。

设置好Ubuntu系统的下载源,保证网络连接好的情况下执行:

sudo apt-get install nfs-kernel-server

创建用于挂载的目录并设置完全权限

mkdir -p /home/sqbin/nfs

sudo chmod 777 /home/sqbin/nfs

设置和启动NFS server。

修改NFS配置文件/etc/exports,添加如下一行:

/home/sqbin/nfs \*(rw,no_root_squash,async)

其中/home/sqbin/nfs是NFS共享的根目录。

执行以下命令启动NFS server:

sudo /etc/init.d/nfs-kernel-server start

执行以下命令重启NFS server:

sudo /etc/init.d/nfs-kernel-server restart

设置单板为NFS客户端

本指导中的NFS客户端指运行OpenHarmony内核的设备。

硬件连接设置。

OpenHarmony内核设备连接到NFS服务器的网络。设置两者IP,使其处于同一网段。比如,设置NFS服务器的IP为10.67.212.178/24,设置OpenHarmony内核设备IP为10.67.212.3/24,注意:此IP为内网私有IP地址,用户使用时有差异,以用户实际IP为准。

OpenHarmony内核设备上的IP信息可通过ifconfig命令查看。

启动网络,确保单板到NFS服务器之间的网络通畅。

启动以太网或者其他类型网络,使用ping命令检查到服务器的网络是否通畅。

OHOS \# ping 10.67.212.178

[0]Reply from 10.67.212.178: time=1ms TTL=63

[1]Reply from 10.67.212.178: time=0ms TTL=63

[2]Reply from 10.67.212.178: time=1ms TTL=63

[3]Reply from 10.67.212.178: time=1ms TTL=63

\--- 10.67.212.178 ping statistics ---

4 packets transmitted, 4 received, 0 loss

客户端NFS初始化,运行命令:

OHOS \# mkdir /nfs

OHOS \# mount 10.67.212.178:/home/sqbin/nfs /nfs nfs 1011 1000

将从串口得到如下回应信息,表明初始化NFS客户端成功。

OHOS \# mount 10.67.212.178:/home/sqbin/nfs /nfs nfs 1011 1000

Mount nfs on 10.67.212.178:/home/sqbin/nfs, uid:1011, gid:1000

Mount nfs finished.

该命令将服务器10.67.212.178上的/home/sqbin/nfs目录挂载到OpenHarmony内核设备上的/nfs上。

利用NFS共享文件

在NFS服务器下新建目录dir,并保存。在OpenHarmony内核下运行ls命令:

OHOS \# ls /nfs

则可从串口得到如下回应:

OHOS \# ls /nfs

Directory /nfs:

drwxr-xr-x 0 u:0 g:0 dir

可见,刚刚在NFS服务器上新建的dir目录已同步到客户端(OpenHarmony内核系统)的/nfs目录,两者保持同步。

同样地,在客户端(OpenHarmony内核系统)上创建文件和目录,在NFS服务器上也可以访问,读者可自行体验。

注:目前,NFS客户端仅支持NFS v3部分规范要求,因此对于规范支持不全的服务器,无法完全兼容。在开发测试过程中,建议使用Linux的NFS server,其对NFS支持很完善。

4.5.2.4 Ramfs

1)基本概念

RAMFS是一个可动态调整大小的基于RAM的文件系统。RAMFS没有后备存储源。向RAMFS中进行的文件写操作也会分配目录项和页缓存,但是数据并不写回到任何其他存储介质上,掉电后数据丢失。

2)运行机制

RAMFS文件系统把所有的文件都放在 RAM 中,所以读/写操作发生在RAM中,可以用RAMFS来存储一些临时性或经常要修改的数据,例如/tmp和/var目录,这样既避免了对存储器的读写损耗,也提高了数据读写速度。

挂载:

mount(NULL, "/dev/shm", "ramfs", 0, NULL)

创建目录:

mkdir(pathname, mode)

创建文件:

open(pathname, O_NONBLOCK \| O_CREAT \| O_RDWR, mode)

读取目录:

dir = opendir(pathname)

ptr = readdir(dir)

closedir(dir)

删除文件:

unlink(pathname)

删除目录:

rmdir(pathname)

去挂载:

umount("/dev/shm")

注:RAMFS只能挂载一次,一次挂载成功后,后面不能继续挂载到其他目录。RAMFS属于调测功能,默认配置为关闭,正式产品中不要使用该功能。

4.5.2.5 Procfs

1)基本概念

procfs是进程文件系统的简称,是一种虚拟文件系统,他用文件的形式,展示进程或其他系统信息。相比调用接口的方式获取信息,以文件操作的方式获取系统信息更为方便。

2)运行机制

OpenHarmony内核中,procfs在开机时会自动挂载到/proc目录下,仅支持内核模块创建文件节点来提供查询服务。

procfs文件的创建无法使用一般的文件系统接口,需要使用ProcMkdir接口创建目录,使用CreateProcEntry接口创建文件。文件节点功能的开发就是实现read和write函数的钩子挂到CreateProcEntry创建的文件中。当用户使用读写procfs的文件时,就会调用到钩子函数来实现自定义的功能。

3)编程实例

下面我们以创建/proc/hello/world文件为例,实现如下功能:

1.在/proc/hello/world位置创建一个文件

2.当读文件内容时,返回"HelloWorld!"

3.当写文件内容时,打印写入的内容

\#include "proc_fs.h"

static int TestRead(struct SeqBuf \*buf, void \*arg)

{

LosBufPrintf(buf, "Hello World!\\n"); /\* 将数据打印到buffer中,这个buffer中的数据会返回到read的结果中 \*/

return 0;

}

static int TestWrite(struct ProcFile \*pf, const char \*buffer, size_t buflen, loff_t \*ppos)

{

if ((buffer == NULL) \|\| (buflen \<= 0)) {

return -EINVAL;

}

PRINTK("your input is: %s\\n", buffer); /\* 注意和上面的read接口区别,这是对write接口输入命令的反馈,这个打印只会打印到控制台 \*/

return buflen;

}

static const struct ProcFileOperations HELLO_WORLD_OPS = {

.read = TestRead,

.write = TestWrite,

};

void HelloWorldInit(void)

{

/\* 创建hello目录 \*/

struct ProcDirEntry \*dir = ProcMkdir("hello", NULL);

if (dir == NULL) {

PRINT_ERR("create dir failed!\\n");

return;

}

/\* 创建world文件 \*/

struct ProcDirEntry \*entry = CreateProcEntry("world", 0, dir);

if (entry == NULL) {

PRINT_ERR("create entry failed!\\n");

return;

}

/\* 将自定义的read和write钩子挂到文件中 \*/

entry-\>procFileOps = \&HELLO_WORLD_OPS;

}

结果验证

启动后在shell输入如下命令

OHOS \# cat /proc/hello/world

OHOS \# Hello World!

OHOS \# echo "yo" \> /proc/hello/world

OHOS \# your input is: yo
4.5.3 适配新的文件系统

所谓对接VFS层,其实就是指实现VFS层定义的若干接口函数,可根据文件系统的特点和需要适配其中部分接口。一般情况下,支持文件读写,最小的文件系统适配看起来是这样的:

struct MountOps g_yourFsMountOps = {

.Mount = YourMountMethod,

};

struct file_operations_vfs g_yourFsFileOps = {

.read = YourReadMethod,

.write = YourWriteMethod,

}

struct VnodeOps g_yourFsVnodeOps = {

.Create = YourCreateMethod;

.Lookup = YourLookupMethod;

.Reclaim = YourReclaimMethod;

};

FSMAP_ENTRY(yourfs_fsmap, "your fs name", g_yourFsMountOps, TRUE, TRUE); // 注册文件系统

注:

open和close接口不是必须要实现的接口,因为这两个接口是对文件的操作,对下层的文件系统一般是不感知的,只有当要适配的文件系统需要在open和close时做一些特别的操作时,才需要实现。

适配文件系统,对基础知识的要求较高,适配者需要对要适配的文件系统的原理和实现具有深刻的理解,本节中不会事无巨细地介绍相关的基础知识,如果您在适配的过程中遇到疑问,建议参考kernel/liteos_a/fs目录下已经适配好的文件系统的代码,可能就会豁然开朗。

  1. 适配Mount接口

Mount是文件系统第一个被调用的接口,该接口一般会读取驱动的参数,根据配置对文件系统的进行初始化,最后生成文件系统的root节点。Mount接口的定义如下:

int (\*Mount)(struct Mount \*mount, struct Vnode \*blkDriver, const void \*data);

其中,第一个参数struct Mount *mount是Mount点的信息,适配时需要填写的是下面的变量:

struct Mount {

const struct MountOps \*ops; /\* Mount相关的函数钩子 \*/

struct Vnode \*vnodeCovered; /\* Mount之后的文件系统root节点 \*/

void \*data; /\* Mount点的私有数据 \*/

};

第二个参数struct Vnode *blkDriver是驱动节点,可以通过这个节点访问驱动。

第三个参数const void *data是mount命令传入的数据,可以根据文件系统的需要处理。

下面以JFFS2为例,详细看一下mount接口是如何适配的:

int VfsJffs2Bind(struct Mount \*mnt, struct Vnode \*blkDriver, const void \*data)

{

int ret;

int partNo;

mtd_partition \*p = NULL;

struct MtdDev \*mtd = NULL;

struct Vnode \*pv = NULL;

struct jffs2_inode \*rootNode = NULL;

LOS_MuxLock(&g_jffs2FsLock, (uint32_t)JFFS2_WAITING_FOREVER);

/\* 首先是从驱动节点中获取文件系统需要的信息,例如jffs2读取的是分区的编号 \*/

p = (mtd_partition \*)((struct drv_data \*)blkDriver-\>data)-\>priv;

mtd = (struct MtdDev \*)(p-\>mtd_info);

if (mtd == NULL \|\| mtd-\>type != MTD_NORFLASH) {

LOS_MuxUnlock(&g_jffs2FsLock);

return -EINVAL;

}

partNo = p-\>patitionnum;

/\* 然后生成一个文件系统的根Vnode,这里注意不要搞混rootNode和根Vnode,rootNode类型是inode,是jffs2内部维护的私有数据,而Vnode是VFS的概念,是通用的文件节点,

这一步实际上就是把文件系统内部的私有信息保存到Vnode中,这样就可以通过Vnode直接找到文件系统中的对应文件。

\*/

ret = jffs2_mount(partNo, \&rootNode);

if (ret != 0) {

LOS_MuxUnlock(&g_jffs2FsLock);

return ret;

}

ret = VnodeAlloc(&g_jffs2Vops, \&pv);

if (ret != 0) {

LOS_MuxUnlock(&g_jffs2FsLock);

goto ERROR_WITH_VNODE;

}

/\* 下面这段填写的是关于这个Vnode对应文件的相关信息,uid\\gid\\mode这部分信息,有的文件系统可能不支持,可以不填 \*/

pv-\>type = VNODE_TYPE_DIR;

pv-\>data = (void \*)rootNode;

pv-\>originMount = mnt;

pv-\>fop = \&g_jffs2Fops;

mnt-\>data = p;

mnt-\>vnodeCovered = pv;

pv-\>uid = rootNode-\>i_uid;

pv-\>gid = rootNode-\>i_gid;

pv-\>mode = rootNode-\>i_mode;

/\* 这里的HashInsert是为了防止重复生成已经生成过的Vnode, 第二个参数一般会选择本文件系统内可以唯一确定某一个文件的信息,例如这里是jffs2内部inode的地址 \*/

(void)VfsHashInsert(pv, rootNode-\>i_ino);

g_jffs2PartList[partNo] = blkDriver;

LOS_MuxUnlock(&g_jffs2FsLock);

return 0;

ERROR_WITH_VNODE:

return ret;

}

...

...

const struct MountOps jffs_operations = {

.Mount = VfsJffs2Bind,

...

...

};

总结:

首先从驱动节点中获取需要的私有信息。

根据私有信息,生成文件系统的根节点。

  1. 适配Lookup接口

Lookup是查找文件的接口,它的函数原型是:

int (*Lookup)(struct Vnode *parent, const char *name, int len, struct Vnode **vnode);

很好理解,就是从父节点parent开始,根据文件名name和文件名长度len,查找到对应的vnode返回给上层。

这个接口适配起来思路很清晰,给了父节点的信息和文件名,实现从父目录中查询名字为name的文件这个功能,同样以JFFS2为例:

int VfsJffs2Lookup(struct Vnode \*parentVnode, const char \*path, int len, struct Vnode \*\*ppVnode)

{

int ret;

struct Vnode \*newVnode = NULL;

struct jffs2_inode \*node = NULL;

struct jffs2_inode \*parentNode = NULL;

LOS_MuxLock(&g_jffs2FsLock, (uint32_t)JFFS2_WAITING_FOREVER);

/\* 首先从private data中提取父节点的信息 \*/

parentNode = (struct jffs2_inode \*)parentVnode-\>data;

/\* 然后查询得到目标节点的信息,注意这里调用的jffs2_lookup是jffs2本身的查询函数 \*/

node = jffs2_lookup(parentNode, (const unsigned char \*)path, len);

if (!node) {

LOS_MuxUnlock(&g_jffs2FsLock);

return -ENOENT;

}

/\* 接着先校验一下查找到的目标是否已经有现成的vnode了,这里对应之前提到的VfsHashInsert \*/

(void)VfsHashGet(parentVnode-\>originMount, node-\>i_ino, \&newVnode, NULL, NULL);

LOS_MuxUnlock(&g_jffs2FsLock);

if (newVnode) {

newVnode-\>parent = parentVnode;

\*ppVnode = newVnode;

return 0;

}

/\* 如果vnode不存在,就新生成一个vnode,并填写相关信息 \*/

ret = VnodeAlloc(&g_jffs2Vops, \&newVnode);

if (ret != 0) {

PRINT_ERR("%s-%d, ret: %x\\n", \__FUNCTION__, \__LINE__, ret);

(void)jffs2_iput(node);

LOS_MuxUnlock(&g_jffs2FsLock);

return ret;

}

Jffs2SetVtype(node, newVnode);

newVnode-\>fop = parentVnode-\>fop;

newVnode-\>data = node;

newVnode-\>parent = parentVnode;

newVnode-\>originMount = parentVnode-\>originMount;

newVnode-\>uid = node-\>i_uid;

newVnode-\>gid = node-\>i_gid;

newVnode-\>mode = node-\>i_mode;

/\* 同时不要忘记将新生成的vnode插入hashtable中 \*/

(void)VfsHashInsert(newVnode, node-\>i_ino);

\*ppVnode = newVnode;

LOS_MuxUnlock(&g_jffs2FsLock);

return 0;

}

总结:

从父节点获取私有数据;

根据私有信息查询到目标文件的私有数据;

通过目标文件的私有数据生成目标Vnode。

注意事项:

通过上面两个接口的适配,其实可以发现一个规律,不管是什么接口,基本都遵循下面的适配步骤:

通过入参的vnode获取文件系统所需的私有数据。

使用私有数据完成接口的功能。

将结果包装成vnode或接口要求的其他返回格式,返回给上层。

核心的逻辑其实在使用私有数据完成接口的功能,这些接口都是些文件系统的通用功能,文件系统在移植前本身应该都有相应实现,所以关键是归纳总结出文件系统所需的私有数据是什么,将其存储在vnode中,供之后使用。一般情况下,私有数据的内容是可以唯一定位到文件在存储介质上位置的信息,大部分文件系统本身都会有类似数据结构可以直接使用,比如JFFS2的inode数据结构。

访问文件时,不一定会调用文件系统中的Lookup接口,仅在上层的路径缓存失效时才会调用到。

通过VfsHashGet找到了已经存在的Vnode,不要直接将其作为结果返回,其储存的信息可能已经失效,请更新相应字段后再返回。

Vnode会根据内存占用在后台自动释放,需要持久保存的信息,不要只保存在Vnode中。

Reclaim接口在Vnode释放时会自动调用,请在这个接口中释放私有数据中的资源。

5 调试与工具

5.1 Shell

5.1.1 Shell介绍

OpenHarmony内核提供的Shell支持调试常用的基本功能,包含系统、文件、网络和动态加载相关命令。同时OpenHarmony内核的Shell支持添加新的命令,可以根据需求来进行定制。

系统相关命令:提供查询系统任务、内核信号量、系统软件定时器、CPU占用率、当前中断等相关信息的能力。

文件相关命令:支持基本的ls、cd等功能。

网络相关命令:支持查询接到开发板的其他设备的IP、查询本机IP、测试网络连接、设置开发板的AP和station模式等相关功能。

新增命令的详细流程可参见Shell命令开发指导和Shell命令编程实例。

注:

在使用Shell功能的过程中,需要注意以下几点:

Shell功能支持使用exec命令来运行可执行文件。

Shell功能支持默认模式下英文输入。如果出现用户在UTF-8格式下输入了中文字符的情况,只能通过回退三次来删除。

Shell功能支持shell命令、文件名及目录名的Tab键联想补全。若有多个匹配项,则根据共同字符, 打印多个匹配项。对于过多的匹配项(打印多于24行),将会进行打印询问(Display all num possibilities?(y/n)),用户可输入y选择全部打印,或输入n退出打印,选择全部打印并打印超过24行后,会进行–More–提示,此时按回车键继续打印,按q键退出(支持Ctrl+c退出)。

Shell端工作目录与系统工作目录是分开的,即通过Shell端cd pwd等命令是对Shell端工作目录进行操作,通过chdir getcwd等命令是对系统工作目录进行操作,两个工作目录相互之间没有联系。当文件系统操作命令入参是相对路径时要格外注意。

在使用网络Shell指令前,需要先调用tcpip_init函数完成网络初始化并完成telnet连接后才能起作用,内核默认不初始化tcpip_init。

不建议使用Shell命令对/dev目录下的设备文件进行操作,这可能会引起不可预知的结果。

Shell功能不符合POSIX标准,仅供调试使用。

Shell功能仅供调试使用,在Debug版本中开启(使用时通过menuconfig在配置项中开启"LOSCFG_DEBUG_VERSION"编译开关进行相关控制),商用产品中禁止包含该功能。

5.1.2 Shell命令开发

新增Shell命令的典型开发流程如下:

1包含如下头文件:

\#include "shell.h"

\#include "shcmd.h"

2注册命令。用户可以选择静态注册命令方式和系统运行时动态注册命令方式,静态注册命令方式一般用在系统常用命令注册,动态注册命令方式一般用在用户命令注册。

静态注册命令方式:

1)通过宏的方式注册。

这个宏的原型为:

SHELLCMD_ENTRY(l, cmdType, cmdKey, paraNum, cmdHook)
表 26 SHELLCMD_ENTRY参数详解
参数描述
l静态注册全局变量名(注意:不与系统中其他symbol重名)。
cmdType命令类型: CMD_TYPE_EX:不支持标准命令参数输入,会把用户填写的命令关键字屏蔽掉,例如:输入ls /ramfs,传入给注册函数的参数只有/ramfs,而ls命令关键字并不会被传入。 CMD_TYPE_STD:支持的标准命令参数输入,所有输入的字符都会通过命令解析后被传入。
cmdKey命令关键字,函数在Shell中访问的名称。
paraNum调用的执行函数的入参最大个数,暂不支持。
cmdHook命令执行函数地址,即命令实际执行函数。

如:

SHELLCMD_ENTRY(ls_shellcmd, CMD_TYPE_EX, "ls", XARGS, (CMD_CBK_FUNC)osShellCmdLs)

2)在build/mk/liteos_tables_ldflags.mk中添加相应选项:

如:上述“ls”命令注册时,需在build/mk/liteos_tables_ldflags.mk中添加“-uls_shellcmd”。其中-u后面跟SHELLCMD_ENTRY的第一个参数。

2动态注册命令方式:

注册函数原型:

UINT32 osCmdReg(CmdT ype cmdType, CHAR \*cmdKey, UINT32 paraNum, CmdCallBackFunc cmdProc)
表 27 UINT32 osCmdReg参数详解
参数描述
cmdType命令类型: CMD_TYPE_EX:不支持标准命令参数输入,会把用户填写的命令关键字屏蔽掉,例如:输入ls /ramfs,传入给注册函数的参数只有/ramfs,而ls命令关键字并不会被传入。 CMD_TYPE_STD:支持的标准命令参数输入,所有输入的字符都会通过命令解析后被传入。
cmdKey命令关键字,函数在Shell中访问的名称。
paraNum调用的执行函数的入参最大个数,暂不支持该参数;当前为默认值XARGS(0xFFFFFFFF)。
cmdHook命令执行函数地址,即命令实际执行函数。

如:

osCmdReg(CMD_TYPE_EX, "ls", XARGS, (CMD_CBK_FUNC)osShellCmdLs)

3 添加内置命令函数原型。

UINT32 osShellCmdLs(UINT32 argc, CHAR \*\*argv)
表 28 osShellCmdLs参数说明
参数参数描述
argcShell命令中,参数个数。
argv为指针数组,每个元素指向一个字符串,可以根据选择命令类型,决定是否要把命令关键字传入给注册函数。

4输入Shell命令,有两种输入方式:

1)在串口工具中直接输入Shell命令。

2)在telnet工具中输入Shell命令(telnet使用方式详见telnet)。

5.1.3 Shell命令编程实例

实例描述:在下面的两个例子中,演示如何使用静态注册命令方式和动态注册命令方式新增一个Shell命令:test。

1静态注册方式

开发流程如下:

定义一个新增命令所要调用的执行函数cmd_test。

使用SHELLCMD_ENTRY函数添加新增命令项。

在链接选项liteos_tables_ldflags.mk中添加链接该新增命令项参数。

重新编译代码后运行。

静态注册编程实例

定义命令所要调用的函数cmd_test:

\#include "shell.h"

\#include "shcmd.h"

int cmd_test(void)

{

printf("hello everybody!\\n");

return 0;

}

新增命令项:

SHELLCMD_ENTRY(test_shellcmd, CMD_TYPE_EX, "test", 0, (CMD_CBK_FUNC)cmd_test);

在链接选项中添加链接该新增命令项参数:

在liteos_tables_ldflags.mk文件的LITEOS_TABLES_LDFLAGS项下添加-utest_shellcmd。

重新编译代码:

make clean;make

用help命令查看当前系统所有的注册命令,可以发现test命令已经注册。(以下命令集合仅供参考,以实际编译运行情况为准。)

OHOS \# help

\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*shell commands:\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*

arp cat cd chgrp chmod chown cp cpup

date dhclient dmesg dns format free help hwi

ifconfig ipdebug kill log ls lsfd memcheck mkdir

mount netstat oom partinfo partition ping ping6 pwd

reset rm rmdir sem statfs su swtmr sync

systeminfo task telnet test tftp touch umount uname

watch writeproc

2动态注册方式开发流程如下:

1)使用osCmdReg函数添加新增命令项。

2)重新编译后运行。

动态注册编程实例

在用户应用函数中调用osCmdReg函数动态注册命令。

\#include "shell.h"

\#include "shcmd.h"

int cmd_test(void)

{

printf("hello everybody!\\n");

return 0;

}

void app_init(void)

{

....

....

osCmdReg(CMD_TYPE_EX, "test", 0,(CMD_CBK_FUNC)cmd_test);

....

}

重新编译代码:

make clean;make

用help命令查看当前系统所有的注册命令,可以发现test命令已经注册。

OHOS \# help

\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*shell commands:\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*

arp cat cd chgrp chmod chown cp cpup

date dhclient dmesg dns format free help hwi

ifconfig ipdebug kill log ls lsfd memcheck mkdir

mount netstat oom partinfo partition ping ping6 pwd

reset rm rmdir sem statfs su swtmr sync

systeminfo task telnet test tftp touch umount uname

watch writeproc
5.1.4 Shell命令使用详解
5.1.4.1系统命令
1)cpup

1 命令功能

cpup命令用于查询系统CPU的占用率。

2 命令格式

cpup [mode] [taskID]

3 参数说明

表 29 参数说明
参数参数说明取值范围
Mode缺省:显示系统最近10s内的CPU占用率。 0:显示系统最近10s内的CPU占用率。 1:显示系统最近1s内的CPU占用率。 其他数字:显示系统启动至今总的CPU 占用率。[0,0xFFFFFFFF]
taskID任务ID号[0,0xFFFFFFFF]

4 使用指南

参数缺省时,显示系统10s前的CPU占用率。

只有一个参数时,该参数为mode,显示系统相应时间前的CPU占用率。

输入两个参数时,第一个参数为mode,第二个参数为taskID,显示对应ID号任务的相应时间前的CPU占用率。

5 使用说明

举例:输入cpup 1 5

6输出说明

在这里插入图片描述

图 30 指令输出结果
2)date

1 命令功能

date命令用于查询及设置系统日期和时间。

2 命令格式

date

date --help

date +[Format]

date -s\_ \_[YY/MM/DD]

date\_ -s [hh:mm:ss]\_

date -r [Filename]

3 参数说明

表 30 参数说明
参数参数说明取值范围
–help使用帮助。N/A
+Format根据Format格式打印日期和时间。–help中列出的占位符。
-s YY/MM/DD设置系统时间,用“/”分割的年月日。>= 1970/01/01
-s hh:mm:ss设置系统时间,用“:”分割的时分秒。N/A
-r Filename查询Filename文件的修改时间。N/A

4 使用指南

date参数缺省时,默认显示当前系统日期和时间。

–help、+Format、-s、-r不能混合使用。

5 使用说明

举例:

输入date +%Y--%m--%d

6输出说明

在这里插入图片描述

图 31 按指定格式打印系统日期
3) dmesg

1 命令功能

dmesg命令用于控制内核dmesg缓存区。

2 命令格式

dmesg

dmesg [-c/-C/-D/-E/-L/-U]

dmesg -s [size]

dmesg -l [level]

dmesg \> [fileA]

3 参数说明

表 31 参数说明
参数参数说明取值范围
-c打印缓存区内容并清空缓存区。N/A
-C清空缓存区。N/A
-D/-E关闭/开启控制台打印。N/A
-L/-U关闭/开启串口打印。N/A
-s size设置缓存区大小 size是要设置的大小。N/A
-l level设置缓存等级。0 - 5
> fileA将缓存区内容写入文件。N/A

4 使用指南

该命令依赖于LOSCFG_SHELL_DMESG,使用时通过menuconfig在配置项中开启"Enable Shell dmesg":

Debug ---\> Enable a Debug Version ---\> Enable Shell ---\> Enable Shell dmesg

dmesg参数缺省时,默认打印缓存区内容。

各“ - ”选项不能混合使用。

写入文件需确保已挂载文件系统。

关闭串口打印会影响shell使用,建议先连接telnet再尝试关闭串口。

5 使用说明

举例:

输入dmesg \> /usr/dmesg.log

6输出说明

在这里插入图片描述

图 32 dmesg重定向到文件。
4)exec

1 命令功能

exec命令属于shell内置命令,目前实现最基础的执行用户态程序的功能。

2 命令格式

exec \<executable-file\>

3 参数说明

表 32 参数说明
参数参数说明取值范围
executable-file有效的可执行文件。N/A

4 使用指南

该命令当前仅支持执行有效的二进制程序,程序成功执行,默认后台运行,但与Shell共用终端,可能会导致程序打印输出与Shell输出交错显示。

5 使用说明

举例:

输入exec helloworld

6输出说明

OHOS \# exec helloworld

OHOS \# hello world!

注:可执行文件执行后,先打印“OHOS #”提示符原因:目前Shell “exec”命令执行均为后台执行,结果可能导致提示符提前打印。

5) free

1 命令功能

free命令可显示系统内存的使用情况,同时显示系统的text段、data段、rodata段、bss段大小。

2 命令格式

free [-k \| -m]

3 参数说明

表 33 参数说明
参数参数说明取值范围
无参数以Byte为单位显示。N/A
-k以KiB为单位显示。N/A
-m以MiB为单位显示。N/A

4使用说明

举例:分别输入free、free -k、free -m.

5输出说明

在这里插入图片描述

图 33 以三种方式显示内存使用情况

表 34 输出说明
输出说明
total表示系统动态内存池总量。
Used表示已使用内存总量。
Free表示未被分配的内存大小。
heap表示已分配堆大小。
Text表示代码段大小。
Data表示数据段大小。
rodata表示只读数据段大小。
Bss表示未初始化全局变量占用内存大小。
6)help

1 命令功能

help命令用于显示当前操作系统内所有操作指令。

2 命令格式

help

3查看系统内所有操作指令。

OHOS \# help

\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*shell commands:\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*

arp cat cd chgrp chmod chown cp cpup

date dhclient dmesg dns format free help hwi

ifconfig ipdebug kill log ls lsfd memcheck mkdir

mount netstat oom partinfo partition ping ping6 pwd

reset rm rmdir sem statfs su swtmr sync

systeminfo task telnet tftp touch umount uname watch

writeproc
7)hwi

1命令格式

hwi命令查询当前中断信息

2 命令格式

Hwi

3 使用指南

输入hwi即显示当前中断号、中断次数及注册中断名称。

若开关LOSCFG_CPUP_INCLUDE_IRQ打开,则还会显示各个中断的处理时间(cycles)、CPU占用率以及中断类型。

4 实用实例

1)显示中断信息(LOSCFG_CPUP_INCLUDE_IRQ关闭)

在这里插入图片描述

图 34 显示中断信息(LOSCFG_CPUP_INCLUDE_IRQ关闭)

2显示中断信息(LOSCFG_CPUP_INCLUDE_IRQ打开)

在这里插入图片描述

图 35 显示中断信息(LOSCFG_CPUP_INCLUDE_IRQ打开)
表35 输出说明
输出说明
InterruptNo中断号。
Count中断次数。
Name注册中断名称。
CYCLECOST中断的处理时间(cycles)。
CPUUSECPU占用率。
CPUUSE10s最近10s CPU占用率。
CPUUSE1s最近1s CPU占用率。
mode中断类型: normal: 非共享中断。 shared: 共享中断。
8)kill

1命令功能

命令用于发送特定信号给指定进程。

2命令格式

kill [signo \| -signo] [pid]

3参数说明

表 36 参数说明
参数参数说明取值范围
signo信号ID。[1,30]
Pid进程ID。[1,MAX_INT]

注; signo有效范围为[0,64],建议取值范围为[1,30],其余为保留内容。

4 使用指南

必须指定发送的信号编号及进程号。

进程编号取值范围根据系统配置变化,例如系统最大支持pid为256,则取值范围缩小为[1-256]。

5 使用实例

  1. 查看当前进程列表,查看需要杀死的进程PID(7)

在这里插入图片描述

图 36 查看进程PID

2)发送信号14(SIGALRM默认行为为进程终止)给7号进程helloworld_d(用户态进程):kill 14 7(kill -14 7效果相同),并查看当前进程列表,7号进程已终止。

在这里插入图片描述

图 37 信号发送结果图

6输出说明

发送成功或失败输出结果如下。

在这里插入图片描述

图 38 发送信号给指定进程

信号发送会显示发送记录,未报错表示信号发送成功。

在这里插入图片描述

图 39 信号发送失败

信号发送失败,上图所示原因为信号发送命令参数无效,请排查信号编号及进程编号是否无效。

9)log

1 命令功能

log命令用于修改&查询日志配置。

2命令格式

log level [levelNum]

3参数说明

表 37 参数说明表
参数参数说明取值范围
levelNum配置日志打印等级。[0x0,0x5]

4使用指南

该命令依赖于LOSCFG_SHELL_LK,使用时通过menuconfig在配置项中开启"Enable Shell lk":

Debug ---\> Enable a Debug Version ---\> Enable Shell ---\> Enable Shell lK。

log level命令用于配置日志的打印等级,包括6个等级

TRACE_EMG = 0,

TRACE_COMMON = 1,

TRACE_ERROR = 2,

TRACE_WARN = 3,

TRACE_INFO = 4,

TRACE_DEBUG = 5

若level不在有效范围内,会打印提示信息。

若log level命令不加[levelNum]参数,则默认查看当前打印等级,并且提示使用方法。

5 使用实例

举例:

输入log level 4

6 输出说明

在这里插入图片描述

图 40 输出说明
10)memcheck

1命令功能

检查动态申请的内存块是否完整,是否存在内存越界造成节点损坏。

2命令格式

Memcheck

3使用指南

当内存池所有节点完整时,输出"system memcheck over, all passed!"。

当内存池存在节点不完整时,输出被损坏节点的内存块信息。

4使用实例

举例:输入memcheck

5输出说明

在这里插入图片描述

图 41 当前没有内存越界

在这里插入图片描述

图 42 出现内存越界
11)oom

1命令功能

查看和设置低内存阈值以及pagecache内存回收阈值。

2命令格式

oom

oom -i [interval]

oom -m [mem byte]

oom -r [mem byte]

oom -h \| --help

3参数说明

表 38 参数说明
参数参数说明取值范围
-i [interval]设置oom线程任务检查的时间间隔。100ms ~ 10000ms
-m [mem byte]设置低内存阈值。0MB ~ 1MB,0MB表示不做低内存阈值检查。
-r [mem byte]设置pagecache内存回收阈值。低内存阈值 ~ 系统可用最大内存。
-h | --help使用帮助。N/A

4使用指南

参数缺省时,显示oom功能当前配置信息。

说明: 当系统内存不足时,会打印出内存不足的提示信息。

5输出说明

在这里插入图片描述

图43 输出说明
表39 输出说明表
输出说明
[oom] OS is in low memory state total physical memory: 0x1bcf000(byte), used: 0x1b50000(byte), free: 0x7f000(byte), low memory threshold: 0x80000(byte)操作系统处于低内存状态。 整个系统可用物理内存为0x1bcf000 byte,已经使用了 0x1b50000 byte, 还剩0x7f000 byte,当前设置的低内存阈值为0x80000 byte。
[oom] candidate victim process init pid: 1, actual phy mem byte: 82602打印当前各个进程的内存使用情况,init进程实际使用82602byte,其中共享内存按照比例算的。
[oom] candidate victim process UserProcess12 pid: 12, actual phy mem byte: 25951558UserProcess12进程实际使用25951558byte内存。
[oom] max phy mem used process UserProcess12 pid: 12, actual phy mem: 25951558当前使用内存最多的进程是UserProcess12。
excFrom: User!当系统处于低内存的情况下,UserProcess12进程再去申请内存时失败退出。
12)pmm

1命令功能

查看系统内存物理页及pagecache物理页使用情况。

2命令格式

pmm

3输出说明

在这里插入图片描述

图44 输出说明
表40 输出说明表
输出说明
phys_seg物理页控制块地址信息
base第一个物理页地址,即物理页内存起始地址
size物理页内存大小
free_pages空闲物理页数量
active anonpagecache中,活跃的匿名页数量
inactive anonpagecache中,不活跃的匿名页数量
active filepagecache中,活跃的文件页数量
inactive filepagecache中,不活跃的文件页数量
pmm pagestotal:总的物理页数,used:已使用的物理页数,free:空闲的物理页数
13)reset

1命令功能

reset命令用于重启设备。

14)sem

1命令功能

sem命令用于查询系统内核信号量相关信息。

2命令格式

sem [ID\_ / fulldata_]

3命令说明

表 41 命令说明表
参数参数说明取值范围
ID信号ID号。[0, 0xFFFFFFFF]
fulldata查询所有在用的信号量信息,打印信息包括如下:SemID, Count, Original Count, Creater TaskEntry, Last Access Time。N/A

4使用指南

参数缺省时,显示所有的信号量的使用数及信号量总数。

sem后加ID,显示对应ID信号量的使用数。

参数fulldata依赖于LOSCFG_DEBUG_SEMAPHORE,使用时通过menuconfig在配置项中开启"Enable Semaphore Debugging":

Debug ---\> Enable a Debug Version ---\> Enable Debug LiteOS Kernel Resource ---\> Enable Semaphore Debugging

5 使用实例

举例1:输入 sem fulldata

6输出说明

在这里插入图片描述

图 45 查询所有在用的信号量信息
表 41输出说明表
输出说明
SemID信号量ID。
Count信号量使用数。

注:sem命令的ID参数输入形式以十进制形式表示或十六进制形式表示皆可。 ● sem命令的ID参数在[0, 1023]范围内时,返回对应ID的信号量的状态(如果对应ID的信号量未被使用则进行提示);其他取值时返回参数错误的提示。

15)stack

1命令功能

查看系统各堆栈使用情况。

2命令格式

stack

3输出说明

在这里插入图片描述

图 46 系统堆栈使用情况
表42输出说明表
输出说明
stack name系统堆栈名
cpu idcpu 号
stack addr栈地址
total size堆栈大小
used size堆栈实际使用大小
16)su

1命令功能

su用于变更为其他使用者的身份。

2命令格式

su [uid] [gid]

3参数说明

表 43 参数说明
参数参数说明取值范围
uid目标用户的用户id值。为空。 [0,60000]
gid目标用户的群组id值。为空。 [0,60000]

4使用指南

su命令缺省切换到root用户,uid默认为0,gid为0。

在su命令后的输入参数uid和gid就可以切换到该uid和gid的用户。

输入参数超出范围时,会打印提醒输入正确范围参数。

5使用实例

举例:su 1000 1000

6输出说明

在这里插入图片描述

图 47切换到为uid为1000,gid为1000的用户
17)swtmr

1命令功能

swtmr命令用于查询系统软件定时器相关信息。

2命令格式

swtmr [ID]

3参数说明

表 44 参数说明
参数参数说明取值范围
ID软件定时器ID号。[0,0xFFFFFFFF]

4使用指南

参数缺省时,默认显示所有软件定时器的相关信息。

swtmr后加ID号时,显示ID对应的软件定时器相关信息。

5 使用实例

举例:输入swtmr和swtmr 1

6输出说明

在这里插入图片描述

图 48 查询所有软件定时器相关信息

在这里插入图片描述

图 49 查询对应 ID 的软件定时器信息
表 45 参数说明
输出说明
SwTmrID软件定时器ID。
State软件定时器状态。 状态可能为:“UnUsed”, “Created”, “Ticking”。
Mode软件定时器模式。 模式可能为:“Once”, “Period”, “NSD(单次定时器,定时结束后不会自动删除)”。
Interval软件定时器使用的Tick数。
Count软件定时器已经工作的次数。
Arg传入的参数。
handlerAddr回调函数的地址。

注:

swtmr命令的ID参数输入形式以十进制形式表示或十六进制形式表示皆可。

swtmr命令的ID参数在[0, 当前软件定时器个数 - 1]范围内时,返回对应ID的软件定时器的状态;其他取值时返回错误提示。

18)systeminfo

1命令功能

systeminfo命令用于显示当前操作系统内资源使用情况,包括任务、信号量、互斥量、队列、定时器等。

2命令格式

Systeminfo

3输出说明

在这里插入图片描述

图 50 查看系统资源使用情况
表 46 输出说明
输出说明
Module模块名称。
Used当前使用量。
Total最大可用量。
Enabled模块是否开启。
Task任务。
Sem信号量。
Mutex互斥量。
Queue队列。
SwTmr定时器。
19)task

1命令功能

task命令用于查询进程及线程信息。

2命令格式

task/task -a

3参数说明

表47 参数说明
参数参数说明取值范围
-a查看更多信息。N/A

4使用指南

参数缺省时默认打印部分任务信息。

5使用实例

举例:输入task

6输出说明

在这里插入图片描述

图 51 查询任务部分信息
表 48输出说明表
输出说明
PID进程ID。
PPID父进程ID。
PGID进程组ID。
UID用户ID。
Status任务当前的状态。
CPUUSE10s10秒内CPU使用率。
PName进程名。
TID任务ID。
StackSize任务堆栈的大小。
WaterLine栈使用的峰值。
MEMUSE内存使用量。
TaskName任务名。
20)uname

1命令格式

uname命令用于显示当前操作系统的名称,版本创建时间,系统名称,版本信息等。

2命令格式

uname [-a \| -s \| -t \| -v \| --help]

3参数说明

表49 参数说明
参数参数说明
无参数默认显示操作系统名称。
-a显示全部信息。
-t显示版本创建的时间。
-s显示操作系统名称。
-v显示版本信息。
–help显示uname指令格式提示。

4使用实例

举例:输入uname -a

5输出说明

在这里插入图片描述

图 52 输出说明
21)vmm

1命令功能

查看进程的虚拟内存使用情况。

2命令格式

vmm [-a / -h / --help]

vmm [pid]

3参数说明

表50 参数说明
参数参数说明取值范围
-a输出所有进程的虚拟内存使用情况N/A
-h | --help命令格式说明N/A
pid进程ID,说明指定进程的虚拟内存使用情况[0,63]

4输出说明

输入vmm 3

在这里插入图片描述

图 53 PID为3的进程虚拟内存使用信息
表 51进程基本信息
输出说明
PID进程ID
aspace进程虚拟内存控制块地址信息
name进程名
base虚拟内存起始地址
size虚拟内存大小
pages已使用的物理页数量

表 52 虚拟内存区间信息

输出说明
region虚拟区间控制块地址信息
name虚拟区间类型
base虚拟区间起始地址
size虚拟区间大小
mmu_flags虚拟区间mmu映射属性
pages已使用的物理页数量(包括共享内存部分)
pg/ref已使用的物理页数量
22)watch

1命令功能

watch命令用于周期性的监视一个命令的运行结果。

2命令格式

watch

watch [-c/-n/-t/–count/–interval/-no-title/–over] [command]

3参数说明

表52 参数说明
参数参数说明缺省值取值范围
-c / --count命令执行的总次数。0xFFFFFF(0,0xFFFFFF]
-n / --interval命令周期性执行的时间间隔(s)。1s(0,0xFFFFFF]
-t / -no-title关闭顶端的时间显示。N/AN/A
command需要监测的命令。N/AN/A
–over关闭当前监测指令。N/AN/A

4使用指南

watch运行过程中可以执行watch --over结束本次watch命令。

5使用实例

输入举例:

watch -n 2 -c 6 task

6输出说明

在这里插入图片描述

图54 watch task 结果

注:示例中,总共有6次task命令打印,每次间隔2秒,截图为最后一次打印详情。

5.1.4.2文件命令
23)cat

1命令功能

cat用于显示文本文件的内容。

2命令格式

cat [pathname]

3参数说明

表53 参数说明
参数参数说明取值范围
pathname文件路径。已存在的文件。

4使用实例

举例:cat hello-harmony.txt

5输出说明

在这里插入图片描述

图 55 查看 hello-harmony.txt 文件的信息
24)cd

1命令功能

cd命令用来改变当前目录。

2使用指南

未指定目录参数时,会跳转至根目录。

cd后加路径名时,跳转至该路径。

路径名以 /(斜杠)开头时,表示根目录。

.表示当前目录。

…表示父目录。

25)chgrp

1命令功能

chgrp用于修改文件的群组。

2命令格式

chgrp [group] [pathname]

3参数说明

表54 参数说明
参数参数说明取值范围
group文件群组。[0,0xFFFFFFFF]
pathname文件路径。已存在的文件。

4使用指南

在需要修改的文件名前加上文件群组值就可以修改该文件的所属组。

5使用实例

举例:chgrp 100 hello-harmony.txt

6输出说明

在这里插入图片描述

图56 修改 hello-harmony.txt 文件的群组为100
26)chmod

1命令功能

chmod用于修改文件操作权限。

2命令格式

chmod [mode] [pathname]

3参数说明

表55 参数说明
参数参数说明取值范围
mode文件或文件夹权限,用8进制表示对应User、Group、及Other(拥有者、群组、其他组)的权限。[0,777]
pathname文件路径。已存在的文件。

4使用指南

在需要修改的文件名前加上文件权限值就可以修改该文件的权限值。

5使用实例

举例:chmod 666 hello-harmony.txt

5输出说明

在这里插入图片描述

图 57 修改 hello-harmony.txt 文件的权限为666
27)chown

1命令功能

chmod用于将指定文件的拥有者改为指定的用户或组。

2命令格式

chown [owner] [group] [pathname]

3参数说明

表 56 参数说明
参数参数说明取值范围
owner文件拥有者。[0,0xFFFFFFFF]
group文件群组。1、为空。 2、[0,0xFFFFFFFF]
pathname文件路径。已存在的文件。

4使用指南

在需要修改的文件名前加上文件拥有者和文件群组就可以分别修改该文件的拥有者和群组。

当owner或group值为-1时则表示对应的owner或group不修改。

group参数可以为空。

5使用实例

举例:chown 100 200 hello-harmony.txt

6输出说明

在这里插入图片描述

图 58 修改 hello-harmony.txt 文件的uid为100,gid为200
28)cp

1命令功能

拷贝文件,创建一份副本。

2命令格式

cp [SOURCEFILE] [DESTFILE]

3参数说明

表 57 参数说明
参数参数说明取值范围
SOURCEFILE源文件路径。目前只支持文件,不支持目录。
DESTFILE目的文件路径。支持目录以及文件。

4使用指南

同一路径下,源文件与目的文件不能重名。

源文件必须存在,且不为目录。

源文件路径支持“*”和“?”通配符,“*”代表任意多个字符,“?”代表任意单个字符。目的路径不支持通配符。当源路径可匹配多个文件时,目的路径必须为目录。

目的路径为目录时,该目录必须存在。此时目的文件以源文件命名。

目的路径为文件时,所在目录必须存在。此时拷贝文件的同时为副本重命名。

目前不支持多文件拷贝。参数大于2个时,只对前2个参数进行操作。

目的文件不存在时创建新文件,已存在则覆盖。

拷贝系统重要资源时,会对系统造成死机等重大未知影响,如用于拷贝/dev/uartdev-0 文件时,会产生系统卡死现象。

5使用实例

举例:cp hello-harmony.txt ./tmp/

6输出说明

图 59显示结果如下:

在这里插入图片描述

图 59 显示结果
29)format

1命令功能

format指令用于格式化磁盘。

2命令格式

format \<dev_inodename\> \<sectors\> \<option\> [label]

3参数说明

表 58 参数说明
参数参数说明
dev_inodename设备名。
sectors分配的单元内存或扇区大小,如果输入0表示参数为空。(取值必须为0或2的幂,fat32下最大值为128,取值0表示自动选择合适的簇大小,不同size的分区,可用的簇大小范围不同,错误的簇大小指定可能导致格式化失败)。
option格式化选项,用来选择文件系统的类型,有如下几种参数选择: 0x01:FMT_FAT 0x02:FMT_FAT32 0x07:FMT_ANY 0x08:FMT_ERASE (USB不支持该选项) 传入其他值皆为非法值,将由系统自动选择格式化方式。若格式化U盘时低格位为 1,会出现错误打印。
label该参数为可选参数,输入值应为字符串,用来指定卷标名。当输入字符串"null"时,则把之前设置的卷标名清空。

4使用指南

format指令用于格式化磁盘,设备名可以在dev目录下查找。format时必须安装存储卡。

format只能格式化U盘、sd和mmc卡,对Nand flash和Nor flash格式化不起作用。

sectors参数必须传入合法值,传入非法参数可能引发异常。

5使用实例

举例:输入

format /dev/mmcblk0 128 2

6输出说明

结果如下

在这里插入图片描述

图 60 输出说明图
30)ls

1命令功能

ls命令用来显示当前目录的内容。

2命令格式

ls [path]

3参数说明

表59 参数说明
参数参数说明取值范围
pathpath为空时,显示当前目录的内容。 path为无效文件名时,显示失败,提示: ls error: No such directory。 path为有效目录路径时,会显示对应目录下的内容。1.为空。 2.有效的目录路径。

4使用指南

ls命令显示当前目录的内容。

ls可以显示文件的大小。

proc下ls无法统计文件大小,显示为0。

5使用实例

举例:输入ls

6输出说明

图 1 查看当前系统路径下的目录,显示的内容如下

在这里插入图片描述

图 61 查看当前系统路径下的目录
31)lsfd

1命令功能

lsfd命令用来显示当前已经打开的文件描述符及对应的文件名。

2命令格式

lsfd

3使用指南

lsfd命令显示当前已经打开文件的fd号以及文件的名字。

4使用实例

举例:输入lsfd

5输出说明

在这里插入图片描述

图 62 lsfd输出说明
32)mkdir

1命令功能

mkdir命令用来创建一个目录。

2命令格式

mkdir [directory]

3使用指南

mkdir后加所需要创建的目录名会在当前目录下创建目录。

mkdir后加路径,再加上需要创建的目录名,即在指定目录下创建目录。

33)mount

1命令功能

mount命令用来将设备挂载到指定目录。

2命令格式

mount \<device\> \<path\> \<name\> [uid gid]

3参数说明

表60 参数说明
参数参数说明取值范围
device要挂载的设备(格式为设备所在路径)。系统拥有的设备。
path指定目录。 用户必须具有指定目录中的执行(搜索)许可权。N/A
name文件系统的种类。vfat, yaffs, jffs, ramfs, nfs,procfs, romfs.
uid giduid是指用户ID。 gid是指组ID。 可选参数,缺省值uid:0,gid:0。N/A

4使用指南

mount后加需要挂载的设备信息、指定目录以及设备文件格式,就能成功挂载文件系统到指定目录。

5使用实例

举例:mount /dev/mmcblk0p0 /bin1/vs/sd vfat

6输出说明

将/dev/mmcblk0p0 挂载到/bin1/vs/sd目录

在这里插入图片描述

图 63 将/dev/mmcblk0p0 挂载到/bin1/vs/sd目录
34)partinfo

1命令功能

partinfo命令用于查看系统识别的硬盘,SD卡多分区信息。

2命令格式

partinfo \<dev_inodename\>

3参数说明

表61 参数说明
参数参数说明取值范围
dev_inodename要查看的分区名字。合法的分区名。
35)partition

1命令功能

partition命令用来查看flash分区信息。

2命令格式

partition [nand / spinor]

3参数说明

表 62 参数说明
参数参数说明取值范围
nand显示nand flash分区信息。N/A
spinor显示spinor flash分区信息。N/A

4使用指南

partition命令用来查看flash分区信息。

仅当使能yaffs文件系统时才可以查看nand flash分区信息,使能jffs或romfs文件系统时可以查看spinor flash分区信息。

5使用实例

举例:partition spinor

6输出说明

查看spinor flash分区信息

在这里插入图片描述

图 64 查看spinor flash分区信息
36)pwd

1命令功能

pwd命令用来显示当前路径。

2命令格式

pwd

3使用指南

pwd 命令将当前目录的全路径名称(从根目录)写入标准输出。全部目录使用 / (斜线)分隔。第一个 / 表示根目录, 最后一个目录是当前目录。

37)rm

1命令功能

rm命令用来删除文件或文件夹。

2命令格式

rm [-r] [dirname / filename]

3参数说明

表63 参数说明
参数参数说明取值范围
-r可选参数,若是删除目录则需要该参数。N/A
dirname/filename要删除文件或文件夹的名称,支持输入路径。N/A

4使用指南

rm命令一次只能删除一个文件或文件夹。

rm -r命令可以删除非空目录。

5使用实例

举例:

输入rm log1.txt

输入rm -r sd

6输出说明

在这里插入图片描述

图 65 用 rm 命令删除文件 log1.txt

在这里插入图片描述

图 66 用 rm -r 删除目录 sd
38)rmdir

1命令功能

rmdir命令用来删除一个目录。

2命令格式

rmdir [dir]

3参数说明

dir:需要删除目录的名称,删除目录必须为空,支持输入路径。

4使用指南

rmdir命令只能用来删除目录。

rmdir一次只能删除一个目录。

rmdir只能删除空目录。

5使用实例

举例:输入rmdir dir

6输出说明

在这里插入图片描述

图 67 删除一个名为 dir 的目录
39)statfs

1命令功能

statfs命令用来打印文件系统的信息,如该文件系统类型、总大小、可用大小等信息。

2命令格式

statfs [directory]

3参数说明

表64 参数说明
参数参数说明取值范围
directory文件系统的路径。必须是存在的文件系统,并且其支持statfs命令,当前支持的文件系统有:JFFS2,FAT,NFS。

4使用指南

打印信息因文件系统而异。

5使用实例

以nfs文件系统为例:

statfs /nfs

在这里插入图片描述

图68 statfs输出说明
40)sync

1命令功能

sync命令用于同步缓存数据(文件系统的数据)到sd卡。

2命令格式

sync

3使用指南

sync命令用来刷新缓存,当没有sd卡插入时不进行操作。

有sd卡插入时缓存信息会同步到sd卡,成功返回时无显示。

4使用实例

举例:输入sync,有sd卡时同步到sd卡,无sd卡时不操作。

41)touch

1命令功能

touch命令用来在指定的目录下创建一个不存在的空文件。

touch命令操作已存在的文件会成功,不会更新时间戳。

2命令格式

touch [filename]

3参数说明

表 1 参数说明

filename:需要创建文件的名称。

4使用指南

touch命令用来创建一个空文件,该文件可读写。

使用touch命令一次只能创建一个文件。

须知: 在系统重要资源路径下使用touch命令创建文件,会对系统造成死机等未知影响,如在/dev路径下执行touch uartdev-0,会产生系统卡死现象。

5使用实例

举例:输入touch file.c 输出说明

6输出说明

在这里插入图片描述

图 69 创建一个名为 file.c 的文件
42)writeproc

1命令功能

proc fs支持传入字符串参数,需要每个文件实现自己的写方法。

2命令格式

writeproc \<data\> \>\> /proc/\<filename\>

3参数说明

表65 参数说明
参数参数说明取值范围
data要输入的字符串,以空格为结束符,如需输入空格,请用""包裹。N/A
filenamedata要传入的proc文件。N/A

4使用指南

proc文件实现自身的write函数,调用writeproc命令后会将入参传入write函数。

说明: procfs不支持多线程访问。

5使用实例

举例:writeproc test \>\> /proc/uptime

6输出说明

OHOS \# writeproc test \>\> /proc/uptime

[INFO]write buf is: test

test \>\> /proc/uptime

说明: uptime proc文件临时实现write函数,INFO日志为实现的测试函数打印的日志。

43)umount

命令功能

umount命令用来卸载指定文件系统。

命令格式

umount [dir]

参数说明

表 66 参数说明
参数参数说明取值范围
dir需要卸载文件系统对应的目录。系统已挂载的文件系统的目录。

4使用指南

umount后加上需要卸载的指定文件系统的目录,即将指定文件系统卸载。

5使用实例

举例:umount /bin1/vs/sd

6输出说明

将已在/bin1/vs/sd挂载的文件系统卸载

在这里插入图片描述

图 70 umount输出示例
5.1.4.3网络命令
44)arp

1命令功能

在以太网中,主机之间的通信是直接使用MAC地址(非IP地址)来通信的,所以,对于使用IP通信的协议,必须能够将IP地址转换成MAC地址,才能在局域网(以太网)内通信。解决这个问题的方法就是主机存储一张IP和MAC地址对应的表,即ARP缓存,主机要往一个局域网内的目的IP地址发送IP包时,就可以从ARP缓存表中查询到目的MAC地址。ARP缓存是由TCP/IP协议栈维护的,用户可通过ARP命令查看和修改ARP表。

2命令格式

arp

arp [-i IF] -s IPADDR HWADDR

arp [-i IF] -d IPADDR

3参数说明

表67 参数说明
参数参数说明取值范围
打印整个ARP缓存的内容。N/A
-i IF指定的网络接口(可选参数)。N/A
-s IPADDR HWADDR增加一条ARP表项,后面的参数是局域网中另一台主机的IP地址及其对应的MAC地址。N/A
-d IPADDR删除一条ARP表项。N/A

4使用指南

arp命令用来查询和修改TCP/IP协议栈的ARP缓存表,增加非同一子网内的IP地址的ARP表项是没有意义的,协议栈会返回失败。

命令需要启动TCP/IP协议栈后才能使用。

5使用实例

举例:

输入arp

在这里插入图片描述

图71 打印整个 ARP 缓存表
表68 参数说明
参数说明
Address表示网络设备的IPv4地址。
HWaddress表示网络设备的MAC地址。
Iface表示该ARP表项使用的接口名。
Type表示该ARP表项是动态的还是静态的,动态是指ARP表项由协议栈自动创建,静态是指ARP表项是由用户增加的。
45)dhclient

1命令功能

设置和查看dhclient的参数。

2命令格式

dhclient \<netif name\>

dhclient -x \<netif name\>

dhclient -gb \<netif name\>

dhclient -sv \<vendor\>

dhclient -gv

dhclient -gd \<index\>

dhclient -sd \<dns_ip\>

3参数说明

表69 参数说明
参数参数说明取值范围
<netif name>启动对应网卡的dhcp请求。网卡名字,eth0。
-x <netif name>关闭对应网卡的dhcp功能。网卡名字,eth0。
-gb <netif name>查看对应网卡的dhcp请求是否完成。网卡名字,eth0。
-sv <vendor>设置dhcp请求的厂商信息。厂商信息,长度是32个字符。
-gv查看dhcp请求的厂商信息。N/A
-gd <index>获取第index个dns server的信息。index,0或者1。
-sd <dns_ip>设置主dns server的ip。dns的ip地址。

4使用指南

dhclient eth0

dhclient -x eth0

dhclient -gb eth0

dhclient -sv MFSI

dhclient -gv

dhclient -gd 0

dhclient -sd 8.8.8.8

5使用实例

在这里插入图片描述

图 72 使用实例
表70输出说明表
输出说明
dhclient: set vendor info [MFSI] success设置厂商信息MFSI信息成功。
dns[0]: 192.168.1.100dns server ip地址为192.168.1.100。
46)dns

1命令功能

命令用于查看和设置单板dns服务器地址。

2命令格式

dns \<1-2\> \<IP\>

dns -a

3参数说明

表 71 参数说明
参数参数说明取值范围
<1-2>选择设置第一个还是第二个DNS服务器。1~2
<IP>服务器IP地址。N/A
-a显示当前设置情况。N/A

4使用实例

举例:

检查当前DNS设置。

设置第二个DNS服务器IP。

检查DNS设置是否成功。

5输出说明

检查当前DNS设置:

OHOS \# dns -a

dns1: 192.168.1.10

dns2: 0.0.0.0

设置第二个DNS服务器IP:

OHOS \# dns 2 192.168.1.2

检查DNS设置是否成功:

OHOS \# dns -a

dns1: 192.168.1.10

dns2: 192.168.1.2
47)ifconfig

1命令功能

ifconfig命令用来查询和设置网卡的IP地址、网络掩码、网关、硬件mac地址等参数。并能够启用/关闭网卡。

2命令格式

ifconfig

[-a]

\<interface\> \<address\> [netmask \<mask\>] [gateway \<address\>]

[hw ether \<address\>] [mtu \<size\>]

[inet6 add \<address\>]

[inet6 del \<address\>]

[up\|down]

3参数说明

表 72 参数说明
参数参数说明取值范围
不带参数打印所有网卡的IP地址、网络掩码、网关、硬件mac地址、MTU、运行状态等信息。N/A
-a打印协议栈收发数据信息。N/A
interface指定网卡名,比如eth0。N/A
address设置IP地址,比如192.168.1.10,需指定网卡名。N/A
netmask设置子网掩码,后面要掩码参数,比如255.255.255.0。N/A
gateway设置网关,后面跟网关参数,比如192.168.1.1。N/A
hw ether设置mac地址, 后面是MAC地址,比如00:11:22:33:44:55。目前只支持ether硬件类型。N/A
mtu设置mtu大小,后面是mtu大小,比如1000。仅支持ipv4情况下的范围为 [68,1500]。 支持ipv6情况下的范围为 [1280,1500]。
add设置ipv6的地址,比如2001🅰️b:c:d:e:f:d,需指定网卡名和inet6。N/A
del删除ipv6的地址,需指定网卡名和inet6。N/A
up启用网卡数据处理,需指定网卡名。N/A
down关闭网卡数据处理,需指定网卡名。N/A

4使用指南

命令需要启动TCP/IP协议栈后才能使用。

由于IP冲突检测需要反应时间,每次使用ifconfig设置IP后会有2S左右的延时。

5使用实例

ifconfig eth0 192.168.100.31 netmask 255.255.255.0 gateway 192.168.100.1 hw ether 00:49:cb:6c:a1:31

ifconfig -a

ifconfig eth0 inet6 add 2001:a:b:c:d:e:f:d

ifconfig eth0 inet6 del 2001:a:b:c:d:e:f:d

6输出说明

设置网络参数

OHOS \# ifconfig eth0 192.168.100.31 netmask 255.255.255.0 gateway 192.168.100.1 hw ether 00:49:cb:6c:a1:31

OHOS \# ifconfig

eth0 ip:192.168.100.31 netmask:255.255.255.0 gateway:192.168.100.1

HWaddr 00:49:cb:6c:a1:31 MTU:1500 Running Default Link UP

lo ip:127.0.0.1 netmask:255.0.0.0 gateway:127.0.0.1

ip6: ::1/64

HWaddr 00 MTU:0 Running Link UP

输出的各参数说明如下表所示:

表 73 参数说明
参数说明
ip板子IP地址。
netmask网络掩码。
gateway网关。
HWaddr板子硬件mac地址。
MTU网络最大传输单元。
Running/Stop网卡是否正在运行。
Default有这项说明此网卡连接到默认网关。
Link UP/Down网卡连接状态。

获取协议栈统计信息

OHOS \# ifconfig -a

RX packets:6922 errors:0 ip dropped:4312 link dropped:67 overrun:0 bytes:0 (0.0 B)

RX packets(ip6):3 errors:0 dropped:0 overrun:0 bytes:0 (0.0 B)

TX packets:1394 errors:0 link dropped:67 overrun:0 bytes:0(0.0 B)

TX packets(ip6):3 errors:0 overrun:0 bytes:0(0.0 B)

输出的各参数说明如下表所示:

表 74 ifconfig -a 参数说明
参数说明
RX packetsIP层已接收的正常数据包的个数。
RX errorIP层已接收的错误数据包的个数,错误类型包括长度错误,校验错误,IP option错误,IP首部protocol错误等。
RX droppedIP层已丢弃的数据包的个数,丢弃原因包括数据包错误,封包无法转发,本地网卡处于关闭状态等。
RX overrunMAC层向上层协议栈投递封包失败的个数,失败原因主要是协议栈资源不足。
RX bytesIP层已接收的正常数据包的总长度,不包括重组未完成的分片的长度。
TX packetsIP层已正常发送或转发的数据包的个数。
TX errorIP层发送失败的数据包的个数,失败原因包括封包无法路由,封包在协议栈内处理失败等。
TX droppedMAC层由于发送失败而丢弃的数据包个数,失败原因包括网卡驱动处理封包失败等。
TX overrun暂未使用。
TX bytesIP层已正常发送或者转发的数据包的总长度。

设置IPv6的地址信息:

OHOS \# ifconfig eth0 inet6 add 2001:a:b:c:d:e:f:d

OHOS \# ifconfig

eth1 ip:192.168.3.60 netmask:255.255.255.0 gateway:0.0.0.0

HWaddr 00:0e:c6:a8:5a:c2 MTU:1500 Running Link UP

eth0 ip:192.168.2.60 netmask:255.255.255.0 gateway:0.0.0.0

ip6: 2001:A:B:C:D:E:F:D/64

HWaddr 46:44:02:02:03:03 MTU:1500 Running Link UP

lo ip:127.0.0.1 netmask:255.0.0.0 gateway:127.0.0.1

ip6: ::1/64

HWaddr 00 MTU:16436 Running Link UP

删除IPv6的地址信:

OHOS \# ifconfig eth0 inet6 del 2001:a:b:c:d:e:f:d

OHOS \# ifconfig

eth1 ip:192.168.3.60 netmask:255.255.255.0 gateway:0.0.0.0

HWaddr 00:0e:c6:a8:5a:c2 MTU:1500 Running Link UP

eth0 ip:192.168.2.60 netmask:255.255.255.0 gateway:0.0.0.0

HWaddr 46:44:02:02:03:03 MTU:1500 Running Link UP

lo ip:127.0.0.1 netmask:255.0.0.0 gateway:127.0.0.1

ip6: ::1/64

HWaddr 00 MTU:16436 Running Link UP
48)ipdebug

1命令功能

ipdebug是控制台命令,是一个调试ipv6信息非常有用的工具,它可以显示IPV6地址前缀,邻居条目信息,目的地缓存条目以及默认的路由条目。

2命令格式

ipdebug

3使用指南

输入命令ipdebug。

4输出说明

ipdebug打印信息如下:

OHOS \# ipdebug

=================

\|\| Prefix List \|\|

=================

Prefix validLifetime Flags

==============================================================================================

2001:: 86384 1

\----------------------------------------------------------------------------------------------

============================

\|\| Neighbor Cache Entries \|\|

============================

2001::1 dev eth0 lladr 44:39:C4:94:5D:44 STALE Router

FE80::4639:C4FF:FE94:5D44 dev eth0 lladr 44:39:C4:94:5D:44 REACHABLE Router

\--------------------------------------------------------------------

===============================

\|\| Destination Cache Entries \|\|

===============================

2001::1 2001::1 pmtu 1500 age 152

FE80::4639:C4FF:FE94:5D44 FE80::4639:C4FF:FE94:5D44 pmtu 1500 age 6

2002::1 FE80::4639:C4FF:FE94:5D44 pmtu 1500 age 17

:: :: pmtu 0 age 219

:: :: pmtu 0 age 219

:: :: pmtu 0 age 219

:: :: pmtu 0 age 219

:: :: pmtu 0 age 219

:: :: pmtu 0 age 219

:: :: pmtu 0 age 219

\--------------------------------------------------------------------

============================

\|\| Default Router Entries \|\|

============================

FE80::4639:C4FF:FE94:5D44 invalidation_timer 1784 flags 0
49)netstat

1命令功能

netstat是控制台命令,是一个监测TCP/IP网络的非常有用的工具,它可以显示实际的网络连接以及每一个网络接口设备的状态信息。netstat用于显示与TCP、UDP协议相关的统计数据,一般用于检验本设备(单板)各端口的网络连接情况。

2命令格式

netstat

3使用指南

直接输入命令。

4使用实例

举例:输入netstat

在这里插入图片描述

图 73 netstat 打印信息

5输出说明

表 75输出说明
输出说明
Proto协议类型。
Recv-Q未被用户读取的数据量。 对于Listen TCP,此值为已完成三次握手,但是未被用户accept的TCP连接的数量。
Send-Q对TCP连接,已发送但未确认的数据量。 对UDP连接,由于IP地址解析未完成而缓存的数据量。
Local Address本地地址和端口。
Foreign Address远程地址和端口。
StateTCP连接状态,UDP此项无意义。

说明: 形如“========== total sockets 32 ====== unused sockets 22 BootTime 27 s ========== ”,表示一共32个套接字,未使用套接字22个,距系统启动已经过27秒。

50)ntpdate

1命令功能

命令用于从服务器同步系统时间。

2命令格式

ntpdate [SERVER_IP1] [SERVER_IP2]...

3参数说明

SERVER_IP:NTP服务器IP。

4使用指南

直接执行ntpdate [SERVER_IP1] [SERVER_IP2]… ntpdate会获取第一个有效服务器IP的时间并显示。

5使用实例

举例:使用ntpdate命令更新系统时间:ntpdate 192.168.1.3。

6输出说明

OHOS # ntpdate 192.168.1.3

time server 192.168.1.3: Mon Jun 13 09:24:25 2016

因为板子和服务器时区的不同,获取后的显示时间可能和服务器时间有数小时的差别。

51)ping

1命令功能

ping命令用于测试网络连接是否正常。

2命令格式

ping\_ [-n cnt_] [-w interval] [-l data_len]\_ \<IP\>\_

ping [-t] [-w interval] \<IP\>

ping -k

3参数说明

表76 参数说明
参数参数说明取值范围
IP要测试是否网络连通的IPv4地址。N/A
-n cnt执行的次数,不带本参数则默认为4次。1-65535
-w interval发送两次ping包的时间间隔,单位毫秒。>0
-l data_lenping包,即ICMP echo request报文的数据长度,不包含ICMP包头。0-65500
-t表示永久ping,直到使用ping -k杀死ping线程。N/A
-k杀死ping线程,停止ping。N/A

4使用指南

ping命令用来测试到目的IP的网络连接是否正常,参数为目的IP地址。

如果目的IP不可达,会显示请求超时。

如果显示发送错误,说明没有到目的IP的路由。

命令需要启动TCP/IP协议栈后才能使用。

5使用实例

举例:输入ping 192.168.1.10

6输出说明

在这里插入图片描述

图 74 ping tftp 服务器地址
52)ping6

1命令功能

ping6用于测试IPv6网络连接是否正常。

2命令格式

ping6 [-c count] [-I interface / sourceAddress] destination

3参数说明

表 77 参数说明
参数参数说明取值范围
-c count执行的次数,不带本参数则默认为4次。1~65535
-I interface指定网卡执行IPv6 ping操作。N/A
-I sourceAddress指定源IPv6地址执行ping操作。N/A
destination目标主机地址。N/A

4使用指南

如果目的IPv6地址不可达,会显示请求超时。

如果显示发送错误,说明没有到目的IPV6的路由。

命令需要启动TCP/IP协议栈后才能使用。

5使用实例

ping6 2001:a:b:c:d:e:f:b

ping6 -c 3 2001:a:b:c:d:e:f:b

ping6 -I eth0 2001:a:b:c:d:e:f:b

ping6 -I 2001:a:b:c:d:e:f:d 2001:a:b:c:d:e:f:b

6输出说明

输入ping6 2001:a:b:c:d:e:f:b

OHOS \# ping6 2001:a:b:c:d:e:f:b PING 2001:A:B:C:D:E:F:B with 56 bytes of data.

56 bytes from 2001:A:B:C:D:E:F:B : icmp_seq=1 time\<1 ms

56 bytes from 2001:A:B:C:D:E:F:B : icmp_seq=2 time\<1 ms

56 bytes from 2001:A:B:C:D:E:F:B : icmp_seq=3 time\<1 ms

56 bytes from 2001:A:B:C:D:E:F:B : icmp_seq=4 time\<1 ms

\--- 2001:a:b:c:d:e:f:b/64 ping statistics ---

4 packets transmitted, 4 received, 0.00% packet loss, time 20ms

rtt min/avg/max = 0/0.00/0 ms

输入ping6 -c 3 2001:a:b:c:d:e:f:b 指定3次进行网络测试

OHOS \# ping6 -c 3 2001:a:b:c:d:e:f:b PING 2001:A:B:C:D:E:F:B with 56 bytes of data.

56 bytes from 2001:A:B:C:D:E:F:B : icmp_seq=1 time\<1 ms

56 bytes from 2001:A:B:C:D:E:F:B : icmp_seq=2 time\<1 ms

56 bytes from 2001:A:B:C:D:E:F:B : icmp_seq=3 time\<1 ms

\--- 2001:a:b:c:d:e:f:b/64 ping statistics ---

3 packets transmitted, 3 received, 0.00% packet loss, time 20ms

rtt min/avg/max = 0/0.00/0 ms

输入 ping6 -I eth0 2001:a:b:c:d:e:f:b 使用指定网卡接口eth0测试IPv6。

OHOS \# ping6 -I eth0 2001:a:b:c:d:e:f:b PING 2001:A:B:C:D:E:F:B with 56 bytes of data.

56 bytes from 2001:A:B:C:D:E:F:B : icmp_seq=1 time=10 ms

56 bytes from 2001:A:B:C:D:E:F:B : icmp_seq=2 time\<1 ms

56 bytes from 2001:A:B:C:D:E:F:B : icmp_seq=3 time\<1 ms

56 bytes from 2001:A:B:C:D:E:F:B : icmp_seq=4 time\<1 ms

\--- 2001:a:b:c:d:e:f:b/64 ping statistics ---

4 packets transmitted, 4 received, 0.00% packet loss, time 30msrtt min/avg/max = 0/2.50/10 ms

输入 ping6 -I 2001:a:b:c:d:e:f:d 2001:a:b:c:d:e:f:b 使用指定的源IPv6地址2001🅰️b:c:d:e:f:d进行测试。

OHOS \# ping6 -I 2001:a:b:c:d:e:f:d 2001:a:b:c:d:e:f:b PING 2001:A:B:C:D:E:F:B with 56 bytes of data.

56 bytes from 2001:A:B:C:D:E:F:B : icmp_seq=1 time\<1 ms

56 bytes from 2001:A:B:C:D:E:F:B : icmp_seq=2 time\<1 ms

56 bytes from 2001:A:B:C:D:E:F:B : icmp_seq=3 time\<1 ms

56 bytes from 2001:A:B:C:D:E:F:B : icmp_seq=4 time\<1 ms

\--- 2001:a:b:c:d:e:f:b/64 ping statistics ---

4 packets transmitted, 4 received, 0.00% packet loss, time 20msrtt min/avg/max = 0/0.00/0 ms
53)telnet

1命令功能

本命令用于启动或关闭telnet server服务。

2命令格式

telnet [on \| off]

3参数说明

表78 参数说明
参数参数说明取值范围
on启动telnet server服务。N/A
off关闭telnet server服务。N/A

4使用指南

telnet启动要确保网络驱动及网络协议栈已经初始化完成,且板子的网卡是link up状态。

暂时无法支持多个客户端(telnet + IP)同时连接开发板。

须知: telnet属于调测功能,默认配置为关闭,正式产品中禁止使用该功能。

5使用实例

举例:输入telnet on

6输出说明

在这里插入图片描述

图 75输入 telnet on
54)tftp

1命令功能

TFTP(Trivial File Transfer Protocol,简单文件传输协议)是TCP/IP协议族中的一个用来在客户机与服务器之间进行简单文件传输的协议,提供简单、低开销的文件传输服务。端口号为69。

tftp命令可以从TFTP服务器上下载文件。

2命令格式

./bin/tftp \<-g/-p\> -l [FullPathLocalFile] -r [RemoteFile] [Host]

3参数说明

表79 参数说明
参数参数说明取值范围
-g/-p文件传输方向: -g 从TFTP服务器获取文件。 -p 上传文件到TFTP服务器。N/A
-l FullPathLocalFile本地文件完整路径。N/A
-r RemoteFile服务端文件名。N/A
Host服务端IP。N/A

4使用指南

在服务器端搭建TFTP服务器,并进行正确配置。

OpenHarmony单板使用tftp命令上传、下载文件。

传输的文件大小是有限制的不能大于32M。

须知: tftp属于调测功能,默认配置为关闭,正式产品中禁止使用该功能。

5使用实例

举例:从服务器下载out文件。

6输出说明

OHOS \# ./bin/tftp -g -l /nfs/out -r out 192.168.1.2

TFTP transfer finish

tftp命令执行后,传输正常完成会显示TFTP transfer finish, 失败的话会显示其他的打印信息帮助定位问题。

5.1.5魔法键使用方法

1使用场景

在系统运行出现无响应等情况时,可以通过魔法键功能确定系统是否被锁中断(魔法键也无响应)或者查看系统任务运行状态等信息。

在中断有响应的情况下,可以通过魔法键查看task信息中 cpup(CPU占用率)看是哪个任务长时间占用CPU导致系统其他任务无响应(一般为比较高优先级任务一直抢占CPU,导致低优先级任务无响应)。

2使用方法

配置宏LOSCFG_ENABLE_MAGICKEY。

魔法键依赖于宏LOSCFG_ENABLE_MAGICKEY,使用时通过menuconfig在配置项中开启“Enable MAGIC KEY”:

Debug —> Enable MAGIC KEY;若关闭该选项,则魔法键失效。

说明:

可以在menuconfig中,将光标移动到LOSCFG_ENABLE_MAGICKEY上,输入“?”,查看帮助信息。

输入“ctrl + r”键,打开魔法键检测功能。

在连接UART或者USB转虚拟串口的情况下,输入“ctrl + r” 键,打开魔法键检测功能,输出 “Magic key on”;再输入一次后,则关闭魔法键检测功能,输出“Magic key off”。魔法键功能如下:

ctrl + z:帮助键,输出相关魔法键简单介绍;

ctrl + t:输出任务相关信息;

ctrl + p:系统主动进入panic,输出panic相关信息后,系统会挂住;

ctrl + e:系统进行简单完整性内存池检查,检查出错会输出相关错误信息,检查正常会输出“system memcheck over, all passed!”。

须知: 魔法键检测功能打开情况下,如果需要通过UART或者USB转虚拟串口输入特殊字符需避免与魔法键值重复,否则魔法键会被误触发,而原有设计功能可能出现错误。

5.2 Trace

5.2.1基本概念

Trace调测旨在帮助开发者获取内核的运行流程,各个模块、任务的执行顺序,从而可以辅助开发者定位一些时序问题或者了解内核的代码运行过程。

5.2.2 运行机制

内核提供一套Hook框架,将Hook点预埋在各个模块的主要流程中, 在内核启动初期完成Trace功能的初始化,并注册Trace的处理函数到Hook中。

当系统触发到一个Hook点时,Trace模块会对输入信息进行封装,添加Trace帧头信息,包含事件类型、运行的cpuid、运行的任务id、运行的相对时间戳等信息;

Trace提供2种工作模式,离线模式和在线模式。

离线模式会将trace frame记录到预先申请好的循环buffer中。如果循环buffer记录的frame过多则可能出现翻转,会覆盖之前的记录,故保持记录的信息始终是最新的信息。Trace循环buffer的数据可以通过shell命令导出进行详细分析,导出信息已按照时间戳信息完成排序。

在这里插入图片描述

图 76 运行机制

在线模式需要配合IDE使用,实时将trace frame记录发送给IDE,IDE端进行解析并可视化展示。

5.2.3 接口说明
5.2.3.1内核态

OpenHarmony LiteOS-A内核的Trace模块提供下面几种功能,接口详细信息可以查看API参考。

表 80 Trace模块接口说明
功能分类接口名描述
启停控制LOS_TraceStart启动Trace
LOS_TraceStop停止Trace
操作Trace记录的数据LOS_TraceRecordDump输出Trace缓冲区数据
LOS_TraceRecordGet获取Trace缓冲区的首地址
LOS_TraceReset清除Trace缓冲区中的事件
过滤Trace记录的数据LOS_TraceEventMaskSet设置事件掩码,仅记录某些模块的事件
屏蔽某些中断号事件LOS_TraceHwiFilterHookReg注册过滤特定中断号事件的钩子函数
插桩函数LOS_TRACE_EASY简易插桩
LOS_TRACE标准插桩

当用户需要针对自定义事件进行追踪时,可按规则在目标源代码中进行插桩,系统提供如下2种插桩接口:

LOS_TRACE_EASY(TYPE, IDENTITY, params…) 简易插桩。

一句话插桩,用户在目标源代码中插入该接口即可。

TYPE有效取值范围为[0, 0xF],表示不同的事件类型,不同取值表示的含义由用户自定义。

IDENTITY类型UINTPTR,表示事件操作的主体对象。

Params类型UINTPTR,表示事件的参数。

示例:

假设需要新增对文件(fd1、fd2)读写操作的简易插桩,

自定义读操作为type:1, 写操作为type:2,则

在读fd1文件的适当位置插入:

LOS_TRACE_EASY(1, fd1, flag, size);

在读fd2文件的适当位置插入:

LOS_TRACE_EASY(1, fd2, flag, size);

在写fd1文件的适当位置插入:

LOS_TRACE_EASY(2, fd1, flag, size);

在写fd2文件的适当位置插入:

LOS_TRACE_EASY(2, fd2,flag, size);

LOS_TRACE(TYPE, IDENTITY, params…) 标准插桩。

相比简易插桩,支持动态过滤事件和参数裁剪,但使用上需要用户按规则来扩展。

TYPE用于设置具体的事件类型,可以在头文件los_trace.h中的enum LOS_TRACE_TYPE中自定义事件类型。定义方法和规则可以参考其他事件类型。

IDENTITY和Params的类型及含义同简易插桩。

示例:

1.在enum LOS_TRACE_MASK中定义事件掩码,即模块级别的事件类型。

定义规范为TRACE_#MOD#_FLAG,#MOD#表示模块名,例如:

TRACE_FS_FLAG = 0x4000

2.在enum LOS_TRACE_TYPE中定义具体事件类型。定义规范为#TYPE# = TRACE_#MOD#_FLAG | NUMBER,例如:

FS_READ = TRACE_FS_FLAG \| 0; // 读文件

FS_WRITE = TRACE_FS_FLAG \| 1; // 写文件

3.定义事件参数。定义规范为#TYPE#_PARAMS(IDENTITY, parma1…) IDENTITY, …

其中的#TYPE#就是上面2中的#TYPE#,例如:

\#define FS_READ_PARAMS(fp, fd, flag, size) fp, fd, flag, size

宏定义的参数对应于Trace缓冲区中记录的事件参数,用户可对任意参数字段进行裁剪:

当定义为空时,表示不追踪该类型事件:

\#define FS_READ_PARAMS(fp, fd, flag, size) // 不追踪文件读事件

4.在适当位置插入代码桩。定义规范为LOS_TRACE(#TYPE#, #TYPE#_PARAMS(IDENTITY, parma1…))

LOS_TRACE(FS_READ, fp, fd, flag, size); // 读文件的代码桩,

#TYPE#之后的入参就是上面3中的FS_READ_PARAMS函数的入参

预置的Trace事件及参数均可以通过上述方式进行裁剪,参数详见kernel\include\los_trace.h。

Trace Mask事件过滤接口LOS_TraceEventMaskSet(UINT32 mask),其入参mask仅高28位生效(对应LOS_TRACE_MASK中某模块的使能位),仅用于模块的过滤,暂不支持针对某个特定事件的细粒度过滤。例如:LOS_TraceEventMaskSet(0x202),则实际设置生效的mask为0x200(TRACE_QUE_FLAG),QUE模块的所有事件均会被采集。一般建议使用的方法为: LOS_TraceEventMaskSet(TRACE_EVENT_FLAG | TRACE_MUX_FLAG | TRACE_SEM_FLAG | TRACE_QUE_FLAG);

如果仅需要过滤简易插桩事件,通过设置Trace Mask为TRACE_MAX_FLAG即可。

Trace缓冲区有限,事件写满之后会覆盖写,用户可通过LOS_TraceRecordDump中打印的CurEvtIndex识别最新记录。

Trace的典型操作流程为:LOS_TraceStart、 LOS_TraceStop、 LOS_TraceRecordDump.

针对中断事件的Trace, 提供中断号过滤,用于解决某些场景下特定中断号频繁触发导致其他事件被覆盖的情况,用户可自定义中断过滤的规则,

示例如下:

BOOL Example_HwiNumFilter(UINT32 hwiNum)

{

if ((hwiNum == TIMER_INT) \|\| (hwiNum == DMA_INT)) {

return TRUE;

}

return FALSE;

}

LOS_TraceHwiFilterHookReg(Example_HwiNumFilter);

则当中断号为TIMER_INT 或者DMA_INT时,不记录中断事件。

5.2.3.2用户态

新增trace字符设备,位于"/dev/trace",通过对设备节点的read\write\ioctl,实现用户态trace的读写和控制:

read: 用户态读取Trace记录数据

write: 用户态事件写入

ioctl: 用户态Trace控制操作,包括

\#define TRACE_IOC_MAGIC 'T'

\#define TRACE_START \_IO(TRACE_IOC_MAGIC, 1)

\#define TRACE_STOP \_IO(TRACE_IOC_MAGIC, 2)

\#define TRACE_RESET \_IO(TRACE_IOC_MAGIC, 3)

\#define TRACE_DUMP \_IO(TRACE_IOC_MAGIC, 4)

\#define TRACE_SET_MASK \_IO(TRACE_IOC_MAGIC, 5)

分别对应Trace启动(LOS_TraceStart)、停止(LOS_TraceStop)、清除记录(LOS_TraceReset)、dump记录(LOS_TraceRecordDump)、设置事件过滤掩码(LOS_TraceEventMaskSet)

具体的使用方法参见用户态编程实例

5.2.4 开发流程
5.2.4.1内核态开发流程

开启Trace调测的典型流程如下:

配置Trace模块相关宏。

配置Trace控制宏LOSCFG_KERNEL_TRACE,默认关,在kernel/liteos_a目录下执行 make update_config命令配置"Kernel->Enable Hook Feature->Enable Trace Feature"中打开:

表81 配置说明表
配置项menuconfig选项含义设置值
LOSCFG_KERNEL_TRACEEnable Trace FeatureTrace模块的裁剪开关YES/NO
LOSCFG_RECORDER_MODE_OFFLINETrace work mode ->Offline modeTrace工作模式为离线模式YES/NO
LOSCFG_RECORDER_MODE_ONLINETrace work mode ->Online modeTrace工作模式为在线模式YES/NO
LOSCFG_TRACE_CLIENT_INTERACTEnable Trace Client Visualization and Control使能与Trace IDE (dev tools)的交互,包括数据可视化和流程控制YES/NO
LOSCFG_TRACE_FRAME_CORE_MSGEnable Record more extended content ->Record cpuid, hardware interrupt status, task lock status记录CPUID、中断状态、锁任务状态YES/NO
LOSCFG_TRACE_FRAME_EVENT_COUNTEnable Record more extended content ->Record event count, which indicate the sequence of happend events记录事件的次序编号YES/NO
LOSCFG_TRACE_FRAME_MAX_PARAMSRecord max params配置记录事件的最大参数个数INT
LOSCFG_TRACE_BUFFER_SIZETrace record buffer size配置Trace的缓冲区大小INT

可选)预置事件参数和事件桩(或使用系统默认的事件参数配置和事件桩)。

(可选)调用LOS_TraceStop停止Trace后,清除缓冲区LOS_TraceReset(系统默认已启动trace)。

(可选)调用LOS_TraceEventMaskSet设置需要追踪的事件掩码(系统默认的事件掩码仅使能中断与任务事件),事件掩码参见los_trace.h 中的LOS_TRACE_MASK定义。

在需要记录事件的代码起始点调用LOS_TraceStart。

在需要记录事件的代码结束点调用LOS_TraceStop。

调用LOS_TraceRecordDump输出缓冲区数据(函数的入参为布尔型,FALSE表示格式化输出,TRUE表示输出到Trace IDE)。

上述第3-7步中的接口,均封装有对应的shell命令,开启shell后可执行相应的命令,对应关系如下:

LOS_TraceReset —— trace_reset

LOS_TraceEventMaskSet —— trace_mask

LOS_TraceStart —— trace_start

LOS_TraceStop —— trace_stop

LOS_TraceRecordDump —— trace_dump

内核态编程实例

本实例实现如下功能:

创建一个用于Trace测试的任务。

设置事件掩码。

启动trace。

停止trace。

格式化输出trace数据。

内核态示例代码

实例代码如下:

\#include "los_trace.h"

UINT32 g_traceTestTaskId;

VOID Example_Trace(VOID)

{

UINT32 ret;

LOS_TaskDelay(10);

/\* 开启trace \*/

ret = LOS_TraceStart();

if (ret != LOS_OK) {

dprintf("trace start error\\n");

return;

}

/\* 触发任务切换的事件 \*/

LOS_TaskDelay(1);

LOS_TaskDelay(1);

LOS_TaskDelay(1);

/\* 停止trace \*/

LOS_TraceStop();

LOS_TraceRecordDump(FALSE);

}

UINT32 Example_Trace_test(VOID){

UINT32 ret;

TSK_INIT_PARAM_S traceTestTask;

/\* 创建用于trace测试的任务 \*/

memset(&traceTestTask, 0, sizeof(TSK_INIT_PARAM_S));

traceTestTask.pfnTaskEntry = (TSK_ENTRY_FUNC)Example_Trace;

traceTestTask.pcName = "TestTraceTsk"; /\* 测试任务名称 \*/

traceTestTask.uwStackSize = 0x800;

traceTestTask.usTaskPrio = 5;

traceTestTask.uwResved = LOS_TASK_STATUS_DETACHED;

ret = LOS_TaskCreate(&g_traceTestTaskId, \&traceTestTask);

if(ret != LOS_OK){

dprintf("TraceTestTask create failed .\\n");

return LOS_NOK;

}

/\* 系统默认情况下已启动trace, 因此可先关闭trace后清除缓存区后,再重启trace \*/

LOS_TraceStop();

LOS_TraceReset();

/\* 开启任务模块事件记录 \*/

LOS_TraceEventMaskSet(TRACE_TASK_FLAG);

return LOS_OK;

}

LOS_MODULE_INIT(Example_Trace_test, LOS_INIT_LEVEL_KMOD_EXTENDED);

内核态结果验证

输出结果如下:

\*\*\*\*\*\*\*TraceInfo begin\*\*\*\*\*\*\*

clockFreq = 50000000

CurEvtIndex = 7

Index Time(cycles) EventType CurTask Identity params

0 0x366d5e88 0x45 0x1 0x0 0x1f 0x4 0x0

1 0x366d74ae 0x45 0x0 0x1 0x0 0x8 0x1f

2 0x36940da6 0x45 0x1 0xc 0x1f 0x4 0x9

3 0x3694337c 0x45 0xc 0x1 0x9 0x8 0x1f

4 0x36eea56e 0x45 0x1 0xc 0x1f 0x4 0x9

5 0x36eec810 0x45 0xc 0x1 0x9 0x8 0x1f

6 0x3706f804 0x45 0x1 0x0 0x1f 0x4 0x0

7 0x37070e59 0x45 0x0 0x1 0x0 0x8 0x1f

\*\*\*\*\*\*\*TraceInfo end\*\*\*\*\*\*\*

输出的事件信息包括:发生时间、事件类型、事件发生在哪个任务中、事件操作的主体对象、事件的其他参数。

EventType:表示的具体事件可查阅头文件los_trace.h中的enum LOS_TRACE_TYPE。

CurrentTask:表示当前运行在哪个任务中,值为taskid。

Identity:表示事件操作的主体对象,可查阅头文件los_trace.h中的#TYPE#_PARAMS。

params:表示的事件参数可查阅头文件los_trace.h中的#TYPE#_PARAMS。

下面以序号为0的输出项为例,进行说明。

Index Time(cycles) EventType CurTask Identity params

0 0x366d5e88 0x45 0x1 0x0 0x1f 0x4

Time cycles可换算成时间,换算公式为cycles/clockFreq,单位为s。

0x45为TASK_SWITCH即任务切换事件,当前运行的任务taskid为0x1。

Identity和params的含义需要查看TASK_SWITCH_PARAMS宏定义:

\#define TASK_SWITCH_PARAMS(taskId, oldPriority, oldTaskStatus, newPriority, newTaskStatus) \\

taskId, oldPriority, oldTaskStatus, newPriority, newTaskStatus

因为#TYPE#_PARAMS(IDENTITY, parma1…) IDENTITY, …,所以Identity为taskId(0x0),第一个参数为oldPriority(0x1f)

说明

params的个数由menuconfig中Enable Trace Feature --> Record max params配置,默认为3,超出的参数不会被记录,用户应自行合理配置该值。

综上所述,任务由0x1切换到0x0,0x1任务的优先级为0x1f,状态为0x4,0x0任务的优先级为0x0。

5.2.4.2 用户态开发流程

通过在menuconfig配置"Driver->Enable TRACE DRIVER",开启Trace驱动。该配置仅在内核Enable Trace Feature后,才可在Driver的选项中可见。

打开“/dev/trace”字符文件,进行读写和IOCTL操作;

系统提供用户态的trace命令,该命令位于/bin目录下,cd bin 后可执行如下命令:

./trace reset 清除Trace记录

./trace mask num 设置Trace事件过滤掩码

./trace start 启动Trace

./trace stop 停止Trace

./trace dump 0/1 格式化输出Trace记录

./trace read nBytes 读取Trace记录

./trace write type id params… 写用户态事件 如:./trace write 0x1 0x1001 0x2 0x3 则写入一条用户态事件,其事件类型为0x1, id为0x1001,参数有2个,分别是0x2和0x3.

用户态命令行的典型使用方法如下:

./trace start

./trace write 0x1 0x1001 0x2 0x3

./trace stop

./trace dump 0

用户态编程实例
本实例实现如下功能:

  • 打开trace字符设备。
  • 设置事件掩码。
  • 启动trace 写trace事件。
  • 停止trace。
  • 格式化输出trace数据。

用户态示例代码

实例代码如下

\#include \<stdio.h\>

\#include \<stdlib.h\>

\#include \<sys/types.h\>

\#include \<sys/stat.h\>

\#include \<sys/ioctl.h\>

\#include \<fcntl.h\>

\#include \<unistd.h\>

\#include \<sys/mman.h\>

\#include \<stdint.h\>

\#define TRACE_IOC_MAGIC 'T'

\#define TRACE_START \_IO(TRACE_IOC_MAGIC, 1)

\#define TRACE_STOP \_IO(TRACE_IOC_MAGIC, 2)

\#define TRACE_RESET \_IO(TRACE_IOC_MAGIC, 3)

\#define TRACE_DUMP \_IO(TRACE_IOC_MAGIC, 4)

\#define TRACE_SET_MASK \_IO(TRACE_IOC_MAGIC, 5)

\#define TRACE_USR_MAX_PARAMS 3

typedef struct {

unsigned int eventType;

uintptr_t identity;

uintptr_t params[TRACE_USR_MAX_PARAMS];

} UsrEventInfo;

int main(int argc, char \*\*argv)

{

size_t mask;

int fd = open("/dev/trace", O_RDWR);

if (fd == -1) {

printf("Trace open failed.\\n");

exit(EXIT_FAILURE);

}

ioctl(fd, TRACE_STOP, NULL);

ioctl(fd, TRACE_RESET, NULL); /\* clear all events \*/

mask = 0x10000; /\* filter kernel events \*/

ioctl(fd, TRACE_SET_MASK, mask);

ioctl(fd, TRACE_START, NULL); /\* start trace \*/

sleep(1);

UsrEventInfo info = {

.eventType = 0x1,

.identity = 0x0001,

.params = {1, 2, 3},

};

(void)write(fd, \&info, sizeof(UsrEventInfo));

info.eventType = 0x2;

info.identity = 0x0002;

(void)write(fd, \&info, sizeof(UsrEventInfo));

ioctl(fd, TRACE_STOP, NULL);

ioctl(fd, TRACE_DUMP, 0);

close(fd);

return 0;

}

用户态结果验证

输出结果如下:

\*\*\*\*\*\*\*TraceInfo begin\*\*\*\*\*\*\*

clockFreq = 50000000

CurEvtIndex = 2

Index Time(cycles) EventType CurTask Identity params

0 0x366d5e88 0xfffffff1 0x1 0x1 0x1 0x2 0x3

1 0x366d74ae 0xfffffff2 0x1 0x2 0x1 0x2 0x3

\*\*\*\*\*\*\*TraceInfo end\*\*\*\*\*\*\*

示例代码中调用了2次write,对应产生2条Trace记录; 用户层事件的EventType高28位固定均为1(即0xfffffff0),仅低4位表示具体的事件类型。

5.3 CPU占有率

5.3.1 基本概念

CPU(中央处理器,Central Processing Unit)占用率分为系统CPU占用率、进程CPU占用率、任务CPU占用率和中断CPU占用率。用户通过系统级的CPU占用率,判断当前系统负载是否超出设计规格。通过系统中各个进程/任务/中断的CPU占用情况,判断各个进程/任务/中断的CPU占用率是否符合设计的预期。

系统CPU占用率(CPU Percent):指周期时间内系统的CPU占用率,用于表示系统一段时间内的闲忙程度,也表示CPU的负载情况。系统CPU占用率的有效表示范围为0~100,其精度(可通过配置调整)为百分比。100表示系统满负荷运转。

进程CPU占用率:指单个进程的CPU占用率,用于表示单个进程在一段时间内的闲忙程度。进程CPU占用率的有效表示范围为0~100,其精度(可通过配置调整)为百分比。100表示在一段时间内系统一直在运行该进程。

任务CPU占用率:指单个任务的CPU占用率,用于表示单个任务在一段时间内的闲忙程度。任务CPU占用率的有效表示范围为0~100,其精度(可通过配置调整)为百分比。100表示在一段时间内系统一直在运行该任务。

中断CPU占用率:指单个中断的CPU占用率,用于表示单个中断在一段时间内的闲忙程度。中断CPU占用率的有效表示范围为0~100,其精度(可通过配置调整)为百分比。100表示在一段时间内系统一直在运行该中断。

5.3.2 运行机制

OpenHarmony LiteOS-A内核CPUP(CPU Percent,CPU占用率)模块采用进程、任务和中断级记录的方式,在进程/任务切换时,记录进程/任务启动时间,进程/任务切出或者退出时,系统会累加整个进程/任务的占用时间; 在执行中断时系统会累加记录每个中断的执行时间。

OpenHarmony 提供以下四种CPU占用率的信息查询:

系统CPU占用率

进程CPU占用率

任务CPU占用率

中断CPU占用率

CPU占用率的计算方法:

系统CPU占用率=系统中除idle任务外其他任务运行总时间/系统运行总时间

进程CPU占用率=进程运行总时间/系统运行总时间

任务CPU占用率=任务运行总时间/系统运行总时间

中断CPU占用率=中断运行总时间/系统运行总时间

5.3.3 接口说明
表82 接口说明表
功能分类接口名称描述
系统CPU占用率LOS_HistorySysCpuUsage获取系统历史CPU占用率
进程CPU占用率LOS_HistoryProcessCpuUsage获取指定进程历史CPU占用率
LOS_GetAllProcessCpuUsage获取系统所有进程的历史CPU占用率
任务CPU占用率LOS_HistoryTaskCpuUsage获取指定任务历史CPU占用率
中断CPU占用率LOS_GetAllIrqCpuUsage获取系统所有中断的历史CPU占用率
5.3.4 开发流程

CPU占用率的典型开发流程:

1调用获取系统历史CPU占用率函数LOS_HistorySysCpuUsage。

2调用获取指定进程历史CPU占用率函数LOS_HistoryProcessCpuUsage。

1)若进程已创建,则关中断,根据不同模式正常获取,恢复中断;

2)若进程未创建,则返回错误码;

3调用获取所有进程CPU占用率函数LOS_GetAllProcessCpuUsage。

1)若CPUP已初始化,则关中断,根据不同模式正常获取,恢复中断;

2)若CPUP未初始化或有非法入参,则返回错误码;

4调用获取指定任务历史CPU占用率函数LOS_HistoryTaskCpuUsage。

1)若任务已创建,则关中断,根据不同模式正常获取,恢复中断;

2)若任务未创建,则返回错误码;

5调用获取所有中断CPU占用率函数LOS_GetAllIrqCpuUsage。

1)若CPUP已初始化,则关中断,根据不同模式正常获取,恢复中断;

2)若CPUP未初始化或有非法入参,则返回错误码;

5.3.5编程实例

本实例实现如下功能:

创建一个用于CPUP测试的任务。

获取当前系统CPUP。

以不同模式获取历史系统CPUP。

获取创建的测试任务的CPUP。

以不同模式获取创建的测试任务的CPUP。

前提条件:

在menuconfig 配置中打开cpup控制开关。

示例代码

代码实现如下:

\#include "los_task.h"

\#include "los_cpup.h"

\#define MODE 4

UINT32 g_cpuTestTaskID;

VOID ExampleCpup(VOID)

{

printf("entry cpup test example\\n");

while(1) {

usleep(100);

}

}

UINT32 ItCpupTest(VOID)

{

UINT32 ret;

UINT32 cpupUse;

TSK_INIT_PARAM_S cpupTestTask = { 0 };

memset(&cpupTestTask, 0, sizeof(TSK_INIT_PARAM_S));

cpupTestTask.pfnTaskEntry = (TSK_ENTRY_FUNC)ExampleCpup;

cpupTestTask.pcName = "TestCpupTsk";

cpupTestTask.uwStackSize = 0x800;

cpupTestTask.usTaskPrio = 5;

ret = LOS_TaskCreate(&g_cpuTestTaskID, \&cpupTestTask);

if(ret != LOS_OK) {

printf("cpupTestTask create failed .\\n");

return LOS_NOK;

}

usleep(100);

/\* 获取当前系统历史cpu占用率 \*/

cpupUse = LOS_HistorySysCpuUsage(CPU_LESS_THAN_1S);

printf("the history system cpu usage in all time:%u.%u\\n",

cpupUse / LOS_CPUP_PRECISION_MULT, cpupUse % LOS_CPUP_PRECISION_MULT);

/\* 获取指定任务的cpu占用率,该测试例程中指定的任务为以上创建的cpup测试任务 \*/

cpupUse = LOS_HistoryTaskCpuUsage(g_cpuTestTaskID, CPU_LESS_THAN_1S);

printf("cpu usage of the cpupTestTask in all time:\\n TaskID: %d\\n usage: %u.%u\\n",

g_cpuTestTaskID, cpupUse / LOS_CPUP_PRECISION_MULT, cpupUse % LOS_CPUP_PRECISION_MULT);

return LOS_OK;

}

结果验证

编译运行得到的结果为:

entry cpup test example

the history system cpu usage in all time: 3.0

cpu usage of the cpupTestTask in all time: TaskID:10 usage: 0.0

5.4内存信息统计

5.4.1基本概念

内存信息包括内存池大小、内存使用量、剩余内存大小、最大空闲内存、内存水线、内存节点数统计、碎片率等。

内存水线:即内存池的最大使用量,每次申请和释放时,都会更新水线值,实际业务可根据该值,优化内存池大小;

碎片率:衡量内存池的碎片化程度,碎片率高表现为内存池剩余内存很多,但是最大空闲内存块很小,可以用公式(fragment=100-100*最大空闲内存块大小/剩余内存大小)来度量

其他统计信息:调用接口LOS_MemInfoGet时,会扫描内存池的节点信息,统计出相关信息。

LOSCFG_MEM_WATERLINE:开关宏,默认关闭;若需要打开这个功能,可以在配置项中开启“Debug-> Enable memory pool waterline or not”。如需获取内存水线,需要打开该配置。

5.4.2 开发流程

关键结构体介绍:

typedef struct {

UINT32 totalUsedSize; // 内存池的内存使用量

UINT32 totalFreeSize; // 内存池的剩余内存大小

UINT32 maxFreeNodeSize; // 内存池的最大空闲内存块大小

UINT32 usedNodeNum; // 内存池的非空闲内存块个数

UINT32 freeNodeNum; // 内存池的空闲内存块个数

\#if (LOSCFG_MEM_WATERLINE == 1) // 默认关闭,可以通过menuconfig配置工具打开

UINT32 usageWaterLine; // 内存池的水线值

\#endif

} LOS_MEM_POOL_STATUS;

内存水线获取:调用LOS_MemInfoGet接口,第1个参数是内存池首地址,第2个参数是LOS_MEM_POOL_STATUS类型的句柄,其中字段usageWaterLine即水线值。

内存碎片率计算:同样调用LOS_MemInfoGet接口,可以获取内存池的剩余内存大小和最大空闲内存块大小,然后根据公式(fragment=100-100*最大空闲内存块大小/剩余内存大小)得出此时的动态内存池碎片率。

5.4.3编程实例

本实例实现如下功能:

创建一个监控任务,用于获取内存池的信息;

调用LOS_MemInfoGet接口,获取内存池的基础信息;

利用公式算出使用率及碎片率。

代码实现如下:

\#include \<stdio.h\>

\#include \<string.h\>

\#include "los_task.h"

\#include "los_memory.h"

\#include "los_config.h"

void MemInfoTaskFunc(void)

{

LOS_MEM_POOL_STATUS poolStatus = {0};

/\* pool为要统计信息的内存地址,此处以OS_SYS_MEM_ADDR为例 \*/

void \*pool = OS_SYS_MEM_ADDR;

LOS_MemInfoGet(pool, \&poolStatus);

/\* 算出内存池当前的碎片率百分比 \*/

unsigned char fragment = 100 - poolStatus.maxFreeNodeSize \* 100 / poolStatus.totalFreeSize;

/\* 算出内存池当前的使用率百分比 \*/

unsigned char usage = LOS_MemTotalUsedGet(pool) \* 100 / LOS_MemPoolSizeGet(pool);

printf("usage = %d, fragment = %d, maxFreeSize = %d, totalFreeSize = %d, waterLine = %d\\n", usage, fragment, poolStatus.maxFreeNodeSize,

poolStatus.totalFreeSize, poolStatus.usageWaterLine);

}

int MemTest(void)

{

unsigned int ret;

unsigned int taskID;

TSK_INIT_PARAM_S taskStatus = {0};

taskStatus.pfnTaskEntry = (TSK_ENTRY_FUNC)MemInfoTaskFunc;

taskStatus.uwStackSize = 0x1000;

taskStatus.pcName = "memInfo";

taskStatus.usTaskPrio = 10;

ret = LOS_TaskCreate(&taskID, \&taskStatus);

if (ret != LOS_OK) {

printf("task create failed\\n");

return -1;

}

return 0;

}

结果验证

编译运行输出的结果如下:

usage = 22, fragment = 3, maxFreeSize = 49056, totalFreeSize = 50132, waterLine = 1414

5.5 内存泄漏检测

5.5.1 基本概念

内存泄漏检测机制作为内核的可选功能,用于辅助定位动态内存泄漏问题。开启该动能,动态内存机制会自动记录申请内存时的函数调用关系(下文简称LR)。如果出现泄漏,就可以利用这些记录的信息,找到内存申请的地方,方便进一步确认。

5.5.2 功能配置

LOSCFG_MEM_LEAKCHECK:开关宏,默认关闭;如需要打开这个功能,可以在配置项中开启“Debug-> Enable Function call stack of Mem operation recorded”。

LOS_RECORD_LR_CNT:记录的LR层数,默认3层;每层LR消耗sizeof(void *)字节数的内存。

LOS_OMIT_LR_CNT:忽略的LR层数,默认2层,即从调用LOS_MemAlloc的函数开始记录,可根据实际情况调整。为啥需要这个配置?有3点原因如下:

LOS_MemAlloc接口内部也有函数调用;

外部可能对LOS_MemAlloc接口有封装;

LOS_RECORD_LR_CNT 配置的LR层数有限;

正确配置这个宏,将无效的LR层数忽略,就可以记录有效的LR层数,节省内存消耗。

5.5.3 开发流程

该调测功能可以分析关键的代码逻辑中是否存在内存泄漏。开启这个功能,每次申请内存时,会记录LR信息。在需要检测的代码段前后,调用LOS_MemUsedNodeShow接口,每次都会打印指定内存池已使用的全部节点信息,对比前后两次的节点信息,新增的节点信息就是疑似泄漏的内存节点。通过LR,可以找到具体申请的代码位置,进一步确认是否泄漏。

调用LOS_MemUsedNodeShow接口输出的节点信息格式如下:每1行为一个节点信息;第1列为节点地址,可以根据这个地址,使用GDB等手段查看节点完整信息;第2列为节点的大小,等于节点头大小+数据域大小;第3~5列为函数调用关系LR地址,可以根据这个值,结合汇编文件,查看该节点具体申请的位置。

node size LR[0] LR[1] LR[2]

0x10017320: 0x528 0x9b004eba 0x9b004f60 0x9b005002

0x10017848: 0xe0 0x9b02c24e 0x9b02c246 0x9b008ef0

0x10017928: 0x50 0x9b008ed0 0x9b068902 0x9b0687c4

0x10017978: 0x24 0x9b008ed0 0x9b068924 0x9b0687c4

0x1001799c: 0x30 0x9b02c24e 0x9b02c246 0x9b008ef0

0x100179cc: 0x5c 0x9b02c24e 0x9b02c246 0x9b008ef0

注: 开启内存检测会影响内存申请的性能,且每个内存节点都会记录LR地址,内存开销也加大。

5.5.4编程实例

本实例实现如下功能:构建内存泄漏代码段。

调用OsMemUsedNodeShow接口,输出全部节点信息打印;

申请内存,但没有释放,模拟内存泄漏;

再次调用OsMemUsedNodeShow接口,输出全部节点信息打印;

将两次log进行对比,得出泄漏的节点信息;

通过LR地址,找出泄漏的代码位置;

示例代码

代码实现如下:

\#include \<stdio.h\>

\#include \<string.h\>

\#include "los_memory.h"

\#include "los_config.h"

void MemLeakTest(void)

{

OsMemUsedNodeShow(LOSCFG_SYS_HEAP_ADDR);

void \*ptr1 = LOS_MemAlloc(LOSCFG_SYS_HEAP_ADDR, 8);

void \*ptr2 = LOS_MemAlloc(LOSCFG_SYS_HEAP_ADDR, 8);

OsMemUsedNodeShow(LOSCFG_SYS_HEAP_ADDR);

}

结果验证

编译运行输出log如下:

node size LR[0] LR[1] LR[2]

0x20001b04: 0x24 0x08001a10 0x080035ce 0x080028fc

0x20002058: 0x40 0x08002fe8 0x08003626 0x080028fc

0x200022ac: 0x40 0x08000e0c 0x08000e56 0x0800359e

0x20002594: 0x120 0x08000e0c 0x08000e56 0x08000c8a

0x20002aac: 0x56 0x08000e0c 0x08000e56 0x08004220

node size LR[0] LR[1] LR[2]

0x20001b04: 0x24 0x08001a10 0x080035ce 0x080028fc

0x20002058: 0x40 0x08002fe8 0x08003626 0x080028fc

0x200022ac: 0x40 0x08000e0c 0x08000e56 0x0800359e

0x20002594: 0x120 0x08000e0c 0x08000e56 0x08000c8a

0x20002aac: 0x56 0x08000e0c 0x08000e56 0x08004220

0x20003ac4: 0x1d 0x08001458 0x080014e0 0x080041e6

0x20003ae0: 0x1d 0x080041ee 0x08000cc2 0x00000000

对比两次log,差异如下,这些内存节点就是疑似泄漏的内存块:

0x20003ac4: 0x1d 0x08001458 0x080014e0 0x080041e6

0x20003ae0: 0x1d 0x080041ee 0x08000cc2 0x00000000

部分汇编文件如下:

MemLeakTest:

0x80041d4: 0xb510 PUSH {R4, LR}

0x80041d6: 0x4ca8 LDR.N R4, [PC, \#0x2a0] ; g_memStart

0x80041d8: 0x0020 MOVS R0, R4

0x80041da: 0xf7fd 0xf93e BL LOS_MemUsedNodeShow ; 0x800145a

0x80041de: 0x2108 MOVS R1, \#8

0x80041e0: 0x0020 MOVS R0, R4

0x80041e2: 0xf7fd 0xfbd9 BL LOS_MemAlloc ; 0x8001998

0x80041e6: 0x2108 MOVS R1, \#8

0x80041e8: 0x0020 MOVS R0, R4

0x80041ea: 0xf7fd 0xfbd5 BL LOS_MemAlloc ; 0x8001998

0x80041ee: 0x0020 MOVS R0, R4

0x80041f0: 0xf7fd 0xf933 BL LOS_MemUsedNodeShow ; 0x800145a

0x80041f4: 0xbd10 POP {R4, PC}

0x80041f6: 0x0000 MOVS R0, R0

其中,通过查找0x080041ee,就可以发现该内存节点是在MemLeakTest接口里申请的且是没有释放的。

5.6 踩内存检测

5.6.1基础概念

踩内存检测机制作为内核的可选功能,用于检测动态内存池的完整性。通过该机制,可以及时发现内存池是否发生了踩内存问题,并给出错误信息,便于及时发现系统问题,提高问题解决效率,降低问题定位成本。

5.6.2功能配置

LOSCFG_BASE_MEM_NODE_INTEGRITY_CHECK:开关宏,默认关闭;若打开这个功能,可以在配置项中开启“Debug-> Enable integrity check or not”。

1、开启这个功能,每次申请内存,会实时检测内存池的完整性。

2、如果不开启该功能,也可以调用LOS_MemIntegrityCheck接口检测,但是每次申请内存时,不会实时检测内存完整性,而且由于节点头没有魔鬼数字(开启时才有,省内存),检测的准确性也会相应降低,但对于系统的性能没有影响,故根据实际情况开关该功能。

由于该功能只会检测出哪个内存节点被破坏了,并给出前节点信息(因为内存分布是连续的,当前节点最有可能被前节点破坏)。如果要进一步确认前节点在哪里申请的,需开启内存泄漏检测功能,通过LR记录,辅助定位。

注意: 开启该功能,节点头多了魔鬼数字字段,会增大节点头大小。由于实时检测完整性,故性能影响较大;若性能敏感的场景,可以不开启该功能,使用LOS_MemIntegrityCheck接口检测。

5.6.3开发流程

通过调用LOS_MemIntegrityCheck接口检测内存池是否发生了踩内存,如果没有踩内存问题,那么接口返回0且没有log输出;如果存在踩内存问题,那么会输出相关log,详见下文编程实例的结果输出。

5.6.4编程实例

本实例实现如下功能:

申请两个物理上连续的内存块;

通过memset构造越界访问,踩到下个节点的头4个字节;

调用LOS_MemIntegrityCheck检测是否发生踩内存。

示例代码

代码实现如下:

\#include \<stdio.h\>

\#include \<string.h\>

\#include "los_memory.h"

\#include "los_config.h"

void MemIntegrityTest(void)

{

/\* 申请两个物理连续的内存块 \*/

void \*ptr1 = LOS_MemAlloc(LOSCFG_SYS_HEAP_ADDR, 8);

void \*ptr2 = LOS_MemAlloc(LOSCFG_SYS_HEAP_ADDR, 8);

/\* 第一个节点内存块大小是8字节,那么12字节的清零,会踩到第二个内存节点的节点头,构造踩内存场景 \*/

memset(ptr1, 0, 8 + 4);

LOS_MemIntegrityCheck(LOSCFG_SYS_HEAP_ADDR);

}

结果验证

编译运行输出log如下:

[ERR][OsMemMagicCheckPrint], 2028, memory check error!

memory used but magic num wrong, magic num = 0x00000000 /\* 提示信息,检测到哪个字段被破坏了,用例构造了将下个节点的头4个字节清零,即魔鬼数字字段 \*/

broken node head: 0x20003af0 0x00000000 0x80000020, prev node head: 0x20002ad4 0xabcddcba 0x80000020

/\* 被破坏节点和其前节点关键字段信息,分别为其前节点地址、节点的魔鬼数字、节点的sizeAndFlag;可以看出被破坏节点的魔鬼数字字段被清零,符合用例场景 \*/

broken node head LR info: /\* 节点的LR信息需要开启内存检测功能才有有效输出 \*/

LR[0]:0x0800414e

LR[1]:0x08000cc2

LR[2]:0x00000000

pre node head LR info: /\* 通过LR信息,可以在汇编文件中查找前节点是哪里申请,然后排查其使用的准确性 \*/

LR[0]:0x08004144

LR[1]:0x08000cc2

LR[2]:0x00000000

[ERR]Memory interity check error, cur node: 0x20003b10, pre node: 0x20003af0 /\* 被破坏节点和其前节点的地址 \*/

5.7 临终遗言

5.7.1使用场景

在无串口的设备上,将系统异常时打印的信息保存到不丢失的存储介质上,方便对运行时问题进行定位。

5.7.2功能说明

该调测功能提供了一种用于保存系统异常时打印信息到不丢失存储介质中的机制,用户可自行注册读写异常时打印信息的钩子函数,实现在不同存储介质上保存异常信息的能力,这样方便无串口的设备的问题定位。接口名为LOS_ExcInfoRegHook,该函数声明在los_config.h中,函数原型:

typedef VOID (\*log_read_write_fn)(UINT32 startAddr, UINT32 space, UINT32 rwFlag, CHAR \*buf);

......

VOID LOS_ExcInfoRegHook(UINT32 startAddr, UINT32 space, CHAR \*buf, log_read_write_fn hook);
5.7.3参数说明
表 83LOS_ExcInfoRegHook 参数说明
参数参数说明
startAddr存取异常信息的物理介质起始地址
space存取的空间大小
buf存取异常信息的内存缓冲区
log_read_write_fn存取异常信息的函数
表84 log_read_write_fn 参数说明
参数参数说明
startAddr存取异常信息的物理介质起始地址
space存取的空间大小
rwFlag读写标记,0为写,1为读
buf存取异常信息的内存缓冲区
5.7.4 开发流程

该功能依赖于宏LOSCFG_SAVE_EXCINFO,使用临终遗言功能时,在配置项中开启“ Enable Saving Exception Information ”:Debug-> Enable Saving Exception Information ;若关闭该选项,则该功能失效。功能开启后,可在SystemInit中调用LOS_ExcInfoRegHook来注册存取异常信息的位置、大小、内存缓冲区以及存取函数。当系统进入异常时,会将异常时系统各类信息先保存在注册时传入的内存缓冲区中,最后调用注册的存取函数,将异常信息写入到物理存储介质中。

注:

注册的存取位置不要跟其他存储重叠。

注册的内存缓冲区不能太小,建议不低于16KiB,否则异常信息会存储不完整。

注册的读写函数对应的具体存储介质的驱动功能正常,才能保证存取功能正常。

5.8 常见问题定位方法

5.8.1 通过异常信息定位问题

系统异常被挂起后,会在串口看到一些关键寄存器的信息,如图所示。可通过这些信息定位到异常所在函数和其调用栈关系,为原因分析提供第一手资料。

在这里插入图片描述

图77 异常信息

上图中的异常信息主要解释4个标签:

标签1:标识异常在内核态;

标签2:标识了异常类型(数据异常时,far后的值是系统异常时CPU访问的地址);

标签3:pc的值标识系统异常时执行指令的位置,klr的值一般标识pc所在函数执行完后下一条要执行的命令。(注:标签4处 traceback 0 lr有值时不用关注klr)。

标签4:lr 的值依次标识正常情况下PC要依次执行的指令的位置。

对于内核异常打印信息,确定PC和LR的具体位置的指令需要结合out目录下OHOS_Image.asm(跟烧写的系统镜像OHOS_Image.bin对应的汇编文件)查看,根据指令所在的位置可确认使用该指令的函数,依次确定LR位置所在的函数,即得到异常发生时的函数调用关系。

5.8.2内存池完整性验证

仅凭上节异常信息定位的基本方法,常常无法直接定位问题所在。并且常常会因为异常的寄存器值而无法对问题进行定位。若怀疑是堆内存越界导致的问题,可以调用内存池完整性检测函数LOS_MemIntegrityCheck进行检查。函数LOS_MemIntegrityCheck将会对系统动态内存池所有的节点进行遍历,如果所有节点正常则函数返回0,不会有任何打印,否则将打印相关的错误信息。该函数的入参使用(VOID *)OS_SYS_MEM_ADDR。

定位堆内存越界踩的问题,一般是在可能存在问题的业务逻辑代码前后使用LOS_MemIntegrityCheck,如果该业务代码不存在问题,则前后两次LOS_MemIntegrityCheck调用不会失败,按前述方式逐步缩小问题定位范围。

5.8.3全局变量踩内存定位方法

如果已知一个全局变量内存域被踩,可在OHOS_Image.map文件中找到该全局变量所在的地址,并且特别注意该地址之前最近被使用的变量,有极大概率是前面的变量(尤其数组类型的或会被强转成其他类型的变量)在使用的过程中内存越界,破坏了这个全局变量。

6基本数据结构

6.1双向链表

6.1.1 基本概念

双向链表是指含有往前和往后两个方向的链表,即每个结点中除存放下一个节点指针外,还增加一个指向前一个节点的指针。其头指针head是唯一确定的。从双向链表中的任意一个结点开始,都可以很方便地访问它的前驱结点和后继结点,这种数据结构形式使得双向链表在查找时更加方便,特别是大量数据的遍历。由于双向链表具有对称性,能方便地完成各种插入、删除等操作,但需要注意前后方向的操作。

6.1.2功能说明

双向链表模块为用户提供下面几种功能,接口详细信息可以查看API参考。

表85 接口说明表
功能分类接口名描述
初始化链表LOS_ListInit将指定节点初始化为双向链表节点
LOS_DL_LIST_HEAD定义一个节点并初始化为双向链表节点
增加节点LOS_ListAdd将指定节点插入到双向链表头端
LOS_ListHeadInsert将指定节点插入到双向链表头端,同LOS_ListAdd
LOS_ListTailInsert将指定节点插入到双向链表尾端
增加链表LOS_ListAddList将指定链表的头端插入到双向链表头端
LOS_ListHeadInsertList将指定链表的头端插入到双向链表头端,同LOS_ListAddList
LOS_ListTailInsertList将指定链表的尾端插入到双向链表头端
删除节点LOS_ListDelete将指定节点从链表中删除
LOS_ListDelInit将指定节点从链表中删除,并使用该节点初始化链表
判断双向链表LOS_ListEmpty判断链表是否为空
LOS_DL_LIST_IS_END判断指定链表节点是否为链表尾端
LOS_DL_LIST_IS_ON_QUEUE判断链表节点是否在双向链表里
获取结构体信息LOS_OFF_SET_OF获取指定结构体内的成员相对于结构体起始地址的偏移量
LOS_DL_LIST_ENTRY获取双向链表中第一个链表节点所在的结构体地址,接口的第一个入参表示的是链表中的头节点,第二个入参是要获取的结构体名称,第三个入参是链表在该结构体中的名称
LOS_ListPeekHeadType获取双向链表中第一个链表节点所在的结构体地址,接口的第一个入参表示的是链表中的头节点,第二个入参是要获取的结构体名称,第三个入参是链表在该结构体中的名称。如果链表为空,返回NULL。
LOS_ListRemoveHeadType获取双向链表中第一个链表节点所在的结构体地址,并把第一个链表节点从链表中删除。接口的第一个入参表示的是链表中的头节点,第二个入参是要获取的结构体名称,第三个入参是链表在该结构体中的名称。如果链表为空,返回NULL。
LOS_ListNextType获取双向链表中指定链表节点的下一个节点所在的结构体地址。接口的第一个入参表示的是链表中的头节点,第二个入参是指定的链表节点,第三个入参是要获取的结构体名称,第四个入参是链表在该结构体中的名称。如果链表节点下一个为链表头结点为空,返回NULL。
遍历双向链表LOS_DL_LIST_FOR_EACH遍历双向链表
LOS_DL_LIST_FOR_EACH_SAFE遍历双向链表,并存储当前节点的后继节点用于安全校验
遍历包含双向链表的结构体LOS_DL_LIST_FOR_EACH_ENTRY遍历指定双向链表,获取包含该链表节点的结构体地址
LOS_DL_LIST_FOR_EACH_ENTRY_SAFE遍历指定双向链表,获取包含该链表节点的结构体地址,并存储包含当前节点的后继节点的结构体地址
6.1.3开发流程

双向链表的典型开发流程:

调用LOS_ListInit/LOS_DL_LIST_HEAD初始双向链表。

调用LOS_ListAdd向链表插入节点。

调用LOS_ListTailInsert向链表尾部插入节点。

调用LOS_ListDelete删除指定节点。

调用LOS_ListEmpty判断链表是否为空。

调用LOS_ListDelInit删除指定节点并以此节点初始化链表。

注:

需要注意节点指针前后方向的操作。

链表操作接口,为底层接口,不对入参进行判空,需要使用者确保传参合法。

如果链表节点的内存是动态申请的,删除节点时,要注意释放内存。

6.1.4编程实例

本实例实现如下功能:

初始化双向链表。

增加节点。

删除节点。

测试操作是否成功。

\#include "stdio.h"

\#include "los_list.h"

static UINT32 ListSample(VOID)

{

LOS_DL_LIST listHead = {NULL,NULL};

LOS_DL_LIST listNode1 = {NULL,NULL};

LOS_DL_LIST listNode2 = {NULL,NULL};

//首先初始化链表

PRINTK("Initial head\\n");

LOS_ListInit(&listHead);

//添加节点1和节点2,并校验他们的相互关系

LOS_ListAdd(&listHead, \&listNode1);

if (listNode1.pstNext == \&listHead && listNode1.pstPrev == \&listHead) {

PRINTK("Add listNode1 success\\n");

}

LOS_ListTailInsert(&listHead, \&listNode2);

if (listNode2.pstNext == \&listHead && listNode2.pstPrev == \&listNode1) {

PRINTK("Tail insert listNode2 success\\n");

}

//删除两个节点

LOS_ListDelete(&listNode1);

LOS_ListDelete(&listNode2);

//确认链表为空

if (LOS_ListEmpty(&listHead)) {

PRINTK("Delete success\\n");

}

return LOS_OK;

}

结果验证

编译运行得到的结果为:

Initial head

Add listNode1 success

Tail insert listNode2 success

Delete success

6.2位操作

6.2.1 基本概念

位操作是指对二进制数的bit位进行操作。程序可以设置某一变量为状态字,状态字中的每一bit位(标志位)可以具有自定义的含义。

6.2.2功能说明

系统提供标志位的置1和清0操作,可以改变标志位的内容,同时还提供获取状态字中标志位为1的最高位和最低位的功能。用户也可以对系统的寄存器进行位操作。位操作模块为用户提供下面几种功能,接口详细信息可以查看API参考。

表 86位操作模块接口
功能分类接口名称描述
置1/清0标志位LOS_BitmapSet对状态字的某一标志位进行置1操作
LOS_BitmapClr对状态字的某一标志位进行清0操作
获取标志位为1的bit位LOS_HighBitGet获取状态字中为1的最高位
LOS_LowBitGet获取状态字中为1的最低位
连续bit位操作LOS_BitmapSetNBits对状态字的连续标志位进行置1操作
LOS_BitmapClrNBits对状态字的连续标志位进行清0操作
LOS_BitmapFfz获取从最低有效位开始的第一个0的bit位
6.2.3 编程实例

实例描述

对数据实现位操作,本实例实现如下功能:

某一标志位置1。

获取标志位为1的最高bit位。

某一标志位清0。

获取标志位为1的最低bit位。

\#include "los_bitmap.h"

\#include "los_printf.h"

static UINT32 BitSample(VOID)

{

UINT32 flag = 0x10101010;

UINT16 pos;

PRINTK("\\nBitmap Sample!\\n");

PRINTK("The flag is 0x%8x\\n", flag);

pos = 8;

LOS_BitmapSet(&flag, pos);

PRINTK("LOS_BitmapSet:\\t pos : %d, the flag is 0x%0+8x\\n", pos, flag);

pos = LOS_HighBitGet(flag);

PRINTK("LOS_HighBitGet:\\t The highest one bit is %d, the flag is 0x%0+8x\\n", pos, flag);

LOS_BitmapClr(&flag, pos);

PRINTK("LOS_BitmapClr:\\t pos : %d, the flag is 0x%0+8x\\n", pos, flag);

pos = LOS_LowBitGet(flag);

PRINTK("LOS_LowBitGet:\\t The lowest one bit is %d, the flag is 0x%0+8x\\n\\n", pos, flag);

return LOS_OK;

}

结果验证

编译运行得到的结果为:

Bitmap Sample!

The flag is 0x10101010

LOS_BitmapSet: pos : 8, the flag is 0x10101110

LOS_HighBitGet:The highest one bit is 28, the flag is 0x10101110

LOS_BitmapClr: pos : 28, the flag is 0x00101110

LOS_LowBitGet: The lowest one bit is 4, the flag is 0x00101110

7标准库

OpenHarmony内核使用musl libc库,支持标准POSIX接口,开发者可基于POSIX标准接口开发内核之上的组件及应用。

7.1 标准库接口框架

musl libc库支持POSIX标准,涉及的系统调用相关接口由OpenHarmony内核适配支持 ,以满足接口对外描述的功能要求。

标准库支持接口的详细情况请参考C库的API文档,其中也涵盖了与POSIX标准之间的差异说明。

在这里插入图片描述

图 78 POSIX接口框架

7.2 操作实例

在本示例中,主线程创建了THREAD_NUM个子线程,每个子线程启动后等待被主线程唤醒,主线程成功唤醒所有子线程后,子线程继续执行直至生命周期结束,同时主线程通过pthread_join方法等待所有线程执行结束。

\#include \<stdio.h\>

\#include \<unistd.h\>

\#include \<pthread.h\>

\#ifdef \__cplusplus

\#if \__cplusplus

extern "C" {

\#endif /\* \__cplusplus \*/

\#endif /\* \__cplusplus \*/

\#define THREAD_NUM 3

int g_startNum = 0; /\* 启动的线程数 \*/

int g_wakenNum = 0; /\* 唤醒的线程数 \*/

struct testdata {

pthread_mutex_t mutex;

pthread_cond_t cond;

} g_td;

/\*

\* 子线程入口函数

\*/

static void \*ChildThreadFunc(void \*arg)

{

int rc;

pthread_t self = pthread_self();

/\* 获取mutex锁 \*/

rc = pthread_mutex_lock(&g_td.mutex);

if (rc != 0) {

printf("ERROR:take mutex lock failed, error code is %d!\\n", rc);

goto EXIT;

}

/\* g_startNum计数加一,用于统计已经获得mutex锁的子线程个数 \*/

g_startNum++;

/\* 等待cond条件变量 \*/

rc = pthread_cond_wait(&g_td.cond, \&g_td.mutex);

if (rc != 0) {

printf("ERROR: pthread condition wait failed, error code is %d!\\n", rc);

(void)pthread_mutex_unlock(&g_td.mutex);

goto EXIT;

}

/\* 尝试获取mutex锁,正常场景,此处无法获取锁 \*/

rc = pthread_mutex_trylock(&g_td.mutex);

if (rc == 0) {

printf("ERROR: mutex gets an abnormal lock!\\n");

goto EXIT;

}

/\* g_wakenNum计数加一,用于统计已经被cond条件变量唤醒的子线程个数 \*/

g_wakenNum++;

/\* 释放mutex锁 \*/

rc = pthread_mutex_unlock(&g_td.mutex);

if (rc != 0) {

printf("ERROR: mutex release failed, error code is %d!\\n", rc);

goto EXIT;

}

EXIT:

return NULL;

}

static int testcase(void)

{

int i, rc;

pthread_t thread[THREAD_NUM];

/\* 初始化mutex锁 \*/

rc = pthread_mutex_init(&g_td.mutex, NULL);

if (rc != 0) {

printf("ERROR: mutex init failed, error code is %d!\\n", rc);

goto ERROROUT;

}

/\* 初始化cond条件变量 \*/

rc = pthread_cond_init(&g_td.cond, NULL);

if (rc != 0) {

printf("ERROR: pthread condition init failed, error code is %d!\\n", rc);

goto ERROROUT;

}

/\* 批量创建THREAD_NUM个子线程 \*/

for (i = 0; i \< THREAD_NUM; i++) {

rc = pthread_create(&thread[i], NULL, ChildThreadFunc, NULL);

if (rc != 0) {

printf("ERROR: pthread create failed, error code is %d!\\n", rc);

goto ERROROUT;

}

}

/\* 等待所有子线程都完成mutex锁的获取 \*/

while (g_startNum \< THREAD_NUM) {

usleep(100);

}

/\* 获取mutex锁,确保所有子线程都阻塞在pthread_cond_wait上 \*/

rc = pthread_mutex_lock(&g_td.mutex);

if (rc != 0) {

printf("ERROR: mutex lock failed, error code is %d\\n", rc);

goto ERROROUT;

}

/\* 释放mutex锁 \*/

rc = pthread_mutex_unlock(&g_td.mutex);

if (rc != 0) {

printf("ERROR: mutex unlock failed, error code is %d!\\n", rc);

goto ERROROUT;

}

for (int j = 0; j \< THREAD_NUM; j++) {

/\* 在cond条件变量上广播信号 \*/

rc = pthread_cond_signal(&g_td.cond);

if (rc != 0) {

printf("ERROR: pthread condition failed, error code is %d!\\n", rc);

goto ERROROUT;

}

}

sleep(1);

/\* 检查是否所有子线程都已被唤醒 \*/

if (g_wakenNum != THREAD_NUM) {

printf("ERROR: not all threads awaken, only %d thread(s) awaken!\\n", g_wakenNum);

goto ERROROUT;

}

/\* join所有子线程,即等待其结束 \*/

for (i = 0; i \< THREAD_NUM; i++) {

rc = pthread_join(thread[i], NULL);

if (rc != 0) {

printf("ERROR: pthread join failed, error code is %d!\\n", rc);

goto ERROROUT;

}

}

/\* 销毁cond条件变量 \*/

rc = pthread_cond_destroy(&g_td.cond);

if (rc != 0) {

printf("ERROR: pthread condition destroy failed, error code is %d!\\n", rc);

goto ERROROUT;

}

return 0;

ERROROUT:

return -1;

}

/\*

\* 示例代码主函数

\*/

int main(int argc, char \*argv[])

{

int rc;

/\* 启动测试函数 \*/

rc = testcase();

if (rc != 0) {

printf("ERROR: testcase failed!\\n");

}

return 0;

}

\#ifdef \__cplusplus

\#if \__cplusplus

}

\#endif /\* \__cplusplus \*/

\#endif /\* \__cplusplus \*/

7.3 与linux的差异

本节描述了OpenHarmony内核承载的标准库与Linux标准库之间存在的关键差异。更多差异详见C库API文档说明。

7.3.1 进程差异

OpenHarmony用户态进程优先级只支持静态优先级且用户态可配置的优先级范围为10(最高优先级)-31(最低优先级)。

OpenHarmony用户态线程优先级只支持静态优先级且用户态可配置的优先级范围为0(最高优先级)-31(最低优先级)。

OpenHarmony进程调度策略只支持SCHED_RR, 线程调度策略支持SCHED_RR和SCHED_FIFO。

7.3.2 内存差异

h2****与Linux mmap的差异

mmap接口原型为:void *mmap (void *addr, size_t length, int prot, int flags, int fd, off_t offset)。

其中,参数fd的生命周期实现与Linux glibc存在差异。具体体现在,glibc在成功调用mmap进行映射后,可以立即释放fd句柄。在OpenHarmony内核中,不允许用户在映射成功后立即关闭相关fd,只允许在取消映射munmap后再进行fd的close操作。如果用户不进行fd的close操作,操作系统将在进程退出时对该fd进行回收。

h2****代码举例

Linux目前支持的情况如下:

int main(int argc, char \*argv[])

{

int fd;

void \*addr = NULL;

...

fd = open(argv[1], O_RDONLY);

if (fd == -1){

perror("open");

exit(EXIT_FAILURE);

}

addr = mmap(NULL, length, PROT_READ, MAP_PRIVATE, fd, offset);

if (addr == MAP_FAILED) {

perror("mmap");

exit(EXIT_FAILURE);

}

close(fd); /\* OpenHarmony does not support closing fd immediately after the mapping is successful. \*/

...

exit(EXIT_SUCCESS);

}

OpenHarmony支持的情况如下:

int main(int argc, char \*argv[])

{

int fd;

void \*addr = NULL;

...

fd = open(argv[1], O_RDONLY);

if (fd == -1) {

perror("open");

exit(EXIT_FAILURE);

}

addr = mmap(NULL, length, PROT_READ, MAP_PRIVATE, fd, offset);

if (addr == MAP_FAILED) {

perror("mmap");

exit(EXIT_FAILURE);

}

...

munmap(addr, length);

close(fd); /\* Close fd after the munmap is canceled. \*/

exit(EXIT_SUCCESS);

}

7.4文件系统

系统目录:用户无权限修改系统目录和设备挂载目录。包含/dev,/proc,/app,/bin,/data,/etc,/lib,/system,/usr目录。

用户目录:用户可以在该目录下进行文件创建、读写,但不能进行设备挂载。用户目录指/storage目录。

除系统目录与用户目录之外,用户可以自行创建文件夹进行设备的挂载。但是要注意,已挂载的文件夹及其子文件夹不允许重复或者嵌套挂载,非空文件夹不允许挂载。

7.5信号

信号默认行为不支持STOP、CONTINUE、COREDUMP功能。

无法通过信号唤醒正在睡眠状态(举例:进程调用sleep函数进入睡眠)的进程。原因:信号机制无唤醒功能,当且仅当进程被CPU调度运行时才能处理信号内容。

进程退出后会发送SIGCHLD给父进程,发送动作无法取消。

信号仅支持1-30号信号,接收方收到多次同一信号,仅执行一次回调函数。

7.6Time

OpenHarmony当前时间精度以tick计算,系统默认10ms/tick。sleep、timeout系列函数时间误差<=20ms。

8 Qemu模拟arm架构配置Harmony Liteos-a

8.1配置ubuntu

8.1.1vm虚拟机中安装ubuntu

ubuntu推荐使用18.04版本,虚拟机推荐使用vm15(双系统也可)

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

启动该虚拟机

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

安装完毕重启即可。

8.1.2安装VMware tools

目的:

安装后用户可以从物理主机直接往虚拟机里面拖文件。

安装后鼠标进入虚拟机后可以直接出来,不安装的话要按CTRL+ALT才可以释放鼠标。

安装后可以解决Ubuntu主窗口分辨率不适应问题,用户可以随意改变虚拟机窗口大小。

安装过程:

  1. 启动虚拟机
  2. 在工具栏找到虚拟机,点击安装 VMware Tools 如下图所示:

在这里插入图片描述

  1. 在桌面打开挂载的DVD盘,将VMwareTools-10.3.2-9925305.tar.gz复制到Home中。

在这里插入图片描述

  1. 在Home中打开终端输入命令

输入解压命令:tar -zxvf VMwareTools-10.3.2-9925305.tar.gz

  1. 解压后,输入ls命令可以查看到文件夹vmware-tools-distrib
  2. 进入该文件夹 cd vmware-tools-distrib/
  3. 执行安装文件 sudo ./vmware-install.pl
8.1.3共享文件夹设置

在这里插入图片描述

1、共享文件夹选中“总是启用”

2、添加文件夹的路径

共享文件夹的位置在 Other Locations文件夹下的mnt文件夹中。

注:出现如下问题如:

在这里插入图片描述

解决方法:

在这里插入图片描述

在这里插入图片描述

然后再次进入即可解决此问题。

如果鼠标在虚拟机中移不出来, 按下键盘的CTRL+ALT即可.

进入系统后

在这里插入图片描述

在这里插入图片描述

8.1.4更新软件源中的所有软件列表与软件
sudo apt-get update与apt-get upgrade

在这里插入图片描述
在这里插入图片描述

8.2 Qemu ARM Virt环境搭建

8.2.1. Qemu ARM Virt简介

arm_virt/ 子目录包含部分Qemu ARM虚拟化平台验证的OpenHarmony kernel_liteos_a的适配代码,含驱动配置、板端配置等。

ARM 虚拟化平台是一个 qemu-system-arm 的目标设备,通过它来模拟一个通用的、基于ARM架构的单板。 Qemu中machine为 virt 的单板就是这种可配置的,例如:选择核的类型、核的个数、内存的大小和安全特性等,单板设备的配置。

这次模拟的配置是:Cortex-A7架构,1个CPU,带安全扩展,GICv2,1G内存。 提示: 系统内存硬编码为32MB

8.2.2 环境搭建

系统要求:Ubuntu18.04及以上64位系统版本。

编译环境搭建分为如下几步:

  • 获取源码

    安装和配置python
    
    安装gn
    
    安装ninja
    
    安装LLVM
    
    安装hb
    
8.2.3 安装和配置Python

8.2.3.1打开Linux编译服务器终端。

8.2.3.2输入如下命令,查看python版本号,需使用python3.7以上版本。

python3 –version

在这里插入图片描述

如果低于python3.7版本,不建议直接升级,请按照如下步骤重新安装。以python3.8为例,按照以下步骤安装python。

8.2.3.3运行如下命令,查看Ubuntu版本:

cat /etc/issue

在这里插入图片描述

8.2.3.4根据Ubuntu不同版本,安装python。

如果Ubuntu 版本为18+,运行如下命令。

sudo apt-get install python3.8

在这里插入图片描述

8.2.3.5设置python和python3软链接为python3.8。

sudo update-alternatives --install /usr/bin/python python /usr/bin/python3.8 1

sudo update-alternatives --install /usr/bin/python3 python3 /usr/bin/python3.8 1

在这里插入图片描述

8.2.3.6安装并升级Python包管理工具(pip3)。

sudo apt-get install python3-setuptools python3-pip -y

sudo pip3 install --upgrade pip

在这里插入图片描述

8.2.4 安装gn

8.2.4.1打开Linux编译服务器终端。

8.2.4.2下载gn工具。

在ubuntu中的浏览器输入网址等待下载https://repo.huaweicloud.com/harmonyos/compiler/gn/1717/linux/gn-linux-x86-1717.tar.gz

在这里插入图片描述

将下载好的压缩文件拷贝到home

在这里插入图片描述

8.2.4.3在根目录下创建gn文件夹。

mkdir \~/gn

8.2.4.4解压gn安装包至~/gn路径下。

tar -xvf gn-linux-x86-1717.tar.gz -C \~/gn

在这里插入图片描述

8.2.4.5设置环境变量。

注:需要安装vim:sudo apt-get install vim

vim \~/.bashrc

8.2.4.6将以下命令拷贝到.bashrc文件的最后一行,保存并退出。

注:进入后按i进入编辑模式,将export PATH=~/gn:$PATH输入后按esc后shift+;后按wq回车保存并退出

export PATH=\~/gn:\$PATH

在这里插入图片描述

8.2.4.7生效环境变量。

source \~/.bashrc
8.2.5安装ninja

8.2.5.1打开Linux编译服务器终端。

8.2.5.2下载ninja工具。

在ubuntu中的浏览器输入网址等待下载

<https://repo.huaweicloud.com/harmonyos/compiler/ninja/1.9.0/linux/ninja.1.9.0.tar>

在这里插入图片描述

将下载好的压缩文件拷贝到home

在这里插入图片描述

8.2.5.3解压ninja安装包至~/ninja路径下。

tar -xvf ninja.1.9.0.tar -C \~/

在这里插入图片描述

8.2.5.4设置环境变量。

vim \~/.bashrc

8.2.5.5将以下命令拷贝到.bashrc文件的最后一行,保存并退出。

export PATH=\~/ninja:\$PATH

在这里插入图片描述

8.2.5.6生效环境变量。

source \~/.bashrc
8.2.6安装llvm

8.2.6.1打开Linux编译服务器终端。

8.2.6.2下载LLVM工具。

在ubuntu中的浏览器输入网址https://repo.huaweicloud.com/harmonyos/compiler/clang/10.0.1-62608/linux/llvm.tar.gz等待下载

在这里插入图片描述

注:针对OpenHarmony_v1.x分支/标签,使用此链接下载LLVM工具

将下载好的压缩文件拷贝到home

在这里插入图片描述

8.2.6.3解压LLVM安装包至~/llvm路径下。

tar -zxvf llvm.tar.gz -C \~/

在这里插入图片描述

注: 针对OpenHarmony_v1.x分支/标签,使用如下命令解压:

tar -xvf llvm-linux-9.0.0-36191.tar -C \~/

8.2.6.4设置环境变量。

vim \~/.bashrc

8.2.6.5将以下命令拷贝到.bashrc文件的最后一行,保存并退出。

export PATH=\~/llvm/bin:\$PATH

在这里插入图片描述

8.2.6.6生效环境变量。

source \~/.bashrc
8.2.7安装hb

注:请先安装Python 3.7.4及以上版本,请见安装和配置Python。

8.2.7.1运行如下命令安装hb

python3 -m pip install --user ohos-build

在这里插入图片描述

8.2.7.2设置环境变量

vim \~/.bashrc

8.2.7.3将以下命令拷贝到.bashrc文件的最后一行,保存并退出。

export PATH=\~/.local/bin:\$PATH

在这里插入图片描述

8.2.7.4执行如下命令更新环境变量。

source \~/.bashrc

8.2.7.5验证:执行"hb -h",有打印以下信息即表示安装成功:

在这里插入图片描述

8.2.7.6卸载方法

python3 -m pip uninstall ohos-build
8.2.8 apt安装全部依赖的工具
sudo apt-get install build-essential gcc g++ make zlib\* libffi-dev e2fsprogs pkg-config flex bison perl bc openssl libssl-dev libelf-dev libc6-dev-amd64 binutils binutils-dev libdwarf-dev u-boot-tools mtd-utils gcc-arm-linux-gnueabi

8.3 QEMU(Quick Emulator)环境搭建

8.3.1 QEMU简介

QEMU可以模拟内核运行在不同的单板,解除对物理开发板的依赖。

约束:只适用于OpenHarmony内核。、

8.3.2QEMU安装

8.3.2.1、安装依赖(Ubuntu 18+)

sudo apt install build-essential zlib1g-dev pkg-config libglib2.0-dev binutils-dev libboost-all-dev autoconf libtool libssl-dev libpixman-1-dev virtualenv flex bison

在这里插入图片描述

8.3.2.2获取源码

wget <https://download.qemu.org/qemu-6.0.0.tar.xz>

在这里插入图片描述

8.3.2.3编译安装

tar -xf qemu-6.0.0.tar.xz

mkdir qemu

sudo apt-get install ninja-build

cd qemu-6.0.0

mkdir build && cd build

sudo ../configure --prefix=(通过pwd获取quem文件夹路径)

在这里插入图片描述

sudo make -j16

在这里插入图片描述

8.3.2.4执行安装命令(等待编译结束根据配置需要一段时间):

sudo make install

在这里插入图片描述

8.3.2.5安装路径添加到环境变量中:

vim \~/.bashrc

在~/.bashrc最末尾加入:

export PATH=\$PATH:qemu_installation_path(通过pwd获取quem/bin文件夹路径)

在这里插入图片描述

8.3.2.6执行如下命令更新环境变量

source \~/.bashrc

8.3.2.7验证

安装成功后,查看qemu提供的工具(按两次Tab键给出以qemu-开头的命令)

在这里插入图片描述

8.4获取源码

8.4.1 OpenHarmony介绍

OpenHarmony是由开放原子开源基金会(OpenAtom Foundation)孵化及运营的开源项目,目标是面向全场景、全连接、全智能时代,搭建一个智能终端设备操作系统的框架和平台,促进万物互联产业的繁荣发展。

开源代码仓库地址:https://openharmony.gitee.com

注:当前的OpenHarmony源代码仅支持在Linux环境下编译。

8.4.2代码仓库获取源码

8.4.2.1安装与配置git

sudo apt-get install git

git config --global user.name "lcy" 注:“”内改为自己的

git config --global user.email "1578260448@qq.com"注:“”内改为自己的

git config --global credential.helper store

在这里插入图片描述

8.4.2.2安装repo

repo工具是用来管理多个GIT仓库的命令,执行如下命令来安装:

sudo apt-get install curl

curl https://gitee.com/oschina/repo/raw/fork_flow/repo-py3 \> repo

sudo cp repo /usr/local/bin/repo && sudo chmod a+x /usr/local/bin/repo

sudo pip3 install -i https://pypi.tuna.tsinghua.edu.cn/simple requests

在这里插入图片描述

8.4.2.3获取harmony源码

创建并进入harmony文件夹

mkdir harmony && cd harmony

repo init -u https://gitee.com/openharmony/manifest.git -b master --no-repo-verify

在这里插入图片描述

repo sync -c

注:若一次不能完全拉下来,需要多更新(repo sync -c)几次直到出现success,必须保证源码的完整性,不可以有丢包产生。

在这里插入图片描述

提示:由于网络原因要有耐心此过程会很久半小时到数小时不等

成功案例:

在这里插入图片描述

curl -s https://packagecloud.io/install/repositories/github/git-lfs/script.deb.sh \| sudo bash

sudo apt-get install git-lfs

sudo repo forall -c 'git lfs pull'

8.5 鸿蒙Liteos-a源码构建

8.5.1 Ubuntu切换默认sh为bash

先用命令看看

ls -l /bin/sh

/bin/sh -\> dash

切换sh为bash如果要修改默认的sh,可以采用命令

sudo dpkg-reconfigure dash

在这里插入图片描述

然后选择否

成功后再执行

ll /bin/sh

结果是:

在这里插入图片描述

8.5.2源码构建

在已经获取的源码根目录,请输入:

hb set

输入 . 代表当前路径

在这里插入图片描述

选择ohemu下的qemu_small_system_demo选项,输出如下:

[OHOS INFO] Input code path: .

OHOS Which product do you need? qemu_small_system_demo

在这里插入图片描述

然后执行构建命令如下:

hb build

在这里插入图片描述

构建成功后会产生 liteos.bin、rootfs_jffs2.img 和 userfs_jffs2.img 的镜像文件。

在构建完成之后,对应的镜像文件在如下目录:

out/arm_virt/qemu_small_system_demo/liteos.bin

out/arm_virt/qemu_small_system_demo/rootfs_jffs2.img

out/arm_virt/qemu_small_system_demo/userfs_jffs2.img

在这里插入图片描述

8.5.3常见问题

8.5.3.1如果是整个命令失败没有其他的提示信息,则检查编译所需的依赖链是否安装完成:

# clang

lcy@lcy-machine:\~\$ clang -v

在这里插入图片描述

# gn

lcy@lcy-machine:\~\$ gn gen -help

在这里插入图片描述

# nija

lcy@lcy-machine:\~\$ ninja --version

在这里插入图片描述

# llvm

lcy@lcy-machine:\~\$ llvm-as -version

在这里插入图片描述
注:在编译1.x版本时llvm应该是9.0.x。而编译ohos2.0版本时llvm则应该是10.0

# qemu

lcy@lcy-machine:\~\$ qemu-system-arm -version

在这里插入图片描述

# hb

lcy@lcy-machine:\~\$hb

在这里插入图片描述

# python

lcy@lcy-machine:\~\$ python3 –version

lcy@lcy-machine:\~\$ python –version

在这里插入图片描述

上述环境测试无误后,如果hb build仍不能成功,去官方文档的issue里找找有没有相同的问题

注:鸿蒙对于该文档维护比较及时,迭代可能较快,很多教程都在实时更新,开发时要尽量关注,如果使用2.0源码,则更要紧跟着文档看看最新的源码在编译时有什么不同

如果在根目录下编译还没有开始make就报错,应该就是gn或者ninja的配置有问题,可以从这方面着手

8.6在qemu中模拟arm架构运行liteos-a

需要保证你的qemu-system-arm 版本尽可能在5.1及以上。

8.6.1运行镜像

在代码根目录下,编译后会生成qemu-run脚本,

在这里插入图片描述

可直接执行./qemu-run进入liteos-a。

执行shell后执行help即可查看现有所有shell命令(类linux)

在这里插入图片描述

执行./qemu-run --help提示如下:

在这里插入图片描述

默认不加参数的情况下,网络不会自动配置。当根目录镜像文件flash.img存在时,镜像不会被重新制作。

8.6.2 退出qemu环境

按下Ctrl-A + x可退出qemu虚拟环境。

8.6.3 常见错误

8.6.3.1qemu-run提示qemu-system-arm运行出错时,如何排查问题?

qemu-run脚本中,完整的执行命令及参数解释如下:

qemu-system-arm -M virt,gic-version=2,secure=on -cpu cortex-a7 -smp cpus=1 -m 1G \

-drive if=pflash,file=flash.img,format=raw \

-netdev bridge,id=net0 \

-device virtio-net-device,netdev=net0,mac=12:22:33:44:55:66 \

-device virtio-gpu-device,xres=800,yres=480 \

-device virtio-mouse-device \

-vnc :20 \

-global virtio-mmio.force-legacy=false

-M 虚拟机类型,ARM virt,GIC 2中断控制器,有安全扩展

-cpu CPU型号

-smp SMP设置,单核

-m 虚拟机可使用的内存限制

-drive if=pflash CFI闪存盘设置

-netdev [可选]网卡后端设置,桥接类型

-device virtio-net-device [可选]网卡设备

-device virtio-gpu-device [可选]GPU设备

-device virtio-mouse-device [可选]鼠标设备

-vnc :20 [可选]远程桌面连接,端口5920

-global QEMU配置参数,不可调整

运行时,qemu-run遇到报错如下报错: failed to parse default acl file

可能是qemu安装方式不同,导致qemu配置文件路径存在一定差异:

使用源码安装默认在/usr/local/qemu/etc/qemu

使用部分Linux发行版安装工具进行安装时,默认在/ect/qemu/目录下

可根据实际情况,确认具体配置目录,并进行如下修改:

echo ‘allow br0’ | sudo tee -a <配置文件路径>

8.7添加第一个helloworld程序

1创建helloworld目录

helloworld目录结构如下:

applications/sample/helloworld

applications/sample/helloworld/src

2创建helloworld.c文件

在 applications/sample/helloworld/src下创建helloworld.c文件,并添加如下代码:


\#include \<stdio.h\>

int main(int argc, char \*\*argv)

{

printf("\\n\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\\n");

printf("\\n\\t\\tHello OHOS!\\n");

printf("\\n\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\\n\\n");

return 0;

}

3为helloword创建BUILD.gn文件

在 applications/sample/helloworld下添加BUILD.gn文件,并添加如下代码:

import("//build/lite/config/component/lite_component.gni")

lite_component("hello-OHOS") {

features = [ ":helloworld" ]

}

executable("helloworld") {

output_name = "helloworld"

sources = [ "src/helloworld.c" ]

include_dirs = []

defines = []

cflags_c = []

ldflags = []

}

提示:hellworld最后目录结构为

applications/sample/helloworld

applications/sample/helloworld/BUILD.gn

applications/sample/helloworld/src

applications/sample/helloworld/src/helloworld.c

4在build/lite/components中新建配置文件helloworld.json,并添加如下代码:

{

"components": [

{

"component": "hello_world_app",

"description": "Communication related samples.",

"optional": "true",

"dirs": [

"applications/sample/helloworld"

],

"targets": [

"//applications/sample/helloworld:hello-OHOS"

],

"rom": "",

"ram": "",

"output": [],

"adapted_kernel": [ "liteos_a" ],

"features": [],

"deps": {

"components": [],

"third_party": []

}

}

]

}

注意:helloworld.json中dirs和targets的属性值是不带src的

5在vendor/ohemu/display_qemu_liteos_a/config.json配置文件中找到subsystems属性,并下面追加helloworld的subsystem配置,配置参考如下:

{

"subsystem": "helloworld",

"components": [

{ "component": "hello_world_app", "features":[] }

]

}

注意:vendor/ohemu/display_qemu_liteos_a/config.json对应的编译模板为 display_qemu,后面编译时需要选择这个模板;另外修改JSON配置的时候一定要将多余的逗号去掉,否则编译时会报错

6编译并构建qemu虚拟环境参考前几章

注意:helloworld 正常编译后会出现在 out/arm_virt/display_qemu/bin中,如果没有,请返回检查相关配置文件中的路径和名称是否有误,并尝试重新编译直到出现helloword

提示:编译完成后,代码根目录下会生成qemu-run脚本,直接运行该脚本默认以非root权限运行qemu环境(不含网络配置)。其他参数配置详见qemu-run --help

7运行helloworld

helloworld在qemu虚拟机的bin目录下面,进入qemu虚拟机环境后,在bin目录下执行 ./helloword,会出现如下信息,表示Hello World程序添加成功

OHOS \# ./helloworld

OHOS \#

\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*

Hello OHOS!

\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*
  • 2
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
LiteOS 是华为公司开发的一款轻量级的操作系统。为了更好地帮助开发者使用和了解 LiteOS,华为提供了详细的 LiteOS-A 文档LiteOS-A 文档是一个针对 LiteOS 的技术文档,其中包含了 LiteOS 的各种功能和特性的详细说明。开发者可以通过阅读文档了解 LiteOS 的体系结构、内核功能、支持的硬件平台、开发环境和开发工具链等信息。文档还提供了一系列的开发指南和示例代码,帮助开发者快速上手和应用 LiteOS文档中还介绍了 LiteOS 的特点和优势。LiteOS 是一个高效、可裁剪的操作系统,适用于各种资源受限的嵌入式设备。它具有超低的内存和存储占用,适用于小型芯片和低功耗设备。LiteOS 还支持多任务和多线程,并提供了丰富的通信机制和轻量级的任务调度器。 除了功能介绍,文档还提供了 LiteOS开发指南和最佳实践建议。开发者可以学习如何配置和编译 LiteOS,如何使用其内置的驱动和功能组件,以及如何进行任务调度和事件处理等操作。文档还包含了一些开发案例和示例代码,帮助开发者理解和应用 LiteOS 的各种功能。 总的来说,LiteOS-A 文档是一个全面而详细的技术指南,为开发者提供了使用和开发 LiteOS 的重要资源。通过阅读和学习文档开发者能够更好地了解 LiteOS 的特性和优势,并能够高效地进行 LiteOS 的应用开发
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

lcy~

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值