Linux内核源码分析-scsi子系统-让磁盘转起来-sd_spinup_disk

如何调用到sd_spinup_disk接口,请查看前面文章【深入浅出SCSI子系统(六)SCSI 磁盘驱动】

链接:深入浅出SCSI子系统(六)SCSI 磁盘驱动_sinat_37817094的博客-CSDN博客

函数sd_spinup_disk()代码(摘自文件drivers/scsi/sd.c)

sd_spinup_disk函数尝试等到SCSI磁盘已经准备好,必要时使磁盘转起来。前者发送TEST UNITREADY命令,后者发送START UNIT命令。

/*
 * spinup disk - called only in sd_revalidate_disk()
 */
static void
sd_spinup_disk(struct scsi_disk *sdkp)
{
	unsigned char cmd[10];
	unsigned long spintime_expire = 0;
	int retries, spintime;
	unsigned int the_result;
	struct scsi_sense_hdr sshdr;
	int sense_valid = 0;

	spintime = 0;

	/* Spin up drives, as required.  Only do this at boot time */
	/* Spinup needs to be done for module loads too. */
	do {
		retries = 0;

		do {
			cmd[0] = TEST_UNIT_READY;
			memset((void *) &cmd[1], 0, 9);

			the_result = scsi_execute_req(sdkp->device, cmd,
						      DMA_NONE, NULL, 0,
						      &sshdr, SD_TIMEOUT,
						      SD_MAX_RETRIES, NULL);

			/*
			 * If the drive has indicated to us that it
			 * doesn't have any media in it, don't bother
			 * with any more polling.
			 */
			if (media_not_present(sdkp, &sshdr))
				return;

			if (the_result)
				sense_valid = scsi_sense_valid(&sshdr);
			retries++;
		} while (retries < 3 && 
			 (!scsi_status_is_good(the_result) ||
			  ((driver_byte(the_result) & DRIVER_SENSE) &&
			  sense_valid && sshdr.sense_key == UNIT_ATTENTION)));

		if ((driver_byte(the_result) & DRIVER_SENSE) == 0) {
			/* no sense, TUR either succeeded or failed
			 * with a status error */
			if(!spintime && !scsi_status_is_good(the_result)) {
				sd_printk(KERN_NOTICE, sdkp, "Unit Not Ready\n");
				sd_print_result(sdkp, the_result);
			}
			break;
		}
					
		/*
		 * The device does not want the automatic start to be issued.
		 */
		if (sdkp->device->no_start_on_add)
			break;

		if (sense_valid && sshdr.sense_key == NOT_READY) {
			if (sshdr.asc == 4 && sshdr.ascq == 3)
				break;	/* manual intervention required */
			if (sshdr.asc == 4 && sshdr.ascq == 0xb)
				break;	/* standby */
			if (sshdr.asc == 4 && sshdr.ascq == 0xc)
				break;	/* unavailable */
			/*
			 * Issue command to spin up drive when not ready
			 */
			if (!spintime) {
				sd_printk(KERN_NOTICE, sdkp, "Spinning up disk...");
				cmd[0] = START_STOP;
				cmd[1] = 1;	/* Return immediately */
				memset((void *) &cmd[2], 0, 8);
				cmd[4] = 1;	/* Start spin cycle */
				if (sdkp->device->start_stop_pwr_cond)
					cmd[4] |= 1 << 4;
				scsi_execute_req(sdkp->device, cmd, DMA_NONE,
						 NULL, 0, &sshdr,
						 SD_TIMEOUT, SD_MAX_RETRIES,
						 NULL);
				spintime_expire = jiffies + 100 * HZ;
				spintime = 1;
			}
			/* Wait 1 second for next try */
			msleep(1000);
			printk(".");

		/*
		 * Wait for USB flash devices with slow firmware.
		 * Yes, this sense key/ASC combination shouldn't
		 * occur here.  It's characteristic of these devices.
		 */
		} else if (sense_valid &&
				sshdr.sense_key == UNIT_ATTENTION &&
				sshdr.asc == 0x28) {
			if (!spintime) {
				spintime_expire = jiffies + 5 * HZ;
				spintime = 1;
			}
			/* Wait 1 second for next try */
			msleep(1000);
		} else {
			/* we don't understand the sense code, so it's
			 * probably pointless to loop */
			if(!spintime) {
				sd_printk(KERN_NOTICE, sdkp, "Unit Not Ready\n");
				sd_print_sense_hdr(sdkp, &sshdr);
			}
			break;
		}
				
	} while (spintime && time_before_eq(jiffies, spintime_expire));

	if (spintime) {
		if (scsi_status_is_good(the_result))
			printk("ready\n");
		else
			printk("not responding...\n");
	}
}

这里看到两层循环,外层循环为下面代码,循环的目的为让SCSI磁盘“准备好”给予足够的等待时间。并不是所有情况都需要这样的循环,代码给了两种情况:

一种是在发送START_STOP命令之后,毕竟SCSI磁盘“转动起来”需要一段时间;

另一种针对某些USB闪存设备,它们的固件运行速度很慢。所以用到了局部变量spintime,初始值为0,在遇到上述两种情况下,设置为1。

//外循环
do
{

}} while (spintime && time_before_eq(jiffies, spintime_expire));

内层循环下面代码,发送TEST_UNIT_READY命令,如果已知磁盘介质不存在,情况就简单了,可以直接返回。否则给它三次机会以忽略Unit Attention。

do {
      cmd[0] = TEST_UNIT_READY;
      memset((void *) &cmd[1], 0, 9);

      the_result = scsi_execute_req(sdkp->device, cmd,
                              DMA_NONE, NULL, 0,
                              &sshdr, SD_TIMEOUT,
                              SD_MAX_RETRIES, NULL);

     /*
      * If the drive has indicated to us that it
      * doesn't have any media in it, don't bother
      * with any more polling.
      */
     if (media_not_present(sdkp, &sshdr))
                return;

     if (the_result)
          sense_valid = scsi_sense_valid(&sshdr);
} while (retries < 3 && 
             (!scsi_status_is_good(the_result) ||
              ((driver_byte(the_result) & DRIVER_SENSE) &&
              sense_valid && sshdr.sense_key == UNIT_ATTENTION)));

内层循环退出,根据其结果,有三种情况可以直接退出外层循环

(1)TEST_UNIT_READY成功,这是显而易见的;其他情况都表示设备还没有准备好,需要在外层循环进一步处理。

(2)因为处理必须给予感测数据,但我们拿不到有效的感测数据,前两种情况对应下面代码;

if ((driver_byte(the_result) & DRIVER_SENSE) == 0) {
	/* no sense, TUR either succeeded or failedwith a status error */
    
    if(!spintime && !scsi_status_is_good(the_result)) {
        sd_printk(KERN_NOTICE, sdkp, "Unit Not Ready\n");
        sd_print_result(sdkp, the_result);
    }

    break;
}
		

(3)所谓处理,是发送START_STOP命令,等待磁盘转动起来,但SCSI设备却指定了no_start_on_add域,不希望发送自动启动命令。

/*
* The device does not want the automatic start to be issued.
*/
 if (sdkp->device->no_start_on_add)
            break;

外层循环的剩余代码都是处理设备未准备好的情况的,又有以下三个分支:

• 如果感测数据为NOT_READY,向SCSI磁盘发送STARTUNIT命令,在此之前需要排除三种情况,这些都是通过附加感测码判断的:(1)需要手工介入,(2)磁盘正待机,(3)磁盘不可用。相应代码如下;

 1 if (sense_valid && sshdr.sense_key == NOT_READY) {
  2     if (sshdr.asc == 4 && sshdr.ascq == 3)
  3         break;  /* manual intervention required 需要手工介入*/
  4     if (sshdr.asc == 4 && sshdr.ascq == 0xb)
  5         break;  /* standby 磁盘正待机*/
  6     if (sshdr.asc == 4 && sshdr.ascq == 0xc)
  7         break;  /* unavailable 磁盘不可用*/
  8     /*
  9      * Issue command to spin up drive when not ready
 10      */
 11     if (!spintime) {
 12         sd_printk(KERN_NOTICE, sdkp, "Spinning up disk...");
 13         cmd[0] = START_STOP;
 14         cmd[1] = 1; /* Return immediately */
 15         memset((void *) &cmd[2], 0, 8);
 16         cmd[4] = 1; /* Start spin cycle */
 17         if (sdkp->device->start_stop_pwr_cond)
 18             cmd[4] |= 1 << 4;
 19         scsi_execute_req(sdkp->device, cmd, DMA_NONE,
 20                 NULL, 0, &sshdr,
 21                 SD_TIMEOUT, SD_MAX_RETRIES,
 22                 NULL);
 23         spintime_expire = jiffies + 100 * HZ;
 24         spintime = 1;
 25     }
 26     /* Wait 1 second for next try */
 27     msleep(1000);
 28     printk(".");
 29 
 30 }

• 如果感测数据为UNIT_ATTENTION,并且附加感测码为0x28,说明可能是USB闪存设备的情况。它和上一种情况一样,都在spintime为0的情况下设置它,开始或继续外层循环。相应代码如下;

  1 /*
  2  *  Wait for USB flash devices with slow firmware.
  3  *  Yes, this sense key/ASC combination shouldn't
  4  *  occur here.  It's characteristic of these devices.
  5  */
  6 } else if (sense_valid &&
  7         sshdr.sense_key == UNIT_ATTENTION &&
  8         sshdr.asc == 0x28) {
  9     if (!spintime) {
 10         spintime_expire = jiffies + 5 * HZ;
 11         spintime = 1;
 12     }
 13     /* Wait 1 second for next try */
 14     msleep(1000);
 15 }

• 其他的感测数据都是不支持的,退出外层循环。相应代码如下。

  1 else {
  2     /* we don't understand the sense code, so it's
  3      *              * probably pointless to loop */
  4     if(!spintime) {
  5         sd_printk(KERN_NOTICE, sdkp, "Unit Not Ready\n");
  6         sd_print_sense_hdr(sdkp, &sshdr);
  7     }
  8     break;
  9 }

执行scsi命令。调用scsi_execute_req()这个函数算是scsi核心层提供的,在include/scsi/scsi_device.h中能找到它的声明:

static inline int scsi_execute_req(struct scsi_device *sdev,
    const unsigned char *cmd, int data_direction, void *buffer,
    unsigned bufflen, struct scsi_sense_hdr *sshdr, int timeout,
    int retries, int *resid)
{
    return scsi_execute_req_flags(sdev, cmd, data_direction, buffer,
        bufflen, sshdr, timeout, retries, resid, 0);
}
unsigned char cmd[10];
unsigned int the_result;

do {
      cmd[0] = TEST_UNIT_READY;
      memset((void *) &cmd[1], 0, 9);

      the_result = scsi_execute_req(sdkp->device, cmd,
                              DMA_NONE, NULL, 0,
                              &sshdr, SD_TIMEOUT,
                              SD_MAX_RETRIES, NULL);

      /*
       * If the drive has indicated to us that it
       * doesn't have any media in it, don't bother
       * with any more polling.
       */
       if (media_not_present(sdkp, &sshdr))
                return;

       if (the_result)
            sense_valid = scsi_sense_valid(&sshdr);
          retries++;
} while (retries < 3 && 
             (!scsi_status_is_good(the_result) ||
              ((driver_byte(the_result) & DRIVER_SENSE) &&
              sense_valid && sshdr.sense_key == UNIT_ATTENTION)));

执行一个scsi命令,第一个参数struct scsi_device的结构体指针,第二个参数则命令cmd,就是command.申请的一个unsigned char类型的数组,总共10个元素,赋了值为TEST_UNIT_READY.Test Unit Ready就是一个很基本的SCSI命令.DMA_NONE代表传输方向,buffer和bufflen咱们用不上,因为这个命令就是测试设备准备好了没有,不需要传递什么数据。调用scsi_execute_req()以执行这个Test Unit Ready命令,返回的结果基本上都是好的,除非设备真的有毛病。

sg_turs这个命令就是用来手工发送Test Unit Ready。安装sg3_utils系列软件包。rpm -qa | grep sg3_utils

unsigned int the_result

执行完命令之后,我们用the_result记录下结果,调用scsi_status_is_good()来判断结果.关于scsi_status_is_good()以及和它相关的一些宏定义于include/scsi/scsi.h文件中:

/*
 *  SCSI Architecture Model (SAM) Status codes. Taken from SAM-3 draft
 *  T10/1561-D Revision 4 Draft dated 7th November 2002.
 */
#define SAM_STAT_GOOD            0x00
#define SAM_STAT_CHECK_CONDITION 0x02
#define SAM_STAT_CONDITION_MET   0x04
#define SAM_STAT_BUSY            0x08
#define SAM_STAT_INTERMEDIATE    0x10
#define SAM_STAT_INTERMEDIATE_CONDITION_MET 0x14
#define SAM_STAT_RESERVATION_CONFLICT 0x18
#define SAM_STAT_COMMAND_TERMINATED 0x22    /* obsolete in SAM-3 */
#define SAM_STAT_TASK_SET_FULL   0x28
#define SAM_STAT_ACA_ACTIVE      0x30
#define SAM_STAT_TASK_ABORTED    0x40


/** scsi_status_is_good - check the status return.
 *
 * @status: the status passed up from the driver (including host and
 *          driver components)
 *
 * This returns true for known good conditions that may be treated as
 * command completed normally
 */
static inline int scsi_status_is_good(int status)
{
    /*
     * FIXME: bit0 is listed as reserved in SCSI-2, but is
     * significant in SCSI-3.  For now, we follow the SCSI-2
     * behaviour and ignore reserved bits.
     */
    status &= 0xfe;
    return ((status == SAM_STAT_GOOD) ||
        (status == SAM_STAT_INTERMEDIATE) ||
        (status == SAM_STAT_INTERMEDIATE_CONDITION_MET) ||
        /* FIXME: this is obsolete in SAM-3 */
        (status == SAM_STAT_COMMAND_TERMINATED));
}

the_result和状态码还是有区别的,毕竟状态码只有那么多,用8位来表示足够了,而the_result我们看到是unsigned int,显然它不只是8位,于是我们就充分利用资源,因此就有了下面这些宏:

/*
 *  Use these to separate status msg and our bytes
 *
 *  These are set by:
 *
 *      status byte = set from target device
 *      msg_byte    = return status from host adapter itself.
 *      host_byte   = set by low-level driver to indicate status.
 *      driver_byte = set by mid-level.
 */
#define status_byte(result) (((result) >> 1) & 0x7f)
#define msg_byte(result)    (((result) >> 8) & 0xff)
#define host_byte(result)   (((result) >> 16) & 0xff)
#define driver_byte(result) (((result) >> 24) & 0xff)

#define sense_class(sense)  (((sense) >> 4) & 0x7)
#define sense_error(sense)  ((sense) & 0xf)
#define sense_valid(sense)  ((sense) & 0x80)


/*
 *  These indicate the error that occurred, and what is available.
 */

#define DRIVER_BUSY         0x01
#define DRIVER_SOFT         0x02
#define DRIVER_MEDIA        0x03
#define DRIVER_ERROR        0x04

#define DRIVER_INVALID      0x05
#define DRIVER_TIMEOUT      0x06
#define DRIVER_HARD         0x07
#define DRIVER_SENSE        0x08


最低的byte是作为status byte用,
driver_byte,即bit23到bit31,这8位被用来承载mid-level设置的信息.而这里用它和DRIVER_SENSE相与,则判断的是是否有sense data,scsi世界里的sense data就是错误信息.这里do-while循环就是如果不成功就最多重复三次,循环结束了之后,再次判断有没有sense data,如果没有,则说明也许成功了.

if ((driver_byte(the_result) & DRIVER_SENSE) == 0) {
     /* no sense, TUR either succeeded or failed
      * with a status error */
      if(!spintime && !scsi_status_is_good(the_result)) {
          sd_printk(KERN_NOTICE, sdkp, "Unit Not Ready\n");
          sd_print_result(sdkp, the_result);
      }
      break;
}

Scsi子系统在于错误判断的代码特别的多.而针对sense data的处理则是错误判断的一部分

/*
 * This is a slightly modified SCSI sense "descriptor" format header.
 * The addition is to allow the 0x70 and 0x71 response codes. The idea
 * is to place the salient data from either "fixed" or "descriptor" sense
 * format into one structure to ease application processing.
 *
 * The original sense buffer should be kept around for those cases
 * in which more information is required (e.g. the LBA of a MEDIUM ERROR).
 */
struct scsi_sense_hdr {     /* See SPC-3 section 4.5 */
    u8 response_code;   /* permit: 0x0, 0x70, 0x71, 0x72, 0x73 */
    u8 sense_key;
    u8 asc;
    u8 ascq;
    u8 byte4;
    u8 byte5;
    u8 byte6;
    u8 additional_length;   /* always 0 for fixed sense format */
};

static inline int scsi_sense_valid(struct scsi_sense_hdr *sshdr)
{
    if (!sshdr)
        return 0;

    return (sshdr->response_code & 0x70) == 0x70;
}

这里定义的struct scsi_sense_hdr就是被用来描述一个sense data.”hdr”就是header的意思,因为sense data可能长度比较长,但是其前8个bytes是最重要的,所以这部分被叫做header,或者说头部,大多数情况下只要理睬头部就够了.

我们看函数scsi_execute_req()中第六个参数是struct scsi_sense_hdr *sshdr,如果命令执行出错了,那么sense data就会通过这个参数返回.所以咱们定义了sshdr,然后咱们通过判断它和它的各个成员,来决定下一步.

在sense data中,最基本的一个元素叫做response_code,它相当于为一个sense data定了性,即它属于哪一个类别,因为sense data毕竟有很多种.response code总共就是8个bits,目前使用的值只有70h,71h,72h,73h,其它的像00h到6Fh以及74h到7Eh这些都是保留的,以备将来之用.所以这里判断的就是response code得0x70,0x71,0x72,0x73才是valid,否则就是invalid.这就是scsi_sense_valid()做的事情.

关于sense data,在SCSI Primary Commands(SPC-3)的4.5节Sense data,专门介绍Sense Data的.Sense data中最有意义的东西叫做sense key和sense code.这两个概念基本上确定了你这个错误究竟是什么错误.

do {

} while (retries < 3 && 
             (!scsi_status_is_good(the_result) ||
              ((driver_byte(the_result) & DRIVER_SENSE) &&
              sense_valid && sshdr.sense_key == UNIT_ATTENTION)));

我们判断sshdr的sense_key是不是等于UNIT_ATTENTION,这个信息表示这个设备可能被重置了或者可移动的介质发生了变化,或者更通俗一点说,只要设备发生了一些变化,然后它希望引起主机控制器的关注,比如说设备原本是on-line的,突然变成了off-line,或者反过来,设备从off-line回到了on-line.在正式读写设备之前,如果有UNIT_ATTENTION条件,必须把它给清除掉.而这(清除UNIT ATTENTION)也正是Test Unit Ready的工作之一.

if (sense_valid && sshdr.sense_key == NOT_READY) {
            
if (sshdr.asc == 4 && sshdr.ascq == 3)
         break;  /* manual intervention required */
if (sshdr.asc == 4 && sshdr.ascq == 0xb) 
         break;  /* standby */
if (sshdr.asc == 4 && sshdr.ascq == 0xc) 
         break;  /* unavailable */
}

而如果sense key等于NOT_READY,则表明这个logical unit不能被访问.(NOT READY: Indicates that the logical unit is not accessible.)而如果sense key等于NOT READY,而asc等于04h,ascq等于03h,这表明”Logical Unit Not Ready,Manual Intervention required”.(详见SPC-4,附录D部分)这说明需要人工干预.

 

unsigned char cmd[10];
if (sense_valid && sshdr.sense_key == NOT_READY) {
            if (sshdr.asc == 4 && sshdr.ascq == 3)
                break;  /* manual intervention required */
            if (sshdr.asc == 4 && sshdr.ascq == 0xb)
                break;  /* standby */
            if (sshdr.asc == 4 && sshdr.ascq == 0xc)
                break;  /* unavailable */
            /*
             * Issue command to spin up drive when not ready
             */
            if (!spintime) {
                sd_printk(KERN_NOTICE, sdkp, "Spinning up disk...");
                cmd[0] = START_STOP;
                cmd[1] = 1; /* Return immediately */
                memset((void *) &cmd[2], 0, 8);
                cmd[4] = 1; /* Start spin cycle */
                if (sdkp->device->start_stop_pwr_cond)
                    cmd[4] |= 1 << 4;
                scsi_execute_req(sdkp->device, cmd, DMA_NONE,
                         NULL, 0, &sshdr,
                         SD_TIMEOUT, SD_MAX_RETRIES,
                         NULL);
                spintime_expire = jiffies + 100 * HZ;
                spintime = 1;
            }
            /* Wait 1 second for next try */
            msleep(1000);
            printk(".");
}

如果磁盘确实应该是NOT_READY,于是我们需要发送下一个命令,即START STOP,在SCSI Block Commands-2(SBC-2)的书中,5.17节专门介绍了START STOP UNIT这个命令.这个命令简而言之,就相当于电源开关,SBC-2中Table 48给出了这个命令的格式:

 结合代码看,咱们把cmd[4]设置为1,实际上就等于是把这张图里的START位设置为1.而在SBC-2中,这个START位的含义如下:

 很明显,这就是真正的电源开关.因此前面再次调用scsi_execute_req以执行START STOP UNIT命令,就是真正的让硬盘转起来。

do{

    if (sense_valid && sshdr.sense_key == NOT_READY) {

        if (!spintime) {

            spintime_expire = jiffies + 100 * HZ;
            spintime = 1;
        }
    

         /* Wait 1 second for next try */
         msleep(1000);
         printk(".");
    }
} while (spintime && time_before_eq(jiffies, spintime_expire));

最外的循环,延时设置100s,如果在期间内,硬盘还是NOT_READY状态,会延时1s。最后还是转不起来就退出。

退出后

if (spintime) {
   if (scsi_status_is_good(the_result))
            printk("ready\n");
   else
            printk("not responding...\n");
}

printk打印not responding,没有把硬盘转起来
 

  • 1
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 2
    评论
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Linux技术芯

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值