RTT学习

发布于:2024-04-23 ⋅ 阅读:(19) ⋅ 点赞:(0)

线程管理方式

可以使用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);

调用这个函数时,系统会从动态堆内存中分配一个线程句柄以及按照参数中指定的栈大小从动态堆内存中分配相应的空间。分配出来的栈空间是按照 rtconfig.h 中配置的 RT_ALIGN_SIZE 方式对齐。

rt_err_t rt_thread_delete(rt_thread_t thread);

调用该函数后,线程对象将会被移出线程队列并且从内核对象管理器中删除,线程占用的堆栈空间也会被释放,收回的空间将重新用于其他的内存分配。实际上,用 rt_thread_delete() 函数删除线程接口,仅仅是把相应的线程状态更改为 RT_THREAD_CLOSE 状态,然后放入到 rt_thread_defunct 队列中;而真正的删除动作(释放线程控制块和释放线程栈)需要到下一次执行空闲线程时,由空闲线程完成最后的线程删除动作。

初始化和脱离线程

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

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

启动线程

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

rt_err_t rt_thread_startup(rt_thread_t thread);

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

获得当前线程

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

rt_thread_t rt_thread_self(void);

使线程让出处理器资源

当有线程的时间片用完或者该线程主动要求让出处理器资源时,它将不再占有处理器,调度器会选择相同优先级的下一个线程执行。
线程调用这个接口后,这个线程仍然在就绪队列中。

rt_err_t rt_thread_yield(void);

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

rt_thread_yield() 函数和 rt_schedule() 函数比较相像,但在有相同优先级的其他就绪态线程存在时,系统的行为却完全不一样。执行 rt_thread_yield() 函数后,当前线程被换出,相同优先级的下一个就绪线程被执行。而执行rt_schedule()函数后,当前线程并不一定被换出,即使被换出,也不会放到就绪线程链表的尾部,而是在系统中选取就绪的优先级最高的线程执行(如果系统中没有比当前线程优先级更高的线程存在,那么执行完 rt_schedule() 函数后,系统将继续执行当前线程)。

使线程睡眠

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

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-Thread对此函数有严格的使用限制:该函数只能使用来挂起当前线程(即自己挂起自己),不可以在线程A中尝试挂起线程B。
而且在挂起线程自己后,需要立刻调用rt_schedule()函数进行手动的线程上下文切换。
这是因为A线程在尝试挂起B线程时,A线程并不清楚B线程正在运行什么程序,一旦B线程正在使用例如互斥量、信号量等影响、阻塞其它线程(如C线程)的内核对象,如果此时其他线程也在等待这个内核对象,那么A线程尝试挂起B线程的操作将会引发其他线程(如C线程)的饥饿,严重危及系统的实时性。

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

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

指示控制命令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_idle_sethook(void (*hook)(void));
rt_err_t rt_thread_idle_delhook(void (*hook)(void));

设置调度器钩子

在整个系统的运行时,系统都处于线程运行、中断触发-响应中断、切换到其它线程,甚至是线程间的切换过程中,可以说系统的上下文切换是系统中最普遍的事件。

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

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

时钟节拍

任何操作系统都需要提供一个时钟节拍,以供系统处理所有和时间有关的事件,如线程的延时、线程的时间片轮转调度以及定时器超时等。

时钟节拍是特定的周期性中断,这个中断可以看做是系统心跳,中断之间的时间间隔取决于不同的应用,一般是1ms-100ms,时钟节拍率越快,系统的实时响应越快,但是系统的额外开销就越大,从系统启动开始计数的时钟节拍数称为系统时间。

时钟节拍的长度可以根据RT_TICK_PER_SECOND的定义来调整,等于1/RT_TICK_PERSECOND秒。

时钟节拍的实现方式

时钟节拍由配置为中断触发模式的硬件定时器产生,当中断到来时,将调用一次:void rt_tick_uncrease(void),通知操作系统已经过去一个系统时钟;

void SysTick_Handler(void){
	rt_interrupt_enter();
	rt_tick_increase();
	rt_interrupt_leave();
}

在中断函数中调用rt_tick_increase()对全局变量rt_tick进行自加。

void rt_tick_increase(void){
	struct rt_thread *thread;
	++rt_tick;
	thread = rt_thread_self();

	--thread->remaining_tick;
	if(thread->remaining_tick == 0){
		thread->remaining_tick = thread->init_tick;
		rt_thread_yield();
	}
	//检查定时器
	rt_timer_check();
}

可以看到全局变量 rt_tick 在每经过一个时钟节拍时,值就会加 1,rt_tick 的值表示了系统从启动开始总共经过的时钟节拍数,即系统时间。此外,每经过一个时钟节拍时,都会检查当前线程的时间片是否用完,以及是否有定时器超时。

中断中的rt_timer_check()用于检查系统硬件定时器链表,如果有定时器超时,将调用相应的超时函数。且所有定时器在定时超时后都会从定时器链表中被移除,而周期性定时器会在它再次启动时被加入定时器链表。

获取时钟节拍

由于全局变量rt_tick在每经过一个时钟节拍时,值就会加1,通过调用rt_tick_get会返回当前rt_tick的值,即可以获取当前的时钟节拍值。此接口可用于记录系统的运行时间长短,或者测量某任务运行的运行的时间。

rt_tick_t rt_tick_get(void);

rt_tick:当前时钟节拍值

定时器管理

定时器,是指从指定的时刻开始,经过一定的指定时间后触发一个事件,例如定个时间提醒第二天能够按时起床。定时器有硬件定时器和软件定时器之分:

  1. 硬件定时器是芯片本身提供的定时功能。一般是由外部晶振提供给芯片输入时钟,芯片向软件模块提供一组配置寄存器,接受控制输入,到达设定时间值后芯片中断控制器产生时钟中断。硬件定时器的精度一般很高,可以达到纳秒级别,并且是中断触发方式。
  2. 软件定时器是由操作系统提供的一类系统接口,它构建在硬件定时器基础之上,使系统能够提供不受数目限制的定时器服务。

RT-Thread操作系统提供软件实现的定时器,以时钟节拍(OS Tick)的时间长度为单位,即定时数值必须是OS Tick的整数倍,例如一个OS Tick是10ms,那么上层软件定时器只能是10ms,20ms,100ms等,RTT的定时器基于系统的节拍,提供了基于节拍整数倍的定时能力。

定时器介绍

RTT的定时器提供两类定时器机制:第一类是单次触发定时器,这类定时器在启动后只会触发一次定时器事件,然后定时器自动停止。
第二类是周期触发定时器,这类定时器会周期性地触发定时器事件,直到用户手动的停止,否则将永远持续执行下去。

另外,根据超时函数执行时所处的上下文环境,RTT的定时器分为HARD_TIMER模式与SOFT_TIMER模式。

在这里插入图片描述
HARD_TIMER
HARD_TIMER模式的定时器超时函数在中断上下文环境中执行,可以在初始化/创建定时器使用参数RT_TIMER_FLAG_HARD_TIMER来指定。

在中断上下文环境中执行时,对于超时函数的要求与中断服务例程的要求相同:执行时间应该尽量短,执行时不应导致当前上下文挂起、等待。
例如在中断上下文中执行的超时函数它不应该试图去申请动态内存、释放动态内存等。

RTT定时器默认的方式是HARD_TIMER模式,即定时器超时后,超时函数在系统时钟中断的上下文环境中运行。
在中断上下文中的执行方式决定了定时器的超时函数不应该调用任何会让当前上下文挂起的系统函数;也不能够执行非常长的时间,否则会导致其它中断的响应时间加长或抢占了其它线程执行的时间。

SOFT_TIMER模式
SOFT_TIMER模式可配置,通过宏定义RT_USING_TIMER_SOFT来决定是否启动该模式。
该模式被启用后,系统会在初始化时创建一个timer线程,然后SOFT_TIMER模式的定时器超时函数都会在timer线程的上下文环境中执行。
可以在初始化/创建定时器时使用参数RT_TIMER_FLAG_SOFT_TIMER来指定设置SOFT_TIMER模式。

定时器工作机制

定时器模块中维护着两个重要的全局变量:

  1. 当前系统经过的tick时间rt_tick(当硬件定时器中断来临时,它将加1);
  2. 定时器链表rt_timer_list。系统新创建并激活的定时器都会按照以超时时间排序的方式插入到rt_timer_list链表中。

如下图所示,系统当前 tick 值为 20,在当前系统中已经创建并启动了三个定时器,分别是定时时间为 50 个 tick 的 Timer1、100 个 tick 的 Timer2 和 500 个 tick 的 Timer3,这三个定时器分别加上系统当前时间rt_tick=20,从小到大排序链接在rt_timer_list链表中,形成如图所示的定时器链表结构。

在这里插入图片描述
而rt_tick随着硬件定时器的触发一直在增长(每一次硬件定时器中断来临,rt_tick变量会加1),50个tick以后,rt_tick从20增长到70,与Timer1的timeout值相等,这时会触发与Timer1定时器相关联的超时函数,同时将Timer1从rt_timer_list链表上删除。同理,100 个 tick 和 500 个 tick 过去后,与 Timer2 和 Timer3 定时器相关联的超时函数会被触发,接着将 Timer2 和 Timer3 定时器从 rt_timer_list 链表中删除。

定时器控制块

在RTT操作系统中,定时器控制块由结构体struct rt_timer定义并形成定时器内核对象,再链接到内核对象容器中进行管理。
它是操作系统用于管理定时器的一个数据结构,会存储定时器的一些信息,例如初始节拍数,超时时的节拍数…

struct rt_timer{
	struct rt_object parent;
	rt_list_t row[RT_TIMER_SKIP_LIST_LEVEL]; /*定时器链表节点*/

	void (*timeout_func)(void *parameter);  /*定时器超时调用的函数*/
	void      *parameter;                         /* 超时函数的参数 */
	rt_tick_t init_tick; /*定时器初始超时节拍数*/
	rt_tick_t timeout_tick; /*定时器实际超时节拍数*/
};
typedef struct rt_timer *rt_timer_t;

定时器跳表算法(Skip List)

在前面介绍定时器的工作方式的时候说过,系统新创建并激活的定时器都会按照以超时时间排序的方式插入到rt_timer_list链表中,也就是说rt_timer_list链表是一个有序链表,RTT中使用了跳表算法来加快搜索链元素的速度。

跳表是一种基于并联链表的数据结构,实现简单,插入、删除、查找的时间复杂度均为O(log n)。
跳表是链表的一种,但它在链表的基础上增加了“跳跃”功能,正是这个功能,使得在查找元素时,跳表能够提供O(log n)的时间复杂度。

在这里插入图片描述
一个有序的链表,从该有序链表中搜索元素{13,39},需要比较的次数分别为{3,5},总共比较次数3+5=8次。

使用跳表算法后可以采用类似二叉搜索树的方法,把一些节点提取出来作为索引,得到如下图所示的结构:
在这里插入图片描述
在这个结构里把{3,18,77}提取出来作为一级索引,这样搜索的时候就可以减少比较次数了,比如在搜索39时仅比较了3次(通过比较3,18,39)。
当然还可以从一级索引提取一些元素出来,作为二级索引,这样更能加快元素搜索。

在这里插入图片描述
所以,定时器跳表可以通过上层的索引,在搜索的时候就减少比较次数,提升查找的效率,这是一种通过“空间换取时间”的算法,在RTT中通过宏定义RT_TIMER_SKIP_LIST_LEVEL来配置跳表的层数,默认为1,表示采用一级有序链表图的有序链表算法,每增加一,表示在原链表基础上增加一级索引。