一个实时操作系统,最重要的就是线程管理,rt-thread是一个硬实时系统,支持抢占式和时间片轮转的方式进行线程调度。
接下来简单描述下线程管理,并重点描述如何实现线程调度。
首先,一个程序是为了实现某种具体功能,而这个功能可能是由诸多小功能组成的,因此我们可以通过分别完成各个小功能以达到整个功能的实现。而线程,可以理解为就是为了实现具体小功能而生的,每个线程负责实现一个小功能,在rt-thread中线程也是最基本的调度单元。
各个线程之间通过一些手段实现同步和数据通信,就能实现原先的整体功能,这种机制特别适合经常需要修改功能的,因为各线程是相互独立的,子功能代码比较集中。另外使用线程还有个好处,那就是可以设定优先级,某些特别重要的子功能,可以赋予更高的线程优先级,确保其能够优先运行。
接下里讲讲如何使用线程吧
线程的操作函数主要有以下几个
- 线程的创建
可以使用 rt_thread_create() 创建一个动态线程,使用 rt_thread_init() 初始化一个静态线程,区别是:动态线程是系统自动从动态内存堆上分配栈空间与线程句柄,静态线程是由用户分配栈空间与线程句柄。
- 线程的删除
与创建对应的,动态创建的线程使用rt_thread_delete()来删除,静态创建的线程使用rt_thread_detach()删除。
- 线程的启动
都是使用的rt_thread_startup()来启动线程,该函数运行后线程就处于就绪态,线程可以被调度运行了。
- 线程挂起和恢复
比较常用的还有rt_thread_suspend()和rt_thread_resume(),挂起就是让线程退出就绪态,线程不会再被调度,恢复则是重新进入就绪态,可以被调度。
另外在创建线程时会指定一个入口函数thread_entry(),具体要实现的功能就在这个函数里实现。如果是需要一直执行的功能,那么线程必须设定为死循环的,可以使用while(1)等让线程永不退出,也不能有break或return等可能导致退出线程的操作。但是,如果一个线程死循环执行了,其它线程怎么办呢,是不是就无法运行了。事实上,其他线程并不是绝对不能运行的,我们知道rt-thread是基于抢占式的,高优先级线程是可以抢到执行权的,但是低优先级的线程确实就无法再运行了,所以我们的线程必须要有让出控制权的能力,一般我们在线程里使用rt_thread_delay()等让出控制权。
下面展示一个线程管理的示例
#include <rtthread.h>
#define THREAD_PRIORITY 25
#define THREAD_STACK_SIZE 512
#define THREAD_TIMESLICE 5
static rt_thread_t tid1 = RT_NULL;
/* 线程 1 的入口函数 */
static void thread1_entry(void *parameter)
{
rt_uint32_t count = 0;
while (1)
{
/* 线程 1 采用低优先级运行,一直打印计数值 */
rt_kprintf("thread1 count: %d\n", count ++);
rt_thread_mdelay(500);
}
}
ALIGN(RT_ALIGN_SIZE)
static char thread2_stack[1024];
static struct rt_thread thread2;
/* 线程 2 入口 */
static void thread2_entry(void *param)
{
rt_uint32_t count = 0;
/* 线程 2 拥有较高的优先级,以抢占线程 1 而获得执行 */
for (count = 0; count < 10 ; count++)
{
/* 线程 2 打印计数值 */
rt_kprintf("thread2 count: %d\n", count);
}
rt_kprintf("thread2 exit\n");
/* 线程 2 运行结束后也将自动被系统脱离 */
}
/* 线程示例 */
int thread_sample(void)
{
/* 创建线程 1,名称是 thread1,入口是 thread1_entry*/
tid1 = rt_thread_create("thread1",
thread1_entry, RT_NULL,
THREAD_STACK_SIZE,
THREAD_PRIORITY, THREAD_TIMESLICE);
/* 如果获得线程控制块,启动这个线程 */
if (tid1 != RT_NULL)
rt_thread_startup(tid1);
/* 初始化线程 2,名称是 thread2,入口是 thread2_entry */
rt_thread_init(&thread2,
"thread2",
thread2_entry,
RT_NULL,
&thread2_stack[0],
sizeof(thread2_stack),
THREAD_PRIORITY - 1, THREAD_TIMESLICE);
rt_thread_startup(&thread2);
return 0;
}
/* 导出到 msh 命令列表中 */
MSH_CMD_EXPORT(thread_sample, thread sample);
接下来讲重点,线程为什么可以被调度,以及如何实现的硬实时。
我们看到线程使用while(1)的方式执行功能,按理说while(1)死循环不是应该出不去的吗,怎么还能跳出去运行别的线程呢。仔细想想,是不是真的这样,你们的裸机前后台系统是什么样的?是不是main函数里也有个while(1),但是定时器或串口接收中断函数是不是照样可以运行,所以显而易见,线程调度用到了中断,只有这样才能打破while(1)的封锁。
那么用的是什么中断呢,直接给答案PendSV。它是一个可延迟执行的中断,所以它不用立即被执行,但它却可以跳出while去执行。而在实际中,PendSV的中断优先级很低,所以也不会打断外设的中断,特别适合RTOS中用作线程调度。下面是PendSV中断执行函数的内容,在这里完成了线程的切换。
PendSV_Handler PROC
EXPORT PendSV_Handler
; disable interrupt to protect context switch
MRS r2, PRIMASK
CPSID I
; get rt_thread_switch_interrupt_flag
LDR r0, =rt_thread_switch_interrupt_flag
LDR r1, [r0]
CBZ r1, pendsv_exit ; pendsv already handled
; clear rt_thread_switch_interrupt_flag to 0
MOV r1, #0x00
STR r1, [r0]
LDR r0, =rt_interrupt_from_thread
LDR r1, [r0]
CBZ r1, switch_to_thread ; skip register save at the first time
MRS r1, psp ; get from thread stack pointer
IF {FPU} != "SoftVFP"
TST lr, #0x10 ; if(!EXC_RETURN[4])
VSTMFDEQ r1!, {d8 - d15} ; push FPU register s16~s31
ENDIF
STMFD r1!, {r4 - r11} ; push r4 - r11 register
IF {FPU} != "SoftVFP"
MOV r4, #0x00 ; flag = 0
TST lr, #0x10 ; if(!EXC_RETURN[4])
MOVEQ r4, #0x01 ; flag = 1
STMFD r1!, {r4} ; push flag
ENDIF
LDR r0, [r0]
STR r1, [r0] ; update from thread stack pointer
switch_to_thread
LDR r1, =rt_interrupt_to_thread
LDR r1, [r1]
LDR r1, [r1] ; load thread stack pointer
IF {FPU} != "SoftVFP"
LDMFD r1!, {r3} ; pop flag
ENDIF
LDMFD r1!, {r4 - r11} ; pop r4 - r11 register
IF {FPU} != "SoftVFP"
CMP r3, #0 ; if(flag_r3 != 0)
VLDMFDNE r1!, {d8 - d15} ; pop FPU register s16~s31
ENDIF
MSR psp, r1 ; update stack pointer
IF {FPU} != "SoftVFP"
ORR lr, lr, #0x10 ; lr |= (1 << 4), clean FPCA.
CMP r3, #0 ; if(flag_r3 != 0)
BICNE lr, lr, #0x10 ; lr &= ~(1 << 4), set FPCA.
ENDIF
pendsv_exit
; restore interrupt
MSR PRIMASK, r2
ORR lr, lr, #0x04
BX lr
ENDP
我们知道线程切换是通过PendSV中断去实现的,我们又知道rt-thread是支持优先级的,那么线程切换时如何查找优先级最高的线程呢。
rt-thread支持256个优先级,但一般来说常用的,比如STM32就是默认的32个优先级。rt-thread提供了一个线程就绪优先级组,32位的整形,每个bit表示一个优先级,所以最多表示32个优先级,如果超过了32个则可以使用线程就绪优先级数组。但是优先级越多,对RAM需求越大,考虑大多单片机的内存资源,这里只描述32位优先级的情况。
rt_uint32_t rt_thread_ready_priority_group,每个bit一个优先级,当优先级为10的线程准备好了,就将rt_thread_ready_priority_group的bit10置1,表示线程已就绪,然后在线程优先级表10(rt_thread_priority_table[10])的位置插入线程。补充一下,rt_thread_priority_table是一个链表类型行的数组,rt_list_t rt_thread_priority_table[RT_THREAD_PRIORITY_MAX];这里面存储了线程控制块的地址,调度器最终就是根据这个地址去进行线程切换的。
好了,总结一下,rt_thread_ready_priority_group每个bit表示一个优先级,如果该bit为1则表示线程处于就绪态,同时对应的rt_thread_priority_table位置会插入对应线程控制块地址。按照优先级排序,低位的线程具有更高的优先级。比如下面这个,优先级最高的就是bit1对应的线程,假设现在调度器开始选取下一个要执行的线程,那么就是bit1对应的线程会被切换执行。等bit1线程执行后原bit1则会置0,下一次加入没有更高优先级线程插入,就会切换到bit2对应的线程。
所以 我们知道,只要从低到高遍历rt_thread_ready_priority_group里的每一个bit就知道哪个线程优先级更高了。但是如果粗暴的循环去判断的话,bit0执行1次判断,bit1执行2次判断,bit9则要执行10次判断了,这显然与rt-thread的硬实时特性不符。rt-thread提供了一种空间换时间的方法,将 8 位整形数的取值范围 0~255 作为数组__lowest_bit_bitmap[]的索引,
索引值第一个出现 1(从最低位开始)的位号作为该数组索引下的成员值,下面是rt-thread里获取当前就绪态最高优先级线程的源码,可以细品。
const rt_uint8_t __lowest_bit_bitmap[] =
{
/* 00 */ 0, 0, 1, 0, 2, 0, 1, 0, 3, 0, 1, 0, 2, 0, 1, 0,
/* 10 */ 4, 0, 1, 0, 2, 0, 1, 0, 3, 0, 1, 0, 2, 0, 1, 0,
/* 20 */ 5, 0, 1, 0, 2, 0, 1, 0, 3, 0, 1, 0, 2, 0, 1, 0,
/* 30 */ 4, 0, 1, 0, 2, 0, 1, 0, 3, 0, 1, 0, 2, 0, 1, 0,
/* 40 */ 6, 0, 1, 0, 2, 0, 1, 0, 3, 0, 1, 0, 2, 0, 1, 0,
/* 50 */ 4, 0, 1, 0, 2, 0, 1, 0, 3, 0, 1, 0, 2, 0, 1, 0,
/* 60 */ 5, 0, 1, 0, 2, 0, 1, 0, 3, 0, 1, 0, 2, 0, 1, 0,
/* 70 */ 4, 0, 1, 0, 2, 0, 1, 0, 3, 0, 1, 0, 2, 0, 1, 0,
/* 80 */ 7, 0, 1, 0, 2, 0, 1, 0, 3, 0, 1, 0, 2, 0, 1, 0,
/* 90 */ 4, 0, 1, 0, 2, 0, 1, 0, 3, 0, 1, 0, 2, 0, 1, 0,
/* A0 */ 5, 0, 1, 0, 2, 0, 1, 0, 3, 0, 1, 0, 2, 0, 1, 0,
/* B0 */ 4, 0, 1, 0, 2, 0, 1, 0, 3, 0, 1, 0, 2, 0, 1, 0,
/* C0 */ 6, 0, 1, 0, 2, 0, 1, 0, 3, 0, 1, 0, 2, 0, 1, 0,
/* D0 */ 4, 0, 1, 0, 2, 0, 1, 0, 3, 0, 1, 0, 2, 0, 1, 0,
/* E0 */ 5, 0, 1, 0, 2, 0, 1, 0, 3, 0, 1, 0, 2, 0, 1, 0,
/* F0 */ 4, 0, 1, 0, 2, 0, 1, 0, 3, 0, 1, 0, 2, 0, 1, 0
};
int __rt_ffs(int value)
{
if (value == 0) return 0;
if (value & 0xff)
return __lowest_bit_bitmap[value & 0xff] + 1;
if (value & 0xff00)
return __lowest_bit_bitmap[(value & 0xff00) >> 8] + 9;
if (value & 0xff0000)
return __lowest_bit_bitmap[(value & 0xff0000) >> 16] + 17;
return __lowest_bit_bitmap[(value & 0xff000000) >> 24] + 25;
}
static struct rt_thread* _get_highest_priority_thread(rt_ubase_t *highest_prio)
{
register struct rt_thread *highest_priority_thread;
register rt_ubase_t highest_ready_priority;
#if RT_THREAD_PRIORITY_MAX > 32
register rt_ubase_t number;
number = __rt_ffs(rt_thread_ready_priority_group) - 1;
highest_ready_priority = (number << 3) + __rt_ffs(rt_thread_ready_table[number]) - 1;
#else
highest_ready_priority = __rt_ffs(rt_thread_ready_priority_group) - 1;
#endif
/* get highest ready priority thread */
highest_priority_thread = rt_list_entry(rt_thread_priority_table[highest_ready_priority].next,
struct rt_thread,
tlist);
*highest_prio = highest_ready_priority;
return highest_priority_thread;
}
除了抢占式的优先级外,线程调度还支持时间片轮转的方式。首先这个时间片轮转只针对相同优先级的线程,根据设置的时间片分配时间,比如只有两线程,优先级都一样,其中线程1时间片10,线程2时间片20,那么整个CPU的2/3的时间都在运行线程2,差不多就这意思。
要实现这个功能,那就必须有个定时器去计时了,在STM32里,这件事交给了SysTick,在SysTick中断函数里有个rt_tick_increase()函数,首先获取当前线程控制块,然后递减当前线程时间片,当剩余时间片为0则重置时间片并让出处理器,在rt_thread_yield里进行下一轮的线程调度。
void rt_tick_increase(void)
{
struct rt_thread *thread;
/* increase the global tick */
#ifdef RT_USING_SMP
rt_cpu_self()->tick ++;
#else
++ rt_tick;
#endif
/* check time slice */
thread = rt_thread_self();
-- thread->remaining_tick;
if (thread->remaining_tick == 0)
{
/* change to initialized tick */
thread->remaining_tick = thread->init_tick;
/* yield */
rt_thread_yield();
}
/* check timer */
rt_timer_check();
}
void SysTick_Handler(void)
{
/* enter interrupt */
rt_interrupt_enter();
rt_tick_increase();
/* leave interrupt */
rt_interrupt_leave();
}
标签:rt,优先级,r1,thread,线程,thread2
From: https://blog.csdn.net/u011436603/article/details/136573292