线程介绍
线程的概念
开发人员把复杂的任务进行功能分解,形成若干个不同功能的小任务,而不同功能的任务由运行于操作系统中的不同程序来完成,再由操作系统统一调度各个程序之间的运行。
这些运行在操作系统之上的程序单元就是线程
在RT-threa中线程是任务的载体,是RT-thread中的最小调度单元
线程的调度
对于单核系统,系统中有多个线程,每个线程的运行都需要占用CPU,但是只有一个CPU,如果想让这些线程有序的进行就需要一个管理者——调度器;
调度器主要负责线程调度,给不同的线程分配运行时间,使操作系统上的所有线程有序进行
线程调度就是按照特定的机制为系统上的所有线程分配CPU使用权
常见的线程调度包括两种:分时调度和抢占式调度
- 分时调度是所有线程轮流使用CP资源,平均分配每个线程占用CPU的时间
- 抢占式调度是根据线程的优先级,让优先级高的线程优先使用CPU,如果优先级相同,则会和分时调用一样轮流使用CPU
上下文切换
每个线程都有自己的上下文,上下文主要存储:代码、数据、堆栈、寄存器、程序计数器等
操作系统的调度器在进行线程调度时,会发生上下文的切换,从正在运行的线程的上下文,切换到另一个上下文。
线程运行是需要环境的,对于线程来说:运行环境就是CPU资源,包括运算单元、程序指针、堆栈指针以及各种通用寄存器。
CPU只有一套,线程运行的时候都会对CPU资源进行修改;当另一个线程运行时,它也想独自占用CPU资源,所以上一个线程必须让出CPU资源。
上一个线程为了防止下一个线程对CPU的资源进行修改,所以它在让出CPU之前会把当前资源使用情况记录下来(保护现场),方便下一次恢复CPU使用资源
当另一个线程得到CPU资源后,此时CPU的资源可能不适合它运行(上一个线程修改的),所以它需要把资源重新布置(恢复现场)
这种CPU资源从一个状态切换到另一个状态的过程就是上下文切换
线程的重要属性
线程栈
RT-thread每个线程都具有独立的栈,,当线程进行切换时,会把当前线程的上下文保存到线程栈中,当线程需要恢复运行时,再从线程栈中读取上下文信息,进行恢复
线程栈还用来存放函数的局部变脸,函数中的局部变量从线程栈中申请空间;函数中的局部变量初始化时从寄存器中分配,当这个函数被调用时,会把局部变量存放到线程栈中
线程的状态
线程分为5个状态:初始状态(RT_THREAD_INIT)、就绪状态(RT_THREAD_READY)、运行状态(RT_THREAD_RUNNING)、挂起状态(RT_THREAD_SUSPEND)、关闭状态(RT_THREAD_CLOSE)
五种状态可是使用函数进行转换,但是就绪到运行之间是由调度器执行的。
上面就是很清晰的转换分析图,网上摘得
线程优先级
线程优先级表示线程被调度的优先级。每个线程都具有优先级,线程越重要,分配的优先级越高
RT-thread最大支持256个优先级,0的优先级最高。Cortex-M系列,普遍采用32个优先级,最低的优先级默认分配给空闲线程使用。
当有比当前线程更高的优先级就绪时,当前线程会被立刻换出来,高优先级线程抢占处理器运行
时间片
每个线程都有时间片,表示每个线程可以占用CPU的运行时间,当时间片用完,线程会让出CPU,但时间片仅对优先级相同的就绪态线程有效。
时间片起到约束线程单次运行时长的作用,其单位是一个系统节拍
线程的入口函数
线程的入口函数是线程实现功能的函数,是线程第一个运行的函数,线程函数由用户设计:
- 无限循环
void rt_thread_entry(void *paramenter)
{
/* 等待事件发生 */
/* 对事件进行服务处理 */
}
- 顺序执行
void rt_thread_entry(void *paramenter)
{
/* 处理事务1 */
/* 处理事务1 */
/* 处理事务1 */
}
RT-thread线程管理
在RT-thread,用线程控制块来描述和管理一个线程,一个线程对应一个线程控制块
/**
* Thread structure
*/
struct rt_thread
{
/* rt object */
char name[RT_NAME_MAX]; /**< the name of thread */
rt_uint8_t type; /**< type of object */
rt_uint8_t flags; /**< thread's flags */
#ifdef RT_USING_MODULE
void *module_id; /**< id of application module */
#endif
rt_list_t list; /**< the object list */
rt_list_t tlist; /**< the thread list */
/* stack point and entry */
void *sp; /**< stack point */
void *entry; /**< entry */
void *parameter; /**< parameter */
void *stack_addr; /**< stack address */
rt_uint32_t stack_size; /**< stack size */
/* error code */
rt_err_t error; /**< error code */
rt_uint8_t stat; /**< thread status */
#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*/
/* priority */
rt_uint8_t current_priority; /**< current priority */
rt_uint8_t init_priority; /**< initialized priority */
#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; /**< thread's initialized tick */
rt_ubase_t remaining_tick; /**< remaining tick */
struct rt_timer thread_timer; /**< built-in thread timer */
void (*cleanup)(struct rt_thread *tid); /**< cleanup function when thread exit */
/* light weight process if present */
#ifdef RT_USING_LWP
void *lwp;
#endif
rt_ubase_t user_data; /**< private user data beyond this thread */
};
typedef struct rt_thread *rt_thread_t;
每个变量都有详细的注释,不做解释了,其中要明白,线程每次结束后空闲线程都会调用一次cleanup用来实现用户设定的清理现场等工作。
RT-thread提供了很多函数用于管理和控制线程
创建线程
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);
使用该函数时,系统会从动态堆内存中分配一个线程句柄以及指定的栈大小从动态堆内存中分配相应的空间。
删除线程
rt_err_t rt_thread_delete(rt_thread_t thread);
对于使用rt_thread_create创建出来的线程,可以使用该函数删除;线程对象将会被移除线程队列,并从内核对象管理器中删除,线程占用堆栈空间也会被释放。
初始化线程
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);
静态线程的线程句柄、线程栈由用户定义。静态线程是指线程控制块、线程运行栈,一般都是全局变量,在编译时被分配处理,内核不负责动态分配内存空间。
用户提供的线程栈首地址需要做系统对齐,可以在定义栈空间时使用ALIGN(RT_ALIGN_SIZE)宏进行对齐
脱离线程
rt_err_t rt_thread_detach(rt_thread_t thread);
脱离线程函数与rt_thread_create函数相对应,rt_thread_create删除对象时rt_thread_create创建的句柄,rt_thread_detach的操作对象是rt_thread_init初始化的线程控制块
启动线程
rt_err_t rt_thread_startup(rt_thread_t thread);
线程睡眠
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_err_t rt_thread_suspend(rt_thread_t thread);
只能自己使用挂起函数挂起自己,不可通过自己挂起别人;因为线程A不知道线程B是运行的什么程序,如果直接挂起,可能会引起一些连锁反应(影响实时性)
自己挂起自己后,需要立刻执行 rt_schedule() 函数进行手动切换上下文,我们不需要在程序中调用,知道就可以
当处于挂起状态的线程,等待时间超过设置的挂起时间,则不再等待资源,并返回就绪状态。
恢复线程
rt_err_t rt_thread_resume(rt_thread_t thread);
让挂起的线程重新进入就绪状态,并将线程放入就绪队列中;如果此时该线程优先级最高,则系统会调用上下文切换函数
获取当前运行的线程
rt_thread_t rt_thread_self(void);
获取当前执行的线程句柄
设置调度器钩子函数
void rt_scheduler_sethook(void (*hook)(rt_thread_t from, rt_thread_t to));
系统运行中,一直处于线程运行,线程切换,中断触发,中断相应中,所以说上下文切换是系统最普遍的事件。用户如果想知道此时刻,发生了什么样的线程切换,就可以设置一个钩子函数
钩子函数hook的声明如下
void hook(rt_thread_t from, rt_thread_t to);
使线程让出资源函数
rt_err_t rt_thread_yield(void);
当当前线程的时间片用完或者线程主动要求让出资源时,他将不再占用资源,调度器会在就绪队列中选择较高优先级的下一个线程执行。
调用这个函数后,这个线程仍然在就绪队列中;
与rt_scheduler()函数看似相同,但是有一定区别的:
调用rt_thread_yield()后,线程进入就绪队列,并放置到队列的末尾,优先让前面的线程先执行
调用rt_sheduler中,该线程会会到就绪队列,但并不会放置到队列末尾,而是尝试根据优先级插队
控制线程
rt_err_t rt_thread_control(rt_thread_t thread, int cmd, void *arg);
PS:在线程启动时,使用的优先级是设置为rt_thread_create或者rt_thread_init初始化时传入的初始化,所以一定要在线程启动后,通过此接口改变线程优先级才能生效