内核入门(三)——线程创建与管理

一、线程概述

1.1 什么是线程

在博客“进程与线程的一个简单解释”中,有对进程、线程的形象介绍。由于进程切换占用过多资源和时间,RT-Thread采取的是多线程模式,对任务进行更为精细的管理。
以下内容摘自《RT-Thread编程指南》:
在日常生活中,我们要完成一个大任务,一般会将它分解成多个简单、容易解决的小问题。在多线程操作系统中,也同样需要开发人员把一个复杂的应用分解成多个小的、可调度的、序列化的程序单元,当合理地划分任务并正确地执行时,这种设计能够让系统满足实时系统的性能及时间的要求,例如让嵌入式系统执行这样的任务,系统通过传感器采集数据,并通过显示屏将数据显示出来,在多线程实时系统中,可以将这个任务分解成两个子任务,如下图所示,一个子任务不间断地读取传感器数据,并将数据写到共享内存中,另外一个子任务周期性的从共享内存中读取数据,并将传感器数据输出到显示屏上。

1.2 RT-Thread线程特性

1.2.1 线程控制块

线程控制块是操作系统用于管理线程的一个数据结构,它会存放线程的一些信息,例如优先级、线程名称、线程状态等,也包含线程与线程之间连接用的链表结构,线程等待事件集合等。
线程控制块的定义如下:

/* 线程控制块*/
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; /* 用户数据*/
};

1.2.2 线程特性

  • 线程栈
    每个线程都有栈空间,在线程控块中定义。线程栈在形式上是一段连续的内存空间,通过定义一个数组,或申请一段动态内存来实现。
    当线程进行切换时,系统会将当前线程的上下文信息(各个变量和数据包所包含的寄存器变量、堆栈信息、内存信息等)保存到线程栈。
    线程栈大小可以这样设定,对于资源相对较大的MCU,可以适当设计较大的线程栈;也可以在初始时设置较大的栈,例如指定大小为1K 或2K 字节,然后在FinSH 中用list_thread 命令查看线程运行的过程中线程所使用的栈的大小,通过此命令,能够看到从线程启动运行时,到当前时刻点,线程使用的最大栈深度,而后加上适当的余量形成最终的线程栈大小,最后对栈空间大小加以修改。
  • 线程状态
    在RT-Thread 中,线程包含五种状态,操作系统会自动根据它运行的情况来动态调整它的状态。
状态 描述
初始状态 当线程刚开始创建还没开始运行时就处于初始状态;在初始状态下,线程不参与调度。此状态在RT-Thread 中的宏定义为RT_THREAD_INIT
就绪状态 在就绪状态下,线程按照优先级排队,等待被执行;一旦当前线程运行完毕让出处理器,操作系统会马上寻找最高优先级的就绪态线程运行。此状态在RT-Thread 中的宏定义为RT_THREAD_READY
运行状态 线程当前正在运行。在单核系统中,只有rt_thread_self() 函数返回的线程处于运行状态;在多核系统中,可能就不止这一个线程处于运行状态。此状态在RT-Thread 中的宏定义为RT_THREAD_RUNNING
挂起状态 也称阻塞态。它可能因为资源不可用而挂起等待,或线程主动延时一段时间而挂起。在挂起状态下,线程不参与调度。此状态在RT-Thread 中的宏定义为RT_THREAD_SUSPEND
关闭状态 当线程运行结束时将处于关闭状态。关闭状态的线程不参与线程的调度。此状态在RT-Thread 中的宏定义为RT_THREAD_CLOSE

我们可以通过响应的API来控制线程的状态转换,如下图。具体操作在线程管理部分介绍。

  • 线程优先级
    RT-Thread 最大支持256 个线程优先级(0~255),数值越小的优先级越高。ARM Cortex-M系列,普遍采用32 个优先级。最低优先级默认分配给空闲线程使用,用户一般不使用。在系统中,当有比当前线程优先级更高的线程就绪时,当前线程将立刻被换出,高优先级线程抢占处理器运行。

  • 时间片
    系统对优先级相同的就绪态线程采用时间片轮转的调度方式进行管理。时间片起到约束线程单次运行时长的作用,其单位是一个系统节拍(OS Tick)。
    假设有2 个优先级相同的就绪态线程A 与B,A 线程的时间片设置为10,B 线程的时间片设置为5,那么当系统中不存在比A 优先级高的就绪态线程时,统会在A、B 线程间来回切换执行,并且每次对A 线程执行10 个节拍的时长,对B 线程执行5 个节拍的时长。

  • 入口函数
    实现线程功能的函数,由线程控制块调用,代码形式可以分为顺序执行结构、无限循环结构。
    作为一个实时系统,一个优先级明确的实时系统,如果一个线程中的程序陷入了死循环操作,那么比它优先级低的线程都将不能够得到执行。所以在实时操作系统中必须注意的一点就是:线程中不能陷入死循环操作,必须要有让出CPU使用权的动作,如循环中调用延时函数或者主动挂起。

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

#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 /* 非法参数*/

1.3 系统线程

系统线程是指由系统创建的线程,用户线程是由用户程序调用线程管理接口创建的线程。在RT-Thread 内核中的系统线程有空闲线程和主线程。

1.3.1 空闲线程

空闲线程是系统创建的最低优先级的线程,线程状态永远为就绪态。当系统中无其他就绪线程存在时,调度器将调度到空闲线程,它通常是一个死循环,且永远不能被挂起。
若某线程运行完毕,系统将自动删除线程:自动执行rt_thread_exit() 函数,先将该线程从系统就绪队列中删除,再将该线程的状态更改为关闭状态,不再参与系统调度,然后挂入rt_thread_defunct 僵尸队列(资源未回收、处于关闭状态的线程队列)中,最后空闲线程会回收被删除线程的资源。
空闲线程也提供了接口来运行用户设置的钩子函数,在空闲线程运行时会调用该钩子函数,适合钩入功耗管理、看门狗喂狗等工作。

1.3.2 主线程

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

二、线程管理

2.1 创建和删除动态线程

线程相关的API具体参考官方的参考手册

  • 创建线程
    该函数用于创建动态线程,调用这个函数时,系统会从动态堆内存中分配一个线程句柄以及按照参数中指定的栈大小从动态堆内存中分配相应的空间。
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 优先级,数字越小优先级越高
tick 时间片大小,表示线程单次运行的时间长度
返回 ——
thread 线程创建成功,并返回线程句柄
RT_NULL 线程创建失败
  • 删除线程
    调用该函数后,线程对象将会被移出线程队列并且从内核对象管理器中删除,线程占用的堆栈空间也会被释放,收回的空间将重新用于其他的内存分配。实际上,用rt_thread_delete() 函数删除线程接口,仅仅是把相应的线程状态更改为RT_THREAD_CLOSE 状态,然后放入到rt_thread_defunct 队列中;而真正的删除动作(释放线程控制块和释放线程栈)需要到下一次执行空闲线程时,由空闲线程完成最后的线程删除动作。
rt_err_t rt_thread_delete(rt_thread_t thread);
参数 描述
thread 要删除的线程句柄(创建成功的返回值)
返回 ——
RT_EOK 线程删除成功
RT_ERROR 线程删除失败

2.2 初始化和剥离静态线程

  • 初始化线程
    静态线程的线程句柄(线程控制块指针)、线程栈由用户提供。静态线程是指线程控制块、线程运行栈一般都设置为全局变量,在编译时就被确定、被分配处理,内核不负责动态分配内存空间。静态线程的初始化由如下函数完成:
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 线程的名称
entry 入口函数
parameter 入口函数的参数
stack_start 线程栈的起始地址
stack_size 线程栈大小,单位字节
priority 优先级,数字越小优先级越高
tick 时间片大小,表示线程单次运行的时间长度
返回 ——
RT_EOK 线程创建成功
RT_ERROR 线程创建失败
  • 剥离线程
    响应的,对于用rt_thread_init() 初始化的线程,使用rt_thread_detach() 将使线程对象在线程队列和内核对象管理器中被脱离。
rt_err_t rt_thread_detach (rt_thread_t thread);
参数 描述
thread 要剥离的线程句柄
返回 ——
RT_EOK 线程剥离成功
RT_ERROR 线程剥离失败

2.3 启动线程

创建/初始化的线程状态处于初始状态,我们需要调用下面的函数接口让该线程进入就绪状态:

rt_err_t rt_thread_startup(rt_thread_t thread);
参数 描述
thread 要启动的线程句柄
返回 ——
RT_EOK 线程启动成功
RT_ERROR 线程启动失败

2.4 获得当前线程

在程序的运行过程中,相同的一段代码可能会被多个线程执行,在执行的时候可以通过下面的函数接口获得当前执行的线程句柄:

rt_thread_t rt_thread_self(void);
返回 描述
thread 当前运行的线程句柄
RT_NULL 返回失败,调度器未启动

2.5 使线程让出管理器资源

线程让出处理器使用下面的函数接口:

rt_err_t rt_thread_yield(void);

调用该函数后,当前线程首先把自己从它所在的就绪优先级线程队列中删除,然后把自己挂到这个优先级队列链表的尾部,然后激活调度器进行线程上下文切换(如果当前优先级只有这一个线程,则这个线程继续执行,不进行上下文切换动作)。

2.6 睡眠

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

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

这三个函数接口的作用相同,调用它们可以使当前线程挂起一段指定的时间,当这个时间过后,线程会被唤醒并再次进入就绪状态。三个函数的传入参数表示睡眠时间,sleep/delay 的传入参数tick以系统节拍(OS tick)为单位;mdelay 的传入参数以ms为单位。

2.7 挂起和恢复线程

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

rt_err_t rt_thread_suspend (rt_thread_t thread);

通常不建议使用该函数。如果不得已需要采用rt_thread_suspend() 函数挂起当前任务, 需要在调用rt_thread_suspend() 函数后立刻调用rt_schedule() 函数进行手动的线程上下文切换。

void rt_schedule (void)	//该函数将执行一次线程调度,选择一个具有最高优先级的线程,然后切换运行

恢复线程就是让挂起的线程重新进入就绪状态,并将线程放入系统的就绪队列中;如果被恢复线程在所有就绪态线程中,位于最高优先级链表的第一位,那么系统将进行线程上下文的切换。线程恢复使用下面的函数接口:

rt_err_t rt_thread_resume (rt_thread_t thread);

2.8 控制线程

当需要对线程进行一些控制或更改时,可以调用以下函数:

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() 函数调用。

2.9 空闲钩子函数

空闲钩子函数是空闲线程的钩子函数,如果设置了空闲钩子函数,就可以在系统执行空闲线程时,自动执行空闲钩子函数来做一些其他事情,比如系统指示灯。设置/ 删除空闲钩子的接口如下:

rt_err_t rt_thread_idle_sethook(void (*hook)(void));
rt_err_t rt_thread_idle_delhook(void (*hook)(void));

空闲线程是一个线程状态永远为就绪态的线程,因此设置的钩子函数必须保证空闲线程在任何时刻都不会处于挂起状态。

2.10 调度器钩子函数

在整个系统的运行时,系统都处于线程运行、中断触发- 响应中断、切换到其他线程,甚至是线程间的切换过程中,或者说系统的上下文切换是系统中最普遍的事件。有时用户可能会想知道在一个时刻发了什么样的线程切换,可以通过调用下面的函数接口设置一个相应的钩子函数。在系统线程切换时,这个钩子函数将被调用:

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表示系统所要切换到的线程控制块指针

猜你喜欢

转载自blog.csdn.net/qq_33604695/article/details/105423597