深入浅出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如何查看文件权限的命令

《Linux如何查看文件权限的命令》Linux中使用ls-R命令递归查看指定目录及子目录下所有文件和文件夹的权限信息,以列表形式展示权限位、所有者、组等详细内容... 目录linux China编程查看文件权限命令输出结果示例这里是查看tomcat文件夹总结Linux 查看文件权限命令ls -l 文件或文件夹

idea的终端(Terminal)cmd的命令换成linux的命令详解

《idea的终端(Terminal)cmd的命令换成linux的命令详解》本文介绍IDEA配置Git的步骤:安装Git、修改终端设置并重启IDEA,强调顺序,作为个人经验分享,希望提供参考并支持脚本之... 目录一编程、设置前二、前置条件三、android设置四、设置后总结一、php设置前二、前置条件

深入浅出SpringBoot WebSocket构建实时应用全面指南

《深入浅出SpringBootWebSocket构建实时应用全面指南》WebSocket是一种在单个TCP连接上进行全双工通信的协议,这篇文章主要为大家详细介绍了SpringBoot如何集成WebS... 目录前言为什么需要 WebSocketWebSocket 是什么Spring Boot 如何简化 We

解密SQL查询语句执行的过程

《解密SQL查询语句执行的过程》文章讲解了SQL语句的执行流程,涵盖解析、优化、执行三个核心阶段,并介绍执行计划查看方法EXPLAIN,同时提出性能优化技巧如合理使用索引、避免SELECT*、JOIN... 目录1. SQL语句的基本结构2. SQL语句的执行过程3. SQL语句的执行计划4. 常见的性能优

Linux系统之lvcreate命令使用解读

《Linux系统之lvcreate命令使用解读》lvcreate是LVM中创建逻辑卷的核心命令,支持线性、条带化、RAID、镜像、快照、瘦池和缓存池等多种类型,实现灵活存储资源管理,需注意空间分配、R... 目录lvcreate命令详解一、命令概述二、语法格式三、核心功能四、选项详解五、使用示例1. 创建逻

C语言进阶(预处理命令详解)

《C语言进阶(预处理命令详解)》文章讲解了宏定义规范、头文件包含方式及条件编译应用,强调带参宏需加括号避免计算错误,头文件应声明函数原型以便主函数调用,条件编译通过宏定义控制代码编译,适用于测试与模块... 目录1.宏定义1.1不带参宏1.2带参宏2.头文件的包含2.1头文件中的内容2.2工程结构3.条件编

Spring Bean初始化及@PostConstruc执行顺序示例详解

《SpringBean初始化及@PostConstruc执行顺序示例详解》本文给大家介绍SpringBean初始化及@PostConstruc执行顺序,本文通过实例代码给大家介绍的非常详细,对大家的... 目录1. Bean初始化执行顺序2. 成员变量初始化顺序2.1 普通Java类(非Spring环境)(

Spring Boot 中的默认异常处理机制及执行流程

《SpringBoot中的默认异常处理机制及执行流程》SpringBoot内置BasicErrorController,自动处理异常并生成HTML/JSON响应,支持自定义错误路径、配置及扩展,如... 目录Spring Boot 异常处理机制详解默认错误页面功能自动异常转换机制错误属性配置选项默认错误处理

如何在Java Spring实现异步执行(详细篇)

《如何在JavaSpring实现异步执行(详细篇)》Spring框架通过@Async、Executor等实现异步执行,提升系统性能与响应速度,支持自定义线程池管理并发,本文给大家介绍如何在Sprin... 目录前言1. 使用 @Async 实现异步执行1.1 启用异步执行支持1.2 创建异步方法1.3 调用

Spring Boot Maven 插件如何构建可执行 JAR 的核心配置

《SpringBootMaven插件如何构建可执行JAR的核心配置》SpringBoot核心Maven插件,用于生成可执行JAR/WAR,内置服务器简化部署,支持热部署、多环境配置及依赖管理... 目录前言一、插件的核心功能与目标1.1 插件的定位1.2 插件的 Goals(目标)1.3 插件定位1.4 核