前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >Linux内核(5.10)-IO全路径-文件系统到磁盘-或远端iscsi/nvmeof协议盘

Linux内核(5.10)-IO全路径-文件系统到磁盘-或远端iscsi/nvmeof协议盘

原创
作者头像
晓兵
修改2023-12-07 20:29:39
7980
修改2023-12-07 20:29:39
举报
文章被收录于专栏:daosdaos

术语/概念

DAX: 磁盘(disk)的访问模式有三种 BUFFERED、DIRECT、DAX。前面提到的由于page cache存在可以避免耗时的磁盘通信就是BUFFERED访问模式的集中体现;但是如果我要求用户的write请求要实时存储到磁盘里,不能只在内存中更新,那么此时我便需要DIRECT模式;大家可能听说过flash分为两种nand flash和nor flash,nor flash可以像ram一样直接通过地址线和数据线访问,不需要整块整块的刷,对于这种场景我们采用DAX模式。所以file_operations的read_iter和write_iter回调函数首先就需要根据不同的标志判断采用哪种访问模式, kernel在2020年12月的patch中提出了folio的概念,我们可以把folio简单理解为一段连续内存,一个或多个page的集合

IO路径简图

同步/异步

APP调用系统调用write(fd,"pilgrimtao is cool",18)

代码语言:javascript
复制
int main()
{
       char buff[128] = {0};
       int fd = open("/var/pilgrimtao.txt", O_CREAT|O_RDWR);

       write(fd, "pilgrimtao is cool", 18);
       pread(fd, buff, 128, 0);
       printf("%s\n", buff);

       close(fd);
       return 0;
}

虚拟文件系统层的写调用栈(VFS), 以XFS举例

代码语言:javascript
复制
iopath,
write(fd, "pilgrimtao is cool", 18)
    ssize_t ksys_write(unsigned int fd, const char __user *buf, size_t count)
        vfs_write(f.file, buf, count, ppos)
            if (file->f_op->write) -> 首选普通写
                ret = file->f_op->write(file, buf, count, pos)
            else if (file->f_op->write_iter) -> 如果没有实现普通写,但是实现了迭代写, 则调用同步写
                ret = new_sync_write(file, buf, count, pos)
                    call_write_iter(filp, &kiocb, &iter)
                        return file->f_op->write_iter(kio, iter) -> fs.h, -> xfs_file_write_iter
                            if (IS_DAX(inode)) -> 如果是直接访问(Direct Access,DAX)
                                return xfs_file_dax_write(iocb, from) -> ...
                            if (iocb->ki_flags & IOCB_DIRECT) -> 如果在写的逻辑__generic_file_write_iter里面,发现设置了IOCB_DIRECT,则调用generic_file_direct_write,里面同样会调用address_space的direct_IO的函数,将数据直接写入硬盘: https://www.cnblogs.com/luozhiyun/p/13061199.html
                                ret = xfs_file_dio_write(iocb, from)
                            return xfs_file_buffered_write(iocb, from) -> 默认带缓存的写
                                iomap_file_buffered_write -> iomap_iter -> .iomap_begin -> xfs_buffered_write_iomap_begin

文件系统层 -> 块层 -> 协议层(iscsi/远端盘), 关键函数queue_rq有不同的实现

SCSi实现: .queue_rq = scsi_queue_rq 下面以它作为举例

ceph实现, 内核驱动接管块层IO: .queue_rq = rbd_queue_rq -> static blk_status_t rbd_queue_rq

Nvme(本地盘)实现: .queue_rq = nvme_queue_rq

Nvmeof(host端)实现: .queue_rq = nvme_rdma_queue_rq

DeviceMapper实现: .queue_rq = dm_mq_queue_rq,

...

参考图:

scsi_queue_rq 实现

代码语言:javascript
复制
xfs_iread_extents -> xfs_btree_visit_blocks -> xfs_btree_readahead_ptr -> xfs_buf_readahead 预读 -> xfs_buf_readahead_map -> xfs_buf_read_map -> _xfs_buf_read -> xfs_buf_submit -> __xfs_buf_submit -> xfs_buf_ioapply_map -> 由VFS -> 进入块层 -> submit_bio(bio) -> submit_bio_noacct -> submit_bio_noacct_nocheck -> __submit_bio_noacct_mq / __submit_bio_noacct -> __submit_bio -> blk_mq_submit_bio -> blk_add_rq_to_plug -> bio合并: blk_mq_submit_bio -> blk_mq_get_new_requests -> blk_mq_sched_bio_merge -> blk_bio_list_merge -> blk_attempt_bio_merge
request插入ctx:blk_mq_submit_bio -> blk_mq_sched_insert_request (blk_mq_insert_request) -> __blk_mq_insert_request -> __blk_mq_insert_req_list -> list_add(&rq->queuelist, &ctx->rq_lists[type])
取出request: blk_mq_run_hw_queue -> __ blk_mq_delay_run_hw_queue -> __ blk_mq_run_hw_queue -> blk_mq_sched_dispatch_requests -> __blk_mq_sched_dispatch_requests -> blk_mq_do_dispatch_ctx -> blk_mq_dequeue_from_ctx -> dispatch_rq_from_ctx -> __blk_mq_sched_dispatch_requests -> blk_mq_flush_busy_ctxs (取出)/ blk_mq_dispatch_rq_list (发送给磁盘)

__submit_bio_noacct_mq -> 我们一次只希望一个 ->submit_bio 处于活动状态,否则堆栈设备的堆栈使用可能会出现问题。 使用 current->bio_list 收集 ->submit_bio 方法处于活动状态时提交的请求列表,然后在返回后处理它们

IO路径, 块io, iscsi层, iopath, 
bool blk_mq_dispatch_rq_list
		ret = q->mq_ops->queue_rq(hctx, &bd) # 关键函数 queue_rq, IO请求入队列
		.queue_rq	= scsi_queue_rq
		static blk_status_t scsi_queue_rq( -> scsi处理流程: https://blog.csdn.net/marlos/article/details/131171560, 这个函数之后大致要完成的工作是,把队列中的request再转化为对硬件的command,接着下发command到硬件,完成io。也就是说,对于request的解析,一定是在command生成之前的。在上面代码的35行之前,是在做一些必要的检查,确保队列、硬件处于正常工作的状态,接着37行,出现一个关键的函数 scsi_prepare_cmd, 顾名思义,command可能会在这个函数中进行初始化
			struct scsi_cmnd *cmd = blk_mq_rq_to_pdu(req) -> cmd已经填充了?
			WARN_ON_ONCE(cmd->budget_token < 0) -> 预算令牌, scsi:blk-mq:从 .get_budget 回调中返回预算令牌 SCSI 使用全局原子变量来跟踪每个 LUN/请求队列的队列深度,当有很多 CPU 核心并且磁盘非常快时,这不能很好地扩展。 通过在 I/O 路径中的 sdev->device_busy 跟踪队列深度,观察到 IOPS 受到很大影响,从 .get_budget 回调中返回预算令牌。 预算令牌可以传递给驱动程序,这样我们就可以用 sbitmap_queue 替换原子变量,并以这种方式缓解缩放问题, 链接:https://lore.kernel.org/r/20210122023317.687987-9-ming.lei@redhat.com
			ret = BLK_STS_RESOURCE -> 块:引入新的块状态代码类型目前我们在块层中使用标准的 Linux errno 值,虽然我们接受任何错误,但一些错误具有超载的魔法含义。 这个补丁引入了一个新的 blk_status_t 值,它包含块层特定的状态代码并明确解释它们的含义。 现在提供了与以前的特殊含义相互转换的助手,但我怀疑我们希望从长远来看摆脱它们——那些有错误输入(例如网络)的驱动程序通常会得到不知道特殊块层的错误 重载,并类似地将它们返回到用户空间通常会返回一些严格来说对于文件系统操作不正确的东西,但这留作以后的练习。目前错误集是一个非常有限的集合,与之前重载的 errno 值密切相关 , 但有一些低挂果来改进它。blk_status_t (ab) 使用稀疏的 __bitwise 注释来允许稀疏类型检查,这样我们就可以很容易地捕捉到传递错误值的地方
			scsi_prepare_cmd -> static blk_status_t scsi_prepare_cmd(struct request *req)
				struct scsi_cmnd
				cmd->prot_op = SCSI_PROT_NORMAL 命令保护操作
				return scsi_cmd_to_driver(cmd)->init_command(cmd) -> .init_command		= sd_init_command -> scsi_init_command -> static blk_status_t sd_init_command -> scsi层里面,高级驱动可不止sd一个,因此,我们可以猜测这个函数只是在做一些通用性的命令初始化,对于特异性的初始化,一定会转交sd驱动处理,所以直接看代码的66行,调用了对应cmd绑定驱动的init_command函数
					case REQ_OP_WRITE
					return sd_setup_read_write_cmnd(cmd)
						bool write = rq_data_dir(rq) == WRITE
						scsi_alloc_sgtables
						dix = scsi_prot_sg_count(cmd) -> 数据保护
						if (protect && sdkp->protection_type == T10_PI_TYPE2_PROTECTION) -> T10保护信息(T10 Protection Information (PI))
						sd_setup_rw10_cmnd(cmd, write, lba, nr_blocks -> static blk_status_t sd_setup_rw10_cmnd -> 打印日志: SCSI_LOG_  -> SCSI_LOG_HLQUEUE -> [66521.609478] sd 6:0:0:0: [sda] tag#23 sd_setup_read_write_cmnd: block=893164736, count=8
							cmd->cmd_len = 10

			static int scsi_dispatch_cmd(struct scsi_cmnd *cmd)
				trace_scsi_dispatch_cmd_start(cmd)
				rtn = host->hostt->queuecommand(host, cmd) -> .queuecommand           = iscsi_queuecommand, -> int iscsi_queuecommand
					iscsi_session_chkready -> 检查会话通过iscsi_session_chkready进行。当会话状态不是ISCSI_SESSION_LOGGED_IN时,不适合处理scsi指令。链接检查通过链接是否存在、链接状态、链接可接收的命令窗口是否达到最大值。这几个方面判断
					task = iscsi_alloc_task(conn, sc)
					iscsi_prep_scsi_cmd_pdu(task)
						ISCSI_DBG_SESSION
					list_add_tail(&task->running, &conn->cmdqueue) -> 将任务插入命令队列 cmdqueue -> 由 iscsi_xmitworker 线程发送命令
					iscsi_conn_queue_xmit(conn)


static void iscsi_xmitworker(struct work_struct *work)
	do iscsi_data_xmit(conn)
		iscsi_xmit_task
			rc = conn->session->tt->xmit_task(task) -> .xmit_task		= iscsi_tcp_task_xmit 发送常规PDU任务
				rc = session->tt->xmit_pdu(task) -> static int iscsi_sw_tcp_pdu_xmit
					iscsi_sw_tcp_xmit
						while (1) iscsi_sw_tcp_xmit_segment(tcp_conn, segment) 传输分段
							tcp_sw_conn->sendpage(sk, sg_page(sg), offset
						segment->done(tcp_conn, segment) 首选按页发送
						kernel_sendmsg(sk, &msg, &iov, 1, copy) 其次降级为内核发送消息
							iov_iter_kvec
							sock_sendmsg(sock, msg)
					memalloc_noreclaim_restore
				iscsi_tcp_get_curr_r2t
				conn->session->tt->alloc_pdu
				iscsi_prep_data_out_pdu -> 初始化 Data-Out
					hdr->ttt = r2t->ttt
					hdr->opcode = ISCSI_OP_SCSI_DATA_OUT
				rc = conn->session->tt->init_pdu
			iscsi_put_task(task)

nvmeof的host端落盘实现(nvme_rdma_queue_rq)

代码语言:javascript
复制
nvme落盘io流程, iopath
static const struct blk_mq_ops nvme_rdma_mq_ops = {
	.queue_rq	= nvme_rdma_queue_rq,
	.complete	= nvme_rdma_complete_rq,
	.init_request	= nvme_rdma_init_request,
	.exit_request	= nvme_rdma_exit_request,
	.init_hctx	= nvme_rdma_init_hctx,
	.timeout	= nvme_rdma_timeout,
	.map_queues	= nvme_rdma_map_queues,
	.poll		= nvme_rdma_poll,
};
static blk_status_t nvme_rdma_queue_rq
	nvme_check_ready -> 对于我们无法发送到设备的状态,默认操作是使其忙碌并在控制器状态恢复后重试。 但是,如果控制器正在删除,或者任何内容被标记为快速故障或 nvme 多路径,则会立即失败。 注意:用于初始化控制器的命令将被标记为快速故障。 注意:nvme cli/ioctl 命令被标记为故障快速
	req->sqe.dma = ib_dma_map_single(dev, req->sqe.data
	ib_dma_mapping_error
	ib_dma_sync_single_for_cpu
	nvme_setup_cmd
	nvme_start_request(rq)
	nvme_rdma_map_data
	ib_dma_sync_single_for_device
	nvme_rdma_post_send <- drivers/nvme/host/rdma.c
		wr.opcode     = IB_WR_SEND -> 发送端产生发送完成事件: IBV_WC_SEND, 接收端产生接收完成事件: IBV_WC_RECV, 在spdk_tgt端产生 RDMA_WR_TYPE_SEND 的类型 -> rdma_wr = (struct spdk_nvmf_rdma_wr *)wc[i].wr_id -> IBV_WC_RECV
		ib_post_send -> nvme_host驱动提交工作请求WR到队列, cpu敲门铃, DMA硬件拷贝和传输数据到tgt端, host生成发送完成事件(...), tgt端生成接收完成事件(...)

设备映射DeviceMap实现(dm_submit_bio)

代码语言:javascript
复制
io_path, io路径
static const struct block_device_operations dm_blk_dops = {
	.submit_bio = dm_submit_bio,  -> static void dm_submit_bio

映射设备区分两种类型:基于通用块层请求的映射设备和基于块设备驱动层请求的映射设备, dm_request_based 函数判断映射设备是否是基于块设备驱动层请求的映射设备,如果是,则返回1;否则返回0。这种判断是根据映射设备的请求队列中是否设置了QUEUE_FLAG_STACKABLE标志作出的,而这个标志在创建映射设备时根据映射表的类型设定
static void dm_submit_bio
	dm_get_live_table_bio
	DMF_BLOCK_IO_FOR_SUSPEND 设备挂起时,io入队
	queue_io(md, bio)
		queue_work(md->wq, &md->work)
	dm_split_and_process_bio(md, map, bio) -> 分割io提交给目标设备

dm_split_and_process_bio
	__split_and_process_bio 选择正确的策略来处理非flush bio
		dm_table_find_target
		__map_bio -> static void __map_bio -> 映射io
			ti->type->map(ti, clone) -> .map = multipath_map_bio -> static int __multipath_map_bio -> bio_set_dev(bio, pgpath->path.dev->bdev) -> pgpath->pg->ps.type->start_io -> 查看DeviceMapper映射表 dmsetup table -> mpatha: 0 209715200 multipath 0 0 1 1 service-time 0 1 2 8:16 1 1 -> 根据路径的吞吐量以及未完成的字节数选择负荷较轻的路径 -> static struct path_selector_type st_ps
	bio_trim
	trace_block_split
	bio_inc_remaining
	submit_bio_noacct -> void submit_bio_noacct(struct bio *bio) ->  为 I/O 重新提交 bio 到块设备层 bio 描述内存和设备上的位置。 这是 submit_bio() 的一个版本,只能用于通过堆叠块驱动程序重新提交给较低级别驱动程序的 I/O。 块层的所有文件系统和其他上层用户应该使用 submit_bio() 代替, bio 在节流之前已经被检查过,所以在从节流队列中调度它之前不需要再次检查它。 为此目的添加 submit_bio_noacct_nocheck() 的助手
		bio_list_add -> kernel会将同一进程的bio统一放到current->bio_list暂时存储,submit bio时从bio_list中一个一个取出进行submit
		__submit_bio_noacct_mq
		submit_bio_noacct_nocheck -> __submit_bio_noacct(bio) -> 确实可以通过递归调用 submit_bio_noacct 添加更多的 bios
			__submit_bio(bio)
				blk_mq_submit_bio -> void blk_mq_submit_bio -> 向块设备提交bio, 会执行调度,合并等操作
					blk_mq_bio_to_request
					blk_queue_bounce -> 弹跳
					__bio_split_to_limits
					blk_mq_bio_to_request(rq, bio, nr_segs) -> request
					blk_mq_insert_request(rq, 0)
						blk_mq_request_bypass_insert
						list_add(&rq->queuelist, &ctx->rq_lists[hctx->type])
						q->elevator->type->ops.insert_requests(hctx, &list, flags)
					blk_mq_run_hw_queue(hctx, true) -> void blk_mq_run_hw_queue
					blk_mq_try_issue_directly
						blk_mq_run_hw_queue
							blk_mq_delay_run_hw_queue -> 异步延迟执行, 内核支持同步和异步两种方式发送request,在hctx中维护了一个delayed_work,用于异步方式往disk发送request,避免进程由于磁盘性能问题阻塞
								kblockd_mod_delayed_work_on(blk_mq_hctx_next_cpu(hctx), &hctx->run_work -> blk_mq_run_work_fn -> static void blk_mq_run_work_fn
								...
						blk_mq_get_budget_and_tag
						__blk_mq_issue_directly
							ret = q->mq_ops->queue_rq(hctx, &bd)
				disk->fops->submit_bio(bio) -> static void dm_submit_bio -> device_mapper io


static void blk_mq_run_work_fn
	blk_mq_run_dispatch_ops(hctx->queue, blk_mq_sched_dispatch_requests(hctx)) -> 返回 -EAGAIN 表明 hctx->dispatch 不为空,我们必须再次运行以避免刷新不足
		if (__blk_mq_sched_dispatch_requests(hctx) == -EAGAIN) -> 如果调度列表中没有剩余请求,则仅向调度程序询问请求。 这是为了避免由于设备队列深度较低而导致我们只调度一部分可用请求的情况。 一旦我们从 IOscheduler 中取出请求,我们就无法再对它们进行合并或排序。 因此,最好尽可能长时间地将它们留在那里。 将硬件队列标记为在这种情况下需要重新启动。如果调度列表上没有任何内容或者我们能够从调度列表中调度,我们希望从调度程序进行调度
			blk_mq_flush_busy_ctxs(hctx, &rq_list)
			blk_mq_dispatch_rq_list(hctx, &rq_list, 0) -> 发送给磁盘 -> stap -l 'kernel.function("blk_mq_dispatch_rq_list")'
				ret = q->mq_ops->queue_rq(hctx, &bd) -> .queue_rq

三条链:current->bio_list存储在当前线程的所有bio; plug->mq_list使能plug/unplug机制时存放在缓存池的bio;若定义IO调度层,IO请求会发送到scheduler list中;若没有定义IO调度层,IO请求会发送到ctx->rq_lists
每个线程若已经在执行blk_mq_submit_bio(),将新下发BIO链入到线程current->bio_list;
依次处理current->list中的每个bio;
若bio中存在数据在高端内存区,在低端内存区分配内存,将高端内存区数据拷贝到新分配的内存区,称为bounce过程,后面单独一节介绍;
检查请求队列中的bio,若过大进行切分,称BIO的切分;
尝试将bio合并到plug->mq_list中,然后尝试合并到IO调度层链表或ctx->rq_lists中;
若没有合并,分配新的request;
若定义plug,且没达到冲刷数目,加入到plug->mq_list;若达到冲刷数目,将冲刷下发(plug/unplug机制);
若定义IO调度器,往IO调度器中插入新的request(对于机械硬盘,通过IO调度层座合并和排序,有利于提高性能);
若 没有定义IO调度器,可以直接下发(对于较快的硬盘如nvme盘,进入调度层可能会浪费时间,跳过IO调度层有利于性能提升)
(1)bounce过程
(2)bio的切分和合并
(3)IO请求和tag的分配
(4)plug/unplug机制
(5)IO调度器
(4)其他								

参考

Linux内核笔记: https://github.com/ssbandjl/linux/blob/v5.10/readme_linux_with_git_log

IO路径-文件系统-系统调用, iopath, IO子系统全流程介绍: https://zhuanlan.zhihu.com/p/545906763

linux内核block层Multi queue多队列核心点分析: https://blog.csdn.net/hu1610552336/article/details/111464548

深入理解 Linux 内核---访问文件: https://blog.csdn.net/u012319493/article/details/85331567

https://blog.csdn.net/weixin_40535588/article/details/120040142

晓兵(ssbandjl)

博客: https://cloud.tencent.com/developer/user/5060293/articles | https://logread.cn | https://blog.csdn.net/ssbandjl

欢迎对高性能分布式存储PureFlash, SPDK, RDMA, 等高性能技术感兴趣的朋友加入PureFlash技术交流(群)

晓兵技术杂谈(系列)

https://cloud.tencent.com/developer/user/5060293/video

欢迎对DAOS, SPDK, RDMA等高性能技术感兴趣的朋友加我WX(ssbandjl)进入DAOS技术交流(群)

DAOS汇总: https://cloud.tencent.com/developer/article/2344030

公众号: 云原生云

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 术语/概念
  • IO路径简图
    • 同步/异步
    • APP调用系统调用write(fd,"pilgrimtao is cool",18)
    • 虚拟文件系统层的写调用栈(VFS), 以XFS举例
    • 文件系统层 -> 块层 -> 协议层(iscsi/远端盘), 关键函数queue_rq有不同的实现
      • scsi_queue_rq 实现
        • nvmeof的host端落盘实现(nvme_rdma_queue_rq)
          • 设备映射DeviceMap实现(dm_submit_bio)
          • 参考
          • 晓兵(ssbandjl)
            • 晓兵技术杂谈(系列)
            相关产品与服务
            对象存储
            对象存储(Cloud Object Storage,COS)是由腾讯云推出的无目录层次结构、无数据格式限制,可容纳海量数据且支持 HTTP/HTTPS 协议访问的分布式存储服务。腾讯云 COS 的存储桶空间无容量上限,无需分区管理,适用于 CDN 数据分发、数据万象处理或大数据计算与分析的数据湖等多种场景。
            领券
            问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档