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()。当发生超时时,此函数
调用可选的 hostt->eh_timed_out() 回调。返回值可以是以下之一
- SCSI_EH_RESET_TIMER
这表示需要更多时间来完成命令。定时器重新启动。
- SCSI_EH_NOT_HANDLED
eh_timed_out() 回调未处理该命令。执行步骤 #2。
- SCSI_EH_DONE
eh_timed_out() 完成了命令。
调用 scsi_abort_command() 来安排异步中止,这可能会发出重试 scmd->allowed + 1 次。对于设置了 SCSI_EH_ABORT_SCHEDULED 标志的命令(这表示该命令已被中止一次,这是失败的重试),当重试次数超过限制时,或者当 EH 截止时间已过期时,不会调用异步中止。在这些情况下,执行步骤 #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,它执行以下操作。
将 scmd->eh_entry 链接到 shost->eh_cmd_q
在 shost->shost_state 中设置 SHOST_RECOVERY 位
递增 shost->host_failed
如果 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() 恢复正常操作,该函数会
检查是否需要锁定门并锁定门。
清除 SHOST_RECOVERY shost_state 位
唤醒 shost->host_wait 上的等待者。如果有人在主机上调用
scsi_block_when_processing_errors()
,则会发生这种情况。(问题 为什么需要它?所有操作在到达 blk 队列后都将被阻止。)在主机上踢所有设备的队列
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 的流程¶
错误完成/超时
- 操作:
为 scmd 调用 scsi_eh_scmd_add()
将 scmd 添加到 shost->eh_cmd_q
设置 SHOST_RECOVERY
shost->host_failed++
- 锁定:
shost->host_lock
EH 开始
- 操作:
将所有 scmd 移动到 EH 的本地 eh_work_q。清除 shost->eh_cmd_q。
- 锁定:
shost->host_lock(并非绝对必要,只是为了保持一致性)
scmd 已恢复
- 操作:
调用
scsi_eh_finish_cmd()
以完成 scmd 的 EH 处理
scsi_setup_cmd_retry()
从本地 eh_work_q 移动到本地 eh_done_q
- 锁定:
无
- 并发性:
每个独立的 eh_work_q 最多一个线程,以保持队列操作无锁
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
锁定 shost->host_lock,将 shost->eh_cmd_q 拼接初始化到本地 eh_work_q 并解锁 host_lock。 请注意,此操作会清除 shost->eh_cmd_q。
调用 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()。
调用 scsi_request_sense(),它发出 REQUEST_SENSE 命令。 如果失败,则不执行任何操作。 请注意,不执行任何操作会导致对 scmd 采取更严重级别的恢复措施。
对 scmd 调用 scsi_decide_disposition()
- 成功
scmd->retries 设置为 scmd->allowed,防止
scsi_eh_flush_done_q()
重试 scmd,并调用scsi_eh_finish_cmd()
。
- 需要重试
- 否则
无操作。
如果 !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。
如果 !list_empty(&eh_work_q),则调用
scsi_eh_ready_devs()
scsi_eh_ready_devs
此函数采取四种越来越严重的措施,使失败的 sdev 准备好接收新命令。
调用 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 操作。
如果 !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。
如果 !list_empty(&eh_work_q),则调用 scsi_eh_bus_reset()
scsi_eh_bus_reset
为每个包含失败 scmd 的通道调用 hostt->eh_bus_reset_handler()。如果总线重置成功,则完成通道上所有就绪或脱机 sdev 的所有失败 scmd 的 EH 处理。
如果 !list_empty(&eh_work_q),则调用 scsi_eh_host_reset()
scsi_eh_host_reset
这是最后的手段。调用 hostt->eh_host_reset_handler()。如果主机重置成功,则完成主机上所有就绪或脱机 sdev 的所有失败 scmd 的 EH 处理。
如果 !list_empty(&eh_work_q),则调用 scsi_eh_offline_sdevs()
scsi_eh_offline_sdevs
将所有仍有未恢复 scmd 的 sdev 脱机并完成 scmd 的 EH 处理。
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 日