Chinaunix首页 | 论坛 | 博客
  • 博客访问: 1248248
  • 博文数量: 122
  • 博客积分: 0
  • 博客等级: 民兵
  • 技术积分: 4004
  • 用 户 组: 普通用户
  • 注册时间: 2014-02-20 08:27
文章分类
文章存档

2016年(1)

2015年(21)

2014年(100)

分类: LINUX

2015-03-30 07:24:04

一、相关概念

如我们了解,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():

点击(此处)折叠或打开

  1. /**
  2.  * scsi_eh_scmd_add - add scsi cmd to error handling.
  3.  * @scmd:    scmd to run eh on.
  4.  * @eh_flag:    optional SCSI_EH flag.
  5.  *
  6.  * Return value:
  7.  *    0 on failure.
  8.  */
  9. /*将错误的IO请求(此时已经是scsi命令了(scmd))加入host的错误处理队列shost->eh_cmd_q,然后唤醒SCSI错误处理内核线程*/
  10. int scsi_eh_scmd_add(struct scsi_cmnd *scmd, int eh_flag)
  11. {
  12.     struct Scsi_Host *shost = scmd->device->host;
  13.     unsigned long flags;
  14.     int ret = 0;

  15.     if (!shost->ehandler)
  16.         return 0;

  17.     spin_lock_irqsave(shost->host_lock, flags);
  18.     /*
  19.      * 设置host状态为recovery状态,此后该host将处于阻塞状态,任何发往该host的IO都将阻塞,直至error handler处理完成
  20.      * 这样设计的目的是因为当IO出现超时或错误时,内核并不知道具体是什么原因导致的,问题可能就出在host上,
  21.      * 当host出现问题而此时不阻塞整个host时,可能导致错误IO继续下发,错误IO不断累积,导致error handler一直无法完成
  22.      * 但是这样设计可能导致当host下一个硬盘出现问题时,其它硬盘的IO都会阻塞到error handler处理完成,这样会导致
  23.      * 整个系统的IO阻塞或延迟,严重时(当host下磁盘数量较多时,比如磁阵或expender场景中),可能导致系统IO瘫痪。
  24.      * 这个问题在一些应用场景中(比如共享存储的云计算场景中)非常严重,需要考虑优化。
  25.      */
  26.     if (scsi_host_set_state(shost, SHOST_RECOVERY))
  27.         if (scsi_host_set_state(shost, SHOST_CANCEL_RECOVERY))
  28.             goto out_unlock;
  29.     /*更新last_reset,为后面的deadline检查做准备*/
  30.     if (shost->eh_deadline && !shost->last_reset)
  31.         shost->last_reset = jiffies;

  32.     ret = 1;
  33.     scmd->eh_eflags |= eh_flag;
  34.     list_add_tail(&scmd->eh_entry, &shost->eh_cmd_q);
  35.     shost->host_failed++;
  36.     /*唤醒SCSI错误处理内核线程*/
  37.     scsi_eh_wakeup(shost);
  38.  out_unlock:
  39.     spin_unlock_irqrestore(shost->host_lock, flags);
  40.     return ret;
  41. }

scsi_eh_scmd_add()->scsi_eh_wakeup():



点击(此处)折叠或打开

  1. /* called with shost->host_lock held */
  2. /*唤醒scsi错误处理线程*/
  3. void scsi_eh_wakeup(struct Scsi_Host *shost)
  4. {
  5.     if (shost->host_busy == shost->host_failed) {
  6.         trace_scsi_eh_wakeup(shost);
  7.         wake_up_process(shost->ehandler);
  8.         SCSI_LOG_ERROR_RECOVERY(5,
  9.                 printk("Waking error handler thread\n"));
  10.     }
  11. }


scsi_error_handler():

点击(此处)折叠或打开

  1. /*SCSI错误处理(error handler)线程执行的函数,完成SCSI错误处理*/
  2. int scsi_error_handler(void *data)
  3. {
  4.     /*data为host(通常对应光纤卡)对应的数据结构*/
  5.     struct Scsi_Host *shost = data;

  6.     /*
  7.      * We use TASK_INTERRUPTIBLE so that the thread is not
  8.      * counted against the load average as a running process.
  9.      * We never actually get interrupted because kthread_run
  10.      * disables signal delivery for the created thread.
  11.      */
  12.     /*循环检查是否需要停止*/
  13.     while (!kthread_should_stop()) {
  14.         /*
  15.          * 设置为S状态。
  16.          * Fixme: 为什么?如果发生了调度呢?首先,如果没开内核抢占,由于内核线程一直在内核态,所以
  17.          * 是不会发生调度的,其次,及时发生了内核抢占,那就只能等待下次被唤醒了(SCSI命令超时后唤醒)
  18.          * 这里设置S状态,是为后面的主动schedule做准备,内核线程在完成相关处理后,必须要主动执行schedule
  19.          * 放弃CPU控制器,否则会一直占用CPU,导致其它进程无法调度(在没开内核抢占的情况下)
  20.          */
  21.         set_current_state(TASK_INTERRUPTIBLE);
  22.         /*错误处理线程退出条件,不太好理解*/
  23.         if ((shost->host_failed == 0 && shost->host_eh_scheduled == 0) ||
  24.          shost->host_failed != shost->host_busy) {
  25.             SCSI_LOG_ERROR_RECOVERY(1,
  26.                 printk("scsi_eh_%d: sleeping\n",
  27.                     shost->host_no));
  28.             /*错误处理退出*/
  29.             schedule();
  30.             continue;
  31.         }
  32.         /*不满足退出条件,继续运行,需要设置为R,防止被抢占后不能回来。*/
  33.         __set_current_state(TASK_RUNNING);
  34.         /*由于默认的scsi_logging_level设置为0,所以这些打印都看不到*/
  35.         SCSI_LOG_ERROR_RECOVERY(1,
  36.             printk("scsi_eh_%d: waking up %d/%d/%d\n",
  37.              shost->host_no, shost->host_eh_scheduled,
  38.              shost->host_failed, shost->host_busy));

  39.         /*
  40.          * We have a host that is failing for some reason. Figure out
  41.          * what we need to do to get it up and online again (if we can).
  42.          * If we fail, we end up taking the thing offline.
  43.          */
  44.         if (!shost->eh_noresume && scsi_autopm_get_host(shost) != 0) {
  45.             SCSI_LOG_ERROR_RECOVERY(1,
  46.                 printk(KERN_ERR "Error handler scsi_eh_%d "
  47.                         "unable to autoresume\n",
  48.                         shost->host_no));
  49.             continue;
  50.         }
  51.         /*如果host定义了特定的错误处理接口和流程,则执行它*/
  52.         if (shost->transportt->eh_strategy_handler)
  53.             shost->transportt->eh_strategy_handler(shost);
  54.         else/*否则执行标准SCSI错误处理流程*/
  55.             scsi_unjam_host(shost);

  56.         /*
  57.          * Note - if the above fails completely, the action is to take
  58.          * individual devices offline and flush the queue of any
  59.          * outstanding requests that may have been pending. When we
  60.          * restart, we restart any I/O to any other devices on the bus
  61.          * which are still online.
  62.          */
  63.         /*错误处理完成,恢复IO执行:清除host的recovery标记,设置为SHOST_RUNNING*/
  64.         scsi_restart_operations(shost);
  65.         if (!shost->eh_noresume)
  66.             scsi_autopm_put_host(shost);
  67.     }
  68.     /*错误处理线程出现异常,需要停止,先设置R状态,防止被抢占后无法回来*/
  69.     __set_current_state(TASK_RUNNING);
  70.     /*打印错误,退出线程*/
  71.     SCSI_LOG_ERROR_RECOVERY(1,
  72.         printk("Error handler scsi_eh_%d exiting\n", shost->host_no));
  73.     shost->ehandler = NULL;
  74.     return 0;
  75. }

scsi_error_handler()->scsi_unjam_host():

点击(此处)折叠或打开

  1. /*SCSI错误处理标准流程*/
  2. static void scsi_unjam_host(struct Scsi_Host *shost)
  3. {
  4.     unsigned long flags;
  5.     /*初始化两个队列,一个用于容纳错误IO请求,另一个用于容纳已经完成错误处理的请求*/
  6.     LIST_HEAD(eh_work_q);
  7.     LIST_HEAD(eh_done_q);
  8.     /*先拿host相关的锁*/
  9.     spin_lock_irqsave(shost->host_lock, flags);
  10.     /*将shost->eh_cmd_q(其中包括错误的IO请求)链表放入eh_work_q链表中*/
  11.     list_splice_init(&shost->eh_cmd_q, &eh_work_q);
  12.     /*操作完成,释放锁*/
  13.     spin_unlock_irqrestore(shost->host_lock, flags);

  14.     SCSI_LOG_ERROR_RECOVERY(1, scsi_eh_prt_fail_stats(shost, &eh_work_q));
  15.     /*
  16.      * 尝试从硬件获取sense信息,即出错信息,需要跟硬件交互获取,只要work_q不为空,都返回失败
  17.      * Fixme:timeout的命令也需要获取sense信息,岂不是可能再超时,更延长了错误处理时间?
  18.      */
  19.     if (!scsi_eh_get_sense(&eh_work_q, &eh_done_q))
  20.         /*
  21.          * 先尝试abort cmd,实际上也是给设备发一个SCSI指令,事情终止错误IO请求的执行,如果Abort成功,
  22.          * 说明该设备还能响应SCSI指令,此时可能设备已经恢复正常了,当所有的错误IO都Abort成功了,
  23.          * 或者检测到设备恢复正常了,那就结束错误恢复流程了。
  24.          */
  25.         if (!scsi_eh_abort_cmds(&eh_work_q, &eh_done_q))
  26.             /*如果Abort不成功,那就只有继续错误处理了,后面就要开始重启设备了。*/
  27.             scsi_eh_ready_devs(shost, &eh_work_q, &eh_done_q);

  28.     spin_lock_irqsave(shost->host_lock, flags);
  29.     if (shost->eh_deadline)
  30.         shost->last_reset = 0;
  31.     spin_unlock_irqrestore(shost->host_lock, flags);
  32.     scsi_eh_flush_done_q(&eh_done_q);
  33. }

scsi_error_handler()->scsi_unjam_host()->scsi_eh_ready_devs():

点击(此处)折叠或打开

  1. /**
  2.  * scsi_eh_ready_devs - check device ready state and recover if not.
  3.  * @shost:     host to be recovered.
  4.  * @work_q: &list_head for pending commands.
  5.  * @done_q:    &list_head for processed commands.
  6.  */
  7. /*
  8.   * SCSI错误处理流程:检测设备状态,并尽力恢复,可能阻塞的时间较长
  9.   * 在Abort命令不成功的情况下执行,此时就只有开始重启设备了。先尝试软重启,再尝试硬重启
  10.   */
  11.   */
  12. void scsi_eh_ready_devs(struct Scsi_Host *shost,
  13.             struct list_head *work_q,
  14.             struct list_head *done_q)
  15. {
  16.     /*向设备发送START_UNIT SCSI指令,尝试软重启*/
  17.     if (!scsi_eh_stu(shost, work_q, done_q))
  18.         /*
  19.          * 软重启不起作用,只有开始硬重启了(跟固件交互完成),实际调用底层驱动注册的钩子完成相关操作。
  20.          * 这里先进行device reset,这里的device对应于LUN
  21.          */
  22.         if (!scsi_eh_bus_device_reset(shost, work_q, done_q))
  23.             /*重启target,这里的target对应于磁阵端控制器的接口*/
  24.             if (!scsi_eh_target_reset(shost, work_q, done_q))
  25.                 /*重启bus(也称channel)*/
  26.                 if (!scsi_eh_bus_reset(shost, work_q, done_q))
  27.                     /*重启host,对应HBA卡*/
  28.                     if (!scsi_eh_host_reset(work_q, done_q))
  29.                         /*所有招数都用完了,还不能恢复,那就只能将设备离线了*/
  30.                         scsi_eh_offline_sdevs(work_q,
  31.                                  done_q);
  32. }

scsi_error_handler()->scsi_unjam_host()->scsi_eh_ready_devs()->scsi_eh_offline_sdevs():

点击(此处)折叠或打开

  1. /**
  2.  * scsi_eh_offline_sdevs - offline scsi devices that fail to recover
  3.  * @work_q:    list_head for processed commands.
  4.  * @done_q:    list_head for processed commands.
  5.  */
  6. /*在SCSI错误处理流程的尾部,之前的所有恢复手段都无效情况下,只能将设备离线了*/
  7. static void scsi_eh_offline_sdevs(struct list_head *work_q,
  8.                  struct list_head *done_q)
  9. {
  10.     struct scsi_cmnd *scmd, *next;
  11.     /*这就是我们常在messages中看到的信息。*/
  12.     list_for_each_entry_safe(scmd, next, work_q, eh_entry) {
  13.         sdev_printk(KERN_INFO, scmd->device, "Device offlined - "
  14.              "not ready after error recovery\n");
  15.         /*设置设备状态为离线,后续新IO请求将不会再向此设备下发,而直接向上返回错误*/
  16.         scsi_device_set_state(scmd->device, SDEV_OFFLINE);        
  17.         if (scmd->eh_eflags & SCSI_EH_CANCEL_CMD) {
  18.             /*
  19.              * FIXME: Handle lost cmds.
  20.              */
  21.         }
  22.         /*将相关命令放入done队列*/
  23.         scsi_eh_finish_cmd(scmd, done_q);
  24.     }
  25.     return;
  26. }



阅读(14397) | 评论(2) | 转发(0) |
给主人留下些什么吧!~~

humjb_19832015-03-30 12:29:34

CU官方博客:加油亲,祝前途光明

呵呵,谢谢!!!

回复 | 举报

CU官方博客2015-03-30 11:06:07

加油亲,祝前途光明