SCSI EH

本文档描述了 SCSI 中间层的错误处理基础设施。有关 SCSI 中间层的更多信息,请参阅SCSI 中层 - 下层驱动程序接口

1. SCSI 命令如何通过中间层并到达 EH

1.1 struct scsi_cmnd

每个 SCSI 命令都用 struct scsi_cmnd(== scmd)表示。一个 scmd 有两个 list_head 将其链接到列表中。这两个是 scmd->list 和 scmd->eh_entry。前者用于空闲列表或每个设备分配的 scmd 列表,对于此 EH 讨论不太重要。后者用于完成和 EH 列表,除非另有说明,否则在此讨论中始终使用 scmd->eh_entry 链接 scmd。

1.2 scmd 如何完成?

一旦 LLDD 获取到 scmd,LLDD 将通过调用在调用 hostt->queuecommand() 时从中间层传递的 scsi_done 回调来完成命令,或者块层将使其超时。

1.2.1 使用 scsi_done 完成 scmd

对于所有非 EH 命令,scsi_done() 是完成回调。它只是调用 blk_complete_request() 来删除块层定时器并引发 SCSI_SOFTIRQ

SCSI_SOFTIRQ 处理程序 scsi_softirq 调用 scsi_decide_disposition() 来确定如何处理命令。scsi_decide_disposition() 查看 scmd->result 值和 sense 数据来确定如何处理命令。

  • 成功

    为该命令调用 scsi_finish_command()。该函数执行一些维护工作,然后调用 scsi_io_completion() 来完成 I/O。然后,scsi_io_completion() 通过调用 blk_end_request 和朋友通知块层已完成的请求,或者在发生错误时确定如何处理剩余的数据。

  • 需要重试

  • 添加到 MLQUEUE

    scmd 被重新排队到 blk 队列。

  • 否则

    为该命令调用 scsi_eh_scmd_add(scmd)。有关此函数的详细信息,请参见 [1-3]。

1.2.2 使用超时完成 scmd

超时处理程序是 scsi_timeout()。当发生超时时,此函数

  1. 调用可选的 hostt->eh_timed_out() 回调。返回值可以是以下之一

    • SCSI_EH_RESET_TIMER

      这表示需要更多时间来完成命令。定时器重新启动。

    • SCSI_EH_NOT_HANDLED

      eh_timed_out() 回调未处理该命令。执行步骤 #2。

    • SCSI_EH_DONE

      eh_timed_out() 完成了命令。

  2. 调用 scsi_abort_command() 来安排异步中止,这可能会发出重试 scmd->allowed + 1 次。对于设置了 SCSI_EH_ABORT_SCHEDULED 标志的命令(这表示该命令已被中止一次,这是失败的重试),当重试次数超过限制时,或者当 EH 截止时间已过期时,不会调用异步中止。在这些情况下,执行步骤 #3。

  3. 为该命令调用 scsi_eh_scmd_add(scmd, SCSI_EH_CANCEL_CMD)。有关更多信息,请参见 [1-4]。

1.3 异步命令中止

在发生超时后,从 scsi_abort_command() 调度命令中止。如果中止成功,该命令将被重试(如果重试次数未耗尽)或以 DID_TIME_OUT 终止。

否则,将为该命令调用 scsi_eh_scmd_add()。有关更多信息,请参见 [1-4]。

1.4 EH 如何接管

scmd 通过 scsi_eh_scmd_add() 进入 EH,它执行以下操作。

  1. 将 scmd->eh_entry 链接到 shost->eh_cmd_q

  2. 在 shost->shost_state 中设置 SHOST_RECOVERY 位

  3. 递增 shost->host_failed

  4. 如果 shost->host_busy == shost->host_failed,则唤醒 SCSI EH 线程

如上所述,一旦任何 scmd 被添加到 shost->eh_cmd_q,就会打开 SHOST_RECOVERY shost_state 位。这会阻止从 blk 队列向主机发出任何新的 scmd;最终,主机上的所有 scmd 要么正常完成,要么失败并被添加到 eh_cmd_q,要么超时并被添加到 shost->eh_cmd_q。

如果所有 scmd 要么完成要么失败,则正在运行的 scmd 的数量等于失败的 scmd 的数量 - 即 shost->host_busy == shost->host_failed。这会唤醒 SCSI EH 线程。因此,一旦被唤醒,SCSI EH 线程就可以期望所有正在运行的命令都已失败,并且已链接到 shost->eh_cmd_q。

请注意,这并不意味着下层是静止的。如果 LLDD 以错误状态完成了 scmd,则假定 LLDD 和下层在该点忘记了 scmd。但是,如果 scmd 已超时,除非 hostt->eh_timed_out() 使下层忘记了 scmd,而目前没有 LLDD 这样做,只要下层关注,该命令仍然处于活动状态,并且完成可能会随时发生。当然,由于定时器已经过期,所有此类完成都将被忽略。

稍后我们将讨论 SCSI EH 如何采取操作来中止 - 使 LLDD 忘记 - 超时的 scmd。

2. SCSI EH 如何工作

LLDD 可以通过以下两种方式之一实现 SCSI EH 操作。

  • 细粒度 EH 回调

    LLDD 可以实现细粒度 EH 回调,并让 SCSI 中间层驱动错误处理并调用适当的回调。这将在 [2-1] 中进一步讨论。

  • eh_strategy_handler() 回调

    这是一个大的回调,应该执行整个错误处理。因此,它应该执行 SCSI 中间层在恢复期间执行的所有任务。这将在 [2-2] 中讨论。

恢复完成后,SCSI EH 通过调用 scsi_restart_operations() 恢复正常操作,该函数会

  1. 检查是否需要锁定门并锁定门。

  2. 清除 SHOST_RECOVERY shost_state 位

  3. 唤醒 shost->host_wait 上的等待者。如果有人在主机上调用 scsi_block_when_processing_errors(),则会发生这种情况。(问题 为什么需要它?所有操作在到达 blk 队列后都将被阻止。)

  4. 在主机上踢所有设备的队列

2.1 通过细粒度回调的 EH

2.1.1 概述

如果 eh_strategy_handler() 不存在,则 SCSI 中间层负责驱动错误处理。EH 的目标有两个 - 使 LLDD、主机和设备忘记超时的 scmd,并使其准备好接收新命令。如果 scmd 被下层忘记,并且下层已准备好再次处理或使 scmd 失败,则称该 scmd 已恢复。

为了实现这些目标,EH 以越来越严重的程度执行恢复操作。某些操作是通过发出 SCSI 命令执行的,而另一些操作是通过调用以下细粒度 hostt EH 回调之一执行的。回调可能会被省略,省略的回调始终被视为失败。

int (* eh_abort_handler)(struct scsi_cmnd *);
int (* eh_device_reset_handler)(struct scsi_cmnd *);
int (* eh_bus_reset_handler)(struct scsi_cmnd *);
int (* eh_host_reset_handler)(struct scsi_cmnd *);

只有当较低严重级别的操作无法恢复某些失败的 scmd 时,才会执行较高严重级别的操作。另请注意,最高严重级别操作的失败意味着 EH 失败,并导致所有未恢复的设备脱机。

在恢复期间,遵循以下规则

  • 对待办事项列表 eh_work_q 上失败的 scmd 执行恢复操作。如果某个 scmd 的恢复操作成功,则会将恢复的 scmd 从 eh_work_q 中删除。

    请注意,对单个 scmd 的恢复操作可以恢复多个 scmd。例如,重置设备会恢复设备上的所有失败的 scmd。

  • 只有在较低严重级别的操作完成后 eh_work_q 不为空时,才会执行较高严重级别的操作。

  • EH 重用失败的 scmd 来发出恢复命令。对于超时的 scmd,SCSI EH 确保 LLDD 在将其重用于 EH 命令之前忘记该 scmd。

当 scmd 被恢复后,使用 scsi_eh_finish_cmd() 将 scmd 从 eh_work_q 移动到 EH 本地 eh_done_q。在所有 scmd 都被恢复后(eh_work_q 为空),会调用 scsi_eh_flush_done_q() 来重试或错误完成(通知上层失败)恢复的 scmd。

仅当 scmd 的 sdev 仍然在线(在 EH 期间未脱机)、未设置 REQ_FAILFAST 且 ++scmd->retries 小于 scmd->allowed 时,才会重试 scmd。

2.1.2 scmd 通过 EH 的流程

  1. 错误完成/超时

    操作

    为 scmd 调用 scsi_eh_scmd_add()

    • 将 scmd 添加到 shost->eh_cmd_q

    • 设置 SHOST_RECOVERY

    • shost->host_failed++

    锁定

    shost->host_lock

  2. EH 开始

    操作

    将所有 scmd 移动到 EH 的本地 eh_work_q。清除 shost->eh_cmd_q。

    锁定

    shost->host_lock(并非绝对必要,只是为了保持一致性)

  3. scmd 已恢复

    操作

    调用 scsi_eh_finish_cmd() 以完成 scmd 的 EH 处理

    • scsi_setup_cmd_retry()

    • 从本地 eh_work_q 移动到本地 eh_done_q

    锁定

    并发性:

    每个独立的 eh_work_q 最多一个线程,以保持队列操作无锁

  4. EH 完成

    操作

    scsi_eh_flush_done_q() 重试 scmd 或通知上层失败。 可以并发调用,但每个独立的 eh_work_q 必须只有一个线程来无锁地操作队列

    • 从 eh_done_q 中删除 scmd,并清除 scmd->eh_entry

    • 如果需要重试,则使用 scsi_queue_insert() 将 scmd 重新排队

    • 否则,为 scmd 调用 scsi_finish_command()

    • 将 shost->host_failed 置零

    锁定

    排队或完成函数执行适当的锁定

2.1.3 控制流程

通过细粒度回调的 EH 从 scsi_unjam_host() 开始。

scsi_unjam_host

  1. 锁定 shost->host_lock,将 shost->eh_cmd_q 拼接初始化到本地 eh_work_q 并解锁 host_lock。 请注意,此操作会清除 shost->eh_cmd_q。

  2. 调用 scsi_eh_get_sense。

scsi_eh_get_sense

对于每个没有有效 sense 数据的错误完成 (!SCSI_EH_CANCEL_CMD) 命令,都会执行此操作。 大多数 SCSI 传输/LLDD 会自动在命令失败时获取 sense 数据(自动 sense)。 出于性能原因,建议使用自动 sense,并且 sense 信息可能会在 CHECK CONDITION 发生和此操作之间不同步。

请注意,如果不支持自动 sense,则使用 scsi_done() 错误完成 scmd 时,scmd->sense_buffer 包含无效的 sense 数据。 在这种情况下,scsi_decide_disposition() 始终返回 FAILED,从而调用 SCSI EH。 当 scmd 到达此处时,将获取 sense 数据并再次调用 scsi_decide_disposition()。

  1. 调用 scsi_request_sense(),它发出 REQUEST_SENSE 命令。 如果失败,则不执行任何操作。 请注意,不执行任何操作会导致对 scmd 采取更严重级别的恢复措施。

  2. 对 scmd 调用 scsi_decide_disposition()

  1. 如果 !list_empty(&eh_work_q),则调用 scsi_eh_abort_cmds()。

scsi_eh_abort_cmds

当主机模板中启用 no_async_abort 时,会对每个超时的命令执行此操作。为每个 scmd 调用 hostt->eh_abort_handler()。如果处理程序成功使 LLDD 和所有相关硬件忘记 scmd,则返回 SUCCESS。

如果成功中止了超时的 scmd,并且 sdev 处于脱机或就绪状态,则为 scmd 调用 scsi_eh_finish_cmd()。否则,会将 scmd 留在 eh_work_q 中以进行更严重级别的操作。

请注意,脱机和就绪状态都意味着 sdev 已准备好处理新的 scmd,其中处理还意味着立即失败;因此,如果 sdev 处于两种状态之一,则无需采取进一步的恢复操作。

使用 scsi_eh_tur() 测试设备就绪状态,该函数会发出 TEST_UNIT_READY 命令。请注意,必须先成功中止 scmd,然后才能将其重用于 TEST_UNIT_READY。

  1. 如果 !list_empty(&eh_work_q),则调用 scsi_eh_ready_devs()

scsi_eh_ready_devs

此函数采取四种越来越严重的措施,使失败的 sdev 准备好接收新命令。

  1. 调用 scsi_eh_stu()

scsi_eh_stu

对于每个失败的 scmd 都有有效 sense 数据的 sdev,如果 scsi_check_sense() 的结果为 FAILED,则发出 START_STOP_UNIT 命令,并使用 start=1。请注意,由于我们显式选择错误完成的 scmd,因此已知较低层已经忘记了 scmd,我们可以将其重用于 STU。

如果 STU 成功,并且 sdev 处于脱机或就绪状态,则使用 scsi_eh_finish_cmd() 完成 sdev 上所有失败 scmd 的 EH 处理。

注意 如果没有实现或失败 hostt->eh_abort_handler(),那么此时我们可能仍有超时的 scmd,并且 STU 不会让较低层忘记这些 scmd。 但是,如果 STU 成功,此函数会完成 sdev 上所有 scmd 的 EH 处理,从而使较低层处于不一致的状态。 似乎仅当 sdev 没有超时的 scmd 时才应采取 STU 操作。

  1. 如果 !list_empty(&eh_work_q),则调用 scsi_eh_bus_device_reset()。

scsi_eh_bus_device_reset

此操作与 scsi_eh_stu() 非常相似,不同之处在于,它不是发出 STU,而是使用 hostt->eh_device_reset_handler()。 此外,由于我们不发出 SCSI 命令,并且重置会清除 sdev 上的所有 scmd,因此无需选择错误完成的 scmd。

  1. 如果 !list_empty(&eh_work_q),则调用 scsi_eh_bus_reset()

scsi_eh_bus_reset

为每个包含失败 scmd 的通道调用 hostt->eh_bus_reset_handler()。如果总线重置成功,则完成通道上所有就绪或脱机 sdev 的所有失败 scmd 的 EH 处理。

  1. 如果 !list_empty(&eh_work_q),则调用 scsi_eh_host_reset()

scsi_eh_host_reset

这是最后的手段。调用 hostt->eh_host_reset_handler()。如果主机重置成功,则完成主机上所有就绪或脱机 sdev 的所有失败 scmd 的 EH 处理。

  1. 如果 !list_empty(&eh_work_q),则调用 scsi_eh_offline_sdevs()

scsi_eh_offline_sdevs

将所有仍有未恢复 scmd 的 sdev 脱机并完成 scmd 的 EH 处理。

  1. 调用 scsi_eh_flush_done_q()

    scsi_eh_flush_done_q

    此时,所有 scmd 都已恢复(或放弃)并通过 scsi_eh_finish_cmd() 放入 eh_done_q。此函数通过重试或通知上层 scmd 失败来刷新 eh_done_q。

2.2 通过 transportt->eh_strategy_handler() 的 EH

调用 transportt->eh_strategy_handler() 来代替 scsi_unjam_host(),并且它负责整个恢复过程。 完成后,处理程序应使较低层忘记所有失败的 scmd,并准备好接收新命令或脱机。 此外,它应执行 SCSI EH 维护工作,以保持 SCSI 中层的完整性。 换句话说,在 [2-1-2] 中描述的步骤中,除了 #1 之外的所有步骤都必须由 eh_strategy_handler() 实现。

2.2.1 transportt->eh_strategy_handler() 之前的 SCSI 中层条件

在进入处理程序时,以下条件成立。

  • 每个失败的 scmd 的 eh_flags 字段都已正确设置。

  • 每个失败的 scmd 都通过 scmd->eh_entry 链接到 scmd->eh_cmd_q。

  • SHOST_RECOVERY 已设置。

  • shost->host_failed == shost->host_busy

2.2.2 transportt->eh_strategy_handler() 之后的 SCSI 中层条件

退出处理程序时,必须满足以下条件。

  • shost->host_failed 为零。

  • 每个 scmd 都处于这样的状态,即对 scmd 调用 scsi_setup_cmd_retry() 不会产生任何影响。

  • shost->eh_cmd_q 已清除。

  • 每个 scmd->eh_entry 都已清除。

  • 对每个 scmd 调用 scsi_queue_insert() 或 scsi_finish_command()。 请注意,处理程序可以自由使用 scmd->retries 和 ->allowed 来限制重试次数。

2.2.3 需要考虑的事项

  • 请注意,超时的 scmd 仍然在较低层处于活动状态。 在使用这些 scmd 执行任何其他操作之前,请使较低层忘记它们。

  • 为了保持一致性,在访问/修改 shost 数据结构时,请获取 shost->host_lock。

  • 完成后,每个失败的 sdev 必须忘记所有活动的 scmd。

  • 完成后,每个失败的 sdev 必须准备好接收新命令或脱机。

Tejun Heo htejun@gmail.com

2005 年 9 月 11 日