【学习笔记】一种特别有意思的 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 一样,不需要额外的显式增加奇怪的函数,也不会每次任务调度都会重新执行任务函数。
【学习笔记】一种特别有意思的 RTOS 任务切换方法的更多相关文章
- WebGL three.js学习笔记 6种类型的纹理介绍及应用
WebGL three.js学习笔记 6种类型的纹理介绍及应用 本文所使用到的demo演示: 高光贴图Demo演示 反光效果Demo演示(因为是加载的模型,所以速度会慢) (一)普通纹理 计算机图形学 ...
- C#数字图像处理算法学习笔记(一)--C#图像处理的3中方法
C#数字图像处理算法学习笔记(一)--C#图像处理的3中方法 Bitmap类:此类封装了GDI+中的一个位图,次位图有图形图像及其属性的像素数据组成.因此此类是用于处理像素数据定义的图形的对象.该类的 ...
- [ 原创 ]学习笔记-三种向ListView中填充简单文本的方法
Android 中ListView是很重要的一块内容 掌握ListView的基本用法 对学习安卓起着举足轻重的作用 今天就介绍一下三种向ListView 填充简单文本的方法 填充其他数据类型的用法之后 ...
- Dynamic CRM 2013学习笔记(十七)JS读写各种类型字段方法及技巧
我们经常要对表单里各种类型的字段进行读取或赋值,下面列出各种类型的读写方法及注意事项: 1. lookup 类型 清空值 var state = Xrm.Page.getAttribute(" ...
- Dynamic CRM 2013学习笔记(二十七)无代码 复制/克隆方法
前面介绍过二种复制/克隆方法:<Dynamic CRM 2013学习笔记(十四)复制/克隆记录> 和<Dynamic CRM 2013学习笔记(二十五)JS调用web service ...
- JNI学习笔记_Java调用C —— 非Android中使用的方法
一.学习笔记 1.java源码中的JNI函数本机方法声明必须使用native修饰. 2.相对反编译 Java 的 class 字节码文件来说,反汇编.so动态库来分析程序的逻辑要复杂得多,为了应用的安 ...
- 【Unity 3D】学习笔记三十五:游戏实例——摄像机切换镜头
摄像机切换镜头 在游戏中常常会切换摄像机来观察某一个游戏对象,能够说.在3D游戏开发中,摄像头的切换是不可或缺的. 这次我们学习总结下摄像机怎么切换镜头. 代码: private var Camera ...
- .NET 5学习笔记(10)——Entity Framework Core之切换SQLServer和SQLite
上一篇我们梳理了CodeFist的一般流程,本篇我们讨论如何在一套代码中,支持SQL Server和SQLite的切换.同时从本篇开始,我们从.NET Core 3.1 迁移到.NET 5.相信.NE ...
- Redis学习笔记--五种数据类型的使用场景
String 1.String 常用命令: 除了get.set.incr.decr mget等操作外,Redis还提供了下面一些操作: 获取字符串长度 往字符串append内容 设置和获取字符串的某一 ...
- Python学习笔记——几种数据类型
1. 列表list: Python内置的一种数据类型是列表:list,用中括号[]表示.list是一种有序的集合,可以随时添加和删除其中的元素,而且元素的类型不必相同.list可以通过下标来访问,范围 ...
随机推荐
- linux下搭建oh-my-zsh环境
目标:因为用习惯了zsh的shell环境,所以习惯在服务器上也搭建zsh环境,但是每次搭建都需要Google每一步骤,感觉很麻烦,所以决定记录一下,免得一次次查 1. 安装zsh zsh是一款shel ...
- 常用到的read命令
记录一下. 几个简单参数介绍 read -p :显示提示信息 read -s :静默模式(Silent mode),不会在屏幕上显示输入的字符.当输入密码和其它确认信息的时候,这是很有必要的. rea ...
- WinDBG详解进程初始化dll是如何加载的
一:背景 1.讲故事 有朋友咨询个问题,他每次在调试 WinDbg 的时候,进程初始化断点之前都会有一些 dll 加载到进程中,比如下面这样: Microsoft (R) Windows Debugg ...
- ubuntu生成pem证书连接服务器(已验证)
SSH 密钥认证是什么? 与用户密码登录相比,SSH 密钥认证更安全,因为只有拥有密钥的人才能连接,并且密钥通过不同的算法进行了很好的加密.它还通过无密码登录使 SSH 连接变得简单. 这个搞两个方案 ...
- springboot的全局异常处理类
import lombok.extern.slf4j.Slf4j; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import or ...
- Installing ClickHouse-22.10.2.11 on openEuler
一.Installing ClickHouse-22.10.2.11 on openEuler 1 地址 https://clickhouse.com https://packages.clickho ...
- 树莓派(香橙派)通过.NET IoT 操作SPI编写屏幕驱动 顺手做个四足机器人(一)
摘要 这片文章主要是记录自己的整活过程,涉及到的技术包括.NET IoT, .NET Web, .NET MAUI,框架采用的也是最新的.NET 7. 本人是用的树莓派Zero 2 W(ubuntu- ...
- i春秋Zone
打开网页是个简单的表单填写, 尝试注入....没用 查看源码,没找到什么有用的信息 只有抓包了 发现一个cookie的login值为0,改为1试试 没什么特别的回显,但这应该就是登录与否的判定了,所以 ...
- i春秋who are you
打开题目网页,抓包,查源码都一无所获,然后就去看cookie,发现一个role变量,刚开始也不知道这个变量是什么,其值也是没有规律的一串字符串.看了别人源码才知道这是base64加密后的字符串,将其用 ...
- A-深度学习面试题
目录 目录 一,滤波器与卷积核 二,卷积层和池化输出大小计算 2.1,CNN 中术语解释 2.2,卷积输出大小计算(简化型) 2.3,理解边界效应与填充 padding 参考资料 三,深度学习框架的张 ...