首页 > 其他分享 >rt-thread 线程管理

rt-thread 线程管理

时间:2024-03-16 14:59:14浏览次数:26  
标签:rt 优先级 r1 thread 线程 thread2

        一个实时操作系统,最重要的就是线程管理,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

相关文章

  • 一个现成的用python写的项目, 有GUI,https://github.com/mustafamerttunali/deep-learni
    安装该项目ENV:Win11Anaconda 1.安装Python3.7, 在Anaconda新建一个python3.7环境2.安装VC++buildtool14.0 以上版本,我从下面这个link下载的最新版是17.6.4https://visualstudio.microsoft.com/visual-cpp-build-tools/否则会遇到 3.修改一下requir......
  • RX 6750 GRE 10GB和rtx 4080对比 评测
    RX6750GRE10GB基于RDNA2架构Navi22核心,晶体管数为172亿,拥有36组计算单元共计2304个流处理器,36个光追单元;加速频率2450MHz,TGP功耗158W,TBP功耗为170W;无限缓存80MB,采用GDDR6显存,显存位宽160-bit;主要用于1080p及1440p级别游戏市场。选RX6750GRE10GB还是rtx4080这些点很......
  • 快速创建一个spring-boot-starter
    可以使用springspi和import两种方法1.导入starter依赖1.1.maven管理工具<dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter</artifactId></dependency>1.2.gradle管理工......
  • Codeforces 1948E Clique Partition
    考虑到有\(|i-j|\),所以肯定是让相邻的一部分在一个团里。考虑构造出一个最长长度的,后面类似复制几遍即可。考虑到\(|k-1|=k-1\),且因为\(a_i\)为一个排列,差的绝对值至少为\(1\),所以只考虑两端最长长度也只可能为\(k\)。不妨先假设最长长度为\(k\)来构造。那么......
  • 七个项目掌握freertos
    1、闪烁LED:最基本的示例项目,涉及到创建一个简单的任务,用于控制LED的闪烁。这个项目会教你如何初始化FreeRTOS并创建任务。#include"FreeRTOS.h"#include"task.h"#defineLED_PIN(某个GPIO引脚)voidvBlinkTask(void*pvParameters){while(1){//Tog......
  • FreeRTOS入门基础
    RTOS是为了更好地在嵌入式系统上实现多任务处理和时间敏感任务而设计的系统。它能确保任务在指定或预期的时间内得到处理。FreeRTOS是一款免费开源的RTOS,它广泛用于需要小型、预测性强、灵活系统的嵌入式设备。创建第一个任务任务函数:任务是通过函数来定义的。函数通常看起......
  • Delphi提高开发效率之GExperts专家的使用说明
    GExperts是一组通过扩展集成开发环境(IDE)来提高Delphi和C++Builer程序员工作效率的工具。是一款开源的IDE扩展专家,由于去外网下载GExperts非常的麻烦,这里直接提供了Delphi7和Delphi11.1下的GExperts安装包,并连带最新源码一起打包,方便大家使用学习。下面直接看他具有哪些功能,留下实......
  • Qt 线程池 QThreadPool
    一.Qt线程池QThreadPool介绍Qt线程池是一种管理多个线程的并发编程模型,通过使用线程池可以提高性能、控制并发度、提供任务队列和简化线程管理。在Qt中,线程池的使用主要涉及以下几个步骤:创建任务类:需要定义一个任务类,该类继承自QRunnable和QObject,以便于能够在线程中运行......
  • Pycharm 中 virtualenv、pipenv、conda 虚拟环境的用法
    文章目录前言虚拟环境的通俗介绍虚拟环境和非虚拟环境该怎么选?通过Virtualenv方式创建虚拟环境通过Pipenv方式创建虚拟环境通过Conda方式创建虚拟环境 前言在网上找了好一些资料,发现介绍Pycharm虚拟环境的不多,查了一些资料,并做个总结。本文主要是介绍Pyc......
  • P2824 [HEOI2016/TJOI2016] 排序 与 ABC297_g Range Sort Query 题解
    洛谷题目链接:排序abc题目链接:Atcoder或者洛谷两道差不多的题拿出来说说。本题有双\(\log\)做法,也有单\(\log\)做法,都讲讲。双\(\log\)做法对于第一个题而言,询问最终\(pos\)位置上的数为多少,那么这个问题是否具有单调性?这个是很有意思的点,我们考虑只关注某个数\(x\)......