使用STM32編寫一個簡單的RTOS:3.線程管理


參考資料:RTT官網文檔
關鍵字:分析RT-Thread源碼、stm32、RTOS、線程管理器。

RT-Thread的線程簡介

線程(thread)是系統能夠進行調度的最小單位,在linux中也是這樣定義的,但是和我們RTOS中的thread更像是linux中的進程,是系統進行資源分配的基本單位,但同時也是調度的基本單位。在其他RTOS上有的將其命名爲任務task。我們現在RTOS中任務的“並行”運行是系統在不斷的切換任務呈現出來的,所以我們的線程棧頂需要維護保存上下文的切換前狀態。在調度和對象容器中,皆需要一個維護一個鏈表,也要記錄線程的狀態及優先級等等。

我們先來看看RTT的線程有哪些狀態。
RT-Thread 中線程的五種狀態:初始狀態、掛起狀態、就緒狀態、運行狀態、關閉狀態。
Alt
RT-Thread 提供一系列的操作系統調用接口,使得線程的狀態在這五個狀態之間來回切換。
幾種狀態間的轉換關係如下圖所示:
Alt
掛起的線程只能通過就緒態進入運行狀態,不能直接到運行狀態。
雖然定義了運行狀態,但實際上這個RT_THREAD_RUNNING沒引用過

RTT中有一個特殊的線程,idle空閒線程,即空閒時就會運行的線程,當就緒表中沒有其他線程時,這個線程就會得到運行,而且idle線程一直是就緒狀態。

另外,空閒線程在 RT-Thread 也有着它的特殊用途:
若某線程運行完畢,系統將自動刪除線程:自動執行 rt_thread_exit() 函數,先將該線程從系統就緒隊列中刪除,再將該線程的狀態更改爲關閉狀態,不再參與系統調度,然後掛入 rt_thread_defunct 殭屍隊列(資源未回收、處於關閉狀態的線程隊列)中,最後空閒線程會回收被刪除線程的資源。

空閒線程也提供了接口來運行用戶設置的鉤子函數,在空閒線程運行時會調用該鉤子函數,適合鉤入功耗管理、看門狗餵狗等工作。

線程的管理方式:

下圖描述了線程的相關操作,包含:創建 / 初始化線程、啓動線程、運行線程、刪除 / 脫離線程。可以使用 rt_thread_create() 創建一個動態線程,使用 rt_thread_init() 初始化一個靜態線程,動態線程與靜態線程的區別是:動態線程是系統自動從動態內存堆上分配棧空間與線程句柄(初始化 heap 之後才能使用 create 創建動態線程),靜態線程是由用戶分配棧空間與線程句柄。

源碼分析

初始化線程

這裏只看一下靜態初始化。

rt_err_t rt_thread_init(struct rt_thread *thread,
                        const char       *name,
                        void (*entry)(void *parameter),
                        void             *parameter,
                        void             *stack_start,
                        rt_uint32_t       stack_size,
                        rt_uint8_t        priority,
                        rt_uint32_t       tick)
{
    /* thread check */
    RT_ASSERT(thread != RT_NULL);
    RT_ASSERT(stack_start != RT_NULL);

    /* init thread object */
    rt_object_init((rt_object_t)thread, RT_Object_Class_Thread, name);

    return _rt_thread_init(thread,
                           name,
                           entry,
                           parameter,
                           stack_start,
                           stack_size,
                           priority,
                           tick);
}

參數tick是線程能持有的cpu時間,當時間走完時,線程並不會掛起,只是調用yield讓出當前cpu權限,這個只對同優先級線程有效,低優先級仍然得不到cpu資源。
這裏先是通過 rt_object_init將thread設置爲了線程對象類。 rt_object_init在對象管理中也有分析過了,就是將thread插入對象容器相應的鏈表中。之後是_rt_thread_init,跟進去。

static rt_err_t _rt_thread_init(struct rt_thread *thread,
                                const char       *name,
                                void (*entry)(void *parameter),
                                void             *parameter,
                                void             *stack_start,
                                rt_uint32_t       stack_size,
                                rt_uint8_t        priority,
                                rt_uint32_t       tick)
{
    /* init thread list */
    rt_list_init(&(thread->tlist));	//調度就緒表時用到

    thread->entry = (void *)entry;
    thread->parameter = parameter;

    /* stack init */
    thread->stack_addr = stack_start;
    thread->stack_size = (rt_uint16_t)stack_size;

    /* init thread stack */
    rt_memset(thread->stack_addr, '#', thread->stack_size);
    thread->sp = (void *)rt_hw_stack_init(thread->entry, thread->parameter,
        (void *)((char *)thread->stack_addr + thread->stack_size - 4),
        (void *)rt_thread_exit);

    /* priority init */
    RT_ASSERT(priority < RT_THREAD_PRIORITY_MAX);
    thread->init_priority    = priority;
    thread->current_priority = priority;

    /* tick init */
    thread->init_tick      = tick;
    thread->remaining_tick = tick;

    /* error and flags */
    thread->error = RT_EOK;
    thread->stat  = RT_THREAD_INIT;

    /* initialize cleanup function and user data */
    thread->cleanup   = 0;
    thread->user_data = 0;

    /* init thread timer */
    rt_timer_init(&(thread->thread_timer),
                  thread->name,
                  rt_thread_timeout,
                  thread,
                  0,
                  RT_TIMER_FLAG_ONE_SHOT);

    return RT_EOK;
}

首先初始化了tlist,這個在調度的時候需要用到(插入就緒表),接着是一些入口函數的賦值,應該不難理解。

接下來就是重點了,首先將棧全部設置爲字符‘#’,這個在算線程棧的最大使用率上需要用到,就是通過這個‘#’來計算的。接着,棧的初始化,這個函數我們在上下文切換時已經接觸過了。現在我來看一下這個函數的具體實現。

rt_uint8_t *rt_hw_stack_init(void       *tentry,
                             void       *parameter,
                             rt_uint8_t *stack_addr,
                             void       *texit)
{
    struct stack_frame *stack_frame;
    rt_uint8_t         *stk;
    unsigned long       i;

    /* 對傳入的棧指針做對齊處理 */
    stk  = stack_addr + sizeof(rt_uint32_t);
    stk  = (rt_uint8_t *)RT_ALIGN_DOWN((rt_uint32_t)stk, 8);
    stk -= sizeof(struct stack_frame);

    /* 得到上下文的棧幀的指針 */
    stack_frame = (struct stack_frame *)stk;

    /* 把所有寄存器的默認值設置爲 0xdeadbeef */
    for (i = 0; i < sizeof(struct stack_frame) / sizeof(rt_uint32_t); i ++)
    {
        ((rt_uint32_t *)stack_frame)[i] = 0xdeadbeef;
    }

    /* 根據 ARM  APCS 調用標準,將第一個參數保存在 r0 寄存器 */
    stack_frame->exception_stack_frame.r0  = (unsigned long)parameter;
    /* 將剩下的參數寄存器都設置爲 0 */
    stack_frame->exception_stack_frame.r1  = 0;                 /* r1 寄存器 */
    stack_frame->exception_stack_frame.r2  = 0;                 /* r2 寄存器 */
    stack_frame->exception_stack_frame.r3  = 0;                 /* r3 寄存器 */
    /* 將 IP(Intra-Procedure-call scratch register.) 設置爲 0 */
    stack_frame->exception_stack_frame.r12 = 0;                 /* r12 寄存器 */
    /* 將線程退出函數的地址保存在 lr 寄存器 */
    stack_frame->exception_stack_frame.lr  = (unsigned long)texit;
    /* 將線程入口函數的地址保存在 pc 寄存器 */
    stack_frame->exception_stack_frame.pc  = (unsigned long)tentry;
    /* 設置 psr 的值爲 0x01000000L,表示默認切換過去是 Thumb 模式 */
    stack_frame->exception_stack_frame.psr = 0x01000000L;

    /* 返回當前線程的棧地址       */
    return stk;
}

首先stk = stack_addr + sizeof(rt_uint32_t),獲取棧頂指針,因爲我們傳進來的時候就-4了。接着是向下對齊,cortex-M3要求4個字節對齊,這裏是8個字節對齊,例如15的話得到的地址就是8,。然後又減去sizeof(struct stack_frame),因爲cortex-m3是向下生長的棧,減去的這部分是用來保存上下文環境的。
接下來是寄存器的初始化,需要注意的是LR = exit函數,PC = entry函數,因爲這裏的這裏的退出函數exit賦值給了LR,在entry函數跑完的時候就會返回LR,而LR我們指向了exit退出函數,這就是爲什麼系統知道我們的線程是否退出了的原因。我們看一下exit裏面做了什麼操作。

static void rt_thread_exit(void)
{
    struct rt_thread *thread;
    register rt_base_t level;

    /* get current thread */
    thread = rt_current_thread;

    /* disable interrupt */
    level = rt_hw_interrupt_disable();

    /* remove from schedule */
    rt_schedule_remove_thread(thread);
    /* change stat */
    thread->stat = RT_THREAD_CLOSE;

    /* remove it from timer list */
    rt_timer_detach(&thread->thread_timer);

    if ((rt_object_is_systemobject((rt_object_t)thread) == RT_TRUE) &&
        thread->cleanup == RT_NULL)
    {
        rt_object_detach((rt_object_t)thread);
    }
    else
    {
        /* insert to defunct thread list */
        rt_list_insert_after(&rt_thread_defunct, &(thread->tlist));
    }

    /* enable interrupt */
    rt_hw_interrupt_enable(level);

    /* switch to next task */
    rt_schedule();
}

主要做了這些事情:

1,將線程從調度就緒表中移除。rt_schedule_remove_thread(thread);
2,修改線程狀態爲RT_THREAD_CLOSE
3,移除線程的定時器
4,從對象容器中移除,如果是動態的對象,則插入rt_thread_defunct中,將在idle中回收。

回到 _rt_thread_init中,接着是一些優先級等的賦值,之後初始化了一個只觸發一次的定時器。
定時器的超時處理函數則是在超時的時候,將線程插入調度就緒表中並調度。

void rt_thread_timeout(void *parameter)
{
    struct rt_thread *thread;

    thread = (struct rt_thread *)parameter;

    /* thread check */
    RT_ASSERT(thread != RT_NULL);
    RT_ASSERT(thread->stat == RT_THREAD_SUSPEND);

    /* set error number */
    thread->error = -RT_ETIMEOUT;

    /* remove from suspend list */
    rt_list_remove(&(thread->tlist));

    /* insert to schedule ready list */
    rt_schedule_insert_thread(thread);

    /* do schedule */
    rt_schedule();
}

這個定時器在sleep,delay和ipc的時候將會用到。
rt_thread_init就介紹到這,rt_thread_create跟這個差不多,這裏也不贅述了。

線程脫離

對於用 rt_thread_init() 初始化的線程,使用 rt_thread_detach() 將使線程對象在線程隊列和內核對象管理器中被脫離。線程脫離函數如下:

rt_err_t rt_thread_detach(rt_thread_t thread)
{
    rt_base_t lock;

    /* thread check */
    RT_ASSERT(thread != RT_NULL);

    if ((thread->stat & RT_THREAD_STAT_MASK) != RT_THREAD_INIT)
    {
        /* remove from schedule */
        rt_schedule_remove_thread(thread);
    }

    /* release thread timer */
    rt_timer_detach(&(thread->thread_timer));

    /* change stat */
    thread->stat = RT_THREAD_CLOSE;

    /* detach object */
    rt_object_detach((rt_object_t)thread);

    if (thread->cleanup != RT_NULL)
    {
        /* disable interrupt */
        lock = rt_hw_interrupt_disable();

        /* insert to defunct thread list */
        rt_list_insert_after(&rt_thread_defunct, &(thread->tlist));

        /* enable interrupt */
        rt_hw_interrupt_enable(lock);
    }

    return RT_EOK;
}

如果線程的狀態不是初始化狀態,就在就緒表中移除,並清除相關標識位。接着將線程狀態設置爲了關閉狀態,從對象容器中移除,判斷cleanup,cleanup我們在init的時候賦值爲0了,所以這裏條件不成立。

啓動線程

初始化完線程後就可以啓動線程啦,下面我們看一下rt_thread_startup具體做了哪些事情。

rt_err_t rt_thread_startup(rt_thread_t thread)
{
	......
    /* set current priority to init priority */
    thread->current_priority = thread->init_priority;

    /* calculate priority attribute */
#if RT_THREAD_PRIORITY_MAX > 32
    thread->number      = thread->current_priority >> 3;            /* 5bit */
    thread->number_mask = 1L << thread->number;
    thread->high_mask   = 1L << (thread->current_priority & 0x07);  /* 3bit */
#else
    thread->number_mask = 1L << thread->current_priority;
#endif
	......
    /* change thread stat */
    thread->stat = RT_THREAD_SUSPEND;
    /* then resume it */
    rt_thread_resume(thread);
    if (rt_thread_self() != RT_NULL)
    {
        /* do a scheduling */
        rt_schedule();
    }

    return RT_EOK;
}

這個函數也比較簡短,通過優先級計算出屬性,用來定位在調度就緒表中的位置,在上下文一篇中我們已經瞭解過了,接着恢復線程到就緒狀態,rt_thread_resume,然後通過rt_thread_self判斷調度器是否已經啓用。最後 rt_schedule調度,切換上下文。

rt_err_t rt_thread_resume(rt_thread_t thread)
{
	......
	 if (thread->stat != RT_THREAD_SUSPEND)
    {
        RT_DEBUG_LOG(RT_DEBUG_THREAD, ("thread resume: thread disorder, %d\n",
                                       thread->stat));
        return -RT_ERROR;
    }
    /* remove from suspend list */
    rt_list_remove(&(thread->tlist));

    rt_timer_stop(&thread->thread_timer);

    /* enable interrupt */
    rt_hw_interrupt_enable(temp);

    /* insert to schedule ready list */
    rt_schedule_insert_thread(thread);

    return RT_EOK;
}

resume函數裏做的事情也比較少,如果不是suspend狀態就不能切換到resume。接着從suspend鏈表中移除,實際上我們剛初始化的時候,tlist是指向自己的,而在 rt_schedule_insert_thread時纔會插入就緒表,在detach的時候插入rt_thread_defunct中。接着就是將定時器停止,然後rt_schedule_insert_thread插入就緒表,並修改線程狀態爲RT_THREAD_READY。之後只要調用rt_schedule就可以啓動該線程了。

掛起線程

掛起線程的作用是將就緒狀態的線程切換爲掛起狀態,也就是將線程從就緒表中移除。

rt_err_t rt_thread_suspend(rt_thread_t thread)
{
    ......
    if (thread->stat != RT_THREAD_READY)
    {
        RT_DEBUG_LOG(RT_DEBUG_THREAD, ("thread suspend: thread disorder, %d\n",
                                       thread->stat));

        return -RT_ERROR;
    }

    /* disable interrupt */
    temp = rt_hw_interrupt_disable();

    /* change thread stat */
    thread->stat = RT_THREAD_SUSPEND;
    rt_schedule_remove_thread(thread);

    /* stop thread timer anyway */
    rt_timer_stop(&(thread->thread_timer));
	......
}

如果不是在就緒狀態,則直接返回,不然就將線程狀態改爲SUSPEND掛起狀態,從調度就緒表中移除,接着停止線程的定時器。

線程睡眠

rt_thread_delay/rt_thread_sleep的作用是讓線程退出調度就緒表中,在tick時間後重新插入調度就緒表中等待調度。這個時候就需要用到在線程初始化時初始化的那個定時器了。

rt_err_t rt_thread_delay(rt_tick_t tick)
{
    return rt_thread_sleep(tick);
}

rt_err_t rt_thread_sleep(rt_tick_t tick)
{
    register rt_base_t temp;
    struct rt_thread *thread;

    /* disable interrupt */
    temp = rt_hw_interrupt_disable();
    /* set to current thread */
    thread = rt_current_thread;
    RT_ASSERT(thread != RT_NULL);

    /* suspend thread */
    rt_thread_suspend(thread);

    /* reset the timeout of thread timer and start it */
    rt_timer_control(&(thread->thread_timer), RT_TIMER_CTRL_SET_TIME, &tick);
    rt_timer_start(&(thread->thread_timer));

    /* enable interrupt */
    rt_hw_interrupt_enable(temp);

    rt_schedule();

    /* clear error number of this thread to RT_EOK */
    if (thread->error == -RT_ETIMEOUT)
        thread->error = RT_EOK;

    return RT_EOK;
}

參數tick就是我們要sleep的時間,具體時間跟RT_TICK_PER_SECOND這個有關係,默認是100,即1tick爲10ms。也是我們sleep的最小單位。
首先將線程給切換到了掛起狀態,接着設置定時器的時間,然後啓動定時器,這是一個只觸發一次的定時器。接着進行調度,之後就進入等待定時器timeout了,timeout前面也已經分析了,將線程重新插入到調度就緒表中,然後調度。

線程讓出

該線程主動要求讓出處理器資源時,它將不再佔有處理器,調度器會選擇相同優先級的下一個線程執行。線程調用這個接口後,這個線程仍然在就緒隊列中。

rt_err_t rt_thread_yield(void)
{
	......
    /* set to current thread */
    thread = rt_current_thread;

    /* if the thread stat is READY and on ready queue list */
    if ((thread->stat & RT_THREAD_STAT_MASK) == RT_THREAD_READY &&
        thread->tlist.next != thread->tlist.prev)
    {
        /* remove thread from thread list */
        rt_list_remove(&(thread->tlist));

        /* put thread to end of ready queue */
        rt_list_insert_before(&(rt_thread_priority_table[thread->current_priority]),
                              &(thread->tlist));

        /* enable interrupt */
        rt_hw_interrupt_enable(level);

        rt_schedule();

        return RT_EOK;
    }
	......
}

首先得到了當前線程的句柄。判斷當前的狀態是不是就緒狀態和是不是(該優先級的)就緒表中只有一個線程。如果均滿足條件,則將線程移除,插到鏈表(同優先級)最後面,在進行調度。
可以看到,rt_thread_yield之後如果沒有更高級的線程進入就緒狀態,則會調度同一優先級的其他線程,如果該優先級下只有自己,那麼將不做調度,也就是rt_thread_yield這個並不能讓低優先級的線程有機會獲得cpu的控制權,這個接口主要針對的還是同等優先級線程纔有作用。

測試

關於線程管理就介紹到這裏。現在我們可以接着繼續完成我們的RTT-Mini了。我們只完成了上下文切換,接着要完善調度器的功能,以及引入對象容器和線程管理。篇幅已經有點長了,這裏就不貼詳細代碼了。

app.c

#define THREAD_STACK_SIZE       256   
                             
unsigned char thread_1_stack[THREAD_STACK_SIZE];
struct thread thread1;
                 
unsigned char thread_2_stack[THREAD_STACK_SIZE];
struct thread thread2;

                             
void thread_1_entry(void *param)
{
    uint32_t i = 0;
    
    while (1) {
        printf("hello. i : %ld\r\n", i++);
        thread_yield();
    }
}


void thread_2_entry(void *param)
{
    uint32_t i = 0;
    
    while (1) {
        printf("world. i : %ld\r\n", i++);
        thread_yield();
    }
}

void app_init(void)
{
    err_t err;
    
    err = thread_init(&thread1, "th1", thread_1_entry, NULL, thread_1_stack, THREAD_STACK_SIZE, 20, 10);
    if (err == E_SUCCESS)
        thread_startup(&thread1);
    
    err = thread_init(&thread2, "th2", thread_2_entry, NULL, thread_2_stack, THREAD_STACK_SIZE, 20, 10);
    if (err == E_SUCCESS)
        thread_startup(&thread2);
}

在這裏插入圖片描述

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章