软件 IC

作者:Robert D. Findlay

软件工程师应该向他们的硬件同行学习管理复杂性的方法。在集成电路出现之前,电子电路通常是复杂的构造,具有许多相互连接的分立元件。电路的复杂性是可见的,难以管理,并且对产品的成本产生不利影响。随着 IC 的出现,大部分复杂性被隐藏在芯片内部。将复杂功能设计到产品中的工作变得容易得多。

尽管多年来进行了许多尝试,但软件设计从未能够复制这种 IC 设计范例。面向对象编程语言和工具的出现本应解决其中一些问题。虽然面向对象设计在 GUI 编程等领域提供了一些显着的改进,但它并不总是能很好地隐藏复杂性。事实上,它们通常只是将复杂性转移到软件开发链中的其他领域,例如测试、工具集、类库设计或学习曲线。

现代硬件设计需要复杂的技能组合。然而,IC 的出现所做的是允许硬件设计师在产品设计中使用给定的芯片,而无需了解芯片本身内部电路的确切物理原理和布局。只要设计师遵守芯片外部接口(引脚)的规范,芯片就会以非常可预测的方式做出反应。这就是复杂性封装所提供的:复杂性隐藏和可预测/可重复的行为。软件设计中的对象提供了部分复杂性隐藏,但软件设计师通常仍然必须掌握一种复杂的语言(例如 C++),才能将对象连接到产品中。对象在提供可预测和可重复的软件行为方面非常差。此外,对象本身的语言通常决定了用于“连接它们”的语言。我们认为,真正的复杂性封装在一个普遍简单且可扩展的 API 背后,才是生产软件 IC 所需要的。软件设计师不应该必须掌握复杂的面向对象语言和工具集,才能“连接”这些软件 IC。至少,软件设计师应该能够选择独立于芯片语言的连接语言。

进程封装

许多 RTOS 都率先使用用户空间进程作为封装方案。QNX TM (http://www.qnx.com/) 是最早使用此方案的系统之一。自 1980 年以来,QNX 发布了一系列基于一组协作进程的创新操作系统,所有进程都使用发送/接收/回复消息传递范例。QNX 的内核设计方法与 Linux 中使用的方法大相径庭,我们不想重新燃起臭名昭著的微内核与单内核之争。可以说,我们认为 QNX 首创的进程模型和发送/接收/回复消息传递范例为成功的软件 IC 提供了关键要素。

在大多数现代操作系统(包括 Linux)中,进程都是非常强大的计算实体。当进程在处理器上运行时,它所包含的资源会受到多层保护。两个进程很难无意中相互产生不利影响。如果给定进程遇到致命错误并崩溃,则很少会危及整个系统。因此,Linux 中的用户空间进程为我们的软件 IC 提供了出色的容器。现在我们所需要的只是引脚的软件模拟。这就是消息传递范例的用武之地。

令牌化消息传递

最强大的消息格式之一也是最简单的格式之一。我们可以将消息简单地视为字节集合。此字节集合分为两个部分。第一个字节字段的长度是固定的,始终存在于每条消息中,这些字节共同表示一个唯一的消息标识符,称为令牌。消息的其余部分(长度可变)表示令牌上下文相关数据。当两个进程希望交换此类消息时,它们只需就令牌方案和它们想要交换的每条令牌化消息的格式达成一致。进程间消息传输层根本不需要关心消息内容。

Software ICs

图 1. 令牌化

消息同步

任何软件设计的目标之一是生产在给定刺激下表现出可预测性和可重复性的软件。许多面向对象和面向消息的设计范例都存在缺陷,因为它们在软件产品中引入了一定程度的不可预测性和随机性。如果使用邮箱方案交换消息,并且发送方和接收方永远不同步,则将极其难以复制或预测两个进程系统可能处于的所有可能状态。这很大程度上取决于这两个进程所处的环境和时间考虑因素。这些模块可能在一个环境中运行良好,但在另一个环境中会发生零星故障。这导致测试和维护成本增加,以及软件产品质量下降。通常,责任被错误地指向多进程设计范例。我们多久听到有人说客户端/服务器软件设计不能很好地处理复杂性。发送/接收/回复同步消息传递的最初设计者认为,答案在于强制在每次消息传递时发生类似状态机的同步。

它的工作原理如下:发送进程编写一条消息并安排将其发送到接收进程。在等待回复时,发送进程被阻塞。另一方面,接收进程一直处于阻塞状态,直到收到发往它的消息。它解除阻塞,读取消息,处理其内容,然后回复发送方。此回复然后解除发送方的阻塞,两个进程再次可以独立运行。

许多人认为,这种阻塞/强制同步会给消息交换引入不必要的复杂性,但当正确应用时,它恰恰会产生相反的效果。通过强制在每次消息传递时发生同步,人们发现我们的多进程应用程序现在开始以非常可预测和可重复的方式运行。导致非同步消息传递方案经常遭受困扰的时间和环境影响消失了。在处理庞大而复杂的应用程序时,这代表着巨大的战略优势。作为额外的好处,由于发送方在消息传输期间被阻塞,并且由接收方进程通过回复显式地解除阻塞,因此可以很容易地安排通过各种媒体(包括一些“慢速”媒体)传输这些消息。如果进程在同一处理器上,则可以通过共享内存交换消息,如果进程在物理上是分离的,或者在拨号情况下的串行线上,则可以通过 Internet 交换消息。虽然进程集合的性能显然会受到消息传输速度的影响,但该性能的可预测性和可重复性不会受到影响。

在软件测试方面,软件可预测性的重要性怎么强调都不为过。没有什么比一个表现出不可预测和不可重复行为的应用程序更让软件 QA 人员渴望转行的了。

SIMPL 开源项目

为了帮助在 Linux 社区中推广这种软件 IC 范例,我们启动了 SIMPL 开源项目 (www.holoweb.net/~simpl)。启用 SIMPL 的进程是 Linux 进程,具有所有功能和保护。启用 SIMPL 的进程能够使用阻塞的发送/接收/回复消息传递方案交换令牌化二进制消息。简而言之,SIMPL 进程具有成为出色软件 IC 的潜力。

在更详细地介绍如何构建这些软件 IC 之前,值得强调此模型的优势。

  • 原则上,SIMPL 进程可以用任何语言编写。虽然 SIMPL 网站上的大部分代码仍然是用 C 语言编写的,但没有理由不能创建 C++ 或 JAVA SIMPL 进程,并与另一个用 Tcl/Tk 或 Python 编写的 SIMPL 进程透明地通信。

  • 用于编写软件 IC 本身的语言绝不会决定与之交互的另一个软件 IC 的语言。此外,给定的 SIMPL 进程无法发现用于构建与之交换消息的另一个 IC 的语言。

  • SIMPL 软件 IC 无法发现或知道其交换伙伴的物理位置。这意味着可以使用本地消息交换来测试相同的二进制映像,然后使用远程消息交换进行部署。总体应用程序性能会有所不同,但单个软件 IC 不需要以任何方式更改。在许多情况下,甚至不需要重新编译。

  • SIMPL 软件 IC 无法发现交换伙伴的内部算法。这意味着可以创建测试桩,完全模拟和复制给定软件 IC 所处的环境。特别是,在完整系统中难以或昂贵地重现的错误条件可以在测试环境中轻松模拟。这些 SIMPL 软件 IC 可以在部署到实际应用之前进行严格的测试。

  • SIMPL 软件 IC 非常适合有多个开发人员的项目。SIMPL 启用进程的实现细节不会影响任何交互进程,前提是实现符合约定的消息 API。虽然糟糕的 IC 实现肯定会对总体应用程序性能产生不利影响,但应用程序仍将运行。一旦确定了糟糕的算法,就可以在与实际应用程序隔离的情况下进行处理,并在测试后替换,甚至无需重新编译相邻的 IC。

基本构建块

最基本的 SIMPL 进程类型是

  • 发送器——那些编写消息并等待回复的进程

  • 接收器——那些等待消息并编写回复的进程

清单 1

清单 2

本节中讨论的所有软件 IC 都将是这两种基本构建元素的复合类型或特殊类型。SIMPL 项目受 LGPL 或类似的开放许可证管辖。以下讨论的软件 IC 的所有源代码都可在 SIMPL 网站上获得。

各种软件 IC

模拟器 SIMPL 范例的最大优势之一是它允许轻松开发测试桩。我们针对这些测试桩进程采用了以下命名约定。

  • 接收器进程的桩称为“模拟器”

  • 发送器进程的桩称为“刺激器”。典型的模拟器设置可能如下所示

Software ICs

图 2. 模拟器

此处正在测试的项目是发送器。理解模拟器的关键在于理解以下事实:只要模拟器符合发送器预期的 SIMPL 命名约定,并符合发送器可以交换的所有预期消息格式,发送器将无法检测到它正在与测试桩通信。这样,发送器进程可以在非常真实的测试“沙箱”中进行严格的测试,而无需以任何方式更改已部署的可执行文件。无需条件编译、测试标志等,这些在非 SIMPL 设计中的单元测试场景中很常见。一旦测试完成,发送器可执行文件就可以按原样部署在最终应用程序中。

模拟器代码的确切组成高度依赖于应用程序。上面的图表说明了一个典型的场景,在该场景中,人们希望能够通过键盘命令直接与模拟器进行交互。此外,罐头回复正从数据文件中馈入。

人们可以想象更复杂的模拟器,其中整个测试序列以高度可控的方式从数据文件中计量输入。

刺激器

当需要单元测试的对象是接收器进程时,通常会使用刺激器来替换测试阶段的真实发送器。

Software ICs

图 3. 刺激器

此处正在测试的项目是接收器。与上面的模拟器的情况一样,这里的关键是,只要刺激器符合所有消息传递和命名约定,接收器进程将无法知道它是从刺激器还是从最终应用程序中的真实发送器接收消息。

与模拟器示例中的发送器进程一样,此处正在测试的接收器在所有方面都可以是最终可部署的可执行文件。在 SIMPL 范例中,再次不需要条件编译或其他可执行文件更改技术。

与模拟器一样,典型的刺激器包含一个键盘接口,供测试人员与之交互。更复杂的刺激器可能会从数据文件中馈入测试输入。

在 SIMPL 应用程序中测试可部署的可执行文件的重要性怎么强调都不为过。根据我们的经验,这是在设计软件应用程序时考虑 SIMPL 范例的最重要原因之一。

中继器

在 SIMPL 系统中,所有进程都有名称。在最简单的系统中,名称通常在命令行上作为启动信息的一部分分配给进程(并传递给其他进程)。有时希望简化需要传递到代码中的名称信息量。称为中继器的构造对于此类事情很有用。

Software ICs

图 4. 中继器

基本中继器操作非常容易掌握。发送器认为中继器进程是其消息的预期接收器。它执行所有正常的名称定位和发送操作,就像它是简单的发送器/接收器对的一部分一样。另一方面,中继器进程根本不处理消息。它只是记住发送器的 ID,然后将该 ID 转发到注册的接收器进程。当接收器收到中继的消息时,它连接到发送器进程的共享内存块并以正常方式检索消息。消息处理完毕后,接收器将回复放置在发送器的回复区域中,并将发送器 ID 回复给中继器进程。然后,中继器进程只需以正常方式回复发送器。请注意,由于 SIMPL 架构,中继器进程在这些活动期间永远不需要复制消息。

与基本发送器/接收器配对相比,此构造的优势在于可以隐藏接收器的名称。也可以在此方案中动态启动和停止接收器,而无需将命名信息重新传递给系统中的各个发送器。如果发生这种情况,则启动消息交换(在上面的图表中称为注册)会负责通知中继器任务新的接收器的名称信息。

动态启动和停止进程而无需循环整个应用程序的能力可能是一个显着的优势,特别是当接收器逻辑正在进行频繁的升级或增强时。这些可以动态地推出,如果出现问题,原始副本可以快速回滚到运行状态。实际上,通过注册方案,两个接收器进程都可以运行,并且快速消息交换将具有将消息“路由”到新接收器(或再次返回)的效果。通过注册方案,有问题的接收器可以覆盖现有注册。虽然有些人可能会将此类事情视为潜在的安全漏洞,但它仅对有权在系统上运行新进程的人员开放。如果这是一个问题,则相对容易在注册过程之上构建证书类型的检查,以大大缩小此漏洞。

显然,与直接消息交换相比,中继器会产生性能损失,但在许多情况下,该构造带来的优势大于劣势。中继器是一个强大的 SIMPL 构造。

代理

除了中继器构造的名称隐藏功能外,有时还希望对消息及其排序施加更大的控制。代理是用于这些类型应用程序的有用构造。

Software ICs

图 5. 代理

要理解代理,需要理解回复阻塞的概念。在正常的 SIMPL 消息交换中,接收器被接收阻塞。一旦发送器发送消息,则称其为回复阻塞。代理构造的关键在于接收器不需要立即回复该特定发送器。它可以简单地记住 ID 并继续处理其业务。实际上,接收器可以“保持”发送器等待并返回到被接收阻塞状态以接收新消息。当通过来自第二个发送器的消息收到新信息时,接收器可以选择使用其先前记住的 ID 回复原始发送器。查看回复阻塞发送器的另一种方法是将其视为不阻塞其“发送器”的“接收器”。

为了避免一些语义混淆,我们采用了如上图所示的代理进程的命名约定。请求者只是普通发送器的另一个名称。就此进程而言,消息的预期接收器是代理本身。然而,代理进程对实际消息内容完全中立。它只是要充当请求者消息的“存储和转发”。重要的是要注意,对于基本 SIMPL 包,所有“发送器”类型的进程都将其消息放置在它们拥有和控制的共享内存块中。实际消息不需要由接收器从发送器的缓冲区中复制出来,而是可以通过链接到共享内存区域直接读取。代理构造利用了这一事实。当请求者向代理发送消息时,代理不会将消息复制到任何地方。它只是记录请求者的 ID 并执行以下两件事之一

  • 将请求者 ID 排队

  • 将请求者 ID 转发到代理进程

此方案中的代理包含此应用程序的所有正常“接收器”逻辑。但是,它作为回复阻塞发送器工作。在其最简单的形式中,代理以三种不同的消息格式与代理通信

  • WHAT_YA_GOT 请求任何排队的消息

  • 代理将回复 GOT_ONE 请求者消息供其处理

  • 代理将通过发送 AGENT_REPLY 消息来响应,其中包含发往请求者的已处理回复

这一切看起来都很复杂,但实际上非常简单。想象以下场景。代理和代理正在运行。代理定位代理,然后说 WHAT_YA_GOT。此时,由于请求者尚未发送任何消息,因此代理只是记录代理的 ID 并使其保持回复阻塞状态。此时,假设请求者生成一条消息并将其发送给代理。代理接收消息并注意到代理已准备好处理它。它只是通过 SIMPL 回复将请求者的 ID 中继到代理,并使请求者保持回复阻塞状态。然后,代理可以自由地处理请求者的消息,而请求者以正常方式保持回复阻塞状态。最终,代理将提出对请求者消息的“响应”。它将其响应包装在其 AGENT_REPLY 消息中,并将其发送给代理。此包装消息的一部分包含请求者进程的 ID。然后,代理解开消息包装,将其回复给请求者,并使代理保持回复阻塞状态,等待下一个请求。

对于请求者来说,这一切都像它一直在向基本接收器发送任何 SIMPL 消息一样。实际上,在处理代理的请求者代码中没有任何区别。那么为什么要费这么大周折呢?

首先,现在可以在此系统中动态启动和停止代理进程,而不会影响请求者(除了延迟对代理循环时到达的请求的响应)。在代理可能正在进行重大修订或升级的系统中,这可能是一个明显的优势。

其次,此系统中的请求者无需知道代理的名称即可与之交换消息。代理构造可以被视为消息网关。

要理解进一步的优势,我们需要检查我们可能有多个请求者都与同一代理和代理通信的情况。在这种情况下,代理实际上将接收所有请求者的消息,并将请求者的发起者 ID 排队。然后,代理逻辑可以控制将这些消息调度到代理的顺序。在正常的发送器/接收器配对中,FIFO 强制执行先进先出排序,并且不可能让优先级较高的消息在队列中提前。在代理方案中,这是非常有可能的。

此外,在正常的 SIMPL 发送器/接收器配对中,消息传递是同步的。有意地很难通过除让接收器执行回复之外的任何其他方式将发送器踢出回复阻塞状态。这意味着超时或“过时数据”等事情很难处理。代理方案使这些事情相对容易管理。当消息在代理队列中挂起时,可以定期启动代理来检查这些消息是否超时或老化。

与基本发送器/接收器对相比,代理构造将遭受性能损失,因为在每个事务中至少需要交换两条额外的消息。然而,代理构造是一个强大的构造,可以在某些设计中发挥巨大优势。

信使

有时在设计中,两个接收器进程需要交换消息。下面说明的信使构造是满足此要求的良好方法。

Software ICs

图 6. 信使

一个典型的例子将涉及用户界面进程。通常,用户界面(无论是简单的基于文本的交互还是 GUI)都希望成为接收器类型的进程。您不经常希望用户界面在发送时被阻塞。在这些设计中,用户界面 (UI) 经常需要来自另一个接收器进程的信息。如果您继续在 UI 中编码阻塞发送,那么您可能会在 UI 的操作中找到一个位置,在该位置,界面会在请求得到服务时冻结。这可能不是期望的行为。

信使构造利用了上面代理构造中说明的延迟回复概念。在我们的讨论中,我们将假设 UI 进程是“receiver1”,而接收者进程是“receiver2”。当信使进程启动时,它做的第一件事是定位它指定的要服务的 UI 进程。一旦定位,信使将向该进程发送注册类型的消息,指示它已准备好采取行动。UI 进程将简单地注意到信使可用但不回复,从而使信使保持回复阻塞状态。在 UI 中需要完成对 receiver2 进程的异步请求的点,会通过信使编写和发送(回复)消息。现在信使被解除阻塞,并继续定位消息并使用阻塞发送将其转发到 receiver2 进程。此时,信使在 receiver2 上回复阻塞,并且 UI 完全可以自由地执行其逻辑允许的其他操作。当 receiver2 回复信使时,信使只需使用阻塞发送将该回复转发到 UI 进程,并再次在 UI 上变为回复阻塞状态。UI 以正常方式接收此消息,注意到它来自信使,标记信使再次可用,并根据编码的逻辑处理消息。

上面描述的简单信使是单请求版本。如果在信使返回其第一个响应之前,在 UI 中生成了第二个旨在用于 receiver2 进程的 UI 请求,则该请求将被拒绝,并指出“信使繁忙”。对此单请求逻辑的简单增强是在 UI 中具有单消息排队功能。“信使繁忙”响应仅在收到原始响应之前尝试第三个 UI 请求时才会出现。在大多数 UI 进程中,此单消息队列已足够。可以轻松构建更大的队列深度算法,但是对此的需求通常表明其他地方的 UI 设计很差。

信使模型的另一种变体是让父进程按需派生信使。在某些情况下,此功能比让信使与 GUI 进程一起预先启动更可取。Web 小程序类型的 GUI 应用程序是需要此信使生成技术的示例。

特别是在用户界面设计中,信使构造确实是非常有用的 SIMPL 构建块。

广播器

在设计中,有时需要一对多的发送器/接收器关系。对于简单的情况,可以简单地让发送器定位所有预期的接收者,并循环发送给每个接收者。在更复杂的设计中,下面说明的广播器构造非常强大。

Software ICs

图 7. 广播器

广播器实际上由两部分组成:接收器和发送器。我们将发送器部分称为广播器。接收器通常是消息队列,我们稍后将看到。它的工作方式如下:队列负责消息排队和排序。广播器维护要发送到的进程列表。

一个典型的序列可能从接收器(例如 receiver1)决定它希望接收广播消息开始。作为该序列的一部分,它向广播器的队列进程发送注册类型的消息。然后,队列会将 REGISTRATION 类型的消息放置在其内部队列上。同时,广播器通过向下发送消息到队列进程,询问是否有任何新的消息排队,从而从其广播序列之一返回。在此示例中,receiver1 的 REGISTRATION 消息作为回复传递给广播器。当广播器进程检测到该消息是新的 REGISTRATION 时,它会对该接收者(本例中为 receiver1)执行 nameLocate,并将 ID 存储在其内部广播列表中。它向队列进程发送确认消息,然后队列进程继续回复并解除原始接收器(receiver1-暂时是发送器)的阻塞。如果内部队列上没有更多消息,则广播器在此阶段将保持回复阻塞状态。此时,发送器可以向广播器的队列进程发送一条旨在广播的消息。通常,队列会将消息排队并立即回复发送器,但是可以执行类似于注册过程的阻塞发送方案。如果队列检测到广播器处于回复阻塞状态,它会立即通过回复将消息转发给广播器。一旦广播器收到消息,它就会注意到这不是注册,因此是要发送给其广播列表中所有注册接收者的消息。一旦此系列发送完成,广播器将发送回队列以获取下一条消息,并且该过程重复进行。

当接收者希望取消其在广播器中的注册时,它只需使用 DEREGISTER 消息重复注册过程即可。队列通常只需将此请求排队并确认。

如果接收者“忘记”注销并简单地消失,则下一次广播尝试将检测到该情况,并且广播器将继续从其内部广播列表中删除该 ID。

广播器构造是一个非常强大的 SIMPL 工具。其用途的一个典型示例是将 GUI 小程序的多个实例与相同的信息同步。

结论

SIMPL 范例为 Linux 开发人员的工具集添加了一些重要的工具。凭借其以进程为中心的封装模型,以及阻塞的发送/接收/回复消息传递,SIMPL 库成为开发软件 IC 的绝佳平台。我们认为,这种软件开发范例的优势是显着且具有成本效益的。

这些软件 IC 的所有源代码都可在 SIMPL 网站上获得。虽然这些 IC 的这种源代码交付方法作为“播种思想”的手段是有效的,但这并不意味着 SIMPL IC 必须以源代码格式交付。SIMPL 项目使用的 LGPL 许可证中没有任何内容阻止软件 IC 以二进制格式交付。

这开启了软件设计的一个激动人心的新时代。在管理项目复杂性方面,软件设计师最终可以与他们的硬件同行相提并论。

资源

Robert D. Findlay (fcsoft@attcanada.ca) 参与软件开发已有 20 多年。虽然过去 15 年的许多项目都涉及 QNX 和各种 UNIX 系统,但过去两年一直专注于 LINUX。在他无休止地寻求“一定有更简单的方法”的过程中,他于五年前共同创立了 FCsoftware。当不为维持业务运转而奔波时,他喜欢与妻子 Gloria 和他们的两只大型犬在他们建于 1860 年的乡村住宅中度过时光。

加载 Disqus 评论