源碼解讀·RT-Thread操作系統從開機到關機

来源:https://www.cnblogs.com/rocotona/archive/2019/07/10/11162233.html
-Advertisement-
Play Games

本篇內容比較簡單,但卻很繁瑣,篇幅也很長,畢竟是囊括了整個操作系統的生命周期。這篇文章的目的是作為後續設計多任務開發的鋪墊,後續會單獨再抽出一篇分析任務的相關知識。另外本篇文章以單核MCU為背景,並且以最新的3.1.xLTS版本源碼進行分析。主要內容目錄如下: 基於bsp/stm32/stm32f1 ...


本篇內容比較簡單,但卻很繁瑣,篇幅也很長,畢竟是囊括了整個操作系統的生命周期。這篇文章的目的是作為後續設計多任務開發的鋪墊,後續會單獨再抽出一篇分析任務的相關知識。另外本篇文章以單核MCU為背景,並且以最新的3.1.xLTS版本源碼進行分析。主要內容目錄如下:

  1. 基於bsp/stm32/stm32f103-mini-system為背景

  2. Cortex-M3的堆棧基礎概念

  3. C語言main函數和rt-thread的main

  4. rt-thread操作系統的傳統初始化與自動初始化組件

  5. 任務是怎樣運行起來的

  6. 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霸占的問題了。

 

感謝各位網友的支持,可以關註我的微信公眾號:鵬城碼夫   (微信號:rocotona)


您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • 本文主要學習在Linux系統中使用Vim文本編輯器編輯文本。 ...
  • 1.製作塊文件 3 個 2.創建迴環設備 關於迴環設備是什麼可以參考如下解釋: 迴環設備( 'loopback device')允許用戶以一個普通磁碟文件虛擬一個塊設備。設想一個磁碟設備,對它的所有讀寫操作都將被重定向到讀寫一個名為 disk-image 的普通文件而非操作實際磁碟或分區的軌道和扇區 ...
  • scp命令用於Linux 之間複製文件和目錄。如果想在windows 環境中使用需要安裝 linux 命令環境,比如 cmder scp是 secure copy的縮寫, scp是linux系統下基於ssh登陸進行安全的遠程文件拷貝命令。 語法: 簡易寫法 參數說明 如果遠程伺服器防火牆有為scp命 ...
  • 一、NVIDIA顯卡驅動 打開終端,輸入: 在新打開的文件夾中,進入以下路徑(不要用命令行): 這時會有幾個文件夾,對每個文件夾都進行以下操作(不要用命令行): 打開終端,輸入: 把下載的顯卡驅動改為簡單的名字,比如2080.run,放到home目錄下 按 Ctrl + Alt + F1,登錄 輸入 ...
  • Linux LVM邏輯捲配置過程詳解(創建、增加、減少、刪除、卸載) 許多Linux使用者安裝操作系統時都會遇到這樣的困境:如何精確評估和分配各個硬碟分區的容量,如果當初評估不准確,一旦系統分區不夠用時可能不得不備份、刪除相關數據,甚至被迫重新規劃分區並重裝操作系統,以滿足應用系統的需要。 LVM是 ...
  • yum( Yellow dog Updater, Modified)是一個在Fedora和RedHat以及SUSE中的Shell前端軟體包管理器。 基於RPM包管理,能夠從指定的伺服器自動下載RPM包並且安裝,可以自動處理依賴性關係,並且一次安裝所有依賴的軟體包,無須繁瑣地一次次下載、安裝。 yum ...
  • Linux目錄和文件——操作目錄和文件 摘要:本文主要學習了Linux系統中關於目錄和文件的操作。 cd命令 cd命令用來切換工作目錄,是Change Directory的縮寫。 基本語法 特殊符號 使用舉例 pwd命令 pwd命令的功能是顯示用戶當前所處的工作目錄,是Print Working D ...
  • CentOS7 SSH 密碼正確,但仍提示“Permission denied” ...
一周排行
    -Advertisement-
    Play Games
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...