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总是使用scmd->eh_entry链接。
1.2 scmd如何完成?¶
一旦LLDD获得scmd,LLDD将通过调用从中间层传递的scsi_done回调函数来完成命令,当调用hostt->queuecommand()时,或者块层会超时。
1.2.1 使用scsi_done完成scmd¶
对于所有非EH命令,scsi_done()是完成回调。它只是调用blk_mq_complete_request()
来删除块层定时器并引发BLOCK_SOFTIRQ。
BLOCK_SOFTIRQ间接调用scsi_complete(),该函数调用scsi_decide_disposition()来决定如何处理该命令。scsi_decide_disposition()查看scmd->result值和sense数据来决定如何处理该命令。
SUCCESS
scsi_finish_command()被调用来处理该命令。该函数做一些维护工作,然后调用scsi_io_completion()来完成I/O。然后,scsi_io_completion()通过调用blk_end_request及其相关函数来通知块层已完成的请求,或者在发生错误时找出如何处理剩余数据。
NEEDS_RETRY
ADD_TO_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)被调用来处理该命令。有关更多信息,请参见[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位就会被打开。这阻止了任何新的scmd从blk队列发出到主机;最终,主机上的所有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的恢复操作成功,则从eh_work_q中删除恢复的scmd。
请注意,对scmd的单个恢复操作可以恢复多个scmd。例如,重置设备会恢复设备上的所有失败scmd。
只有在较低严重性操作完成后eh_work_q不为空时,才会采取更高严重性的操作。
EH重用失败的scmd来发出恢复命令。对于超时的scmd,SCSI EH确保LLDD在将scmd重用于EH命令之前忘记该scmd。
当一个scmd被恢复时,该scmd使用scsi_eh_finish_cmd()
从eh_work_q移动到EH本地eh_done_q。在所有scmd都恢复后(eh_work_q为空),调用scsi_eh_flush_done_q()
来重试或错误完成(通知上层失败)恢复的scmd。
只有当其sdev仍然在线(未在EH期间下线)、未设置REQ_FAILFAST并且++scmd->retries小于scmd->allowed时,scmd才会被重试。
2.1.2 scmd通过EH的流程¶
错误完成/超时
- 操作:
scsi_eh_scmd_add()被调用来处理scmd
将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()
来EH-完成scmd
从本地eh_work_q移动到本地eh_done_q
- 锁定:
无
- 并发:
每个单独的eh_work_q最多一个线程,以保持队列操作无锁
EH完成
- 操作:
scsi_eh_flush_done_q()
重试scmd或通知上层失败。可以并发调用,但每个单独的eh_work_q必须最多只有一个线程来无锁地操作队列
scmd从eh_done_q中移除,scmd->eh_entry被清除
如果需要重试,则使用scsi_queue_insert()重新排队scmd
否则,调用scsi_finish_command()来处理scmd
将shost->host_failed清零
- 锁定:
队列或完成函数执行适当的锁定
2.1.3 控制流程¶
通过细粒度回调的EH从scsi_unjam_host()开始。
scsi_unjam_host
锁定shost->host_lock,将shost->eh_cmd_q splice_init到本地eh_work_q,并解锁host_lock。请注意,shost->eh_cmd_q由此操作清除。
调用scsi_eh_get_sense。
scsi_eh_get_sense
对于每个没有有效sense数据的错误完成的命令,都会采取此操作。大多数SCSI传输/LLDD在命令失败时自动获取sense数据(autosense)。建议使用Autosense,因为性能原因,并且因为在CHECK CONDITION发生和此操作之间,sense信息可能会不同步。
请注意,如果不支持autosense,则使用scsi_done()错误完成scmd时,scmd->sense_buffer包含无效的sense数据。在这种情况下,scsi_decide_disposition()总是返回FAILED,因此调用SCSI EH。当scmd到达这里时,获取sense数据并再次调用scsi_decide_disposition()。
调用发出REQUEST_SENSE命令的scsi_request_sense()。如果失败,则不执行任何操作。请注意,不采取任何操作会导致对scmd采取更高严重性的恢复措施。
对scmd调用scsi_decide_disposition()
- SUCCESS
scmd->retries设置为scmd->allowed,防止
scsi_eh_flush_done_q()
重试scmd,并调用scsi_eh_finish_cmd()
。
- NEEDS_RETRY
- 否则
无操作。
如果!list_empty(&eh_work_q),则调用
scsi_eh_ready_devs()
scsi_eh_ready_devs
此函数采取四个越来越严重的措施,使失败的sdev准备好接收新命令。
调用scsi_eh_stu()
scsi_eh_stu
对于每个具有失败的scmd的sdev,该scmd具有有效sense数据,其中
scsi_check_sense()
的判断为FAILED,则发出START STOP UNIT命令,且start=1。请注意,由于我们显式选择错误完成的scmd,因此已知下层已忘记scmd,我们可以将其重用于STU。如果STU成功并且sdev处于离线或就绪状态,则使用
scsi_eh_finish_cmd()
EH-完成sdev上的所有失败scmd。注意 如果hostt->eh_abort_handler()未实现或失败,我们可能仍然有超时的scmd,并且STU不会使下层忘记这些scmd。但是,如果STU成功,则此函数EH-完成sdev上的所有scmd,使下层处于不一致状态。看来只有当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脱机,并EH-完成scmd。
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中间层的完整性。IOW,在[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为零。
shost->eh_cmd_q被清除。
每个scmd->eh_entry都被清除。
scsi_queue_insert()或scsi_finish_command()被调用来处理每个scmd。请注意,处理程序可以自由使用scmd->retries和->allowed来限制重试次数。
2.2.3 需要考虑的事项¶
要知道超时的scmd仍然在下层处于活动状态。在对这些scmd执行任何其他操作之前,请使下层忘记它们。
为了保持一致性,在访问/修改shost数据结构时,请获取shost->host_lock。
完成后,每个失败的sdev都必须忘记所有活动scmd。
完成后,每个失败的sdev必须准备好接收新命令或脱机。
Tejun Heo htejun@gmail.com
2005年9月11日