深入浅出SCSI子系统(七)SCSI命令执行

2023-12-28 10:09

本文主要是介绍深入浅出SCSI子系统(七)SCSI命令执行,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

目录

SCSI命令执行

scsi_execute_req

scsi_execute

blk_execute_rq

blk_execute_rq_nowait

blk_end_sync_rq


SCSI命令执行

scsi_execute_req

上面看到,在探测过程中发送的SCSI命令,如INQUIRY和SPINUP,它们的执行都调用了scsi_execute_req函数。从请求的角度,它们是发源于SCSI层的,但更常见的是来自上层的请求,如读/写文件等。这里将跟踪scsi_execute_req函数的执行,至于来自上层的I/O请求执行过程,在防止后面的块I/O子系统中讨论。

函数scsi_execute_req()代码(摘自文件drivers/scsi/scsi_lib.c)

int scsi_execute_req_flags(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, u64 flags)
{}

scsi_execute_req函数插入请求并等待结果。

第一个参数为指向SCSI设备描述符的指针;

第二个参数为要执行的SCSI命令字符串;

第三个参数为数据传输方向,DMA_FROM_DEVICE和DMA_TO_DEVICE分别表示从设备读取、向设备写入,而DMA_NONE表示没有数据传输;

第四个参数为指向保存结果的缓冲区指针;

第五个参数为缓冲区长度;

第六个参数为指向用于保存标准格式的感测头信息的缓冲区;

第七个参数为以秒为单位的请求超时时间;

第八个参数为重试请求的次数;

第九个参数为可选的剩余长度。

int scsi_execute_req_flags(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, u64 flags)
{char *sense = NULL;int result;if (sshdr) {sense = kzalloc(SCSI_SENSE_BUFFERSIZE, GFP_NOIO);if (!sense)return DRIVER_ERROR << 24;}result = scsi_execute(sdev, cmd, data_direction, buffer, bufflen,sense, timeout, retries, flags, resid);if (sshdr)scsi_normalize_sense(sense, SCSI_SENSE_BUFFERSIZE, sshdr);//用于将固定感测数据格式和描述符感测数据格式的感测数据规格化成一种公共的格式kfree(sense);return result;
}

SCSI规范支持固定感测数据格式和描述符感测数据格式。两个格式在所包含的域以及这些域的偏移位置都不尽相同。Linux提取它们的相似点,定义了一个公共的格式,这就是scsi_sense_hdr结构(参见文件include/scsi/scsi_eh.h)。

如果调用者要求返回感测头信息,则需要在执行SCSI命令的同时获取感测数据,为此分配必要的空间sense = kzalloc(SCSI_SENSE_BUFFERSIZE, GFP_NOIO)。

然后调用scsi_execute函数执行具体的工作,这个函数将返回命令执行的结果,并且把感测数据保存下来。

接着上面的话题,如果调用者要求返回感测头信息,就需要基于感测数据转换。SCSI中间层提供了一个公共函数scsi_normalize_sense(文件drivers/scsi/scsi_error.c),用于将固定感测数据格式和描述符感测数据格式的感测数据规格化成一种公共的格式。

scsi_execute

函数scsi_execute()代码(摘自文件drivers/scsi/scsi_lib.c)

int scsi_execute(struct scsi_device *sdev, const unsigned char *cmd,int data_direction, void *buffer, unsigned bufflen,unsigned char *sense, int timeout, int retries, u64 flags,int *resid)

scsi_execute函数的参数基本上和前面的调用者相同。成功返回0;否则返回非零值。

int scsi_execute(struct scsi_device *sdev, const unsigned char *cmd,int data_direction, void *buffer, unsigned bufflen,unsigned char *sense, int timeout, int retries, u64 flags,int *resid)
{struct request *req;int write = (data_direction == DMA_TO_DEVICE);int ret = DRIVER_ERROR << 24;req = blk_get_request(sdev->request_queue, write, __GFP_RECLAIM);if (IS_ERR(req))return ret;
}

它调用来自块I/O子系统的blk_get_request函数从SCSI请求队列中分配一个空闲的request(块设备驱动层请求,在块I/O子系统中介绍),估计这是将request_queue域放在scsi_device结构中,而不是scsi_disk结构中的原因。毕竟,有源自SCSI中间层的请求被添加到SCSI设备的请求队列,即使SCSI磁盘驱动还没有加载,或对于非磁盘类SCSI设备。

int scsi_execute(struct scsi_device *sdev, const unsigned char *cmd,int data_direction, void *buffer, unsigned bufflen,unsigned char *sense, int timeout, int retries, u64 flags,int *resid)
{if (bufflen &&	blk_rq_map_kern(sdev->request_queue, req,buffer, bufflen, __GFP_RECLAIM))
}

调用blk_rq_map_kern将内核数据映射到request。

• int blk_rq_map_kern(struct request_queue *, struct request *, void *,unsigned int, gfp_t)映射内核数据到一个块设备驱动层请求,用于REQ_TYPE_BLOCK_PC;

• int blk_rq_map_user(struct request_queue *, struct request *, structrq_map_data *, void __user *, unsigned long, gfp_t)映射用户数据到一个块设备驱动层请求,用户与REQ_TYPE_BLOCK_PC;

• int blk_rq_map_sg(struct request_queue *, struct request *, structscatterlist *)映射一个块设备驱动层请求到聚散列表;

• int blk_rq_map_integrity_sg(struct request *, struct scatterlist *)映射块设备驱动层请求中的完整性向量到聚散列表。

int scsi_execute(struct scsi_device *sdev, const unsigned char *cmd,int data_direction, void *buffer, unsigned bufflen,unsigned char *sense, int timeout, int retries, u64 flags,int *resid)
{req->cmd_len = COMMAND_SIZE(cmd[0]);memcpy(req->cmd, cmd, req->cmd_len);req->sense = sense;req->sense_len = 0;req->retries = retries;req->timeout = timeout;req->cmd_flags |= flags | REQ_QUIET | REQ_PREEMPT;
}

初始化request描述符,包括:

• 将SCSI命令字符串复制到其中,并设置其长度;

• 设置感测数据缓冲区,并清零感测数据长度;

• 设置请求执行的重试次数和超时时间;

• 设置请求的命令类型REQ_TYPE_BLOCK_PC,它让SCSI策略例程知道如何执行该请求,确切地说,如何为这个请求构造可以派发到SCSI主机适配器的SCSI命令。

至此,已经完全准备好一个请求,现在可以提交执行。根据块I/O子系统的规则,应该将它插入到I/O调度器队列。

int scsi_execute(struct scsi_device *sdev, const unsigned char *cmd,int data_direction, void *buffer, unsigned bufflen,unsigned char *sense, int timeout, int retries, u64 flags,int *resid)
{/** head injection *required* here otherwise quiesce won't work*/blk_execute_rq(req->q, NULL, req, 1);
}

但需要注意两点,这个SCSI命令的执行要求同步,也就是说,只有等待命令执行完成,才能继续往下走,块I/O子系统为此提供了一个公共函数blk_execute_rq。这个SCSI命令比较紧急,需要优先执行,第四个参数传入1,在插入I/O调度器队列时将它放在队列头部。此外,第二个参数为NULL,笔者判断这是因为对设备的请求源自SCSI层,我们不能认定这就是一个SCSI磁盘类设备,或者SCSI磁盘驱动已经被加载。

blk_execute_rq

函数blk_execute_rq()代码(摘自文件block/blk_exec.c)

int blk_execute_rq(struct request_queue *q, struct gendisk *bd_disk,struct request *rq, int at_head)

blk_execute_rq是块I/O子系统提供的公共函数,它被SCSI层调用,这是Linux内核中低层调用高层的又一例证。函数插入一个完全准备好的请求到I/O调度器队列中,以备执行,并等待其完成。第一个参数为指向请求队列描述符的指针;第二个参数为指向对应通用磁盘描述符的指针;第三个参数为指向要插入的请求的指针;第四个参数表示插入的位置,如果为1,表示将请求插入队列头部;否则插入队列尾部。

函数利用Linux内核中的completion来实现完成等待逻辑。

completion是一种简单的同步机制,标志“things may proceed”。要使用completion,必须在文件中包含<linux/completion.h>,同时创建一个类型为struct completion的变量。这个变量可以静态地声明和初始化:

#define DECLARE_COMPLETION(work) \struct completion work = COMPLETION_INITIALIZER(work)DECLARE_COMPLETION(my_comple)

或者动态初始化:

static inline void init_completion(struct completion *x)struct completion my_comple;
init_completion(my_comple);

 如果驱动程序要在执行后面操作之前等待某个过程的完成,它可以调用wait_for_completion,以要完成的事件为参数:

extern void wait_for_completion(struct completion *);

如果其他部分代码可以确定事件已经完成,可以调用下面两个函数之一来唤醒等待该事件的进程:

extern void complete(struct completion *);
extern void complete_all(struct completion *);

前一个函数将只唤醒一个等待进程,而后一个函数唤醒等待该事件的所有进程。由于completion的实现方式,即使complete在wait_for_competion之前调用,也可以正常工作。

int blk_execute_rq(struct request_queue *q, struct gendisk *bd_disk,struct request *rq, int at_head)
{DECLARE_COMPLETION_ONSTACK(wait);rq->end_io_data = &wait;
}

就blk_execute_rq函数的例子,它将completion变量记录在request的end_io_data域。

int blk_execute_rq(struct request_queue *q, struct gendisk *bd_disk,struct request *rq, int at_head)
{blk_execute_rq_nowait(q, bd_disk, rq, at_head, blk_end_sync_rq);
}

接着调用blk_execute_rq_nowait函数,传入一个回调函数blk_end_sync_rq,之后调用wait_for_completion在completion变量上等待。显然需要有另外的代码逻辑调用complete或complete_all让它结束等待,继续执行。我们接着看下去。

blk_execute_rq_nowait

函数blk_execute_rq_nowait()代码(摘自文件block/blk_exec.c)

void blk_execute_rq_nowait(struct request_queue *q, struct gendisk *bd_disk,struct request *rq, int at_head,rq_end_io_fn *done)
{rq->end_io = done;__elv_add_request(q, rq, where);__blk_run_queue(q);}

blk_execute_rq_nowait函数可以在将请求插入到I/O调度队列后,直接返回,这就是它的函数名的来历。

这个函数追加了一个参数,即请求完成回调函数,它被记录在请求的end_io域,提交请求的工作调用了__elv_add_request。在提交后,调用__generic_unplug_device,让请求队列的策略例程“处理”起来。

最终,这个请求会被处理,在后面的块I/O子系统的请求处理完成流程中,我们会看到,对于来自SCSI子系统的请求,记录在request描述符的end_io域的请求完成回调函数会被调用,这就是我们前面传入的blk_end_sync_rq函数

blk_end_sync_rq

函数blk_end_sync_rq()代码(摘自文件block/blk_exec.c)

static void blk_end_sync_rq(struct request *rq, int error)
{struct completion *waiting = rq->end_io_data;rq->end_io_data = NULL;complete(waiting);
}

blk_end_sync_rq函数处理请求的完成事件,我们现在知道,它唯一需要做的就是唤醒在完成等待上等待的线程。它从request的end_io_data域取得记录的completion变量,然后调用complete函数。

int blk_execute_rq(struct request_queue *q, struct gendisk *bd_disk,struct request *rq, int at_head)
{wait_for_completion_io(&wait);
}

也就是说,让blk_execute_rq函数在wait_for_completion_io(&wait);
}之后继续执行下去。这个函数判断请求处理是否出现错误。若没有错误,返回0;否则返回负的错误码。

int scsi_execute(struct scsi_device *sdev, const unsigned char *cmd,int data_direction, void *buffer, unsigned bufflen,unsigned char *sense, int timeout, int retries, u64 flags,int *resid)
{blk_execute_rq(req->q, NULL, req, 1);
}

blk_execute_rq当然返回到scsi_execute函数的blk_execute_rq(req->q, NULL, req, 1)。后者再进行一些琐碎的处理,最终返回并结束scsi_execute_req函数。至此,同步阻塞的SCSI命令执行流程已经跟踪完毕,针对调用者发起的SCSI命令而返回的响应数据已经被存放在调用者传入的缓冲区了。

这篇关于深入浅出SCSI子系统(七)SCSI命令执行的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/545604

相关文章

Linux使用dd命令来复制和转换数据的操作方法

《Linux使用dd命令来复制和转换数据的操作方法》Linux中的dd命令是一个功能强大的数据复制和转换实用程序,它以较低级别运行,通常用于创建可启动的USB驱动器、克隆磁盘和生成随机数据等任务,本文... 目录简介功能和能力语法常用选项示例用法基础用法创建可启动www.chinasem.cn的 USB 驱动

关于Maven生命周期相关命令演示

《关于Maven生命周期相关命令演示》Maven的生命周期分为Clean、Default和Site三个主要阶段,每个阶段包含多个关键步骤,如清理、编译、测试、打包等,通过执行相应的Maven命令,可以... 目录1. Maven 生命周期概述1.1 Clean Lifecycle1.2 Default Li

windows系统下shutdown重启关机命令超详细教程

《windows系统下shutdown重启关机命令超详细教程》shutdown命令是一个强大的工具,允许你通过命令行快速完成关机、重启或注销操作,本文将为你详细解析shutdown命令的使用方法,并提... 目录一、shutdown 命令简介二、shutdown 命令的基本用法三、远程关机与重启四、实际应用

Linux使用nohup命令在后台运行脚本

《Linux使用nohup命令在后台运行脚本》在Linux或类Unix系统中,后台运行脚本是一项非常实用的技能,尤其适用于需要长时间运行的任务或服务,本文我们来看看如何使用nohup命令在后台... 目录nohup 命令简介基本用法输出重定向& 符号的作用后台进程的特点注意事项实际应用场景长时间运行的任务服

Redis的Hash类型及相关命令小结

《Redis的Hash类型及相关命令小结》edisHash是一种数据结构,用于存储字段和值的映射关系,本文就来介绍一下Redis的Hash类型及相关命令小结,具有一定的参考价值,感兴趣的可以了解一下... 目录HSETHGETHEXISTSHDELHKEYSHVALSHGETALLHMGETHLENHSET

如何使用 Bash 脚本中的time命令来统计命令执行时间(中英双语)

《如何使用Bash脚本中的time命令来统计命令执行时间(中英双语)》本文介绍了如何在Bash脚本中使用`time`命令来测量命令执行时间,包括`real`、`user`和`sys`三个时间指标,... 使用 Bash 脚本中的 time 命令来统计命令执行时间在日常的开发和运维过程中,性能监控和优化是不

C#如何优雅地取消进程的执行之Cancellation详解

《C#如何优雅地取消进程的执行之Cancellation详解》本文介绍了.NET框架中的取消协作模型,包括CancellationToken的使用、取消请求的发送和接收、以及如何处理取消事件... 目录概述与取消线程相关的类型代码举例操作取消vs对象取消监听并响应取消请求轮询监听通过回调注册进行监听使用Wa

PHP执行php.exe -v命令报错的解决方案

《PHP执行php.exe-v命令报错的解决方案》:本文主要介绍PHP执行php.exe-v命令报错的解决方案,文中通过图文讲解的非常详细,对大家的学习或工作有一定的帮助,需要的朋友可以参考下... 目录执行phpandroid.exe -v命令报错解决方案执行php.exe -v命令报错-PHP War

Oracle数据库执行计划的查看与分析技巧

《Oracle数据库执行计划的查看与分析技巧》在Oracle数据库中,执行计划能够帮助我们深入了解SQL语句在数据库内部的执行细节,进而优化查询性能、提升系统效率,执行计划是Oracle数据库优化器为... 目录一、什么是执行计划二、查看执行计划的方法(一)使用 EXPLAIN PLAN 命令(二)通过 S

CentOS系统使用yum命令报错问题及解决

《CentOS系统使用yum命令报错问题及解决》文章主要讲述了在CentOS系统中使用yum命令时遇到的错误,并提供了个人解决方法,希望对大家有所帮助,并鼓励大家支持脚本之家... 目录Centos系统使用yum命令报错找到文件替换源文件为总结CentOS系统使用yum命令报错http://www.cppc