深入浅出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(10) -- zset类型命令使用

zset是有序集合,内部除了存储元素外,还会存储一个score,存储在zset中的元素会按照score的大小升序排列,不同元素的score可以重复,score相同的元素会按照元素的字典序排列。 1. zset常用命令 1.1 zadd  zadd key [NX | XX] [GT | LT]   [CH] [INCR] score member [score member ...]

30常用 Maven 命令

Maven 是一个强大的项目管理和构建工具,它广泛用于 Java 项目的依赖管理、构建流程和插件集成。Maven 的命令行工具提供了大量的命令来帮助开发人员管理项目的生命周期、依赖和插件。以下是 常用 Maven 命令的使用场景及其详细解释。 1. mvn clean 使用场景:清理项目的生成目录,通常用于删除项目中自动生成的文件(如 target/ 目录)。共性规律:清理操作

maven 编译构建可以执行的jar包

💝💝💝欢迎莅临我的博客,很高兴能够在这里和您见面!希望您在这里可以感受到一份轻松愉快的氛围,不仅可以获得有趣的内容和知识,也可以畅所欲言、分享您的想法和见解。 推荐:「stormsha的主页」👈,「stormsha的知识库」👈持续学习,不断总结,共同进步,为了踏实,做好当下事儿~ 专栏导航 Python系列: Python面试题合集,剑指大厂Git系列: Git操作技巧GO

利用命令模式构建高效的手游后端架构

在现代手游开发中,后端架构的设计对于支持高并发、快速迭代和复杂游戏逻辑至关重要。命令模式作为一种行为设计模式,可以有效地解耦请求的发起者与接收者,提升系统的可维护性和扩展性。本文将深入探讨如何利用命令模式构建一个强大且灵活的手游后端架构。 1. 命令模式的概念与优势 命令模式通过将请求封装为对象,使得请求的发起者和接收者之间的耦合度降低。这种模式的主要优势包括: 解耦请求发起者与处理者

linux 判断某个命令是否安装

linux 判断某个命令是否安装 if ! [ -x "$(command -v git)" ]; thenecho 'Error: git is not installed.' >&2exit 1fi

jenkins 插件执行shell命令时,提示“Command not found”处理方法

首先提示找不到“Command not found,可能我们第一反应是查看目标机器是否已支持该命令,不过如果相信能找到这里来的朋友估计遇到的跟我一样,其实目标机器是没有问题的通过一些远程工具执行shell命令是可以执行。奇怪的就是通过jenkinsSSH插件无法执行,经一番折腾各种搜索发现是jenkins没有加载/etc/profile导致。 【解决办法】: 需要在jenkins调用shell脚

Lua 脚本在 Redis 中执行时的原子性以及与redis的事务的区别

在 Redis 中,Lua 脚本具有原子性是因为 Redis 保证在执行脚本时,脚本中的所有操作都会被当作一个不可分割的整体。具体来说,Redis 使用单线程的执行模型来处理命令,因此当 Lua 脚本在 Redis 中执行时,不会有其他命令打断脚本的执行过程。脚本中的所有操作都将连续执行,直到脚本执行完成后,Redis 才会继续处理其他客户端的请求。 Lua 脚本在 Redis 中原子性的原因

Linux命令(4):fg与bg命令

fg、bg、jobs、&、ctrl + z都是跟系统任务有关的,虽然现在基本上不怎么需要用到这些命令,但学会了也是很实用的 一.& 最经常被用到 这个用在一个命令的最后,可以把这个命令放到后台执行 二.ctrl + z 可以将一个正在前台执行的命令放到后台,并且暂停 三.jobs 查看当前有多少在后台运行的命令 四.fg 将后台中的命令调至前台继续运行 如果后台中有多个命令,可以

Linux命令(3):sz与rz命令

一般来说,linux服务器大多是通过ssh客户端来进行远程的登陆和管理的,使用ssh登陆linux主机以后,如何能够快速的和本地机器进行文件的交互呢,也就是上传和下载文件到服务器和本地; 与ssh有关的两个命令可以提供很方便的操作: sz:将选定的文件发送(send)到本地机器 rz:运行该命令会弹出一个文件选择窗口,从本地选择文件上传到服务器(receive) rz,sz是便是Linux

Smarty模板执行原理

为了实现程序的业务逻辑和内容表现页面的分离从而提高开发速度,php 引入了模板引擎的概念,php 模板引擎里面最流行的可以说是smarty了,smarty因其功能强大而且速度快而被广大php web开发者所认可。本文将记录一下smarty模板引擎的工作执行原理,算是加深一下理解。 其实所有的模板引擎的工作原理是差不多的,无非就是在php程序里面用正则匹配将模板里面的标签替换为php代码从而将两者