深入浅出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

相关文章

Redis 的 SUBSCRIBE命令详解

《Redis的SUBSCRIBE命令详解》Redis的SUBSCRIBE命令用于订阅一个或多个频道,以便接收发送到这些频道的消息,本文给大家介绍Redis的SUBSCRIBE命令,感兴趣的朋友跟随... 目录基本语法工作原理示例消息格式相关命令python 示例Redis 的 SUBSCRIBE 命令用于订

防止Linux rm命令误操作的多场景防护方案与实践

《防止Linuxrm命令误操作的多场景防护方案与实践》在Linux系统中,rm命令是删除文件和目录的高效工具,但一旦误操作,如执行rm-rf/或rm-rf/*,极易导致系统数据灾难,本文针对不同场景... 目录引言理解 rm 命令及误操作风险rm 命令基础常见误操作案例防护方案使用 rm编程 别名及安全删除

Java使用jar命令配置服务器端口的完整指南

《Java使用jar命令配置服务器端口的完整指南》本文将详细介绍如何使用java-jar命令启动应用,并重点讲解如何配置服务器端口,同时提供一个实用的Web工具来简化这一过程,希望对大家有所帮助... 目录1. Java Jar文件简介1.1 什么是Jar文件1.2 创建可执行Jar文件2. 使用java

C++统计函数执行时间的最佳实践

《C++统计函数执行时间的最佳实践》在软件开发过程中,性能分析是优化程序的重要环节,了解函数的执行时间分布对于识别性能瓶颈至关重要,本文将分享一个C++函数执行时间统计工具,希望对大家有所帮助... 目录前言工具特性核心设计1. 数据结构设计2. 单例模式管理器3. RAII自动计时使用方法基本用法高级用法

Java实现远程执行Shell指令

《Java实现远程执行Shell指令》文章介绍使用JSch在SpringBoot项目中实现远程Shell操作,涵盖环境配置、依赖引入及工具类编写,详解分号和双与号执行多指令的区别... 目录软硬件环境说明编写执行Shell指令的工具类总结jsch(Java Secure Channel)是SSH2的一个纯J

深入浅出Spring中的@Autowired自动注入的工作原理及实践应用

《深入浅出Spring中的@Autowired自动注入的工作原理及实践应用》在Spring框架的学习旅程中,@Autowired无疑是一个高频出现却又让初学者头疼的注解,它看似简单,却蕴含着Sprin... 目录深入浅出Spring中的@Autowired:自动注入的奥秘什么是依赖注入?@Autowired

python 线程池顺序执行的方法实现

《python线程池顺序执行的方法实现》在Python中,线程池默认是并发执行任务的,但若需要实现任务的顺序执行,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋... 目录方案一:强制单线程(伪顺序执行)方案二:按提交顺序获取结果方案三:任务间依赖控制方案四:队列顺序消

Go语言连接MySQL数据库执行基本的增删改查

《Go语言连接MySQL数据库执行基本的增删改查》在后端开发中,MySQL是最常用的关系型数据库之一,本文主要为大家详细介绍了如何使用Go连接MySQL数据库并执行基本的增删改查吧... 目录Go语言连接mysql数据库准备工作安装 MySQL 驱动代码实现运行结果注意事项Go语言执行基本的增删改查准备工作

Linux查询服务器 IP 地址的命令详解

《Linux查询服务器IP地址的命令详解》在服务器管理和网络运维中,快速准确地获取服务器的IP地址是一项基本但至关重要的技能,下面我们来看看Linux中查询服务器IP的相关命令使用吧... 目录一、hostname 命令:简单高效的 IP 查询工具命令详解实际应用技巧注意事项二、ip 命令:新一代网络配置全

Linux grep 命令的使用指南

《Linuxgrep命令的使用指南》本文给大家介绍Linuxgrep命令的使用指南,包括基础搜索语法、实践指南,感兴趣的朋友跟随小编一起看看吧... 目录linux grep 命令全面使用指南一、基础搜索语法1. 基本文本搜索2. 多文件搜索二、常用选项详解1. 输出控制选项2. 上下文控制选项三、正则表达