本篇内容比较简单, 但却很繁琐, 篇幅也很长, 毕竟是囊括了整个操作系统的生命周期. 这篇文章的目的是作为后续设计多任务开发的铺垫, 后续会单独再抽出一篇分析任务的相关知识. 另外本篇文章以单核 MCU 为背景, 并且以最新的 3.1.xLTS 版本源码进行分析. 主要内容目录如下:
基于 bsp/stm32/stm32f103-mini-system 为背景
Cortex-M3 的堆栈基础概念
C 语言 main 函数和 rt-thread 的 main
rt-thread 操作系统的传统初始化与自动初始化组件
任务是怎样运行起来的
Idle 任务与新的构想
基于 bsp/stm32/stm32f103-mini-system 的开机介绍
关于体系结构的知识这里不做过多的介绍, 因为这些知识要讲清楚的话足以写出一本大部头的书出来. 不过会简单介绍一些必要的东西.
Stm32f103 单片机是 cortex-m3 内核, 在 cortex-m3 内核中使用双堆栈 psp 和 msp, 模式分为线程模式和 handler 模式, 权限级别分为非特权级别和特权级别(现在只需要知道这么多就行了),handler 模式就是当处理发生中断的时候自动进入的模式, 其 handler 模式永远为特权级.
上电开机最开始运行的是 MCU 内部的 ROM 部分, 这部分代码我们通常看不到, 其通常是对芯片进行必要的初始化, 比如 FLASH 和 RAM 的时钟初始化等, 然后跳转到用户 flash 区域运行用户代码. 在 STM32 中用户 flash 地址从 0x08000000 开始. 我们写的代码都是从这里开始运行的. 其次由于 cortexM 规定其用户 FLASH 区域的最前面必须是一张中断向量表. 所以也就是说 STM32 的 0x08000000 开始是一张中断向量表, 这是必须的也是默认的, 当然在之后还可以重映射其它地方的向量表. 这张向量表中的第一项是一个栈地址, 第二项复位向量地址. 下面贴一段向量表部分代码(摘录自 startup_stm32f103xb.s):
- __Vectors DCD __initial_sp ; Topof Stack
- DCD Reset_Handler ; Reset Handler
- DCD NMI_Handler ; NMIHandler
- DCD HardFault_Handler ; Hard Fault Handler
- DCD MemManage_Handler ; MPU Fault Handler
- DCD BusFault_Handler ; Bus Fault Handler
- DCD UsageFault_Handler ; Usage Fault Handler
- DCD 0 ; Reserved
- DCD 0 ; Reserved
- DCD 0 ; Reserved
- DCD 0 ; Reserved
- DCD SVC_Handler ; SVCall Handler
- DCD DebugMon_Handler ; Debug Monitor Handler
- DCD 0 ; Reserved
- DCD PendSV_Handler ; PendSV Handler
- DCD SysTick_Handler ; SysTick Handler
另外需要注意的是开机后会自动进入复位异常, 通常我们叫上电复位过程, 不过意外的是上电复位处理的模式是特权级线程模式. 在特权模式下堆栈指针将使用 MSP, 非特权模式下可以被切换到 PSP.RT-Thread 操作系统就是这么做的. 所以回过头来看, 中断向量表第一项指定了 MSP 的栈起始地址, 并被自动加载到 MSP, 第二项指定了复位向量地址, 也被自动加载到 PC 并运行. 这样一来开机后我们能通过 debug 看到 PC 指针最先指向复位向量的第一条指令上. 我们看一下 stm32f103 在 armcc 编译器上的复位向量代码:
- ; Reset handler
- Reset_Handler PROC
- EXPORT Reset_Handler [WEAK]
- IMPORT __main
- IMPORT SystemInit
- LDR R0, =SystemInit
- BLX R0
- LDR R0, =__main
- BX R0
- ENDP
这是一段汇编代码, 其完成两件事, 第一件事调用 systemInit 函数完成一些初始化, 第二件事跳转到__main 函数. 其中 systemInit 函数我们是可以找到并可以修改的一个 C 语言实现的函数(暂时不讨论, 有兴趣的可以看 system_stm32f1xx.c). 而这个__main 就牛逼了, 这既不是我们自己写的 C 语言的 main 也看不到它在哪里实现的. 但是现在进入__main 后它就是会跑到你最终用 C 语言写的 main. 这个__main 的来龙去脉稍后会在第三部分分析.
Cortex-M3 的堆栈基础概念
在 Cortex-M3 的处理器内核上堆栈指针分为 PSP 和 MSP.handler 模式下总是使用 MSP, 线程模式可以通过 CONTROL 寄存器来配置(修改的时候必须处于特权模式才可以).
之所以需要这样设计就是为了将普通软件和系统软件通过权限隔离开, 避免普通用户权限操作系统关键资源带来安全风险. 当我们使用带有操作系统的环境进行开发时, 操作系统就会将关键操作例如任务切换, 中断处理等在特权模式操作. 而其它的操作都会运行在非特权模式下完成.
操作系统一般都会将必要的操作封装出 API 接口, 以提供给普通软件调用. 而这背后的设计思想就是通过触发异常, 然后进入特权模式运行异常向量处理程序. 而这段异常处理程序早就让操作系统实现了, 进而这部分特权操作是操作系统接管处理的. 这也就避免用户普通软件去进行不必要的特权操作. 例如用户任务想主动放弃 CPU 从而调用 yield,yield 将进行任务切换, 其中过程大概是 "选出另一个任务"->"触发 SVC 或者 Pendsv 异常"->进入 SVC/Pendsv 的 handler 异常处理程序, 此时是特权模式, 完成操作后返回到新任务运行. 在 RT-Thread 中进入任务切换是通过触发 Pendsv 异常.
C 语言 main 函数和 RT-Thread 的 main
前面提到过开机启动最后进入复位向量处运行, 最终调用__main 就跑到我们外面写的 C 语言的 main 函数了. 但这并非这么简单, 在从__main 到我们的 main 中间还有一系列操作比如初始化堆栈, 初始化全局变量区域, 初始化 C 运行时库等, 然后再在最后调用用户的 main 函数.
不过在不同的编译器上这个__main 并非是固定的, 这里也就 armcc 是如此, 如果是 GCC 和 IAR 的话其就不太一样, 不过不影响我们分析核心主题. 这里仅以借用 armcc 为例来分析主题中心思想. 另外在说明 RT-Thread 中开启 RT_USING_USER_MAIN 的时候在 ARMCC 编译器上还有一个支持挂钩的操作, 这种操作一般见于补丁修复的时候. 其实现方式是在原有函数的名字前加上 $Sub$$ 前缀就可以将原有函数劫持下来, 并通过加上 $Super$$ 前缀再调用原始函数. 具体如下: The followingexample shows how to use $Super$$and $Sub$$ to insert a callto the function ExtraFunc() before the call to the legacy functionfoo().
- extern void ExtraFunc(void);
- extern void $Super$$foo(void);
- /* this functionis called instead of the original foo() */
- void $Sub$$foo(void)
- {
- ExtraFunc(); /* does some extra setup work */
- $Super$$foo(); /* calls the original foo() function */
- /* To avoid calling the original foo() function
- * omit the $Super$$foo(); function call.
- */
- }
上例中原本有一个原始函数叫做 foo, 但是现在通过 $Sub$$foo 来劫持所有调用 foo 的地方, 自动会调用 $Sub$$foo, 然后新的 $Sub$$foo 里面先调用自己的扩展实现 ExtraFunc 后, 再接着调用原始版本的 foo 函数, 不过调用原始的 foo 是加了前缀 $Super$$ 的 $Super$$foo.
当使用 RT-Thread 操作系统开启 RT_USING_USER_MAIN 后就是利用这种骚操作来完成 RT-Thread 操作系统的初始化过程的.(代码摘录自 components.c)
- extern int $Super$$main(void);
- /* re-definemain function */
- int $Sub$$main(void)
- {
- rtthread_startup();
- return 0;
- }
关于 rtthread_startup 函数稍后再讲解, 不过先接着看下面这个函数:
- /* the systemmain thread */
- void main_thread_entry(void*parameter)
- {
- extern int main(void);
- extern int $Super$$main(void);
- /* RT-Thread components initialization*/
- rt_components_init();
- /* invoke system main function */
- #if defined(__CC_ARM) || defined(__CLANG_ARM)
- $Super$$main(); /* for ARMCC. */
- #elif defined(__ICCARM__) || defined(__GNUC__)
- main();
- #endif
- }
上面这个函数其实是个小任务, 就是完成组件初始化后再跳转到用户 main 函数的. 这个小任务在 rtthread_startup 中调用 rt_application_init 时创建的, 所以此时 rt-thread 系统早就以经跑起来了. 也就是说当调用 rtthread_startup 后正常情况就不再会返回到原来的调用地方, 接下来会交给系统的调度器去接管, 切换运行任务去了. 看下面的代码了解 rt_application_init:
- void rt_application_init(void)
- {
- rt_thread_t tid;
- #ifdef RT_USING_HEAP
- tid = rt_thread_create("main", main_thread_entry, RT_NULL,
- RT_MAIN_THREAD_STACK_SIZE, RT_MAIN_THREAD_PRIORITY, 20);
- RT_ASSERT(tid != RT_NULL);
- #else
- rt_err_t result;
- tid = &main_thread;
- result = rt_thread_init(tid, "main", main_thread_entry, RT_NULL,
- main_stack, sizeof(main_stack), RT_MAIN_THREAD_PRIORITY, 20);
- RT_ASSERT(result == RT_EOK);
- /* if not define RT_USING_HEAP, using toeliminate the warning */
- (void)result;
- #endif
- rt_thread_startup(tid);
- }
至此, 关于各种 main 的子子孙孙以经差不多了解清楚了, 其流程大概如下:
ResetHandle->__main->$Sub$$main->(rtthread_startup->rt_application_init->main_thread_entry)->$Super$$main.
其中 $Super$$main 就是我们的用户 main 函数. 如果没有启用 RT_USING_USER_MAIN 那就简单了, 其流程如下:
ResetHandle->__main->main
接下来再接着分析 $Sub$$main 中调用的 rtthread_startup 函数.
RT-Thread 操作系统的传统初始化与自动初始化组件
这里着重讨论 rtthread_startup 函数, 因为这就是 RT-Thread 操作系统的入口和初始化流程. 不过既然说到 rtthread_startup 函数了, 就不得不一起介绍一下 RT-Thread 操作系统的自动初始化组件了.
rtthread_startup 函数是一个函数调用链, 依次调用各个阶段的初始化函数, 并在最后启动调度器不再返回. 代码摘录自 components.c
- int rtthread_startup(void)
- {
- rt_hw_interrupt_disable();
- /* board level initialization
- * NOTE: please initialize heap insideboard initialization.
- */
- rt_hw_board_init();
- /* show RT-Thread version */
- rt_show_version();
- /* timer system initialization */
- rt_system_timer_init();
- /* scheduler system initialization */
- rt_system_scheduler_init();
- #ifdef RT_USING_SIGNALS
- /* signal system initialization */
- rt_system_signal_init();
- #endif
- /* create init_thread */
- rt_application_init();
- /* timer thread initialization */
- rt_system_timer_thread_init();
- /* idle thread initialization */
- rt_thread_idle_init();
- /* start scheduler */
- rt_system_scheduler_start();
- /* never reach here */
- return 0;
- }
以上代码我们主要脉络是这样的: 先关闭全局中断 ->初始化硬件板上的资源 ->打印 RT-Thread 的 LOGO->系统定时器功能初始化 ->调度器初始化 ->signal 功能初始化 ->应用程序初始化 (这个通常是用来创建用户任务的)-> 系统软 timer 任务初始化 ->系统 idle 任务初始化 ->启动调度器, 永远不再返回.
这里我们先来说一下为什么要先关闭全局中断, 因为在初始化过程中, 有可能 MCU 就有其它的中断和异常触发了, 这个时候系统还没有初始化完成, 这就势必导致系统出现故障, 所以先关闭全局中断, 并在启动调度器后再打开.
rt_hw_board_init 非常关键, 在这个函数里面必须完成一些必须的初始化过程: 堆内存系统的初始化和硬件资源模块以及如果开启了自动初始化组件时还需要调用 rt_components_board_init 完成必要的初始化, 这个函数是自动初始化组件的一个接口.(代码摘录自 bsp\stm32\libraries\HAL_Drivers\drv_common.c)
- RT_WEAK void rt_hw_board_init()
- {
- #ifdef SCB_EnableICache
- /* EnableI-Cache---------------------------------------------------------*/
- SCB_EnableICache();
- #endif
- #ifdef SCB_EnableDCache
- /* Enable D-Cache---------------------------------------------------------*/
- SCB_EnableDCache();
- #endif
- /* HAL_Init() function is called at thebeginning of the program */
- HAL_Init();
- /* System clock initialization */
- SystemClock_Config();
- rt_hw_systick_init();
- /* Heap initialization */
- #if defined(RT_USING_HEAP)
- rt_system_heap_init((void*)HEAP_BEGIN, (void*)HEAP_END);
- #endif
- /* Pin driver initialization is open bydefault */
- #ifdef RT_USING_PIN
- rt_hw_pin_init();
- #endif
- /* USART driver initialization is openby default */
- #ifdef RT_USING_SERIAL
- rt_hw_usart_init();
- #endif
- /* Set the shell console output device*/
- #ifdef RT_USING_CONSOLE
- rt_console_set_device(RT_CONSOLE_DEVICE_NAME);
- #endif
- /* Board underlying hardwareinitialization */
- #ifdef RT_USING_COMPONENTS_INIT
- rt_components_board_init();
- #endif
- }
然后回到 rtthread_startup 函数中再看 rt_application_init 函数, 由于我们是用的 stm32 的 BSP, 这个 bsp 系列是使用自动初始化组件和 RT_USING_USER_MAIN 功能的, 所以过程稍微隐蔽一些, 先是在 rt_application_init 中创建了一个小任务, 然后再在小任务中调用了 rt_components_init, 这也是自动初始化组件的接口. 如果没有开启自动初始化组件的话, 通常我们的用户任务可以在 rt_application_init 中创建了. 也可以像这里的实现一样, 先创建一个小任务, 然后再在小任务里完成一些初始化和创建用户任务.
然后再回到 rthtread_startup 中看到有初始化软 timer 和 idle 任务的, 其中软件 timer 功能是可以通过裁剪配置选择的, 如果打开后就可以在后续创建 softtimer. 否则所有的 timer 都会在 OS TICK 的中断上下文中计时. 另外这个 idle 任务也是系统中必不可少和优先级最低的任务. 即使我们启动调度器后没有创建任何用户任务, 系统中也有一个 idle 任务在运行. Idle 任务的优先级最低, 在此我建议开发人员最好不要将自己的用户任务优先级配置成最低以免和 idle 竞争时间片, 这会给你今后的开发带来不必要的麻烦. 关于这个问题, 我最后会提出一些新的设计构想. 不过这里先要介绍一下 idle 任务的功能. Idle 任务会在系统空闲时被调度运行, 所以我们通常在 idle 任务里做低功耗设计. 其次 idle 任务里还会完成系统资源的回收. 例如被删除的任务, 被删除的 module 等.
最后 rthtread_startup 启动调度器 rt_system_scheduler_start 开始调度系统的任务, 从此就开始运行任务, 不再返回. 这里又要记住一个概念, 在上文提到的 PSP 和 MSP, 到目前为止 MCU 还是使用一开始中断向量表中指定的 MSP 栈. 但是当调度任务后, 任务会有自己的栈, 且 rt-thread 系统会将任务的栈切换到 PSP 栈指针. 值得注意的是, 这个 MSP 是全局共享的, 所有的中断程序都会使用这个栈空间, 所以我们需要根据自己的情况来配置这个 MSP 栈的空间大小.
接下来我们再来介绍自动初始化组件. RT-Thread 中的自动初始化组件思路来自于 Linux 内核. 其实现手段是将需要初始化的函数接口通过链接器指令放在特殊的 section 中. 这个 section 的概念是当我们程序最终链接成一个 image 后会形成一个标准格式的文件, 其中 armcc 中叫做 ARM ELF. 详细的介绍可以查阅官方资料. 其中 ELF 文件就有将代码分成称为 section 的区域, 可以称作段. 并且可以指定自己的代码放在指定名称的段中, 且可以指定这个 section 段的 ROM 地址. 这样当我们设计玩初始化接口后, 通过链接器的指令以及链接脚本文件将我们的初始化代码放在特定的地方, 并且利用命名规则来做到顺序排序. 等需要调用初始化的时候可以利用这些 section 的地址转换成函数指针直接批量循环调用. 通常你会在 MDK 的工程文件链接器参数中看到这样的指令:--keep *.o(.rti_fn.*), 这是为了在链接阶段保证这些自定义段不被删除. 同时也可以看出 rti_fn 就是自动初始化组件的 section 名字. 类似的将函数放置在这些段中的链接器指令如下:(摘录自 rtdef.h)
- /*initialization export */
- #ifdef RT_USING_COMPONENTS_INIT
- typedef int (*init_fn_t)(void);
- #ifdef _MSC_VER/* we do notsupport MS VC++ compiler */
- #define INIT_EXPORT(fn,level)
- #else
- #if RT_DEBUG_INIT
- struct rt_init_desc
- {
- const char* fn_name;
- const init_fn_t fn;
- };
- #define INIT_EXPORT(fn, level) \
- const char __rti_##fn##_name[] =#fn; \
- RT_USED const struct rt_init_desc __rt_init_desc_##fn SECTION(".rti_fn."level)=\
- { __rti_##fn##_name, fn};
- #else
- #define INIT_EXPORT(fn, level) \
- RT_USED const init_fn_t __rt_init_##fn SECTION(".rti_fn."level)= fn
- #endif
- #endif
- #else
- #define INIT_EXPORT(fn, level)
- #endif
- /* board initroutines will be called in board_init() function */
- #define INIT_BOARD_EXPORT(fn) INIT_EXPORT(fn,"1")
- /*pre/device/component/env/App init routines will be called in init_thread */
- /* componentspre-initialization (pure software initilization) */
- #define INIT_PREV_EXPORT(fn) INIT_EXPORT(fn,"2")
- /* deviceinitialization */
- #define INIT_DEVICE_EXPORT(fn) INIT_EXPORT(fn,"3")
- /* componentsinitialization (dfs, lwip, ...) */
- #define INIT_COMPONENT_EXPORT(fn) INIT_EXPORT(fn,"4")
- /* environmentinitialization (mount disk, ...) */
- #define INIT_ENV_EXPORT(fn) INIT_EXPORT(fn,"5")
- /* appliationinitialization (rtgui application etc ...) */
- #define INIT_APP_EXPORT(fn) INIT_EXPORT(fn,"6")
其中不同的数字代表不同的初始化顺序, 可以根据需要来选择. 接着如上文提到的两个函数 rt_components_board_init 和 rt_components_init 是如何实现的: 摘录自 components.c
- #ifdef RT_USING_COMPONENTS_INIT
- /*
- * Components Initialization will initializesome driver and components as following
- * order:
- * rti_start --> 0
- * BOARD_EXPORT --> 1
- * rti_board_end --> 1.end
- *
- * DEVICE_EXPORT --> 2
- * COMPONENT_EXPORT --> 3
- * FS_EXPORT --> 4
- * ENV_EXPORT --> 5
- * APP_EXPORT --> 6
- *
- * rti_end --> 6.end
- *
- * These automatically initialization, thedriver or component initial function must
- * be defined with:
- * INIT_BOARD_EXPORT(fn);
- * INIT_DEVICE_EXPORT(fn);
- * ...
- * INIT_APP_EXPORT(fn);
- * etc.
- */
- static int rti_start(void)
- {
- return 0;
- }
- INIT_EXPORT(rti_start,"0");
- static int rti_board_start(void)
- {
- return 0;
- }
- INIT_EXPORT(rti_board_start,"0.end");
- static int rti_board_end(void)
- {
- return 0;
- }
- INIT_EXPORT(rti_board_end,"1.end");
- static int rti_end(void)
- {
- return 0;
- }
- INIT_EXPORT(rti_end,"6.end");
- /**
- * RT-Thread Components Initialization forboard
- */
- void rt_components_board_init(void)
- {
- #if RT_DEBUG_INIT
- int result;
- const struct rt_init_desc *desc;
- for (desc = &__rt_init_desc_rti_board_start; desc <&__rt_init_desc_rti_board_end; desc ++)
- {
- rt_kprintf("initialize %s", desc->fn_name);
- result = desc->fn();
- rt_kprintf(":%d done\n", result);
- }
- #else
- const init_fn_t *fn_ptr;
- for (fn_ptr = &__rt_init_rti_board_start; fn_ptr <&__rt_init_rti_board_end; fn_ptr++)
- {
- (*fn_ptr)();
- }
- #endif
- }
- /**
- * RT-Thread Components Initialization
- */
- void rt_components_init(void)
- {
- #if RT_DEBUG_INIT
- int result;
- const struct rt_init_desc *desc;
- rt_kprintf("do components initialization.\n");
- for (desc = &__rt_init_desc_rti_board_end; desc < &__rt_init_desc_rti_end; desc++)
- {
- rt_kprintf("initialize %s", desc->fn_name);
- result = desc->fn();
- rt_kprintf(":%d done\n", result);
- }
- #else
- const init_fn_t *fn_ptr;
- for (fn_ptr = &__rt_init_rti_board_end; fn_ptr <&__rt_init_rti_end; fn_ptr++)
- {
- (*fn_ptr)();
- }
- #endif
- }
之所以要分开这两个函数就是因为 board 阶段的初始化比其它普通的组件初始化早, board 阶段的初始化通常没什么系统资源依赖. 而其它情况下则通常在操作系统已经完成必要的初始化后才能做的初始化才会放在 rt_components_init 里.
任务是怎样运行起来的
要说明任务是怎么运行起来的, 就得知道任务是怎么创建的, 其次结合之前写的文章 <源码解读. RT-Thread 多任务调度算法> 就差不多了. 那么这里就介绍一下任务的创建. 照样用上面的 rt_application_init 里创建任务的代码来举例:
- void rt_application_init(void)
- {
- rt_thread_t tid;
- #ifdef RT_USING_HEAP
- tid = rt_thread_create("main", main_thread_entry, RT_NULL,
- RT_MAIN_THREAD_STACK_SIZE, RT_MAIN_THREAD_PRIORITY, 20);
- RT_ASSERT(tid != RT_NULL);
- #else
- rt_err_t result;
- tid = &main_thread;
- result =rt_thread_init(tid, "main", main_thread_entry, RT_NULL,
- main_stack, sizeof(main_stack),RT_MAIN_THREAD_PRIORITY, 20);
- RT_ASSERT(result == RT_EOK);
- /* if not define RT_USING_HEAP, using toeliminate the warning */
- (void)result;
- #endif
- rt_thread_startup(tid);
- }
首先要说明的是 RT-Thread 任务创建有两种, 一种是动态的, 一种是静态的. 所谓的动态就是其任务栈自动在堆内存中分配; 静态是用户自己指定栈空间, 当然通常这个栈来自于用户定义的数组. 如上例中当 RT_USING_HEAP 宏被打开, 也就是有堆内存的时候会采用 rt_thread_create 接口来创建动态资源的任务. 当然可以利用 rt_thread_init 来创建一个静态资源的任务. 先来了解一下这两个函数在创建任务时的一些参数:"main" 这是任务的名称, 任务名称用一个字符串来指定, 不是很重要, 不过最好能起到一定的说明性, 有利于今后调试用. main_thread_entry 这是任务的入口函数, 所谓的任务就是一个 C 语言中的函数而已. RT_NULL, 这是传给任务入口函数的参数, 如果没有就为 NULL. 因为 RT_Thread 中的任务原型为: void (*entry)(void*parameter);RT_MAIN_THREAD_STACK_SIZE 为任务的栈大小, 以字节为单位. RT_MAIN_THREAD_PRIORITY 为任务的优先级号. 20 为任务的时间片大小. 其中静态任务中还有 tid 代表任务的 TCB 数据结构句柄. main_stack 为栈空间起始地址. 当用动态创建的方法创建成功后会返回一个任务的 TCB 任务句柄出来. 之后我们利用 rt_thread_startup(任务句柄)的形式启动任务即可. 例如上例中 rt_thread_startup(tid); 不过 rt_thread_startup 函数真正的功能是将任务放置于调度队列中, 并置任务状态为 ready, 由此交给调度器去调度, 能不能立马运行取决与调度器的调度. 一般情况下, 要想任务获得运行必须满足的条件: 调度器已经运行, 任务已经 ready, 没有更高优先级任务, 没有中断发生. 只要条件满足调度器就会调度此任务, 做好必要的栈初始化和状态置位, 就会切换到任务开始运行. 只要任务获得运行就会使用创建任务时指定的栈空间.
不过一般的任务通常是一直运行, 持续的服务. 形式如下:
- void task(void *parameter)
- {
- while (1)
- {
- // do_work();
- }
- }
idle 任务与新的构想
上面解释过 idle 任务在 rt-thread 操作系统中的功能: 释放资源, 低功耗设计.
关于资源释放通常是任务的析构过程, 这就是任务的结束. 例如上例中的 main_thread_entry 任务之所以称为小任务的原因就是它做完事情就结束了. 那么可能就会想, 既然任务都结束了那么它的资源如何释放呢? 比如栈空间, TCB 等. 这就是 idle 该干的事情. 即使所有的用户任务都结束, 最后也会剩下 idle 任务在运行. 如果有必要的话, 可以在 idle 任务中可以通过调用低功耗组件进入低功耗或者干脆调用电源开关控制来关机.
其次 idle 任务占用了最低优先级. 虽然用户任务也可以使用和 idle 任务相同的优先级, 但是并不建议这样做, 比如在低功耗设计时就会出问题. 另外我个人在思考一个问题, idel 任务既然以经在设计之初就明确了其获得运行的条件, 那么何不做成无需优先级的任务, 唯一的调度决策就是: 当调度器没有任务处于 ready 状态时就切换到 idel 任务运行. 这就无需关注最低优先级被 idle 霸占的问题了.
来源: https://www.cnblogs.com/rocotona/p/11162233.html