46、支持软件容错的增量恢复缓存技术解析

支持软件容错的增量恢复缓存技术解析

1. 恢复缓存(Recovery Cache)

恢复缓存是一种用于支持软件容错的重要机制。具体的私有类型 Recovery_Cache 扩展了 Recovery_Point 包导出的抽象类型 Recovery_Data Establish Restore Discard 这几个过程是基本恢复接口的具体实现。

恢复缓存本质上是恢复区域的集合。由于恢复缓存设施主要用于恢复块,其中恢复区域是严格嵌套的,因此使用栈来管理这些区域是合适的。在私有部分的完整声明是一个包含栈的记录扩展。每个栈元素包含一系列缓存条目,每个条目是存储值和对相应可恢复应用对象的引用的组合。由于支持的可恢复对象数量没有上限,序列用列表表示。这种表示方式(缓存条目列表的栈)与 Anderson 和 Lee 以及 Flaviu Cristian 引入的设施类似。为了实现终结处理,私有部分的完整类型被声明为 Limited_Controlled 的扩展。

2. 可恢复应用类型(Recoverable Application Types)

具体实现导出了一个名为 Recoverable 的类型,用于定义可恢复应用类型的接口。其目的是通过对 Recoverable 进行扩展,支持任意应用定义类型的对象的状态保存和恢复。由于这些扩展将在 Recoverable 的派生类中,调度机制允许实现操作应用定义类型的对象,而无需对它们有具体的了解。这是面向对象编程中抽象类型、继承和动态调度的经典应用。

应用程序可以根据需要定义任意数量的 Recoverable 抽象类型的不同扩展。每当对指定的 Recovery_Cache 对象调用 Establish Restore Discard 时,所有指定该 Recovery_Cache 对象的(从 Recoverable 派生的类型的)对象都可以被操作。程序员在详细说明可恢复应用对象时,通过判别式永久指定一个 Recovery_Cache 对象。 Recoverable 的所有后代都会继承该判别式,并且所有此类对象都必须指定其 Recovery_Cache 对象。

Recoverable 类型在私有部分被完全定义为 Controlled 类型的后代,因此它具有用户定义赋值的功能。应用程序定义的具体后代将继承我们包中定义的赋值语义。

3. 特定于实现的组件(Implementation - Specific Components)

Recovery_Cache Recoverable 类型被定义为具有实现所需组件的类型扩展。与 Recoverable 类型相关的组件给用户定义赋值带来了困难。

从性能角度来看,与列表栈的逻辑表示的一个差异是根本性的。由于恢复相对不常使用,正常执行必须优先于恢复设施进行优化。因此,增量赋值语句必须尽可能高效。在使用恢复时,恢复点被丢弃的频率比恢复的频率更高,所以 Discard 过程也必须尽可能高效。这两种情况的主要困难在于需要搜索无界的恢复区域列表。赋值的实现必须确定一个条目是否已经被放置在当前恢复区域中。对于当前区域中的每个条目, Discard 过程的实现还必须检查前一个区域列表,以确定该条目是否存在于那里。为了避免这些搜索,我们使用 Anderson 和 Lee 描述的方法之一,额外消耗一些存储。

具体来说,恢复区域与对应于其在栈中深度的编号级别相关联;每个 Recovery_Cache 对象有一个专用值表示该缓存的当前级别。每个可恢复应用对象有一个关联的数值,表示该对象最近的缓存条目所在的恢复区域。赋值实现会比较应用对象的级别和关联的 Recovery_Cache 对象的级别,以确定是否需要新的缓存条目。同样,在丢弃当前区域之前, Discard 过程会比较这些字段,以确定当前区域的任何条目是否应合并到前一个区域。因此,每个 Recovery_Cache 对象有一个数值 Current_Level 组件,每个可恢复应用对象有一个名为 Max_Region_Entered 的数值。

另一个组件处理 Recoverable Recovery_Cache 对象生命周期的差异。 Restore 过程遍历栈顶的恢复区域列表,以恢复相应可恢复应用对象的最近缓存值。然而,只有当我们知道相应的应用对象仍然存在时,才能恢复这些值。由于对象可以在嵌套的声明区域中声明,并且分配的对象可以手动释放,可能会出现悬空引用。后续调用 Restore 过程可能会尝试使用这些无效引用,产生不可预测的影响。为了避免解引用任何悬空引用,我们为每个可恢复应用对象关联一个布尔标志 Known_Valid 。每个 Known_Valid 标志指示对象是否已知存在。 Known_Valid False 的对象不会被恢复。

下面是相关组件的关系图:

graph LR
    classDef process fill:#E5F6FF,stroke:#73A6FF,stroke-width:2px

    A(Recoverable):::process --> B(Reference):::process
    A --> C(Known_Valid):::process
    A --> D(Max_Region_Entered):::process
    A --> E(Prior_Values Stack):::process
    A --> F(Descriptor):::process
    F --> G(Recovery_Object):::process
    F --> H(Application Extension):::process
    I(Recovery_Cache):::process --> J(Descriptor List):::process
    I --> K(Known_Objects Map):::process
    I --> L(Current_Level):::process
    I --> M(Active_Regions Stack):::process
4. 赋值(Assignment)

在对给定的 Recovery_Cache 对象调用 Establish 之后,对关联的可恢复对象的第一次赋值将在获取新值之前存储当前值。我们使用 Ada 的用户定义赋值来实现这一效果。

在 Ada 中,对于 Controlled 类型的后代,可以通过重写 Adjust 过程以及根据情况重写 Finalize 过程来间接重新定义赋值操作。在典型实现中,当一个受控类型的对象是赋值语句的目标时,首先会调用 Finalize 过程,并将该对象作为参数传递。然后,将右侧值的逐位副本复制到目标对象中。最后,调用 Adjust 过程,并将目标对象作为参数传递,以完成赋值效果。需要注意的是,编译器可能会使用临时对象,而不是直接进行复制。

然而, Finalize Adjust 过程也会因其他原因被调用,包括对象销毁和赋值操作在多个地方被调用,例如赋值语句、对象销毁(包括声明对象的销毁和分配对象的释放)以及函数返回的受控类型值的终结处理等。

在赋值语句中,虽然 Adjust 是与用户定义赋值相关的主要过程,但我们必须使用 Finalize 过程来保存 Recoverable 子类的值。因为要增量存储的值是赋值新值之前的当前值,而 Adjust 过程在右侧值的逐位副本替换目标对象的值之后才被调用,此时存储当前值就太晚了。而且, Adjust 过程在某些情况下(如带有显式初始值的对象声明)存储值是不合适的。

我们对 Recoverable 类型的 Finalize 过程的实现不尝试进行任何存储回收,因为无法区分是由于赋值还是对象销毁而调用该过程。由于不能将所有必要的资源直接声明为 Recoverable 类型中的简单记录组件,一些存储不会自动回收。例如,从 Recoverable 派生的类型的每个对象都有一个布尔标志 Known_Valid 来指示对象是否已知存在。显然,不能将 Known_Valid 声明为 Recoverable 类型中的记录组件,因为这些标志会随着它们要指示的对象一起消失。因此,我们定义了一个隐藏类型 Descriptor 来维护关于可恢复应用对象的所有此类信息;每个 Descriptor 对象专门用于一个可恢复应用对象。恢复区域列表中的指针实际上指向 Descriptor ,而不是应用对象本身。为了回收分配给 Descriptor 的存储, Recovery_Cache 类型的 Finalize 过程会自动回收它们,而不是 Recoverable 类型的 Finalize 过程。

为了防止在相应的应用对象不再存在后使用 Descriptor ,我们在 Finalize 过程中执行增量赋值功能,并将对象标记为可能不存在。 Finalize 过程将 Descriptor 中的对象 Known_Valid 标志设置为 False ,而 Adjust 过程将其重置为 True 。如果应用对象真的即将被销毁, Adjust 过程不会被调用,标志将保持为 False Restore 过程会忽略 Known_Valid 标志为 False Descriptor

赋值操作对非受限值的身份维护也有影响。任何可以通过赋值操作赋值的对象在赋值过程中会被逐位复制,整个对象会被覆盖,因此被认为是“匿名”的。任何单个“身份”都会丢失,因此赋值兼容的值必须被认为是可以相互自由替换的。Ada 的用户定义赋值模型支持这种值的透明性,因为在调用 Finalize Adjust 过程时,只传递赋值的目标对象,而不是目标和源对象。这两个过程执行的所有处理都必须基于单个目标对象。对于非受限类型,使用源和目标信息进行部分更新是不可能的。

为了维护可恢复应用对象的身份,我们使用地址作为永久且不受赋值影响的身份标识。每个 Recovery_Cache 对象有一个从它管理的对象的地址到相应 Descriptor 的映射。为了提高效率,我们使用哈希映射,对传递给 Finalize Adjust 的实际参数的地址进行哈希处理。由于 Recoverable 及其后代是带标签的类型,它们总是通过引用传递,因此形式参数不是副本,这些地址是有效的。 Finalize Adjust 过程使用这个映射来获取指向相应 Descriptor 的指针。

赋值操作对用户界面也有影响。由于 Adjust Finalize 过程只传递赋值的目标对象,目标对象必须包含对关联的 Recovery_Cache 对象的引用,以便 Adjust Finalize 过程可以访问哈希 Descriptor 映射。然而,对 Recovery_Cache 对象的引用也是一个在赋值过程中不被保留的身份实例。与每个 Recoverable 对象关联的访问类型判别式用于避免这种身份丢失。因此,可恢复对象的声明需要指定管理它们的 Recovery_Cache 对象。虽然判别式可能会被覆盖,但如果程序员尝试对具有不同判别式的对象进行赋值,编译器会引发 Constraint_Error ,从而防止任何有效的更改。不幸的是,这种方法意味着可恢复应用对象只能赋值给与同一 Recovery_Cache 对象关联的其他可恢复应用对象,不支持让 Recoverable 对象与多个 Recovery_Cache 对象关联的更动态的方法。

在错误检测方面, Finalize Adjust 过程除了在赋值语句中被调用外,还会在各种情况下被调用,包括临时对象、聚合和对象声明的初始值。为了忽略这些情况, Finalize Adjust 过程会忽略那些地址未绑定在哈希 Descriptor 映射中的对象,实际上就是忽略没有 Descriptor 的对象。不幸的是,这种方法意味着我们无法检测到那些无意中没有通过 Register 过程分配 Descriptor 对象的对象的赋值。由于程序员完全负责调用 Register 过程,不检测这种错误是特别遗憾的。一种替代方法是在 Finalize 过程中根据需要生成 Descriptor 并将它们绑定到哈希映射中。虽然这些不必要的 Descriptor 不会导致失败,因为 Known_Valid 标志将为 False ,但可能会分配大量这样的 Descriptor 对象。调用 Restore Discard 过程时会访问每个 Descriptor 来检查 Known_Valid 标志,从而导致性能不佳。目前的方法似乎是最好的折衷方案。

下面是赋值操作的相关步骤总结:
1. 调用 Establish 后,首次赋值存储旧值。
2. 重写 Adjust Finalize 实现赋值操作。
3. Finalize 存储旧值并标记对象可能不存在。
4. Adjust 重置 Known_Valid 标志。
5. 使用地址映射维护对象身份。
6. 声明可恢复对象指定 Recovery_Cache
7. 忽略无 Descriptor 对象避免不必要调用。

5. 初始化应用对象

对于类型 Ada.Finalization.Controlled (以及 Limited_Controlled 类型)定义的子程序包括 Initialize 过程。当一个受控对象创建后,如果声明时没有提供初始值,就会自动调用 Initialize 过程的具体重写版本。对于有限受控类型,由于不允许赋值(包括初始值赋值), Initialize 过程总是会被调用。

然而,增量恢复缓存依赖于赋值操作,因此不能使用有限类型。如前面所述,实现会将某些数据结构与 Recoverable 类型的具体子类对象相关联(这些数据结构是恢复缓存对象的一部分,而不是可恢复应用对象本身)。由于 Initialize 过程并非总是被调用,所以不能用它来创建这些数据结构。

为了解决这个问题,我们定义了类范围的 Register 过程来初始化可恢复应用对象。当检测到此类对象未注册时,会引发 Registration_Error 异常。我们没有将该过程命名为 “Initialize”,是为了避免与受控类型的 Initialize 过程混淆。

初始化应用对象的步骤如下:
1. 定义 Register 过程用于初始化可恢复应用对象。
2. 当检测到对象未注册时,抛出 Registration_Error 异常。

6. 相关技术对比

在软件容错领域,有其他一些方法与我们所讨论的增量恢复缓存技术有所不同。

Rubira - Calsavara 和 Stroud 在 C++ 中的方法也依赖于用户定义赋值。在他们的方案中,所有可恢复状态会合并到一个对象(一种类型)中,并在执行恢复块变体之前保存。为了避免复制大量数据,用户需要为每种类型定义一个 “懒” 赋值,即只有当值实际改变时才创建副本。所有类型(包括预定义的基本类型,如 int float )都需要进行这样的定义。而我们的方法将复杂性从用户转移到了一个中央设施,但要求可恢复应用类型继承自我们定义的具有增量赋值语义的类型。

另一种方法是扩展编译器或其他工具,使容错支持完全自动化。在给定的程序文本部分,实现定义的编译指示(编译器指令)会要求编译器生成代码以实现容错机制。这种方法与我们的方法的主要区别在于,我们的方法在程序源代码中直接表达容错机制。编译器扩展方法的优点是用户无需确定哪些对象需要状态保存和恢复。而在我们的方法中,程序员必须明确声明可恢复应用对象,并将其与恢复点关联起来,这存在疏忽的可能性。不过,恢复块的实现会调用我们的例程,所以在这方面编译器生成方法并没有优势。此外,无论哪种方法,开发者都需要表明需要恢复机制。我们所描述的设施的主要优点是任何 Ada 编译器都可以支持它们,不需要扩展编译器或其他专用工具。而且,通过适当的同步,本文描述的设施可以集成到涉及多个控制线程的容错机制中。

下面是不同方法的对比表格:
| 方法 | 复杂性承担方 | 状态保存方式 | 对用户要求 | 编译器依赖 | 多线程支持 |
| ---- | ---- | ---- | ---- | ---- | ---- |
| Rubira - Calsavara 和 Stroud 的 C++ 方法 | 用户 | 合并到一个对象保存 | 为所有类型定义 “懒” 赋值 | 无特殊要求 | 未提及 |
| 编译器扩展方法 | 编译器 | 编译器自动生成代码 | 无需确定保存对象 | 需要扩展编译器 | 未提及 |
| 增量恢复缓存技术 | 中央设施 | 增量保存 | 继承特定类型,明确声明对象 | 普通 Ada 编译器 | 可集成 |

7. 总结

我们已经证明 Ada 95 可以直接表达抽象恢复接口,并基于增强赋值产生与应用无关的增量恢复实例。这是一个有价值的展示,因为 Ada 广泛应用于有容错要求的应用程序中,并且通常被认为非常适合该领域。

然而,Ada 的初始化、终结处理和用户定义赋值模型使实现变得复杂。具体来说,这三个功能在一个基于可赋值值可替换性的模型中由三个过程共享。因此,当需要赋值时,由于模型基于值的匿名性,很难保留和管理对象身份。其他语言提供了不同的模型,其中初始化、终结处理和赋值是单独的构造。在那种模型中,只有必要的组件会被更改,从而保留其他值和目标对象的任何身份。Ada 的赋值模型使用逐位复制,因此不支持同时保留身份和进行赋值。此外,由于初始值可以通过多种方式提供,对于非受限类型,使用任何特定机制进行初始化都不能保证。同样,终结处理也存在类似问题,无法保证创建唯一身份。

尽管存在这些挑战,但我们通过合理的设计和实现,如使用地址映射维护对象身份、定义 Register 过程初始化对象等,使得增量恢复缓存技术在 Ada 中得以有效实现,为软件容错提供了一种可行的解决方案。

下面是整个技术流程的 mermaid 流程图:

graph LR
    classDef process fill:#E5F6FF,stroke:#73A6FF,stroke-width:2px

    A(定义 Recovery_Cache):::process --> B(定义 Recoverable 类型):::process
    B --> C(关联 Recovery_Cache 和 Recoverable):::process
    C --> D(调用 Establish):::process
    D --> E(首次赋值存储旧值):::process
    E --> F(执行赋值操作):::process
    F --> G{是否需要恢复?}:::process
    G -->|是| H(调用 Restore):::process
    G -->|否| I(继续正常执行):::process
    H --> J(恢复对象状态):::process
    I --> K(可能调用 Discard):::process
    K --> L(处理恢复区域):::process
    M(初始化应用对象):::process --> C

综上所述,支持软件容错的增量恢复缓存技术在 Ada 中虽然面临一些挑战,但通过精心设计和实现,能够有效地为软件提供容错能力,并且具有较好的通用性和可扩展性。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值