同步
原子操作的动机来自于在共享资源时需要同步两个或多个实体。一个简单的例子可能是基于多线程 RTOS 的系统中的两个线程,这两个线程都需要偶尔向它们共享的 UART 外围设备发送消息。
在操作中,一个线程可能会获得对 UART 的控制权并持有该控制权,直到其整个消息被发送。否则,如果另一个线程决定在第一个线程未完成时发送消息,这两个线程可能会通过 UART 发送替代字符,从而导致控制台端口出现一堆难以理解的乱码。
为了解决这个问题,我们可以定义一个资源,就像单个内存位置一样简单,它包含一个指示 UART 是忙还是可用的数据字。假设这个位置的 0 表示 UART 空闲,而 1 表示它正在使用中。
当线程 1 有消息要发送时,它会读取锁定字。它看到它是 0,所以它写一个 1 到这个位置。任何其他想要发送消息的线程都会发现这个锁或信号量包含一个 1,并且会等到它被清除后再尝试获取资源以供自己使用。
如果检查和设置锁的过程被中断,就会出现问题。
考虑以下情况:
任务 1 读取锁定字。
它找到包含零的单词。
任务 1 由于更高优先级的任务或导致其暂停的中断而停止。
当任务 1 被挂起时,任务 2 以更高的优先级运行。
任务 2 需要发送一条消息并读取它发现包含零的锁定字。
任务 1 将 1 写入锁定字,知道它现在拥有 UART 的唯一控制权,直到其消息被发送。
由于所需资源不可用,任务 2 释放控制权。
任务 1 继续运行,知道锁包含 0,将 1 写入锁字。
现在,任务 1 和任务 2 都确定它们对 UART 资源具有独特的控制权,并将交错地向其发送字符,因为他们看到它在逐个字符的基础上变得可用。
原子操作
解决这个问题需要一个原子操作,以确保检查和设置锁定字的完整事务在任何其他代理(即使具有更高优先级的代理)可以中断正在进行的关键操作之前完成。
原子操作只是以一个不间断的顺序完成的操作。即使这是一个复杂的事件序列。
对原子操作的支持内置于 C11 及更高版本等语言标准中。但是,用于在机器指令级别和硬件级别实现这一点的实际机制必须存在于 CPU 的指令集架构 (ISA) 和系统硬件实现中,以确保正确操作。
在上述情况下,任务 1 必须完成所有测试和设置锁的步骤,然后任何其他任务或实体才能访问内存中的锁字。
在单核系统中,这通常可以通过简单地在读取锁定字之前禁用所有中断并在写入锁定后重新启用它们来确保。通过这种方式,不会发生中断,因此不会导致内核在其测试和设置操作的中间抢占任务 1。
当然,在具有其他总线主控器的单核系统的情况下,例如 DMA 控制器和可以直接写入内存的外围设备,这可能会因这些其他总线主控器之一在错误的时间写入锁定值而被违反。但是,可以通过确保存储锁变量的内存不能被系统中的任何其他总线主控器访问来防止这种情况发生。
多核复杂性
现在我们看看在同一个芯片上有多个计算元素的情况,比如多核芯片,其中使用内存锁定来确保在不同内核上运行的线程也可以共享资源而不会相互破坏。
在这种情况下,我们无法阻止不同的总线主机访问锁定位置。这是因为锁定位置是每个内核必须能够读取和设置的位置,以确保正确共享公共资源(如上述 UART)。
现在我们必须确保一个核心一旦启动就可以完成其读-修改-写序列,在任何其他核心或总线主控器可以访问正在设置锁的内存位置之前。
在 Arm 架构的 8.1 版及更高版本中,为此添加了新的原子指令。我将把这个例子集中在新的指令上。一种这样的指令是 LDADD 指令及其变体。该指令从内存中读取一个值,将芯片上的一个寄存器中的值相加,然后将结果写回内存,同时保持内存总线,直到整个操作完成。
这样,系统可以保证没有其他总线主控器可以修改内存中的值,从而使两个主控器都认为他们拥有共享资源的所有权。
这段代码完成后,处理器可以检查读取的值,以验证它实际上是资源的唯一所有者,并且它的值对应于在操作开始之前可用的资源。
现实世界的影响
好消息是,如果您使用 RTOS 或操作系统在单核或多核线程环境中管理线程,这一切都在较低级别的系统代码中得到处理。然而,了解底层指令集和内存硬件必须设计为支持这些锁定机制以使这一切正常工作是有用的。如果这些机制设计不正确或被直接操纵寄存器误用,多个内核可能会无意中同时控制打算在使用时独占的资源。要调试这类情况,需要先进的多核调试功能,其中可以观察和控制在系统中的多个内核上运行的代码。
调试多核同步
多核调试器可以通过显示在多个内核或线程上运行的程序来帮助查找同步问题,以及根据另一个内核上的断点有选择地停止和启动内核的能力应该是确定这种机制问题的理想选择。
在图 1 中,我们可以在 IAR Embedded Workbench 中看到带有 4 个 CPU 的 NXP i.MX 8。所有核心都可以单独启动和停止。
图 1:每个内核独立的调试器控制。(来源:IAR 系统)
图 2 显示了在不同 CPU 上运行的代码中使用多个断点并结合使用互斥锁(Arm 提供的示例):_mutex_acquire() 和 _mutex_release(),设置标志以阻止所使用对象的在素数计算中。
图 2:在单个内核中使用互斥锁和断点。(来源:IAR 系统)
最常见的错误之一是滥用或未使用交叉触发接口 (CTI)。对于 Arm,CoreSight 交叉触发接口 (CTI) 通过交叉触发矩阵 (CTM) 连接到每个内核。CTI 使调试逻辑、ETM 跟踪单元和 PMU 能够相互交互并与其他 CoreSight 组件交互。这使得每个核心可以独立地停止和重置。不得不操纵“自制”的 CTI 变通方法,手动控制和停止内核,也许动态使用宏是一项不可能完成的任务。默认情况下,这应该并且需要由来自探针(CTI 接口信号)和软件调试端的良好调试器处理。图 3 显示了完全控制 CTI 的用例。
图 3:使用交叉触发接口 (CTI) 进行完全控制。(来源:IAR 系统)
一旦全部结合在一起,具有多核支持的调试器就可以在非对称和对称场景中控制内核,甚至可以组合在一起。图 4 显示了运行 4 个 Cortex-A53 和 1 个 Cortex-M4 的 NXP i.MX 8 设备。MCU 和 MPU 可以独立停止、监控和控制。虽然所有 4 个 Cortex-A53 内核或单个内核都从主会话运行,但可以在 Cortex-M4 合作伙伴端设置断点,并专注于可能正在运行整个设备的安全监视器的此应用程序。
图 4:在配备 4 个 Cortex-A53 和 1 个 Cortex-M4 的 NXP i.MX 8 设备上运行的多核会话。(来源:IAR 系统)
在应用程序中使用并行性和并发性旨在更有效地使用可用内核。然而,它的代价是增加了应用程序的复杂性,以及如何将源代码拆分成更小的部分以尽可能高效地运行。
概括
在单个芯片或系统中同步多个内核需要原子操作和执行这些操作的硬件。首次开发这种硬件/软件组合时,支持多核调试和观察的全功能调试器对于发现此类系统的问题至关重要。无法想象如何通过在整个代码中使用 print 语句来实现相同的控制,并使所有内容完美同步。每个开发人员都应该得到一个可以处理多核并完全控制所有线程的调试解决方案。IAR Embedded Workbench 及其调试器功能提供了这样一个工具,在开发和调试这些复杂系统时非常有用。
Aaron Bauch是IAR Systems的一名高级现场应用工程师,与美国东部和加拿大的客户合作。Aaron 曾为英特尔、Analog Devices 和 Digital Equipment Corporation 等公司从事嵌入式系统和软件方面的工作。他的设计涵盖了广泛的应用,包括医疗仪器、导航和银行系统。Aaron 还以南新罕布什尔大学教授的身份教授了许多大学水平的课程,包括嵌入式系统设计。Bauch 先生拥有纽约州纽约市 The Cooper Union 的电气工程学士学位和哥伦比亚大学的电气工程硕士学位。
全部0条评论
快来发表一下你的评论吧 !