RTThread 线程管理

线程是实现任务的载体,它是 RT-Thread 中最基本的调度单位,它描述了一个任务执行的运行环境,也描述了这个任务所处的优先等级,重要的任务可设置相对较高的优先级,非重要的任务可以设置较低的优先级,不同的任务还可以设置相同的优先级,轮流运行。

线程管理的功能特点

RT-Thread 线程管理的主要功能是对线程进行管理和调度,系统中总共存在两类线程,分别是系统线程和用户线程

系统线程是由 RT-Thread 内核创建的线程 ,用户线程是由应用程序创建的线程 ,这两类线程都会从内核对象容器中分配线程对象,当线程被删除时,也会被从对象容器中删除,如下图所示,每个线程都有重要的属性,如线程控制块、线程栈、入口函数等。

RTthread调度器是抢占式的,主要工作就是从就绪线程列表中查找最高优先级线程,保证最高优先级线程能够被运行。当一个运行着的线程使一个比他优先级高的线程满足运行条件,当前线程的cpu使用权就被剥夺了或者说让出了。

线程的工作机制

线程控制块

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_uint32_t user_data;                      /* 用户数据 */
};

其中 init_priority 是线程创建时指定的线程优先级,在线程运行过程当中是不会被改变的(除非用户执行线程控制函数进行手动调整线程优先级)。cleanup 会在线程退出时,被空闲线程回调一次以执行用户设置的清理现场等工作。最后的一个成员 user_data 可由用户挂接一些数据信息到线程控制块中,以提供一种类似线程私有数据的实现方式。

线程重要属性

线程栈

RT-Thread 线程具有独立的栈,当进行线程切换时,会将当前线程的上下文存在栈中,当线程要恢复运行时,再从栈中读取上下文信息,进行恢复。

线程栈还用来存放函数中的局部变量:函数中的局部变量从线程栈空间中申请;函数中局部变量初始时从寄存器中分配(ARM 架构),当这个函数再调用另一个函数时,这些局部变量将放入栈中。

对于线程第一次运行,可以以手工的方式构造这个上下文来设置一些初始的环境:入口函数(PC 寄存器)、入口参数(R0 寄存器)、返回位置(LR 寄存器)、当前机器运行状态(CPSR 寄存器)。

线程状态

线程运行的过程中,同一时间内只允许一个线程在处理器中运行,从运行的过程上划分,线程有不同的运行状态。 在 RT-Thread 中,线程包含五种状态,操作系统会自动根据它运行的情况来动态调整它的状态。 RT-Thread 中线程的五种状态,如下所示:

  1. 当线程刚开始创建还没开始运行时就处于初始状态。初始状态下线程不参与调度。 此状态在 RT-Thread 中的宏定义为 RT_THREAD_INIT

  1. 就绪状态,线程按照优先级排队等待被执行。一旦当前线程运行完毕, 操作系统会马上寻找最高优先级的就绪态线程运行。此状态在 RT-Thread 中的宏定义为 RT_THREAD_READY

  1. 运行状态。 线程当前正在运行。在单核系统中,只有 rt_thread_self() 函数返回的线程处于运行状态;在多核系统中,可能就不止这一个线程处于运行状态。此状态在 RT-Thread 中的宏定义为 RT_THREAD_RUNNING

  1. 挂起状态。也叫做阻塞态。它可能因为资源不可用而挂起等待或者线程主动延时一段时间而挂起,挂起状态下线程不参与调度。 此状态在 RT-Thread 中的宏定义为 RT_THREAD_SUSPEND

  1. 关闭状态。 当线程运行结束时将处于关闭状态。关闭状态的线程不参与线程的调度。此状态在 RT-Thread 中的宏定义为 RT_THREAD_CLOSE

线程优先级

RT-Thread 线程的优先级是表示线程被调度的优先程度。每个线程都具有优先级,线程越重要,赋予的优先级就应越高,线程被调度的可能才会越大。 RT-Thread 最大支持 256 个线程优先级 (0~255),数值越小的优先级越高,0 为最高优先级。

时间片

每个线程都有时间片这个参数,但时间片仅对优先级相同的就绪态线程有效。系统对优先级相同的就绪态线程采用时间片轮转的调度方式进行调度时,时间片起到约束线程单次运行时长的作用 , 其单位是一个系统节拍(OS Tick) 。

假设有 2 个优先级相同的就绪态线程 A 与 B,A 线程的时间片设置为 10,B 线程的时间片设置为 5,那么当系统中不存在比 A 优先级高的就绪态线程时,系统会在 A、B 线程间来回切换执行,并且每次对 A 线程执行 10 个节拍的时长,对 B 线程执行 5 个节拍的时长,如下图。

线程的入口函数

线程控制块中的entry是线程的入口函数,它是线程实现预期功能的函数。入口函数由用户自行设计,一般有两种代码形式

无限循环模式

在实时系统中线程通常那个是被动式的:这个是由实时系统的特性所决定的,实时系统通常总是等待外界事情的发生而后进行相应的服务

void thread_entry(void* paramenter)
{
    while (1)
    {
    /* 等待事件的发生 */

    /* 对事件进行服务、进行处理 */
    }
}

实时操作系统中必须注意的一点就是:线程中不能陷入死循环操作,必须要有让出 CPU 使用权的动作,如循环中调用延时函数或者主动挂起。用户设计这种无限循环的线程的目的,就是为了让这个线程一直被系统循环调度运行,永不删除。

顺序执行或有限次循环模式

如简单的顺序语句、do while() 或 for()循环等,此类线程不会循环或不会永久循环,可谓是 “一次性” 线程,一定会被执行完毕。在执行完毕后,线程将被系统自动删除。

static void thread_entry(void* parameter)
{
    /* 处理事务 #1 */
    …
    /* 处理事务 #2 */
    …
    /* 处理事务 #3 */
}

线程错误码

一个线程就是一个执行场景,错误码是与执行环境密切相关的所以每个线程配备了一个变量用于保存错误码,错误码由以下几种

#define RT_EOK 0 /* 无错误 */

#define RT_ERROR 1 /* 普通错误 */

#define RT_ETIMEOUT 2 /* 超时错误 */

#define RT_EFULL 3 /* 资源已满 */

#define RT_EEMPTY 4 /* 无资源 */

#define RT_ENOMEM 5 /* 无内存 */

#define RT_ENOSYS 6 /* 系统不支持 */

#define RT_EBUSY 7 /* 系统忙 */

#define RT_EIO 8 /* IO 错误 */

#define RT_EINTR 9 /* 中断系统调用 */

#define RT_EINVAL 10 /* 非法参数 */

线程状态切换

RT-Thread 提供一系列的操作系统调用接口,使得线程的状态在这五个状态之间来回切换。几种状态间的转换关系如图所示:

  • 线程通过调用函数 rt_thread_create/init() 进入到初始状态(RT_THREAD_INIT)

  • 初始状态的线程通过调用函数 rt_thread_startup() 进入到就绪状态(RT_THREAD_READY)

  • 就绪状态的线程被调度器调度后进入运行状态(RT_THREAD_RUNNING)

  • 当处于运行状态的线程调用 rt_thread_delay(),rt_sem_take(),rt_mutex_take(),rt_mb_recv() 等函数或者获取不到资源时,将进入到挂起状态(RT_THREAD_SUSPEND)

  • 处于挂起状态的线程,如果等待超时依然未能获得资源或由于其他线程释放了资源,那么它将返回到就绪状态

  • 挂起状态的线程,如果调用 rt_thread_delete/detach() 函数,将更改为关闭状态(RT_THREAD_CLOSE)

  • 而运行状态的线程,如果运行结束,就会在线程的最后部分执行 rt_thread_exit() 函数,将状态更改为关闭状态。

系统线程

在 RT-Thread 内核中的系统线程有空闲线程和主线程。

空闲线程

空闲线程(idle)是系统创建的最低优先级的线程,线程状态永远为就绪态。

当系统中无其他就绪线程存在时,调度器将调度到空闲线程,它通常是一个死循环,且永远不能被挂起。另外,空闲线程在 RT-Thread 也有着它的特殊用途 :

若某线程运行完毕,系统将自动删除线程:自动执行 rt_thread_exit() 函数,先将该线程从系统就绪队列中删除,再将该线程的状态更改为关闭状态,不再参与系统调度,然后挂入 rt_thread_defunct 僵尸队列(资源未回收、处于关闭状态的线程队列)中,最后空闲线程会回收被删除线程的资源。

空闲线程也提供了接口来运行用户设置的钩子函数,在空闲线程运行时会调用该钩子函数,适合处理功耗管理、看门狗喂狗等工作。空闲线程必须有得到执行的机会,即其他线程不允许一直while(1)死卡,必须调用具有阻塞性质的函数;否则例如线程删除、回收等操作将无法得到正确执行。

主线程

在系统启动时,系统会创建 main 线程,它的入口函数为 main_thread_entry(),用户的应用入口函数 main() 就是从这里真正开始的,系统调度器启动后,main 线程就开始运行,过程如下图,用户可以在 main() 函数里添加自己的应用程序初始化代码。

线程的管理方式

下图描述了线程的相关操作,包含:创建/初始化线程,启动线程,运行线程,删除/脱离线程。可以使用 rt_thread_create() 创建一个动态线程,使用 rt_thread_init() 初始化一个静态线程,动态线程与静态线程的区别是:

动态线程是系统自动从动态内存堆上分配栈空间与线程句柄(初始化 heap 之后才能使用 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);

参数描述表

  • name 线程的名称

  • entry 线程入口函数

  • parameter 线程入口函数参数

  • stack_size 线程栈大小,单位是字节

  • priority 线程的优先级。优先级范围根据系统配置情况(rtconfig.h 中的 RT_THREAD_PRIORITY_MAX 宏定义),如果支持的是 256 级优先级,那么范围是从 0~255,数值越小优先级越高,0 代表最高优先级

  • tick 线程的时间片大小。时间片(tick)的单位是操作系统的时钟节拍。当系统中存在相同优先级线程时,这个参数指定线程一次调度能够运行的最大时间长度。这个时间片运行结束时,调度器自动选择下一个就绪态的同优先级线程进行运行

  • thread 线程创建成功,返回线程句柄

  • RT_NULL 线程创建失败

对于一些使用 rt_thread_create() 创建出来的线程,当不需要使用,或者运行出错时,我们可以使用下面的函数接口来从系统中把线程完全删除掉:rt_err_t rt_thread_delete(rt_thread_t thread);

初始化和脱离线程

线程的初始化可以使用下面的函数接口完成,来初始化静态线程对象:

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);

需要注意的是,用户提供的栈首地址需做系统对齐(例如 ARM 上需要做 4 字节对齐)

接口参数和返回值

  • thread 线程句柄。线程句柄由用户提供出来,并指向对应的线程控制块内存地址

  • name 线程的名称

  • entry 线程入口函数

  • parmeter 线程入口函数参数

  • stack_start 线程栈起始地址

  • stack_size 线程栈大小,单位是字节(需要栈对齐

  • priority 线程优先级

  • tick 线程时间片

对于用 rt_thread_init() 初始化的线程,使用 rt_thread_detach() 将使线程对象在线程队列和内核对象管理器中被脱离。线程脱离函数如下:

rt_err_t rt_thread_detach (rt_thread_t thread);

参数和返回值

  • thread 线程句柄

这个函数接口是和 rt_thread_delete() 函数相对应的, rt_thread_delete() 函数操作的对象是 rt_thread_create() 创建的句柄,而 rt_thread_detach() 函数操作的对象是使用 rt_thread_init() 函数初始化的线程控制块。同样,线程本身不应调用这个接口脱离线程本身。

启动线程

创建(初始化)的线程状态处于初始状态,并未进入就绪线程的调度队列,我们可以在线程初始化 / 创建成功后调用下面的函数接口让该线程进入就绪态:

rt_err_t rt_thread_startup(rt_thread_t thread);

调用这个函数的时候线程的状态更改为就绪状态,并且放到相应优先队列中等待调度。如果新启动的线程优先级比当前线程优先级高将立即切换到这个线程

使线程让出处理器资源

当前线程的时间片用完或者该线程主要要求让出处理器资源时,它将不再占有处理器,调度器会优先选择相同优先级的下一个线程执行。线程让出器使用下面的函数接口

rt_err_t rt_thread_yield(void);

使线程睡眠

在实际应用中,我们有时需要让当前线程延迟一段时间, 在指定的时间到达后重新运行,这就叫做 “线程睡眠”。线程睡眠可使用以下三个函数接口:

rt_err_t rt_thread_sleep(rt_tick_t tick);

rt_err_t rt_thread_delay(rt_tick_t tick);

rt_err_t rt_thread_mdelay(rt_int32_t ms);

参数和返回值

挂起和恢复线程

线程调用 rt_thread_delay() 时,线程将主动挂起;当调用 rt_sem_take(),rt_mb_recv() 等函数时,资源不可使用也将导致线程挂起。处于挂起状态的线程,如果其等待的资源超时(超过其设定的等待时间),那么该线程将不再等待这些资源,并返回到就绪状态;或者,当其他线程释放掉该线程所等待的资源时,该线程也会返回到就绪状态。

线程挂起使用下面的函数接口:

rt_err_t rt_thread_suspend (rt_thread_t thread);

参数和返回值

恢复线程就是让挂起的线程重新进入就绪状态,并将线程放入系统的就绪队列中。 程恢复使用下面的函数接口:

rt_err_t rt_thread_resume (rt_thread_t thread);

控制线程

需要对线程进行一些其它控制时可以调动如下的函数接口

rt_err_t rt_thread_control(rt_thread_t thread, rt_uint8_t cmd, void* arg);

  • thread 线程句柄

  • cmd 指示控制命令

  • arg 控制参数

cmd当前支持的命令包括

•RT_THREAD_CTRL_CHANGE_PRIORITY:动态更改线程的优先级;

•RT_THREAD_CTRL_STARTUP:开始运行一个线程,等同于 rt_thread_startup() 函数调用;

•RT_THREAD_CTRL_CLOSE:关闭一个线程,等同于 rt_thread_delete() 或 rt_thread_detach() 函数调用。

设置调度器钩子

有时用户可能会想知道在一个时刻发生了什么样的线程切换,可以通过调用下面的函数接口设置一个相应的钩子函数。在系统线程切换时,这个钩子函数将被调用:

void rt_scheduler_sethook(void (*hook)(struct rt_thread* from, struct rt_thread* to));

参数

void hook(struct rt_thread* from, struct rt_thread* to);

hook参数

线程示例

这个例子创建一个动态线程初始化一个静态线程,一个线程在运行完毕后自动被系统删除,另一个线程一直打印计数,如下代码:

#include<rtthread.h>

#define THREAD_PROIRITY 25

#define THREAD_STACK_SIZE 512

#define THREAD_TIMESLICE 5

static rt_thread tid1 = RT_NULL;

/* 线程 1 的入口函数 */

static void thread1_enrty(void *parmeter)

{

rt_uin32_t count = 0;

while(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;

static void thread2_entry(void *param)

{

rt_uint32_t count = 0;

// /* 线程 2 拥有较高的优先级,以抢占线程 1 而获得执行 *

for(count=0;count < 10;count++)

{

rt_kprintf("thread2 count: %d\n", count);

}

rt_kprintf("thread2 exit\n");

/* 线程 2 运行结束后也将自动被系统脱离 */

}

int thread_sample(void)

{

t1d1 + rt_thread_create("thread1",

thread1_entry, RT_NULL,

THREAD_STACK_SIZE,

THREAD_PRIORITY, THREAD_TIMESLICE);

if (tid1 != RT_NULL)

rt_thread_startup(tid1);

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);

结果如下

msh >thread_sample

msh >thread2 count: 0

thread2 count: 1

thread2 count: 2

thread2 count: 3

thread2 count: 4

thread2 count: 5

thread2 count: 6

thread2 count: 7

thread2 count: 8

thread2 count: 9

thread2 exit

thread1 count: 0

thread1 count: 1

thread1 count: 2

thread1 count: 3

线程2执行到一定值后自动删除,计数停止。线程1一直打印计数。

线程时间片轮转调度示例

这个例子创建两个线程,在执行时会一直打印计数,如下代码:

#include <rtthread.h>

#define THREAD_STACK_SIZE 1024

#deline THREAD_PRIORITY 20

#deline THREAD_TIMESLICE 10

/* 线程入口 */

static void thread_entry(void* parmeter)

{

rt_utin32_t value;

rt_utin32_t count = 0;

value = (rt_uint32_t)parameter;

while(1)

{

if(0 == (count % 5))

{

rt_kprintf("thread %d is running ,thread %d count = %d\n", value , value , count);

if(count> 200)

return ;

}

count ++

}

}

  • 1
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值