參考資料:RTT官網文檔
關鍵字:分析RT-Thread源碼、stm32、RTOS、線程管理器。
RT-Thread的線程簡介
線程(thread)是系統能夠進行調度的最小單位,在linux中也是這樣定義的,但是和我們RTOS中的thread更像是linux中的進程,是系統進行資源分配的基本單位,但同時也是調度的基本單位。在其他RTOS上有的將其命名爲任務task。我們現在RTOS中任務的“並行”運行是系統在不斷的切換任務呈現出來的,所以我們的線程棧頂需要維護保存上下文的切換前狀態。在調度和對象容器中,皆需要一個維護一個鏈表,也要記錄線程的狀態及優先級等等。
我們先來看看RTT的線程有哪些狀態。
RT-Thread 中線程的五種狀態:初始狀態、掛起狀態、就緒狀態、運行狀態、關閉狀態。
RT-Thread 提供一系列的操作系統調用接口,使得線程的狀態在這五個狀態之間來回切換。
幾種狀態間的轉換關係如下圖所示:
掛起的線程只能通過就緒態進入運行狀態,不能直接到運行狀態。
雖然定義了運行狀態,但實際上這個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);
}