0%

ras 软件方案调研

uefi 代码示例来自华为 hisi 1620

APEI

UEFI
ACPI Platform Error Interfaces

Provides a standard way to convey error info From Firmware to OS

BERT

Boot Error Record Table
Record fatal errors, then report it in the second boot
记录启动过程中的关键错误信息, 在下一次启动时报告错误.

  • 在 OS 未接管平台的控制权限之前 firmware(如 BIOS 或者 UEFI)检测到错误,导致系统无法继续启动,可以通过 BIOS/FIRMWARE 将这种类型的错误写入到特定的存储位置。这样一来,在下一次的正常启动过程中,OS 可以通过特定的方法将之前保存的错误读取出来分析并处理。
  • 系统运行过程中 firmware 检测到了致命错误,以至于 firmware 决定不通知 OS 而是直接重启(如 CPU 风扇突然坏了,瞬间过热,如果不立刻重启会烧毁 CPU),在重启前 firmware 可以记录下相关的错误信息以便之后分析出错原因

注:只有 BIOS/FIRMWARE 才有能力对 BERT 执行写入操作;对于 OS 而言,BERT 仅仅是一个只读的表。BERT 出现的意义在于希望采用一种统一的接口来记录特定类型的硬件错误(主要是一些致命的),从而简化 BIOS/FIRMWARE 和 OS 的实现。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
-+ ApeiEntryPoint(ImageHandle, SystemTable)
\ -|+ if SetupData.EnRasSupport
"通过协议的 GUID 查找对应的协议, 查找acpi 表格协议"
\ -+ gBS->LocateProtocol(&gEfiAcpiTableProtocolGuid, NULL, &mAcpiTableProtocol);
"查找 ACPI 标准数据表协议, 操作系统和驱动程序可以获取系统硬件配置信息,进行系统初始化和配置,以及支持电源管理等功能。"
| -+ gBS->LocateProtocol(&gEfiAcpiSdtProtocolGuid, NULL, &mAcpiSdtProtocol);
| -+ gBS->AllocatePool (EfiReservedMemoryType, sizeof (APEI_TRUSTED_FIRMWARE_STRUCTURE), (VOID**)&mApeiTrustedfirmwareData) "分配内存池"
| - gBS->SetMem (mApeiTrustedfirmwareData, sizeof (APEI_TRUSTED_FIRMWARE_STRUCTURE), 0)); "memset 0"
| -+ OemInitBertTable (ImageHandle);
\ - BERT_CONTEXT Context;
| -+ BertHeaderCreator (&Context, BOOT_ERROR_REGION_SIZE); "Bert 表分配内存"
\ - Context->BertHeader = AllocateZeroPool (sizeof (EFI_ACPI_6_0_BOOT_ERROR_RECORD_TABLE_HEADER));
| - Context->Block = AllocateReservedZeroPool (ErrorBlockSize);
"构造header 包含上图的 OEMID OEM_table_id creator_id等如 EFI_ACPI_ARM_OEM_REVISION EFI_ACPI_ARM_CREATOR_ID"
| - *Context->BertHeader = (EFI_ACPI_6_0_BOOT_ERROR_RECORD_TABLE_HEADER) {
ARM_ACPI_HEADER(
EFI_ACPI_6_0_BOOT_ERROR_RECORD_TABLE_SIGNATURE,
EFI_ACPI_6_0_BOOT_ERROR_RECORD_TABLE_HEADER,
EFI_ACPI_6_0_BOOT_ERROR_RECORD_TABLE_REVISION
),
| - ErrorBlockInitial (Context.Block, EFI_ACPI_6_2_ERROR_SEVERITY_NONE);
| -+ BertSetAcpiTable (&Context); "初始化 Bert Table"
"ACPI 表格是存储着系统硬件配置信息和固件与操作系统通信信息的数据结构。在 UEFI 中,
有时需要向 UEFI 固件中添加自定义的 ACPI 表格,
以便在操作系统启动时提供特定的系统配置信息,或支持特定的硬件功能"
\ -+ mAcpiTableProtocol->InstallAcpiTable ( "通过Acpi 表格协议安装 Bert 表"
mAcpiTableProtocol,
Bert,
Bert->Header.Length,
&AcpiTableHandle);

linux driver

drivers/acpi/apei/bert.c

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
-+ bert_init
\ -+ acpi_get_table(ACPI_SIG_BERT, 0, (struct acpi_table_header **)&bert_tab); "获取bert table"
| - region_len = bert_tab->region_length; "获取 bert_table length"
| - apei_resources_init(&bert_resources); "初始化 apei_resources 结构体"
"向bert_resources 下添加IOMEM 类型的资源,以便在发生错误时能够正确地处理这些资源。 "
| - apei_resources_add(&bert_resources, bert_tab->address, region_len, true);
| -+ apei_resources_request(&bert_resources, "APEI BERT");
| -+ boot_error_region = ioremap_cache(bert_tab->address, region_len);
| -|+ if boot_error_region
\ -+ bert_print_all(boot_error_region, region_len);
\ - "Error records from previous boot"
| -+ cper_estatus_print(KERN_INFO HW_ERR, estatus);
\ - " %s event severity: %s", severity "打印log错误级别"
| -+ foreach section
\ -+ cper_estatus_print_section(newpfx, gdata, sec_no)
\ - severity = gdata->error_severity;
| - printk("%s""Error %d, type: %s\n", pfx, sec_no, cper_severity_str(severity));
| - printk("%s""fru_id: %pUl\n", pfx, gdata->fru_id); "fru_id fru_text等, 见上图的表结构"
| - printk("%s""fru_text: %.20s\n", pfx, gdata->fru_text);
...

ERST

Error Record Serialization Table
Provides details necessary to communicate with on-board persistent storage for error recording

提供必要的详细信息, 协同存储错误记录

ERST 本质上是一个用来永久存储错误的抽象接口软件可以通过 ERST 表将各种错误信息保存到 ERST 中,再由 ERST 写入到可用于永久存储的物理介质中。ERST 并没有一个严格的定义来界定什么是“错误”,换言之,软件可以保存任何信息到 ERST 中,只要软件认为是有意义,有价值的信息就可以.
物理介质未必一定是 flash 或 NVRAM,可以是网络存储或者其他。

ERST 的主要作用就是用来存储各种硬件或者平台相关的错误,错误类型包括:

  • Corrected Error(CE)
  • Uncorrected Recoverable Error(UCR)
  • Uncorrected Non-Recoverable Error,或者说 Fatal Error。

换言之,只要是软件可以记录的错误,都可以将其保存到 ERST 当中。加上之前谈到的 BERT 表,这样一来,无论系统运行在哪个阶段,当出现硬件或平台相关的错误时,通过 APEI 接口,都有办法将错误保存下来。这样一来就可以在之后通过适当的方法将错误读取出来进行分析,从而加快定位产生错误的原因并加以解决。

EINJ

Error Injection Table
Provides a generic Interface which OSPM can inject hardware Errors to the platform without requiring platform Specific software.

提供通用接口方便 os 向硬件注入错误.
EINJ 的主要作用是用来注入错误并触发错误,或者说,EINJ 是一个用来测试的表。EINJ 可以注入各种类型的硬件错误,这些注入的错误不是模拟的,而是通过 EINJ 和底层 firmware 以及硬件配合真实产生的。通过 EINJ 注入的硬件错误是真正的错误,和硬件真实发生的错误没有差别。这样一来,平台设计者和软件开发人员可以使用 EINJ 在软硬件发布之前测试平台的软硬件环境是否可靠,是否具有足够的容错性以及完备性,而不必等到在平台发布之后的使用过程中出现错误时再来检测系统是否可靠。
EINJ 支持的错误注入方式非常丰富。从错误类型上划分,和 ERST 一样,包括 Corrected Error(CE),Uncorrected Recoverable Error(UCR),以及 Uncorrected Non-Recoverable Error,或者说 Fatal Error。从错误来源划分,可以分为 Processor,Memory,以及 PCI-E 设备等类型。通过交叉组合,至少有 9 种可以注入的错误。
至少有 9 种可以注入的错误。

  • 0x00000001 Processor Correctable
  • 0x00000002 Processor Uncorrectable non-fatal
  • 0x00000004 Processor Uncorrectable fatal
  • 0x00000008 Memory Correctable
  • 0x00000010 Memory Uncorrectable non-fatal
  • 0x00000020 Memory Uncorrectable fatal
  • 0x00000040 PCI Express Correctable
  • 0x00000080 PCI Express Uncorrectable fatal
  • 0x00000100 PCI Express Uncorrectable non-fatal
  • 0x00000200 Platform Correctable
  • 0x00000400 Platform Uncorrectable non-fatal
  • 0x00000800 Platform Uncorrectable fatal

使用 EINJ 进行错误注入有两个步骤:

  1. 根据需要产生错误注入需要的 trigger 表(trigger action table),这个 trigger 表是 BIOS/FIRMWARE 根据用户需要注入的错误类型动态生成的,不能人为手工构造;
  2. 是触发这个 trigger 表,让其在合适的位置产生需要的错误。至于产生了错误之后,如何处理错误,如何修复错误之类的事情,和 EINJ 无关。

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

  1. 使用 SET_ERROR_TYPE 这个 ACTION 向 EINJ 表中注入一个错误
  2. 根据第一步设定的错误,与 EINJ 相关的 firmware 会动态生成一个 Trigger Error Action 表,使用 GET_TRIGGER_ERROR_ACTION_TABLE 这个动作可以得到这个 trigger 表,然后操作这个 trigger 表可以触发之前注入的错误,从而达到测试特定错误类型的目的。

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

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

kernel driver

1
2
3
4
5
6
7
8
9
10
11
$ cd /sys/kernel/debug/apei/einj
# See which errors can be injected
$ cat available_error_type
# Set memory address for injection
$ echo 0x12345000 > param1
$ Mask 0xfffffffffffff000 - anywhere in this page
$ echo $((-1 << 12)) > param2
# Choose correctable memory error
$ echo 0x8 > error_type
# Inject now
$ echo 1 > error_inject

ACPI 5.0 BIOS 也可能允许注入特定于供应商的错误。在这种情况下,名为 vendor 的文件将包含标识信息,从 BIOS 中希望可以允许希望使用的应用程序使用特定于供应商的扩展,以告知他们正在 BIOS 上运行支持它。
所有供应商扩展都在其中设置了 0x80000000 位 error_type。文件 vendor_flags 控制对 param1 的解释和 param2(1 =处理器,2 =内存,4 = PCI)。

GHES of HEST

Generic Hardware Error Source - GHES
Hardware Error Source Table - HEST

HOW to get trigger: Notification Structure
WHERE are the error records:

Error Status Address
(GAS : Generic Address Structure)

HOW to release records’ mem:
Read Ack Register

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

其中最为特殊也是最为重要的硬件错误源类型就是 GHES (Generic Hardware Error Source)。GHES 是一个通用硬件错误源,换言之,任何类型的硬件错误都可以使用 GHES 来定义,而无需使用之前提到的特定硬件错误源,如内存控制器错误等。

当前无论是软件还是 BIOS/FIRMWARE 的实现,基本上都是只使用 GHES 来实现 HEST 的功能,至于其他特定的硬件错误源,基本上都没有使用(PCI-E AER 的部分代码检测了 PCI-E 类型的硬件错误源)。

在 FFM (Firmware-First handling) 使能的情况下,一般而言,所有 CE 类型的错误通过 SCI 中断报告给 OS,然后 OS 在 HEST/GHES 中查表,检测并处理可能的硬件错误;所有 UC 和 Fatal 类型的错误通过 NMI 报告给 OS,然后 OS 在 NMI 的 handler 中查表,检测并处理可能的硬件错误。这些规定并不是硬性要求的,平台设计者完全可以根据需要使用 NMI 来处理所有的错误类型,包括 CE, UC 和 Fatal 类型的错误,也可以只使用 NMI 来处理 UC 和 Fatal 类型的错误,而使用轮询的方式来处理 CE 类型的错误。

linux kernel ghes 处理

Ghes_probe 函数中,根据 HEST 表中传递的检测错误类型,查看相关 kernel 配置选项是否支持。包括 arm 相关的 SEA 错误,NMI,本地中断。
调用 ghes_new 函数,初始化 struct ghes 结构。初始化 ghes 结构,映射表中 Error Status Address。为存放错误信息数据申请内存。

根据上报错误方式,注册不同的处理流程,包括如下:
(1)轮询方式,根据表中传递的 poll_interval 时间,创建定时器。在定时器处理函数 ghes_poll_func 中,调用 ghes_proc。在这个函数中:
A、读取 GHES 结构中传递的 Error Status Address。首先将读到的 struct acpi_hest_generic_status 结构拷贝到前面申请的内存中,检测相关错误信息长度是否合法。然后将后面的错误信息拷贝。
B、如上报错误的严重级别大于 GHES_SEV_PANIC,则将错误信息打印,清除错误状态以及记录错误信息的内存块。然后进入 kernel panic。
C、调用 ghes_do_proc 函数处理错误。这个函数中,获取错误数据块中各 section 中 section_type 以及 error_severity。同时判断 fru_id 和 fru_text 字段是否有效。 1>如为内存相关错误,以下为错误类型分类
* @ HW_EVENT_ERR_CORRECTED: Corrected Error - 表示检测到 ECC 纠正的错误
* @ HW_EVENT_ERR_UNCORRECTED : 表示 ECC 无法纠正的错误,但不是致命的错误 (可能是在未使用的内存区域,或者内存控制器可以从中恢复,例如,通过重新尝试操作)
* @ HW_EVENT_ERR_DEFERRED: Deferred Error - 表示处理不紧急的不可纠正的错误。这可能是由于硬件数据中毒,系统可以继续操作,直到中毒的数据被消耗。也可以采取主动的措施,例如,offlining 页面等。
* @ HW_EVENT_ERR_FATAL: 致命错误-无法恢复的不可更正错误。
* @ HW_EVENT_ERR_INFO: 规范定义了第四种类型的错误: 信息日志。
首先获取 struct cper_sec_mem_err 数据块。调用
Ghes_edac_report_mem_error 函数,这个函数中,将错误信息中包括错误类型,错误地址,内存颗粒,错误内存位置填充到错误报告 buffer 中(struct edac_raw_error_desc)。
错误类型如下:

把错误写到 ftrace 的一个跟踪项中,最后调用 edac_raw_mc_handle_error
分别通过 edac_ce_error 处理 ECC 类型的错误以及调用 edac_ue_error 处理其他错误。
最后调用 ghes_handle_memory_failure
如果
- 错误级别为可修复级别且 CPER_SEC_ERROR_THRESHOLD_EXCEEDED 置位(表示内核中止使用这个资源)
- 错误是可恢复的类型。满足上述条件之一则调用 memory_failure_queue 函数。这个函数在检测到页面的硬件内存损坏时由硬件错误处理程序调用。它调度错误页面的恢复,包括删除页面,杀死进程等。

如为 pcie aer 错误
PCIe AER 错误需要发送到 AER 驱动程序进行报告和恢复。GHES 的严重程度与以下 AER 严重程度相对应,并需要进行以下处理:
- GHES_SEV_CORRECTABLE -> AER_CORRECTABLE — 需要由 AER 驱动报告,但不需要恢复。
- GHES_SEV_RECOVERABLE -> AER_NONFATAL
- GHES_SEV_RECOVERABLE && CPER_SEC_RESET -> AER_FATAL
这两种情况都需要 AER 驱动报告和恢复。
GHES_SEV_PANIC 不会进行这种处理,因为内核必须将进入 panic 状态。

(2) 如果为外部中断,则通过 GHES 中传递的中断号,申请中断处理函数,在中断处理函数 ghes_irq_func 调用 ghes_proc。下面的处理流程和上面轮询一致。
(3) 同样的如果为 SCI 中断,调用 notifier_call 回调函数 ghes_notify_hed,遍历 ghes_hed 链表,分别执行 ghes_proc 函数,处理错误
(4) 如果为 NMI 中断,处理错误级别大于 GHES_SEV_PANIC,则直接 kernel panic。否则如配置 CONFIG_ARCH_HAVE_NMI_SAFE_CMPXCHG,则调到工作队列处理函数 ghes_proc_in_irq 中。执行相当于下半部的处理过程。最终调用 ghes_do_proc 函数执行上述相关错误的处理。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
-+ ghes_probe(platform_device* ghes_dev)
\ - acpi_hest_generic *generic = ghes_dev->dev.platform_data;
| -+ switch (generic->notify.type)
\ -|+ case ACPI_HEST_NOTIFY_POLLED:
\ -+ ghes_add_timer(ghes);
| -|+ case ACPI_HEST_NOTIFY_EXTERNAL:
\ -+ acpi_gsi_to_irq(generic->notify.vector, &ghes->irq);
| - request_irq(ghes->irq, ghes_irq_func, IRQF_SHARED, "GHES IRQ", ghes); "申请中断. 中断处理函数为 ghes_irq_func"
| -|+ case ACPI_HEST_NOTIFY_SCI ACPI_HEST_NOTIFY_GSIV ACPI_HEST_NOTIFY_GPIO
\ - register_acpi_hed_notifier(&ghes_notifier_hed);
"`register_acpi_hed_notifier`是一个用于注册ACPI HED(Hardware Error Device)通知的函数。
HED是一种ACPI设备,用于监测硬件错误并报告给操作系统。HED通知允许内核注册一个回调函数,
当硬件错误发生时,内核会调用这个回调函数来处理错误。"
| - list_add_rcu(&ghes->list, &ghes_hed);
| -|+ case ACPI_HEST_NOTIFY_SEA:
"`ACPI_HEST_NOTIFY_SEA`表示SEA(Software Error Announcement)错误通知类型。
当系统中的硬件出现错误时,硬件错误设备(Hardware Error Device,HED)可以使用SEA通知类型向操作系统报告错误信息。
SEA通知类型用于通知操作系统发生了一个由硬件处理的错误事件,并提供相关的错误信息。"
\ -+ ghes_sea_add(ghes);
\ - list_add_rcu(&ghes->list, &ghes_sea);
| -|+ case ACPI_HEST_NOTIFY_NMI:
"`ACPI_HEST_NOTIFY_NMI`表示NMI(Non-Maskable Interrupt)错误通知类型。
当系统中的硬件出现严重的错误时,硬件错误设备(Hardware Error Device,HED)可以使用NMI通知类型向操作系统发出NMI中断,
以通知操作系统发生了一个严重的硬件错误事件。"
\ -+ ghes_nmi_add(ghes);
\ -+ register_nmi_handler(NMI_LOCAL, ghes_notify_nmi, 0, "ghes");
\ ---+ ghes_notify_nmi(cmd, regs)
\ -+ ghes_in_nmi_spool_from_list(&ghes_nmi, FIX_APEI_GHES_NMI)
\ -+ list_for_each_entry_rcu(ghes, rcu_list, list)
\ -+ ghes_in_nmi_queue_one_entry(ghes, fixmap_idx)
"将 GHES 错误信息封装成一个 NMI 消息,
并将该消息推入 NMI 队列。之后,当系统处理 NMI 中断时,
可以调用注册的 GHES 错误处理回调函数,从而处理 GHES 错误。"
\ -+ sev = ghes_severity(estatus->error_severity); "获取严重级别"
\ -|+ if sev >= GHES_SEV_PANIC "错误级别 > panic"
\ - __ghes_panic(ghes, estatus, buf_paddr, fixmap_idx); "调用panic"\
| - "构造 estatus_node, 从ghes 数据中获取 "
| -+ llist_add(&estatus_node->llnode, &ghes_estatus_llist);
"加入全局链表 ghes_estatus_llist 中"
"该链表在 ghes_proc_in_irq 中处理"
| -|+ case ACPI_HEST_NOTIFY_SOFTWARE_DELEGATED:
"`ACPI_HEST_NOTIFY_SOFTWARE_DELEGATED`表示软件委派错误通知类型。
当系统中的硬件出现错误时,硬件错误设备(Hardware Error Device,HED)可以使用软件委派通知类型向操作系统报告错误信息。
这种通知类型允许硬件错误设备将错误信息委托给操作系统的特定软件模块来处理,而不是直接由硬件设备进行错误处理。"
\ -+ apei_sdei_register_ghes(ghes);
\ -+ sdei_register_ghes(ghes, ghes_sdei_normal_callback, ghes_sdei_critical_callback);
"往下指向 arch 实现, 如 arm的 sdei_register_ghes 函数 drivers/firmware/arm_sdei.c"
\ ---+ ghes_sdei_normal_callback "回调"
\ -+ __ghes_sdei_callback(ghes, FIX_APEI_GHES_SDEI_NORMAL);
\ -+ irq_work_queue(&ghes_proc_irq_work);
"中断处理程序中调用`irq_work_queue`函数,将IRQ工作推入队列,
从而在合适的时机执行所需的工作。
即中断下半部的处理, 处理函数为 ghes_proc_in_irq"
\ ---+ init_irq_work(&ghes_proc_irq_work, ghes_proc_in_irq);