RT-Thread 学习记录之线程
线程简介
线程可以理解为一个又一个的任务,在传统嵌入式裸机开发中,系统只能处理完一个任务再接着处理另一个任务,因此对时序和任务顺序的要求十分严格。但在 RT-Thread 中,我们可以创建一个又一个任务线程,通过调度算法在各个任务中快速切换,以达到实时操作的效果。
在 RT-Thread 中,线程是调度的最小单位,通过线程的设置可以看出任务的重要程度和运行环境。线程执行时的运行环境,也就是开辟的栈空间,通常称为上下文,具体来说就是各个变量和数据,比如该线程中的局部变量、寄存器状态等。
由此可大致分析出, RT-Thread 中正是采用定时器、中断和链表来进行线程间不停切换的,我们稍后做分析。
RT-Thread 中的线程有:初始状态、就绪状态、运行状态、挂起状态、关闭状态这五种状态,所谓的线程调度,就是通过调度器和 RT-Thread 提供的一些接口函数,使得线程在这五种状态中切换。
源码分析
静态线程初始化/脱离
线程控制块结构体 struct rt_thread ( 去掉多余定义后 ):
struct rt_thread
{
/* rt 对象 */
char name[RT_NAME_MAX]; /* 线程名称 */
rt_uint8_t type; /* 对象类型 */
rt_uint8_t flags; /* 标志位 */
rt_list_t list; /* 对象列表 */
rt_list_t tlist; /* 线程列表 */
/* 栈指针和入口函数 */
void *sp; /* 栈指针 */
void *entry; /* 入口函数指针 */
void *parameter; /* 参数 */
void *stack_addr; /* 栈地址指针 */
rt_uint32_t stack_size; /* 栈大小 */
/* 错误码 */
rt_err_t error; /* 错误码 */
rt_uint8_t stat; /* 线程状态 */
/* 优先级 */
rt_uint8_t current_priority; /* 当前优先级 */
rt_uint8_t init_priority; /* 初始优先级 */
rt_uint32_t number_mask;
rt_ubase_t init_tick; /* 线程初始化计数值 */
rt_ubase_t remaining_tick; /* 线程剩余计数值 */
struct rt_timer thread_timer; /* 内置线程定时器 */
void (*cleanup)(struct rt_thread *tid); /* 线程退出清除函数 */
rt_ubase_t user_data; /* 用户数据 */
};
线程的控制块结构中必要的属性如上,它是操作系统用于管理线程的一个数据结构,会存放线程的一些信息,例如优先级、线程名称、线程状态等,也包含线程与线程之间连接用的链表结构,线程等待事件集合等。
其中 init_priority 是线程创建时指定的线程优先级,在线程运行过程当中是不会被改变的(除非用户执行线程控制函数进行手动调整线程优先级)。cleanup 会在线程退出时,被空闲线程回调一次以执行用户设置的清理现场等工作。最后的一个成员 user_data 可由用户挂接一些数据信息到线程控制块中,以提供类似线程私有数据的实现。
接下来我们查看静态线程初始化函数 rt_thread_init :
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)
{
/* 线程断言检查 */
RT_ASSERT(thread != RT_NULL);
RT_ASSERT(stack_start != RT_NULL);
/* 初始化线程 */
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);
}
可分析出,在初始化静态线程时,会先调用 rt_object_init 函数将该线程对象初始化并加入系统,之后调用 _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)
{
/* 初始化线程链表 */
rt_list_init(&(thread->tlist));
/* 指定线程入口函数和参数 */
thread->entry = (void *)entry;
thread->parameter = parameter;
/* 栈空间初始化 */
thread->stack_addr = stack_start;
thread->stack_size = stack_size;
/* 初始化线程栈 */
rt_memset(thread->stack_addr, '#', thread->stack_size);
/* 栈的生长方式判断 ( 我们一般使用向下生长的方式,所以该宏不定义 ) */
#ifdef ARCH_CPU_STACK_GROWS_UPWARD
thread->sp = (void *)rt_hw_stack_init(thread->entry, thread->parameter,
(void *)((char *)thread->stack_addr),
(void *)_rt_thread_exit);
#else //调用 rt_hw_stack_init 初始化栈,栈顶的值为 【栈首地址 + 栈大小 - 4】
thread->sp = (void *)rt_hw_stack_init(thread->entry, thread->parameter,
(rt_uint8_t *)((char *)thread->stack_addr + thread->stack_size - sizeof(rt_ubase_t)),
(void *)_rt_thread_exit);
#endif /* ARCH_CPU_STACK_GROWS_UPWARD */
/* 优先级初始化 */
RT_ASSERT(priority < RT_THREAD_PRIORITY_MAX);
thread->init_priority = priority;
thread->current_priority = priority;
thread->number_mask = 0;
#if RT_THREAD_PRIORITY_MAX > 32
thread->number = 0;
thread->high_mask = 0;
#endif /* RT_THREAD_PRIORITY_MAX > 32 */
/* 时间片初始化 */
thread->init_tick = tick;
thread->remaining_tick = tick;
/* 错误码和标志位初始化 */
thread->error = RT_EOK;
thread->stat = RT_THREAD_INIT;
/* 初始化线程清除退出函数和用户数据 */
thread->cleanup = 0;
thread->user_data = 0;
/* 初始化线程内置定时器 */
rt_timer_init(&(thread->thread_timer),
thread->name,
rt_thread_timeout,
thread,
0,
RT_TIMER_FLAG_ONE_SHOT);
/* 绑定钩子函数 */
RT_OBJECT_HOOK_CALL(rt_thread_inited_hook, (thread));
return RT_EOK;
}
由此可得静态初始化线程流程:
接下来我们分析线程脱离函数。
对于用 rt_thread_init() 初始化的线程,使用 rt_thread_detach() 将使线程对象在线程队列和内核对象管理器中被脱离。
rt_thread_detach 函数如下:
rt_err_t rt_thread_detach(rt_thread_t thread)
{
rt_base_t lock;
/* 线程断言检查(判断是否为空、是否为线程对象、是否为系统对象) */
RT_ASSERT(thread != RT_NULL);//是否为空
RT_ASSERT(rt_object_get_type((rt_object_t)thread) == RT_Object_Class_Thread);
RT_ASSERT(rt_object_is_systemobject((rt_object_t)thread));
if ((thread->stat & RT_THREAD_STAT_MASK) == RT_THREAD_CLOSE)
return RT_EOK;
if ((thread->stat & RT_THREAD_STAT_MASK) != RT_THREAD_INIT)
{
/* 从就绪表中移除 */
rt_schedule_remove_thread(thread);
}
/* 线程清理 */
_rt_thread_cleanup_execute(thread);
/* 释放定时器 */
rt_timer_detach(&(thread->thread_timer));
/* 改变线程状态为关闭状态 */
thread->stat = RT_THREAD_CLOSE;
/* 栈中分配的线程直接从object管理表中移除 */
if (rt_object_is_systemobject((rt_object_t)thread) == RT_TRUE)
{
rt_object_detach((rt_object_t)thread);
}
else
{
/* 关闭中断 */
lock = rt_hw_interrupt_disable();
/* 动态分配的线程移除到僵尸列表中 */
rt_thread_defunct_enqueue(thread);
/* 开启中断 */
rt_hw_interrupt_enable(lock);
}
return RT_EOK;
}
对 rt_thread_detach 函数简单分析可知线程脱离的大概流程,在进行完断言判断后会再次对线程的状态进行判断,如果处于关闭状态,会直接返回 RT_EOK ,不是关闭或初始状态则会直接从就绪列表移除该线程。之后会进行线程清理,释放定时器,再将线程的状态切换为关闭状态,从 object 管理列表中移除该线程。最后顺利则返回 RT_EOK。
动态线程创建/删除
我们先来看看 rt_thread_create 函数:
rt_thread_t rt_thread_create(const char *name,
void (*entry)(void *parameter),
void *parameter,
rt_uint32_t stack_size,
rt_uint8_t priority,
rt_uint32_t tick)
{
struct rt_thread *thread;
void *stack_start;
/* 创建一个 RT_Object_Class_Thread 类型的 object 对象 */
thread = (struct rt_thread *)rt_object_allocate(RT_Object_Class_Thread,
name);
if (thread == RT_NULL)
return RT_NULL;
/* 申请分配线程栈空间 */
stack_start = (void *)RT_KERNEL_MALLOC(stack_size);
if (stack_start == RT_NULL)
{
/* 失败需要先删除该object并释放内存 */
rt_object_delete((rt_object_t)thread);
return RT_NULL;
}
/* 调用_rt_thread_init */
_rt_thread_init(thread,
name,
entry,
parameter,
stack_start,
stack_size,
priority,
tick);
return thread;
}
可见动态创建的线程,除了栈空间是动态申请外,与静态初始化线程基本一致。在此不做赘述。
再来看看 rt_thread_delete 函数:
rt_err_t rt_thread_delete(rt_thread_t thread)
{
rt_base_t lock;
/* 线程断言检查(判断是否为空、是否为线程对象、是否为系统对象 */
RT_ASSERT(thread != RT_NULL);
RT_ASSERT(rt_object_get_type((rt_object_t)thread) == RT_Object_Class_Thread);
RT_ASSERT(rt_object_is_systemobject((rt_object_t)thread) == RT_FALSE);
if ((thread->stat & RT_THREAD_STAT_MASK) == RT_THREAD_CLOSE)
return RT_EOK;
if ((thread->stat & RT_THREAD_STAT_MASK) != RT_THREAD_INIT)
{
/* 从就绪表中移除 */
rt_schedule_remove_thread(thread);
}
/* 线程清理 */
_rt_thread_cleanup_execute(thread);
/* 释放定时器 */
rt_timer_detach(&(thread->thread_timer));
/* 关闭中断 */
lock = rt_hw_interrupt_disable();
/* 改变线程状态为关闭状态 */
thread->stat = RT_THREAD_CLOSE;
/* 将线程移除到僵尸列表中 */
rt_thread_defunct_enqueue(thread);
/* 开启中断 */
rt_hw_interrupt_enable(lock);
return RT_EOK;
}
动态创建的线程需要使用 rt_thread_delete 函数进行删除,与静态线程脱离不同的地方在于,动态创建的线程不属于系统对象,不会加入 object 管理表,所以在进行删除操作时,不需要将其从 object 管理列表中删除,只需要将其插入到僵尸线程列表即可。
线程分离函数可以将静态线程从 object 中移除,线程删除函数则只能将动态分配的线程移除到僵尸线程表中。
启动线程
无论我们使用哪种方式 创建/初始化 线程,刚 创建/初始化 好的线程都处于初始状态,而要让线程跑起来,则需要将其切换成就绪状态,由此来让调度器对该线程进行调度。
rt_thread_startup 函数主要作用是将当前 创建/初始化 完成的线程从 RT_THREAD_INIT 状态切换到 RT_THREAD_READY 状态。
rt_thread_startup 函数:
rt_err_t rt_thread_startup(rt_thread_t thread)
{
/* 断言检查(是否为空、是否处于初始状态、是否为线程对象) */
RT_ASSERT(thread != RT_NULL);
RT_ASSERT((thread->stat & RT_THREAD_STAT_MASK) == RT_THREAD_INIT);
RT_ASSERT(rt_object_get_type((rt_object_t)thread) == RT_Object_Class_Thread);
/* 将当前优先级设置为初始化优先级 */
thread->current_priority = thread->init_priority;
/* 计算优先级 */
#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;//计算当前number_mask
#endif /* RT_THREAD_PRIORITY_MAX > 32 */
/* 输出日志信息 */
RT_DEBUG_LOG(RT_DEBUG_THREAD, ("startup a thread:%s with priority:%d\n",
thread->name, thread->init_priority));
/* 改变线程状态为挂起状态 */
thread->stat = RT_THREAD_SUSPEND;
/* 将线程切换到就绪状态 */
rt_thread_resume(thread);
/* 获得当前执行的线程句柄 */
if (rt_thread_self() != RT_NULL)
{
/* 做一次调度 */
rt_schedule();
}
return RT_EOK;
}
由上述代码可分析出, RT-Thread 在将线程由初始状态切换为就绪状态时,并没有直接将 stat 属性更改为 RT_THREAD_READY ,而是先将其更改为 RT_THREAD_SUSPEND 挂起状态,再由 rt_thread_resume 函数将其切换为就绪状态。此函数将恢复线程并将其放入系统就绪队列。
我们来看看 rt_thread_resume 函数:
rt_err_t rt_thread_resume(rt_thread_t thread)
{
register rt_base_t temp;
/* 断言检查(是否为空、是否为线程对象) */
RT_ASSERT(thread != RT_NULL);
RT_ASSERT(rt_object_get_type((rt_object_t)thread) == RT_Object_Class_Thread);
/* 输出日志信息 */
RT_DEBUG_LOG(RT_DEBUG_THREAD, ("thread resume: %s\n", thread->name));
/* 判断线程状态是否为挂起状态,不是则返回 -RT_ERROR */
if ((thread->stat & RT_THREAD_STAT_MASK) != RT_THREAD_SUSPEND)
{
/* 输出日志信息 */
RT_DEBUG_LOG(RT_DEBUG_THREAD, ("thread resume: thread disorder, %d\n",
thread->stat));
return -RT_ERROR;
}
/* 关闭中断 */
temp = rt_hw_interrupt_disable();
/* 将该线程从链表移除 */
rt_list_remove(&(thread->tlist));
/* 停止计时 */
rt_timer_stop(&thread->thread_timer);
/* 将该线程插入就绪链表,线程状态将被设置为就绪并从挂起链表中删除。 */
rt_schedule_insert_thread(thread);
/* 使能中断 */
rt_hw_interrupt_enable(temp);
/* 绑定钩子函数 */
RT_OBJECT_HOOK_CALL(rt_thread_resume_hook, (thread));
return RT_EOK;
}
从以上源码分析可知,在进行初始状态切换就绪状态时,需要先将其转换为挂起状态进行过渡,之后进入 rt_schedule_insert_thread 函数后将该线程插入就绪链表,线程状态被设置为就绪 。
接下来我们看看 rt_schedule_insert_thread 函数的实现:
void rt_schedule_insert_thread(struct rt_thread *thread)
{
register rt_base_t temp;
/* 断言检查(是否为空) */
RT_ASSERT(thread != RT_NULL);
/* 关闭中断 */
temp = rt_hw_interrupt_disable();
/* 判断线程状态 */
if (thread == rt_current_thread)
{
/* 将线程状态切换为运行状态 */
thread->stat = RT_THREAD_RUNNING | (thread->stat & ~RT_THREAD_STAT_MASK);
goto __exit;
}
/* 将线程状态切换为就绪状态并插入就绪链表 */
thread->stat = RT_THREAD_READY | (thread->stat & ~RT_THREAD_STAT_MASK);
/* 将线程插入就绪链表 */
rt_list_insert_before(&(rt_thread_priority_table[thread->current_priority]),
&(thread->tlist));
/* 输出日志信息 */
RT_DEBUG_LOG(RT_DEBUG_SCHEDULER, ("insert thread[%.*s], the priority: %d\n",
RT_NAME_MAX, thread->name, thread->current_priority));
/* 设置优先级 */
#if RT_THREAD_PRIORITY_MAX > 32
rt_thread_ready_table[thread->number] |= thread->high_mask;
#endif /* RT_THREAD_PRIORITY_MAX > 32 */
rt_thread_ready_priority_group |= thread->number_mask;
__exit:
/* 使能中断 */
rt_hw_interrupt_enable(temp);
}
在该函数中做了一次判断,并将线程状态切换为了就绪态且加入就绪链表中。
通过上述分析可大概总结出线程启动流程:
补充
栈的生长方式
在 RT-Thread 中,栈的生长方式有两种:
- 向下生长:递减方式,即栈顶是栈的最后一个地址,数组的最后一个元素。
- 向上生长:递增方式,即栈顶是栈的首个元素的地址,数组首个元素。
而线程栈就是向下生长的,栈顶的值为 【栈首地址 + 栈大小 - 4】。
动态和静态线程的对比
- 动态线程不需要输入栈的起始地址,不需要定义线程的控制块,只要指出线程栈的大小。
- 静态线程的线程控制块和线程栈都需要静态地定义出来,而动态线程则不需要提前定义出来,是运行的时候自动分配的。
- 运行效率上,静态线程的线程控制块和线程栈都在芯片的RAM中,速度没有区别。
- 静态定义的空间在编译时就已经确定,它不是可以动态分配的,所以不能被释放。只能脱离线程控制器。
总结
线程是 RT-Thread 内核中极其重要的部分,这篇文章仅对线程的创建、脱离、启动等做了简介,之后会对线程的调度、状态切换等进行更详细的说明分析。