编写设备驱动程序

第 13 章 强化 Solaris 驱动程序

借助故障管理体系结构 (Fault Management Architecture, FMA) I/O 故障服务,驱动程序开发者可将故障管理功能集成到 I/O 设备驱动程序中。Solaris I/O 故障服务框架定义了一组接口,使得所有驱动程序可以协调工作,并执行基本的错误处理任务和活动。总体上,Solaris FMA 除了可进行响应和恢复外,还可进行错误处理和故障诊断。FMA 是 Sun 的预测性自我修复策略的一个组成部分。

当驱动程序除了将 I/O 故障服务框架用于错误处理和诊断外,还使用本文档中介绍的防御性编程做法时,认为该驱动程序已经过强化。驱动程序强化测试工具测试是否已正确实现 I/O 故障服务和防御性编程要求。

本文档包含以下各节:

Sun 故障管理体系结构 I/O 故障服务

本节介绍如何为 I/O 设备驱动程序集成故障管理错误报告、错误处理和诊断。本节深入探讨了 I/O 故障服务框架以及如何在设备驱动程序内利用 I/O 故障服务 API。

本节讨论以下主题:

什么是预测性自我修复?

传统上,系统会将硬件和软件错误信息以系统日志消息的形式直接导出给管理员以及管理软件。错误检测、诊断、报告和处理通常会嵌入到每个驱动程序的代码中。

Solaris OS 预测性自我修复系统这样的系统是最早的也是最重要的自我诊断系统。自我诊断意味着系统提供根据观察到的症状自动诊断问题的技术,然后将诊断结果用于触发自动响应和恢复。硬件故障或软件缺陷可与一组可能观察到的、称为错误的症状相关联。系统检测到错误后所生成的数据称为错误报告或 ereport

在具有自我修复功能的系统中,ereport 由系统捕获,并编码为由可扩展事件协议描述的一组名称-值对,从而形成 ereport 事件。收集 ereport 事件和其他数据是为了便于进行自我修复,还会将其分发给名为诊断引擎的软件组件,诊断引擎设计用来诊断与系统检测到的错误症状对应的底层问题。诊断引擎在后台运行,并以无提示方式使用错误遥测,直到它可以生成诊断或预测故障为止。

在处理足够的遥测从而得出结论之后,诊断引擎会生成名为故障事件的另一个事件。然后,会向对特定故障事件感兴趣的所有代理广播该故障事件。代理是可对特定故障事件启动恢复和做出响应的软件组件。被称为 Solaris Fault Manager 的软件组件 fmd(1M) 可在 ereport 生成器、诊断引擎和代理软件之间管理事件的多路复用。

Solaris Fault Manager

Solaris Fault Manager fmd(1M) 负责将传入错误遥测事件分发给相应的诊断引擎。诊断引擎负责识别产生错误症状的基础硬件故障或软件缺陷。fmd(1M) 守护进程是故障管理器的 Solaris OS 实现。它在引导时启动,并且会装入系统中可用的所有诊断引擎和代理。Solaris Fault Manager 还为系统管理员和服务人员提供用于观察故障管理活动的界面。

诊断、可疑列表和故障事件

进行诊断后,会以 list.suspect 事件的形式输出诊断。list.suspect 事件是由一个或多个可能的故障或缺陷事件构成的事件。有时候,诊断无法将错误原因的范围缩小至单个故障或缺陷。例如,底层问题可能是连接控制器与主系统总线的线路中断。问题也可能在于总线上的某个组件或总线本身。在这种特定情况下,list.suspect 事件将包含多个故障事件:一个是连接到总线的每个控制器的事件,另一个是总线本身的事件。

除了介绍诊断的故障外,故障事件还包含四个可应用诊断的有效负荷成员。

例如,在给定时间内针对特定内存位置收到特定数量的 ECC 可纠正错误后,CPU 和内存诊断引擎会对有故障的 DIMM 发出诊断(list.suspect 事件)。


# fmdump -v -u 38bd6f1b-a4de-4c21-db4e-ccd26fa8573c
TIME                 UUID                                 SUNW-MSG-ID
Oct 31 13:40:18.1864 38bd6f1b-a4de-4c21-db4e-ccd26fa8573c AMD-8000-8L
100%  fault.cpu.amd.icachetag

Problem in: hc:///motherboard=0/chip=0/cpu=0
Affects: cpu:///cpuid=0
FRU: hc:///motherboard=0/chip=0
Location: SLOT 2

在此示例中,fmd(1M) 识别到资源中的一个问题,具体而言是 CPU (hc:///motherboard=0/chip=0/cpu=0 )。为了抑制出现更多错误症状并防止发生无法纠正的错误,对一个 ASRU (cpu:///cpuid=0) 进行了标识,以便弃用 (retirement)。需要更换的组件是 FRU (hc:///motherboard=0/chip=0)。

响应代理

代理是可为了响应诊断或修复而执行操作的软件组件。例如,CPU 和内存弃用代理设计为对包含 fault.cpu.* 事件的 list.suspect 执行操作。cpumem-retire 代理将尝试在服务中使 CPU 脱机或弃用物理内存页。如果该代理成功,则会在故障管理器的 ASRU 缓存中为成功弃用的页或 CPU 添加一项。以下示例所示的 fmadm(1M) 实用程序显示了被诊断为有故障的内存等级的一项。系统无法使其脱机、将其弃用或禁用的 ASRU 也会在 ASRU 缓存中存在一项,但会将其视为被降级。降级意味着与 ASRU 关联的资源有故障,但无法从服务中将该 ASRU 删除。目前,Solaris 代理软件不能对 I/O ASRU(设备实例)执行操作。缓存中所有有故障的 I/O 资源项都处于降级状态。


# fmadm faulty
   STATE RESOURCE / UUID
-------- ----------------------------------------------------------------------
degraded mem:///motherboard=0/chip=1/memory-controller=0/dimm=3/rank=0
         ccae89df-2217-4f5c-add4-d920f78b4faf
-------- ----------------------------------------------------------------------

弃用代理的主要用途是隔离(从服务中安全删除)被诊断为有故障的硬件或软件部分。

代理还可以执行其他重要操作,例如以下操作:

消息 ID 和字典文件

系统日志消息代理会提取诊断的输出(list.suspect 事件),并将特定消息写入控制台或 /var/adm/messages。控制台消息通常可能会难以理解。FMA 提供一个每当将 list.suspect 事件发送至系统日志消息时都会生成的已定义故障消息结构,从而对此问题进行了修正。

系统日志代理会生成一个消息标识符 (message identifier, MSG ID)。事件注册表生成字典文件(.dict 文件),这些文件可将 list.suspect 事件映射到将用来标识和查看关联知识文章的结构化消息标识符。消息文件(.po 文件)则将消息 ID 映射到诊断引擎可以生成的每个可能的可疑故障列表中的本地化消息。下面是一个测试系统中发出的故障消息示例。


SUNW-MSG-ID: AMD-8000-7U, TYPE: Fault, VER: 1, SEVERITY: Major
EVENT-TIME: Fri Jul 28 04:26:51 PDT 2006
PLATFORM: Sun Fire V40z, CSN: XG051535088, HOSTNAME: parity
SOURCE: eft, REV: 1.16
EVENT-ID: add96f65-5473-69e6-dbe1-8b3d00d5c47b
DESC: The number of errors associated with this CPU has exceeded 
acceptable levels. Refer to http://sun.com/msg/AMD-8000-7U for 
more information.
AUTO-RESPONSE: An attempt will be made to remove this CPU from service.
IMPACT: Performance of this system may be affected.
REC-ACTION: Schedule a repair procedure to replace the affected CPU. 
Use fmdump -v -u <EVENT_ID> to identify the module.

系统拓扑

为了确定可能发生故障的位置,诊断引擎需要表示出给定软件或硬件系统的拓扑。fmd(1M) 守护进程为诊断引擎提供了一个可在诊断期间使用的拓扑快照句柄。拓扑信息用来表示在每个故障事件中找到的资源、ASRU 和 FRU。拓扑也可以用来存储平台标签、FRUID 和序列号标识。

故障事件中的资源有效负荷成员始终由平台机箱周围组件的物理路径位置来表示。例如,从主系统总线桥接至 PCI 本地总线的 PCI 控制器功能由其 hc 模式路径名来表示:

hc:///motherboard=0/hostbridge=1/pcibus=0/pcidev=13/pcifn=0

故障事件中的 ASRU 有效负荷成员通常由绑定到硬件控制器、设备或功能的 Solaris 设备树实例名称来表示。对于可能由专门为 I/O 设备设计的弃用代理的将来实现执行的操作,FMA 使用 dev 模式以其本地格式表示 ASRU:

dev:////pci@1e,600000/ide@d

故障事件中的 FRU 有效负荷表示形式随距离被诊断为有故障的 I/O 资源最近的可更换组件而异。例如,中断的嵌入式 PCI 控制器的故障事件可能会将系统的主板命名为需要更换的 FRU:

hc:///motherboard=0

标签有效负荷是一个字符串,用于按照 FRU 在机箱或主板上的印刷形式给出其位置,例如,在 DIMM 插槽或 PCI 卡插槽旁边。

Label: SLOT 2

错误处理

本节介绍如何使用 I/O 故障服务 API 来处理驱动程序内的错误。本节讨论驱动程序应如何指示和初始化其故障管理功能、生成错误报告以及注册驱动程序的错误处理程序例程。

摘录内容来自源代码示例,这些示例演示如何从 Broadcom 1Gb NIC 驱动程序 bge 中使用 I/O 故障服务 API。以这些示例为模型,了解如何将故障管理功能集成到您自己的驱动程序中。请按照以下步骤研究完整的 bge 驱动程序代码:

被指示提供 FMA 错误报告遥测的驱动程序将检测错误,并确定这些错误对驱动程序所提供服务的影响。检测到错误后,驱动程序应确定其服务受影响的时间以及程度。

I/O 驱动程序必须立即响应检测到的错误。相应的响应包括:

驱动程序检测到的错误以 ereport 的形式传递给故障管理守护进程。ereport 是由 FMA 事件协议定义的结构化事件。该事件协议是一组常用数据字段的规范,除了可疑故障列表外,这些字段还必须用于描述所有可能的错误和故障事件。Ereport 被收集为错误遥测流,并分发给诊断引擎。

声明故障管理功能

强化的设备驱动程序必须向 I/O 故障管理框架声明其故障管理功能。使用 ddi_fm_init(9F) 函数声明驱动程序的故障管理功能。

void ddi_fm_init(dev_info_t *dip, int *fmcap, ddi_iblock_cookie_t *ibcp)

可从驱动程序 attach(9E)detach(9E) 入口点的内核上下文中调用 ddi_fm_init() 函数。通常会从 attach() 入口点调用 ddi_fm_init() 函数。ddi_fm_init() 函数根据 fmcap 来分配和初始化资源。fmcap 参数必须设置为以下故障管理功能的按位或:

强化的叶驱动程序通常设置上述所有功能。但是,如果父结点不能支持任何一项请求的功能,则关联的位会被清除并按此情况返回给驱动程序。在从 ddi_fm_init(9F) 返回之前,I/O 故障服务框架会创建一组故障管理功能属性: fm-ereport-capablefm-accchk-capablefm-dmachk-capablefm-errcb-capable。可使用 prtconf(1M) 命令来观察当前支持的故障管理功能级别。

为了使驱动程序支持故障管理功能的管理选择,请导出故障管理功能级别属性并将其设置为上面 driver.conf(4) 文件中描述的值。在使用所需功能列表调用 ddi_fm_init() 之前,必须设置并读取 fm-capable 属性。

来自 bge 驱动程序的以下示例显示了 bge_fm_init() 函数,该函数调用 ddi_fm_init(9F) 函数的包装。可在 bge_attach() 函数中调用 bge_fm_init() 函数。

static void
bge_fm_init(bge_t *bgep)
{
        ddi_iblock_cookie_t iblk;

        /* Only register with IO Fault Services if we have some capability */
        if (bgep->fm_capabilities) {
                bge_reg_accattr.devacc_attr_access = DDI_FLAGERR_ACC;
                dma_attr.dma_attr_flags = DDI_DMA_FLAGERR;
                /* 
                 * Register capabilities with IO Fault Services
                 */
                ddi_fm_init(bgep->devinfo, &bgep->fm_capabilities, &iblk);
                /*
                 * Initialize pci ereport capabilities if ereport capable
                 */
                if (DDI_FM_EREPORT_CAP(bgep->fm_capabilities) ||
                    DDI_FM_ERRCB_CAP(bgep->fm_capabilities))
                        pci_ereport_setup(bgep->devinfo);
                /*
                 * Register error callback if error callback capable
                 */
                if (DDI_FM_ERRCB_CAP(bgep->fm_capabilities))
                        ddi_fm_handler_register(bgep->devinfo,
                        bge_fm_error_cb, (void*) bgep);
        } else {
                /*
                 * These fields have to be cleared of FMA if there are no
                 * FMA capabilities at runtime.
                 */
                bge_reg_accattr.devacc_attr_access = DDI_DEFAULT_ACC;
                dma_attr.dma_attr_flags = 0;
        }
}

清除故障管理资源

ddi_fm_fini(9F) 函数清除为支持 dip 的故障管理而分配的资源。

void ddi_fm_fini(dev_info_t *dip)

可从驱动程序 attach(9E)detach(9E) 入口点的内核上下文中调用 ddi_fm_fini() 函数。

来自 bge 驱动程序的以下示例显示了 bge_fm_fini() 函数,该函数调用 ddi_fm_fini(9F) 函数的包装。可在 bge_unattach() 函数中调用 bge_fm_fini() 函数,而在 bge_attach()bge_detach() 函数中调用 bge_unattach 函数。

static void
bge_fm_fini(bge_t *bgep)
{
        /* Only unregister FMA capabilities if we registered some */
        if (bgep->fm_capabilities) {
                /*
                 * Release any resources allocated by pci_ereport_setup()
                 */
                if (DDI_FM_EREPORT_CAP(bgep->fm_capabilities) ||
                    DDI_FM_ERRCB_CAP(bgep->fm_capabilities))
                        pci_ereport_teardown(bgep->devinfo);
                /*
                 * Un-register error callback if error callback capable
                 */
                if (DDI_FM_ERRCB_CAP(bgep->fm_capabilities))
                        ddi_fm_handler_unregister(bgep->devinfo);
                /*
                 * Unregister from IO Fault Services
                 */
                ddi_fm_fini(bgep->devinfo);
        }
}

获取故障管理功能位掩码

ddi_fm_capable(9F) 函数返回当前为 dip 设置的功能位掩码。

void ddi_fm_capable(dev_info_t *dip)

报告错误

本节提供有关以下主题的信息:

对错误事件排队

ddi_fm_ereport_post(9F) 函数对 ereport 事件排队,以便传送给故障管理器守护进程 fmd(1M)

void ddi_fm_ereport_post(dev_info_t *dip, 
                         const char *error_class, 
                         uint64_t ena, 
                         int sflag, ...)

sflag 参数指示调用方是否愿意等待系统内存和事件通道资源变为可用。

ENA 指示此错误报告的错误编号关联 (Error Numeric Association, ENA)。ENA 可能已初始化,并且是从其他错误检测软件模块(如总线结点驱动程序)中获得的。如果 ENA 设置为 0,它将被 ddi_fm_ereport_post() 初始化。

名称-值对 (nvpair) 变量参数列表包含非数组 data_type_t 类型的一个或多个名称、类型、值指针 nvpair 元组,或者包含 data_type_t 数组类型的一个或多个名称、类型、元素数、值指针元组。nvpair 元组补足诊断所需要的 ereport 事件有效负荷。参数列表的结尾由 NULL 指定。

报告标准 I/O 控制器错误中介绍的用于 I/O 控制器的 ereport 类名和有效负荷可适用于 error_class。可以定义其他 ereport 类名和有效负荷,但必须在 Sun 事件注册表中进行注册,并伴有特定于驱动程序的诊断引擎软件或 Eversholt 故障树 (Eversholt fault tree, eft) 规则。有关 Sun 事件注册表和 Eversholt 故障树规则的更多信息,请参见 OpenSolaris 项目故障管理社区

void
bge_fm_ereport(bge_t *bgep, char *detail)
{
        uint64_t ena;
        char buf[FM_MAX_CLASS];
        (void) snprintf(buf, FM_MAX_CLASS, "%s.%s", DDI_FM_DEVICE, detail);
        ena = fm_ena_generate(0, FM_ENA_FMT1);
        if (DDI_FM_EREPORT_CAP(bgep->fm_capabilities)) {
                ddi_fm_ereport_post(bgep->devinfo, buf, ena, DDI_NOSLEEP,
                    FM_VERSION, DATA_TYPE_UINT8, FM_EREPORT_VERS0, NULL);
        }
}

检测和报告与 PCI 相关的错误

使用 pci_ereport_post(9F) 时,会自动检测和报告与 PCI(包括 PCI、PCI-X 和 PCI-E)相关的错误。

void pci_ereport_post(dev_info_t *dip, ddi_fm_error_t *derr, uint16_t *xx_status)

驱动程序不需要为 PCI 本地总线配置状态寄存器中发生的错误生成特定于驱动程序的 ereport。pci_ereport_post() 函数可以报告数据奇偶校验错误、主机异常中止、目标异常中止、发出信号的系统错误等。

如果 pci_ereport_post() 将由驱动程序使用,则此前 pci_ereport_setup(9F) 必须已经在驱动程序的 attach(9E) 例程中调用,pci_ereport_teardown(9F) 随后必须在驱动程序的 detach(9E) 例程中调用。

下面的 bge 代码样例显示了从驱动程序的错误处理程序中调用 pci_ereport_post() 函数的 bge 驱动程序。另请参见注册错误处理程序

/*
 * The I/O fault service error handling callback function
 */
/*ARGSUSED*/
static int
bge_fm_error_cb(dev_info_t *dip, ddi_fm_error_t *err, const void *impl_data)
{
     /*
      * as the driver can always deal with an error 
      * in any dma or access handle, we can just return 
      * the fme_status value.
      */
     pci_ereport_post(dip, err, NULL);
     return (err->fme_status);
}

报告标准 I/O 控制器错误

针对 I/O 控制器的常见错误定义了一组标准的设备 ereport。只要检测到本节中所述的错误症状之一,便应生成这些 ereport。

本节中所述的 ereport 将分发给 eft 诊断引擎以进行诊断,eft 诊断引擎使用一组常用的标准规则来诊断这些 ereport。设备驱动程序检测的其他任何错误都必须在 Sun 事件注册表中定义为 ereport 事件,并必须伴有特定于设备的诊断软件或 eft 规则。

DDI_FM_DEVICE_INVAL_STATE

驱动程序已检测到设备处于无效状态。

当驱动程序检测到所传送或接收的数据看起来无效时,该驱动程序应发布错误。例如,在 bge 代码中,当 bge_chip_reset()bge_receive_ring() 例程检测到无效数据时,这些例程将生成 ereport.io.device.inval_state 错误。

/*
 * The SEND INDEX registers should be reset to zero by the
 * global chip reset; if they're not, there'll be trouble
 * later on.
 */
sx0 = bge_reg_get32(bgep, NIC_DIAG_SEND_INDEX_REG(0));
if (sx0 != 0) {
    BGE_REPORT((bgep, "SEND INDEX - device didn't RESET"));
    bge_fm_ereport(bgep, DDI_FM_DEVICE_INVAL_STATE);
    return (DDI_FAILURE);
}
/* ... */
/*
 * Sync (all) the receive ring descriptors
 * before accepting the packets they describe
 */
DMA_SYNC(rrp->desc, DDI_DMA_SYNC_FORKERNEL);
if (*rrp->prod_index_p >= rrp->desc.nslots) {
    bgep->bge_chip_state = BGE_CHIP_ERROR;
    bge_fm_ereport(bgep, DDI_FM_DEVICE_INVAL_STATE);
    return (NULL);
}
DDI_FM_DEVICE_INTERN_CORR

设备已报告自我纠正的内部错误。例如,设备的内部缓冲区中的硬件已检测到可纠正的 ECC 错误。

bge 驱动程序中未使用此错误标志。有关使用此错误的示例,请参见 OpenSolaris 中的 nxge_fm.c 文件。执行以下步骤来研究 nxge 驱动程序代码:

DDI_FM_DEVICE_INTERN_UNCORR

设备已报告无法纠正的内部错误。例如,设备的内部缓冲区中的硬件已检测到不可纠正的 ECC 错误。

bge 驱动程序中未使用此错误标志。有关使用此错误的示例,请参见 OpenSolaris 中的 nxge_fm.c 文件。

DDI_FM_DEVICE_STALL

驱动程序检测到数据传输已意外停顿。

bge_factotum_stall_check() 例程提供了停顿检测的示例。

dogval = bge_atomic_shl32(&bgep->watchdog, 1);
if (dogval < bge_watchdog_count)
    return (B_FALSE);

BGE_REPORT((bgep, "Tx stall detected, 
watchdog code 0x%x", dogval));
bge_fm_ereport(bgep, DDI_FM_DEVICE_STALL);
return (B_TRUE);
DDI_FM_DEVICE_NO_RESPONSE

设备未对驱动程序命令进行响应。

bge_chip_poll_engine(bge_t *bgep, bge_regno_t regno,
        uint32_t mask, uint32_t val)
{
        uint32_t regval;
        uint32_t n;

        for (n = 200; n; --n) {
                regval = bge_reg_get32(bgep, regno);
                if ((regval & mask) == val)
                        return (B_TRUE);
                drv_usecwait(100);
        }
        bge_fm_ereport(bgep, DDI_FM_DEVICE_NO_RESPONSE);
        return (B_FALSE);
}
DDI_FM_DEVICE_BADINT_LIMIT

设备引发了过多的连续性无效中断。

bge() 驱动程序内的 bge_intr 例程提供了有问题的中断检测的示例。bge_fm_ereport() 函数是 ddi_fm_ereport_post(9F) 函数的包装。请参见对错误事件排队中的 bge_fm_ereport() 示例。

if (bgep->missed_dmas >= bge_dma_miss_limit) {
    /*
     * If this happens multiple times in a row,
     * it means DMA is just not working.  Maybe
     * the chip has failed, or maybe there's a
     * problem on the PCI bus or in the host-PCI
     * bridge (Tomatillo).
     *
     * At all events, we want to stop further
     * interrupts and let the recovery code take
     * over to see whether anything can be done
     * about it ...
     */
    bge_fm_ereport(bgep,
        DDI_FM_DEVICE_BADINT_LIMIT);
    goto chip_stop;
}

服务影响函数

具有故障管理功能的驱动程序必须指示错误是否影响了设备所提供的服务。检测错误并在必要时关闭服务之后,驱动程序应调用 ddi_fm_service_impact(9F) 例程来反映设备实例的当前服务状态。诊断和恢复软件可以使用该服务状态来帮助确定问题或对问题做出反应。

当驱动程序本身检测到错误时以及框架检测到错误并将访问或 DMA 句柄标记为有故障时,均应调用 ddi_fm_service_impact() 例程。

void ddi_fm_service_impact(dev_info_t *dip, int svc_impact)

ddi_fm_service_impact() 接受以下服务影响值 (svc_impact):

DDI_SERVICE_LOST

由于设备故障或软件缺陷,设备提供的服务不可用。

DDI_SERVICE_DEGRADED

驱动程序无法提供正常服务,但驱动程序可以提供部分服务或降级的服务。例如,驱动程序可能必须重复尝试执行操作才能取得成功,或者它至少要以配置的速度运行。

DDI_SERVICE_UNAFFECTED

驱动程序已检测到错误,但设备实例提供的服务不会受到影响。

DDI_SERVICE_RESTORED

设备提供的所有服务都已恢复。

调用 ddi_fm_service_impact() 时会根据服务影响例程的服务影响参数代表驱动程序生成以下 ereport:

在以下 bge 代码中,驱动程序确定由于出现错误,它无法成功地重新开始传送或接收数据包。设备的服务状态转换为 DDI_SERVICE_LOST。

/*
 * All OK, reinitialize hardware and kick off GLD scheduling
 */
mutex_enter(bgep->genlock);
if (bge_restart(bgep, B_TRUE) != DDI_SUCCESS) {
    (void) bge_check_acc_handle(bgep, bgep->cfg_handle);
    (void) bge_check_acc_handle(bgep, bgep->io_handle);
    ddi_fm_service_impact(bgep->devinfo, DDI_SERVICE_LOST);
    mutex_exit(bgep->genlock);
    return (DDI_FAILURE);
}

注 –

不应从已注册的回调例程中调用 ddi_fm_service_impact() 函数。


访问属性结构

DDI_FM_ACCCHK_CAPABLE 设备驱动程序必须设置其访问属性,以指示它能够处理寄存器读取或写入期间发生的程控 I/O (programmed I/O, PIO) 访问错误。应将 ddi_device_acc_attr(9S) 结构中的 devacc_attr_access 字段设置为驱动程序可以检查和处理数据路径错误的系统的指示器。ddi_device_acc_attr 结构包含以下成员:

ushort_t devacc_attr_version;
uchar_t devacc_attr_endian_flags;
uchar_t devacc_attr_dataorder;
uchar_t devacc_attr_access;             /* access error protection */

在到设备或来自设备的数据路径中检测到的错误可由设备驱动程序的一个或多个父结点来处理。

devacc_attr_version 字段必须至少设置为 DDI_DEVICE_ATTR_V1。如果 devacc_attr_version 字段未设置为 DDI_DEVICE_ATTR_V1,则将忽略 devacc_attr_access 字段。

可将 devacc_attr_access 字段设置为以下值:

DDI_DEFAULT_ACC

此标志指示当出现错误时系统将采取缺省操作(如果合适,则进入紧急状态)。DDI_FM_ACCCHK_CAPABLE 驱动程序不能使用此属性。

DDI_FLAGERR_ACC

此标志指示系统将尝试处理与访问句柄关联的错误并从该错误中恢复。驱动程序应使用用于 Solaris 设备驱动程序的防御性编程方法中介绍的技术,并应使用 ddi_fm_acc_err_get(9F) 定期检查错误,之后才能允许数据回传给调用应用程序。

DDI_FLAGERR_ACC 标志可提供:

  • 通过驱动程序回调收到的错误通知

  • 通过 ddi_fm_acc_err_get(9F) 注册的驱动程序回调获得的错误通知

DDI_CAUTIOUS_ACC

DDI_CAUTIOUS_ACC 标志可为驱动程序进行的每个程控 I/O 访问提供高级别的保护。


注 –

使用此标志将对驱动程序的性能造成重大影响。


DDI_CAUTIOUS_ACC 标志指示访问驱动程序可以预见错误。系统尝试尽可能正常地处理与此句柄关联的错误并从该错误中恢复。最终不会生成错误报告,但句柄的 fme_status 标志将设置为 DDI_FM_NONFATAL。此标志在功能上与 ddi_peek(9F)ddi_poke(9F) 等效。

使用 DDI_CAUTIOUS_ACC 可提供:

  • 对总线的独占访问

  • 陷阱 (On trap) 保护-(ddi_peek()ddi_poke()

  • 通过使用 ddi_fm_handler_register(9F) 注册的驱动程序回调获得的错误通知

  • 通过 ddi_fm_acc_err_get(9F) 注册的驱动程序回调获得的错误通知

通常,驱动程序应在代码路径中的适当接合点处检查数据路径错误,以确保数据一致并确保 I/O 软件堆栈中显示正确的错误状态。

DDI_FM_ACCCHK_CAPABLE 设备驱动程序必须将其 devacc_attr_access 字段设置为 DDI_FLAGERR_ACC 或 DDI_CAUTIOUS_ACC。

DMA 属性结构

与访问句柄设置一样,DDI_FM_DMACHK_CAPABLE 设备驱动程序必须将其 ddi_dma_attr(9S) 结构的 dma_attr_flag 字段设置为 DDI_DMA_FLAGERR 标志。系统将尝试从与设置了 DDI_DMA_FLAGERR 的句柄关联的错误中恢复。ddi_dma_attr 结构包含以下成员:

uint_t          dma_attr_version;       /* version number */
uint64_t        dma_attr_addr_lo;       /* low DMA address range */
uint64_t        dma_attr_addr_hi;       /* high DMA address range */
uint64_t        dma_attr_count_max;     /* DMA counter register */
uint64_t        dma_attr_align;         /* DMA address alignment */
uint_t          dma_attr_burstsizes;    /* DMA burstsizes */
uint32_t        dma_attr_minxfer;       /* min effective DMA size */
uint64_t        dma_attr_maxxfer;       /* max DMA xfer size */
uint64_t        dma_attr_seg;           /* segment boundary */
int             dma_attr_sgllen;        /* s/g length */
uint32_t        dma_attr_granular;      /* granularity of device */
uint_t          dma_attr_flags;         /* Bus specific DMA flags */

设置 DDI_DMA_FLAGERR 标志的驱动程序应使用用于 Solaris 设备驱动程序的防御性编程方法中介绍的技术,并且应该在 DMA 事务完成时或者代码路径的重要点处使用 ddi_fm_dma_err_get(9F) 检查数据路径错误。这样可以确保数据一致并且 I/O 软件堆栈中显示正确的错误状态。

使用 DDI_DMA_FLAGERR 可提供:

获取错误状态

如果发生的故障影响到通过句柄映射的资源,则会更新错误状态结构,以反映在总线或 I/O 数据路径中的其他设备驱动程序在处理错误期间捕获的错误信息。

void ddi_fm_dma_err_get(ddi_dma_handle_t handle, ddi_fm_error_t *de, int version)

void ddi_fm_acc_err_get(ddi_acc_handle_t handle, ddi_fm_error_t *de, int version)

ddi_fm_dma_err_get(9F) 和 ddi_fm_acc_err_get(9F) 函数分别为 DMA 或访问句柄返回错误状态。应将版本字段设置为 DDI_FME_VERSION。

访问句柄错误意味着已检测到一种错误,该错误影响到达使用该访问句柄的设备或来自该设备的 PIO 事务。该驱动程序接收到的任何数据(例如,通过最新的 ddi_get8(9F) 调用)均应被视为可能已损坏。发送到设备的任何数据(例如,通过最新的 ddi_put32(9F) 调用)也都可能已损坏,或根本未被接收。然而,基本故障可能是瞬态的,因而驱动程序可以通过调用 ddi_fm_acc_err_clear(9F)、将设备重置为已知状态、重试任何可能出错的事务来尝试进行恢复。

如果指示 DMA 句柄出现错误,则意味着检测到错误已经(或将要)影响设备和当前绑定到句柄(如果句柄当前未绑定,则为最近绑定)的内存之间的 DMA 事务。可能的原因包括 DMA 数据路径中的组件出现故障,或设备尝试进行无效的 DMA 访问。驱动程序通过重试和重新分配内存可能能够继续。应将当前(或以前)绑定到句柄的内存的内容视为不确定的,并应将其释放回系统。一旦绑定或重新绑定句柄,与当前事务关联的故障指示便会丢失,但由于故障可能持续存在,因此将来的 DMA 操作可能不会成功。

清除错误

在句柄检测到错误后,驱动程序希望在无需释放和重新分配句柄的前提下重试请求时,应调用 ddi_fm_acc_err_clear()ddi_fm_dma_err_clear(9F) 例程。

void ddi_fm_acc_err_clear(ddi_acc_handle_t handle, int version)

void ddi_fm_dma_err_clear(ddi_dma_handle_t handle, int version)

注册错误处理程序

当操作系统通过陷阱或错误中断检测到错误时,错误处理活动可能会开始。如果负责处理错误的软件(错误处理程序)无法立即隔离出现故障的 I/O 操作中涉及的设备,它必须尝试在设备树内查找可以执行错误隔离的软件模块。Solaris 设备树提供了向子级传播结点驱动程序错误处理活动的结构化方法,这些子级可能对错误具有更详细的了解,并可捕获错误状态和隔离问题设备。

驱动程序可以使用 I/O 故障服务框架注册错误处理程序回调。错误处理程序应特定于错误的类型以及进行错误检测的子系统。调用驱动程序的错误处理程序例程时,驱动程序必须检查与设备事务关联的任何未解决的错误并生成 ereport 事件。驱动程序还必须在其 ddi_fm_error(9S) 结构中返回错误处理程序状态。例如,如果已经确定系统的完整性受到威胁,则错误处理程序可能采取的最合适的操作是使系统进入紧急状态。

当错误可能与特定的设备实例关联时,父结点驱动程序会调用回调。注册错误处理程序的设备驱动程序必须为 DDI_FM_ERRCB_CAPABLE。

void ddi_fm_handler_register(dev_info_t *dip, ddi_err_func_t handler, void *impl_data)

ddi_fm_handler_register(9F) 例程向 I/O 故障服务框架注册错误处理程序回调。应在驱动程序故障管理初始化 (ddi_fm_init()) 之后在驱动程序的 attach(9E) 入口点中调用 ddi_fm_handler_register() 函数,以便进行回调注册。

错误处理程序回调函数必须执行以下操作:

驱动程序错误处理程序会接收以下内容:

必须在驱动程序的 attach(9E) 或 detach(9E) 入口点的内核上下文中调用 ddi_fm_handler_register()ddi_fm_handler_unregister(9F) 。可以从内核、中断或高级别中断上下文中调用注册的错误处理程序回调。因此,错误处理程序:

设备驱动程序负责:

可在错误处理程序函数内执行这些操作。但是,由于对锁定的限制以及错误处理程序函数并非始终了解故障发生时驱动程序所执行操作的上下文,因此,更通常的做法是,如前所述在驱动程序的正常路径内内联调用 ddi_fm_acc_err_get(9F) 和 ddi_fm_dma_err_get(9F) 之后执行这些操作。

/*
 * The I/O fault service error handling callback function
 */
/*ARGSUSED*/
static int
bge_fm_error_cb(dev_info_t *dip, ddi_fm_error_t *err, const void *impl_data)
{
     /*
      * as the driver can always deal with an error 
      * in any dma or access handle, we can just return 
      * the fme_status value.
      */
     pci_ereport_post(dip, err, NULL);
     return (err->fme_status);
}

故障管理数据和状态结构

驱动程序错误处理回调会被传递一个指向数据结构的指针,该数据结构包含常见的故障管理数据和错误处理状态。

数据结构 ddi_fm_error 包含用于当前错误的 FMA 协议 ENA、错误处理程序回调的状态、错误预期标志以及与父结点检测到的错误关联的任何潜在访问或 DMA 句柄。

fme_ena

此字段通过调用父结点来进行初始化,并可能在达到驱动程序的已注册回调例程之前随着错误处理传播链不断增大。如果驱动程序检测到自身的相关错误,它应在调用 ddi_fm_ereport_post() 之前使此 ENA 增大。

fme_acc_handlefme_dma_handle

如果父级能够将在其级别上检测到的错误与设备驱动程序映射或绑定的句柄相关联,则这些字段中包含有效的访问或 DMA 句柄。

fme_flag

如果调用父级确定错误是由于 DDI_CAUTIOUS_ACC 受保护的操作引起的,fme_flag 将设置为 DDI_FM_ERR_EXPECTED。在这种情况下,fme_acc_handle 有效,并且驱动程序应只检查并报告不与 DDI_CAUTIOUS_ACC 受保护操作关联的错误。否则,fme_flag 将设置为 DDI_FM_ERR_UNEXPECTED,并且驱动程序必须执行完整的错误处理任务。

fme_status

从其错误处理程序回调返回后,驱动程序必须立即将 fme_status 设置为以下值之一:

  • DDI_FM_OK-未检测到任何错误,此设备实例的操作状态一直保持不变。

  • DDI_FM_FATAL-出现错误,并且驱动程序将其视为对系统致命的错误。例如,对 pci_ereport_post(9F) 的调用可能已检测到系统致命错误。在这种情况下,驱动程序应报告驱动程序的上下文中可能存在的任何其他错误信息。

  • DDI_FM_NONFATAL-驱动程序已检测到错误,但不将其视为对系统致命的错误。驱动程序已确定错误,并且已隔离该错误或者确认将隔离该错误。

  • DDI_FM_UNKNOWN-已检测到错误,但驱动程序无法隔离设备或确定错误对系统操作状态的影响。

诊断故障

故障管理守护进程 fmd(1M) 为诊断引擎 (diagnosis engine, DE) 插件模块的开发提供编程接口。可通过编写 DE 来使用和诊断任何错误遥测或特定错误遥测。eft DE 设计为根据以 Eversholt 语言指定的诊断规则来诊断任意数量的 ereport 类。

标准叶设备诊断

大多数 I/O 子系统都使用 eft DE 和规则集来诊断与设备和设备驱动程序相关的问题。已为 PCI 叶设备指定了一组报告标准 I/O 控制器错误中列出的标准 ereport。除了这些 ereport 外,同时还提供了提取遥测并确定关联设备故障的 eft 诊断规则。生成这些 ereport 的驱动程序不需要交付其他任何诊断软件或 eft 规则。

检测和生成这些 ereport 时将产生以下故障事件:

fault.io.pci.bus-linkerr

PCI 总线上的硬件故障

fault.io.pci.device-interr

设备内的硬件故障

fault.io.pci.device-invreq

设备中的硬件故障或驱动程序的缺陷,导致设备发送无效请求

fault.io.pci.device-noresp

设备中的硬件故障,导致驱动程序不对有效请求做出响应

fault.io.pciex.bus-linkerr

链路中的硬件故障

fault.io.pciex.bus-noresp

链路关闭,导致设备无法对有效请求做出响应

fault.io.pciex.device-interr

设备内的硬件故障

fault.io.pciex.device-invreq

设备中的硬件故障或驱动程序的缺陷,导致设备发送无效请求

fault.io.pciex.device-noresp

设备中的硬件故障,导致设备无法对有效请求做出响应

专门的设备诊断

要生成其他 ereport 或提供更专门的诊断软件或 eft 规则的驱动程序开发者可以通过编写基于 C 的 DE 或 eft 诊断规则集来实现此目标。有关信息,请参见 OpenSolaris 项目故障管理社区

事件注册表

Sun 事件注册表是所有类名、ereport、故障、缺陷、混乱和可疑列表 (list.suspect) 事件的中央信息库。该事件注册表中还包含所有事件成员有效负荷的当前定义以及重要的非有效负荷信息,例如内部文档、可疑列表、字典和知识文章。例如,ereport.iofault.io 是对 I/O 驱动程序开发者特别重要的两个基本类名。

FMA 事件协议定义随每个注册事件提供的有效负荷成员的基本集合。开发者还可以定义其他事件,以帮助诊断引擎(或 eft 规则)将可疑列表范围缩小至特定故障。

词汇表

本节使用以下术语:

Agent(代理)

用于描述订阅 fault.* 或 list.* 事件的故障管理器模块的通用术语。代理用于弃用有故障的资源、将诊断结果告知管理员并桥接至更高级别的管理框架。

ASRU(Automated System Reconfiguration Unit,自动系统重新配置单元)

ASRU 是可由软件或硬件禁用以便隔离系统中的问题并抑制生成更多错误报告的资源。

DE(Diagnosis Engine,诊断引擎)

一个故障管理模块,其用途是通过订阅传入错误事件的一个或多个类并使用这些事件来解决与系统中每个问题关联的案例来诊断问题。

ENA(Error Numeric Association,错误编号关联)

错误编号关联 (Error Numeric Association, ENA) 是一个编码的整数,用于唯一标识给定故障区域和时间段内的错误报告。ENA 还指示错误与以前的错误之间的关系,以作为辅助影响。

Error(错误)

意外的情况、结果、信号或数据。错误是问题在系统中的症状。每个问题通常都会产生许多不同种类的错误。

ereport(Error Report,错误报告)

随特定错误捕获的数据。错误报告格式通过创建命名错误报告的类和通过定义使用 Sun 事件注册表的模式提前定义。

ereport event(Error Event,错误事件)

表示错误报告实例的数据结构。错误事件表示为名称-值对列表。

Fault(故障)

硬件组件的故障行为。

Fault Boundary(故障边界)

可为其枚举一组特定故障的硬件或软件元素的逻辑分区。

Fault Event(故障事件)

在协议中编码的故障诊断的实例。

Fault Manager(故障管理器)

负责通过一个或多个诊断引擎进行故障诊断以及状态管理的软件组件。

FMRI(Fault Managed Resource Identifier,故障管理资源标识符)

FMRI 是类似于 URL 的标识符,它在故障管理系统中充当特定资源的规范名称。每个 FMRI 中都包括一个标识资源类型的模式,以及特定于该模式的一个或多个值。FMRI 可以表示为类似于 URL 的字符串或名称-值对列表数据结构。

FRU(Field Replaceable Unit,现场可更换单元)

FRU 是可在现场由客户或服务提供商更换的资源。可为硬件(例如,系统板)或软件(例如,软件包或修补程序)定义 FRU。

资源

以下资源可提供附加信息:

用于 Solaris 设备驱动程序的防御性编程方法

本节针对设备驱动程序提供了一些方法,可用于避免系统出现紧急情况并挂起、浪费系统资源以及扩大数据损坏范围。如果除了 I/O 故障服务框架之外,驱动程序还将这些防御性编程做法用于错误处理和诊断,则认为该驱动程序已进行强化。

所有 Solaris 驱动程序都应遵循以下编码做法:

使用单独的设备驱动程序实例

Solaris 内核允许一个驱动程序具有多个实例。每个实例都有自己的数据空间,但与其他实例共享文本和某些全局数据。设备是基于每个实例进行管理的。除非将驱动程序设计用于在内部处理任何故障转移,否则驱动程序应针对每个硬件使用单独的实例。一个插槽可能具有一个驱动程序的多个实例,例如,具有多功能卡。

独占使用 DDI 访问句柄

驱动程序进行的所有 PIO 访问都必须使用以下系列例程中的 Solaris DDI 访问函数:

驱动程序不应根据 ddi_regs_map_setup(9F) 返回的地址直接访问已映射的寄存器。请避免使用 ddi_peek(9F)ddi_poke(9F) 例程,因为这些例程不使用访问句柄。

由于 DDI 访问提供了对数据读入内核的方式进行控制的机会,因此 DDI 访问机制很重要。

检测已损坏的数据

以下各节介绍可能发生数据损坏的位置以及如何检测损坏。

设备管理和控制数据的损坏

驱动程序应假定,从设备获取的任何数据(无论通过 PIO 还是 DMA)都可能已被损坏。需要特别指出的是,对于基于设备数据的指针、内存偏移以及数组索引要格外小心。此类值可以是恶性的,因为取消引用这些值时会导致内核出现紧急情况。在使用之前,应针对所有此类值执行范围和对齐检查(如果需要)。

即使是非恶性指针,仍然可能具有误导性。例如,指针可能指向某个对象的有效但错误的实例。驱动程序应尽量交叉检查指针以及该指针所指向的对象,或者对通过该指针获得的数据进行验证。

其他类型的数据也可能具有误导性,如包长度、状态字或通道 ID。应尽可能地对这些数据类型进行检查。可对包长度进行范围检查,以确保该长度既不为负,也不比包含缓冲区大。可针对“不可能”的位对状态字进行检查。可将通道 ID 与有效 ID 的列表进行匹配。

其中,一个值标识一个流,驱动程序必须确保该流仍然存在。处理 STREAMS 的异步性质意味着可在设备中断仍未完成时中断流。

驱动程序不应从设备中重新读取数据。数据应只读取一次,然后进行验证并以驱动程序的本地状态进行存储。此方法可避免数据在初始读取时正确但以后重新读取时错误的风险。

驱动程序还应确保已限制所有循环。例如,返回连续 BUSY 状态的设备不能锁定整个系统。

已接收数据的损坏

设备错误可能导致将损坏的数据放置在接收缓冲区中。此类损坏与设备域之外(例如,网络中)发生的损坏几乎没有区别。通常可利用现有软件处理此类问题。例如,在协议栈的传输层进行完整性检查,或者在使用该设备的应用程序内进行完整性检查。

如果不打算在较高层对已接收的数据进行完整性检查,则可在驱动程序自身内对数据进行完整性检查。对已接收数据中的损坏进行检测的方法通常特定于设备。例如,校验和与 CRC 即是可执行的检查种类。

DMA 隔离

有缺陷的设备可能通过总线启动错误的 DMA 传输。此类数据传输可能会损坏以前传送的正常数据。发生故障的设备可能会生成损坏的地址,该地址可能会污染甚至不属于自己的驱动器的内存。

在具有 IOMMU 的系统中,设备只能写入映射为对于 DMA 可写入的页。因此,此类页只应归一个驱动程序实例所有。不应将这些页与其他任何内核结构共享。尽管该页被映射为对于 DMA 可写入的页,但驱动程序应怀疑该页中的数据。在将页传递到驱动程序之外以及对数据进行验证之前,必须从 IOMMU 取消映射页。

可以使用 ddi_umem_alloc(9F) 来保证已分配整个对齐的页,或分配多页并忽略第一个页边界之下的内存。可以使用 ddi_ptob(9F) 确定 IOMMU 页的大小。

或者,驱动程序可以选择在处理数据之前将其复制到内存中较安全的部分。如果已执行此操作,则必须先使用 ddi_dma_sync(9F) 同步数据。

ddi_dma_sync() 的调用应在使用 DMA 向设备传送数据之前指定 SYNC_FOR_DEV,并在使用 DMA 从设备向内存传送数据之后指定 SYNC_FOR_CPU

在某些基于 PCI 且具有 IOMMU 的系统中,设备可以使用 PCI 双地址循环(64 位地址)绕过 IOMMU。此功能使设备可能损坏主内存的任何区域。设备驱动程序不得尝试使用此类模式,并应禁用此类模式。

处理有问题的中断

驱动程序必须标识有问题的中断,因为不断声明中断会严重影响系统性能,几乎一定会使单处理器计算机产生延迟。

有时,驱动程序可能很难将特定中断标识为无效。对于网络驱动程序,如果指示接收中断,但没有新缓冲区可用,则无需执行任何操作。当此情况为孤立事件时,这并不是一个问题,因为另一个例程(如读取服务)可能已完成实际工作。

另一方面,出现连续中断但不需要驱动程序处理任何工作,这表示一个有问题的中断行。因此,平台允许在执行防御性操作之前发生许多明显无效的中断。

当显示有工作需要处理时,挂起的设备可能无法更新其缓冲区描述符。驱动程序应阻止此类重复请求。

在某些情况下,平台特定总线驱动程序可能能够识别一直未请求的中断,并且可以禁用违例设备。但是,这依赖于驱动程序识别有效中断并返回相应值的能力。除非驱动程序检测到设备合法声明了中断,否则驱动程序应返回 DDI_INTR_UNCLAIMED 结果。仅当设备实际要求驱动程序执行一些有用的工作时,中断才是合法的。

其他更偶然的中断的合法性更难认证。预期中断标志是评估中断是否有效的有用工具。请考虑一个中断,如描述符释放,如果先前已分配设备的所有描述符,则可生成该中断。如果驱动程序检测到它已从卡中获取最后一个描述符,便可以设置一个预期中断标志。如果传送关联的中断时未设置此标志,则该中断为可疑中断。

有些提示性中断可能无法预测,例如指示介质已断开连接或帧同步已丢失的中断。检测此类中断是否有问题的最简单方法是:第一次出现中断时屏蔽此特定源,直到下一个轮询周期。

如果在禁用期间再次发生中断,则该中断无效。有些设备具有即使掩码寄存器已禁用关联源并且不可能引起中断时仍可读取的中断状态位。您可以设计更适合的、特定于设备的算法。

应避免对中断状态位进行无限循环。如果传送开始时设置的状态位都不要求任何实际工作,请中断此类循环。

其他编程注意事项

除了前面各节中讨论的要求外,还请考虑以下问题:

线程交互

设备驱动程序中内核出现紧急情况通常是由设备发生故障后内核线程的意外交互引起的。设备出现故障时,线程可能会以您意想不到的方式进行交互。

如果处理例程较早终止,则条件变量等待程序将由于从未给定预期信号而被阻塞。尝试向其他模块通知故障或处理意外回调会导致不需要的线程交互。请考虑设备发生故障期间获取和释放互斥锁的顺序。

如果源自上游 STREAMS 模块的线程被用来意外返回该模块,则这些线程可能得出自相矛盾的结果。请考虑使用备用线程来处理异常消息。例如,过程可以使用读端服务例程与 M_ERROR 进行通信,而不直接使用读端 putnext(9F) 处理错误。

在关闭期间由于故障而无法处于静态的发生故障的 STREAMS 设备会在流终止后生成中断。中断处理程序不得尝试使用过时的流指针来处理消息。

自上而下请求的威胁

针对有缺陷的硬件为系统提供保护的同时,您还需要针对驱动程序误用提供保护。尽管驱动程序可以假定内核基础结构始终正确(受信任的核心),但传递给它的用户请求可能具有破坏性。

例如,用户可以请求对用户提供的数据块 (M_IOCTL) 执行某一操作,该数据块小于消息的控制部分所指示的块大小。驱动程序绝不应该信任用户应用程序。

请考虑您的驱动程序可以接收的每种类型的 ioctl 的构造以及 ioctl 可能引起的潜在危害。驱动程序应执行检查,以确保它不处理格式错误的 ioctl

自适应策略

驱动程序可以使用有故障的硬件继续提供服务。驱动程序可以尝试使用用于访问设备的备用策略来解决已确定的问题。假定损坏的硬件不可预测并且已知与其他设计复杂性关联的风险,则自适应策略并不总是明智的选择。这些策略最多应限制为定期中断轮询和重试尝试。定期重试设备可使驱动程序了解设备恢复的时间。强制驱动程序禁用中断后,定期轮询可以控制中断机制。

理论上,系统始终有一个备用设备来提供重要的系统服务。内核或用户空间中的服务多路复用程序可在设备出现时提供维护系统服务的最佳方法。此类做法将不在本节中进行介绍。

驱动程序强化测试工具

驱动程序强化测试工具测试是否已正确实现 I/O 故障服务和防御性编程要求。强化的设备驱动程序可从潜在的硬件故障中复原。必须在驱动程序开发过程中测试设备驱动程序的复原能力。此类测试要求驱动程序以受控制并且可重复的方式处理多种典型硬件故障。通过驱动程序测试工具可在软件中仿真此类硬件故障。

驱动程序强化测试工具是一种 Solaris 设备驱动程序开发工具。该测试工具可在处于开发阶段的驱动程序访问其硬件时注入多种仿真硬件故障。本节介绍如何配置测试工具、创建错误注入规范(称为 errdef)以及对设备驱动程序执行测试。

测试工具可截获从驱动程序到各种 DDI 例程的调用,然后损坏调用的结果,就像硬件引起损坏一样。此外,该工具还允许损坏对特定寄存器的访问以及定义更多随机类型的损坏。

测试工具可以在运行指定的工作负荷期间通过跟踪所有寄存器访问以及直接内存访问 (direct memory access, DMA) 和中断使用情况自动生成测试脚本。生成的脚本将在向每个访问中注入一组故障的同时,重新运行该工作负荷。

驱动程序测试器应从生成的脚本中删除重复的测试案例。

测试工具作为名为 bofi(表示 bus_ops 故障注入)的设备驱动程序和两个用户级实用程序(th_define(1M)th_manage(1M))来实现。

测试工具可执行以下任务:

故障注入

驱动程序强化测试工具截获并在请求时损坏驱动程序对其硬件进行的每次访问。本节提供您在创建故障以测试驱动程序的可复原性时应了解的信息。

Solaris 设备在名为设备树(devinfo 树)的类似树的结构内进行管理。devinfo 树的每个节点都存储着与系统中某一设备的特定实例相关的信息。每个叶节点对应于一个设备驱动程序,而其他所有节点都称为子树节点。通常,结点 (nexus) 表示总线。总线节点将叶驱动程序与总线相关项隔离,从而可以生成在体系结构上独立的驱动程序。

许多 DDI 函数(特别是数据访问函数)都会导致向上调用总线结点驱动程序。当叶驱动程序访问其硬件时,它会将句柄传递给访问例程。总线结点了解如何处理句柄和实现请求。符合 DDI 标准的驱动程序只通过使用这些 DDI 访问例程来访问硬件。在这些向上调用到达指定的总线结点之前,测试工具会截获这些向上调用。如果数据访问与驱动程序测试器指定的标准相符,访问将被损坏。如果数据访问与该标准不符,则将其提供给总线结点,以便通过常规方式进行处理。

驱动程序通过使用 ddi_regs_map_setup(9F) 函数获取访问句柄:

ddi_regs_map_setup(dip, rset, ma, offset, size, handle)

参数指定要映射哪个“板外”内存。驱动程序在引用映射的 I/O 地址时必须使用返回的句柄,因为句柄用于将驱动程序与总线层次结构的详细信息隔离开来。因此,请不要直接使用返回的映射地址 ma。直接使用映射地址会导致当前以及将来无法使用数据访问函数机制。

对于程控 I/O,数据访问函数组为:

Xrepcnt 是要传送的字节数。X 是总线传送大小,为 8、16、32 或 64 字节。

DMA 具有与之类似但更为丰富的一组数据访问函数。

设置测试工具

驱动程序强化测试工具属于 Solaris Developer Cluster 的一部分。如果尚未安装此 Solaris 簇,则必须手动安装适用于您平台的测试工具软件包。

安装测试工具

要安装测试工具软件包(SUNWftduu 和 SUNWftdur),请使用 pkgadd(1M) 命令。

以超级用户身份转到软件包所在目录,并键入:


# pkgadd -d . SUNWftduu SUNWftdur

配置测试工具

安装测试工具后,请在 /kernel/drv/bofi.conf 文件中设置属性,以将工具配置为与驱动程序交互。完成工具配置后,重新引导系统以装入工具驱动程序。

测试工具行为由 /kernel/drv/bofi.conf 配置文件中设置的引导时属性控制。

首次安装工具时,可通过设置以下属性来使工具可以截获对驱动程序的 DDI 访问:

bofi-nexus

总线结点类型,如 PCI 总线

bofi-to-test

所测试的驱动程序的名称

例如,要测试名为 xyznetdrv 的 PCI 总线网络驱动程序,请设置以下属性值:

bofi-nexus="pci"
bofi-to-test="xyznetdrv"

其他属性与用于从使用 PIO 的外围设备读取和写入以及与使用 DMA 的外围设备进行双向数据传送的 Solaris DDI 数据访问机制的使用和工具检查相关。

bofi-range-check

设置此属性时,测试工具将检查传递给 PIO 数据访问函数的参数的一致性。

bofi-ddi-check

设置此属性时,测试工具将验证 ddi_map_regs_setup(9F) 返回的映射地址未在数据访问函数的上下文之外使用。

bofi-sync-check

设置此属性时,测试工具将验证 DMA 函数的使用是否正确并确保驱动程序对 ddi_dma_sync(9F) 的使用符合规范。

测试驱动程序

本节介绍如何使用 th_define(1M)th_manage(1M) 命令创建并注入故障。

创建故障

th_define 实用程序为 bofi 设备驱动程序提供了一个接口,以用于定义 errdef。errdef 对应于有关如何损坏设备驱动程序对其硬件的访问的规范。th_define 命令行参数确定要注入的故障的确切性质。如果提供的参数定义了一致的 errdef,th_define 进程将使用 bofi 驱动程序存储 errdef。该进程将使自身暂停,直至 errdef 给定的条件得到满足为止。实际上,当访问计数达到零 (0) 时,暂停将结束。

注入故障

测试工具在数据访问级别运行。数据访问具有以下特征:

测试工具截获数据访问并将适当的故障注入驱动程序。th_define(1M) 命令指定的 errdef 可对以下信息编码:

使用 -a acc_chk 选项可仿真 errdef 中的框架故障。

故障注入过程

    注入故障的过程涉及两个阶段:

  1. 使用 th_define(1M) 命令创建 errdef。

    通过向 bofi 驱动程序传递测试定义来创建 errdef,该驱动程序会存储这些定义,因此可以使用 th_manage(1M) 命令来访问它们。

  2. 创建工作负荷,然后使用 th_manage 命令激活和管理 errdef。

    th_manage 命令是到 bofi 工具驱动程序可以识别的各种 ioctl 的用户接口。th_manage 命令在驱动程序名称和实例级别运行并且包含以下命令: get_handles 用于列出访问句柄,start 用于激活 errdef,stop 用于取消激活 errdef。

    激活 errdef 将导致合格的数据访问出现故障。th_manage 实用程序支持以下命令: broadcast 用于提供 errdef 的当前状态,clear_errors 用于清除 errdef。

    有关更多信息,请参见 th_define(1M) 和 th_manage(1M) 手册页。

测试工具警告

可对测试工具进行配置,以便通过以下方法来处理警告消息:

使用第二种方法有助于确定问题的根本原因。

如果将 bofi-range-check 属性值设置为 warn,当工具检测到驱动程序违反 DDI 函数的范围时,该工具将列显以下消息(或者,如果设置为 panic,则会进入紧急状态):

ddi_getX() out of range addr %x not in %x
ddi_putX() out of range addr %x not in %x
ddi_rep_getX() out of range addr %x not in %x
ddi_rep_putX() out of range addr %x not in %x

X 为 8、16、32 或 64。

当工具已请求插入 1000 个以上额外中断时,如果驱动程序未检测到中断逾限 (jabber),则会列显以下消息:

undetected interrupt jabber - %s %d

使用脚本自动完成测试过程

可以使用日志记录访问类型 th_define(1M) 实用程序来创建故障注入测试脚本:


# th_define -n name -i instance -a log [-e fixup_script]

th_define 命令使实例脱机,然后再使其恢复联机。然后,th_define 运行 fixup_script 描述的工作负荷并记录驱动程序实例进行的 I/O 访问。

将会使用可选参数的集合调用 fixup_script 两次。会在实例脱机前调用该脚本一次,然后在实例恢复联机后再次调用该脚本。

以下变量将被传递到调用的可执行文件的环境中:

DRIVER_PATH

实例的设备路径

DRIVER_INSTANCE

驱动程序的实例编号

DRIVER_UNCONFIGURE

当实例即将脱机时设置为 1

DRIVER_CONFIGURE

当实例已恢复联机时设置为 1

通常,fixup_script 可确保所测试的设备处于适合脱机的状态(未配置)或处于适合注入错误的状态(例如,已配置、无错误并为工作负荷服务)。以下脚本是用于网络驱动程序的最小脚本:

#!/bin/ksh
driver=xyznetdrv
ifnum=$driver$DRIVER_INSTANCE
 
if [[ $DRIVER_CONFIGURE = 1 ]]; then
   ifconfig $ifnum plumb	
   ifconfig $ifnum ...	
   ifworkload start $ifnum
elif [[ $DRIVER_UNCONFIGURE = 1 ]]; then	
   ifworkload stop $ifnum	
   ifconfig $ifnum down	
   ifconfig $ifnum unplumb
fi
exit $?

注 –

ifworkload 命令应将工作负荷作为一项后台任务来启动。故障注入发生在 fixup_script 配置所测试的驱动程序并使其联机(DRIVER_CONFIGURE 设置为 1)之后。


如果存在 -e fixup_script 选项,它必须是命令行中的最后一个选项。如果不存在 -e 选项,则使用缺省脚本。缺省脚本会反复尝试使所测试的设备脱机和联机。因此,工作负荷由驱动程序的 attach()detach() 路径构成。

生成的日志将转换为一组适合运行独立的 (unassisted) 故障注入测试的可执行脚本。这些脚本创建在当前目录的子目录中,名称为 driver.test.id。脚本将在运行 fixup_script 描述的工作负荷的同时向驱动程序中注入故障,一次一个。

驱动程序测试器可对测试自动化过程生成的 errdef 进行实质性控制。请参见 th_define(1M) 手册页。

如果测试器为测试脚本选择了合适的工作负荷范围,则工具可为驱动程序各方面的强化提供良好的覆盖率。但是,要取得满覆盖率,测试器可能需要手动创建其他测试案例。请将这些案例添加至测试脚本。为确保测试可及时完成,您可能需要手动删除重复的测试案例。

自动化测试过程

    以下过程介绍了自动化测试:

  1. 确定要测试的驱动程序的各个方面。

    测试驱动程序中与硬件交互的所有方面:

    • 连接和分离

    • 在堆栈下检测和取消检测

    • 正常数据传送

    • 记录的调试模式

    必须为每种使用模式生成单独的工作负荷脚本 (fixup_script)。

  2. 对于每种使用模式,准备可执行程序 ( fixup_script),该可执行程序可对设备进行配置和取消配置,并可创建和终止工作负荷。

  3. 使用 errdef 以及访问类型 -a log 运行 th_define(1M) 命令。

  4. 等待日志填充。

    日志中包含 bofi 驱动程序的内部缓冲区的转储。此数据包含在脚本的前面。

    由于创建日志可能需要几秒钟到几分钟的时间,因此可使用 th_manage broadcast 命令检查进度。

  5. 转到已创建的测试目录并运行主测试脚本。

    主脚本将按顺序运行每个生成的测试脚本。每个寄存器集会生成单独的测试脚本。

  6. 存储结果,以用于分析。

    成功的测试结果(如 success (corruption reported)success (corruption undetected))表明所测试的驱动程序工作正常。如果工具检测到驱动程序在报告故障后无法报告服务影响或者驱动程序无法检测到访问或 DMA 句柄已被标记为有故障,则结果将报告为 failure (no service impact reported)

    输出中出现几个 test not triggered 故障并不碍事。但是,若干个此类故障将表明测试没有正常工作。当驱动程序访问的寄存器与生成测试脚本时的寄存器不同时,会出现这些故障。

  7. 同时对驱动程序的多个实例运行测试,以测试错误路径的多线程。

    例如,每个 th_define 命令都会创建一个单独的目录,其中包含测试脚本和主脚本:


    # th_define -n xyznetdrv -i 0 -a log -e script
    # th_define -n xyznetdrv -i 1 -a log -e script
    

    创建后,并行运行主脚本。


    注 –

    生成的脚本只生成仿真的故障注入,这些故障注入基于日志记录 errdef 处于活动状态期间记录的内容。定义工作负荷时,请确保记录所需结果。此外,还要分析生成的日志和故障注入规范。请验证生成的测试脚本所创建的硬件访问覆盖率是否满足需要。