一、相关概念
如我们了解,Linux IO框架层次结构复杂,IO从用户态到最终的物理硬件设备,需要经历层层的考验,流程比较冗长。Linux中IO栈的冗长也一直有较大争议。个人看来,这方面确实有优化空间,尤其是在本文分析“错误处理机制”的方面,在一些应用场景中,问题很明显。
SCSI层是系统IO框架中的一个关键层,位于块设备层之下,对于SCSI类存储设备来说(包括:SCSI、SAS、光纤磁阵、ISCSI磁阵等),SCSI层是相关IO的必经之路。
实际应用中,可能会出现IO错误或超时的情况,SCSI层提供了相应错误处理机制,用于尽力恢复IO。本文主要介绍SCSI层的错误处理机制。
二、基本原理
SCSI层提供的错误处理机制,主要针对两种IO错误类型(其实也就只有这两种类型):
1、IO错误(IO Error)。IO错误是底层固件(比如SAS控制器固件或光纤卡固件)主动上报的事件(中断),表示下发的IO请求(SCSI命令)执行完成,但是出错了。出现的情况较少,典型情况如:磁盘损坏导致IO执行错误。
2、IO超时(IO Timeout)。IO超时是IO请求下发后,在指定时间内(在IO请求下发时会启动相应的定时器,默认为30s,有些特殊的指令设置为60s或10s)没有执行完成(执行完成的标志是有相应的中断事件上报)。那可能的原因就多了,在IO链路上的每个环节出现问题(而又没有相应的事件主动上报时),都可能出现超时,这种情况相对比较复杂。典型的案例为:光纤卡链路问题(虚断)。
SCSI层提供的错误处理机制基本原理为:
1、在host初始化时启动相应的错误处理内核线程(scsi_eh_x)
2、当出现IO超时(由定时器触发后检测)或IO错误(由软中断触发后检测)时,将相应的错误IO命令加入到指定的队列中,并设置host状态为recovery(非常关键,这意味这至此所有该host的IO都将阻塞至error handler处理完成),然后唤醒相应的内核线程。
3、内核线程中,遍历相应的队列,对每个错误IO进行如下恢复操作:
1)尝试abort相应命令,实质为向指定设备发生Abort指令,如果设备还能响应,则abort成功,该错误IO处理结束。
2)如果abort失败,则尝试向设备发送START_UNIT指令,尝试让设备进行软重启。如果设备还能响应命令,那软重启可能成功。
3)如果软重启失败,那只能尝试硬重启了。硬重启分如下几个层次:
a、先尝试reset device
b、如果device reset失败,则尝试target rest
c、如果target reset失败,则尝试bus(也称channel) reset
d、如果bus reset失败,最后尝试host reset。
4)如果host reset后,设备状态仍不能恢复,那就无计可施了,只能将设备离线了。
5)设备离线后,继续处理相应队列中剩余的错误IO,当所有错误IO都处理完毕后,恢复host状态为running,至此error handler处理完成,Host状态恢复正常,IO阻塞结束。
新版本中的超时机制:
由于error handler的处理时间可能会很长,新版本中加入了超时(deadline)机制,为其设置了一个eh_deadline,该参数可以作为模块加载参数设置:
/*eh_deadline为新版本中加入,为SCSI IO错误处理流程设置超时,防止阻塞时间过长*/
module_param_named(eh_deadline, shost_eh_deadline, uint, S_IRUGO|S_IWUSR);
MODULE_PARM_DESC(eh_deadline,
"SCSI EH deadline in seconds (should be between 1 and 2^32-1)");
也可以通过sysfs设置:
static DEVICE_ATTR(eh_deadline, S_IRUGO | S_IWUSR, show_shost_eh_deadline, store_shost_eh_deadline);
基本原理为:在整个错误处理流程中,增加数个检查点(比如device reset过程中),检查错误处理流程持续时间是否超时,如果超时,则跳过中间流程,直接进入host reset流程,加快处理步伐。
三、主要问题
现有SCSI错误处理流程的主要问题为:
进入SCSI处理处理流程之前,会设置host状态为recovery状态,此后该host将处于阻塞状态,任何发往该host的IO都将阻塞,直至error handler处理完成。这样设计的目的是因为当IO出现超时或错误时,内核并不知道具体是什么原因导致的,问题可能就出在host上,当host出现问题而此时不阻塞整个host时,可能导致错误IO继续下发,错误IO不断累积,导致error handler一直无法完成。但是这样设计可能导致当host下一个硬盘出现问题时,其它硬盘的IO都会阻塞到error handler处理完成,这样会导致整个系统的IO阻塞或延迟,严重时(当host下磁盘数量较多时,比如磁阵或expender场景中),可能导致系统IO瘫痪。这个问题在一些应用场景中(比如共享存储的云计算场景中)非常严重,即使在新内核版本中加入了超时机制,但是由于host reset的时间通常也比较长,当错误IO数量较多时,IO阻塞时间仍比较长,而且不可控,需要考虑优化。
四、主要流程:
1、SCS错误处理线程初始化流程:
scsi_host_alloc->
shost->ehandler = kthread_run(scsi_error_handler, shost,
"scsi_eh_%d", shost->host_no);
2、错误处理线程触发流程:
超时 软中断
scsi_times_out(IO超时) scsi_softirq_done(IO错误)
---------------------
\/
scsi_eh_scmd_add-> //将错误IO请求加入到error handler对应的链表中
scsi_eh_wakeup-> //唤醒错误处理线程进行处理
wake_up_process(shost->ehandler);
3、错误处理线程处理流程:
scsi_error_handler-> //错误线程流程入口
scsi_unjam_host-> //错误处理主要流程
scsi_eh_abort_cmds-> //尝试abort命令
scsi_eh_ready_devs->
scsi_eh_stu-> //尝试软重启
scsi_eh_bus_device_reset-> //尝试reset device
scsi_eh_target_reset-> //尝试reset target
scsi_eh_bus_reset-> //尝试reset bus
scsi_eh_host_reset-> //尝试reset host
scsi_eh_offline_sdevs //将问题设备离线
五、代码分析
scsi_eh_scmd_add():
-
/**
-
* scsi_eh_scmd_add - add scsi cmd to error handling.
-
* @scmd: scmd to run eh on.
-
* @eh_flag: optional SCSI_EH flag.
-
*
-
* Return value:
-
* 0 on failure.
-
*/
-
/*将错误的IO请求(此时已经是scsi命令了(scmd))加入host的错误处理队列shost->eh_cmd_q,然后唤醒SCSI错误处理内核线程*/
-
int scsi_eh_scmd_add(struct scsi_cmnd *scmd, int eh_flag)
-
{
-
struct Scsi_Host *shost = scmd->device->host;
-
unsigned long flags;
-
int ret = 0;
-
-
if (!shost->ehandler)
-
return 0;
-
-
spin_lock_irqsave(shost->host_lock, flags);
-
/*
-
* 设置host状态为recovery状态,此后该host将处于阻塞状态,任何发往该host的IO都将阻塞,直至error handler处理完成
-
* 这样设计的目的是因为当IO出现超时或错误时,内核并不知道具体是什么原因导致的,问题可能就出在host上,
-
* 当host出现问题而此时不阻塞整个host时,可能导致错误IO继续下发,错误IO不断累积,导致error handler一直无法完成
-
* 但是这样设计可能导致当host下一个硬盘出现问题时,其它硬盘的IO都会阻塞到error handler处理完成,这样会导致
-
* 整个系统的IO阻塞或延迟,严重时(当host下磁盘数量较多时,比如磁阵或expender场景中),可能导致系统IO瘫痪。
-
* 这个问题在一些应用场景中(比如共享存储的云计算场景中)非常严重,需要考虑优化。
-
*/
-
if (scsi_host_set_state(shost, SHOST_RECOVERY))
-
if (scsi_host_set_state(shost, SHOST_CANCEL_RECOVERY))
-
goto out_unlock;
-
/*更新last_reset,为后面的deadline检查做准备*/
-
if (shost->eh_deadline && !shost->last_reset)
-
shost->last_reset = jiffies;
-
-
ret = 1;
-
scmd->eh_eflags |= eh_flag;
-
list_add_tail(&scmd->eh_entry, &shost->eh_cmd_q);
-
shost->host_failed++;
-
/*唤醒SCSI错误处理内核线程*/
-
scsi_eh_wakeup(shost);
-
out_unlock:
-
spin_unlock_irqrestore(shost->host_lock, flags);
-
return ret;
-
}
scsi_eh_scmd_add()->scsi_eh_wakeup():
-
/* called with shost->host_lock held */
-
/*唤醒scsi错误处理线程*/
-
void scsi_eh_wakeup(struct Scsi_Host *shost)
-
{
-
if (shost->host_busy == shost->host_failed) {
-
trace_scsi_eh_wakeup(shost);
-
wake_up_process(shost->ehandler);
-
SCSI_LOG_ERROR_RECOVERY(5,
-
printk("Waking error handler thread\n"));
-
}
-
}
scsi_error_handler():
-
/*SCSI错误处理(error handler)线程执行的函数,完成SCSI错误处理*/
-
int scsi_error_handler(void *data)
-
{
-
/*data为host(通常对应光纤卡)对应的数据结构*/
-
struct Scsi_Host *shost = data;
-
-
/*
-
* We use TASK_INTERRUPTIBLE so that the thread is not
-
* counted against the load average as a running process.
-
* We never actually get interrupted because kthread_run
-
* disables signal delivery for the created thread.
-
*/
-
/*循环检查是否需要停止*/
-
while (!kthread_should_stop()) {
-
/*
-
* 设置为S状态。
-
* Fixme: 为什么?如果发生了调度呢?首先,如果没开内核抢占,由于内核线程一直在内核态,所以
-
* 是不会发生调度的,其次,及时发生了内核抢占,那就只能等待下次被唤醒了(SCSI命令超时后唤醒)。
-
* 这里设置S状态,是为后面的主动schedule做准备,内核线程在完成相关处理后,必须要主动执行schedule
-
* 放弃CPU控制器,否则会一直占用CPU,导致其它进程无法调度(在没开内核抢占的情况下)
-
*/
-
set_current_state(TASK_INTERRUPTIBLE);
-
/*错误处理线程退出条件,不太好理解*/
-
if ((shost->host_failed == 0 && shost->host_eh_scheduled == 0) ||
-
shost->host_failed != shost->host_busy) {
-
SCSI_LOG_ERROR_RECOVERY(1,
-
printk("scsi_eh_%d: sleeping\n",
-
shost->host_no));
-
/*错误处理退出*/
-
schedule();
-
continue;
-
}
-
/*不满足退出条件,继续运行,需要设置为R,防止被抢占后不能回来。*/
-
__set_current_state(TASK_RUNNING);
-
/*由于默认的scsi_logging_level设置为0,所以这些打印都看不到*/
-
SCSI_LOG_ERROR_RECOVERY(1,
-
printk("scsi_eh_%d: waking up %d/%d/%d\n",
-
shost->host_no, shost->host_eh_scheduled,
-
shost->host_failed, shost->host_busy));
-
-
/*
-
* We have a host that is failing for some reason. Figure out
-
* what we need to do to get it up and online again (if we can).
-
* If we fail, we end up taking the thing offline.
-
*/
-
if (!shost->eh_noresume && scsi_autopm_get_host(shost) != 0) {
-
SCSI_LOG_ERROR_RECOVERY(1,
-
printk(KERN_ERR "Error handler scsi_eh_%d "
-
"unable to autoresume\n",
-
shost->host_no));
-
continue;
-
}
-
/*如果host定义了特定的错误处理接口和流程,则执行它*/
-
if (shost->transportt->eh_strategy_handler)
-
shost->transportt->eh_strategy_handler(shost);
-
else/*否则执行标准SCSI错误处理流程*/
-
scsi_unjam_host(shost);
-
-
/*
-
* Note - if the above fails completely, the action is to take
-
* individual devices offline and flush the queue of any
-
* outstanding requests that may have been pending. When we
-
* restart, we restart any I/O to any other devices on the bus
-
* which are still online.
-
*/
-
/*错误处理完成,恢复IO执行:清除host的recovery标记,设置为SHOST_RUNNING*/
-
scsi_restart_operations(shost);
-
if (!shost->eh_noresume)
-
scsi_autopm_put_host(shost);
-
}
-
/*错误处理线程出现异常,需要停止,先设置R状态,防止被抢占后无法回来*/
-
__set_current_state(TASK_RUNNING);
-
/*打印错误,退出线程*/
-
SCSI_LOG_ERROR_RECOVERY(1,
-
printk("Error handler scsi_eh_%d exiting\n", shost->host_no));
-
shost->ehandler = NULL;
-
return 0;
-
}
scsi_error_handler()->scsi_unjam_host():
-
/*SCSI错误处理标准流程*/
-
static void scsi_unjam_host(struct Scsi_Host *shost)
-
{
-
unsigned long flags;
-
/*初始化两个队列,一个用于容纳错误IO请求,另一个用于容纳已经完成错误处理的请求*/
-
LIST_HEAD(eh_work_q);
-
LIST_HEAD(eh_done_q);
-
/*先拿host相关的锁*/
-
spin_lock_irqsave(shost->host_lock, flags);
-
/*将shost->eh_cmd_q(其中包括错误的IO请求)链表放入eh_work_q链表中*/
-
list_splice_init(&shost->eh_cmd_q, &eh_work_q);
-
/*操作完成,释放锁*/
-
spin_unlock_irqrestore(shost->host_lock, flags);
-
-
SCSI_LOG_ERROR_RECOVERY(1, scsi_eh_prt_fail_stats(shost, &eh_work_q));
-
/*
-
* 尝试从硬件获取sense信息,即出错信息,需要跟硬件交互获取,只要work_q不为空,都返回失败
-
* Fixme:timeout的命令也需要获取sense信息,岂不是可能再超时,更延长了错误处理时间?
-
*/
-
if (!scsi_eh_get_sense(&eh_work_q, &eh_done_q))
-
/*
-
* 先尝试abort cmd,实际上也是给设备发一个SCSI指令,事情终止错误IO请求的执行,如果Abort成功,
-
* 说明该设备还能响应SCSI指令,此时可能设备已经恢复正常了,当所有的错误IO都Abort成功了,
-
* 或者检测到设备恢复正常了,那就结束错误恢复流程了。
-
*/
-
if (!scsi_eh_abort_cmds(&eh_work_q, &eh_done_q))
-
/*如果Abort不成功,那就只有继续错误处理了,后面就要开始重启设备了。*/
-
scsi_eh_ready_devs(shost, &eh_work_q, &eh_done_q);
-
-
spin_lock_irqsave(shost->host_lock, flags);
-
if (shost->eh_deadline)
-
shost->last_reset = 0;
-
spin_unlock_irqrestore(shost->host_lock, flags);
-
scsi_eh_flush_done_q(&eh_done_q);
-
}
scsi_error_handler()->scsi_unjam_host()->scsi_eh_ready_devs():
-
/**
-
* scsi_eh_ready_devs - check device ready state and recover if not.
-
* @shost: host to be recovered.
-
* @work_q: &list_head for pending commands.
-
* @done_q: &list_head for processed commands.
-
*/
-
/*
-
* SCSI错误处理流程:检测设备状态,并尽力恢复,可能阻塞的时间较长
-
* 在Abort命令不成功的情况下执行,此时就只有开始重启设备了。先尝试软重启,再尝试硬重启
-
*/
-
*/
-
void scsi_eh_ready_devs(struct Scsi_Host *shost,
-
struct list_head *work_q,
-
struct list_head *done_q)
-
{
-
/*向设备发送START_UNIT SCSI指令,尝试软重启*/
-
if (!scsi_eh_stu(shost, work_q, done_q))
-
/*
-
* 软重启不起作用,只有开始硬重启了(跟固件交互完成),实际调用底层驱动注册的钩子完成相关操作。
-
* 这里先进行device reset,这里的device对应于LUN
-
*/
-
if (!scsi_eh_bus_device_reset(shost, work_q, done_q))
-
/*重启target,这里的target对应于磁阵端控制器的接口*/
-
if (!scsi_eh_target_reset(shost, work_q, done_q))
-
/*重启bus(也称channel)*/
-
if (!scsi_eh_bus_reset(shost, work_q, done_q))
-
/*重启host,对应HBA卡*/
-
if (!scsi_eh_host_reset(work_q, done_q))
-
/*所有招数都用完了,还不能恢复,那就只能将设备离线了*/
-
scsi_eh_offline_sdevs(work_q,
-
done_q);
-
}
scsi_error_handler()->scsi_unjam_host()->scsi_eh_ready_devs()->scsi_eh_offline_sdevs():
-
/**
-
* scsi_eh_offline_sdevs - offline scsi devices that fail to recover
-
* @work_q: list_head for processed commands.
-
* @done_q: list_head for processed commands.
-
*/
-
/*在SCSI错误处理流程的尾部,之前的所有恢复手段都无效情况下,只能将设备离线了*/
-
static void scsi_eh_offline_sdevs(struct list_head *work_q,
-
struct list_head *done_q)
-
{
-
struct scsi_cmnd *scmd, *next;
-
/*这就是我们常在messages中看到的信息。*/
-
list_for_each_entry_safe(scmd, next, work_q, eh_entry) {
-
sdev_printk(KERN_INFO, scmd->device, "Device offlined - "
-
"not ready after error recovery\n");
-
/*设置设备状态为离线,后续新IO请求将不会再向此设备下发,而直接向上返回错误*/
-
scsi_device_set_state(scmd->device, SDEV_OFFLINE);
-
if (scmd->eh_eflags & SCSI_EH_CANCEL_CMD) {
-
/*
-
* FIXME: Handle lost cmds.
-
*/
-
}
-
/*将相关命令放入done队列*/
-
scsi_eh_finish_cmd(scmd, done_q);
-
}
-
return;
-
}
阅读(14200) | 评论(2) | 转发(0) |