【学习笔记】一种特别有意思的 RTOS 任务切换方法

一、介绍说明

目前常见流行的 RTOS 实现方式,如 FreeRTOS、uCosII、RT-Thread 等等,它们的内部的任务切换实现原理都差不多,都是通过借助汇编,根据不同的情况读写 CPU 寄存器(R0~R15)来实现保护现场和恢复现场以及指令跳转,效率很高,但也就意味着很难做到跨平台使用。

前段时间朋友向我推荐了一款非常精巧的 OS (cocoOS),无意中发现其内部实现的任务切换机制特别地有意思,竟然未涉及到 CPU 的寄存器,纯靠 C 语言的语法实现任务切换。这就意味着很容易地跨平台使用,除了需要提供时基以外,几乎不需要做任何改动即可投入使用,这着实让我惊奇不已。

官方网站:www.cocoos.net  代码仓库:github.com/cocoOS/cocoOS

总结来说:cocoOS 是通过代码行号 __LINE__ 和借助 switch() 和 case 实现执行位置的记录和跳转,再通过 return 实现中断任务。

二、原理解析 

以下是一份可以在 Linux 跑的完整示例代码:

#include <stdio.h>
#include <pthread.h>
#include <unistd.h>
#include <time.h>
#include <cocoos.h>

static void *ticker(void* arg) 
{
	struct timespec req = {.tv_sec = 0,.tv_nsec = 1000000};
	while(1) {
		nanosleep(&req, NULL);
		os_tick();
	}
	return (void*)0;
}

static void system_setup(void)
{
	pthread_t tid;

	if (pthread_create(&tid, NULL, &ticker, NULL)) {
		printf("can't create thread\n");
		while(1) usleep(1000);
	}
	return ;
}

static void myTask1() 
{
	task_open();
	for (;;) 
	{
		printf("[myTask1] -> sleep 1000 ms\n");
		task_wait(1000);
		printf("[myTask1] -> sleep done\n")
	}
	task_close();
}

static void myTask2() 
{
	task_open();
	for (;;) 
	{
		printf("[myTask2] -> sleep 3000 ms\n");
		task_wait(3000);
		printf("[myTask2] -> sleep done\n")
	}
	task_close();
}

int main(void) {

	system_setup();
	
	os_init();
	
	task_create( myTask1, NULL, 10, 0, 0, 0 );
	task_create( myTask2, NULL, 20, 0, 0, 0 );
	
	os_start();

	return 0;
}

执行的结果: 

与其它 RTOS 不一样的是,每个任务实体中多出了 task_open()\task_close(),它俩实际上是宏定义:

#define task_open()		OS_BEGIN
#define OS_BEGIN		uint16_t os_task_state = os_task_internal_state_get(running_tid);\
						switch ( os_task_state )\
						{ \
						case 0:

#define task_close()	OS_END
#define OS_END			os_task_kill(running_tid);\
						running_tid = NO_TID;\
						return;}

再看 task_wait() 的实现,其实际也是一个宏定义:

#define task_wait(x)			OS_WAIT_TICKS(x,0)

#define OS_WAIT_TICKS(x,y)		do {\
								    os_task_wait_time_set( running_tid, y, x );\
								    OS_SCHEDULE(0);\
								} while ( 0 )
								
#define OS_SCHEDULE(ofs)		os_task_internal_state_set(running_tid, __LINE__+ofs);\
								running_tid = NO_TID;\
								return;\
								case (__LINE__+ofs):

从这两个宏的展开实现,就可以看出任务调度的实现原理,可以通过 gcc 来看 myTask1 任务宏展开后的代码对比:

第 3 行的 os_task_state 会在创建任务时被默认设置为 0,因此任务首次执行时会进入到 for 循环中,当任务进入 for 循环后调用 task_wait(100) 时,会执行 11 ~ 15 行代码,通过  __LINE__ 来得到当前代码行号为 34,然后调用 os_task_internal_state_set() 保存,并作为 case 34 条件,然后设置任务状态为 WAITING_TIME,再通过 15行的 return 返回,该任务结束运行,回归 os 调度下一个任务。

uint8_t task_create( taskproctype taskproc, void *data, uint8_t prio, Msg_t *msgPool, uint8_t poolSize, uint16_t msgSize ) {
    ......
    task->internal_state = 0;    // 默认设置为 0 
    task->taskproc = taskproc;
    ......
    return task->tid;
}

uint16_t os_task_internal_state_get( uint8_t tid ) {
    return task_list[ tid ].internal_state;
}

void os_task_internal_state_set( uint8_t tid, uint16_t state ) {
    task_list[ tid ].internal_state = state;
}
void os_task_wait_time_set( uint8_t tid, uint8_t id, uint32_t time ) {
    os_assert( tid < nTasks );
    os_assert( time > 0 );
    
    task_list[ tid ].clockId = id;
    task_list[ tid ].time = time;
    task_waiting_time_set( tid );
}

static void task_waiting_time_set( uint8_t tid ) {
    task_list[ tid ].state = WAITING_TIME;
}

与此同时 os_tick() 会间隔 1 毫秒检查这些任务,myTask1 的等待时间到达,会被设置为就绪态 READY。OS 开始调度 myTask1 ,此时 myTask1 函数会再次调用,通过 os_task_internal_state_get() 来获取之前设置的行号 34,作为 switch(34) 的条件,满足 case 34 条件从而跳转到所设置行号的空指令执行。接者就执行到了第 19 行代码。

这里是 main() 调用的 os_start() 的实现:

void os_start( void ) {
    running = 1;
    os_enable_interrupts();

    for (;;) {
        os_schedule();  // 不停的进行 OS 任务执行和调度
    }
}

static void os_schedule( void ) {

    running_tid = NO_TID;

#ifdef ROUND_ROBIN
    /* Find next ready task */
    running_tid = os_task_next_ready_task();
#else
    /* Find the highest prio task ready to run */
    running_tid = os_task_highest_prio_ready_task();   // 寻找已就绪的最高优先级任务
#endif
    
    if ( running_tid != NO_TID ) {  // NO_TID 为 255
        os_task_run();  // 运行任务
    }
    else {
        os_cbkSleep();
    }
}

void os_task_run( void ) {
    os_assert( running_tid < nTasks );
    task_list[ running_tid ].taskproc();  // 调用任务函数
}

 三、优缺点

缺点:

从 cocoOS 的任务切换实现原理可以确定,该内核并非是抢占式内核,也未曾实现互斥锁机制,并且每个任务函数在被调度时都会被重新调用,因此在应用时,任务内部的变量最好是静态变量。任务切换的效率上自然比不上目前流行的 CPU 寄存器保存和恢复现场以及代码跳转。

优点:

cocoOS 尽管有上述不足,但凭着其巧妙的设计完完全全的避开了不同芯片平台之间的差异,几乎是拿来即可编译使用。cocoOS 也支持信号量、事件、消息队列等基本的任务通信机制,并且可裁剪,使用的是静态数组,对硬件资源占用非常小,内核实现也很简单易懂。在一些硬件资源比较紧张的 MCU 上,需要实现一些较为复杂的业务逻辑,都可以上这套 cocoOS。

四、思考

这 OS 唯一让我觉得比较别扭的是每次任务调度,都会重头开始执行任务函数。我觉得应该可以通过 setjmp() / longjmp() 来实现记录和跳转,这样任务实体就和其它常见的 RTOS 一样,不需要额外的显式增加奇怪的函数,也不会每次任务调度都会重新执行任务函数。

posted @ 2022-04-08 00:45  Love_梦想  阅读(112)  评论(0编辑  收藏  举报