目录

深入浅出SCSI子系统(二)SCSI 磁盘驱动

1.SCSI磁盘驱动重要结构体:

3.异步执行部分

3 重新校验磁盘

4 让磁盘转起来


深入浅出SCSI子系统(二)SCSI 磁盘驱动

Linux SCSI子系统实现了各种类型的高层驱动,如SCSI磁盘驱动、SCSI磁带驱动和SCSI CDROM驱动等。SCSI磁盘驱动对应的文件sd.c

SCSI总线类型scsi_bus_type   SCSI设备和SCSI驱动 -->sd_probe()

在SCSI设备被发现,或者SCSI(高层)驱动被加载时,会在SCSI总线类型上和驱动链表中驱动,或者设备链表中的设备尝试绑定。在SCSI总线类型(scsi_bus_type,文件drivers/scsi/scsi_sysfs.c)中定义的match回调函数将首先被调用。

1.SCSI磁盘驱动重要结构体:

(1)SCSI磁盘结构体scsi_disk

 这个结构中看到了一个指向通用磁盘描述符的指针,即disk域,看到了指向SCSI设备描述符的指针,即device域,还有一个指向SCSI驱动描述符的指针,即drives域。

在Linux中,用三个结构来共同表示一个SCSI磁盘,每个结构反映了SCSI磁盘的某方面属性:

• scsi_device:给出了作为SCSI设备方面的属性;

• scsi_disk:给出了作为SCSI磁盘方面的属性;

• gendisk:给出了作为通用磁盘方面的属性。

 (2)SCSI驱动结构体 scsi_driver

(3)SCSI磁盘驱动实例为sd_template,定义在文件drivers/scsi/sd.c

PCI子系统和SCSI子系统的工作模式:

(1)PCI子系统的工作模式:PCI核心注册PCI总线类型,扫描PCI总线得到所有的PCI设备。PCI HBA驱动注册PCI设备驱动,逐个和PCI设备根据设备ID匹配;

(2)SCSI子系统的工作模式:SCSI中间层注册SCSI总线类型,SCSI较高层注册SCSI设备驱动,SCSI较低层(SCSI HBA驱动)则负责扫描SCSI总线得到所有的SCSI设备。SCSI设备将和SCSI设备驱动的匹配是根据SCSI INQUIRY响应中的SCSI设备类型域。

2. 同步执行部分

sd_probe函数是SCSI磁盘驱动的实际入口。它在驱动初始化期间,以及有新的SCSI设备被挂载到系统中时被调用。对于每个出现的SCSI设备(不仅仅是磁盘)都会调用一次。这个函数被从SCSI中间层调用,它在给定的<host, channel, id, lun>和新的设备名(如/dev/sda)之间建立映射。更精确地说,在这里选定了块设备的主设备编号和次设备编号。

static int sd_probe(struct device *dev):这个函数具有唯一的参数,即驱动模型中的设备描述符指针。这个指针实际上指向SCSI设备的内嵌通用设备,即sdev_gendev域,因为SCSI设备是通过内嵌通用设备链入SCSI总线类型(scsi_bus_type)的设备链表的。若成功,这个函数返回0;否则返回错误码。

static int sd_probe(struct device *dev)
{
1.struct scsi_device *sdp = to_scsi_device(dev);//获取scsi磁盘设备描述符

2.struct scsi_disk *sdkp;
  sdkp = kzalloc(sizeof(*sdkp), GFP_KERNEL);//分配scsi_disk磁盘描述符

3.struct gendisk *gd;
  gd = alloc_disk(SD_MINORS);//分配gendisk。gendisk描述符的分配相对复杂,需要处理分区,因此调用专门的alloc_disk函数。对于SCSI磁盘,系统固定可以有15个分区,加上作为整体使用的磁盘,一共是SD_MINORS(16)个,作为参数传入。

4.error = ida_get_new(&sd_index_ida, &index);
  error = sd_format_disk_name(“sd”, index, gd->disk_name, DISK_NAME_LEN);//为SCSI磁盘分配设备名,设备名为sda-sdz,sdaa-sdzz, sdaaa-sdzzz

5.sdkp->device = sdp;
  sdkp->driver = &sd_template;
  sdkp->disk = gd;//构建SCSI磁盘各种结构关系中的一部分,将scsi_disk的device、driver及disk域分别指向scsi_disk、scsi_driver及gendisk描述符。

6.device_initialize(&sdkp->dev);//初始化scsi磁盘内嵌device
  sdkp->dev.parent = dev;
  sdkp->dev.class = &sd_disk_class;
  dev_set_name(&sdkp->dev, "%s", dev_name(dev));
  device_add(&sdkp->dev)//为scsi磁盘在sysfs中创建目录
/*SCSI磁盘在sysfs文件系统中创建目录。由于SCSI磁盘的内嵌设备有parent(为SCSI设备的内嵌通用对象),有class(为sd_disk_class),且parent没有class,该目录会在SCSI设备目录中创建在以类名(scsk_disk)为名字的子目录下,目录名字被设置成SCSI设备名。举例说明,/sys/devices/pci0000:00/0000:00:10.0/host2/target2:0:0/2:0:0:0/scsi_disk/2:0:0:0。*/

}

下图:SCSI设备及其派生的SCSI磁盘之间的驱动模型设备关系

3.异步执行部分

sd_probe_async函数解析:

static void sd_probe_async(void *data)
{
1.struct scsi_device *sdp;
  struct scsi_disk *sdkp = data;
  struct gendisk *gd;
 sdp = sdkp->device;
  gd = sdkp->disk;
  index = sdkp->index;
  dev = &sdp->sdev_gendev;//驱动模型中的设备描述符
//通过已存在的关系,找到指向SCSI设备描述符的指针以及指向通用磁盘描述符的指针。

2.    
  gd->fops = &sd_fops;//块设备操作表指针指向sd_fops
  gd->private_data = &sdkp->driver;//private_data域指向scsi_disk结构中driver域的地址
  gd->queue = sdkp->device->request_queue;//queue指向SCSI设备描述符的请求队列

3.sd_revalidate_disk(gd); //该函数的第一次调用,在add_disk之前进行
(1)sd_spinup_disk(sdkp);//向scsi磁盘发送命令,让磁盘转动起来
(2)sd_read_capacity(sdkp, buffer); //读取磁盘容量
(3)sd_read_block_limits(sdkp);
    sd_read_block_characteristics(sdkp);
(4)sd_read_write_protect_flag(sdkp, buffer);//读取磁盘写保护标志
(5)sd_read_cache_type(sdkp, buffer);//读取磁盘缓存类型
(6)sd_read_app_tag_own(sdkp, buffer)//读取磁盘ATO位
(7)sdkp->first_scan = 0;//此标志是判断是否函数第一次调用
(8)blk_queue_ordered(sdkp->disk->queue, ordered, sd_prepare_flush);//根据SCSI磁盘特性确定IO的屏障能力
(9)set_capacity(disk, sdkp->capacity);//根据INQUERY命令查询到的磁盘容量设置

4.blk_queue_prep_rq(sdp->request_queue, sd_prep_fn);//将scsi请求队列的prep_rq_fn设置为sd_prep_fn此后不仅可以处理来自scsi层的请求,也可以处理来自通用io层的请求,可以为bio请求构建scsi命令

5.gd->driverfs_dev = &sdp->sdev_gendev;//sysfs文件系统中联系通用磁盘和scsi设备,每个SCSI device对应一个通用磁盘


6.dev_set_drvdata(dev, sdkp);//将scsi磁盘描述符指针赋值给sdp->sdev_gendev->drv_data域,sdp为scsi_device类型指针scsi磁盘驱动的其它回调函数(如sd_rescan, sd_shutdown等)都需要拿到scsi磁盘描述符

7.add_disk(gd);//将通用磁盘添加到系统

8.SCSI磁盘探测已经完成,我们在输出消息后返回。

}

3 重新校验磁盘

在SCSI磁盘探测过程中,两次调用sd_revalidate_disk。这是有意安排的,让在add_disk之前和之后两次调用sd_revalidate_disk。其原因在于向块I/O子系统确定注册的方式。

sd_revalidate_disk()
{
1.sd_spinup_disk()//向SCSI磁盘发送命令让磁盘转动起来

2.sd_read_capacity(sdkp, buffer);//读取磁盘容量

3.sd_read_write_protect_flag()//读取SCSI磁盘的写保护标志

4.sd_read_cache_type()//读取SCSI磁盘的缓存类型

5.sd_read_app_tag_own()//读取SCSI磁盘的ATO位,即确定操作系统释放可以访问它的DIF三元组中的应用程序标签
//上述函数都是通过向SCSI磁盘发送命令,并查询返回的数据格式。

6.sdkp->first_scan = 0;//通过first_scan域判断sd_revalidate_disk函数及它调用到的上述函数到底是在第一次运行时(add_disk之前),还是在第二次运行时(add_disk之后)。

7.blk_queue_ordered(sdkp->disk->queue, ordered, sd_prepare_flush);//根据SCSI磁盘特性确定IO的屏障能力  将冲刷磁盘缓存的回调函数设置为sd_prepare_flush:最终调用SCSI命令SYNCHRONIZE_CACHE来实现将磁盘缓冲写到介质上

8.set_capacity(disk, sdkp->capacity);//根据INQUERY命令查询到的磁盘容量,设置通用磁盘结构中的capacity域设置

}

4 让磁盘转起来

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

两层循环,外层循环的目的为让SCSI磁盘“准备好”给予足够的等待时间。

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

内层循环退出,根据其结果,有三种情况可以直接退出外层循环:(1)TEST_UNIT_READY成功,这是显而易见的;其他情况都表示设备还没有准备好,需要在外层循环进一步处理。所以:(2)因为处理必须给予感测数据,但我们拿不到有效的感测数据(3)所谓处理,是发送START_STOP命令,等待磁盘转动起来,但SCSI设备却指定了no_start_on_add域,不希望发送自动启动命令。

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

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

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

• 其他的感测数据都是不支持的,退出外层循环。退出外层循环后,根据结果可能输出一些内核消息,本函数即返回到调用者。

GitHub 加速计划 / li / linux-dash
10.39 K
1.2 K
下载
A beautiful web dashboard for Linux
最近提交(Master分支:2 个月前 )
186a802e added ecosystem file for PM2 4 年前
5def40a3 Add host customization support for the NodeJS version 4 年前
Logo

旨在为数千万中国开发者提供一个无缝且高效的云端环境,以支持学习、使用和贡献开源项目。

更多推荐