本文主要是介绍深入浅出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命令执行的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!