概述
文章目录
- RT-Thread的线程简介
- 源码分析
- 初始化线程
- 线程脱离
- 启动线程
- 挂起线程
- 线程睡眠
- 线程让出
- 测试
参考资料: 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, %dn",
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, %dn",
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 : %ldrn", i++);
thread_yield();
}
}
void thread_2_entry(void *param)
{
uint32_t i = 0;
while (1) {
printf("world. i : %ldrn", 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);
}
最后
以上就是迷人金鱼为你收集整理的使用STM32编写一个简单的RTOS:3.线程管理的全部内容,希望文章能够帮你解决使用STM32编写一个简单的RTOS:3.线程管理所遇到的程序开发问题。
如果觉得靠谱客网站的内容还不错,欢迎将靠谱客网站推荐给程序员好友。
发表评论 取消回复