Toggle navigation
首页
问答
文章
积分商城
专家
专区
更多专区...
文档中心
返回主站
搜索
提问
会员
中心
登录
注册
rtthread文档
学习笔记
新手学习
从非实时操作系统视角看 RT-Thread
发布于 2023-04-27 00:23:05 浏览:899
订阅该版
[tocm] # 从非实时操作系统视角看 RT-Thread >本文主要在学习官方文档过程的一个学习和记录。 > >从一个只接触过 Linux,没有接触过 RTOS 的人的视角看 RT-Thread。 # 前言 先贴一段维基百科对实时操作系统的定义: >**实时操作系统**(Real-time operating system, RTOS),又称**即时操作系统**,它会按照排序执行、管理系统资源,并为开发应用程序提供一致的基础。 > >实时操作系统与一般的[操作系统](https://zh.wikipedia.org/wiki/%E6%93%8D%E4%BD%9C%E7%B3%BB%E7%BB%9F "操作系统")相比,最大的特色就是“[实时性](https://zh.wikipedia.org/wiki/%E5%AE%9E%E6%97%B6%E8%AE%A1%E7%AE%97 "实时计算")”^[[1]](https://zh.wikipedia.org/zh-sg/%E5%AE%9E%E6%97%B6%E6%93%8D%E4%BD%9C%E7%B3%BB%E7%BB%9F#cite_note-1)^,如果有一个任务需要执行,实时操作系统会马上(在较短时间内)执行该任务,不会有较长的延时。这种特性保证了各个任务的及时执行。 > >设计实时操作系统的首要目标不是高的[吞吐量](https://zh.wikipedia.org/wiki/%E5%90%9E%E5%90%90%E9%87%8F "吞吐量"),而是保证任务在特定时间内完成,因此衡量一个实时操作系统坚固性的重要指标,是系统从接收一个任务,到完成该任务所需的时间,其时间的变化称为[抖动](https://zh.wikipedia.org/wiki/%E5%AE%9E%E6%97%B6%E8%AE%A1%E7%AE%97 "实时计算")。可以依抖动将实时操作系统分为两种:硬实时操作系统及软实时操作系统,硬实时操作系统比软实时操作系统有更少的抖动: > > - 硬实时操作系统**必须**使任务在确定的时间内完成。 > - 软实时操作系统能让**绝大多数**任务在确定时间内完成。^[[2]](https://zh.wikipedia.org/zh-sg/%E5%AE%9E%E6%97%B6%E6%93%8D%E4%BD%9C%E7%B3%BB%E7%BB%9F#cite_note-2)^ > > 实时操作系统与一般的操作系统有着不同的[调度](https://zh.wikipedia.org/wiki/%E6%8E%92%E7%A8%8B "调度")算法。普通的操作系统的调度器对于线程优先级等方面的处理更加灵活;而实时操作系统追求最小的[中断延时](https://zh.wikipedia.org/w/index.php?title=%E4%B8%AD%E6%96%AD%E5%BB%B6%E6%97%B6&action=edit&redlink=1)和[线程切换延时](https://zh.wikipedia.org/wiki/%E4%B8%8A%E4%B8%8B%E6%96%87%E4%BA%A4%E6%8F%9B "上下文切换")。^[[3]](https://zh.wikipedia.org/zh-sg/%E5%AE%9E%E6%97%B6%E6%93%8D%E4%BD%9C%E7%B3%BB%E7%BB%9F#cite_note-3)^ 关于实时操作系统的定义,无外乎最关注的都是实时性,甚至为了实时性可以牺牲吞吐量。除此之外,实时操作系统经常是在性能差、资源小的应用场景中运行的,所以其内核也十分精简。而在我看来,实时操作系统和通用操作系统一大区别在于:实时操作系统的用户只有一方程序员,而通用操作系统除了普通用户之外还有多方程序员 所以,很多通用操作系统当中的考虑在实时操作系统中并不存在,例如这个API交给用户是否安全和用户对操作系统的控制权限等等。所以在我看来,实时操作系统更像是: >一个管理着硬件资源供程序员更好编程的 Framework。 所以本篇文章将从一个只接触过 Linux,没有接触过 RTOS 的人的视角看 RT-Thread: >1. **实时的线程调度** >2. **实时的消息、事件处理机制和提供内核级的优先级翻转处理方式** >3. **避免提供实时性不确定的API** >4. **减少粗粒度的锁和长期关中断的使用** >5. **针对实时性设计的SMP** RT-Thread 的核心是通过`libcpu/BSP`适配`ARM/RISC-V/MIPS`等不同架构和开发板,如启动过程、初始化过程、引脚复用等等以及通过线程管理、中断管理和内存管理等模块完成整个操作系统的核心功能。而上层的组件基于内核部分来完成相应的实现,其中最重要的就是设备驱动。如下图。 ![image20230426171538dvm3ggjpng 2372×1244 b3logfilecom](https://b3logfile.com/siyuan/1611023828328/assets/image-20230426171538-dvm3ggj.png) # 线程管理 > **实时的线程调度** ## 基础概念 >RT-Thread 的线程管理 在 RT-Thread 中,最小的程序实体就是线程,线程是实现任务的载体,它是 RT-Thread 中最基本的调度单位,它描述了一个任务执行的运行环境。这和通用操作系统中有很大不同,包括在内存管理方式上。 每个线程最重要的属性就是线程控制块、线程栈、入口函数等。线程的许多属性都会保存在线程控制块中,而所有线程都会保存在内核对象容器中进行统一的管理。 线程控制块的详细定义如下: ```c /* 线程控制块 */ 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; /* 用户数据 */ }; ``` 1. **线程栈**:RT-Thread 没一个线程都具有独立的栈,当进行线程切换时,会将当前线程的上下文存在栈中,当线程要恢复运行时,再从栈中读取上下文信息,进行恢复。 2. **线程状态**:线程运行的过程中,同一时间内只允许一个线程在处理器中运行,从运行的过程上划分,线程有多种不同的运行状态,如初始状态、挂起状态、就绪状态等。 3. **线程优先级**:RT-Thread 线程的优先级是表示线程被调度的优先程度。每个线程都具有优先级,线程越重要,赋予的优先级就应越高,线程被调度的可能才会越大。 4. **时间片**:时间片起到约束线程单次运行时长的作用,其单位是一个系统节拍(`OS Tick`)。 5. **线程入口函数**:线程实现预期功能的函数。 根据线程控制块,RT-Thread 提供了一系列操作的 `API` 用来管理线程,主要有:(*详细见*[线程管理 (rt-thread.org)](https://www.rt-thread.org/document/site/#/rt-thread-version/rt-thread-standard/programming-manual/thread/thread)) - 创建和初始化线程(*初始化和脱离线程*) - 启动和获得线程 - 线程调度类 - 主动让出`yield`CPU - 睡眠 - 挂起和恢复 ## **实时的线程调度** 时间片轮转法是指将 `CPU` 时间划分成若干个时间片,每个进程或线程占用一个时间片,当时间片用完时,将CPU分配给下一个进程或线程。如果一个进程或线程的时间片用完了,但是它还没有完成,那么它就被放到就绪队列的末尾等待下一轮调度。 Linux 进程和线程的调度方式都是采用时间片轮转法`(Round-Robin)`。RT-Thread 也基于时间片,但是优先级的重要程度高于时间片,因为时间片只在同一优先级的任务当中进行轮转。 在 Linux 中,进程和线程都被看作是任务`(Task)`,任务由一个结构体`(task_struct)`来表示,其中包含了进程或线程的所有信息,包括状态、优先级、CPU 时间等。任务的调度是由调度器来完成的,Linux 中有多种调度器,如 `CFS(Completely Fair Scheduler)`、`O(1) Scheduler`等。CFS是 Linux 内核中默认的调度器,它的核心思想在于所有运行的进程或线程都有相同的权重,并且系统将 CPU 时间按比例分配(*虚拟运行时间*)给它们,以实现公平调度。O(1) Scheduler则是一种基于时间片的调度器,它能够快速地找到就绪队列中优先级最高的任务进行调度。所以在 Linux 当中是尽力避免进程饥饿的情况,而这在 RT-Thread中有很大的不同。 RT-Thread 中的线程调度算法是基于优先级的全抢占式多线程调度算法,即在系统中除了中断处理函数、调度器上锁部分的代码和禁止中断的代码是不可抢占的之外,系统的其他部分都是可以抢占的,包括线程调度器自身。 RT-Thread 的线程调度器的主要的工作就是从就绪线程列表中查找最高优先级线程,保证最高优先级的线程能够被运行,最高优先级的任务一旦就绪,总能得到 `CPU` 的使用权。此时的情况可能是: >当一个运行着的线程使一个比它优先级高的线程满足运行条件(*获取到正在等待的资源*),当前线程的 CPU 使用权就被剥夺了,或者说被让出了,高优先级的线程立刻得到了 CPU 的使用权。如果是中断服务程序使一个高优先级的线程满足运行条件,中断完成时,被中断的线程挂起,优先级高的线程开始运行。 但是在 RT-Thread 中不允许一个线程完全的不陷入阻塞状态中,因为需要一个线程去回收被删除线程的资源(*但是此时当 RT-Thread 中任务足够多时,是不是有可能发送空闲线程永远无法执行的问题?*)。此外,相同优先级的线程间采用时间片的轮转调度算法进行调度,使每个线程运行相应时间。 --- # 线程间的同步和通信 >**实时的消息、事件处理机制和提供内核级的优先级翻转处理方式** ## 基础概念 >线程间的同步与通信 ### 线程间的同步 在多线程程序中,为了保证多个线程之间的正确协作,需要对它们之间的访问和操作进行协调和同步,以避免出现不可预料的结果。在多线程程序中,每个线程都有自己的执行序列,如果不进行同步,就有可能出现两个或多个线程在执行过程中互相干扰,从而导致程序出现错误或崩溃。 在 RT-Thread 当中除了关中断的方式(*`rt_hw_interrupt_disable()`*)之外,还提供三种方式进行线程间的同步:信号量、互斥量、消息队列 #### 信号量 信号量`(Semaphore)`是一种用于进程或线程之间同步和互斥的机制。它可以用来控制对共享资源的访问,避免多个进程或线程同时访问共享资源而产生的竞态条件。 当一个进程或线程需要访问共享资源时,它会尝试获取信号量。如果信号量的值大于0,则表示有空闲资源可以使用,该进程或线程可以继续执行。同时,该进程或线程会将信号量的值减1,表示已经使用了一个资源。 在 RT-Thread 中,操作系统用信号量控制块管理信号量,由结构体 `struct rt_semaphore` 表示。另外一种 C 表达方式 `rt_sem_t`,表示的是信号量的句柄,在 C 语言中的实现是指向信号量控制块的指针。信号量控制块结构的详细定义如下: ```c 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-Thread 提供了一系列操作的 API 用来管理信号量,主要有:(*详细见*[线程间同步 (rt-thread.org)](https://www.rt-thread.org/document/site/#/rt-thread-version/rt-thread-standard/programming-manual/ipc1/ipc1)) - 创建和删除信号量 - 初始化和脱离信号量 - 获取(*等待与无等待*)和释放信号量 #### 互斥量 互斥量和信号量不同的是:拥有互斥量的线程拥有互斥量的所有权。互斥量的状态只有两种,开锁或闭锁(两种状态值)。当有线程持有它时,互斥量处于闭锁状态,由这个线程获得它的所有权。相反,当这个线程释放它时,将对互斥量进行开锁,失去它的所有权。 在 RT-Thread 中,互斥量控制块是操作系统用于管理互斥量的一个数据结构,由结构体 `struct rt_mutex` 表示。另外一种 C 表达方式 `rt_mutex_t`,表示的是互斥量的句柄。互斥量控制块结构的详细定义如下: ```c struct rt_mutex { struct rt_ipc_object parent; /* 继承自 ipc_object 类 */ rt_uint16_t value; /* 互斥量的值 */ rt_uint8_t original_priority; /* 持有线程的原始优先级 */ rt_uint8_t hold; /* 持有线程的持有次数 */ struct rt_thread *owner; /* 当前拥有互斥量的线程 */ }; /* rt_mutext_t 为指向互斥量结构体的指针类型 */ typedef struct rt_mutex* rt_mutex_t; ``` 根据互斥量控制块,RT-Thread 提供了一系列操作的 API 用来管理互斥量,主要有:(*详细见*[线程间同步 (rt-thread.org)](https://www.rt-thread.org/document/site/#/rt-thread-version/rt-thread-standard/programming-manual/ipc1/ipc1)) - 创建/删除互斥量 - 初始化/脱离互斥量 - 获取(*等待与无等待*)和释放互斥量 #### 事件集 事件集主要用于线程间的同步,与信号量不同,它的特点是可以实现一对多,多对多的同步。即一个线程与多个事件的关系可设置为:其中任意一个事件唤醒线程,或几个事件都到达后才唤醒线程进行后续的处理;同样,事件也可以是多个线程同步多个事件。这种多个事件的集合可以用一个 32 位无符号整型变量来表示,变量的每一位代表一个事件,线程通过 “逻辑与” 或“逻辑或”将一个或多个事件关联起来,形成事件组合。事件的 “逻辑或” 也称为是独立型同步,指的是线程与任何事件之一发生同步;事件 “逻辑与” 也称为是关联型同步,指的是线程与若干事件都发生同步。 在 RT-Thread 中,事件集控制块是操作系统用于管理事件的一个数据结构,由结构体 `struct rt_event` 表示。另外一种 C 表达方式 `rt_event_t`,表示的是事件集的句柄。事件集控制块结构的详细定义如下: ```c struct rt_event { struct rt_ipc_object parent; /* 继承自 ipc_object 类 */ /* 事件集合,每一 bit 表示 1 个事件,bit 位的值可以标记某事件是否发生 */ rt_uint32_t set; }; /* rt_event_t 是指向事件结构体的指针类型 */ typedef struct rt_event* rt_event_t; ``` RT-Thread 提供了如下对事件集的操作 API:(*详细见*[线程间同步 (rt-thread.org)](https://www.rt-thread.org/document/site/#/rt-thread-version/rt-thread-standard/programming-manual/ipc1/ipc1)) - 创建 / 初始化事件集 - 删除 / 脱离事件集。 - 发送事件 / 接收事件 ### 线程间的通信 #### 邮箱和消息队列 邮箱服务是实时操作系统中一种典型的线程间通信方法。举一个简单的例子,有两个线程,线程 1 检测按键状态并发送,线程 2 读取按键状态并根据按键的状态相应地改变 LED 的亮灭。这里就可以使用邮箱的方式进行通信,线程 1 将按键的状态作为邮件发送到邮箱,线程 2 在邮箱中读取邮件获得按键状态并对 LED 执行亮灭操作。 而消息队列是另一种常用的线程间通讯方式,是邮箱的扩展。能够接收来自线程或中断服务例程中不固定长度的消息。 同上,邮箱和消息队列也通过其各自的控制块进行管理,详细定义如下: ```c struct rt_mailbox { struct rt_ipc_object parent; rt_uint32_t* msg_pool; /* 邮箱缓冲区的开始地址 */ rt_uint16_t size; /* 邮箱缓冲区的大小 */ rt_uint16_t entry; /* 邮箱中邮件的数目 */ rt_uint16_t in_offset, out_offset; /* 邮箱缓冲的进出指针 */ rt_list_t suspend_sender_thread; /* 发送线程的挂起等待队列 */ }; typedef struct rt_mailbox* rt_mailbox_t; struct rt_messagequeue { struct rt_ipc_object parent; void* msg_pool; /* 指向存放消息的缓冲区的指针 */ rt_uint16_t msg_size; /* 每个消息的长度 */ rt_uint16_t max_msgs; /* 最大能够容纳的消息数 */ rt_uint16_t entry; /* 队列中已有的消息数 */ void* msg_queue_head; /* 消息链表头 */ void* msg_queue_tail; /* 消息链表尾 */ void* msg_queue_free; /* 空闲消息链表 */ rt_list_t suspend_sender_thread; /* 发送线程的挂起等待队列 */ }; typedef struct rt_messagequeue* rt_mq_t; ``` 邮箱和消息队列都提供相应的操作 API,主要包括:(*详细见*[线程间通信 (rt-thread.org)](https://www.rt-thread.org/document/site/#/rt-thread-version/rt-thread-standard/programming-manual/ipc2/ipc2?id=%e6%b6%88%e6%81%af%e9%98%9f%e5%88%97)) - 创建/删除邮箱(消息队列) - 初始化/脱离邮箱(消息队列) - 发送/删除邮箱(消息队列) ## **实时的消息、事件处理机制和提供内核级的优先级翻转处理方式** 在通用操作系统中关于消息队列的实现中,对于等待在消息队列上的进程、线程,操作系统可能会按照 `FIFO`(先进先出)的方式进行调度,如果此时有多个接受者,那么接受者也是按照 `FIFO` 的原则接受消息(数据)。 但是 RT-Thread 会提供基于优先级的处理方式:两个任务优先级是分别是10和20,同时等待一个消息,如果按照优先级方式处理,则优先级为10的任务会优先收到消息。虽然 RT-Thread 也支持使用`RT_IPC_FLAG_FIFO`来创建按照先进先出方式来处理消息队列,但是官方建议除非应用程序非常在意先来后到,并且清楚地明白所有涉及到该消息队列的线程都将会变为非实时线程,方可使用 `RT_IPC_FLAG_FIFO`,否则建议采用`RT_IPC_FLAG_PRIO`,即确保线程的实时性。 除此之外,实时操作系统在提供线程间的同步机制时,会提供内核级的优先级翻转处理方式。 所谓优先级翻转,即当一个高优先级线程试图通过信号量机制访问共享资源时,如果该信号量已被一低优先级线程持有,而这个低优先级线程在运行过程中可能又被其它一些中等优先级的线程抢占,因此造成高优先级线程被许多具有较低优先级的线程阻塞,实时性难以得到保证。如下图所示:有优先级为 A、B 和 C 的三个线程,优先级 `A> B > C`。线程 A,B 处于挂起状态,等待某一事件触发,线程 C 正在运行,此时线程 C 开始使用某一共享资源 M。在使用过程中,线程 A 等待的事件到来,线程 A 转为就绪态,因为它比线程 C 优先级高,所以立即执行。但是当线程 A 要使用共享资源 M 时,由于其正在被线程 C 使用,因此线程 A 被挂起切换到线程 C 运行。如果此时线程 B 等待的事件到来,则线程 B 转为就绪态。由于线程 B 的优先级比线程 C 高,且线程B没有用到共享资源 M ,因此线程 B 开始运行,直到其运行完毕,线程 C 才开始运行。只有当线程 C 释放共享资源 M 后,线程 A 才得以执行。在这种情况下,优先级发生了翻转:线程 B 先于线程 A 运行。这样便不能保证高优先级线程的响应时间。 ![优先级反转 M 为信号量](https://www.rt-thread.org/document/site/rt-thread-version/rt-thread-standard/programming-manual/ipc1/figures/06priority_inversion.png) 在 RT-Thread 中,互斥量可以解决优先级翻转问题,实现的是优先级继承协议 (Sha, 1990)。优先级继承是通过在线程 A 尝试获取共享资源而被挂起的期间内,将线程 C 的优先级提升到线程 A 的优先级别,从而解决优先级翻转引起的问题。这样能够防止 C(间接地防止 A)被 B 抢占,如下图所示。优先级继承是指,提高某个占有某种资源的低优先级线程的优先级,使之与所有等待该资源的线程中优先级最高的那个线程的优先级相等,然后执行,而当这个低优先级线程释放该资源时,优先级重新回到初始设定。因此,继承优先级的线程避免了系统资源被任何中间优先级的线程抢占。 ![优先级继承 M 为互斥量](https://www.rt-thread.org/document/site/rt-thread-version/rt-thread-standard/programming-manual/ipc1/figures/06priority_inherit.png) # 内存管理 >**避免提供实时性不确定的API:以内存管理为例** ## **避免提供实时性不确定的API:以内存管理为例** 在 Liunx 中,内存管理可以从以下几个方面来看: 1. 页式存储管理:Linux 使用基于页式存储管理的虚拟内存机制。它将物理内存划分为大小相等的页,将虚拟地址空间也划分为大小相等的页。当程序访问一个虚拟地址时,Linux会将其映射到对应的物理页上。这样做的好处是可以让每个进程都认为自己拥有整个物理内存,从而简化了编程。 2. 页面替换算法:当物理内存不足时,Linux 使用页面替换算法将一些不常用的页面换出到磁盘上,以释放物理内存。Linux 默认使用的页面替换算法是LRU(`Least Recently Used`),即将最长时间未被使用的页面换出。同时,Linux 也提供了其他的页面替换算法,例如`CLOCK`和`Random`。 3. 内存分配机制:Linux 使用了不同的内存分配机制来管理内存。其中,最常见的是伙伴算法和 `slab` 分配器。伙伴算法将物理内存划分为大小相等的块,每个块都是2的幂次方大小的2进制数。当分配请求到达时,Linux会寻找大小最合适的块并将其分配给程序。`slab`分配器则更为灵活,它将物理内存划分为不同大小的对象池,每个对象池都有自己的内存分配器和内存回收器。 4. 内存保护机制:Linux 提供了内存保护机制来保护程序的内存不被其他程序或系统组件访问。其中,最常见的内存保护机制是页表机制,它使用了硬件的支持来实现内存保护。此外,Linux还提供了其他的内存保护机制,例如内存访问错误处理机制和内存安全检查机制。 其中1、2、3点对于实时操作系统都是不可接受的,因为这会破坏实时操作系统的实时性,或者说时间的可预测性。 1. 对于第一和第二点,多数实时操作系统都不支持虚拟内存(page file/swap area),主要原因是缺页中断(page fault)会导致任务调度的不确定性增加。一次缺页中断的开销十分巨大(通常都是毫秒级),波及的代码很多,导致用户程序执行的不确定性增加。 2. 对于第三点,这样分配内存的时间同样是不确定的。根据需要存储的数据的长度在内存中去寻找一个与这段数据相适应的空闲内存块,然后将数据存储在里面。而寻找这样一个空闲内存块所耗费的时间是不确定的,因此对于实时系统来说,这就是不可接受的,实时系统必须要保证内存块的分配过程在可预测的确定时间内完成,否则实时任务对外部事件的响应也将变得不可确定。 3. 对于第四点,实时操作系统通常也不会选用页表机制去实现内存保护,但是可以选择使用内存保护单元(Memery Protection Unit,MPU)。 除此之外,实时操作系统还需要考虑内存碎片以及资源紧张的问题。 ## RT-Thread 的内存管理 为了解决上述问题,RT-Thread 主要提供了三种方式进行内存管理,小内存管理算法、slab 管理算法和 memheap 管理算法(详细见[内存管理 (rt-thread.org)](https://www.rt-thread.org/document/site/#/rt-thread-version/rt-thread-standard/programming-manual/memory/memory?id=%e5%86%85%e5%ad%98%e6%b1%a0))。 RT-Thread 即使在含有`MMU`的开发板上,也采用直接映射的方式,内核和所有任务共享同一个地址空间,物理内存地址和设备寄存器地址空间采用不同的映射属性,但是映射的虚拟地址和物理地址相一致。 ### 小内存管理算法 小内存管理算法是一个简单的内存分配算法。初始时,它是一块大的内存。当需要分配内存块时,将从这个大的内存块上分割出相匹配的内存块,然后把分割出来的空闲内存块还回给堆管理系统中。每个内存块都包含一个管理用的数据头,通过这个头把使用块与空闲块用双向链表的方式链接起来。释放时则是相反的过程,但分配器会查看前后相邻的内存块是否空闲,如果空闲则合并成一个大的空闲内存块。 ### slab 管理算法 RT-Thread 的 `slab` 分配器是在 DragonFly BSD 创始人 Matthew Dillon 实现的 `slab` 分配器基础上,针对嵌入式系统优化的内存分配算法。最原始的 `slab` 算法是 Jeff Bonwick 为 Solaris 操作系统而引入的一种高效内核内存分配算法。 RT-Thread 的 `slab` 分配器实现主要是去掉了其中的对象构造及析构过程,只保留了纯粹的缓冲型的内存池算法。`slab` 分配器会根据对象的大小分成多个区(zone),也可以看成每类对象有一个内存池。 ### memheap 管理算法 `memheap` 管理算法适用于系统含有多个地址可不连续的内存堆。使用 `memheap` 内存管理可以简化系统存在多个内存堆时的使用:当系统中存在多个内存堆的时候,用户只需要在系统初始化时将多个所需的 `memheap` 初始化,并开启 `memheap` 功能就可以很方便地把多个 `memheap`(地址可不连续)粘合起来用于系统的 `heap` 分配。 # 中断管理和设备驱动 >**减少粗粒度的锁和长期关中断的使用** ## **减少粗粒度的锁和长期关中断的使用** 在这一点上,非实时操作系统和实时操作系统相同,都要尽量减少粗粒度的锁、耗时的中断处理以及长期关中断。 在编写 Linux 设备驱动时,我们使用 `request_irq `申请中断的时候注册的中断服务函数属于中断处理的上半部,只要中断触发,那么中断处理函数就会执行。 但是中断处理中可能存在着许多耗时的操作,电容触摸屏通过中断通知 `CPU` 有触摸事件发生,`CPU` 响应中断,然后 通过 `IIC` 接口读取触摸坐标值并将其上报给系统。但是我们都知道 `IIC` 的速度最高也只有 `400Kbit/S`,所以在中断中通过 `IIC` 读取数据就会浪费时间。 此时,我们可以将通过 `IIC` 读取触摸数据的操作暂后执行,中断处理函数仅仅相应中断,然后清除中断标志位即可。这个时候中断处理过程就分为了两部分: - **上半部**:上半部就是中断处理函数,那些处理过程比较快,不会占用很长时间的处理就可以放在上半部完成。 - ** 下半部**:如果中断处理过程比较耗时,那么就将这些比较耗时的代码提出来,交给下半部去执行,这样中断处理函数就会快进快出。 因此,Linux 内核将中断分为上半部和下半部的主要目的就是实现中断处理函数的快进快出,那些对时间敏感、执行速度快的操作可以放到中断处理函数中,也就是上半部。剩下的所有工作都可以放到下半部去执行,比如在上半部将数据拷贝到内存中,关于数据的具体处理就 可以放到下半部去执行。而在 Linux 中提供了软中断、`tasklet`、工作队列等方式去实现下半部。 在 RT-Thread 显然也有类似的处理,我们以使用 `UART`设备驱动为例。 ## UART 设备驱动使用 对于非实时操作系统来说,如果收到一个外部中断的通常做法是把中断作为一个事件通告给另外一个任务,interrupt handler在处理完关键数据以后,立即打开中断,驱动的中断处理程序以一个高优先级任务的方式继续执行。一般来说,使用 `UART` 的流程如下: 1. 首先查找串口设备获取设备句柄。 2. 初始化回调函数发送使用的信号量,然后以读写及中断接收方式打开串口设备。 3. 设置串口设备的接收回调函数,之后发送字符串,并创建读取数据线程。 - 读取数据线程会尝试读取一个字符数据,如果没有数据则会挂起并等待信号量,当串口设备接收到一个数据时会触发中断并调用接收回调函数,此函数会发送信号量唤醒线程(此为上半部),此时线程会马上读取接收到的数据(此为下半部)。 - 此示例代码不局限于特定的 `BSP`,根据 `BSP` 注册的串口设备,修改示例代码宏定义 `SAMPLE_UART_NAME` 对应的串口设备名称即可运行。 ![串口中断接收及轮询发送序列图](https://www.rt-thread.org/document/site/rt-thread-version/rt-thread-standard/programming-manual/device/uart/uart_v1/figures/uart-int.png) ```c /* * 程序清单:这是一个 串口 设备使用例程 * 例程导出了 uart_sample 命令到控制终端 * 命令调用格式:uart_sample uart2 * 命令解释:命令第二个参数是要使用的串口设备名称,为空则使用默认的串口设备 * 程序功能:通过串口输出字符串"hello RT-Thread!",然后错位输出输入的字符 */ #include
#define SAMPLE_UART_NAME "uart2" /* 用于接收消息的信号量 */ static struct rt_semaphore rx_sem; static rt_device_t serial; /* 接收数据回调函数 */ static rt_err_t uart_input(rt_device_t dev, rt_size_t size) { /* 串口接收到数据后产生中断,调用此回调函数,然后发送接收信号量 */ rt_sem_release(&rx_sem); return RT_EOK; } static void serial_thread_entry(void *parameter) { char ch; while (1) { /* 从串口读取一个字节的数据,没有读取到则等待接收信号量 */ while (rt_device_read(serial, -1, &ch, 1) != 1) { /* 阻塞等待接收信号量,等到信号量后再次读取数据 */ rt_sem_take(&rx_sem, RT_WAITING_FOREVER); } /* 读取到的数据通过串口错位输出 */ ch = ch + 1; rt_device_write(serial, 0, &ch, 1); } } static int uart_sample(int argc, char *argv[]) { rt_err_t ret = RT_EOK; char uart_name[RT_NAME_MAX]; char str[] = "hello RT-Thread!\r\n"; if (argc == 2) { rt_strncpy(uart_name, argv[1], RT_NAME_MAX); } else { rt_strncpy(uart_name, SAMPLE_UART_NAME, RT_NAME_MAX); } /* 查找系统中的串口设备 */ serial = rt_device_find(uart_name); if (!serial) { rt_kprintf("find %s failed!\n", uart_name); return RT_ERROR; } /* 初始化信号量 */ rt_sem_init(&rx_sem, "rx_sem", 0, RT_IPC_FLAG_FIFO); /* 以中断接收及轮询发送模式打开串口设备 */ rt_device_open(serial, RT_DEVICE_FLAG_INT_RX); /* 设置接收回调函数 */ rt_device_set_rx_indicate(serial, uart_input); /* 发送字符串 */ rt_device_write(serial, 0, str, (sizeof(str) - 1)); /* 创建 serial 线程 */ rt_thread_t thread = rt_thread_create("serial", serial_thread_entry, RT_NULL, 1024, 25, 10); /* 创建成功则启动线程 */ if (thread != RT_NULL) { rt_thread_startup(thread); } else { ret = RT_ERROR; } return ret; } /* 导出到 msh 命令列表中 */ MSH_CMD_EXPORT(uart_sample, uart device sample); ``` # 多核调度 >**针对实时性设计的SMP** 对于通用操作系统来说,例如 Linux,并不需要考虑多核下任务调度的实时性。但是这对于实时操作系统仍然是必须的。所以 RT-Thread 实现了自己的多核调度(*详细见*[SMP 介绍与移植 (rt-thread.org)](https://www.rt-thread.org/document/site/#/rt-thread-version/rt-thread-standard/programming-manual/smp/smp))。 ## 任务特性 RT-Thread 中的任务分为以下状态: - 运行态:任务正在某个 `CPU` 上执行; - 就绪态:任务随时可以被执行,但尚未分配到 `CPU` ,因此等待在某个就绪态任务表中; - 挂起态:任务因为条件不满足(等待超时或者数据到来等),而不能够被执行; - 关闭态:任务已经被删除,正在等待被回收。 在进入正常运行阶段后,每个 `CPU` 都独自地运行中断处理、调度器以及任务的代码。RT-Thread 在多核系统上运行时存在以下特性: 1. 同一时刻,一个任务(线程)只会运行在一个 `CPU` 上; 2. 每个 CPU 互斥地访问全局调度器数据,以确定将要在当前 `CPU` 上运行的任务; 3. 支持将任务绑定在某一个 `CPU` 上运行。 ## 调度策略 为了实现上述目标,RT-Thread 调度器实现了两种就绪任务队列: 1. 全局就绪任务表 `rt_thread_ready_table[]` ,包含没有绑定 `CPU` 的就绪任务; 2. `CPU` 局部就绪任务表 `ready_table[]` ,每个 `CPU` 对应一个,包含绑定到对应 `CPU` 的就绪任务。典型的 `CPU` 绑定任务是每个 `CPU` 自己的 idle 任务。 当 CPU 需要切换任务执行时,任务调度器查找系统中优先级最高的就绪任务执行,即全局就绪任务表和当前 `CPU` 的局部就绪任务表中优先级最高的任务。在优先级相同的情况下,优先选取局部任务表中的任务。 相对应的是,如果一个任务由其它状态变为就绪态,则进行如下处理: 1. 如果它不是 `CPU` 绑定任务,则把它挂入全局就绪表,并向其它的所有 `CPU` 发送 `IPI` 中断,通知它们检查是否需要切换任务,因为其它 CPU 的当前任务的优先级可能低于此就绪态任务,因而会发生优先级抢占; 2. 如果它是一个 `CPU` 绑定任务,检查它是否比对应 `CPU` 的当前任务优先级高,如果是则发生优先级抢占,否则把它挂入对应 `CPU` 的局部就绪任务表。整个过程不通知其它 `CPU` 。 # 总结 总的来说,RT-Thread 的任何设计都围绕着实时性,无论是线程调度还是中断管理等等。理解实时操作系统相比如 Linux 这样的通用操作系统的应用场景以及实时性设计,是学习的关键。 # 引用 > [RT-Thread 文档中心](https://www.rt-thread.org/document/site/#/) > > https://zhuanlan.zhihu.com/p/86861756
5
条评论
默认排序
按发布时间排序
登录
注册新账号
关于作者
dejavudwh
这家伙很懒,什么也没写!
文章
7
回答
0
被采纳
0
关注TA
发私信
相关文章
1
大神们,rt-thread启用WDT了,但是还是没启动,怎么办?
2
求一个师傅带带队,有偿交学费 肯吃苦
3
自己按照官方手册 在drv_gpio.c里面找不到PIN脚信息
4
rtt studio f4默认生成的代码无法使用
5
官方例程中的 USB设置配置不成功
6
STM32F4的虚拟串口 的USB时钟如何配置
7
AT24CXX 软件包函数 at24cxx的问题
8
rtthread studio和bsp文件之间生成的区别和联系?
9
pwm根据手册修改为对应的引脚后无效
10
文件系统挂实验 ls命令异常
推荐文章
1
RT-Thread应用项目汇总
2
玩转RT-Thread系列教程
3
国产MCU移植系列教程汇总,欢迎查看!
4
机器人操作系统 (ROS2) 和 RT-Thread 通信
5
五分钟玩转RT-Thread新社区
6
【技术三千问】之《玩转ART-Pi》,看这篇就够了!干货汇总
7
关于STM32H7开发板上使用SDIO接口驱动SD卡挂载文件系统的问题总结
8
STM32的“GPU”——DMA2D实例详解
9
RT-Thread隐藏的宝藏之completion
10
【ART-PI】RT-Thread 开启RTC 与 Alarm组件
热门标签
RT-Thread Studio
串口
Env
LWIP
SPI
AT
Bootloader
Hardfault
CAN总线
FinSH
ART-Pi
USB
DMA
文件系统
RT-Thread
SCons
RT-Thread Nano
线程
MQTT
STM32
RTC
FAL
rt-smart
ESP8266
I2C_IIC
WIZnet_W5500
UART
ota在线升级
PWM
cubemx
freemodbus
flash
packages_软件包
BSP
潘多拉开发板_Pandora
定时器
ADC
GD32
flashDB
socket
中断
Debug
编译报错
SFUD
msh
rt_mq_消息队列_msg_queue
keil_MDK
ulog
MicroPython
C++_cpp
本月问答贡献
出出啊
1517
个答案
342
次被采纳
小小李sunny
1443
个答案
289
次被采纳
张世争
805
个答案
174
次被采纳
crystal266
547
个答案
161
次被采纳
whj467467222
1222
个答案
148
次被采纳
本月文章贡献
出出啊
1
篇文章
4
次点赞
小小李sunny
1
篇文章
1
次点赞
张世争
1
篇文章
1
次点赞
crystal266
2
篇文章
2
次点赞
whj467467222
2
篇文章
1
次点赞
回到
顶部
发布
问题
投诉
建议
回到
底部