高级平台错误接口在 Linux 平台上的应用(2)
- UID
- 1066743
|
高级平台错误接口在 Linux 平台上的应用(2)
当前的 Linux 实现只向用户空间提供了读取和删除(对应擦除行为)的接口,并没有写入的接口,这也意味着记录错误的动作只能由内核完成。当前在 Linux 内核中只有两个地方使用了这个写入接口:一个是内核在发生 MCE 异常时系统会根据 APEI 的配置情况调用这个接口
1
2
3
4
5
6
| 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 的关系pstore 提供的回调接口为 struct pstore_info,定义在 include/linux/pstore.h 中。对应的 ERST 后端实现如下所示:
1
2
3
4
5
6
7
8
9
| 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 步操作:
- 使用 SET_ERROR_TYPE 这个 ACTION 向 EINJ 表中注入一个错误
- 根据第一步设定的错误,与 EINJ 相关的 firmware 会动态生成一个 Trigger Error Action 表,使用 GET_TRIGGER_ERROR_ACTION_TABLE 这个动作可以得到这个 trigger 表,然后操作这个 trigger 表可以触发之前注入的错误,从而达到测试特定错误类型的目的 清单 3. EINJ 的注入过程
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
| 在 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 执行过程的图形化示例
|
|
|
|
|
|