第 3 章 - Azure RTOS ThreadX 的功能组件

本章从功能角度介绍了高性能 Azure RTOS ThreadX 内核。 每个功能组件都将采用易于理解的方式进行介绍。

执行概述

ThreadX 应用程序包含四种类型的程序执行:初始化、线程执行、中断服务例程 (ISR) 和应用程序计时器。

图 2 显示了各种不同类型的程序执行。 本章的后续部分更详细地介绍了其中的每种类型。

初始化

顾名思义,这是 ThreadX 应用程序中的第一种程序执行。 初始化包括处理器重置与线程计划循环入口点之间的所有程序执行。

线程执行

初始化完成后,ThreadX 会进入其线程计划循环。 计划循环查找准备好执行的应用程序线程。 找到准备就绪的线程后,ThreadX 将控制权转交给该线程。 系统完成该线程(或另一个优先级较高的线程变为就绪)后,将执行权转交回线程计划循环,以查找下一个优先级最高的就绪线程。

这个持续执行和计划线程的过程是 ThreadX 应用程序中最常见的程序执行类型。

中断服务例程 (ISR)

中断是实时系统的基础。 如果没有中断,系统将很难及时响应外部环境的变化。 检测到中断时,处理器会保存当前程序执行的重要信息(通常在堆栈上),然后将控制权转交到预定义的程序区域。 这个预定义的程序区域通常称为中断服务例程。 在大多数情况下,中断发生在线程执行期间(或线程调度循环中)。 但也可能在执行 ISR 或应用程序计时器时发生中断。

程序执行类型

图 2. 程序执行类型

应用程序计时器

应用程序计时器与 ISR 类似,不同之处在于硬件实现(通常使用单个定期硬件中断)已对应用程序隐藏。 应用程序使用此类计时器来执行超时、定期任务和/或监视器服务。 与 ISR 一样,应用程序计时器最常中断线程执行。 但与 ISR 不同的是,应用程序计时器无法相互中断。

内存用量

ThreadX 与应用程序一起驻留。 因此,ThreadX 的静态内存(或固定内存)使用情况取决于开发工具,例如编译器、链接器和定位符。 动态内存(或运行时内存)使用情况由应用程序直接控制。

静态内存使用情况

大多数开发工具将应用程序映像分为五个基本区域:指令、常数、已初始化的数据、未初始化的数据和系统堆栈 。 图 3 显示了这些内存区域的示例。

请务必了解,这只是一个示例。 实际的静态内存布局特定于处理器、开发工具和基础硬件。

指令区域包含程序的所有处理器指令。 此区域通常最大,一般位于 ROM 中。

常数区域包含各种已编译的常数,包括程序中定义或引用的字符串。 此外,此区域包含已初始化的数据区域的“初始副本”。 在内存使用情况编译器的初始化过程中,常数区域的这个部分用于设置 RAM 中已初始化的数据区域。 常数区域通常在指令区域之后,一般位于 ROM 中。

已初始化的数据和未初始化的数据区域包含所有全局和静态变量。 这些区域始终位于 RAM 中。

系统堆栈通常紧跟在初始化和未初始化的数据区域之后设置。

系统堆栈供编译器在初始化期间使用,然后供 ThreadX 在初始化期间使用,随后在 ISR 处理中使用。

内存区域示例

图 3. 内存区域示例

动态内存使用情况

如前所述,动态内存使用情况由应用程序直接控制。 与堆栈、队列和内存池关联的控制块和内存区域可以放置在目标内存空间中的任何位置。 这是一项重要的功能,因为该功能有助于轻松利用不同类型的物理内存。

例如,假设目标硬件环境具有快速内存和慢速内存。 如果应用程序需要额外的性能来处理高优先级线程,则可将其控制块 (TX_THREAD) 和堆栈放置在快速内存区域中,这可能会显著提高其性能。

初始化

了解初始化过程非常重要。 初始硬件环境在此处设置。 此外,这里还为应用程序指定了初始个性化设置。

注意

ThreadX 尝试(尽可能)采用完整的开发工具的初始化过程。 这样,以后就可以更轻松地升级到开发工具的新版本。

系统重置向量

所有微处理器都具有重置逻辑。 当(硬件或软件)发生重置时,将从特定内存位置检索应用程序入口点的地址。 检索到入口点后,处理器会将控制权转交到该位置。 应用程序入口点通常用本机程序集语言编写,并且通常由开发工具提供(至少采用模板形式)。 在某些情况下,ThreadX 会附带入口程序的特殊版本。

开发工具初始化

低级初始化完成后,控制权将转交给开发工具的高级初始化。 这个位置通常设置了已初始化的全局变量和静态 C 变量。 请记住,其初始值将从常数区域检索。 具体的初始化处理将特定于开发工具。

main 函数

完成开发工具初始化后,系统将控制权转交给用户提供的 main 函数。 此时,应用程序会控制接下来执行的操作。 对于大多数应用程序,main 函数只调用 tx_kernel_enter,这是 ThreadX 的入口。 但是,应用程序可在进入 ThreadX 之前执行初步处理(通常用于硬件初始化)。

重要

对 tx_kernel_enter 的调用不会返回结果,因此请勿在此后执行任何处理。

tx_kernel_enter

entry 函数协调各种内部 ThreadX 数据结构的初始化,然后调用应用程序的定义函数 tx_application_define

当 tx_application_define 返回时,控制权将转交给线程调度循环。 这标志着初始化结束。

应用程序定义函数

tx_application_define 函数定义所有初始应用程序线程、队列、信号灯、互斥、事件标志、内存池和计时器。 在应用程序的正常操作过程中,还可以在线程中创建和删除系统资源。 但是,所有初始应用程序资源都在此处定义。

值得一提的是,tx_application_define 函数只有一个输入参数。 “第一个可用”的 RAM 地址是该函数唯一的输入参数。 该地址通常用作线程堆栈、队列和内存池的初始运行时内存分配起点。

注意

初始化完成后,只有正在执行的线程才能创建和删除系统资源(包括其他线程)。 因此,在初始化期间必须至少创建一个线程。

中断

在整个初始化过程中,中断处于禁用状态。 如果应用程序以某种方式启用中断,则可能出现不可预知的行为。 图 4 显示了从系统重置到特定于应用程序的初始化的整个初始化过程。

线程执行

计划和执行应用程序线程是 ThreadX 最重要的活动。 线程通常定义为具有专用用途的半独立程序段。 所有线程的组合处理构成了应用程序。

线程在初始化或线程执行期间通过调用 tx_thread_create 来动态创建。 创建的线程处于“就绪”或“已挂起”状态。

初始化过程

图 4. 初始化过程

线程执行状态

了解线程的不同处理状态是了解整个多线程环境的关键要素。 ThreadX 有五个不同的线程状态:就绪、已挂起、正在执行、已终止和已完成 。 图 5 显示了 ThreadX 的线程状态转换图。

线程状态转换

图 5. 线程状态转换

线程准备好执行时处于“就绪”状态。 在处于就绪状态的线程中,只有优先级最高的就绪线程才会执行。 发生这种情况时,ThreadX 会执行该线程,然后将其状态更改为“正在执行”。

如果更高优先级的线程已准备就绪,正在执行的线程会恢复为“就绪”状态。 然后执行新的高优先级就绪线程,并将其逻辑状态更改为“正在执行”。 每次发生线程抢占时,即会在“就绪”和“正在执行”之间转换。

在任意指定时刻,只有一个线程处于“正在执行”状态。 这是因为处于“正在执行”状态的线程可以控制基础处理器。

处于“已挂起”状态的线程不符合执行条件。 导致处于“已挂起”状态的原因包括挂起时间、队列消息、信号灯、互斥锁、事件标志、内存和基本线程挂起。 排除导致挂起的原因后,线程将恢复“就绪”状态。

处于“已完成”状态的线程是指已完成其处理任务并从其 entry 函数返回的线程。 entry 函数在线程创建期间指定。 处于“已完成”状态的线程无法再次执行。

线程处于“已终止”状态,因为另一个线程或该线程本身调用了 tx_thread_terminate 服务。 处于“已终止”状态的线程无法再次执行。

重要

如果需要重新启动已完成或已终止的线程,应用程序必须先删除该线程。 然后才能重新创建并重新启动该线程。

线程进入/退出通知

某些应用程序可能会发现,在特定线程首次进入、完成或终止时收到通知非常有利。 ThreadX 通过 tx_thread_entry_exit_notify 服务提供此功能。 此服务为特定线程注册应用程序通知函数,ThreadX 会在该线程开始运行、完成或终止时调用该函数。 应用程序通知函数在调用后可以执行特定于应用程序的处理。 这通常涉及通过 ThreadX 同步基元通知此事件的另一个应用程序线程。

线程优先级

如前所述,线程是具有专用用途的半独立程序段。 但是,所有线程在创建时并非完全相同! 某些线程具有比其他线程更重要的专用用途。 这种异类的线程重要性是嵌入式实时应用程序的标志。

ThreadX 在创建线程时通过分配表示其“优先级”的数值来确定线程的重要性。 ThreadX 的最大优先级数可在 32 到 1024(增量为 32)之间进行配置。 实际的最大优先级数由 TX_MAX_PRIORITIES 常数在 ThreadX 库的编译过程中确定。 设置更多优先级并不会显著增加处理开销。 但是,每个包含 32 个优先级的组额外需要 128 字节的 RAM 进行管理。 例如,32 个优先级需要 128 字节的 RAM,64 个优先级需要 256 字节的 RAM,而 96 个优先级需要 384 字节的 RAM。

默认情况下,ThreadX 具有 32 个优先级,范围从优先级 0 到优先级 31。 数值越小,优先级越高。 因此,优先级 0 表示最高优先级,而优先级 (TX_MAX_PRIORITIES-1) 表示最低优先级。

通过协作调度或时间切片,多个线程可以具有相同的优先级。 此外,线程优先级还可以在运行时更改。

线程调度

ThreadX 根据线程的优先级来调度线程。 优先级最高的就绪线程最先执行。 如果有多个具有相同优先级的线程准备就绪,则按照先进先出 (FIFO) 的方式执行。

轮循调度

ThreadX 支持通过轮循调度处理具有相同优先级的多个线程。 此过程通过以协作方式调用 tx_thread_relinquish 来实现。 此服务为相同优先级的所有其他就绪线程提供了在 tx_thread_relinquish 调用方再次执行之前执行的机会。

时间切片

“时间切片”是轮循调度的另一种形式。 时间片指定线程在不放弃处理器的情况下可以执行的最大计时器时钟周期数(计时器中断)。 在 ThreadX 中,时间切片按每个线程提供。 线程的时间片在创建时分配,可在运行时修改。 当时间片过期时,具有相同优先级的所有其他就绪线程有机会在时间切片线程重新执行之前执行。

当线程挂起、放弃、执行导致抢占的 ThreadX 服务调用或自身经过时间切片后,该线程将获得一个新的线程时间片。

当时间切片的线程被抢占时,该线程将在其剩余的时间片内比具有相同优先级的其他就绪线程更早恢复执行。

注意

使用时间切片会产生少量系统开销。 由于时间切片仅适用于多个线程具有相同优先级的情况,因此不应为具有唯一优先级的线程分配时间片。

优先

抢占是为了支持优先级更高的线程而暂时中断正在执行的线程的过程。 此过程对正在执行的线程不可见。 当更高优先级的线程完成时,控制权将转交回发生抢占的确切位置。 这是实时系统中一项非常重要的功能,因为该功能有助于快速响应重要的应用程序事件。 尽管抢占是一项非常重要的功能,但也可能导致各种问题,包括资源不足、开销过大和优先级反转。

抢占阈值 (Preemption-threshold™)

为了缓解抢占的一些固有问题,ThreadX 提供了一个独特的高级功能,名为抢占阈值。

抢占阈值允许线程指定禁用抢占的优先级上限。 优先级高于上限的线程仍可以执行抢占,但不允许优先级低于上限的线程执行抢占。

例如,假设优先级为 20 的线程只与一组优先级介于 15 到 20 之间的线程进行交互。 在其关键部分中,优先级为 20 的线程可将其抢占式阀值设置为 15,从而防止该线程和与之交互的所有线程发生抢占。 这仍允许(优先级介于 0和 14 之间)真正重要的线程在其关键部分处理中抢占此线程的资源,这会使处理的响应速度更快。

当然,仍有可能通过将其抢占式阀值设置为 0 来为线程禁用所有抢占。 此外,可以在运行时更改抢占阈值。

注意

使用抢占阈值会禁用指定线程的时间切片。

优先级继承

ThreadX 还支持其互斥服务内的可选优先级继承,本章稍后将对此进行介绍。 优先级继承允许低优先级线程暂时假设正在等待归较低优先级线程所有的互斥的高优先级线程的优先级。 借助此功能,应用程序可以消除中间优先级线程的抢占,从而避免出现不确定的优先级倒置。 当然,也可以使用“抢占式阀值”获得类似的结果。

线程创建

应用程序线程在初始化或执行其他应用程序线程的过程中创建。 应用程序可以创建的线程数量没有限制。

线程控制块 TX_THREAD

每个线程的特征都包含在其控制块中。 此结构在 tx_api.h 文件中定义。

线程的控制块可以位于内存中的任意位置,但最常见的是通过在任何函数的作用域外部定义该控件块来使其成为全局结构。

如同所有动态分配内存一样,将控制块放置于其他区域时需要多加小心。 如果在 C 函数内分配控制块,则与之相关联的内存是调用线程堆栈的一部分。 通常,请避免对控制块使用本地存储,因为在函数返回后,将释放其所有局部变量堆栈空间,而不管另一个线程是否正将其用于控制块。

在大多数情况下,应用程序不知道线程控制块的内容。 但在某些情况下,尤其是在调试过程中,观察特定成员会很有用。 下面是一些更有用的控制块成员。

tx_thread_run_count 包含记录线程已调用次数的计数器。 计数器增加表示正在调度和执行线程。

tx_thread_state 包含相关线程的状态。 下面列出了可能的线程状态。

线程状态
TX_READY (0x00)
TX_COMPLETED (0x01)
TX_TERMINATED (0x02)
TX_SUSPENDED (0x03)
TX_SLEEP (0x04)
TX_QUEUE_SUSP (0x05)
TX_SEMAPHORE_SUSP (0x06)
TX_EVENT_FLAG (0x07)
TX_BLOCK_MEMORY (0x08)
TX_BYTE_MEMORY (0x09)
TX_MUTEX_SUSP (0x0D)

注意

当然,线程控制块中还有很多有趣的字段,包括堆栈指针、时间片值、优先级等。欢迎用户查看控制块成员,但严格禁止修改!

重要

本节前面提到的“正在执行”状态没有等同的状态。 这不是必需的,因为在给定时间只有一个正在执行的线程。 正在执行的线程的状态也是 TX_READY。

当前正在执行的线程

如前所述,在任何给定时间都只有一个正在执行的线程。 有多种方法可以识别正在执行的线程,具体取决于发出请求的线程。 通过调用 tx_thread_identify,程序段可以获取正在执行的线程的控制块地址。 这在从多个线程执行的应用程序代码的共享部分中很有用。

在调试会话中,用户可以检查 ThreadX 内部指针 _tx_thread_current_ptr。 该数组包含当前正在执行的线程的控制块地址。 如果此指针为 NULL,则不执行任何应用程序线程;也就是说,ThreadX 在其调度循环中等待线程准备就绪。

线程堆栈区域

每个线程都必须有自己的堆栈,用于保存其上次执行和编译器使用的上下文。 大多数 C 编译器使用堆栈来执行函数调用和临时分配局部变量。 图 6 显示了典型的线程堆栈。

线程堆栈在内存中的位置取决于应用程序。 堆栈区域在线程创建期间指定,可以位于目标地址空间的任意位置。 这是一项重要的功能,因为应用程序可借助该功能,通过将重要线程堆栈置于高速 RAM 中来提高线程的性能。

堆栈内存区域(示例)

典型的线程堆栈

图 6. 典型的线程堆栈

应该设置多大的堆栈是有关线程的最常见问题之一。 线程的堆栈区域必须大到足以容纳最坏情况下的函数调用嵌套、局部变量分配,并保存其最后一个执行上下文。

最小堆栈大小 TX_MINIMUM_STACK 由 ThreadX 定义。 这种大小的堆栈支持保存线程的上下文、最少量的函数调用和局部变量分配。

但对大多数线程来说,最小的堆栈大小太小,用户必须通过检查函数调用嵌套和局部变量分配来确定最坏情况下的大小要求。 当然,最好从较大的堆栈区域开始。

调试应用程序后,如果内存不足,可以调整线程堆栈大小。 常用的技巧是在创建线程之前,使用诸如 (0xEFEF) 之类易于识别的数据模式预设所有堆栈区域。 在应用程序经过全面测试后,可以对堆栈区域进行检查,具体方法是通过查找堆栈区域(其中的数据模式仍保持不变)来查看实际使用了多少堆栈。 图 7 显示了堆栈在线程完全执行后预设为 0xEFEF。

堆栈内存区域(另一个示例)

堆栈预设为 0xEFEF*

图 7. 堆栈预设为 0xEFEF

重要

默认情况下,ThreadX 使用值 0xEF 初始化每个线程堆栈的每个字节。

内存缺陷

线程的堆栈需求可能很大。 因此,务必要将应用程序设计为可以容纳数量合理的线程。 此外,必须注意避免在线程中过度使用堆栈。 应避免使用递归算法和大型本地数据结构。

在大多数情况下,溢出的堆栈会导致线程执行损坏其堆栈区域相邻(通常在此位置之前)的内存。 其结果不可预测,但大多会导致程序计数器出现反常的变化。 这通常称为“深陷细枝末节”。当然,防止这种情况的唯一方法是确保所有线程堆栈足够大。

可选的运行时堆栈检查

ThreadX 提供了在运行时检查每个线程的堆栈是否损坏的功能。 默认情况下,ThreadX 在创建过程中使用 0xEF 数据模式填充线程堆栈的每个字节。 如果应用程序生成了已定义 TX_ENABLE_STACK_CHECKING 的 ThreadX 库,ThreadX 会检查每个线程的堆栈是否因为线程挂起或恢复而损坏。 如果检测到堆栈损坏,ThreadX 将调用在调用 tx_thread_stack_error_notify 时指定的应用程序堆栈错误处理例程。 否则,如果未指定堆栈错误处理程序,ThreadX 将调用内部 _tx_thread_stack_error_handler 例程。

重新进入

多线程处理的真正优点之一是,可以从多个线程中调用相同的 C 函数。 这提供了强大的功能,还有助于减少代码空间。 但是,此功能要求从多个线程调用的 C 函数是可重入的函数。

基本上,可重入函数将调用方的返回地址存储在当前堆栈上,并且不依赖于先前设置的全局或静态 C 变量。 大多数编译器将返回地址放在堆栈上。 因此,应用程序开发人员必须只关心如何使用全局和静态变量 。

非重入函数的一个例子是在标准 C 库中找到的字符串标记函数 strtok。 此函数在后续调用时“记住了”前面的字符串指针。 此函数通过静态字符串指针实现此功能。 如果从多个线程调用此函数,则最有可能返回无效的指针。

线程优先级缺陷

选择线程优先级是多线程处理最重要的方面之一。 有时很容易根据感知的线程重要性概念来分配优先级,而不是确定运行时到底需要什么。 滥用线程优先级会导致其他线程资源枯竭、产生优先级反转、减少处理带宽,致使应用程序出现难以理解的运行时行为。

如前所述,ThreadX 提供基于优先级的抢占式调度算法。 优先级较低的线程只能在没有更高优先级的线程可以执行时才会执行。 如果优先级较高的线程始终准备就绪,则不会执行优先级较低的线程。 这种情况称为线程资源不足。

大多数线程资源不足的问题都是在调试初期检测到的,可通过确保优先级较高的线程不会连续执行来解决。 另外,还可以在应用程序中添加此逻辑,以便逐渐提高资源不足的线程的优先级,直到有机会执行这些线程。

与线程优先级相关的另一个缺陷是“优先级倒置”。 当优先级较高的线程由于优先级较低的线程占用其所需资源而挂起时,将会发生优先级倒置。 当然,在某些情况下,有必要让两个优先级不同的线程共享一个公用资源。 如果这些线程是唯一处于活动状态的线程,优先级倒置时间就与低优先级线程占用资源的时间息息相关。 这种情况既具有确定性又非常正常。 不过,如果在这种优先级反转的情况,中等优先级的线程变为活动状态,优先级反转时间就不再确定,并且可能导致应用程序失败。

主要有三种不同的方法可防止 ThreadX 中出现不确定的优先级反转。 首先,在设计应用程序优先级选择和运行时行为时,可以采用能够防止出现优先级反转问题的方式。 其次,优先级较低的线程可以利用抢占阈值来阻止中等优先级线程在其与优先级较高的线程共享资源时执行抢占。 最后,使用 ThreadX 互斥对象保护系统资源的线程可以利用可选的互斥优先级继承来消除不确定的优先级反转。

优先级开销

要减少多线程处理开销,最容易被忽视的一种方法是减少上下文切换的次数。 如前所述,当优先级较高的线程执行优先于正在执行的线程时,则会发生上下文切换。 值得一提的是,在发生外部事件(例如中断)和执行线程发出服务调用时,优先级较高的线程可能变为就绪状态。

为了说明线程优先级对上下文切换开销的影响,假设有一个包含三个线程的环境,这些线程分别命名为 thread_1、thread_2 和 thread_3。 进一步假定所有线程都处于等待消息的挂起状态。 当 thread_1 收到消息后,立即将其转发给 thread_2。 随后,thread_2 将此消息转发给 thread_3。 Thread_3 只是丢弃此消息。 每个线程处理其消息后,则会返回并等待另一个消息。

执行这三个线程所需的处理存在很大的差异,具体取决于其优先级。 如果所有线程都具有相同优先级,则会在执行每个线程之前发生一次上下文切换。 当每个线程在空消息队列中挂起时,将会发生上下文切换。

但是,如果 thread_2 的优先级高于 thread_1,thread_3 的优先级高于 thread_2,上下文切换的次数将增加一倍。 这是因为当其检测到优先级更高的线程现已准备就绪时,会在 tx_queue_send 服务中执行另一次上下文切换。

ThreadX 抢占阈值机制可以避免出现这些额外的上下文切换,并且仍支持前面提到的优先级选择。 这是一项重要的功能,因为该功能允许在调度期间使用多个线程优先级,同时避免在线程执行期间出现一些不需要的上下文切换。

运行时线程性能信息

ThreadX 提供可选的运行时线程性能信息。 如果 ThreadX 库和应用程序是在定义 TX_THREAD_ENABLE_PERFORMANCE_INFO 的情况下生成的,ThreadX 会累积以下信息。

整个系统的总数:

  • 线程恢复数

  • 线程挂起数

  • 服务调用抢占次数

  • 中断抢占次数

  • 优先级倒置数

  • 时间切片数

  • 放弃次数

  • 线程超时数

  • 挂起中止数

  • 空闲系统返回数

  • 非空闲系统返回数

每个线程的总数:

  • 恢复数

  • 挂起数

  • 服务调用抢占次数

  • 中断抢占次数

  • 优先级倒置数

  • 时间切片数

  • 线程放弃

  • 线程超时数

  • 挂起中止数

此信息在运行时通过 tx_thread_performance_info_get 和 tx_thread_performance_system_info_get 服务提供。 线程性能信息在确定应用程序是否正常运行时非常有用。 此信息对于优化应用程序也很有用。 例如,如果服务调用抢占数量相对较多,可能表明线程的优先级和/或抢占阈值过低。 此外,如果空闲系统返回次数相对较少,可能表明优先级较低的线程没有完全挂起。

调试缺陷

调试多线程应用程序更为困难,因为同一个程序代码可以通过多个线程执行。 在这种情况下,只使用断点可能不够。 调试器还必须使用条件断点观察当前线程指针 _tx_thread_current_ptr,以查看调用线程是否为要调试的线程。

其中很多工作都是使用各种开发工具供应商提供的多线程支持包进行处理。 因为这些支持并设计简单,因此将 ThreadX 与不同的开发工具集成相对容易。

堆栈大小始终是多线程处理的重要调试主题。 如果观察到无法解释的行为,通常最好是增加所有线程的堆栈大小,尤其是要执行的最后一个线程的堆栈大小!

提示

使用定义的 TX_ENABLE_STACK_CHECKING 生成 ThreadX 库也是个好办法。 这将有助于在处理过程中尽早排除堆栈损坏问题。

消息队列

消息队列是 ThreadX 中线程间通信的主要方式。 消息队列中可以驻留一个或多个消息。 保留单个消息的消息队列通常称为邮箱。

消息通过 tx_queue_send 复制到队列,然后通过 tx_queue_receive 从队列中复制。 唯一的例外是在等待空队列中的消息时线程会挂起。 在这种情况下,发送到队列的下一条消息将直接放入该线程的目标区域。

每个消息队列都是一个公用资源。 ThreadX 对如何使用消息队列没有任何限制。

创建消息队列

消息队列由应用程序线程在初始化期间或运行时创建。 应用程序中的消息队列数没有限制。

消息大小

每个消息队列都支持许多固定大小的消息。 可用的消息大小为 1 到 16 个 32 位的字(含)。 消息大小在创建队列时指定。 超过 16 个字的应用程序消息必须通过指针传递。 为此,可以创建消息大小为 1 个字的队列(足以容纳一个指针),然后发送和接收消息指针,而不是整个消息。

消息队列容量

队列可以容纳的消息数是消息大小与创建期间提供的内存区域大小的函数。 队列的总消息容量的计算方法是,将每条消息中的字节数除以所提供的内存区域的总字节数。

例如,如果支持消息大小为 1 个 32 位字(4 个字节)的消息队列,是使用 100 字节内存区域创建的,则其容量为 25 条消息。

队列内存区域

如前所述,用于缓冲消息的内存区域在队列创建期间指定。 与 ThreadX 中的其他内存区域一样,该区域可以位于目标地址空间的任何位置。

这是一项重要的功能,因为它为应用程序提供了相当大的灵活性。 例如,应用程序可能会在高速 RAM 中定位重要队列的内存区域,从而提高性能。

线程挂起

尝试从队列发送或接收消息时,应用程序线程可能会挂起。 线程挂起通常涉及等待来自空队列的消息。 但是,线程也可能在尝试向已满队列发送消息时挂起。

解决挂起的条件后,请求的服务完成,等待的线程也相应恢复。 如果同一队列中的多个线程挂起,这些线程将按照挂起的顺序 (FIFO) 恢复。

不过,如果应用程序在取消线程挂起的队列服务之前调用 tx_queue_prioritize,还可以恢复优先级。 队列设置优先级服务将优先级最高的线程置于挂起列表的前面,让所有其他挂起的线程采用相同的 FIFO 顺序。

超时也可用于所有队列挂起。 从根本上说,超时会指定线程保持挂起状态的最大计时器时钟周期数。 如果发生超时,则会恢复线程,该服务会返回相应的错误代码。

队列发送通知

某些应用程序可能会发现,在将消息放入队列时收到通知十分有利。 ThreadX 通过 tx_queue_send_notify 服务提供此功能。 此服务将提供的应用程序通知函数注册到指定的队列。 只要有消息发送到队列,ThreadX 就会调用此应用程序通知函数。 应用程序通知函数内的确切处理由应用程序决定;但这通常包括恢复相应的线程以处理新消息。

队列事件链接 (Queue Event chaining™)

ThreadX 中的通知功能可用于链接各种同步事件。 当单个线程必须处理多个同步事件时,这通常很有用。

例如,假设单个线程负责处理来自五个不同队列的消息,还必须在没有可用消息时挂起。 通过为每个队列注册应用程序通知函数,并引入额外的计数信号灯,即可轻松实现这一点。 具体而言,每次调用应用程序通知函数时,该函数就会执行 tx_semaphore_put(信号灯计数表示所有五个队列中的消息总数)。 处理线程通过 tx_semaphore_get 服务挂起此信号灯。 当信号灯可用时(在这种情况下是消息可用时),即会恢复处理线程。 随后,它会询问每个队列来获取一条消息,处理找到的消息,然后执行另一个 tx_semaphore_get,以等待下一条消息。 在不使用事件链的情况下实现这一点非常困难,可能需要更多线程和/或附加的应用程序代码。

通常情况下,“事件链”会减少线程、降低开销和减少 RAM 要求。 它还提供了一种非常灵活的机制来处理更复杂系统的同步要求。

运行时队列性能信息

ThreadX 提供可选的运行时队列性能信息。 如果 ThreadX 库和应用程序是在定义 TX_QUEUE_ENABLE_PERFORMANCE_INFO 的情况下生成的,ThreadX 会累积以下信息

整个系统的总数:

  • 发送的消息数

  • 收到的消息数

  • 队列为空的挂起数

  • 队列已满的挂起数

  • 队列已满返回的错误数(未指定挂起)

  • 队列超时数

每个队列的总数:

  • 发送的消息数

  • 收到的消息数

  • 队列为空的挂起数

  • 队列已满的挂起数

  • 队列已满返回的错误数(未指定挂起)

  • 队列超时数

此信息在运行时通过 tx_queue_performance_info_get 和 tx_queue_performance_system_info_get 服务提供。 队列性能信息在确定应用程序是否正常运行时非常有用。 此信息对于优化应用程序也很有用。 例如,如果“队列已满的挂起数”相对较多,则表明增加队列大小可能有好处。

队列控制块 TX_QUEUE

每个消息队列的特征都可在其控制块中找到。 控制块包含受关注的信息,例如队列中的消息数。 此结构在 tx_api.h 文件中定义。

消息队列控制块也可以位于内存中的任意位置,但最常见的是通过在任何函数的作用域外部定义该控件块来使其成为全局结构。

消息目标缺陷

如前所述,消息将在队列区域和应用程序数据区域之间复制。 请务必确保收到消息的目标大到足以容纳整个消息。 否则,可能会损坏消息目标后面的内存。

注意

如果堆栈上的消息目标太小,就特别致命,没有什么比损坏函数的返回地址更重要!

统计信号量

ThreadX 提供 32 位计数信号灯,其值范围在 0 到 4,294,967,295 之间。 计数信号灯有两个操作:tx_semaphore_get 和 tx_semaphore_put 。 执行获取操作会将信号灯数量减一。 如果信号灯为 0,获取操作不会成功。 获取操作的逆操作是放置操作。 该操作会将信号灯数量加一。

每个计数信号灯都是一个公用资源。 ThreadX 对如何使用计数信号灯没有任何限制。

计数信号灯通常用于互相排斥。 不过,也可将计数信号灯用作事件通知的方法。

互相排斥

互相排斥用于控制线程对某些应用程序区域(也称为关键部分或应用程序资源)的访问 。 将信号灯用于互相排斥时,信号灯的“当前计数”表示允许访问的线程总数。 在大多数情况下,用于互相排斥的计数信号灯的初始值为 1,这意味着每次只有一个线程可以访问关联的资源。 只有 0 或 1 值的计数信号灯通常称为二进制信号灯。

重要

如果使用二进制信号灯,用户必须阻止同一个线程对其已拥有的信号灯执行获取操作。 第二个获取操作将失败,并且可能导致调用线程无限期挂起和资源永久不可用。

事件通知

还可以采用生成者-使用者的方式,将计数信号灯用作事件通知。 使用者尝试获取计数信号灯,而生成者则在有可用的信息时增加信号灯。 此类信号灯的初始值通常为 0,此值不会在生成者为使用者准备好信息之前增加。 用于事件通知的信号灯也可能从使用 tx_semaphore_ceiling_put 服务调用中获益。 此服务确保信号灯计数值永远不会超过调用中提供的值。

创建计数信号灯

计数信号灯由应用程序线程在初始化期间或运行时创建。 信号灯的初始计数在创建过程中指定。 应用程序中计数信号灯的数量没有限制。

线程挂起

尝试对当前计数为 0 的信号灯执行获取操作时,应用程序线程可能会挂起。

执行放置操作后,才会执行挂起线程的获取操作并恢复该线程。 如果同一计数信号灯上挂起多个线程,这些线程将按照挂起的顺序 (FIFO) 恢复。

不过,如果应用程序在取消线程挂起的信号灯放置调用之前调用 tx_semaphore_prioritize,还可以恢复优先级。 信号灯设置优先级服务将优先级最高的线程放于挂起列表的前面,同时让所有其他挂起的线程采用相同的 FIFO 顺序。

信号灯放置通知

某些应用程序可能会发现,在放置信号灯时收到通知十分有利。 ThreadX 通过 tx_semaphore_put_notify 服务提供此功能。 此服务将提供的应用程序通知函数注册到指定的信号灯。 只要放置了信号灯,ThreadX 就会调用此应用程序通知函数。 应用程序通知函数内的确切处理由应用程序决定;但这通常包括恢复相应的线程以处理新信号灯放置事件。

信号灯事件链接 (Semaphore Event chaining™)

ThreadX 中的通知功能可用于链接各种同步事件。 当单个线程必须处理多个同步事件时,这通常很有用。

例如,应用程序可以为每个对象注册一个通知例程,而不是为队列消息、事件标志和信号灯而挂起单独的线程。 在调用后,应用程序通知例程会恢复单个线程,该线程可以询问每个对象以查找并处理新事件。

通常情况下,“事件链”会减少线程、降低开销和减少 RAM 要求。 它还提供了一种非常灵活的机制来处理更复杂系统的同步要求。

运行时信号灯性能信息

ThreadX 提供可选的运行时信号灯性能信息。 如果 ThreadX 库和应用程序是在定义 TX_SEMAPHORE_ENABLE_PERFORMANCE_INFO 的情况下生成的,ThreadX 会累积以下信息。

整个系统的总数:

  • 信号灯放置数

  • 信号灯获取数

  • 信号灯获取挂起数

  • 信号灯获取超时数

每个信号灯的总数:

  • 信号灯放置数

  • 信号灯获取数

  • 信号灯获取挂起数

  • 信号灯获取超时数

此信息在运行时通过 tx_semaphore_performance_info_get 和 tx_semaphore_performance_system_info_get 服务提供。 信号灯性能信息在确定应用程序是否正常运行时非常有用。 此信息对于优化应用程序也很有用。 例如,“信号灯获取超时数”相对较高可能表明其他线程占用资源的时间太长。

信号灯控制块 TX_SEMAPHORE

每个计数信号灯的特征都可在其控制块中找到。 该控制块包含诸如当前的信号灯计数等信息。 此结构在 tx_api.h 文件中定义。

信号灯控制块可以位于内存中的任意位置,但最常见的是通过在任何函数的作用域外部定义该控件块来使其成为全局结构。

抱死

与用于互相排斥的信号灯相关的最有趣且最危险的缺陷之一是“抱死”。 抱死或“死锁”是指两个或多个线程在尝试获取归对方所有的信号灯时无限期挂起的情况。

这种情况最好用两个线程、两个信号灯的示例来说明。 假设第一个线程拥有第一个信号灯,第二个线程拥有第二个信号灯。 如果第一个线程尝试获取第二个信号灯,同时第二个线程尝试获取第一个信号灯,这两个线程就会进入死锁状态。 此外,如果这些线程永远保持挂起状态,与之关联的资源也会永久锁定。 图 8 说明了这一示例。

抱死(示例)

挂起线程的示例

图 8. 挂起线程的示例

对于实时系统,可以通过对线程获取信号灯的方式设置一些限制来防止抱死。 线程每次只能拥有一个信号灯。 或者,如果线程按照相同的顺序收集多个信号灯,则可以拥有这些信号灯。 在前面的示例中,如果第一个和第二个线程按顺序获取第一个和第二个信号灯,则可防止抱死。

提示

也可以使用与获取操作关联的挂起超时从抱死状态中恢复。

优先级反转

与互相排斥信号灯相关的另一个缺陷是优先级反转。 “线程优先级缺陷”更全面地讨论了本主题。

根本问题源自这种情况,即低优先级线程拥有较高优先级线程所需的信号灯。 这本身很正常。 但是,它们之间具有优先级的线程可能会导致优先级反转持续不确定的时间。 这种情况可通过以下方式处理:谨慎选择线程优先级,使用抢占式阀值,并将拥有该资源的线程的优先级暂时提升到高优先级线程的级别。

Mutexes

除了信号灯,ThreadX 还提供互斥对象。 互斥实质上是二进制信号灯,这意味着每次只有一个线程可以拥有一个互斥。 此外,同一个线程可能会在拥有互斥锁时多次成功执行互斥获取操作,准确来说是4,294,967,295 次。 互斥对象有两个操作:tx_mutex_get 和 tx_mutex_put。 获取操作获得不归另一个线程所有的互斥锁,而放置操作释放以前获得的互斥锁。 对于要释放互斥锁的线程,放置操作数必须等于先前的获取操作数。

每个互斥都是一个公用资源。 ThreadX 对如何使用互斥没有任何限制。

ThreadX 互斥仅用于互相排斥。 与计数信号灯不同,互斥锁不能作为事件通知的方法。

互斥锁互相排斥

与“计数信号灯”部分的讨论类似,互相排斥用于控制线程对特定应用程序区域(也称为关键部分或应用程序资源)的访问 。 如果可用,ThreadX 互斥的所有权计数就为 0。 线程获得互斥后,在互斥时每成功执行一次 Get 操作,所有权计数就会递增一次,每成功执行一次 Put 操作则会递减一次。

创建互斥

ThreadX 互斥由应用程序线程在初始化期间或运行时创建。 互斥的初始条件总是“可用”。在创建互斥时,也可以选择“优先级继承”。

线程挂起

当尝试对已归另一个线程所有的互斥执行获取操作时,应用程序线程可能会挂起。

当拥有线程执行了相同数量的获取操作后,将执行挂起线程的获取操作,为其提供互斥所有权,并恢复线程。 如果多个线程在同一互斥上挂起,这些线程将按照挂起的相同顺序 (FIFO) 恢复。

但是,如果在创建期间选择了互斥优先级继承,则会自动执行优先级恢复。 如果应用程序在取消线程挂起的互斥放置调用之前调用 tx_mutex_prioritize,还可以恢复优先级。 互斥设置优先级服务将优先级最高的线程置于挂起列表的前面,让所有其他挂起的线程采用相同的 FIFO 顺序。

运行时互斥性能信息

ThreadX 提供可选的运行时互斥性能信息。 如果 ThreadX 库和应用程序是在定义 TX_MUTEX_ENABLE_PERFORMANCE_INFO 的情况下生成的,ThreadX 会累积以下信息。

整个系统的总数:

  • 互斥锁放置数

  • 互斥锁获取数

  • 互斥锁获取挂起数

  • 互斥锁获取超时数

  • 互斥锁优先级倒置数

  • 互斥优先级继承数

每个互斥锁的总数:

  • 互斥锁放置数

  • 互斥锁获取数

  • 互斥锁获取挂起数

  • 互斥锁获取超时数

  • 互斥锁优先级倒置数

  • 互斥优先级继承数

此信息在运行时通过 tx_mutex_performance_info_get 和 tx_mutex_performance_system_info_get 服务提供。 互斥性能信息可用于确定应用程序是否正常运行。 此信息对于优化应用程序也很有用。 例如,“互斥获取超时数”相对较高可能表明其他线程占用资源的时间太长。

互斥控制块 TX_MUTEX

每个互斥锁的特征都可在其控制块中找到。 该控制块包含诸如当前互斥锁所有权计数以及拥有互斥锁的线程的指针等信息。 此结构在 tx_api.h 文件中定义。 互斥控制块可以位于内存中的任意位置,但最常见的是通过在任何函数的作用域外部定义该控件块来使其成为全局结构。

抱死

与互斥锁所有权相关的最有趣且最危险的缺陷之一是“抱死”。 抱死或死锁是指两个或多个线程在尝试获取归其他线程所有的互斥时无限期挂起的情况。 有关抱死及其补救措施的讨论也完全适用于互斥对象。

优先级反转

如前所述,与互相排斥相关的主要缺陷是优先级反转。 “线程优先级缺陷”更全面地讨论了本主题。

根本问题源自这种情况,即低优先级线程拥有较高优先级线程所需的信号灯。 这本身很正常。 但是,它们之间具有优先级的线程可能会导致优先级反转持续不确定的时间。 与前面讨论的信号灯不同,ThreadX 互斥对象具有可选的优先级继承。 优先级继承的基本思路是,优先级较低的线程将其优先级暂时提升为需要归低优先级线程所有的相同互斥的高优先级线程的优先级。 当优先级较低的线程释放互斥锁时,即会恢复为其原始优先级,并为优先级较高的线程提供互斥锁所有权。 此功能可将倒置量限制为拥有互斥锁的较低优先级线程的时间,以此来消除不确定的优先级倒置。 当然,本章前面讨论的用于处理不确定的优先级倒置的技巧也适用于互斥。

事件标志

事件标志为线程同步提供了强大的工具。 每个事件标志由一个位表示。 事件标志按照 32 个一组的形式排列。 线程可以同时对组中的所有 32 个事件标记执行操作。 事件由 tx_event_flags_set 设置,由 tx_event_flags_get 检索。

可通过在当前事件标志和新事件标志之间执行逻辑 AND/OR 运算来设置事件标志。 逻辑运算的类型(AND 或 OR)在 tx_event_flags_set 调用中指定。

可使用类似的逻辑选项来检索事件标志。 获取请求可以指定需要所有指定的事件标志(一个逻辑 AND)。

获取请求还可以指定任何指定的事件标志都满足该请求(一个逻辑 OR)。 与事件标志检索相关的逻辑运算类型在 tx_event_flags_get 调用中指定。

重要

如果 TX_OR_CLEAR 或 TX_AND_CLEAR 由该请求指定,则使用满足获取请求的事件标志(例如,设置为零)。

每个事件标志组都是一个公用资源。 ThreadX 对如何使用事件标志组没有任何限制。

创建事件标志组

事件标志组由应用程序线程在初始化期间或运行时创建。 创建事件标志组时,组中的所有事件标志均设置为零。 应用程序中事件标志组的数量没有限制。

线程挂起

尝试从组中获取事件标志的任意逻辑组合时,应用程序线程可能会挂起。 设置事件标志后,将检查所有挂起线程的获取请求。 所有现已包含所需事件标志的线程都会恢复。

注意

设置事件标志组的事件标志时,将检查该事件标志组中所有已挂起的线程。 当然,这会带来额外的开销。 因此,最好将使用同一事件标志组的线程数限制为合理的数量。

事件标志设置通知

某些应用程序可能会发现,在设置事件标志时收到通知十分有利。 ThreadX 通过 tx_event_flags_set_notify 服务提供此功能。 此服务提供的应用程序通知函数注册到指定的事件标志组。 只要在组中设置了事件标志,ThreadX 就会调用此应用程序通知函数。 应用程序通知函数内的确切处理由应用程序决定,但这通常包括恢复相应的线程以处理新事件标志。

事件标志事件链接 (Event Flags Event chaining™)

ThreadX 中的通知功能可用于“链接”各种同步事件。 当单个线程必须处理多个同步事件时,这通常很有用。

例如,应用程序可以为每个对象注册一个通知例程,而不是为队列消息、事件标志和信号灯而挂起单独的线程。 在调用后,应用程序通知例程会恢复单个线程,该线程可以询问每个对象以查找并处理新事件。

通常情况下,“事件链”会减少线程、降低开销和减少 RAM 要求。 它还提供了一种非常灵活的机制来处理更复杂系统的同步要求。

运行时事件标志性能信息

ThreadX 提供可选的运行时事件标志性能信息。 如果 ThreadX 库和应用程序是在定义 TX_EVENT_FLAGS_ENABLE_PERFORMANCE_INFO 的情况下生成的,ThreadX 会累积以下信息。

整个系统的总数:

  • 事件标志集数

  • 事件标志获取数

  • 事件标志获取挂起数

  • 事件标志获取超时数

每个事件标志组的总数:

  • 事件标志集数

  • 事件标志获取数

  • 事件标志获取挂起数

  • 事件标志获取超时数

此信息在运行时通过 tx_event_flags_performance_info_get 和 tx_event_flags_performance_system_info_get 服务提供。 事件标志的性能信息在确定应用程序是否正常运行时非常有用。 此信息对于优化应用程序也很有用。 例如,tx_event_flags_get 服务中的超时次数相对较多,可能表明事件标志挂起超时时间太短。

事件标志组控制块 TX_EVENT_FLAGS_GROUP

每个事件标志组的特征都可在其控制块中找到。 该控制块包含诸如当前事件标志设置和事件中挂起的线程数等信息。 此结构在 tx_api.h 文件中定义。

事件组控制块可以位于内存中的任意位置,但最常见的是通过在任何函数的作用域外部定义该控件块来使其成为全局结构。

内存块池

在实时应用程序中,采用快速且确定的方式分配内存始终是一项挑战。 考虑到这一点,ThreadX 提供了创建和管理多个固定大小的内存块池的功能。

由于内存块池由固定大小的块组成,因此永远不会出现任何碎片问题。 当然,碎片会导致出现本质上不确定的行为。 此外,分配和释放固定大小内存块所需的时间与简单的链接列表操作所需的时间相当。 另外,还可以在可用列表的开头完成内存块分配和取消分配。 这可以提供最快的链接列表处理速度,并且有助于将实际的内存块保存在缓存中。

缺乏灵活性是固定大小内存池的主要缺点。 池的块大小必须足够大,才能处理其用户最坏情况下的内存需求。 当然,如果对同一个池发出了许多大小不同的内存请求,则可能会浪费内存。 一种可能的解决方案是创建多个不同的内存块池,这些池包含不同大小的内存块。

每个内存块池都是一个公用资源。 ThreadX 对如何使用池没有任何限制。

创建内存块池

内存块池由应用程序线程在初始化期间或运行时创建。 应用程序中内存块池的数量没有限制。

内存块大小

如前所述,内存块池包含许多固定大小的块。 块大小(以字节为单位)在创建池时指定。

注意

ThreadX 为池中的每个内存块增加了少量开销(C 指针的大小)。 此外,ThreadX 可能需要填充块大小,从而确保每个内存块的开头能够正确对齐。

池容量

池中的内存块数是在创建过程中提供的内存区域的块大小和总字节数的函数。 池容量的计算方法是将块大小(包括填充和指针开销字节)除以提供的内存区域的总字节数。

池的内存区域

如前所述,块池的内存区域在创建时指定。 与 ThreadX 中的其他内存区域一样,该区域可以位于目标地址空间的任何位置。

这是一项重要的功能,因为它提供了相当大的灵活性。 例如,假设某个通信产品有一个用于 I/O 的高速内存区域。 将此内存区域设置为 ThreadX 内存块池,即可轻松对其进行管理。

线程挂起

在等待空池中的内存块时,应用程序线程可能会挂起。 当块返回到池时,将为挂起的线程提供此块,并恢复线程。

如果同一内存块池中挂起多个线程,这些线程将按挂起的顺序 (FIFO) 恢复。

不过,如果应用程序在取消线程挂起的块释放调用之前调用 tx_block_pool_prioritize,还可以恢复优先级。 块池设置优先级服务将优先级最高的线程置于挂起列表的前面,让所有其他挂起的线程采用相同的 FIFO 顺序。

运行时块池性能信息

ThreadX 提供可选的运行时块池性能信息。 如果 ThreadX 库和应用程序是在定义 TX_BLOCK_POOL_ENABLE_PERFORMANCE_INFO 的情况下生成的,ThreadX 会累积以下信息。

整个系统的总数:

  • 已分配的块数

  • 已释放的块数

  • 分配挂起数

  • 分配超时数

每个块池的总数:

  • 已分配的块数

  • 已释放的块数

  • 分配挂起数

  • 分配超时数

此信息在运行时通过 tx_block_pool_performance_info_get 和 tx_block_pool_performance_system_info_get 服务提供。 块池性能信息在确定应用程序是否正常运行时非常有用。 此信息对于优化应用程序也很有用。 例如,“分配挂起数”相对较高可能表明块池太小。

内存块池控制块 TX_BLOCK_POOL

每个内存块池的特征都可在其控制块中找到。 该控制块包含诸如可用的内存块数和内存池块大小等信息。 此结构在 tx_api.h 文件中定义。

池控制块也可以位于内存中的任意位置,但最常见的是通过在任何函数的作用域外部定义该控件块来使其成为全局结构。

覆盖内存块

务必确保已分配内存块的用户不会在其边界之外写入。 如果发生这种情况,则会损坏其相邻的内存区域(通常是后续区域)。 结果不可预测,且对于应用程序来说通常很严重。

内存字节池

ThreadX 内存字节池与标准 C 堆类似。 与标准 C 堆的不同之处在于,该内存字节池可以包含多个内存字节池。 此外,线程可在池中挂起,直到请求的内存可用为止。

内存字节池的分配与传统的 malloc 调用类似,其中包含所需的内存量(以字节为单位)。 内存采用“首次适应”的方式从池中分配;例如,使用满足请求的第一个可用内存块。 此块中多余的内存会转换为新块,并放回可用内存列表中。 此过程称为“碎片”。

相邻的可用内存块在后续的分配搜索过程中合并为一个足够大的可用内存块。 此过程称为碎片整理。

每个内存字节池都是一个公用资源。 除了不能从 ISR 调用内存字节服务之外,ThreadX 对如何使用池没有任何限制。

创建内存字节池

内存字节池由应用程序线程在初始化期间或运行时创建。 应用程序中内存字节池的数量没有限制。

池容量

内存字节池中可分配的字节数略小于创建期间指定的字节数。 这是因为可用内存区域的管理带来了一些开销。 池中的每个可用内存块都需要相当于两个 C 指针的开销。 此外,创建的池包含两个块:一个较大的可用块和在内存区域末端永久分配的一个较小的块。 这个分配块用于提高分配算法的性能。 这样就无需在合并期间持续检查池区域末端。

在运行时,池中的开销通常会增加。 如果分配奇数字节数,系统会加以填充,以确保正确对齐下一个内存块。 此外,随着池变得更加零碎,开销也会增加。

池的内存区域

内存字节池的内存区域在创建过程中指定。 与 ThreadX 中的其他内存区域一样,该区域可以位于目标地址空间的任何位置。 这是一项重要的功能,因为它提供了相当大的灵活性。 例如,如果目标硬件有高速内存区域和低速内存区域,用户可以通过在每个区域中创建池来管理这两个区域的内存分配。

线程挂起

在等待池中的内存字节时,应用程序线程可能会挂起。 当有足够的连续内存可用时,将为已挂起的线程提供其请求的内存,并且恢复线程。

如果同一内存字节池中挂起多个线程,则按这些线程挂起的顺序 (FIFO) 为其提供内存(恢复)。

不过,如果应用程序在信号灯发出取消线程挂起的字节释放调用之前调用 tx_byte_pool_prioritize,还可以恢复优先级。 字节池设置优先级服务将最高优先级的线程置于挂起列表的前面,让所有其他挂起的线程采用相同的 FIFO 顺序。

运行时字节池性能信息

ThreadX 提供可选的运行时字节池性能信息。 如果 ThreadX 库和应用程序是在定义 TX_BYTE_POOL_ENABLE_PERFORMANCE_INFO 的情况下生成的,ThreadX 会累积以下信息

整个系统的总数:

  • 分配数

  • 版本

  • 搜索的片段数

  • 合并的片段数

  • 创建的片段数

  • 分配挂起数

  • 分配超时数

每个字节池的总数:

  • 分配数

  • 版本

  • 搜索的片段数

  • 合并的片段数

  • 创建的片段数

  • 分配挂起数

  • 分配超时数

此信息在运行时通过 tx_byte_pool_performance_info_get 和 tx_byte_pool_performance_system_info_get 服务提供。 字节池性能信息在确定应用程序是否正常运行时非常有用。 此信息对于优化应用程序也很有用。 例如,“分配挂起数”相对较高可能表明字节池太小。

内存字节池控制块 TX_BYTE_POOL

每个内存字节池的特征都可在其控制块中找到。 该控制块包含诸如池中可用的字节数等有用的信息。 此结构在 tx_api.h 文件中定义。

池控制块也可以位于内存中的任意位置,但最常见的是通过在任何函数的作用域外部定义该控件块来使其成为全局结构。

非确定性行为

尽管内存字节池提供了最灵活的内存分配,但这些池也受一些非确定性行为的影响。 例如,内存字节池可能有 2,000 字节的可用内存,但可能无法满足 1,000 字节的分配请求。 这是因为无法保证有多少可用字节是连续的。 即使存在 1,000 字节可用块,也不能保证找到此块需要多长时间。 完全有可能需要搜索整个内存池来查找这个 1,000 字节块。

提示

由于内存字节池的不确定性行为,通常应避免在需要确定性实时行为的区域中使用内存字节服务。 许多应用程序在初始化或运行时配置期间预先分配其所需的内存。

覆盖内存块

务必确保已分配内存的用户不会在其边界之外写入。 如果发生这种情况,则会损坏其相邻的内存区域(通常是后续区域)。 结果不可预测,且对于程序执行来说通常是灾难性的。

应用程序计时器

快速响应异步外部事件是嵌入式实时应用程序中最重要的功能。 但是,其中的许多应用程序还必须按预定的时间间隔执行某些活动。

借助 ThreadX 应用程序计时器,应用程序能够按特定的时间间隔执行应用程序 C 函数。 应用程序计时器也可能只过期一次。 这种类型的计时器称为“单次计时器”,而重复间隔计时器称为“定期计时器”。

每个应用程序计时器都是一个公用资源。 ThreadX 对如何使用应用程序计时器没有任何限制。

计时器间隔

在 ThreadX 中,时间间隔通过定期计时器中断来测量。 每个计时器中断称为计时器时钟周期。 计时器时钟周期之间的实际时间由应用程序指定,但 10 毫秒是大多数实现的标准时间。 定期计时器设置通常位于 tx_initialize_low_level 程序集文件中。

值得一提的是,基础硬件必须能够生成定期中断,应用程序计时器才会正常运行。 在某些情况下,处理器具有内置的定期中断功能。 如果处理器没有此功能,用户的主板必须包含可生成定期中断的外围设备。

重要

即使没有定期中断源,ThreadX 仍可正常工作。 但随后会禁用所有与计时器相关的处理。 这包括时间切片、挂起超时和计时器服务。

计时器准确性

计时器过期时间根据时钟周期指定。 达到每个计时器时钟周期时,指定到期值将减一。 由于应用程序计时器可在计时器中断(或计时器时钟周期)之前启用,因此,实际过期时间可能会提前一个时钟周期。

如果计时器时钟周期速率为 10 毫秒,应用程序计时器可能会提前 10 毫秒过期。 与 1 秒计时器相比,这对 10 毫秒计时器更重要。 当然,增加计时器中断频率会减少此误差范围。

计时器执行

应用程序计时器按照其激活的顺序执行。 例如,如果创建了三个具有相同过期值的计时器并已激活,这些计时器对应的过期函数将保证按它们激活的顺序执行。

创建应用程序计时器

应用程序计时器由应用程序线程在初始化期间或运行时创建。 应用程序中应用程序计时器的数量没有限制。

运行时应用程序计时器性能信息

ThreadX 提供可选的运行时应用程序计时器性能信息。 如果 ThreadX 库和应用程序是在定义 TX_TIMER_ENABLE_PERFORMANCE_INFO 的情况下生成的,ThreadX 会累积以下信息。

整个系统的总数:

  • 激活数

  • 停用数

  • 重新激活(定期计时器)

  • expirations

  • 过期调整数

每个应用程序计时器的总数:

  • 激活数

  • 停用数

  • 重新激活(定期计时器)

  • expirations

  • 过期调整数

此信息在运行时通过 tx_timer_performance_info_get 和 tx_timer_performance_system_info_get 服务提供。 应用程序计时器性能信息在确定应用程序是否正常运行时非常有用。 此信息对于优化应用程序也很有用。

应用程序计时器控制块 TX_TIMER

每个应用程序计时器的特征都可在其控制块中找到。 该控制块包含诸如 32 位过期标识值等有用信息。 此结构在 tx_api.h 文件中定义。

应用程序计时器控制块可以位于内存中的任意位置,但最常见的是通过在任何函数的作用域外部定义该控件块来使其成为全局结构。

计时器过多

默认情况下,应用程序计时器在优先级为 0 时运行的隐藏系统线程中执行,该线程的优先级通常比任何应用程序线程都高。 因此,在应用程序计时器内进行处理应保持最小值。

如果可能,还应尽可能避免使用在每个时钟周期过期的计时器。 这种情况可能导致应用程序的开销过大。

重要

如前所述,应用程序计时器在隐藏的系统线程中执行。 因此,请不要在应用程序计时器的过期函数内执行任何 ThreadX 服务调用时选择挂起。

相对时间

除了前面所述的应用程序计时器,ThreadX 还提供单个连续递增的 32 位时钟周期计数器。 每次发生计时器中断时,时钟周期计数器或时间就会加一。

应用程序可以通过分别调用 tx_time_get 和 tx_time_set 来读取或设置此 32 位计数器。 此时钟周期计数器的使用完全由应用程序确定。 ThreadX 不在内部使用此计时器。

中断

快速响应异步事件是嵌入式实时应用程序的主函数。 应用程序知道此类事件是因为硬件中断而出现的。

中断是处理器执行中的异步更改。 发生中断时,中断处理器通常会将当前执行的一小部分保存在堆栈上,并将控制权转交给相应的中断向量。 中断向量基本上只是负责处理特定类型中断的例程的地址。 确切的中断处理过程特定于处理器。

中断控制

使用 tx_interrupt_control 服务,应用程序可以启用和禁用中断。 上一个中断启用/禁用状态由此服务返回。 值得一提的是,中断控制只会影响当前正在执行的程序段。 例如,如果某个线程禁用中断,这些中断仅在该线程执行期间保持禁用状态。

注意

不可屏蔽的中断 (NMI) 是无法通过硬件禁用的中断。 此类中断可供 ThreadX 应用程序使用。 但是,不允许应用程序的 NMI 处理例程使用 ThreadX 上下文管理或任何 API 服务。

ThreadX 托管中断

ThreadX 提供具有完整中断管理的应用程序。 此管理包括保存和还原中断执行的上下文。 此外,ThreadX 允许在中断服务例程 (ISR) 内调用特定服务。 下面是允许从应用程序 ISR 调用的 ThreadX 服务的列表。

tx_block_allocate
tx_block_pool_info_get tx_block_pool_prioritize
tx_block_pool_performance_info_get
tx_block_pool_performance_system_info_get tx_block_release
tx_byte_pool_info_get tx_byte_pool_performance_info_get
tx_byte_pool_performance_system_info_get
tx_byte_pool_prioritize tx_event_flags_info_get
tx_event_flags_get tx_event_flags_set
tx_event_flags_performance_info_get
tx_event_flags_performance_system_info_get
tx_event_flags_set_notify tx_interrupt_control
tx_mutex_performance_info_get
tx_mutex_performance_system_info_get tx_queue_front_send
tx_queue_info_get tx_queue_performance_info_get
tx_queue_performance_system_info_get tx_queue_prioritize
tx_queue_receive tx_queue_send tx_semaphore_get
tx_queue_send_notify tx_semaphore_ceiling_put
tx_semaphore_info_get tx_semaphore_performance_info_get
tx_semaphore_performance_system_info_get
tx_semaphore_prioritize tx_semaphore_put tx_thread_identify
tx_semaphore_put_notify tx_thread_entry_exit_notify
tx_thread_info_get tx_thread_resume
tx_thread_performance_info_get
tx_thread_performance_system_info_get
tx_thread_stack_error_notify tx_thread_wait_abort tx_time_get
tx_time_set tx_timer_activate tx_timer_change
tx_timer_deactivate tx_timer_info_get
tx_timer_performance_info_get
tx_timer_performance_system_info_get

重要

不允许从 ISR 中暂停。 因此,从 ISR 发出的所有 ThreadX 服务调用的 wait_option 参数必须设置为 TX_NO_WAIT。

ISR 模板

若要管理应用程序中断,必须在应用程序 ISR 的开头和结尾调用多个 ThreadX 实用程序。 中断处理的确切格式因端口而异。

以下小代码段是大多数 ThreadX 托管 ISR 的典型代码段。 在大多数情况下,此处理采用汇编语言。

_application_ISR_vector_entry:

; Save context and prepare for

; ThreadX use by calling the ISR

; entry function.

CALL _tx_thread_context_save

; The ISR can now call ThreadX

; services and its own C functions

; When the ISR is finished, context

; is restored (or thread preemption)

; by calling the context restore ; function. Control does not return!

JUMP _tx_thread_context_restore

高频中断

某些中断发生的频率很高,导致在每次中断时保存和还原完整上下文消耗过多的处理带宽。 在这种情况下,应用程序通常有一种小型汇编语言 ISR,用于对大多数此类高频中断执行一定量的处理。

在某个时间点之后,这种小型 ISR 可能需要与 ThreadX 交互。 这种交互通过调用上述模板所述的入口和出口函数来实现。

中断延迟

ThreadX 在短时间内锁定中断。 禁用中断的最大时间与保存或还原线程上下文所需的时间大致相同。