目录
RTT线程管理
线程管理特点
线程工作机制
线程控制块
线程属性
线程状态之间切换
线程相关操作
创建和删除线程
创建线程
删除线程
动态创建线程实例
启动线程
初始化和脱离线程
初始化线程
脱离线程
静态创建线程实例
线程辅助函数
获得当前线程
让出处理器资源
线程睡眠
控制线程函数
设置和删除idle线程hook函数
设置钩子函数
删除钩子函数
设置调度器hook函数
线程调度器hook函数实例
RT-Thread是支持多任务的操作系统,多任务是通过多线程的方式实现。线程是任务的载体,是RTT中最基本的调度单位。
线程在运行的时候,它自己会认为独占CPU运行
线程执行时的运行环境称为上下文(与之相对应的有中断上下文),具体来说就是各个变量和数据,包括所有的寄存器变量、堆栈、内存信息等。
RT-Thread 线程管理的主要功能是对线程进行管理和调度,系统中总共存在两类线程,分别是系统线程和用户线程,系统线程是由 RT-Thread 内核创建的线程,用户线程是由应用程序创建的线程,这两类线程都会从内核对象容器中分配线程对象,当线程被删除时,也会被从对象容器中删除。
RT-Thread 的线程调度器是抢占式的,主要的工作就是从就绪线程列表中查找最高优先级线程,保证最高优先级的线程能够被运行,最高优先级的任务一旦就绪,总能得到 CPU 的使用权。
当调度器调度线程切换时,先将当前线程上下文保存起来,当再切回到这个线程时,线程调度器将该线程的上下文信息恢复。(我们在切换完以后,当再次回到之前执行的线程时候,要从被打断的地方重新开始执行,所以我们要将线程上下文先保存起来)线程再被调度之前,我们要保存现场,保存完现场之后再执行高优先级的线程,在我们再调度回来的时候,再恢复现场。但这些不需要我们应用层手动来做。
线程控制块由结构体 struct rt_thread 表示,线程控制块是操作系统用于管理线程的一个数据结构,它会存放线程的一些信息,例如优先级、线程名称、线程状态等,也包含线程与线程之间连接用的链表结构(内核在管理多个线程的时候,将描述每个线程的结构体采用一个列表的形式来管理起来,为了方便内核后期来查找和管理),线程等待事件集合等。
struct rt_thread
{
/* rt object */
char name[RT_NAME_MAX]; /**
其中有个对称多核处理器的功能,如果属于cortex-M系列的处理器,因为是单核,所以不使用对称多核处理器,但cortex-A系统多核支持
注:cleanup函数指针指向的函数,会在线程退出的时候,被idle线程回调一次,执行用户设置的清理现场等工作。
RT-Thread 线程具有独立的栈,当进行线程切换时,会将当前线程的上下文存在栈中,当线程要恢复运行时,再从栈中读取上下文信息,进行恢复。
可以通过查看线程控制块结构体的flags成员来判断当前线程所处状态
RT-Thread 最大支持 256 个线程优先级 (0~255),数值越小的优先级越高,0 为最高优先级。在一些资源比较紧张的系统中,可以根据实际情况选择只支持 8 个或 32 个优先级的系统配置;对于 ARM Cortex-M系列,普遍采用 32 个优先级。最低优先级默认分配给空闲线程使用,用户一般不使用。在系统中,当有比当前线程优先级更高的线程就绪时,当前线程将立刻被换出,高优先级线程抢占处理器运行
每个线程都有时间片这个参数,但时间片仅对优先级相同的就绪态线程有效。(如果多个线程的优先级相同,对于同一优先级的线程,调度的时候根据时间片进行调度,即每个线程调度一段时间然后切换另一个线程来调度执行)
注意:
作为一个实时系统,一个优先级明确的实时系统,如果一个线程中的程序陷入了死循环操作,那么比它优先级低的线程都将不能够得到执行。所以在实时操作系统中必须注意的一点就是:线程中不能陷入死循环操作,必须要有让出 CPU使用权的动作,如循环中调用延时函数或者主动挂起(两种方法都能使线程处于挂起态,挂起态是不参加线程调度的,这时我们的系统就可以调度其它线程去执行)。
注:就绪状态和运行状态是等同的,区别在于谁拥有线程的执行权。
就绪状态通过主动挂起等待或者主动延时一段时间,进入挂起状态,不参与线程调度
处于挂起状态的线程通过序号2中的函数可以恢复到就绪状态(如线程重启、信号释放、锁释放等等)
处于运行状态的线程通过序号1中的函数可以进入到挂起状态,这些函数与序号2中是相对的
线程相关的操作包括:创建(动态)/初始化(静态)、启动、运行、删除/脱离。
动态线程与静态线程的区别是:动态线程是系统自动从动态内存堆上分配栈空间与线程句柄(初始化 heap 之后才能使用 create 创建动态线程),静态线程是由用户分配栈空间与线程句柄。
参数1:用于给当前线程起名字
参数2:函数指针,指向了线程处理函数
参数3:用于给线程处理函数传递参数
参数4:要创建的线程对应线程栈的大小(指定大小之后,系统会自动动态分配空间)
参数5:线程优先级0-31(由高到低)
参数6:如果优先级相同的线程都处于就绪态,线程调度由tick来决定,按照时间片轮流调度
/**
* This function will create a thread objectand allocate thread object memory
* and stack.
*
* @param name the name of thread, which shallbe unique
* @param entry the entry function of thread
* @param parameter the parameter of threadenter function
* @param stack_size the size of thread stack
* @param priority the priority of thread
* @param tick the time slice if there are samepriority thread
*
* @return the created thread object
*/
rt_thread_t rt_thread_create(constchar *name,
void (*entry)(void*parameter),
void *parameter,
rt_uint32_tstack_size,
rt_uint8_t priority,
rt_uint32_t tick)
返回值类型为线程控制块,目的是将初始化好的线程信息写到线程控制块的每个成员变量中,后期再根据线程的状态来修改线程控制块中对应信息的值
删除线程函数对应创建线程函数
参数为线程控制块结构体的指针
返回值为错误码,如果删除成功返回RT_EOK、失败返回 -RT_ERROR
/**
* This function will delete a thread. Thethread object will be removed from
* thread queue and deleted from system objectmanagement in the idle thread.
*
* @param thread the thread to be deleted
*
* @return the operation status, RT_EOK on OK,-RT_ERROR on error
*/
rt_err_t rt_thread_delete(rt_thread_tthread)
首先创建一个通过线程块结构体指针创建一个线程结构体指针变量;调用线程创建函数,并用刚刚定义的指针变量来接收返回值
然后定义线程处理函数,并给线程创建函数填入参数,其中处理函数参数设置为NULL,栈空间设置为1024,优先级20,时间片为5(后期定时器细论)。一定要注意参数类型一一对应。
接着通过返回值判断线程是否创建成功,成功用LOG_D打印调试信息,否则用LOG_E打印失败信息。其中RT_NULL的宏值为0
最后编写完线程处理函数。设置一个while循环,在循环中通过rt_kprintf(相当于串口printf)打印输出信息。接着用rt_thread_mdelay函数进行延时1s,用于释放当前CPU资源,让线程调度器调度其它线程。
现象
通过打开串口终端,发现线程被创建成功,但是没有打印运行信息
通过输入list_thread命令查看当前存在线程,发现我们创建的线程已经存在,是处于初始状态的,如果想要运行线程,必须调用线程启动函数rt_thread_startup启动
注意:线程删除函数,我们尽量不要人为去调用。因为我们创建好线程以后,就希望线程能够一直去处理相关的事务。如果线程处理函数里面的处理操作被执行完了(函数运行结束),系统会自动调用线程删除函数来回收资源
参数为线程控制块结构体的指针
返回值为错误码,如果删除成功返回RT_EOK、失败返回 -RT_ERROR
/**
* This function will start a thread and put itto system ready queue
*
* @param thread the thread to be started
*
* @return the operation status, RT_EOK on OK,-RT_ERROR on error
*/
rt_err_t rt_thread_startup(rt_thread_t thread)
注:当调用这个函数时,将把线程的状态更改为就绪状态,并放到相应优先级队列中等待调度。如果新启动的线程优先级比当前线程优先级高,将立刻切换到这个线程。
在创建线程后,我们如果失败就返回RT_ENOMEM,表示可能由于没有空间导致了创建线程失败。
如果创建成功,就启动线程
运行结果
通过查看线程状态,可发现当前线程处于suspend挂起的状态
这是由于我们在线程处理函数中,大部分的时间都处于休眠(延时函数),休眠就是从运行态或者就绪态切换到挂起状态
线程的初始化可以使用下面的函数接口完成,来初始化静态线程对象:
参数1:是线程控制块结构体,以指针的形式传入参数
参数2:用于给当前线程起名字
参数3:函数指针,指向了线程处理函数
参数4:用于给线程处理函数传递参数
参数5:栈的起始地址(分配好空间的线程栈的首地址)
参数6:要创建的线程对应线程栈的大小
参数7:线程优先级0-31(由高到低)
参数8:如果优先级相同的线程都处于就绪态,线程调度由tick来决定,按照时间片轮流调度
返回值与动态创建不同,如果错误这里返回的是负数错误码
注:structrt_thread *即rt_thread_t
/**
* This function will initialize a thread,normally it's used to initialize a
* static thread object.
*
* @param thread the static thread object
* @param name the name of thread, which shallbe unique
* @param entry the entry function of thread
* @param parameter the parameter of threadenter function
* @param stack_start the start address ofthread stack
* @param stack_size the size of thread stack
* @param priority the priority of thread
* @param tick the time slice if there are samepriority thread
*
* @return the operation status, RT_EOK on OK,-RT_ERROR on error
*/
rt_err_t rt_thread_init(structrt_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)
作用是将线程从当前的线程队列里移除出去
参数是线程控制块结构体,以指针的形式传入参数
返回值为错误码
/**
* This function will detach a thread. Thethread object will be removed from
* thread queue and detached/deleted fromsystem object management.
*
* @param thread the thread to be deleted
*
* @return the operation status, RT_EOK on OK,-RT_ERROR on error
*/
rt_err_t rt_thread_detach(rt_thread_t thread)
首先创建好线程结构体对象,开辟好栈空间,编写线程处理函数
调用线程初始化函数
运行结果
可以发现两个线程都被创建成功并且处于初始状态,此外tshell为当前终端线程,tidle0为空闲线程,timer为定时器
我们同时启动两个线程,可以发现两个线程交替运行
我们将线程2处理函数中的延时关闭,重新编译下载
成功发现优先级高的线程2运行完之后,才调度线程1
在程序的运行过程中,相同的一段代码可能会被多个线程执行,在执行的时候可以通过下面的函数接口获得当前执行的线程句柄
/**
* This function will return self thread object
*
* @return the self thread object , failedRT_NULL
*/
rt_thread_t rt_thread_self(void)
该函数的作用让当前运行的线程让出CPU的使用权,恢复到就绪态,调度器将会选择其它处于就绪态中优先级最高的线程去调度
/**
* This function will let current thread yieldprocessor, and scheduler will
* choose a highest thread to run. After yieldprocessor, the current thread
* is still in READY state.
*
* @return RT_EOK
*/
rt_err_t rt_thread_yield(void)
下面两个函数的作用相同,都是延时一定的时钟节拍数,让我们当前的线程处于睡眠态(阻塞态)
/**
* This function will let current thread sleepfor some ticks.
*
* @param tick the sleep ticks
*
* @return RT_EOK
*/
rt_err_t rt_thread_sleep(rt_tick_ttick)
rt_err_t rt_thread_delay(rt_tick_ttick)
第三个函数,延时一段时间ms,让我们当前的线程处于睡眠态(阻塞态)
/**
* This function will let current thread delayfor some milliseconds.
*
* @param tick the delay time
*
* @return RT_EOK
*/
rt_err_t rt_thread_mdelay(rt_int32_tms)
该函数的作用是根据控制命令来控制某个线程的行为
参数1:线程句柄结构体指针
参数2:控制命令
参数3:控制命令所传入的参数;比如我们参数2传入的命令为改变线程优先级,我们就需要传入优先级数值(取地址传入),并强转为(void *)
/**
* This function will control thread behaviors according to control command.
*
* @param thread the specified thread to becontrolled
* @param cmd the control command, whichincludes
* RT_THREAD_CTRL_CHANGE_PRIORITY for changing priority level of thread;
* RT_THREAD_CTRL_STARTUP for starting a thread; == rt_thread_startup()
* RT_THREAD_CTRL_CLOSE for delete a thread; == rt_thread_delete()
* RT_THREAD_CTRL_BIND_CPU for bind the thread to a CPU.
* @param arg the argument of control command
*
* @return RT_EOK
*/
rt_err_trt_thread_control(rt_thread_t thread, int cmd, void *arg)
控制命令包括:
RT-Thread向用户提供了一种可以设置用户自定义的空闲线程,也称钩子线程。
一般钩子函数有两个作用:
1. 在线程进行调度切换时,会执行调度,我们可以设置一个调度器钩子,这样可以在线程切换时,做一些额外的事情,这个例子是在调度器钩子函数中打印线程间的切换信息。
2. 在主线程空闲的时候做一些空闲时监控的事情,指示灯闪烁等非紧急的任务。
在使用钩子函数时候必须保证空闲线程都不会被挂起,也就是说,rt_thread_delay()和re_sem_take() 等会导致线程挂起阻塞的函数都不能被使用在钩子函数中
传入的参数为一个函数指针,传入我们自己定义的钩子函数名
/**
* @ingroup Hook
* This function sets a hook function to idlethread loop. When the system performs
* idle loop, this hook function should beinvoked.
*
* @param hook the specified hook function
*
* @return RT_EOK: set OK
* -RT_EFULL: hook list is full
*
* @note the hook function must be simple andnever be blocked or suspend.
*/
rt_err_t rt_thread_idle_sethook(void(*hook)(void))
/**
* delete the idle hook on hook list
*
* @param hook the specified hook function
*
* @return RT_EOK: delete OK
* -RT_ENOSYS: hook was not found
*/
rt_err_t rt_thread_idle_delhook(void(*hook)(void))
注意:空闲线程是一个线程状态永远为就绪态的线程,因此设置的钩子函数必须保证空闲线程在任何时刻都不会处于挂起状态,例如 rt_thread_delay(),rt_sem_take() 等可能会导致线程挂起的函数都不能使用。
在整个系统的运行时,系统都处于线程运行、中断触发 - 响应中断、切换到其他线程,甚至是线程间的切换过程中,或者说系统的上下文切换是系统中最普遍的事件。有时用户可能会想知道在一个时刻发生了什么样的线程切换,可以通过调用下面的函数接口设置一个相应的钩子函数。在系统线程切换时,这个钩子函数将被调用
/**
* This function will set a hook function,which will be invoked when thread
* switch happens.
*
* @param hook the hook function
*/void
rt_scheduler_sethook(void (*hook)(struct rt_thread *from, struct rt_thread *to))
通过该函数我们可以知道线程的切换关系,通过我们定义一个回调函数,在回调函数中打印相关信息。当线程进行切换的时候,会自动调用我们设置的回调函数,并将原线程和目标线程的结构体句柄传入到我们的回调函数中,从而执行相关信息操作。
我们先将之前两个线程处理函数的运行打印都设置为5次,方便待会儿查看调度效果
编写自定义hook函数,在函数中打印调度关系
设置hook函数,传入我们自定义的hook函数
运行结果
从中可以看出线程的调度关系为:首先从用户主线程切换到th2线程,然后从th2线程切换到tshell线程,接着从tshell线程切换到idle线程,最终从idle线程切换到th1线程。之后就一直是th1->idle->th2的来回切换。但最终th1线程和th2线程执行完后,会调度idle线程,此时终端继续不显示
此时键盘输入,比如输入“l”,此时终端会显示从idle线程切换到tshell,然后再从tshell切换回idle
更多的hook函数请查看官方参考手册。