内核基础
- RT-thread的启动流程
从启动文件 startup_xx.S开始,进入到rtthread的入口函数
rtthread_startup
,在该入口函数之中完成RT-thread的系统初始化,初始化系统相关的硬件,以及内核对象,创建main线程,初始化定时器,调度器等。
-
RT-thread程序内存分布
- program size
- code:代码段
- RO-data:只读数据段,存放程序定义的常量
- RW-data:读写数据段,存放初始化值不为零的全局变量
- ZI-data:0数据段,存放未初始化的全局变量,及初始化为0的变量
- program size
-
RT-thread的自动初始化机制
初始化函数不需要被显示调用,只需要在函数定义处使用宏定义方式声明,就会在系统使用过程中被执行。
-
自动初始化接口
- INIT_BOARD_EXPORT(fn) 非常早期的初始化,此时调度器还未启动
- INIT_PREV_EXPORT(fn) 主要是用于纯软件的初始化、没有太多依赖的函数
- INIT_DEVICE_EXPORT(fn) 外设驱动初始化相关,比如网卡设备
- INIT_COMPONENT_EXPORT(fn) 组件初始化,比如文件系统或者 LWIP
- INIT_ENV_EXPORT(fn) 系统环境初始化,比如挂载文件系统
- INIT_APP_EXPORT(fn) 应用初始化,比如 GUI 应用
-
RT-thread内核对象模型
- 静态对象:使用时分配内存空间,不依赖于内存堆管理器
- 动态对象:系统自动分配内初空间,依赖于内存堆管理器
-
内核对象管理框架
对象容器给每类内核对象分配了一个链表,所有的内核对象都被链接到该链表上。
- 内核对象的派生和继承关系
提高系统的可重用性以及拓展性,提供了统一的对象操作方式
- RT-thread内核配置
rtconfig.h
该配置文件是系统配置之后自动生成的,无需手动添加,里面存在关于系统内核,ipc通信,内存管理,以及内核设备对象,自动初始化方式,finsh组件等有关宏定义。
线程管理
- 多线程的基本概念:
在需要完成一个大型的任务时,我们通常把这个任务分解成一个一个小任务,这些小任务也就是一个一个的线程,通过线程优先级,时间片来调度线程,不断切换,最大限度的利用好cpu的资源。在RT-thread中,线程的切换是抢占式的,线程的优先级越高抢占的能力越强,优先级高线程抢占优先级低的线程来执行,当优先级高的线程执行完毕,优先级低的线程再执行。在调度器切换线程时,线程挂起,线程的上下文保存在线程栈中,线程恢复运行时读取上下文。(线程控制块+线程栈 = 一个线程的运行环境)
-
线程相关属性
- 线程栈
- 线程状态
- 初始状态
- 就绪状态
- 运行状态
- 挂起状态
- 关闭状态
- 线程优先级:通过线程的优先级来抢占调度线程
- 时间片:在线程优先级相同的情况下,通过时间片来控制相同优先级线程执行时间
-
系统线程
- 空闲线程:优先级最低,不可以被挂起,回收被删除线程的资源
- 主线程:main线程,入口函数为
main_thread_entry
-
静态线程与动态线程
- 动态线程,系统自动从动态内存堆上分配栈空间与线程句柄
- 静态线程,用户分配栈空间和线程句柄
-
系统调度相关API
- 启动线程:
rt_thread_startup
- 获得当前线程:
rt_thread_self
- 使线程让出处理器资源:
rt_thread_yield
(相同优先级,让出时间片) - 使线程睡眠:
rt_thread_sleep
rt_thread_delay
rt_thread_mdelay
- 挂起和恢复:
rt_thread_suspend
rt_thread_resume
- 控制线程:
rt_thread_control
- 启动线程:
-
空闲钩子函数:
rt_thread_idle_sethook
rt_thread_idel_delhook
空闲线程是最小优先级,该线程永远不可被挂起,可以在空闲线程钩子里执行一些操作监测系统是否正常执行。空闲钩子函数里面测量系统的空闲时间。
- 调度器钩子:
rt_scheduler_sethook
调度器钩子函数可以查看当前线程的切换状态 。
RT-thread的时钟管理
- 时钟节拍:
1/RT_TICK_PER_SECOND
,使用于线程的延时,时间片轮转,定时器超时等,是一种周期性中断。
实现原理:当硬件定时器每促发一次,就调用一次systick_handler
,在该函数中调用rt_tick_increase()
对全局变量rt_tick
进行自加,检查时间片是否执行完毕,以及剩余时间片的多少,如果时间片执行完毕,将线程挂起,从定时器链表移除。周期性定时器会在它再次启动时加入定时器链表。
获取当前时钟节拍:rt_tick_get
-
定时器
- 硬件定时器
- 软件定时器
-
RT-thread定时器
- 单次触发定时器
- 周期触发定时器
-
定时器工作机制:
rt_tick
- 定时器链表
rt_timer_list
,根据当前的tick值来插入到定时器链表的相应位置
- 定时器的高精度延时
rt_hw_us_delay
线程间同步
- 临界区:
当多个线程同时访问一块共享内存块,这块区域就是临界区,需要通过线程间的IPC机制来访问,以保证临界区的数据的可靠性
- 线程同步核心思想
访问临界区的时候只允许一个线程或者一类线程访问
- 进入/退出临界区方式
- rt_hw_interrupt_disable(), rt_hw_interrupt_enable()
- rt_enter_critical(),rt_exit_critical()
信号量
- 信号量工作机制:
创建信号量时分配信号值,线程可以获取和释放它,当信号量可用值为0时,线程获取不到信号量,就会被挂起,直至其他线程释放信号量。
- 二值信号量(信号锁,互斥量)
- 计数信号量
- 信号量控制块
struct rt_semaphore
{
struct rt_ipc_object parent; /**< inherit from ipc_object */
rt_uint16_t value; /**< value of semaphore. */
rt_uint16_t reserved; /**< reserved field */
};
typedef struct rt_semaphore *rt_sem_t;
-
信号量相关接口:
-
信号量使用场合
- 线程同步
- 线程锁
- 中断与线程同步: 中断与线程间的互斥不能采用信号量(锁)的方式,而应采用开关中断的方式。
- 资源计数
-
信号量的相关工作流程
- 首先可以通过
rt_sem_create
创建一个动态信号量,先给信号量赋初值,可以有也可以没有,如果是用于事件的发生信号量的初始值为0,如果是共享资源,初始化的信号量值应该为一个可用值。可以使用rt_sem_take
获取信号量,当信号量的值大于零,获取到一次信号量,信号量的值就减1。当信号量的值为0,可以设置信号量的等待时间,永久等待或者等待一段时间,如果等待时间为零,直接返回,如果等待时间不为零,首先将线程挂起,之后重置线程定时器,执行线程调度,等待信号量的释放。可以使用rt_sem_release
来释放信号量,如果有线程因为无法获取信号量而挂起,这个线程将恢复,否则就将信号量的值加1,在该函数的最后执行线程调度。
- 首先可以通过
互斥量
- 工作机制:
拥有互斥量的线程拥有互斥量的所有权,互斥量支持递归可以防止优先级翻转,使用的是优先级继承算法,优先级继承是指,提高某个占有某种资源的低优先级线程的优先级,使之与所有等待该资源的线程中优先级最高的那个线程的优先级相等,然后执行,而当这个低优先级线程释放该资源时,优先级重新回到初始设定
在持有互斥量的过程中,不得再更改持有互斥量线程的优先级。
未被线程持有时是开锁,被线程持有时立刻转为闭锁
-
互斥量管理方式:
- 获取互斥量
rt_mutex_take
- 获取互斥量
如果互斥量没有被其他线程控制,那么申请该互斥量的线程将成功获得该互斥量。如果互斥量已经被当前线程线程控制,则该互斥量的持有计数加 1,当前线程也不会挂起等待。
在互斥量的初始化中,已经把互斥量的值初始化为1,当获取到互斥量的值时,互斥量的值减小1,当互斥量的值大于零,那么此时就形成互斥锁,准备接受互斥的线程,在互斥量释放之前,只有该互斥量的线程被线程锁锁定,不会被其他线程打断,保护临界区。同时,将线程当前的优先级赋值给互斥锁拥有者的线程优先级,防止优先级翻转的发生。
- 释放信号量 `rt_mutex_release`
使用该函数接口时,只有已经拥有互斥量控制权的线程才能释放它,每释放一次该互斥量,它的持有计数就减 1。当该互斥量的持有计数为零时(即持有线程已经释放所有的持有操作),它变为可用,等待在该信号量上的线程将被唤醒。如果线程的运行优先级被互斥量提升,那么当互斥量被释放后,线程恢复为持有互斥量前的优先级。
- 互斥锁
保护共享资源,当一个线程拥有互斥锁的时候可以保护共享资源不被其他线程破坏。
- 互斥量使用场合
- 线程多次持有互斥量的情况下。这样可以避免同一线程多次递归持有而造成死锁的问题。
- 可能会由于多线程同步而造成优先级翻转的情况。
事件集
- 事件集的工作机制:
事件集主要用于线程之间的同步,可以实现事件与线程的一对一或者一对多关联,即一个事件可以触发线程或者多个事件同时发送触发线程。每一个线程拥有一个32位的事件标志,也就是说一个线程可以对应32个事件的发生。事件只用于同步,不用于传输数据,事件发送多次只是相当于发送了一次。
- 事件集控制块
struct rt_event
{
struct rt_ipc_object parent; /**< inherit from ipc_object */
rt_uint32_t set; /**< event set */
};
typedef struct rt_event *rt_event_t;
-
事件集的管理方式
-
事件集的使用场合
事件集在一定程度上可以替代信号量使用,用于线程之间的通信,可以是一对一也可以是一对多,可以选择事件的接收方式 RT_EVENT_FLAG_OR ,RT_EVENT_FLAG_AND,RT_EVENT_FLAG_CLEAR等。一个事件集包含32个事件,特定线程只等待,接收它关注的事件。当有它们关注的事件发生,线程将被唤醒执行相应的操作。
- 事件集的创建
首先创建一个时间集对象,之后给这个对象赋初值,初始化ipc对象,将时间集的初值设置为0
- 事件集的删除
首先恢复所有因为事件集而挂起的线程,之后删除事件集对象
- 事件的发送
首先关闭中断,将需要发送的事件赋值给对应的事件集对象,获取线程,检查事件集标志位,然后遍历等待在 event 事件集对象上的等待线程链表,判断是否有线程的事件激活要求与当前 event 对象事件标志值匹配,如果有,则唤醒该线程。下表描述了该函数的输入参数与返回值:
- 事件的接收
首先根据事件的标志,确定事件是否正常发生,如果事件符合标志规则,事件已经正常发生,则根据参数是否设置清除标志来重置相应的事件标志位,当设置的超时时间为0,直接返回错误,否则继续等待,将线程挂起,开始计算时间,执行线程调度,直到事件到达。
线程间通信
邮箱
- 邮箱的基本概念:
邮箱是操作系统中通信的一种基本方法,可以在线程与线程之间,中断与线程之间进行消息的传递,,特点是开销比较低,效率较高,邮箱中的每一封邮件只能容纳4字节的内容,一封邮件恰好可以容纳一个指针。
- 邮箱的工作机制:
创建邮箱对象首先会创建一个邮箱对象的控制块,然后给邮箱分配一块内存空间用来存放邮件,这块内存的大小等于邮件大小(4字节)与邮箱容量的乘积。当一个线程向邮箱发送邮件时,如果邮箱没满,将把邮件复制到邮箱中。如果邮箱已经满了,发送线程可以设置超时时间,选择等待挂起或直接返回 - RT_EFULL。如果发送线程选择挂起等待,那么当邮箱中的邮件被收取而空出空间来时,等待挂起的发送线程将被唤醒继续发送。
当一个线程从邮箱中接收邮件时,如果邮箱是空的,接收线程可以选择是否等待挂起直到收到新的邮件而唤醒,或可以设置超时时间。当达到设置的超时时间,邮箱依然未收到邮件时,这个选择超时等待的线程将被唤醒并返回 - RT_ETIMEOUT。如果邮箱中存在邮件,那么接收线程将复制邮箱中的 4 个字节邮件到接收缓存中。
- 邮箱控制块
struct rt_messagequeue
{
struct rt_ipc_object parent; /**< inherit from ipc_object */
void *msg_pool; /**< start address of message queue */
rt_uint16_t msg_size; /**< message size of each message */
rt_uint16_t max_msgs; /**< max number of messages */
rt_uint16_t entry; /**< index of messages in the queue */
void *msg_queue_head; /**< list head */
void *msg_queue_tail; /**< list tail */
void *msg_queue_free; /**< pointer indicated the free node of queue */
rt_list_t suspend_sender_thread; /**< sender thread suspended on this message queue */
};
-
邮箱的管理方式
-
邮箱的使用场合:
当需要在线程间传递比较大的消息时,可以把指向一个缓冲区的指针作为邮件发送到邮箱中,即邮箱也可以传递指针。可以创建一个消息结构体,将数据的指针和数据块长度的变量放到该结构体中。
信号
- 信号的概念:
信号(又称为软中断信号),在软件层次上是对中断机制的一种模拟,在原理上,一个线程收到一个信号与处理器收到一个中断请求可以说是类似的。
- 信号的工作机制:
线程之间不必通过任何操作来等待信号的到达,类似于中断,线程之间可以互相发送软中断信号,应用程序可以使用的信号为SIGUSER1(10)和SIGUSER2(12)
-
线程对信号的处理方式:
- 对于不同的信号,指定信号处理函数
- 忽略信号
- 对信号的处理保留默认值
-
信号的管理方式:
中断
- 中断的分类:
- 同步中断
- 异步中断
- 中断的基本概念:
当正在执行某一正常事件,此时发生了另外一个需要紧急处理的事件,系统先暂停正在执行的事件,转而执行更紧急的事件,当紧急事件处理完毕,再恢复原来事件的执行。
-
RT-thread中断工作机制
- 中断向量表
- 中断处理过程
1. 中断前导程序:保护cpu中断现场,通知内核进入中断
2. 用户中断服务程序
3. 中断后续程序:通知内核离开中断,恢复中断cpu上下文 - 中断延迟:
识别中断时间 + [等待中断打开时间] + [关闭中断时间] - 中断嵌套:中断执行过程中被高优先级中断打断,转而执行高优先级中断,执行完毕后返回执行被打断的中断。
- 中断栈:保存中断上下文以供恢复现场
-
RT-thread中断管理接口:
-
全局中断开关(中断锁):
rt_hw_interrupt_disable
rt_hw_interrupt_enable
是禁止多线程访问临界区的最简单的一种方式,关中断保证当前线程不会被其他事件打断.
由于关闭全局中断会导致整个系统不能响应中断,所以在使用关闭全局中断做为互斥访问临界区的手段时,必须需要保证关闭全局中断的时间非常短,例如运行数条机器指令的时间
- 中断通知
rt_interrupt_enter
rt_interrupt_leave
rt_interrupt_get_nest