高级平台错误接口在 Linux 平台上的应用

APEI 是 RAS 架构中一个重要的组成部分,它提供了一个强大并且统一的接口,极大方便了软硬件开发人员之间的协同开发。本文着重论述了 APEI 的组成实现以及它在 Linux 环境下的相关应用。

陈 功, 软件工程师, Intel

陈功,在 Intel 北京开源技术中心工作。负责 Intel 服务器平台的相关开发和维护工作。



2012 年 3 月 09 日

简介

长久以来,工作在 x86 平台上的硬件使用多样的方式向上层软件报告硬件错误,有的通过 PCI-E 总线传递错误消息,有的需要读写特定的寄存器组来得到错误信息,还有的通过产生特定的中断或者异常来报告错误状态。在这些各式各样方法的背后,是硬件设计人员和软件开发人员耗费大量的时间用来定义接口以及接口实现。这样做的直接后果是增加了太多不必要的开销。因此,一个统一高效的接口无疑是非常有必要的,APEI(Advanced Platform Error Interfaces)的出现,正是为了解决这一长久存在的问题。新的 APEI 规范统一了软硬件之间的接口,降低了软硬件开发人员的开发复杂度。不但如此,新的 APEI 接口更加灵活,便于扩展。譬如说,APEI 的规范定义中大量使用了 UEFI 中已有的结构定义,这样做极大提高了 APEI 和未来的 UEFI BIOS 的兼容性。APEI 作为 RAS 体系结构中的一个重要组成部分,其作用是显而易见的,因此,熟悉并学习使用 APEI 对于构建整个系统的 RAS 体系结构是非常重要且必要的。

APEI 的结构组成并不复杂,简单而言,就是 4 张表。他们分别是:

BOOT Error Record Table (BERT)

Error Record Serialization Table (ERST)

Error Injection Table (EINJ)

Hardware Error Source Table (HEST)

以下将对这 4 张表的功能以及在 Linux 中的实现分别加以介绍。


BERT (Boot Error Record Table)

BERT 表如同其名称定义所描述的一样,主要用来记录在启动过程中出现的错误。系统的启动过程分为多个阶段,如果硬件平台在上电自检时发现某一个 CPU 出现异常无法启动,那么可以禁止激活这个 CPU,让其他 CPU 继续启动。这种机制称为 FRB(Fault Resilient Booting);如果在 OS 未接管平台的控制权限之前 firmware(如 BIOS 或者 UEFI)检测到错误,导致系统无法继续启动,可以通过 BIOS/FIRMWARE 将这种类型的错误写入到特定的存储位置。这样一来,在下一次的正常启动过程中,OS 可以通过特定的方法将之前保存的错误读取出来分析并处理。这就是 BERT 的主要用途。不过,也有可能是在系统运行过程中 firmware 检测到了致命错误,以至于 firmware 决定不通知 OS 而是直接启动(想想 CPU 风扇突然坏了,瞬间过热,如果不立刻重启会烧毁 CPU),在重启前 firmware 可以记录下相关的错误信息以便之后分析出错原因。在目前阶段,BERT 的用途还没有完全定下来,并且只有 BIOS/FIRMWARE 才有能力对 BERT 执行写入操作;对于 OS 而言,BERT 仅仅是一个只读的表。到目前为止,还没有一款 BIOS 提供对 BERT 的正式支持,因此,相关的代码也没有在 Linux 中实现。这也是到目前为止 APEI 体系中中唯一还没有在 Linux 中实现的一个模块。

在 x86 平台的发展过程中,BERT 并不是第一种,也不会是最后一种用来记录硬件错误的方法,在过去乃至现在的很长一段时间内,BIOS/FIRMWARE 都是把特定的硬件错误记录到 BMC(Baseband Management Controller)中,再通过相应的管理程序进行错误解析。BERT 出现的意义在于希望采用一种统一的接口来记录特定类型的硬件错误(主要是一些致命的),从而简化 BIOS/FIRMWARE 和 OS 的实现。


ERST (Error Record Serialization Table)

ERST 本质上是一个用来永久存储错误的抽象接口。软件可以通过 ERST 将各种错误信息保存到 ERST 中,再由 ERST 写入到可用于永久存储的物理介质中。ERST 并没有一个严格的定义来界定什么是“错误”,换言之,软件可以保存任何信息到 ERST 中,只要软件认为是有意义,有价值的信息就可以。这里用来存储的介质也未必就一定是 flash 或者 NVRAM 等常见的永久存储介质,它也可以是网络存储,如 NFS。用户不需要关心存储介质的类型,也不必关心具体的存储位置,只要确保使用 ERST 提供的标准读写接口,就可以方便的将错误信息进行保存和读取。这也是为什么说 ERST 是一个抽象接口的原因。

ERST 的主要作用就是用来存储各种硬件或者平台相关的错误,错误类型包括 Corrected Error(CE),Uncorrected Recoverable Error(UCR),以及 Uncorrected Non-Recoverable Error,或者说 Fatal Error。换言之,只要是软件可以记录的错误,都可以将其保存到 ERST 当中。加上之前谈到的 BERT 表,这样一来,无论系统运行在哪个阶段,当出现硬件或平台相关的错误时,通过 APEI 接口,都有办法将错误保存下来。这样一来就可以在之后通过适当的方法将错误读取出来进行分析,从而加快定位产生错误的原因并加以解决。

flash 的擦除操作

flash/NVRAM 这些永久存储介质不同于普通磁盘(SSD 磁盘除外)和内存,写入操作只能将数据 bit 位从 1 写为 0,当数据 bit 位变为 0 后,不能通过写入操作再变为 1。因此需要通过一个擦除操作将数据 bit 位从 0 变为 1 后才能执行新的写入。

ERST 是一个抽象接口,因此其提供的操作方法也都是抽象出来的动作行为。简单来说,ERST 提供了 3 种基本操作行为:读取(read),写入(write)和擦除(erase)。读取和写入很容易理解,而擦除操作主要是针对 flash,NVRAM 这些永久存储介质使用的。下面以写入操作为例来描述一下 ERST 的操作过程:

  1. OS 需要初始化 ERST 指定格式的错误记录,包括一个特定的标记“ER”用来识别是否为 ERST 的记录项。值得说明的是 ERST 的写入操作为了可以在任何环境下都能使用,包括实时环境,其加锁操作使用的不是普通的 spinlock,而是 raw_spinlock 以确保其原子性。
    清单 1. ERST 的写入过程
    在 drivers/acpi/apei/erst.c 中
     int erst_write(const struct cper_record_header *record) 
     { 
            int rc; 
            unsigned long flags; 
            struct cper_record_header *rcd_erange; 
    
            if (erst_disable) 
                    return -ENODEV; 
    
            if (memcmp(record->signature, CPER_SIG_RECORD, CPER_SIG_SIZE)) 
                    return -EINVAL; 
    
            if (erst_erange.attr & ERST_RANGE_NVRAM) { 
                    if (!raw_spin_trylock_irqsave(&erst_lock, flags)) 
                            return -EBUSY; 
                    rc = __erst_write_to_nvram(record); 
                    raw_spin_unlock_irqrestore(&erst_lock, flags); 
                    return rc; 
            } 
    
            if (record->record_length > erst_erange.size) 
                    return -EINVAL; 
    
            if (!raw_spin_trylock_irqsave(&erst_lock, flags)) 
                    return -EBUSY; 
            memcpy(erst_erange.vaddr, record, record->record_length); 
            rcd_erange = erst_erange.vaddr; 
            /* signature for serialization system */ 
            memcpy(&rcd_erange->persistence_information, "ER", 2); 
    
            rc = __erst_write_to_storage(0); 
            raw_spin_unlock_irqrestore(&erst_lock, flags); 
    
            return rc; 
     }
  2. 通过调用内嵌的 __erst_write_to_storage 函数,继续执行 APEI 规范中写入操作规定的的一系列 ACTION 完成最终的写入。这些动作的顺序依次为:
     ACPI_ERST_BEGIN_WRITE,
     ACPI_ERST_SET_RECORD_OFFSET,
     ACPI_ERST_EXECUTE_OPERATION,
     ACPI_ERST_CHECK_BUSY_STATUS,
     ACPI_ERST_GET_COMMAND_STATUS,
     ACPI_ERST_END
    清单 2. ERST 写入操作的内部命令序列
    在 drivers/acpi/apei/erst.c 中
     static int __erst_write_to_storage(u64 offset) 
     { 
            struct apei_exec_context ctx; 
            u64 timeout = FIRMWARE_TIMEOUT; 
            u64 val; 
            int rc; 
    
            erst_exec_ctx_init(&ctx); 
           rc = apei_exec_run_optional(&ctx, ACPI_ERST_BEGIN_WRITE);
            if (rc) 
                    return rc; 
            apei_exec_ctx_set_input(&ctx, offset); 
            rc = apei_exec_run(&ctx, ACPI_ERST_SET_RECORD_OFFSET); 
            if (rc) 
                    return rc; 
            rc = apei_exec_run(&ctx, ACPI_ERST_EXECUTE_OPERATION); 
            if (rc) 
                    return rc; 
            for (;;) { 
                    rc = apei_exec_run(&ctx, ACPI_ERST_CHECK_BUSY_STATUS); 
                    if (rc) 
                            return rc; 
                    val = apei_exec_ctx_get_output(&ctx); 
                    if (!val) 
                            break; 
                    if (erst_timedout(&timeout, SPIN_UNIT)) 
                            return -EIO; 
            } 
            rc = apei_exec_run(&ctx, ACPI_ERST_GET_COMMAND_STATUS); 
            if (rc) 
                    return rc; 
            val = apei_exec_ctx_get_output(&ctx); 
            rc = apei_exec_run_optional(&ctx, ACPI_ERST_END); 
            if (rc) 
                    return rc; 
    
            return erst_errno(val); 
     }

当前的 Linux 实现只向用户空间提供了读取和删除(对应擦除行为)的接口,并没有写入的接口,这也意味着记录错误的动作只能由内核完成。当前在 Linux 内核中只有两个地方使用了这个写入接口:一个是内核在发生 MCE 异常时系统会根据 APEI 的配置情况调用这个接口

 do_machine_check 
  mce_end 
    mce_reign 
      mce_panic 
        apei_write_mce 
          erst_write

另一个则是下文即将提到的 pstore 文件系统使用了这个接口。

由于 ERST 在设计的时候并没有考虑多用户的读取操作,也就是说,无论有多少用户同时并发访问 ERST,到了 ERST 的底层 firmware 那里都认为是一个用户在访问,这样一来,会产生这样一个问题:如果用户 A 和用户 B 同时访问 ERST,假设用户 A 先获得了访问权限,当用户 A 调用 GET_NEXT_RECORD_ID 读取下一条 ERST 中的错误记录后,用户 B 再调用 GET_NEXT_RECORD_ID 读取下一条时,将不会得到和用户 A 一样的错误记录,而是用户 A 获得的错误记录之后的下一条记录。这是由于 ERST 的底层 firmware 实现中,其用于记录当前记录位置的指针始终是顺序向后移动的,只要有访问动作,这个记录指针就会向后移动一位,从而造成了上述问题。由于 ERST 并没有提供一个所谓的“GET_ERROR_BY_ID”的命令,所以 ERST 不会也不存在这样的操作方式来获取特定的错误记录(firmware 的设计决定了它向 OS 提供的接口)。当前的解决方法是通过软件在系统内存中实现了一个错误记录的缓存。当用户通过 GET_NEXT_RECORD_ID 读取 ERST 的错误记录时,实际上是优先从系统内存中保存的一个副本中获得的。这样一来,对于以上的情景,OS 只需要对用户 A 和用户 B 分别返回一份同样的错误记录即可。当然,完整的实现还要考虑更多的情况,其核心就是如何同步内存副本中的错误记录和保存在 ERST 底层存储介质中的物理记录,譬如当一条错误记录已经失效时,如何更新内存副本中的记录,又或者当内存副本中存在 / 不存在 ERST 底层存储介质中的物理记录时,又该如何更新内存副本中的记录。读者可根据 erst.c 中的有关代码自行分析(如 __erst_record_id_cache_add_one 和 __erst_record_id_cache_compact 等函数)。

由于 ERST 是一个底层的抽象接口,对于终端用户是无法直接访问的,因此需要提供一个用户空间接口供用户访问。目前在 Linux 内核中是通过一个名为 pstore(Persistent store)的文件系统来完成这一功能的。pstore 向上对用户提供了一个简单易用的文件系统接口,向下对底层的存储设备提供了一组抽象回调函数接口,对于像 ERST 这样的抽象存储设备可以通过 pstore 提供的回调接口进行挂接,从而利用 pstore 进行各种操作。pstore 是一个内存文件系统,类似于 /proc 或者 tmpfs 这类文件系统,其特点是所有的数据都保存在内存中,不占用任何磁盘空间,系统重启后其文件系统中的所有数据丢失,必须从适当的地方重新导入。这里所说的系统重启后数据丢失并不和 pstore 名字所描述的永久存储相冲突,因为 pstore 所说的永久存储指的是数据可以永久存储在底层存储设备提供的永久存储空间中,并非文件系统本身。

图 1. pstore 和 ERST 的关系
图 1. pstore 和 ERST 的关系

pstore 提供的回调接口为 struct pstore_info,定义在 include/linux/pstore.h 中。对应的 ERST 后端实现如下所示:

 static struct pstore_info erst_info = { 
        .owner          = THIS_MODULE, 
        .name           = "erst", 
        .open           = erst_open_pstore, 
        .close          = erst_close_pstore, 
        .read           = erst_reader, 
        .write          = erst_writer, 
        .erase          = erst_clearer 
 };

在理想情况下,在操作系统启动完成后,应该有一个初始化脚本或者 daemon 程序自动装载 pstore 文件系统,这时 pstore 文件系统会读取底层 ERST 对应的存储介质上的错误记录并创建相应的文件列表(一条错误记录可能对应着多个文件),由于一般 ERST 对应的存储介质如 NVRAM 或者 flash 都比较小,因此初始化脚本或者 daemon 程序应该尽快将这些错误记录备份到其他可靠的地方然后通过 pstore 文件系统将其删除,以免 ERST 存储的错误记录过多导致底层的存储介质空间不足而无法写入新的错误记录。

pstore 作为一个通用的永久存储接口,其功能并不仅仅限于对 ERST 的支持,最近在 Linux 内核中新加入的 UEFI 的后端(backend)就是一个新的实例。用户可以通过配置 pstore 使用 UEFI 或者 ERST 作为后端。如果将 UEFI 作为后端,那么错误记录会保存到 UEFI 的命名空间(namespace)中,而不是 ERST 中。由于 UEFI 和 ERST 各自的特点,其通过 pstore 创建的错误记录形式也不相同。譬如对于 UEFI 而言,一条错误记录的最大尺寸只有 1K,因此一次系统 panic 产生的错误日志可能需要几个甚至十几个文件才能完整的记录下来。

由于 APEI 的接口较新,很多 BIOS 的实现并不完全正确和完善。为了测试 ERST 自身功能的正确性,内核还提供了一个 erst-dbg 的调试模块供用户使用,通过这个调试接口,用户可以方便的测试 ERST 的 read/write/erase 等操作的正确性以及其他一些辅助功能。用户可以通过 mce-test 测试套件 (git://git.kernel.org/pub/scm/utils/cpu/mce/mce-test.git)来进行测试工作,具体的操作方法可以参见 mce-test/hwpoison/Makefile 中有关 test-erst 的部分。


EINJ (Error Injection Table)

在 APEI 定义的所有表中,EINJ 是最为特别的一个。因为 EINJ 不是一个用来记录或者保存错误的表,相反,EINJ 的主要作用是用来注入错误并触发错误,或者说,EINJ 是一个用来测试的表。EINJ 可以注入各种类型的硬件错误,这些注入的错误不是模拟的,而是通过 EINJ 和底层 firmware 以及硬件配合真实产生的。通过 EINJ 注入的硬件错误是真正的错误,和硬件真实发生的错误没有差别。这样一来,平台设计者和软件开发人员可以使用 EINJ 在软硬件发布之前测试平台的软硬件环境是否可靠,是否具有足够的容错性以及完备性,而不必等到在平台发布之后的使用过程中出现错误时再来检测系统是否可靠。从这个层面上来说,EINJ 提供了一个非常便利的方法供平台设计者和软件开发人员使用,极大地提高了像 Mission Critical 系统的可用性。

EINJ 支持的错误注入方式非常丰富。从错误类型上划分,和 ERST 一样,包括 CE,UC 以及 Fatal Error。从错误来源划分,可以分为 Processor,Memory,以及 PCI-E 设备等类型。通过交叉组合,至少有 9 种可以注入的错误。在当前阶段,不是所有的平台都能同时支持这 9 种错误类型的注入。一般来说,Memory 较其它设备更容易出错,出错后影响范围更广,因此对 Memory 的错误注入是首先要保证的。

EINJ 的实现特点有些类似 ERST,一来都使用了 UEFI 所定义的数据结构作为其组成部分,二来都采用了抽象的动作行为(一般称为 ACTION)来实现所需的功能。相比之下,EINJ 的实现要更加灵活和困难一些。简单来说,使用 EINJ 进行错误注入有两个步骤:第一步根据需要产生错误注入需要的 trigger 表(trigger action table),这个 trigger 表是 BIOS/FIRMWARE 根据用户需要注入的错误类型动态生成的,不能人为手工构造;第二步是触发这个 trigger 表,让其在合适的位置产生需要的错误。至于产生了错误之后,如何处理错误,如何修复错误之类的事情,就和 EINJ 无关了,毕竟这是平台之上的软硬件应该考虑的问题,EINJ 的工作就是能够产生必要的错误,仅此而已。

EINJ 的注入过程基本上是一个 2 步操作:

  1. 使用 SET_ERROR_TYPE 这个 ACTION 向 EINJ 表中注入一个错误
  2. 根据第一步设定的错误,与 EINJ 相关的 firmware 会动态生成一个 Trigger Error Action 表,使用 GET_TRIGGER_ERROR_ACTION_TABLE 这个动作可以得到这个 trigger 表,然后操作这个 trigger 表可以触发之前注入的错误,从而达到测试特定错误类型的目的
    清单 3. EINJ 的注入过程
    在 drivers/acpi/apei/einj.c 中
     static int __einj_error_inject(u32 type, u64 param1, u64 param2) 
     { 
            struct apei_exec_context ctx; 
            u64 val, trigger_paddr, timeout = FIRMWARE_TIMEOUT; 
            int rc; 
    
            einj_exec_ctx_init(&ctx); 
    
           rc = apei_exec_run_optional(&ctx, ACPI_EINJ_BEGIN_OPERATION);
            if (rc) 
                    return rc; 
            apei_exec_ctx_set_input(&ctx, type); 
            rc = apei_exec_run(&ctx, ACPI_EINJ_SET_ERROR_TYPE); 
            if (rc) 
                    return rc; 
            if (einj_param) { 
                    writeq(param1, &einj_param->param1); 
                    writeq(param2, &einj_param->param2); 
            } 
            rc = apei_exec_run(&ctx, ACPI_EINJ_EXECUTE_OPERATION); 
            if (rc) 
                    return rc; 
            for (;;) { 
                    rc = apei_exec_run(&ctx, ACPI_EINJ_CHECK_BUSY_STATUS); 
                    if (rc) 
                            return rc; 
                    val = apei_exec_ctx_get_output(&ctx); 
                    if (!(val & EINJ_OP_BUSY)) 
                            break; 
                    if (einj_timedout(&timeout)) 
                            return -EIO; 
            } 
            rc = apei_exec_run(&ctx, ACPI_EINJ_GET_COMMAND_STATUS); 
            if (rc) 
                    return rc; 
            val = apei_exec_ctx_get_output(&ctx); 
            if (val != EINJ_STATUS_SUCCESS) 
                    return -EBUSY; 
    
            rc = apei_exec_run(&ctx, ACPI_EINJ_GET_TRIGGER_TABLE); 
            if (rc) 
                    return rc; 
            trigger_paddr = apei_exec_ctx_get_output(&ctx); 
            rc = __einj_error_trigger(trigger_paddr); 
            if (rc) 
                    return rc; 
            rc = apei_exec_run_optional(&ctx, ACPI_EINJ_END_OPERATION); 
    
            return rc; 
     }
    图 2. EINJ 执行过程的图形化示例
    图 2. EINJ 执行过程的图形化示例

什么是 GAS

GAS (Generic Address Structure) 是 ACPI 规范中定义的一种标准数据结构,用来描述一个处于系统 IO 空间中的寄存器的各种信息,包括寄存器地址,类型和访问模式等。参见 ACPI 4.0a 5.2.3.1 节

在整个过程当中,关键点是从 GET_TRIGGER_ERROR_ACTION_TABLE 动作完成后得到的 trigger 表的基地址。这个 trigger 表的基地址作为入口参数被 __einj_error_trigger 函数调用,最终完成错误触发。简单来说,__einj_error_trigger 需要完成两件事:

  1. 根据 GET_TRIGGER_ERROR_ACTION_TABLE 返回的 trigger 表的地址(本质上是一个复合结构)进行相应的 IO 资源分配(这里的 IO 资源主要由 GAS 提供)
  2. 调用 ACPI_EINJ_TRIGGER_ERROR 动作完成错误触发。
图 3. EINJ 返回的 trigger 地址展开
图 3. EINJ 返回的 trigger 地址展开

什么是 FFM(Firmware First Mode)

在 x86 平台上,当硬件发生错误时,根据系统的配置情况,硬件可以直接把错误信息发送给 CPU,然后交由系统软件处理;也可以交给 BIOS/FIRMWARE 先行处理,然后由 BIOS/FIRMWARE 来决定是否需要将这个错误交给 CPU 进行进一步处理,如果需要 CPU 的参与,又以何种方式进行。这后一种操作方式就是所谓的 FFM。由于 BIOS/FIRMWARE 是平台相关的,因此 BIOS/FIRMWARE 相比 OS 而言更加清楚硬件平台的配置情况,甚至包含各种必须的 workaround,定制和优化。这样一来,在 FFM 使能的情况下,BIOS/FIRMWARE 利用这一优势,可以有针对性的对发生的硬件错误进行分析,处理以及分发,也可以更加准确的记录错误的现场信息。这样一来,不但对硬件错误可以做出更准确,更复杂的处理,而且可以降低 OS 的复杂性和冗余度。

FFM 模式的实现要点在于 SMI 中断的使用。举例来说,如果 FFM 被禁用,硬件产生错误时发送 NMI 中断给 CPU,然后交由 OS 处理;如果 FFM 使能,硬件产生错误时不会发送 NMI 中断,而是触发 SMI 中断。这个中断会使 CPU 进入 SMM 模式,在 SMM 模式中,BIOS/FIRMWARE 会对错误进行初步处理,清除硬件的错误状态,然后根据需要在结束 SMI 中断处理后产生合适的中断发送给 CPU,例如 SCI 或者 NMI,来通知 CPU 进行下一步处理工作。如果 BIOS/FIRMWARE 不需要 CPU 做进一步的处理,那么在退出 SMM 模式后,就不会产生新的中断,可能只需要 CPU 在轮询时检查某些状态寄存器即可。需要指出的是,并不是所有的错误源都适用于 FFM,譬如 Machine Check Exception 就不能使用 FFM,而只能通过 MCE 直接报告给 CPU 进行处理。

图 4. FFM 的处理过程
图 4. FFM 的处理过程

FFM 模式的开启需要硬件,BIOS/FIRMWARE 和软件三者的配合。首先硬件的物理连接要确保可以使用 FFM,其次 BIOS/FIRMWARE 要有对应的处理逻辑并且通过 ACPI 将必要的表项导出供系统软件使用,最后系统软件需要根据情况开启或者禁用 FFM。这三者缺一不可。硬件平台是否支持 FFM 需要查看相关的硬件手册。BIOS/FIRMWARE 是否支持可以通过检查 BIOS 的配置选项,有些 BIOS 会缺省隐藏这一选项,在这种情况下需要通过 BIOS 的提供厂商来确定 FFM 是否被支持。在软件层面,需要通过调用 ACPI 的 _OSC 方法来查询并开启 FFM 模式。在这个 _OSC 调用中,其核心是两个特定的 UUID。

第一个 UUID 定义在 ACPI 规范中(ACPI 4.0a 规范 6.2.10.2 227 页)。通过调用 _OSC 方法并使用这个 UUID 做为参数,可以判断当前系统是否支持 FFM。

清单 4. FFM 模式的初始状态检查
在 drivers/acpi/bus.c 中
 static u8 sb_uuid_str[] = "0811B06E-4A27-44F9-8D60-3CBBC22E7B48";
 static void acpi_bus_osc_support(void) 
 { 
        u32 capbuf[2]; 
        struct acpi_osc_context context = { 
               .uuid_str = sb_uuid_str,
                .rev = 1, 
                .cap.length = 8, 
                .cap.pointer = capbuf, 
        }; 
        acpi_handle handle; 
        ……
        ……
        if (!ghes_disable) 
                capbuf[OSC_SUPPORT_TYPE] |= OSC_SB_APEI_SUPPORT; 
        if (ACPI_FAILURE(acpi_get_handle(NULL, "\\_SB", &handle))) 
                return; 
        if (ACPI_SUCCESS(acpi_run_osc(handle, &context))) { 
                u32 *capbuf_ret = context.ret.pointer; 
                if (context.ret.length > OSC_SUPPORT_TYPE) 
                        osc_sb_apei_support_acked = 
                                capbuf_ret[OSC_SUPPORT_TYPE]& 
                                    OSC_SB_APEI_SUPPORT; 
                kfree(context.ret.pointer); 
        } 
 }

然而很多 BIOS/FIRMWARE 在实现时并未使用这个标准的 UUID,而是为了兼容 Windows 系统,使用了微软私有的 WHEA 中定义的一个 UUID 来完成这一功能。为了能让 Linux 系统使用这一强大功能,Linux 也不得不“伪装”成 Windows 来完成检测,如下所示:

清单 5. FFM 模式初始状态的附加检查
在 drivers/acpi/apei/apei-base.c 中
 int apei_osc_setup(void) 
 { 
       static u8 whea_uuid_str[] = "ed855e0c-6c90-47bf-a62a-26de0fc5ad5c"; 
        acpi_handle handle; 
        u32 capbuf[3]; 
        struct acpi_osc_context context = { 
                .uuid_str       = whea_uuid_str, 
                .rev            = 1, 
                .cap.length     = sizeof(capbuf), 
                .cap.pointer    = capbuf, 
        }; 

        capbuf[OSC_QUERY_TYPE] = OSC_QUERY_ENABLE; 
        capbuf[OSC_SUPPORT_TYPE] = 1; 
        capbuf[OSC_CONTROL_TYPE] = 0; 

        if (ACPI_FAILURE(acpi_get_handle(NULL, "\\_SB", &handle)) 
            || ACPI_FAILURE(acpi_run_osc(handle, &context))) 
                return -EIO; 
        else { 
                kfree(context.ret.pointer); 
                return 0; 
        } 
 }

由于 WHEA 是微软的私有协议,因此除了 UUID 本身以外,我们并不知道其内部结构是如何定义的,因此如何传递参数给这个 _OSC 方法是不甚清楚的。这也是为什么在内核中会出现这样的 patch(commit id: b3b46d7)。

清单 6. FFM 模式最终使能状态判断
在 drivers/acpi/apei/ghes.c 中
 static int __init ghes_init(void) 
 { 
……
……
        rc = apei_osc_setup(); 
        if (rc == 0 && osc_sb_apei_support_acked) 
                pr_info(GHES_PFX "APEI firmware first mode is "
                        "enabled by APEI bit and WHEA _OSC.\n"); 
        else if (rc == 0 && !osc_sb_apei_support_acked) 
                pr_info(GHES_PFX "APEI firmware first mode is "
                        "enabled by WHEA _OSC.\n"); 
        else if (rc && osc_sb_apei_support_acked) 
                pr_info(GHES_PFX "APEI firmware first mode is "
                        "enabled by APEI bit.\n"); 
        else 
                pr_info(GHES_PFX "Failed to enable APEI "
                        "firmware first mode.\n"); 
……
……
 }

通过这两种方式,最终可以完成对 FFM 的支持。由上述代码可以看到,FFM 和 GHES 有很大的联系。这是因为在 FFM 使能的模式下,无论是硬件自身发生的错误,还是通过 EINJ 触发产生的错误,都会被记录在 HEST/GHES 中,下文将具体介绍 HEST/GHES 的功能和作用。


HEST (Hardware Error Source Table)

在 HEST 中定义了很多硬件相关的错误源和错误类型。定义这些硬件错误源的目的在于标准化软硬件错误接口的实现。有了 HEST,当发生特定类型的硬件错误,如 PCI-E 设备产生了一个 UC 类型的错误时,BIOS/FIRMWARE 有统一的方法更新特定的寄存器和内部状态,软件有统一的方法去处理和解析错误。HEST 中定义了很多硬件错误源,如 MCE, NMI, PCI-E,GHES 等等。

图 5. HEST 的基本组成结构
图 5. HEST 的基本组成结构

这其中最为特殊也是最为重要的硬件错误源类型就是 GHES(Generic Hardware Error Source)。从字面上理解,GHES 是一个通用硬件错误源,换言之,任何类型的硬件错误都可以使用 GHES 来定义,而无需使用之前提到的特定硬件错误源,如 MCE 等。而事实上,当前无论是软件还是 BIOS/FIRMWARE 的实现,基本上都是只使用 GHES 来实现 HEST 的功能,至于其他特定的硬件错误源,基本上都没有使用(PCI-E AER 的部分代码检测了 PCI-E 类型的硬件错误源)。在 FFM 使能的情况下,一般而言,所有 CE 类型的错误通过 SCI 中断报告给 OS,然后 OS 在 HEST/GHES 中查表,检测并处理可能的硬件错误;所有 UC 和 Fatal 类型的错误通过 NMI 报告给 OS,然后 OS 在 NMI 的 handler 中查表,检测并处理可能的硬件错误。这些规定并不是硬性要求的,平台设计者完全可以根据需要使用 NMI 来处理所有的错误类型,包括 CE, UC 和 Fatal 类型的错误,也可以只使用 NMI 来处理 UC 和 Fatal 类型的错误,而使用轮询的方式来处理 CE 类型的错误。

以 CE 类型的错误通过 SCI 中断处理,UC 和 Fatal 类型的错误通过 NMI 处理为例,Linux 中的处理逻辑如下所示:

清单 7. 初始化 GHES 的错误分类
在 drivers/acpi/apei/ghes.c 中:
 static int __devinit ghes_probe(struct platform_device *ghes_dev) 
 { 
……
        switch (generic->notify.type) { 
		……
        case ACPI_HEST_NOTIFY_SCI: 
                mutex_lock(&ghes_list_mutex); 
                if (list_empty(&ghes_sci)) 
                        register_acpi_hed_notifier(&ghes_notifier_sci);
                list_add_rcu(&ghes->list, &ghes_sci); 
                mutex_unlock(&ghes_list_mutex); 
                break; 
        case ACPI_HEST_NOTIFY_NMI: 
                len = ghes_esource_prealloc_size(generic); 
                ghes_estatus_pool_expand(len); 
                mutex_lock(&ghes_list_mutex); 
                if (list_empty(&ghes_nmi)) 
                        register_nmi_handler(NMI_LOCAL, ghes_notify_nmi, 0, 
                                                "ghes"); 
                list_add_rcu(&ghes->list, &ghes_nmi); 
                mutex_unlock(&ghes_list_mutex); 
                break; 		……
		 } 
		……
 }

从上述代码中可以看到,SCI 的 notifier 挂在了 HED 设备(Hardware Error Device)的通知链上,当硬件发生了 CE 类型的错误时,HED 设备(HED 设备在 ACPI 中的 device id 为 PNP0C33)会执行如下代码:

清单 8. CE 类型错误的处理过程
在 drivers/acpi/hed.c 中
 /* 
 * SCI to report hardware error is forwarded to the listeners of HED, 
 * it is used by HEST Generic Hardware Error Source with notify type 
 * SCI. 
 */ 
 static void acpi_hed_notify(struct acpi_device *device, u32 event) 
 { 
        blocking_notifier_call_chain(&acpi_hed_notify_list, 0, NULL); 
 }

这样一来,之前注册在 HED 设备通知链上的 ghes_notifier_sci 就可以得到调用,从而完成后继的查 HEST/GHES 表以及处理错误等工作;对于 NMI 的处理也是类似,当硬件发生了 UC/Fatal 类型的错误产生 NMI 时,Linux 会在 NMI 的 handler 里面做类似的工作,这其中就包括有 HEST/GHES 注册的回调函数。

清单 9. UC/Fatal 类型错误的处理过程
在 arch/x86/kernel/nmi.c 中
 static notrace __kprobes void default_do_nmi(struct pt_regs *regs) 
 { 
        unsigned char reason = 0; 
        int handled; 
        bool b2b = false; 

        /* 
         * CPU-specific NMI must be processed before non-CPU-specific 
         * NMI, otherwise we may lose it, because the CPU-specific 
         * NMI can not be detected/processed on other CPUs. 
         */ 
        ……
        ……
        handled = nmi_handle(NMI_LOCAL, regs, b2b); 
        ……
 }

nmi_handle 的实现如下所示。它主要的作用就是遍历挂在 nmi_desc[NMI_LOCAL | NMI_UNKNOWN] 链表上的回调函数完成对 NMI 的处理。这个新的处理机制是从 Linux 3.1 内核引入的,在此之前,NMI 一直使用 notifier 通知链的方式完成类似的工作,有兴趣的读者可以通过 commit c9126b2 进行更深入的阅读。

清单 10. 遍历 NMI handler
在 arch/x86/kernel/nmi.c 中
 static int notrace __kprobes nmi_handle(unsigned int type, 
                                               struct pt_regs *regs, bool b2b) 
 { 
        struct nmi_desc *desc = nmi_to_desc(type); 
        struct nmiaction *a; 
        int handled=0; 

        rcu_read_lock(); 

        /* 
         * NMIs are edge-triggered, which means if you have enough 
         * of them concurrently, you can lose some because only one 
         * can be latched at any given time.  Walk the whole list 
         * to handle those situations. 
         */ 
       list_for_each_entry_rcu(a, &desc->head, list)
                handled += a->handler(type, regs); 

        rcu_read_unlock(); 

        /* return total number of NMI events handled */ 
        return handled; 
 }

值得注意的是,由于 NMI context 的特殊性,譬如关中断,在 x86_64 上使用特殊的栈帧等,因此对于 HEST/GHES 而言,如果处于 NMI context,处理要格外小心。譬如不可以调用 printk 进行打印输出(printk 在调用时会持有一把锁,如果在 NMI 中调用 printk 可能会因为 NMI 抢占本 CPU 上其他中断处理中的 printk 操作而造成死锁),因而也不可以在 NMI 中使用其他加锁操作,而要使用无锁操作(lock-less)。这些细节和 APEI 本身没有直接关联,有兴趣的读者可以通过阅读相关的代码自行分析。以下以 GHES 中 NMI 类型的错误处理为例来说明整个 GHES 的处理过程。

在介绍 GHES 的处理逻辑之前,有必要先了解一下 HEST/GHES 的数据结构定义,以便于理解。

图 6. HEST/GHES 的数据结构图示
图 6. HEST/GHES 的数据结构图示

从上表可以看到,GHES 基本上是一个塔形的数据结构。一层结构层叠在另一层结构之上。需要注意的是,GHES Error Data 并没有直接保存在 GHES 的数据结构当中,而是通过 GAS 中包含的地址指针间接引用。每一个 GHES Header 都包含着一份属于自己的 GHES Error Data,在每一个 GHES Error Data 当中,可能有一项或者多项 GHES Error Data Entry。

下表是一个通过 acpidump/acpixtract/iasl 实际解析出来的 HEST 表。

注:

acpidump/acpixtract/iasl 等工具是用来完成 ACPI 表解析的一整套工具集。

许多 Linux 发行版都包含 acpidump,可以从这里下载:

http://www.lesswatts.org/projects/acpi/utilities.php

如果想要比较完善的支持 APEI 的解析,最好还是下载一些较新的源码包自行编译安装。譬如从这里:

http://packages.debian.org/source/sid/acpidump

iasl 的下载和使用说明可见:

http://www.acpica.org/downloads/

如何使用 acpidump 相关的工具可见:

http://www.lesswatts.org/projects/acpi/utilities.php

表 1. 解析之后的 HEST 实例
 /* 
 * Disassembly of HEST.dat 
 * ACPI Data Table [HEST] 
 * Format: [HexOffset DecimalOffset ByteLength]  FieldName : FieldValue 
 */ 
 [000h 0000   4]                Signature : "HEST"    /* Hardware Error Source Table */ 
 [004h 0004   4]                 Table Length : 000000A8 
 [008h 0008   1]                     Revision : 01 
 [009h 0009   1]                     Checksum : E2 
 [00Ah 0010   6]                       Oem ID : "xxxxxx"
 [010h 0016   8]                 Oem Table ID : "xxxxxx"
 [018h 0024   4]                 Oem Revision : 00000001 
 [01Ch 0028   4]              Asl Compiler ID : "xxxxxx"
 [020h 0032   4]        Asl Compiler Revision : 00000001 

 [024h 0036   4]           Error Source Count : 00000002 

 [000h 0000   4]                  Signature : "HEST" /* Hardware Error Source Table */ 
 [004h 0004   4]                 Table Length : 000000A8 
 [008h 0008   1]                     Revision : 01 
 [009h 0009   1]                     Checksum : E2 
 [00Ah 0010   6]                       Oem ID : "xxxxxx"
 [010h 0016   8]                 Oem Table ID : "xxxxxx"
 [018h 0024   4]                 Oem Revision : 00000001 
 [01Ch 0028   4]              Asl Compiler ID : "xxxxxx"
 [020h 0032   4]        Asl Compiler Revision : 00000001 

 [024h 0036   4]           Error Source Count : 00000002 

 [028h 0040   2]                 Subtable Type : 0009 (Generic Hardware Error Source) 
 [02Ah 0042   2]                      Source Id : 0000 
 [02Ch 0044   2]            Related Source Id : FFFF 
 [02Eh 0046   1]                       Reserved : 00 
 [02Fh 0047   1]                         Enabled : 01 
 [030h 0048   4]       Records To Preallocate : 00000001 
 [034h 0052   4]      Max Sections Per Record : 00000001 
 [038h 0056   4]           Max Raw Data Length : 00001000 

 [03Ch 0060  12]         Error Status Address : <Generic Address Structure> 
 [03Ch 0060   1]                       Space ID : 00 (SystemMemory) 
 [03Dh 0061   1]                      Bit Width : 40 
 [03Eh 0062   1]                     Bit Offset : 00 
 [03Fh 0063   1]         Encoded Access Width : 04 (QWord Access:64) 
 [040h 0064   8]                         Address : 000000007BE17018 

 [048h 0072  28]                      Notify : <Hardware Error Notification Structure> 
 [048h 0072   1]                    Notify Type : 03 (SCI)
 [049h 0073   1]                  Notify Length : 1C 
 [04Ah 0074   2]   Configuration Write Enable : 0000 
 [04Ch 0076   4]                   PollInterval : 00000000 
 [050h 0080   4]                          Vector : 00000000 
 [054h 0084   4]      Polling Threshold Value : 00000000 
 [058h 0088   4]     Polling Threshold Window : 00000000 
 [05Ch 0092   4]        Error Threshold Value : 00000000 
 [060h 0096   4]       Error Threshold Window : 00000000 

 [064h 0100   4]    Error Status Block Length : 00001000 

 [068h 0104   2]                 Subtable Type : 0009 (Generic Hardware Error Source) 
 [06Ah 0106   2]                      Source Id : 0001 
 [06Ch 0108   2]             Related Source Id : FFFF 
 [06Eh 0110   1]                        Reserved : 00 
 [06Fh 0111   1]                         Enabled : 01 
 [070h 0112   4]       Records To Preallocate : 00000001 
 [074h 0116   4]      Max Sections Per Record : 00000001 
 [078h 0120   4]          Max Raw Data Length : 00001000 

 [07Ch 0124  12]         Error Status Address : <Generic Address Structure> 
 [07Ch 0124   1]                       Space ID : 00 (SystemMemory) 
 [07Dh 0125   1]                      Bit Width : 40 
 [07Eh 0126   1]                     Bit Offset : 00 
 [07Fh 0127   1]         Encoded Access Width : 04 (QWord Access:64) 
 [080h 0128   8]                        Address : 000000007BE18020 

 [088h 0136  28]                       Notify : <Hardware Error Notification Structure> 
 [088h 0136   1]                    Notify Type : 04 (NMI)
 [089h 0137   1]                  Notify Length : 1C 
 [08Ah 0138   2]   Configuration Write Enable : 0000 
 [08Ch 0140   4]                    PollInterval : 00000000 
 [090h 0144   4]                           Vector : 00000000 
 [094h 0148   4]      Polling Threshold Value : 00000000 
 [098h 0152   4]     Polling Threshold Window : 00000000 
 [09Ch 0156   4]        Error Threshold Value : 00000000 
 [0A0h 0160   4]       Error Threshold Window : 00000000 

 [0A4h 0164   4]    Error Status Block Length : 00001000

从上表中可以得知:这是一个 HEST 类型的表(Signature = "HEST");包含着 2 个错误源(Error Source Count = 2);这两个错误源都是 GHES 类型(Subtable Type = 9),一个是 SCI(Notify Type = 3),另一个是 NMI(Notify Type = 4)。错误类型为 SCI 的 GHES 中的 GAS 寄存器地址为 0x000000007BE17018,寄存器宽度为 8 个字节(Bit Width = 0x40),寄存器类型为 MMIO(Space ID = 0);错误类型为 NMI 的 GHES 中的 GAS 寄存器地址为 0x000000007BE18020,寄存器宽度为 8 个字节(Bit Width = 0x40),寄存器类型为 MMIO(Space ID = 0)。

在 FFM 使能的情况下,当系统中发生一个 UC/Fatal 类型的硬件错误时,CPU 首先进入 SMM 模式,将控制权交给底层的 firmware。在 firmware 处理结束后,产生 NMI 中断,由上可知,这个中断最终会调用注册在 NMI 通知链上的 GHES 回调函数,也就是 ghes_notify_nmi。

清单 11. NMI 类型错误的处理过程:第一部分
在 drivers/acpi/apei/ghes.c 中
 static int ghes_notify_nmi(unsigned int cmd, struct pt_regs *regs) 
 { 
        struct ghes *ghes, *ghes_global = NULL; 
        int sev, sev_global = -1; 
        int ret = NMI_DONE; 

        raw_spin_lock(&ghes_nmi_lock); 
        list_for_each_entry_rcu(ghes, &ghes_nmi, list) { 
                if (ghes_read_estatus(ghes, 1)) { 
                        ghes_clear_estatus(ghes); 
                        continue; 
                } 
                sev = ghes_severity(ghes->estatus->error_severity); 
                if (sev > sev_global) { 
                        sev_global = sev; 
                        ghes_global = ghes; 
                } 
                ret = NMI_HANDLED; 
        } 

        if (ret == NMI_DONE) 
                goto out;

ghes_notify_nmi 函数的第一部分首先检查相应的 GHES 错误源,判断当前产生 NMI 的错误源以及相应的错误严重级别。这个错误严重级别关系到最终采取的处理方式,是尝试恢复错误,还是重启系统。

清单 12. NMI 类型错误的处理过程:第二部分
        if (sev_global >= GHES_SEV_PANIC) { 
                oops_begin(); 
                __ghes_print_estatus(KERN_EMERG HW_ERR, ghes_global->generic, 
                                     ghes_global->estatus); 
                /* reboot to log the error! */ 
                if (panic_timeout == 0) 
                        panic_timeout = ghes_panic_timeout; 
                panic("Fatal hardware error!"); 
        }

这部分代码用来对严重错误进行先行处理。当错误的严重程度超过了一定限度后,无法进行错误恢复,或者说无法进行容错处理,而只能通过重启系统的方式来恢复当前不可靠的系统环境。

清单 13. NMI 类型错误的处理过程:第三部分
        list_for_each_entry_rcu(ghes, &ghes_nmi, list) { 
 #ifdef CONFIG_ARCH_HAVE_NMI_SAFE_CMPXCHG 
                u32 len, node_len; 
                struct ghes_estatus_node *estatus_node; 
                struct acpi_hest_generic_status *estatus; 
 #endif 
                if (!(ghes->flags & GHES_TO_CLEAR)) 
                        continue; 
 #ifdef CONFIG_ARCH_HAVE_NMI_SAFE_CMPXCHG 
                if (ghes_estatus_cached(ghes->estatus)) 
                        goto next; 
                /* Save estatus for further processing in IRQ context */ 
                len = apei_estatus_len(ghes->estatus); 
                node_len = GHES_ESTATUS_NODE_LEN(len); 
                estatus_node = (void *)gen_pool_alloc(ghes_estatus_pool, 
                                                      node_len); 
                if (estatus_node) { 
                        estatus_node->generic = ghes->generic; 
                        estatus = GHES_ESTATUS_FROM_NODE(estatus_node); 
                        memcpy(estatus, ghes->estatus, len); 
                        llist_add(&estatus_node->llnode, &ghes_estatus_llist); 
                } 
 next: 
 #endif 
                ghes_clear_estatus(ghes); 
        } 
 #ifdef CONFIG_ARCH_HAVE_NMI_SAFE_CMPXCHG 
        irq_work_queue(&ghes_proc_irq_work); 
 #endif 

 out: 
        raw_spin_unlock(&ghes_nmi_lock); 
        return ret; 
 }

这是最后一部分代码。如果这个错误还不足以引起系统重启,那么可以尝试进行错误恢复。正如上文提到的,NMI context 是一个非常受限制的环境,因此所有的处理应该尽可能的简单快速。这里的核心思想就是采用推迟处理。首先将 GHES 错误源中获取的信息复制到安全地方,然后将其依次挂载到特定的链表上,最后将整个处理推迟到 IRQ context 中进行处理。CONFIG_ARCH_HAVE_NMI_SAFE_CMPXCHG 宏的作用用来判断当前体系结构能否支持可靠的无锁处理(具体可参见 http://en.wikipedia.org/wiki/Compare-and-swap)。而 irq_work_queue 的作用则是唤醒放在 IRQ_WORK 上的与 GHES 有关的工作队列(这里就是 ghes_proc_irq_work,对应的处理函数为 ghes_proc_in_irq),以便在 NMI 结束并返回到 IRQ context 时继续完成未决的处理工作。

什么是 IRQ_WORK

IRQ_WORK 在“硬”中断上下文中执行。它向 NMI handler 提供了一种可以推后执行的机制,以便于完成某些不能在 NMI context 中执行的代码。相关代码请参考 kernel/irq_work.c 以及 commit e360adbe2

由于在 ghes_proc_in_irq 函数执行时,系统已经从 NMI context 退出进入 IRQ context,因此相应的限制也少了很多,譬如调用 printk 进行打印就是安全的。ghes_proc_in_irq 的作用就是遍历之前保存但尚未处理的所有 GHES 错误项,调用 ghes_do_proc 进行最终的处理。

清单 14. NMI 类型错误的细化处理
在 drivers/acpi/apei/ghes.c 中
 static void ghes_do_proc(const struct acpi_hest_generic_status *estatus) 
 { 
        int sev, sec_sev; 
        struct acpi_hest_generic_data *gdata; 

        sev = ghes_severity(estatus->error_severity); 
        apei_estatus_for_each_section(estatus, gdata) { 
                sec_sev = ghes_severity(gdata->error_severity); 
                if (!uuid_le_cmp(*(uuid_le *)gdata->section_type, 
                                 CPER_SEC_PLATFORM_MEM)) { 
                        struct cper_sec_mem_err *mem_err; 
                        mem_err = (struct cper_sec_mem_err *)(gdata+1); 
 #ifdef CONFIG_X86_MCE 
                        apei_mce_report_mem_error(sev == GHES_SEV_CORRECTED, 
                                                  mem_err); 
 #endif 
 #ifdef CONFIG_ACPI_APEI_MEMORY_FAILURE 
                        if (sev == GHES_SEV_RECOVERABLE && 
                            sec_sev == GHES_SEV_RECOVERABLE && 
                            mem_err->validation_bits & 
                                     CPER_MEM_VALID_PHYSICAL_ADDRESS) { 
                                unsigned long pfn; 
                                pfn = mem_err->physical_addr >> PAGE_SHIFT; 
                                memory_failure_queue(pfn, 0, 0); 
                        } 
 #endif 
                } 
        } 
 }

在 ghes_do_proc 函数中,需要根据错误的不同进行分类处理。首先需要判断错误类型,是内存错误(错误类型为 CPER_SEC_PLATFORM_MEM),还是 PCI-E 错误(错误类型为 CPER_SEC_PCIE)。如果是内存错误,还需要根据错误的严重程度进行分别处理。如果是 CE 类型的错误,需要通过 APEI 和 MCE 之间的接口将错误信息转储到内核的 MCE 日志缓冲区中,供之后用户层的 daemon,如 mcelog 读取用;如果是 UC 类型的错误,则需要交给 HWPOISON(参见参考资料中 RAS 在 x86 上的应用及 Linux 实现一文)进行后继处理。由于 HWPOISON 需要工作在 process context,因此不能直接在 IRQ context 调用 HWPOISON 相关的函数,而是和之前 IRQ WORK 的执行类似,不过这里唤醒的是普通的工作队列罢了。


结论

APEI 作为一个非常新的接口规范,在很多平台中尚未实现,只有少部分最新的 XEON 服务器中提供了部分支持,随着新的 XEON 服务器平台的发布,APEI 的支持也会逐渐完善起来。除了 APEI,底层原生的 UEFI BIOS 也在尝试着在硬件错误处理,错误报告方面做更多的工作;在硬件平台这个层面上,随着 CPU 集成了越来越多的外设和控制器,作为整个平台核心的 CPU,也在不断加强这方面的功能,APEI 将以怎样一种方式将自身整合到更高一级的架构组成中,我们拭目以待。

参考资料

学习

讨论

  • 加入 developerWorks 中文社区,developerWorks 社区是一个面向全球 IT 专业人员,可以提供博客、书签、wiki、群组、联系、共享和协作等社区功能的专业社交网络社区。

条评论

developerWorks: 登录

标有星(*)号的字段是必填字段。


需要一个 IBM ID?
忘记 IBM ID?


忘记密码?
更改您的密码

单击提交则表示您同意developerWorks 的条款和条件。 查看条款和条件

 


在您首次登录 developerWorks 时,会为您创建一份个人概要。您的个人概要中的信息(您的姓名、国家/地区,以及公司名称)是公开显示的,而且会随着您发布的任何内容一起显示,除非您选择隐藏您的公司名称。您可以随时更新您的 IBM 帐户。

所有提交的信息确保安全。

选择您的昵称



当您初次登录到 developerWorks 时,将会为您创建一份概要信息,您需要指定一个昵称。您的昵称将和您在 developerWorks 发布的内容显示在一起。

昵称长度在 3 至 31 个字符之间。 您的昵称在 developerWorks 社区中必须是唯一的,并且出于隐私保护的原因,不能是您的电子邮件地址。

标有星(*)号的字段是必填字段。

(昵称长度在 3 至 31 个字符之间)

单击提交则表示您同意developerWorks 的条款和条件。 查看条款和条件.

 


所有提交的信息确保安全。


static.content.url=http://www.ibm.com/developerworks/js/artrating/
SITE_ID=10
Zone=Linux
ArticleID=801297
ArticleTitle=高级平台错误接口在 Linux 平台上的应用
publish-date=03092012