rtt学习
内核
线程调度:除了中断处理函数、调度器上锁部分的代码和禁止中断的代码是不可抢占的之外,系统的其他部分都是可以抢占的。有256个优先级,0最高,STM32默认36个
时钟管理:分为单次和周期,需要设置定时器回调函数
线程同步:使用信号量、互斥量、事件集实现,简单来说是获取与释放,学长说一般信号量能够实现互斥量的工作
线程通信:邮箱和消息队列。邮箱中一封邮件的长度固定为 4 字节大小;消息队列能够接收不固定长度的消息,并把消息缓存在自己的内存空间中。
内存管理:静态内存池管理及动态内存堆管理。静态内存池系统对内存块分配的时间是恒定的。如果为空则会挂起或阻塞线程等待内存释放
线程管理
线程运行时,会认为自己独占CPU
功能特点
分为系统线程(由RTT内核创建)和用户线程(由应用程序创建)
线程属性:线程控制块、线程栈、入口函数
特点:抢占式,优先查找优先级最高的线程
如果是中断让高优先级的线程满足条件,会在中断函数完成之后挂起中断线程,运行优先级高的线程
切换线程时会保留当前线程的上下文
工作机制
线程控制块
/* 线程控制块 */
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; /* 用户数据 */
};
线程属性
线程栈

线程状态

线程优先级
RTT最大支持0~255,0为最高,最低优先级一般分配给空闲进程
时间片
对优先级相同的线程采用时间片的约束方式,用来约束线程单次运行时长
果就绪列表中有相同优先级的线程,则这两个线程会按照时间片长短被轮番调度
比如OS tick精度为1ms,那么五个时间片就是运行五个OS Tick。
如下图,设计了 thread1、thread2 两个相同优先级的线程,thread1 时间片为 10, thread2 时间片为 5

在两个线程都创建好之后,先运行时间片长的thread1,经过10个tick后运行时间片短的thread2,经过五个时间片后再运行thread1,如此循环。
入口函数
- 无限循环模式
比如hero线程的例程里

这样的线程必须要有让出CPU使用权的动作,比如调用延时函数或主动挂起。
用无限循环可以保证线程一直被循环调度永不删除
- 有限次模式

例如上面的代码,一旦执行完毕线程就会被删除
线程错误码

线程状态切换
1.线程通过函数rt_thread_create/init进入初始状态RT_THREAD_INIT

2.初始状态的线程调用rt_thread_startup进入就绪状态RT_THREAD_READY

3.就绪状态的线程被调度后进入运行状态RT_THREAD_RUNNING
4.当处于运行状态的线程调用 rt_thread_delay(),rt_sem_take(),rt_mutex_take(),rt_mb_recv() 等函数或者获取不到资源时,进入到挂起状态RT_THREAD_SUSPEND

5.挂起状态的线程,如果超时仍然没有获得资源或者被释放了资源,就会进入就绪状态RT_THREAD_READY
如果调用rt_thread_delete/detach()函数,将进入到关闭状态RT_THREAD_CLOSE
6.运行状态的线程,如果运行完毕,会在线程最后执行rt_thread_exit()进入关闭状态
系统线程
空闲线程
最低优先级,永远为就绪态,永远为死循环不能被挂起
也负责线程删除和回收
主线程
系统启动时创建,用于进入主函数
入口函数为main_thread_entry
调试的时候会发现代码一开始是在$$Sub$$main()这里的
线程管理方式

创建


例子:

删除


这个函数会先把要删除的线程改成close状态,放入僵尸队列,等待下一次空闲线程运行再删除
初始化


和create比多了一个线程句柄
脱离


注意创建和删除,初始化和脱离是对应的
创建和初始化的区别:create是创建动态线程,而init是初始化静态线程。
启动


一般放在初始化或者创建线程之后,把线程的状态改为就绪状态。如果新启动的线程优先级更高会立刻切换到他。
我之前就是因为没加这个函数导致线程进不去,一定要加
获得当前线程

让出处理器资源

当前线程时间片用完或者主动让出。
线程睡眠
·
用处都是让线程挂起一段时间

sleep和delay都是以OS TICK为单位
但是mdelay是以ms为单位
挂起


该函数只能用来挂起自己,并且需要立刻使用rt_schedule()切换上下文
恢复

控制



钩子函数
我大体的理解就是把写好的函数看成一个个钩子,通过调用函数指针的方式让指针指向这个函数,把这个过程形象的比作挂钩子的过程
设置和删除空闲钩子
空闲钩子就是在空闲进程中进行的钩子函数,比如可以设置系统指示灯,直观地表示空闲进程正在进行



设置调度器钩子
调度器钩子会在每次线程切换的时候运行


hook的声明如下:


不允许调用系统API,不允许挂起上下文
一些具体使用可以看这篇语雀
线程间同步的信号量
我对同步的理解:控制各个线程按照预定的先后次序进行。
比如我要对一个地址进行修改和读取,如果没有同步可能会出现修改和读取同时进行的情况。
临界区:多个线程同时操作或者访问的同一块区域(代码)。
线程间同步的核心思想是:访问临界区的时候只允许一个(或者一类)线程运行
信号量

每个信号量都包含信号量值和线程等待队列,信号量值对应可使用的实例(资源)数目。当信号量实例为0时,再申请该信号量的线程就会被挂起在等待队列上。
信号量控制块
struct rt_semaphore
{
struct rt_ipc_object parent; /* 继承自 ipc_object 类 */
rt_uint16_t value; /* 信号量的值 */
};
/* rt_sem_t 是指向 semaphore 结构体的指针类型 */
typedef struct rt_semaphore* rt_sem_t;
rt_sem_t表示函数句柄,是一个指向信号量控制块的指针
管理方式

创建
rt_sem_t rt_sem_create(const char *name,
rt_uint32_t value,
rt_uint8_t flag);

关于FIFO和PRIO的区别:
FIFO:先入先出,等待队列中先进入的线程优先获得等待的信号量
PRIO:优先级等待,等待队列按照优先级排序,优先级高的先获得等待的信号量
删除
rt_err_t rt_sem_delete(rt_sem_t sem);
如果删除时有线程正在等待信号量,则会先唤醒线程在进行信号量的删除
初始化
对于静态信号量,使用init进行初始化
rt_err_t rt_sem_init(rt_sem_t sem,
const char *name,
rt_uint32_t value,
rt_uint8_t flag)

例如:

脱离
rt_err_t rt_sem_detach(rt_sem_t sem);
和初始化对应,不要搞错了
获取
信号量值大于0时,线程将获得信号量,对应信号量值减1
rt_err_t rt_sem_take (rt_sem_t sem, rt_int32_t time);

如果信号量值为0,则会根据参数time直接返回,挂起一段时间或者永久等待,直到该信号量被释放。
如果在time的范围内仍然没有用得到信号量,就会超时返回。
例如:

释放
线程间通信
邮箱
邮箱可以用作线程间通信,每一封邮件可以容纳4字节内容,各个线程则可以读取邮箱里的邮件来进行处理

线程发送邮件到邮箱时,如果邮箱已满,就可以设置挂起或者直接返回,接收同理。

浙公网安备 33010602011771号