xuos-web/docs/doc/kernel/task.md

37 KiB
Raw Blame History

任务管理

任务机制

任务task是 XiUOS 中处理器使用权分配的最小单位。每个任务有自己的程序栈与寄存器上下文,在多处理器平台上可以互不干扰地同时运行,但单个处理器上任意时刻只能有一个任务在运行。用户可以使用 XiUOS 提供的接口创建任意数量的任务。内核会对系统中的所有任务按照一定策略(抢占式优先级或先来先服务)进行调度,最大限度地利用处理器资源。

任务状态

XiUOS 中的任务在任意时刻都处于就绪ready、运行running、阻塞/挂起suspend、退出quit四种状态之一。状态之间的变化关系如下图所示。

  • 任务在创建完成后会进入就绪状态并被加入就绪队列等待内核 CPU 调度
  • 当任务被调度开始运行时,任务会进入运行状态
  • 若任务在运行过程中被更高优先级的任务抢占,则被抢占的任务会回到就绪队列并再次进入就绪状态
  • 当任务在运行过程中申请资源失败时,任务会被挂起并进入挂起状态,并在所申请资源能够被满足时回到就绪状态
  • 当任务执行完成,即从入口函数返回时,会进入终止状态,并由内核回收其相关资源

任务调度

任务调度即从系统就绪队列中按一定策略选择某一任务使其进入运行状态的过程。XiUOS 支持以下调度方式:

  • 抢占式优先级调度即在创建任务时用户可以指定任务的优先级内核总是选取就绪队列中优先级最高的任务。当新创建的任务优先级高于正在运行的任务的优先级时当前运行任务的CPU使用权会被抢占。若就绪队列中最高优先级任务有多个则这些任务会按时间片轮转方式交替运行。
  • 先来先服务FIFO调度即任务按照被创建的顺序依次被执行。当一个任务运行完成后系统才会让下一个任务开始运行。
  • 时间片轮转RR调度即任务按照分配的时间片执行时间片结束调度一个新的就绪任务执行当前任务重新就绪等待下一次的调度。

任务结构定义

每一个任务在内核中都有一个task_descriptor描述符其结构体定义如下

struct task_descriptor
{
	void                     *stack_pointer;
	struct dync_sched_member dync_sched_member_x;
	struct task_baseinfo     t_baseinfo_x;
   
#ifdef XS_USING_SMP
    struct smp_info          smp_info_x;
#endif 

#if defined(XS_USING_EVENT)
	xs_uint32                event_ctrl1:3;
	xs_uint32                event_ctrl2:29;
#endif
    
    xs_err_t                 error;                                 
    XS_DOUBLE_LINKLIST       link;                                  
};

其中stack_pointer 指向任务堆栈的起始地址dync_sched_member_x 包含与任务调度相关的信息t_baseinfo_x 记录任务的基本信息smp_info_x 统计与多处理器相关的信息event_ctrl1 / event_ctrl2 用于实现事件集机制(详见任务通信error为任务调用内核接口时最近的错误码即用户线程在使用内核接口时可能会执行失败此时内核接口返回-1具体的错误码被保存在error成员中且在下一次调用内核接口失败时被覆盖link 用于组织内核中所有的任务。各复合成员的详细定义如下。

  • struct dync_sched_member
struct dync_sched_member {
    xs_uint8  stat;                                                 
    xs_uint8  advance_cnt;
    xs_uint8  cur_prio; 
    xs_ubase  origin_timeslice;                             
    xs_ubase  rest_timeslice;

#ifdef XS_USING_TASK_ISOLATION
	xs_uint8  isolation_flag;
	void      *isolation;
#endif

    union {
    	XS_DOUBLE_LINKLIST   sched_link;
    	XS_AVL               sched_avl;
    };
    XS_DOUBLE_LINKLIST   pend_link;
    xs_Timer_t           task_timer;                    
};

#define XS_SUSPEND    ((1) << (0))
#define XS_READY      ((1) << (1))
#define XS_RUNNING    ((1) << (2))
#define XS_QUIT       ((1) << (3))

struct dync_sched_member结构用于记录与调度相关的信息。stat记录任务的当前状态可以为挂起XS_SUSPEND、就绪XS_READY、运行XS_RUNNING或退出XS_QUIT。advance_cnt表示在配置成短作业预先调度时优先处理的时间片周期个数。cur_prio表示任务当前的优先级用于优先级反转该优先级可以高于任务创建时配置的优先级。origin_timeslice表示在时间片轮转调度时任务每次运行的时间片。isolation_flag变量和指针isolation支持地址空间隔离。sched_link和sched_avl构成的联合体为就绪队列节点XiUOS中就绪队列可以组织为双链表sched_link或平衡二叉树sched_avl。pend_link为任务挂起时使用的等待队列节点。task_timer为任务睡眠的计数器。

  • struct task_baseinfo
struct task_baseinfo {
    char      name[XS_NAME_MAX];
    void      *func_entry;
    void      *func_param;
    xs_uint32 stack_size;
    xs_uint8  origin_prio;
};

struct task_baseinfo结构记录了任务的基本属性包括任务的名称name、入口函数func_entry和参数func_param、栈大小stack_size、初始优先级origin_prio

  • struct smp_info
struct smp_info {
    xs_uint8  combined_coreid;
    xs_uint8  running_coreid;
};

struct smp_info结构包含多处理器相关的信息其成员分别表示该任务绑定的CPU ID与正在运行的CPU ID。

任务函数接口

struct xs_utask
{
	char        name[XS_NAME_MAX];         
    void        *func_entry;                
    void        *func_param;     
    xs_uint32   stack_size;  
    xs_uint8    prio; 
};
typedef struct xs_utask xs_utask_x;

struct task_descriptor* xs_UserTaskCreate(xs_utask_x task);

该函数用于用户态的任务创建。任务的各个属性由struct xs_utask结构表示包括任务的名称、入口函数及参数、栈大小和优先级在调用该函数时需要传入该结构的实例用于配置任务属性。任务创建成功后内核会为其分配指定大小的栈及其他结构如struct task_descriptor并返回任务描述符指针。

参数 描述
task 任务配置属性
xs_int32 xs_UserTaskDelete(struct task_descriptor task);

该函数用于删除一个任务强制其进入退出状态。若删除成功则返回XS_EOK否则返回-XS_ERROR。

参数 描述
task 待删除的任务描述符
xs_int32 xs_UserTaskCoreCombine(struct task_descriptor task_id, xs_uint8 core_id);

该函数用于将任务绑定至指定的处理器上。若绑定成功则返回XS_EOK否则返回-XS_ERROR。

参数 描述
task_id 待绑定的任务描述符
core_id 待绑定的处理器ID
xs_int32 xs_UserTaskCoreUncombine(struct task_descriptor task);

该函数用于解除任务与处理器的绑定。若解除成功则返回XS_EOK否则返回-XS_ERROR。

参数 描述
task 待解除绑定的任务描述符
xs_int32 xs_UserTaskDelay(xs_ticks_x ticks);

该函数用于将当前任务挂起一定时间单位为tick。挂起时间结束后任务会进入就绪状态等待系统调用。

参数 描述
ticks 任务挂起时间单位为tick
struct task_descriptor* xs_UserTaskSearch(char *name)

该函数根据任务名称获取任务描述符。

参数 描述
name 任务名称

任务通信

XiUOS 提供多种任务间通信机制,包括消息队列、信号量、互斥量与事件集。

消息队列

消息队列message queue提供可供多个任务读或写的消息缓冲区其中消息指固定长度的任意数据块。消息队列的容量有限。当消息队列满时向消息队列写入的任务会被挂起当消息队列空时从消息队列读取的任务会被挂起。

消息队列结构定义

struct xs_MsgQueue
{
    xs_uint16            id; 

    void                 *msg_buf;
	xs_uint16            index;
    xs_uint16            num_msgs;
    xs_uint16            each_len;
    xs_uint16            max_msgs;

    XS_DOUBLE_LINKLIST   send_pend_list;
    XS_DOUBLE_LINKLIST   recv_pend_list;
	XS_DOUBLE_LINKLIST   link;
};
成员 描述
id 消息队列ID用于唯一标识一个消息队列
msg_buf 用于存储消息的缓冲区的首地址
index 当前缓冲区中第一个消息的序号
num_msgs 当前缓冲区中的消息数量
each_len 每条消息的大小,单位为字节
max_msgs 缓冲区中最多能存放的消息数量
send_pend_list 被挂起的发送任务链表
recv_pend_list 被挂起的接收任务链表
link 系统中所有消息队列构成的链表

消息队列函数接口

xs_int32 xs_UserMsgQueueCreate(xs_uint16 msg_len, xs_uint16 max_msgs);

该函数用于创建一个消息队列。创建成功后新的消息队列会被加入内核的消息队列管理链表并返回该消息队列的IDID默认范围0-255可配置。

参数 描述
msg_len 每条消息的长度,单位为字节
max_msgs 缓冲区中最多存放的消息数量
void xs_UserMsgQueueDelete(xs_uint16 id);

该函数用于删除一个已创建的消息队列。

参数 描述
id 待删除的消息队列ID
xs_int32 xs_UserMsgQueueSend(xs_uint16 id, const void *msg, xs_uint16 size, xs_ticks_x wait_time);

该函数用于向消息队列发送一个消息。若消息发送成功则返回XS_EOK若不成功等待超时则返回-XS_ETIMEOUT。

参数 描述
id 目标消息队列ID
msg 消息数据首地址
size 消息长度
wait_time 等待时间上限单位ticks若为0则不等待
xs_int32 xs_UserMsgQueueRecv(xs_uint16 id, void *buf, xs_uint16 size, xs_ticks_x wait_time);

该函数用于从消息队列接收一个消息。若消息接收成功则返回XS_EOK若不成功等待超时则返回-XS_ETIMEOUT。

参数 描述
id 来源消息队列ID
buf 用于接收消息数据的缓冲区
size 缓冲区大小
wait_time 等待时间上限单位ticks若为0则不等待
xs_int32 xs_UserMsgQueueReinit(xs_uint16 id);

该函数用于将一个消息队列复位。

参数 描述
id 消息队列ID

信号量

信号量semaphore具有一个给定的初值。任务可以获取或释放一个信号量。当任务获取信号量时信号量的值递减释放信号量时信号量的值递增。当信号量的值递减至0时后续尝试获取信号量的任务会被挂起。当任务释放信号量时内核会从该信号量的挂起队列上唤醒一个任务。信号量可以实现任务间的同步与互斥。

信号量结构定义

struct xs_Semaphore
{
	xs_uint16                id;
	xs_uint16                value;
	
	XS_DOUBLE_LINKLIST       pend_list;
	XS_DOUBLE_LINKLIST       link;
};
成员 描述
id 信号量ID用于唯一标识一个信号量
value 信号量的当前值
pend_link 挂起任务链表
link 系统中所有信号量构成的链表

信号量函数接口

xs_int32 xs_UserSemaphoreCreate(xs_uint16 val);

该函数用于创建一个信号量。创建成功后新的信号量会被加入内核的信号量管理链表并返回该信号量的IDID默认范围0-255可配置。

参数 描述
val 信号量的初值
void xs_UserSemaphoreDelete(xs_uint16 id);

该函数用于删除一个已创建的信号量。

参数 描述
id 待删除的信号量ID
xs_int32 xs_UserSemaphoreObtain(xs_uint16 id, xs_ticks_x wait_time);

该函数用于获取一个信号量。若获取成功则返回XS_EOK若不成功等待超时则返回-XS_ETIMEOUT。

参数 描述
id 欲获取的信号量ID
wait_time 等待时间上限单位ticks若为0则不等待
xs_int32 xs_UserSemaphoreAbandon(xs_uint16 id);

该函数用于释放一个信号量。

参数 描述
id 待释放的信号量ID
xs_int32 xs_UserSemaphoreSetValue(xs_uint16 id, xs_uint16 val);

该函数用于将一个信号量的值进行重置。

参数 描述
id 来源消息队列ID
val 重置的信号量的值

互斥量

互斥量mutex可以视作一个初值为1的信号量同样有获取和释放操作。互斥量一般用于任务间的互斥。

互斥量结构定义

struct xs_Mutex
{
	xs_uint16                     id;
	
	xs_uint8                      origin_prio;
	xs_uint8                      recursive_cnt;
	
	struct task_descriptor        *holder;

	XS_DOUBLE_LINKLIST            pend_list;
	XS_DOUBLE_LINKLIST            link;
};
成员 描述
id 互斥量ID用于唯一标识一个互斥量
origin_prio 持有互斥量的任务的原优先级,用于避免优先级反转
recursive_cnt 持有互斥量的任务获取互斥量的次数,用于实现递归锁
holder 持有互斥量的任务
pend_list 挂起任务链表
link 系统中所有互斥量构成的链表

互斥量函数接口

xs_int32 xs_UserMutexCreate(void);

该函数用于创建一个互斥量。创建成功后新的互斥量会被加入内核的互斥量管理链表并返回该互斥量的IDID默认范围0-255可配置。

void xs_UserMutexDelete(xs_uint16 id);

该函数用于删除一个已创建的互斥量。

参数 描述
id 待删除的互斥量的ID
xs_int32 xs_UserMutexObtain(xs_uint16 id, xs_ticks_x wait_time);

该函数用于获取一个互斥量。若获取成功则返回XS_EOK若不成功等待超时则返回-XS_ETIMEOUT。

参数 描述
id 欲获取的互斥量的ID
wait_time 等待时间上限单位ticks若为0则不等待
xs_int32 xs_UserMutexAbandon(xs_uint16 id);

该函数用于释放一个互斥量。

参数 描述
id 待释放的互斥量的ID

事件集

事件集event set允许用户定义一个事件集合集合中的每个事件都可以被任务触发或等待。任务可以同时等待多个事件此时等待触发的条件可以配置为 AND 或者 OR :当等待触发条件配置为 AND 时,只有所有被等待的事件均被触发才视作等待结束;当等待触发条件配置为 OR 时,任意一个被等待的事件触发即视作等待结束。使用事件集可以实现多对多的任务间同步与互斥。

事件集结构定义

#define XS_EVENT_AND        (1 << 29)   
#define XS_EVENT_OR         (1 << 30)
#define XS_EVENT_AUTOCLEAN  (1 << 31)
#define XS_EVENTS(x)        (1 << x)

struct xs_event
{
	xs_uint16                    id; 
	xs_uint32                    trigger_way;
	
	XS_DOUBLE_LINKLIST           pend_list;
	XS_DOUBLE_LINKLIST           link;
};
成员 描述
id 事件集ID用于唯一标识一个事件集
trigger_way 高3位用于记录事件集属性等待触发条件、是否自动清空低29位用于表示至多29个事件
pend_list 等待任务链表
link 系统中所有事件集构成的链表

事件集函数接口

xs_int32 xs_UserEventCreate(xs_uint32 options);

该函数用于创建一个事件集。options参数用于配置事件集的属性。可配置的属性有等待触发方式XS_EVENT_AND 或 XS_EVENT_OR及等待触发后是否自动清空其他已触发的事件XS_EVENT_AUTOCLEAN。创建成功后新的事件集会被加入内核的事件集管理链表并返回该事件集的IDID默认范围0-255可配置。

参数 描述
options 事件集配置选项,须在 XS_EVENT_AND 及 XS_EVENT_OR 中指定其一,并可以按位或上 XS_EVENT_AUTOCLEAN
void xs_UserEventDele(xs_uint16 id);

该函数用于删除一个已创建的事件集。

参数 描述
id 待删除的事件集的ID
xs_int32 xs_UserEventTrigger(xs_uint16 id, xs_uint32 events);

该函数用于触发事件集中的一组事件。

参数 描述
id 事件集ID
events 欲触发的事件其中被置1的位标识被触发的事件 ,可以使用 XS_EVENTS 宏按位或发送事件类型
xs_int32 xs_UserEventProcess(xs_uint16 id, xs_uint32 events, xs_ticks_x wait_time);

该函数用于等待事件集中的一组事件。若等待成功则返回 XS_EOK此时若 XS_EVENT_AUTOCLEAN 被打开则事件集中所有已触发事件会被清空;若等待失败(超时)则返回 -XS_ETIMEOUT。

参数 描述
id 事件集ID
events 欲等待的事件其中被置1的位标识被等待的事件可以使用XS_EVENTS宏按位或接收事件类型
wait_time 等待时间上限单位ticks若为0则不等待
xs_int32 xs_UserEventConfig(xs_uint16 id, xs_uint32 options);

该函数用于配置事件集中的一种事件类型。若配置成功则返回 XS_EOKoptions 参数用于配置事件集的属性。可配置的属性有等待触发方式XS_EVENT_AND 或 XS_EVENT_OR及等待触发后是否自动清空其他已触发的事件XS_EVENT_AUTOCLEAN

参数 描述
id 事件集ID
options 事件集配置选项,须在 XS_EVENT_AND 及 XS_EVENT_OR 中指定其一,并可以按位或上 XS_EVENT_AUTOCLEAN
xs_int32 xs_UserEventReinit(xs_uint16 id);

该函数用于将一个事件的值进行重置。

参数 描述
id 来源消息队列ID

任务隔离

背景及动机

XiUOS 是一个支持多任务的操作系统,对任务的数量没有限制。在 XiUOS 中,每个任务都需要自己的堆栈,同时也可能会动态申请内存资源。任务在运行过程中发生内存溢出是 RTOS 系统中最常见的问题,所以限制任务的内存空间访问是保证 RTOS 稳定运行的关键。

ARM 和 RISC-V 在体系架构上都提供了内存访问的保护功能可以通过对特定寄存器的硬编程实现对指定内存区域访问权限的设置。然而现有的大多数物联网操作系统并没有使用体系结构提供的内存保护功能来对任务运行的地址空间进行隔离保护。XiUOS 充分考虑任务运行的安全问题,在不影响任务正常执行的情况下,对每个任务所允许访问的内存地址空间进行限制。除此之外,任务在动态申请内存、释放内存、内存共享时也提供隔离服务。

XiUOS 任务隔离的总体设计思想是将物理内存地址空间划分为信任地址空间和非信任地址空间。XiUOS 的内核任务运行在信任地址空间可以访问所有信任地址空间和非信任地址空间XiUOS 的用户程序运行在非信任地址空间,通过”内核服务表“的方式访问内核任务提供的功能。

内核服务表

ARM 和 RISC-V 在体系架构上支持机器在特权模式和非特权模式之间转换。XiUOS 的内核任务运行在特权模式下可以访问体系结构支持的、可编程的所有硬件资源。XiUOS 的用户程序运行在非特权模式下对硬件资源的访问权限是受限制的。为了实现用户程序受限的硬件资源访问以及内核任务提供的其他功能XiUOS 的内核为用户程序提供一组服务接口来满足应用程序的这些需求,这一组内核服务接口称为内核服务表。应用程序访问内核服务接口的流程如下:

  1. 应用程序执行异常调用指令并指定相应内核服务号和参数;
  2. 通过软中断指令产生一个调用异常,之后 CPU 切换到特权模式并强制执行异常处理流程,异常处理流程提取内核服务号和参数,并将服务号作为索引;
  3. 根据索引从内核服务表中查找对应的内核服务接口;
  4. 在特权模式下,执行所需的内核服务,完成后切换回非特权模式继续执行。

任务隔离表

在 XiUOS 中,任务描述符 task_descriptor 管理系统的任务其中包含了任务优先级、任务名称、任务状态等信息。为了管理任务可访问的内存空间task_descriptor 描述符在 dync_sched_member 子结构中增加任务隔离标志位 isolation_flag 和任务隔离表 isolation_table, 其定义如下:

struct dync_sched_member
{
   
   #ifdef  XS_USING_TASK_ISOLATION
          xs_uint8  isolation_flag;
          void     *isolation_table;
   #endif
   
};

用户程序运行在非信任地址空间,默认开启隔离机制。在用户程序对应的任务被创建时,任务隔离标志位 isolation_flag 被置为1其允许访问的地址空间范围由 isolation_table 指针所指定。isolation_table 包含了任务可访问的多个内存地址空间,每个内存地址空间用一个 isolation_region 数据结构来描述,这个数据结构包括一段连续的地址空间范围和访问权限,其具体结构取决于体系架构提供的内存保护功能,如 ARM 提供的 MPU 和 RISC-V 提供的 PMP 功能。isolation_region 的定义如下:

#if defined(XS_USING_TASK_ISOLATION)
typedef struct isolation_region 
{
  #ifdefined (XS_RISCV32)  
    xs_uint8  region_cfg;
    xs_uint32 region_addr;
  #elif defined (XS_RISCV64)
    xs_uint8  region_cfg;
    xs_uint64 region_addr;
  #elif defined (XS_ARM32)
    xs_uint32 region_cfg;
    xs_uint32 region_addr;
  #endif
}isolation_region;
#endif

隔离机制

  1. 任务内存结构分布及 isolation_region
    XiUOS 中的任务在编译链接后形成Linux通用的ELF文件结构其中包括.code、.data、.bss等段。在任务加载过程中.code、.data、.bss会被加载到对应的内存段中。在创建任务时先判断任务类型xs_UserTaskCreate用于创建任务其中 isolation_flag 标志位会被置为1。当为任务分配好栈空间后isolation_table 中为.code、.data、.bss 等段分别创建一个 isolation_region并设置对应的地址范围和访问权限如.code段对应的 isolation_region 为读和执行权限,.data 段对应的 isolation_region 为只读权限,.bss 段和栈对应的 isolation_region 为读写权限。
  2. 任务切换时的隔离
    在 XiUOS 中,多个任务共享有限个 CPU 核,采用优先级加时间片的调度方式,当一个任务时间片耗尽或者主动让出 CPU 的使用权时,内核调度程序负责保存当前任务的上下文信息,并从等待队列中挑选下一个就绪任务,恢复其上下文信息,并允许其获取 CPU 的使用权。为了保证每个任务只能访问自己的内存空间,内核调度程序在恢复任务上下文时,会先根据 isolation_flag 标志判断该任务是否为用户程序,如果为用户程序,则当前 CPU 核在运行该任务时,只允许访问本任务 isolation_table 中定义的内存区域,对于其它区域没有访问权限。
  3. 动态申请/释放内存时的隔离
    XiUOS 的任务通过 malloc/free 等内核服务接口来动态申请和释放内存空间,并能根据用户任务申请和释放的内存地址更新任务隔离表。当用户程序通过 malloc 等内核服务接口向内核申请指定大小的内存空间时,系统会在用户程序对应的任务中增加对内存空间的访问权限,同时将更新后的任务隔离表加载到内存保护单元配置寄存器中,使其生效。当用户程序通过 free 等内核服务接口向内核释放指定大小的内存空间时,系统会在用户程序对应的任务隔离表中清除对这段内存空间的访问权限,同时加载更新后的任务隔离表到内存保护单元寄存器中,使其生效。
    此外XiUOS 还支持共享内存的任务隔离,其基本思路同动态内存申请/释放时相同。进一步地当用户程序因为某种原因试图访问没有权限的内存空间时CPU 会产生一个访问错误的异常,并进入内核服务接口的异常处理流程,在这个流程中会将该任务直接杀死并回收任务资源。
  4. RISC-V 64架构任务隔离
    大部分 RISC-V 架构的 CPU 拥有特权模式和非特权模式。这两种模式都为 CPU 核提供了物理内存保护PMPPhysical Memory Protection功能。通过编程 PMP可以为指定大小的内存区域设置读、写、执行等访问权限。PMP 包括 8~16 组配置寄存器和地址寄存器,一组配置寄存器和地址寄存器称为一个 PMP entryPMP entry 对应之前定义的 isolation_region用于标识一段内存地址空间的访问权限。 对于一个正在运行用户任务其isolation_table的内容大致如下
isolation_table[16] = 
        {
            {  //.code
               .region_cfg = __PMP_REGION_CFG( 0, // L=0用户任务遵守RWX指定的权限内核任务拥有全部权限
               .region_addr = __PMP_REGION_ADDR(text_start_addr,region_start_addr_size);
            },
            {   //.data
                .region_cfg = __PMP_REGION_CFG( 0, //L=0用户任务遵守RWX指定的权限内核任务拥有全部权限
                .region_addr = __PMP_REGION_ADDR(data_start_addr,region_start_addr_size);
            },
            {  //.bss段
                .region_cfg = __PMP_REGION_CFG( 0, //L=0用户任务遵守RWX指定的权限内核任务拥有全部权限
                .region_addr = __PMP_REGION_ADDR(bss_start_addr,region_start_addr_size);
            },
            {  // stack
                .region_cfg = __PMP_REGIONP_CFG( 0,//L=0用户任务遵守RWX指定的权限内核任务拥有全部权限
                .region_addr = __PMP_REGION_ADDR(stack_start_addr,region_start_addr_size);
            },
		
        };

相关PMP操作的接口如下

// isolation_table增加一个region
PMP_add_region(isolation_region *isolation_table, void *region_address, xs_size_t size, int flag);
// isolation_table增加清除一个region
PMP_clear_region(isolation_region *isolation_table, void *region_address,xs_size_t size);
// 将isolation_table加载到PMP中
PMP_load(isolation_region *isolation_table, xs_uint8 coreid);
  1. ARM-32 架构任务隔离
    ARM32 的 handler mode 和 thread mode 分别对应特权模式和非特权模式。ARM32 架构的 MPU 单元可以对内存地址空间的访问权限进行设置从而实现任务地址空间的隔离访问。MPU 通过将内存空间划分为多个 “region” 进行权限设置,一个 region 就是一段连续的地址空间,对应之前定义的 isolation_region一般 MPU 支持设置8~16个regions。在启用MPU后程序就无法访问定义之外的地址区间也不得访问未经授权的region否则将会触发内存访问错误。对于正在运的行用户任务ARM32 的 isolation_table 的内容同 RISC-V 架构基本相同。
    相关MPU的操作接口如下
// isolation_table增加一个region
MPU_add_region(isolation_region *isolation_table, void *region_address, xs_size_t size, int flag);
// isolation_table增加清除一个region
MPU_clear_region(isolation_region *isolation_table, void *region_address, xs_size_t size);
// 将isolation_table加载到MPU中
MPU_load(isolation_region *isolation_table, xs_uint8 coreid);

性能测试

概述

下面分别测试XiUOS系统运行在基于 ARM 和 RISC-V 不同处理器的开发板时,任务的切换时间。

基于 ARM 处理器的任务切换性能测试

测试方法

为了测试系统的任务切换时间考虑使用GPIO管脚进行测试将GPIO在任务切换开始和结束时分别置为高电平和低电平。

  • 配置C13管脚为输出模式接示波器通道1或2
  • 示波器GND和开发板GND共地对接

RISCV TEST CONNECT

XiUOS的任务切换函数为xs_SwitchKthreadContext在SwitchKthreadContext函数入口位置将C13管脚置为高电平出口位置置为低电平则C13管脚保持高电平的时间即切换时间。

编程代码清单

void realtime_taskswitch_test()
{
    xs_PinMode(GPIO_C13, PIN_MODE_OUTPUT);
    xs_PinWrite(GPIO_C13, PIN_LOW);

    while(1){
        xs_DelayKThread(1);
    }
}

因为测试单板为cortex-m4单板该系列单板的线程切换是基于pendSV CPU异常进行线程切换因此下面基于该特点区分测试场景

  • 测量pendSV异常切换在SwitchKthreadContext函数入口处将C13管脚置为高电平在pendSV异常处理过程保存现场之后切换到目标任务之前将C13管脚置为低电平。得出的管脚电平时间即为带pendSV异常的的任务切换时间。
  • 只测了SwitchKthreadContext在SwitchKthreadContext函数入口处将C13管脚置为高电平在出口位置将C13管脚置为低电平。得出的管脚电平时间即为不计算pendSV异常的的任务切换时间。
xs_SwitchKthreadContext:
    /* 将GPIO C13置为高电平 */
    LDR  r2, =0x40020818         // 测试代码
    MOV  r3, #0x2000             // 测试代码
    STR  r3, [r2]                // 测试代码

    LDR  r2, =xs_KthreadSwitchInterruptFlag
    LDR  r3, [r2]
    CMP  r3, #1
    BEQ  _reswitch
    MOV  r3, #1
    STR  r3, [r2]

    LDR  r2, =xs_InterruptFromKthread
    STR  r0, [r2]

_reswitch:
    LDR  r2, =xs_InterruptToKthread
    STR  r1, [r2]

    LDR  r0, =NVIC_INT_CTRL
    LDR  r2, =NVIC_PENDSVSET
    STR  r1, [r0]

    /* 将GPIO C13置为低电平 */    
    LDR  r2, =0x4002081a        // 测试代码
    MOV  r3, #0x2000            // 测试代码
    STR  r3, [r2]               // 测试代码

    BX   LR

.global PendSV_Handler
.type  PendSV_Handler, %function
PendSV_Handler:
    MRS  r2, PRIMASK
    CPSID I

    LDR  r0, =xs_KthreadSwitchInterruptFlag
    LDR  r1, [r0]
    CBZ  r1, pendsv_exit

    MOV  r1, #0x00
    STR  r1, [r0]

    LDR  r0, =xs_InterruptFromKthread
    LDR  r1, [r0]
    CBZ  r1, switch_to_thread

    MRS  r1, psp

    STMFD  r1!, {r4 -  r11}

#if defined (__VFP_FP__) && !defined(__SOFTFP__)
    MOV  r4, #0x00

    TST  lr, #0x10
    MOVEQ r4, #0x01

    STMFD  r1!, {r4}
#endif

    LDR  r0, [r0]
    STR  r1, [r0]

switch_to_thread:
    /* 将GPIO C13置为低电平 */
    LDR  r2, =0x4002081a        // 测试代码
    MOV  r3, #0x2000            // 测试代码
    STR  r3, [r2]               // 测试代码

示波器测试选项设置

  • 通道设置
    • 耦合:直流
    • 带宽限制:关闭
    • 伏/格:粗调
    • 探头10X 电压
    • 反相:关闭
  • 触发设置
    • 类型:边沿
    • 信源: CH1
    • 斜率:上升
    • 模式:自动
    • 触发电压:略低于最高电平即可
  • 测量设置
    • 测量选通:开启
    • 类型:时间
    • 信源CH1
    • Scale500ns

测试结果

RISCV TEST CONNECT

从示波器测试结果上来看,单独测试 SwitchKthreadContext 的执行时间是1.26us。

RISCV TEST CONNECT

从示波器测试结果上来看,测试 SwitchKthreadContext 加上 pendSV 异常的的执行时间是17us。

基于 RISC-V 处理器的任务切换性能测试

测试方法

为了测试系统的任务切换时间考虑使用GPIO管脚进行测试将GPIO在任务切换开始和结束时分别置为高电平和低电平。

  • 配置GPIO18管脚为输出模式接示波器通道1或2
  • 示波器GND和开发板GND共地对接

RISCV TEST CONNECT

XiUOS的任务切换函数为 xs_SwitchKthreadContext在 SwitchKthreadContext 函数入口位置将 GPIO18 管脚置为高电平出口位置置为低电平则GPIO18管脚保持高电平的时间即切换时间。

编程代码清单

void realtime_taskswitch_test()
{
    xs_PinMode(GPIO_18, PIN_MODE_OUTPUT);
    xs_PinWrite(GPIO_18, PIN_LOW);
    while(1){
        xs_DelayKThread(1);
    }
}

初始化GPIO18为输出模式并初始化为低电平在while1当中调用delay函数每隔1个时间片发生一次调度。在下面的switch函数入口和出口位置操作GPIO。

.global xs_SwitchKthreadContext
xs_SwitchKthreadContext:
    /* 将 GPIO18 置为高电平 */
    lui  a5, 0x38001        // 测试代码
    addi a5, a5, 12         // 测试代码
    lw   a5, 0(a5)          // 测试代码
    sext.w  a4, a5          // 测试代码 
    lui  a5, 0x38001        // 测试代码
    addi a5, a5, 12         // 测试代码
    ori  a4, a4, 5          // 测试代码
    sext.w a4, a4           // 测试代码
    sw   a4, 0(a5)          // 测试代码

    addi  sp, sp, -32 * REGBYTES
    STORE sp, (a0)

    STORE x1, 0 * REGBYTES(sp)
    STORE x1, 1 * REGBYTES(sp)

    csrr a0, mstatus
    andi a0, a0, 8
    beqz a0, save_mpie
    li   a0, 0x80
> save_mpie: ...

> #ifdef XS_USING_SMP ...
  #endif
    /* 将GPIO18 置为低电平 */
    lui  a5, 0x38001         // 测试代码
    addi a5, a5, 12          // 测试代码
    lw   a5, 0(a5)           // 测试代码
    sext.w a4, a5            // 测试代码
    lui  a5, 0x38001         // 测试代码
    addi a5, a5, 12          // 测试代码
    addi a4, a4, -6          // 测试代码
    sext.w a4, a4            // 测试代码
    sw  a4, 0(a5)            // 测试代码

    j xs_switch_kthread_context_exit

示波器测试选项设置

  • 通道设置
    • 耦合:直流
    • 带宽限制:关闭
    • 伏/格:粗调
    • 探头10X 电压
    • 反相:关闭
  • 触发设置
    • 类型:边沿
    • 信源: CH1
    • 斜率:上升
    • 模式:自动
    • 触发电压:略低于最高电平即可
  • 测量设置
    • 测量选通:开启
    • 类型:时间
    • 信源CH1
    • Scale250ns

测试结果

RISCV TEST CONNECT

从示波器测试结果上来看,测试 SwitchKthreadContext 的执行时间是160ns。

任务切换性能测试对比

对sylixos的中断响应性能测试结果如下

操作系统 测试开发板 CPU 任务切换时间(ns)
sylixos mini210s开发板 ARM Cortex-A8 主频 1GHz 577.1
XiUOS KD233开发板 RISC-V K210 主频 400MHz 160
XiUOS STM32F407G-DISC1开发板 STM32f407 主频 168MHz 1260

结果分析:

  • XiUOS 在RISC-V K210 400MHz CPU主频上任务切换时间为 160 ns 低于 sylixos 的 577.1 ns
  • 若进行同等1 GHz 主频换算K210 上的任务切换时间应为 62.5 nsXiUOS 的任务切换的效率比 sylixos 提高 89.16%
  • 在ARM stm32f407 168MHz CPU主频任务切换时间 1260 ns高于1GHz主频测试的sylixos
  • 若进行同等1 GHz 主频换算STM32F407 上的任务切换时间应为 206.718 nsXiUOS 的任务切换的效率比 sylixos 提高 64.18%

由于XiUOS优化了任务切换的流程减少了执行指令数因此同等主频条件下任务切换时间更短。

使用场景

  • 在多处理器设备上,多个任务可以并行运行,从而提高处理器的利用率。
  • 在一些中断驱动的应用中,如果中断需要处理的工作过于复杂,则可以创建一个任务专门用于处理相关工作,从而改善中断延迟。