目录
线程管理
线程管理特点
线程工作机制
线程控制块
线程属性
线程栈
线程状态
线程优先级
时间片
线程入口函数
无限循环模式
顺序执行或有限次循环模式
线程错误码
线程状态切换
线程操作
创建动态线程
删除
初始化静态线程
脱离
获得当前线程
让出处理器资源
睡眠
控制线程
挂起线程
恢复线程
设置钩子函数
运行代码
RT-Thread是一个嵌入式实时多线程操作系统,基本属性之一是支持多任务,也就是允许多个任务同时运行,但是这并不意味着处理器在同一时刻真地执行了多个任务。事实上,这是通过多线程的方式实现的。线程是 RT-Thread 中最基本的调度单位,我们可以设置不同的优先级,重要的任务可设置相对较高的优先级,非重要的任务可以设置较低的优先级,不同的任务还可以设置相同的优先级,轮流运行,其实有点类似于之前学的中断。
当线程运行时,它会认为自己是以独占 CPU 的方式在运行,线程执行时的运行环境称为上下文,也就是各个变量和数据,包括所有的寄存器变量、堆栈、内存信息等。
RT-Thread 线程管理的主要功能是对线程进行管理和调度,系统中总共存在两类线程,分别是系统线程和用户线程,系统线程是由 RT-Thread 内核创建的线程,用户线程是由应用程序创建的线程,这两类线程都会从内核对象容器中分配线程对象,当线程被删除时,也会被从对象容器中删除。 而我们主要学习的就是用户线程的创建与调度,以及管理之类的。
RT-Thread 的线程调度器是抢占式的,主要的工作就是从就绪线程列表中查找最高优先级线程,保证最高优先级的线程能够被运行,最高优先级的任务一旦就绪,就能得到 CPU 的使用权。
当调度器调度线程切换时,先将当前线程上下文保存起来,当再切回到这个线程时,线程调度器将该恢复线程的上下文信息。
线程控制块就是一个结构体,描述了线程里面的所有信息,例如优先级、线程名称、线程状态等,也包含线程与线程之间连接用的链表结构,线程等待事件集合等。
我们来仔细看一下这个结构体。
/* 线程控制块 */
struct rt_thread
{
/* rt 对象 */
char name[RT_NAME_MAX]; /* 线程名称 */
rt_uint8_t type; /* 对象类型 */
rt_uint8_t flags; /* 标志位 */
#ifdef RT_USING_MODULE
void *module_id; /**< id of application module */
#endif
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; /* 线程状态 */
/* 对称多处理器,多核才用,我们不用 */
#ifdef RT_USING_SMP
rt_uint8_t bind_cpu; /**< thread is bind to cpu */
rt_uint8_t oncpu; /**< process on cpu` */
rt_uint16_t scheduler_lock_nest; /**< scheduler lock count */
rt_uint16_t cpus_lock_nest; /**< cpus lock count */
rt_uint16_t critical_lock_nest; /**< critical lock count */
#endif /*RT_USING_SMP*/
/* 优先级 */
rt_uint8_t current_priority; /* 当前优先级 */
rt_uint8_t init_priority; /* 初始优先级 */
/* 如果优先级大于32 */
#if RT_THREAD_PRIORITY_MAX > 32
rt_uint8_t number;
rt_uint8_t high_mask;
#endif
rt_uint32_t number_mask;
/* 事件 */
#if defined(RT_USING_EVENT)
/* thread event */
rt_uint32_t event_set; /* 事件集合 */
rt_uint8_t event_info;
#endif
#if defined(RT_USING_SIGNALS)
rt_sigset_t sig_pending; /**< the pending signals */
rt_sigset_t sig_mask; /**< the mask bits of signal */
/* 对称多处理器,多核才用,我们不用 */
#ifndef RT_USING_SMP
void *sig_ret; /**< the return stack pointer from signal */
#endif
rt_sighandler_t *sig_vectors; /**< vectors of signal handler */
void *si_list; /**< the signal infor list */
#endif
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; /* 用户数据 */
};
rt_list_t list;
这里面的 rt_list_t 是一个什么结构类型呢,我们来看看。我们提高定义可以得到,这是一个双链表类型。
/**
* Double List structure
*/
struct rt_list_node
{
struct rt_list_node *next; /**< point to next node. */
struct rt_list_node *prev; /**< point to prev node. */
};
typedef struct rt_list_node rt_list_t;
cleanup 会在线程退出时,被空闲线程回调一次以执行用户设置的清理现场等工作。最后的一个成员 user_data 可由用户挂接一些数据信息到线程控制块中,以提供一种类似线程私有数据的实现方式。
RT-Thread 线程具有独立的栈,当进行线程切换时,会将当前线程的上下文存在栈中,当线程要恢复运行时,再从栈中读取上下文信息,进行恢复。线程栈还用来存放函数中的局部变量:函数中的局部变量从线程栈空间中申请;函数中局部变量初始时从寄存器中分配,当这个函数再调用另一个函数时,这些局部变量将放入栈中。
线程栈的增长方向是芯片构架密切相关的,对于 ARM Cortex-M 架构,线程栈可构造如下图所示。
主要包含5个状态,我们来看一看。
状态 | 描述 |
---|---|
初始状态 | 当线程刚开始创建还没开始运行时就处于初始状态;在初始状态下,线程不参与调度。此状态在 RT-Thread 中的宏定义为 RT_THREAD_INIT |
就绪状态 | 在就绪状态下,线程按照优先级排队,等待被执行;一旦当前线程运行完毕让出处理器,操作系统会马上寻找最高优先级的就绪态线程运行。此状态在 RT-Thread 中的宏定义为 RT_THREAD_READY 这个状态线程是参与调度的,只不过没有占用CPU。 |
运行状态 | 线程当前正在运行。在单核系统中,只有 rt_thread_self() 函数返回的线程处于运行状态;在多核系统中,可能就不止这一个线程处于运行状态。此状态在 RT-Thread 中的宏定义为 RT_THREAD_RUNNING |
挂起状态 | 也称阻塞态。它可能因为资源不可用而挂起等待,或线程主动延时一段时间而挂起。在挂起状态下,线程不参与调度。此状态在 RT-Thread 中的宏定义为 RT_THREAD_SUSPEND |
关闭状态 | 当线程运行结束时将处于关闭状态。关闭状态的线程不参与线程的调度。此状态在 RT-Thread 中的宏定义为 RT_THREAD_CLOSE |
RT-Thread 最大支持 256 个线程优先级 (0~255),数值越小的优先级越高,0 为最高优先级。在一些资源比较紧张的系统中,可以根据实际情况选择只支持 8 个或 32 个优先级的系统配置;对于 ARM Cortex-M 系列,普遍采用 32 个优先级。最低优先级默认分配给空闲线程使用,用户一般不使用,主要用来回收资源。在系统中,当有比当前线程优先级更高的线程就绪时,当前线程将立刻被换出,高优先级线程抢占处理器运行。
每个线程都有时间片这个参数,但时间片仅对优先级相同的就绪态线程有效。系统对优先级相同的就绪态线程采用时间片轮转的调度方式进行调度时,时间片起到约束线程单次运行时长的作用。
线程控制块中的 entry 是线程的入口函数,它是线程实现预期功能的函数。线程的入口函数由用户设计实现,一般有以下两种代码形式。
作为一个实时系统,一个优先级明确的实时系统,如果一个线程中的程序陷入了死循环操作,那么比它优先级低的线程都将不能够得到执行。所以在实时操作系统中必须注意的一点就是:线程中不能陷入死循环操作,必须要有让出 CPU 使用权的动作,如循环中调用延时函数或者主动挂起。用户设计这种无限循环的线程的目的,就是为了让这个线程一直被系统循环调度运行,永不删除。
void thread_entry(void* paramenter)
{
while (1)
{
/* 等待事件的发生 */
/* 对事件进行服务、进行处理 */
}
}
如简单的顺序语句、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_create/init() 进入到初始状态;初始状态的线程通过调用函数 rt_thread_startup() 进入到就绪状态;就绪状态的线程被调度器调度后进入运行状态,调用rt_thread_suspend()函数切换为挂起状态;当处于运行状态的线程调用 rt_sem_take(),rt_mutex_take(),rt_mb_recv() 等函数或者获取不到资源时,将进入到挂起状态;处于挂起状态的线程,如果等待超时依然未能获得资源或由于其他线程释放了资源,那么它将返回到就绪状态。挂起状态的线程,如果调用 rt_thread_delete/detach() 函数,将更改为关闭状态;而运行状态的线程,如果运行结束,就会在线程的最后部分执行 rt_thread_exit() 函数,将状态更改为关闭状态。
就绪状态与运行状态是等同的。
线程的相关操作包含:创建 / 初始化线程、启动线程、运行线程、删除 / 脱离线程。可以使用 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,// 线程优先级 0-31
rt_uint32_t tick)// 时间片
// 返回的是线程控制块
参数 | 描述 |
---|---|
name | 线程的名称;线程名称的最大长度由 rtconfig.h 中的宏 RT_NAME_MAX 指定,多余部分会被自动截掉 |
entry | 线程入口函数 |
parameter | 线程入口函数参数 |
stack_size | 线程栈大小,单位是字节 |
priority | 线程的优先级。优先级范围根据系统配置情况(rtconfig.h 中的 RT_THREAD_PRIORITY_MAX 宏定义),如果支持的是 256 级优先级,那么范围是从 0~255,数值越小优先级越高,0 代表最高优先级 |
tick | 线程的时间片大小。时间片(tick)的单位是操作系统的时钟节拍。当系统中存在相同优先级线程时,这个参数指定线程一次调度能够运行的最大时间长度。这个时间片运行结束时,调度器自动选择下一个就绪态的同优先级线程进行运行 |
返回 | 线程控制块 |
thread | 线程创建成功,返回线程句柄 |
RT_NULL | 线程创建失败 |
调用该函数后,线程对象将会被移出线程队列并且从内核对象管理器中删除,线程占用的堆栈空间也会被释放,收回的空间将重新用于其他的内存分配。
rt_err_t rt_thread_delete(rt_thread_t thread)// 参数是结构体指针,返回值是一个错误码
参数 | 描述 |
---|---|
thread | 要删除的线程句柄 |
返回 | 错误码 |
RT_EOK | 删除线程成功 |
-RT_ERROR | 删除线程失败 |
接下来,我们来自己创建一个线程,代码如下。
rt_thread_t th1_ptr = NULL;
void th_entry(void *parameter)
{
while(1){
rt_kprintf("th_entry running......");
rt_thread_mdelay(1000);
}
}
int main(void)
{
th1_ptr = rt_thread_create("first_th",th_entry,NULL,512,20,5);
if (th1_ptr == RT_NULL) {
LOG_E("rt_thread_create fail ....");
}
LOG_D("rt_thread_create successed....");
}
运行结果如下,我们可以清楚的看到线程是创建成功了,接下来我们来尝试一下其他的函数,delete函数我们不建议自己去调用,因为线程会自己调用。
和创建动态线程很像,就是加上了栈起始参数。 静态线程是指线程控制块、线程运行栈一般都设置为全局变量,在编译时就被确定、被分配处理,内核不负责动态分配内存空间。
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 | 线程句柄。线程句柄由用户提供出来,并指向对应的线程控制块内存地址 |
name | 线程的名称;线程名称的最大长度由 rtconfig.h 中定义的 RT_NAME_MAX 宏指定,多余部分会被自动截掉 |
entry | 线程入口函数 |
parameter | 线程入口函数参数 |
stack_start | 线程栈起始地址 |
stack_size | 线程栈大小,单位是字节。在大多数系统中需要做栈空间地址对齐(例如 ARM 体系结构中需要向 4 字节地址对齐) |
priority | 线程的优先级。优先级范围根据系统配置情况(rtconfig.h 中的 RT_THREAD_PRIORITY_MAX 宏定义),如果支持的是 256 级优先级,那么范围是从 0 ~ 255,数值越小优先级越高,0 代表最高优先级 |
tick | 线程的时间片大小。时间片(tick)的单位是操作系统的时钟节拍。当系统中存在相同优先级线程时,这个参数指定线程一次调度能够运行的最大时间长度。这个时间片运行结束时,调度器自动选择下一个就绪态的同优先级线程进行运行 |
返回 | 错误码 |
RT_EOK | 线程创建成功 |
-RT_ERROR | 线程创建失败 |
对于用 rt_thread_init() 初始化的线程,使用 rt_thread_detach() 将使线程对象在线程队列和内核对象管理器中被脱离。线程脱离函数如下:
rt_err_t rt_thread_detach (rt_thread_t thread);
参数 | 描述 |
---|---|
thread | 线程句柄,它应该是由 rt_thread_init 进行初始化的线程句柄。 |
返回 | —— |
RT_EOK | 线程脱离成功 |
-RT_ERROR | 线程脱离失败 |
在程序的运行过程中,相同的一段代码可能会被多个线程执行,在执行的时候可以通过下面的函数接口获得当前执行的线程句柄:
rt_thread_t rt_thread_self(void);
返回 | 描述 |
---|---|
thread | 当前运行的线程句柄 |
RT_NULL | 失败,调度器还未启动 |
当前线程的时间片用完或者该线程主动要求让出处理器资源时,它将不再占有处理器,调度器会选择相同优先级的下一个线程执行。线程调用这个接口后,这个线程仍然在就绪队列中。线程让出处理器使用下面的函数接口:
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);
参数 | 描述 |
---|---|
tick/ms | 线程睡眠的时间: sleep/delay 的传入参数 tick 以 1 个 OS Tick 为单位 ,这个不一样; mdelay 的传入参数 ms 以 1ms 为单位; |
返回 | —— |
RT_EOK | 操作成功 |
当需要对线程进行一些其他控制时,例如动态更改线程的优先级,可以调用如下函数接口:
rt_err_t rt_thread_control(rt_thread_t thread, rt_uint8_t cmd, void* arg);
函数参数 | 描述 |
---|---|
thread | 线程句柄 |
cmd | 指示控制命令 |
arg | 控制参数(参数地址) |
返回 | —— |
RT_EOK | 控制执行正确 |
-RT_ERROR | 失败 |
指示控制命令 cmd 当前支持的命令包括:
•RT_THREAD_CTRL_CHANGE_PRIORITY:动态更改线程的优先级;
•RT_THREAD_CTRL_STARTUP:开始运行一个线程,等同于 rt_thread_startup() 函数调用;
•RT_THREAD_CTRL_CLOSE:关闭一个线程,等同于 rt_thread_delete() 或 rt_thread_detach() 函数调用。
线程挂起使用下面的函数接口:
rt_err_t rt_thread_suspend (rt_thread_t thread);
参数 | 描述 |
---|---|
thread | 线程句柄 |
返回 | —— |
RT_EOK | 线程挂起成功 |
-RT_ERROR | 线程挂起失败,因为该线程的状态并不是就绪状态 |
恢复线程就是让挂起的线程重新进入就绪状态,并将线程放入系统的就绪队列中;
rt_err_t rt_thread_resume (rt_thread_t thread);
参数 | 描述 |
---|---|
thread | 线程句柄 |
返回 | —— |
RT_EOK | 线程恢复成功 |
-RT_ERROR | 线程恢复失败,因为该个线程的状态并不是 RT_THREAD_SUSPEND 状态 |
在整个系统的运行时,系统都处于线程运行、中断触发 - 响应中断、切换到其他线程,甚至是线程间的切换过程中,或者说系统的上下文切换是系统中最普遍的事件。有时用户可能会想知道在一个时刻发生了什么样的线程切换,可以通过调用下面的函数接口设置一个相应的钩子函数。在系统线程切换时,这个钩子函数将被调用:
void rt_scheduler_sethook(void (*hook)(struct rt_thread* from, struct rt_thread* to));
钩子函数 hook() 的声明如下:
void hook(struct rt_thread* from, struct rt_thread* to);
函数参数 | 描述 |
---|---|
from | 表示系统所要切换出的线程控制块指针 |
to | 表示系统所要切换到的线程控制块指针 |
rt_thread_t th1_ptr = NULL;
struct rt_thread th2;
rt_uint8_t th2_stack[512] = {0};
void th1_entry(void *parameter)
{
int i;
for(i = 0; i < 5; i++)
{
rt_kprintf("th1_entry running......\n");
rt_thread_mdelay(1000);
}
}
void th2_entry(void *parameter)
{
int i;
for(i = 0; i < 5; i++)
{
rt_kprintf("th2_entry running......\n");
rt_thread_mdelay(1000);
}
}
void scheduler_hook(struct rt_thread* from, struct rt_thread* to)
{
rt_kprintf("from:%s ------> to:%s\n",from->name,to->name);
}
int main(void)
{
int ret = 0;
rt_scheduler_sethook(scheduler_hook);
th1_ptr = rt_thread_create("first_th",th1_entry,NULL,512,20,5);
if (th1_ptr == RT_NULL) {
LOG_E("rt_thread_create fail ....\n");
return -RT_ENOMEM;
}
LOG_D("rt_thread_create successed....\n");
/* 运行线程*/
rt_thread_startup(th1_ptr);
ret = rt_thread_init(&th2, "second_th", th2_entry, NULL, th2_stack, sizeof(th2_stack), 19, 5);
if (ret < 0) {
LOG_E("rt_thread_init fail ....\n");
return ret;
}
LOG_D("rt_thread_init successed....\n");
/* 运行线程*/
rt_thread_startup(&th2);
}
运行结果
本节学习的是有关线程的知识,不得不说,RT官方的手册实在是太清晰了。