RAS 在 x86 上的应用及 Linux 实现

简介

RAS 的全称为 Reliability, Availability and Serviceability。Reliability(可靠性)指的是系统必须尽可能的可靠,不会意外的崩溃,重启甚至导致系统物理损坏,这意味着一个具有可靠性的系统必须能够对于某些小的错误能够做到自修复,对于无法自修复的错误也尽可能进行隔离,保障系统其余部分正常运转。Availability(可用性)指的是系统必须能够确保尽可能长时间工作而不下线,即使系统出现一些小的问题也不会影响整个系统的正常运行,在某些情况下甚至可以进行 Hot Plug 的操作,替换有问题的组件,从而严格的确保系统的 downtime 时间在一定范围内。Serviceability 指的是系统能够提供便利的诊断功能,如系统日志,动态检测等手段方便管理人员进行系统诊断和维护操作,从而及早的发现错误并且修复错误。综上所述,RAS 作为一个整体,其作用在于确保整个系统尽可能长期可靠的运行而不下线,并且具备足够强大的容错机制。这对于像大型的数据中心,网络中心如股票证券交易所,电信机房,银行的数据库中心等应用环境是不可或缺的一部分。

由于 RAS 的独特性,构建一个稳固的 RAS 系统并不是一个简单的工程,这需要软硬件的协同配合。以 X86 平台为例,在 CPU,chipset,IO 控制器等硬件层面上需要提供诸如 ECC/Parity 检验这类的错误检测,错误纠正和错误报告扩展,同时为了提高可靠性,可能还需要进一步提供 RAID 存储以及可以 Hot Plug 的硬盘,内存,甚至是 CPU,以确保在必要的时候可以快速的替换出问题的组件。在 Firmware(这里可近似认为是 BIOS)这样的中间层则需要提供一个无缝的接口,对硬件报告的错误能够处理,同时也能够将错误或者处理的结果通知给 OS 以便软件能做进一步处理。对于 OS 和 application 而言,则需要提供更加细分的处理逻辑,譬如能够将错误分类分级以便进行有针对性的处理,记录等操作,或者提供测试接口以便自诊断或者调试。以下就以 x86 平台为例,对以上提及的几个方面分别介绍其功能和在 Linux 系统中的实现。


CPU/Memory 的 Hot Plug

Hot Plug 包括常说的 Hot Plug-in/out。Hot Plug 的最大作用就是可以在线更新,从而确保系统能够持续可靠的运行。从硬件的角度看,Hot Plug 有四种状态:

  1. CPU/Memory 不在 socket 上,firmware/BIOS 不知道它的存在
  2. CPU/Memory 在 socket 上,firmware/BIOS 已知但是未通知 OS,因此 OS 未知
  3. CPU/Memory 在 socket 上,firmware/BIOS 也通知了 OS,OS 已知但是未使能它,application 无法使用
  4. CPU/Memory 在 socket 上,firmware/BIOS,OS 都已知并且 OS 使能了它,application 可以使用

从 2 到 3 的变化,就是通常所说的 Hot Add,反之则是 Hot Remove;从 3 到 4 的变化,就是通常说的 online,反之则是 offline。从系统设计的角度来看,任何一个 CPU/Memory 都是对等的,都可以执行 Hot Plug 操作,但是由于软硬件的某些限制,对于 BSP(Boot Strap Processor)CPU 一般是不可以被 Hot Remove 的。

下面以 CPU 的 Hot Add 为例描述这一过程:

  1. 用户将 CPU 插入一个空闲的 socket 中
  2. 用户通过 Hot Plug 的接口初始化 Hot Add 这一动作。接口可以是 OS 提供的 UI 接口,按一个按钮,或者是某些管理接口,如 IPMI,AMT
  3. firmware/BIOS 对插入的 CPU 进行必要的初始化操作,如配置 QPI 总线的路由表,更新地址解码等
  4. 通过 ACPI 中断接口(SCI 中断)向 OS 产生一个 Hot Add 的事件
  5. OS 在接收到这个 ACPI 事件后首先需要通过 ACPI 的 _OSI 方法检查当前系统是否支持"Module Device"的能力,如果是则表明可以进行 Hot Add 操作
  6. OS 通过 ACPI 的 _MAT 方法得到 MADT 描述表,用来初始化 Local APIC/SAPIC 以及 local NMI 中断
  7. OS 对新增的 CPU 进行相关的电源管理配置,如 P/C/T state
  8. OS 调用 ACPI 的 _OST 方法通知 firmware/BIOS 本次 Hot Add 成功与否

在这个过程中,ACPICA(ACPICA 是 OS 用来和 firmware/BIOS 的接口)会完成绝大部分和 ACPI table 交互的工作,最终,ACPICA 会通过工作线程 acpi_os_execute_deferred将后继的事件分发处理交由 kernel 完成。


清单 1. CPU Hot-Add 的执行过程

				
 static void acpi_os_execute_deferred(struct work_struct *work) 
 { 
    struct acpi_os_dpc *dpc = container_of(work, struct acpi_os_dpc, work); 

    if (dpc->wait) 
        acpi_os_wait_events_complete(NULL); 

    dpc->function(dpc->context);/* acpi_ev_notify_dispatchwill be called */ 
    kfree(dpc); 
 } 

 acpi_ev_notify_dispatch -> 
    container_notify_cb 

 static void container_notify_cb(acpi_handle handle, u32 type, void *context) 
 { 
    struct acpi_device *device = NULL; 
    int result; 
    int present; 
    acpi_status status; 

    present = is_device_present(handle); 

    switch (type) { 
    case ACPI_NOTIFY_BUS_CHECK: 
          /* Fall through */ 
    case ACPI_NOTIFY_DEVICE_CHECK: 
            printk(KERN_WARNING "Container driver received %s event/n", 
                   (type == ACPI_NOTIFY_BUS_CHECK) ? 
                   "ACPI_NOTIFY_BUS_CHECK" : "ACPI_NOTIFY_DEVICE_CHECK"); 
            status = acpi_bus_get_device(handle, &device); 
            if (present) { 
                if (ACPI_FAILURE(status) || !device) { 
                        result = container_device_add(&device, handle); 
                        if (!result) 
                            kobject_uevent(&device->dev.kobj, 
                                   KOBJ_ONLINE); 
                        else 
                            printk(KERN_WARNING 
                               "Failed to add container/n"); 
                } 
            } else { 
                if (ACPI_SUCCESS(status)) { 
                    /* device exist and this is a remove request */ 
                    kobject_uevent(&device->dev.kobj, KOBJ_OFFLINE); 
                } 
            } 
            break; 
        case ACPI_NOTIFY_EJECT_REQUEST: 
            if (!acpi_bus_get_device(handle, &device) && device) { 
                kobject_uevent(&device->dev.kobj, KOBJ_OFFLINE); 
            } 
            break; 
        default: 
            break; 
        } 
        return; 
 } 

 

对于 Hot Add 操作,首先系统会打印诸如 Container driver received ACPI_NOTIFY_BUS_CHECK event”的信息到控制台,然后根据检测到的 Bus 状态和 Device 状态决定是否需要在 ACPI Bus 上添加一个新的设备,这里当然是需要的,因此 container_device_add会被调用,在执行过程中利用 sysfs 的对象模型,根据安装在 ACPI Bus 上的 driver 来匹配新增加的设备,经历

 acpi_bus_add 
  acpi_bus_scan 
    device_add 
        driver_probe_device 
          acpi_device_probe 
            acpi_processor_add 

 

等一系列操作,最终一个新的 CPU 会被添加到相应的节点上并提供一个 sysfs 的接口供用户使用。然后用户可以通过执行诸如"echo 1 > /sys/devices/system/cpu/cpuX/online"这样的命令来使能新加入的 CPU,即 CPU online 操作,从而让 CPU 可以进入正常的调度作业。至此,一个新的 CPU 就被加入到正在运行的系统中了。

注:什么是 CPU_XXX_FROZEN 事件

在 CPU offline 操作中真正使用的事件只有 CPU_DOWN_PREPARE 和 CPU_DEAD,无论是 BSP 还是 non-boot 的 CPU。因为正在执行 offline 操作的 CPU,其上正在执行的 task 必定是活动的,否则无法执行代码。另外两个事件,CPU_DOWN_PREPARE_FROZEN / CPU_DEAD_FROZEN 其实是为了 non-boot CPU 在 suspend 的时候使用。换言之,suspend/resume 利用了 Hot Plug 的部分机制来完成自己的操作。如下所示:

 #define CPU_DOWN_PREPARE_FROZEN ( 
    CPU_DOWN_PREPARE | CPU_TASKS_FROZEN) 
 suspend_enter -> 
 disable_nonboot_cpus -> 
 /* 1 means CPU_TASKS_FROZEN */ 
 _cpu_down(cpu, 1)

 

online/offline 的 sys 接口位于 drivers/base/cpu.c 中:

 static SYSDEV_ATTR(online, 0644, show_online, store_online); 

 

以 CPU offline 为例,实现过程如下:

 store_online->cpu_down(_cpu_down) 

 

  • 发送通知事件 CPU_DOWN_PREPARE 或者 CPU_DOWN_PREPARE_FROZEN 给准备 offline 的 CPU,让其进行准备工作。这包括:
    • 将所有关联在准备 offline 的 CPU 上的进程迁移到其他 CPU 上
    • 将所有关联在准备 offline 的 CPU 上的中断迁移到其他 CPU 上
  • OS 调用体系结构相关的函数 __cpu_disable()执行体系结构相关的清理工作
  • 如果以上操作成功,则发送一个清理成功的事件到处于 CPU_DEAD 或者 CPU_DEAD_FROZEN 的 CPU 上


清单 2. CPU offline 的执行过程

				
 static int __ref _cpu_down(unsigned int cpu, int tasks_frozen) 
 { 
    .... 
    err = __raw_notifier_call_chain(&cpu_chain, CPU_DOWN_PREPARE| mod, 
                                hcpu, -1, &nr_calls); 
    .... 
    err = __stop_machine(take_cpu_down, &tcd_param, cpumask_of(cpu)); 
    .... 
    /* This actually kills the CPU. */ 
    __cpu_die(cpu); 

    /* CPU is completely dead: tell everyone.  Too late to complain. */ 
    if (raw_notifier_call_chain(&cpu_chain, CPU_DEAD| mod, 
                            hcpu) == NOTIFY_BAD) 
        BUG(); 
    .... 
 } 

 static int __ref take_cpu_down(void *_param) 
 { 
    struct take_cpu_down_param *param = _param; 
    int err; 

    /* Ensure this CPU doesn't handle any more interrupts. */ 
    err = __cpu_disable(); 
    if (err < 0) 
        return err; 

    raw_notifier_call_chain(&cpu_chain, CPU_DYING | param->mod, 
                            param->hcpu); 

    /* Force idle task to run as soon as we yield: it should 
        immediately notice cpu is offline and die quickly. */ 
       sched_idle_next(); 
		
    return 0; 
 } 

 

BSP Hot Remove 的限制

在 x86 架构中,默认的 timer 中断定义如下

 static struct irqaction irq0  = { 
  .handler = timer_interrupt, 
  .flags = IRQF_DISABLED | 
   IRQF_NOBALANCING| 
  IRQF_IRQPOLL | IRQF_TIMER, 
 .name = "timer"
	 };

 

由于 IRQF_NOBALANCING 的存在,在 IRQ descriptor 初始化的时候会和其他中断有所分别:

 __setup_irq 
  .... 
 /* Exclude IRQ from balancing */ 
 /* if requested */ 
 if (new->flags & IRQF_NOBALANCING) 
   desc->status |= IRQ_NO_BALANCING; 
 .... 
	

 

以使用 IOAPIC 中断控制器为例,在初始化中断向量表的 RTE(Redirection Table Entry)时,由于 IRQ_NO_BALANCING 的限制会使得 IRQ0 被绑定在了 BSP CPU 上。

 setup_ioapic_dest 
  .... 
 if (desc->status & (IRQ_NO_BALANCING| 
          IRQ_AFFINITY_SET)) 
    mask = desc->affinity; 
	   .... 
				

 

即使通过 /proc/irq/xx/smp_affnity 来修改 interrupt affinity 也是不可以的,因为在其对应的写接口实现中限制仍然存在:

 irq_affinity_proc_write 
 .... 
 if (!irq_to_desc(irq)->chip->set_affinity 
  ||no_irq_affinity || 
     irq_balancing_disabled(irq)) 
     return -EIO; 
	   .... 
				

 

目前 Linux 内核对 CPU online/offline 的实现已经比较稳定,但是对于 CPU/Memory 的 Hot Add 支持还不是很稳定,因为这涉及到 firmware/BIOS 与 OS 的交互,如插入一个新的内存模块时 OS 需要得到用来更新 NUMA 节点相关的 SRAT 表,这需要 firmware/BIOS 和 OS 两方面协同工作。这还主要是针对 64bit 的平台而言,至于 32bit 的平台,基本上还处于一个不可用的状态。这主要是实用性的问题,毕竟这类需求多用在服务器平台上。而对于服务器而言,由于内存尺寸,寻址空间等限制很少会选择 32bit 的平台,所以很多功能在 23bit 的平台上没有实现或者并没有充分测试过可用性。至于 Hot Remove,无论是软件,firmware/BIOS 甚至是硬件平台,其支持程度都很不成熟,到目前为止还无法真正投入使用。这也一定程度上限制了 Linux 在服务器领域的进一步拓展。


MCA(Machine Check Architecture)

Machine Check Exception 是一类由硬件错误触发的异常,譬如当 CPU 检测到总线,CHIPSET,内存,CACHE,TLB 等硬件出现致命错误时会触发这类异常。一般来说这些错误对系统的稳定性危害极大而且无法恢复,通常会触发系统的复位操作。这些错误在一个大型的服务器环境如服务器集群或者是云计算的环境下是不可避免的,因此必须对此有相应的处理机制。在 MCA 架构的出现之前,OS 对 MCE 的处理非常有限,经常就是简单的重启系统,有时也会显示如下所示的错误输出:

 CPU x: Machine check exception... 

 

或者

 Memory DIMM ID of error: ... 

 

对于管理员而言,简单的系统重启难以接受,而且出错现场经常无法保存,从而无法排错。即使能够保留下一些日志,除非有很强的专业知识,否则完全不知道真正产生错误的原因是什么。这些问题都在新的 MCA 中得到了解决和改进。利用新的 MCA 架构,OS 可以根据不同的错误源产生的错误类别,错误的严重程度,软件可以选择隔离错误,记录错误,甚至屏蔽错误源(对于不重要的非致命的错误类型)以避免干扰,或是必须要复位系统。而且在新的 MCA 架构下,错误的记录管理,以及可读性都有了很大的提高。

MCA 对各种错误的如表 1 所示:


表 1. MCA 错误分类分级表

Error Handling Category
System Reset
Multi-bit Error in Kernel
Non-Recoverable/Fatal
OS Recoverable: System Available
Multi-bit Error in Application
Recoverable
OS Corrected: Execution Continues
Patrol Scrub Error
Firmware Corrected: Execution Continues
Single-bit Error in Write-Through Cache
Corrected
Hardware Corrected: Execution Continues
Most Single-bit Errors

 

什么是 Recoverable

如果错误不可以被纠正,但是通过软件有选择的执行某些操作后可以恢复系统软件的正常运行而不会产生复位,那么这种错误类型称为 Recoverable

对于可以纠正的错误(CE -- corrected error)类型,需要通过 CMCI(Corrected Machine Check Interrupt)中断来处理(并不是所有的 CE 都需要)。这是一个存在于每个 CPU 的 Local Vector Table 中的中断向量而不是一个异常;对于不可纠正但是可以恢复的错误(UCR Error -- Uncorrected Recoverable Error),通过传统的 MCE 异常处理;对于致命的错误,软件也无法恢复的,将通过 MCE 强制系统复位。

MCA 为了实现对错误的分类分级,采用了以 BANK 为处理单位的机制。如图一所示:


图 1. MCA 寄存器缩略图

全局相关的寄存器组定义了如何开启 MCA 的能力。每一个 BANK 则具体对应一类错误源,如 CPU,MEMORY,CACHE,CHIPSET 等等。每一个 BANK 都可以进行单独的控制,这样软件就能够针对每一个 BANK 使用特定的方式进行处理。由于 MCA 以时间窗口为单位对错误进行采样,因此在每一个采样结束时有可能会发现有不止一个的错误产生,但是只会触发一次中断或者异常,因此当软件进行处理时必要要轮询所有的 BANK 以确保每一个产生的错误都可以被处理,见清单 3 代码所示:


清单 3. CMCI 和 MCE 的中断处理函数

				
 smp_threshold_interrupt 
    mce_threshold_vector (intel_threshold_interrupt) 
        machine_check_poll 
            { 
                .... 
                for (i = 0; i < banks; i++) { 
                    if (!mce_banks[i].ctl || !test_bit(i, *b)) 
                        continue; 
                .... 
            } 
 MCE 的中断处理函数
 machine_check_vector (do_machine_check) 
 { 
    .... 
    for (i = 0; i < banks; i++) { 
        __clear_bit(i, toclear); 
            if (!mce_banks[i].ctl) 
                continue; 
    .... 
 } 

 

CMCI 是在 MCA 架构中新增的一种中断,它的实现采用 threshold 的概念完成,如图二所示:


图 2. CMCI 中断触发逻辑

只有在中断使能的前提下,CMCI 才能起作用。如果 Error threshold 也使能,则只有当产生同类型的错误达到 threshold 的预设值时,一个 CMCI 中断才会产生,当预设值为 0 或者为负值时 CMCI 不起作用。注意,当中断发生后,软件必须负责清除 MCi_STATUS 寄存器,否则不会触发下一次 CMCI 中断。CMCI 是基于 BANK 的,也就是说每一个 BANK 都可以根据需要开关 CMCI 中断,threshold 的值也可以根据需要有所不同。

以 CACHE 相关的 BANK 为例,通过设置 threshold,用户可以对 CACHE 的当前状态进行读取。当 CACHE 错误事件的增加还未超过 threshold 时,其状态称为"green",表明 CACHE 是健康的;如果错误事件的记录超出这个 threshold 时,其状态会自动变为"yellow",表示当前 CACHE 仍然是可靠的,但是由于其报告的错误数量很大,超过了预设的阈值,处于报警状态。当管理员看到这样的信息时,最好尽快对系统,尤其是对 CPU 做相应的处理。以 Intel 的 CPU 为例,CMCI 的初始化代码如清单 4 所示:


清单 4. CMCI 的初始化过程

				
 static void intel_init_cmci(void) 
 { 
    int banks; 

    if (!cmci_supported(&banks)) 
        return; 

    mce_threshold_vector = intel_threshold_interrupt; 
    cmci_discover(banks, 1);
    /* 
     * For CPU #0 this runs with still disabled APIC, but that's 
     * ok because only the vector is set up. We still do another 
     * check for the banks later for CPU #0 just to make sure 
     * to not miss any events. 
     */ 
    apic_write(APIC_LVTCMCI, THRESHOLD_APIC_VECTOR|APIC_DM_FIXED); 
    cmci_recheck(); 
 } 

 

通过 cmci_discover即可对每一个可以使用 CMCI 的 bank 进行必要的初始化操作。

除了 CMCI,MCA 还新增了对 UCR Error(Uncorrected Recoverable Error)的支持。上文提到,UCR 通过 MCE 异常处理,不过 UCR 还可以细分为 SRAR 和 SRAO 两种不同的类型的 MCE。SRAR(software recoverable action required)和 SRAO(software recoverable action optional)的相同点是:对于已知类型的错误,OS 需要根据错误类型执行不同的动作将系统从错误状态中恢复;不同点是对于未知类型的错误,SRAR 需要将系统复位,而 SRAO 则可以让系统继续正常运行。下文讨论的 HWPOISON 就是利用了 UCR 的划分来完成不同的处理行为。

MCE 的错误会被记录在内核特定的缓冲区中,目前最大支持 32 个记录。当超过这个数时,除非原有的错误记录被读取过,否则新的错误不会覆盖原有的记录,这是基于更早的错误更有价值的假设来设定的。应用软件可以通过 /dev/mcelog 读取错误记录并进行解码。这是因为 MCE 产生的原始信息是经过编码的,内核没有实现将解码后的可读信息通过 /dev/mcelog 接口提供给用户,而是将原始信息交给用户层,由用户层的程序来解码,当然这仅仅是一个策略选择的问题。目前主要使用 mcelog 这个程序来完成以上功能。由于内核缓冲区十分有限,因此,mcelog 一般是作为一个 daemon 运行在后台,定时读取 /dev/mcelog 来获取最新的错误记录并将其保存在磁盘上。

由于 MCE 相对于其他中断异常十分稀有,因此测试 MCA 显得格外困难。当前采用 error inject 的方式来测试 MCA。通过 inject,可以触发真正的异常-可以看作是黑盒测试,通过异常来检测 OS 对 MCE 的处理正确与否;也可以产生虚假的(faked)异常-可以看作是白盒测试,开发人员可以通过 faked 类型的异常来测试代码的覆盖率和执行流程。内核通过 mce-inject 模块来实现相应的 inject 接口。如果支持 inject,则 /dev/mcelog 是可写的,否则只读。清单 5 展示了 inject 接口是如何支持写操作的过程。


清单 5. MCE inject 接口的注册过程

				
 static int inject_init(void) 
 { 
    if (!alloc_cpumask_var(&mce_inject_cpumask, GFP_KERNEL)) 
        return -ENOMEM; 
    printk(KERN_INFO "Machine check injector initialized/n"); 
    mce_chrdev_ops.write =
				mce_write; 
    register_die_notifier(&mce_raise_nb); 
    return 0; 
 } 
 mcelog 设备的注册过程如下:
 struct file_operations mce_chrdev_ops = { 
    .open                   = mce_open, 
    .release               = mce_release, 
    .read                   = mce_read, 
    .poll                   = mce_poll, 
    .unlocked_ioctl       = mce_ioctl, 
 }; 
 EXPORT_SYMBOL_GPL(mce_chrdev_ops); 

 static struct miscdevice mce_log_device = { 
    MISC_MCELOG_MINOR, 
    "mcelog", 
    &mce_chrdev_ops, 
 }; 

 static __init int mcheck_init_device(void) 
 { 
    .... 
    misc_register(&mce_log_device); 
    .... 
 } 

 

目前在用户空间主要使用的注入工具是 mce-inject。通过 mce-inject 和 mcelog 的联动,就可以触发各种特定的异常并分析异常的结果来测试 MCA 的工作正确与否。


HWPOISON

HWPOISN 的应用背景与基本原理

随着内存容量和密度的增长,内存出现错误的机率也在不断变大,如何保证系统的容错性以确保正常运行,是 RAS 中很重要的一个课题。而 HWPOISON 就是在这样一个应用背景下出现的技术。当硬件(如内存控制器)发现一个内存页中的数据出错并且无法被纠正,就会标记该页面,并抛出 MCE 异常并通知 OS 该页面已包含错误数据,需要立即被隔离。从而确保错误数据不会被自身或应用程序“消费”,或进一步被写入磁盘。一般来说,大多数出错的页面都可以被 OS 简单有效地隔离,从而避免错误的扩散甚至系统复位,但也有些 OS 不值得或者不可能处理的情况,比如页面被复杂数据结构所引用,或是包含了内核自身的代码和数据,或者恰好在硬件发现问题到 OS 隔离的时间间隙内被访问。如果这些未被隔离的页面被访问,将会被硬件捕获并触发新的 MCE 异常,从而导致 kernel panic。可隔离的页面包括 LRU page 和 buddy system 中的页面,都是和进程相关的;由内核分配使用的页面,如 driver,内核线程,slab 管理器等等都不能被隔离。对于可隔离的页面,如果这个页面映射的是磁盘文件,即 page cache,并且这个页面是干净的,也就是说和磁盘数据是同步的,那么直接丢弃这个页面即可,反之如果这个页面被改写过或者说是脏页则不能让这个有问题的内存页面污染磁盘文件,所有映射了该页面的进程必须被终止;如果这个页面是某个进程正在使用的匿名页面(anonymous page),譬如进程使用的栈或者堆,那么必须终止这个进程以防止错误扩散到本进程以外的其他地址空间中。由于 HWPOISON 的实现依赖于 MCA 架构的实现,而 MCA 架构只有在 Intel 较新的 Xeon 处理器(Nehalem-EX)中才获得支持,因此直到 Linux2.6.32,HWPOISON 才第一次被正式引入进来。

HWPOISON 对于 corrupted 页面的处理方式

当硬件检测到一个无法纠正的内存 ECC 校验错误时,如果这个页面正在被“消费”,会产生 Action Required 类型的 MCE 异常;反之则产生 Action Optional 类型的 MCE 异常。从产生 MCE 开始,这个内存页必须尽快从内存管理系统中隔离并且确保永远不会被使用,除非重启系统。但是由于内存页可能处于各种各样的状态,譬如被改写过,是否处于 swap 分区中,是 anonymous 映射还是 file-backed 映射等等,因此需要分门别类的进行相应的处理。总的来说有四种处理结果:recovery/delay/ignore/failure。Recovery 意味着 HWPOISON 采取了一定的动作,可能仅仅只有一步操作,也可能是一系列的处理,但是最终这个内存页被成功隔离,因此被称为 Recovery。这里的 recovery 并不是说内存错误被修复,这个内存页又可以使用了,而是指这个内存页从一切可以导向 / 访问这个页面的数据结构中隔离开了,如 buddy system,LRU 队列,radix tree 等等。对于 delay/ignore/failure 而言,它们和 recovery 最大的不同是如果当前处理动作是 recovery,则意味着内存页被隔离开了,而 delay/ignore/failure 并没有在处理行为结束时将内存页隔离开,而仅仅是在 OS 中标识了当前内存页被 poison 而已。Delay 意味着需要延后处理,譬如当 HWPOISON 面对的是一个出错的空闲的内存页时,HWPOISON 不用做过多的处理,只需要将其标识为 poison,等待后继的内存分配函数(这里指 prep_new_page)执行时自动将其跳过并从 buddy system 中隔离开,永不再用;Ignore 则意味着 HWPOISON 放弃处理某些类型的内存页,忽略即可。这些类型包括有:已经被标识为 poison 的内存页,无需再处理一次;无效的页面,如页框号超出了当前系统的最大内存;内核占用的页面,因为如果处理的话很可能导致系统变得更加不稳定;某些引用计数((struct page *)page->_count)为 0 的页面,这里特指那些为 high order 但是并没有标识为 PG_compound 的页面,这是因为一个 high order 的页面包含连续的几个物理页,但是只有 head 页才会更新引用计数,其他的页面中的引用计数均为 0,由于 high order 的页面是连续的物理地址,可能处于各种状态,例如正在被设备用作 DMA 传输,在这种情况下就无法杀掉使用的进程,因为这个"进程"就是内核本身,因此对这种页面目前没有再做细分处理,全部忽略;Failure 则针对以上所以处理情形之外的未知页或者是大页,或者是在处理的过程中出现问题也会视为 failure。这里所说的大页不是上文提及的 high order 的页面,而是指开启 PAE,PSE 之后使用的 2M/4M 甚至更大的页面,这种页面标识有 PG_compound 以确保这个大页面不会被细分。因为目前反向映射不提供对大页的支持,而且对大页的支持还需要将 hugetlbfs 考虑进来,由于其复杂性目前并没有实现对大页的支持,因此只能当作处理失败。以上四种处理结果,Recovery 和 Delay 都可以认为是“隔离成功”,因为 OS 确信该页面不会再被访问,从而造成系统崩溃;而 Ignore 和 Failure 都应当被视为“隔离失败”,因为系统可能在接下来的任何时刻崩溃。

Early Kill VS. Late Kill

当一个内存页是干净的状态时,无论这个页是映射磁盘文件的一个 page cache 页,还是进程使用的 anonymous 页,由于已经是和磁盘同步的,所以不用担心丢失数据,可以直接从磁盘读取或者在 page fault 的时候从磁盘中读取。因此处理逻辑也很简单:首先将所有使用这个页面的进程中对应的页表项取消映射,然后直接丢弃这个页面即可;但是对于脏页显然要麻烦的多,因为脏页面就意味着有数据还没来得及同步,因此无法从磁盘中恢复有效的数据,所以对于使用脏页的进程只能被杀掉。HWPOISON 的具体处理过程如下:首先要将所有使用这个页面的进程中对应的页表项取消映射,然后根据需要选择是否立即杀掉相应的进程。这里 HWPOISON 使用了一个 sysctl 接口 vm.memory_failure_early_kill 来选择是立刻杀掉所有有关的进程还是仅仅把对应的页表项取消映射,等到 page fault 的时候再进行必要的处理。如果是选择立刻杀掉进程的话,HWPOISON 通过构造一个 SIGBUS 信号来杀掉相应的进程。

 SIGBUS with siginfo_t payload 
    si_trapno       18 (on x86 means machine check) 
    si_addr         virtual memory page address of failure in current process 
    si_addr_lsb    LSB of granularity of the error (e.g. 12 for a 4K page) 
    si_code 
        BUS_MCEERR_AO 
 Action optional. Take the specific page out of free list or similar 
        BUS_MCEERR_AR 
 Action required. Error in current execution context. 
               Need to abort right now (siglongjmp etc.) 

 

反向映射(RMAP)

RMAP 提供了一个发现哪些进程正在使用给定的内存物理页的机制。当前反向映射的实现采用的是基于对象(VMA)而不是基于 PTE 的方式,以节省内存尤其是低端内存的占用。

即使是要杀掉的进程是像 qemu 这样的虚拟机客户端,也没有太大的不同,只要 qemu 能够将这个信号适当的转发到 guest vm 的内部,从逻辑上也讲也只会杀掉真正使用这个内存页的虚拟进程,而不会将整个虚拟机都杀掉。不过对于存在于 swap cache 中的页面处理则有些特别。因为放在 swap cache 中的页面有可能已经不存在通常意义上的 rmap,因而无法通过 rmap 遍历所有引用当前页面的进程,从而无法进行 early kill,因此对于 swap cache 中的页面只能使用 late kill 的方式进行处理,即使通过 sysctl 接口选择了 early kill 也是一样的。

HWPOISON 在 Linux 内核中的基本实现

HWPOISON 的核心实现放在了 mm/memory-failure.c 中。首先需要在 MCE 异常处理中将 poisoned 的内存页面保存下来供 HWPOISON 进行后继处理,如清单 6 所示:


清单 6. HWPOISON 与 MCE 的关联过程

				
 void do_machine_check(struct pt_regs *regs, long error_code) 
 { 
    .... 
    if (severity == MCE_AO_SEVERITY && mce_usable_address(&m)) 
        mce_ring_add(m.addr >> PAGE_SHIFT); 
    .... 
 } 

 

然后 MCE 的工作线程 mce_process_work会调用 mce_notify_process进入 HWPOISON 的主入口 memory_failure来执行以上所述的各种处理逻辑。


清单 7. HWPOISON 在 Linux 内核中的核心接口

				
 void mce_notify_process(void) 
 { 
     unsigned long pfn; 
     mce_notify_irq(); 
     while (mce_ring_get(&pfn)) 
         memory_failure(pfn, MCE_VECTOR);
 } 

 

在 HWPOISON 的核心逻辑之外,其涉及的范围涵盖了内存管理的方方面面,譬如在页错处理时不能将一个已经 poisoned 的页面重新映射给进程使用,因此会有如清单 8 所示的处理:


清单 8. 页错处理中对 HWPOISON 的支持

				
 static int __do_fault(struct mm_struct *mm, struct vm_area_struct *vma, 
                unsigned long address, pmd_t *pmd, 
                pgoff_t pgoff, unsigned int flags, pte_t orig_pte) 
 { 
     .... 
     if (unlikely(PageHWPoison(vmf.page))) { 
          if (ret & VM_FAULT_LOCKED) 
              unlock_page(vmf.page); 
          return VM_FAULT_HWPOISON; 
     } 
     .... 
 } 

 

又比如为了支持软件 inject 功能,需要在 madvice 系统调用中添加一个新的调用类别:


清单 9. HWPOISON 在 madvice 系统调用中增加的调用类别

				
 SYSCALL_DEFINE3(madvise, unsigned long, start, size_t, len_in, int, behavior) 
 { 
    .... 
    #ifdef CONFIG_MEMORY_FAILURE 
        if (behavior == MADV_HWPOISON || behavior == MADV_SOFT_OFFLINE) 
            return madvise_hwpoison(behavior, start, start+len_in); 
    #endif 
    .... 
 } 

 /* 
 * Error injection support for memory error handling. 
 */ 
 static int madvise_hwpoison(int bhv, unsigned long start, unsigned long end) 
 { 
    int ret = 0; 

    if (!capable(CAP_SYS_ADMIN)) 
        return -EPERM; 
    for (; start < end; start += PAGE_SIZE) { 
        struct page *p; 
        int ret = get_user_pages_fast(start, 1, 0, &p); 
        if (ret != 1) 
            return ret; 
        if (bhv == MADV_SOFT_OFFLINE) { 
            printk(KERN_INFO "Soft offlining page %lx at %lx/n", 
                    page_to_pfn(p), start); 
            ret = soft_offline_page(p, MF_COUNT_INCREASED); 
            if (ret) 
                break; 
            continue; 
        } 
        printk(KERN_INFO "Injecting memory failure for page %lx at %lx/n", 
                page_to_pfn(p), start); 
        /* Ignore return value for now */ 
        __memory_failure(page_to_pfn(p), 0, MF_COUNT_INCREASED); 
    } 
    return ret; 
 } 

 

所有有关 HWPOISON 的内核 patch 可以通过在 linux 的主 git tree 中以 HWPOISON 为关键字进行搜索即可。与此同时,正是由于 HWPOISON 在内核中的实现分布很广,因而测试起来格外困难。为此,HWPOISON 本身就提供了非常多的测试接口。

  • madvice(address, length, MADV_POISON)可以用来直接选择要 poison 的虚拟地址
  • debugfs 接口 /sys/kernel/debug/hwpoison/corrupt-pfn 可以直接选择要测试的页框号
  • 通过 /dev/mcelog 可以直接读写 MCA 寄存器来完成各种 inject 操作

所有的这些接口都可以使用 mce-test 这个 test suite 来完成集成测试。通过 mce-test 可以完成对 HWPOISON 的功能测试,覆盖率测试和压力测试。其核心测试程序存放在 mce-test 安装目录 /hwpoison/tinjpage.c 中。具体的测试方法可以参考 mce-test 工程中自带的使用说明以获得详细的指导步骤。


APEI(ACPI Platform Error Interface)

CPER & Serialization

标准错误接口 CPER(Common Platform Error Record)定义在 UEFI 2.1 specification 的 Appendix N 中。

Serialization 类似于 C++ 中的流,这里特指使用 CPER 的编码方式将记录保存在 non-volatile 的设备如 flash/NVRAM 上。

APEI 是定义在 ACPI4.0 规范中的一个面向硬件错误管理的接口。APEI 的产生,主要是为了统一 firmware/BIOS 和 OS 之间的错误交互机制,使用标准的错误接口进行管理,同时也扩展了错误接口的内容以便实现更加灵活丰富的功能,通过 APEI,firmware/BIOS 甚至可以在将错误报告到 OS 之前就进行解析,譬如通过 firmware/BIOS 提供一个简单的管理环境,不用进入 OS 就可以分析处理错误,或者利用 APEI 人为注入错误以测试某些特定的错误是否可以正确处理。本质上说,APEI 就是四张表:Error Record Serialization Table (ERST),BOOT Error Record Table (BERT),Hardware Error Source Table (HEST),Error Injection Table (EINJ)。ERST 用来保存错误记录,通过 daemon 程序可以定期写入到 flash 或者 NVRAM 这类非易失性介质中永久保存;BERT 用来记录本次启动之前保留下来的未处理的错误;HEST 则提供了对各式各样的硬件错误源的控制管理;EINJ 则是提供了一个便捷的错误注入接口以方便测试其他的 APEI 接口和相关的 RAS 特性。

APEI 所涵盖的硬件错误类型十分丰富,包括处理器,chipset,总线以及 I/O 设备产生的各种硬件错误。对于 MCE,NMI 这样的严重错误,根据错误的严重程度,可以选择通过 CPER 记录下来,或是需要重启系统,在重启系统之前同时使用 BERT 保存本次的出错信息供重启后继续处理;对于普通的错误则可以定期轮询并记录保存在磁盘上。

APEI 提供的 inject 机制十分便利。通过一个与 OS 无关的接口,注入需要的硬件错误,从而检测硬件是否可以正确的错误报告,从而达到了诊断硬件的目的。在这个基础上,OS 可以提供一个简便的错误处理机制来完善软件对硬件的诊断和管理。APEI 的 inject 实现基本上是一个 2 步操作:

  1. 软件通过 SET_ERROR_TYPE 这个动作每次在 EINJ 表中注入一个错误
  2. 软件通过 GET_TRIGGER_ERROR_ACTION_TABLE 得到需要的 Trigger Error Action 表,然后通过读写这个表来触发上一步注入的错误

从表二中可以清楚的看到 EINJ 工作的具体过程,具体的实现流程可以参见 ACPI 4.0 SPEC 的 17 章。


图 3. EINJ 执行过程

由于目前内核对 APEI 的支持才刚刚起步,相关的实现还没有合并到内核中。读者可以在 linux-acpi 的 git tree 中搜索相关的 patch(作者为 Huang Ying,实现主要放在 drivers/acpi/apei 目录下)。


展望

从当前的实际情况来说,一个 RAS 系统很大程度上更加依赖于硬件本身的纠错和恢复能力。以内存的 migration 为例,它是通过内存控制器,CPU 和相关的总线控制来实现将一个内存模块中的数据无缝迁移到其他的内存模块中,所有的操作对于软件来说是完全透明的,在迁移完成后所有的内存物理地址不会发生任何变化。这在简化软件复杂度的同时也带来了新的问题:譬如在一个 NUMA 系统中每个节点(node)访问其他节点的内存速度是不一样的,如果内存的 migration 操作对 OS 完全透明的话,软件就无法根据 migration 之后新的内存布局来调整使用内存的方式,这样就会降低系统的整体性能;从另一方面而言,内存的 migration 操作是由硬件发起的,软件不能主动发起,这样当需要更换内存时只能等待内存硬件出问题或者下线整个系统才能进行处理,非常不便。但是,为了实现软件上的灵活性,还需要硬件提供更多更强大的接口和处理机制,firmware 需要更新以确保硬件和软件的交流足够便利和可行,至于软件,则有更多的代码有待实现和完善。

除了本文介绍的几个 RAS 特性以外,还有更多的 RAS 特性并没有被介绍,譬如 PCI-e AER(Advance Error Report),ACPI 中的 aggregator device driver,PFA(Predictive Failure Analysis,用来协助 Memory Migration)等等。这些特性目前有的已经在 Linux 内核中实现,有的正在实现过程中,有兴趣的读者可以通过网络以及有关的文档,代码做进一步的了解。

你可能感兴趣的:(RAS 在 x86 上的应用及 Linux 实现)