SPDK学习笔记----理解spdk的工作原理

引言

前段时间,我将<<DPDK和SPDK开源社区>>公众号中收录的SPDK相关的文章学习了一遍,最近结合最新的代码v23.0.x,对example目录下的示例程序hello_word/hello_blob/hello_bdev,以及app目录下的应用程序vhost/nvmef的源码进行了走读分析:和大家熟悉的存储服务程序一样,可以将一个spdk程序分成管理面和数据面,管理面负责程序运行环境的初始化,各子系统及模块的初始化,rpc动态管理,数据面根据管理面中设置好的运行环境和数据结构完成具体的I/O操作。我将以vhost-blk为例,从管理面的初始化,数据面的I/O操作两个层面介绍spdk的实现原理。

注:本文不适合初学者,您至少应该将公众号<<DPDK和SPDK开源社区>>中的文章阅读一遍,了解spdk的基本概念,实现思想,机制,最好在环境上运行,调试下example下面的示例程序,同时对qemu设备虚拟化(virtio)有所了解

virtio

在进入正文前,我们一起来了解下virtio,virtio是当前虚拟化应用中的主要IO(半)虚拟化技术,包括前端驱动(front-end),传输队列(virt-queue)和后端设备(back-end)三部分,前端驱动运行在Guest虚拟机或者bare-metal裸金属应用中,后端设备位于Host主机的Hypervisor虚拟化进程或者独立的进程中,传输队列主要通过共享内存在前端驱动和后端设备间传递数据。后端设备的实现一直在变化,从完全在qemu进程中实现,到vhost, 再到vdpa。传统virtio及vhost-user的实现原理图如下(IO路径如黑色虚线,控制路径如绿色虚线):
传统virtio和vhost-user
从上图我们可以看到:传统virtio和vhost-user在guest侧的处理都是一样的, IO请求从用户程序下发,经过内核IO栈到达virtio驱动,然后通过virtqueue将请求传递给后端设备处理,后端设备处理完成后以中断(irqfd)方式通知虚拟机。

上图左侧的传统virtio实现中,后端设备完全基于Qemu实现,(IO请求添加到virtqueue后,虚拟机会陷入内核,通过KVM通知后端设备有IO达到),qemu进程中的io线程通过virtio-backend从virtqueue中取出请求,将请求通过系统调用传递给内核IO栈,NVMe控制器完成请求后通过物理中断通知qemu的io线程,qemu的io线程通过virtio-backend将响应放入virtqueue并通过虚拟中断通知虚拟机。

上图右侧是vhost-user的实现,控制面仍需qemu参与完成,而virtqueue的数据面从qemu中剥离由vhost程序实现,vhost程序以polling的方式从virtqueue中取出请求,然后将请求交给用户态的NVMe驱动处理,vhost程序以polling方式获取NVMe设备的请求响应,将响应请求添加到virtqueue并通过虚拟中断通知虚拟机。

vhost-blk控制面

配置编译好spdk后,app目录下会生成一个vhost文件,这就是vhost-user服务的可执行文件,如下是本文使用的一个vhost服务示例:

//-S参数指定socket路径, -c指定了json配置文件(内容如下),运行在core1
# sdpk/app/vhost -S /var/tmp -c vhost.json -m 0x1

# cat vhost.json
{
	"subsystems": [
	{
		"subsystem": "bdev",      //创建一个内存设备作为vhost-blk的磁盘
      	"config": [
       	{
          	"method": "bdev_malloc_create",
          	"params": {
            	"name": "Malloc0",
           		"num_blocks": 32768,
            	"block_size": 512
          	}
        }
      	]
    },
    {
		“subsystem”: 	"vhost-blk",  //创建vhost-blk设备,名称为VhostBlk.0,磁盘为内存设备Malloc0
		"config":[
		{
			"method": "vhost_create_blk_controller",
			"params":{
			    "ctrlr":"VhostBlk.0"
				"dev_name":"Malloc0",
				"transport":"vhost_user_blk"
			}
		}
		]
    }
    ]
}

//guest挂载vhost-blk设备的参数,path指向vhost-blk设备的unix socket监听路径
 -chardev socket,id=spdk_vhost_blk0,path=/var/tmp/VhostBlk.0 -device vhost-user-blk-pci,chardev=spdk_vhost_blk0

下图显示了Guest与vhost服务建立连接后的示意图(左侧是控制路径,右侧是io路径),后文逐层拆解控制面结构关系的建立过程及IO过程:
IO栈
执行vhost命令后,会启动一个常驻服务,该服务会依次进行spdk框架及自定义部分的初始化,spdk框架初始化包括:dpdk环境初始化,reactor初始化,bdev子系统初始化,vhost子系统初始化,启动unix socket的监听,等待客户端的链接;vhost的自定义初始化内容由上述的json文件指定。spdk框架初始化的核心调用如下,后文对各阶段的初始化进行简单介绍,逐步厘清各层及数据结构关系的建立:

各spdk应用程序的spdk框架初始化过程基本相同,可以套用下面的过程
-----------------------------------------------
\spdk_app_start
    \app_setup_env //dpdk环境初始化
       \rte_eal_init
          \rte_eal_memzone_init
          \rte_eal_memory_init
          \rte_eal_malloc_heap_init
          \rte_bus_probe
       \spdk_env_dpdk_post_init
    \spdk_reactors_init    //reactor初始化:创建全局的event mempool和msg mempool,初始化reactor数组并给每个reactor创建event ring
       \spdk_mempool_create
       \spdk_ring_create
    \spdk_thread_create    //创建app thread(初始化spdk_thread对象, 创建msg ring),添加到reactor的threads列表(先创建event添加到reactor的event ring,reactor启动后,reactor_run会先执行event ring中的event,然后将app thread添加到reactor的threads列表)
       \spdk_ring_create
    \spdk_reactor_start    //通过rte_eal_remote_launch启动各reactor,线程函数为reactor_run
----------------------------------------------
\bootstrap_fn   //bdev/vhost初始化:boostrap_fn在reactor启动前,通过msg添加到app thread的msg ring,reactor启动后轮询thread的msg ring执行,bdev子系统初始化,vhost子系统初始化
   \spdk_subsystem_init_from_json_config
   \app_start_rpc
-----------------------------------------------

reactor初始化

下图展示了初始化过程中建立的reactor和spdk_thread间的示意关系:
reactor与spdk_thread关系图
spdk app程序启动时,根据设置的参数会在每个指定的运行核core上创建reactor实例,通过全局链表g_reactors来管理所有的实例。调用spdk_thread_create方法创建spdk_thread实例(如:“app_thread”),并初始化( 每个spdk_thread实例,包含一个spdk_lw_thread扩展,并设置由g_thread_id维护的递增id), 然后调用线程方法g_thread_op_fn(实例为:reactor_thread_op)将spdk_thread实例挂接到reactor的threads列表上,通过全局链表g_threads来管理所有的线程实例。

注:g_thread_op_fn从全局的event mempool创建一个event实例并初始化,event的方法设置为_schedule_thread,参数设置为spdk_thread的扩展对象spdk_lw_thread,接着将event添加到“下一个”reactor的event ring中(在没有指定spdk_thread的core id的情况下,会选择round-bin的下一个reactor)。

在spdk app启动的最后调用spdk_reactors_start启动reactor,执行函数为reactor_run, 首先枚举event ring,逐个执行每个事件函数 ---- _schedule_thread函数将spdk_thread的扩展对象spdk_lw_thread添加到reactor的threads列表),然后轮询threads列表 ---- 对于每个thread,首先枚举执行msg ring中的消息,接着轮询active_pollers列表,最后轮询timed_pollers红黑树。这样整个polling机制就运行起来了。

注:app_setup_env 在进行环境初始化时,会将服务主线程绑定到main_lcore,在其他的运行核core上创建pthread工作线程,reactor(执行函数reactor_run)运行在各自运行核core的pthread上

bdev子系统

spdk app程序启动时,通过SPDK_SUBSYSTEM_REGISTER注册各子系统到SPDK框架(通过g_subsystems列表管理所有的子系统),通过SPDK_SUBSYSTEM_DEPEND注册子系统的依赖(通过g_subsystems_deps列表管理所有的子系统依赖),当前有10多个子系统,它们的依赖关系如下:
子系统依赖关系
通过SPDK_BDEV_MODULE_REGISTER注册bdev模块(通过g_bdev_mgr.bdev_modules列表管理所有的bdev模块),bdev module提供不同模块类型的具体实现,当前有20+种,常用的有nvme,virtio_scsi,virto_blk等。

在spdk app启动的后期(启动reactor前),通过spdk_thread_send_msg向“app_thread”线程的msg ring添加一个消息(消息的执行函数是bootstrap_fn),在reactor启动后,reactor开始轮询threads列表(reactor会优先处理event ring中的event,将“app_thread”添加到reactor的threads列表),对于每个thread,先枚举执行msg ring中的消息,然后轮询active_pollers列表,最后是timed_pollers红黑树。

消息的bootstrap_fn方法被调用执行rpc的初始化,以及子系统和bdev module的初始化 ---- 过程中调用spdk_io_device_register注册设备对象io_device,指定创建和销毁io_channel的回调函数(spdk_io_channel_create_cb/spdk_io_channel_destroy_cb), 通过全局红黑树g_io_devies来管理所有的io_device实例。io_channel是per-cpu/core的结构,是运行核操作设备的抽象,在首次调用spdk_get_io_channel时,会创建对应设备的io_channel结构并添加到对应spdk_thread的io_channels红黑树中,io_channel包含一个扩展结构可以指向任何数据,通常指向下一层子系统的channel结构,这样不同层次的设备抽象就关联起来了。下图显示了io_device,io_channel与spdk_thread的示意关系:

io_devices,io_channel,spdk_thread关系图
完成各子系统和bdev module的初始化后,初始化例程根据json文件进行app相关的初始化,本文中包括:Malloc0内存磁盘的初始化以及vhost-blk设备的初始化

Malloc0磁盘初始化

bdev子系统在层次上类似了Linux的通用块层,起着承上启下的作用,为上层的存储服务和存储协议提供统一的接口抽象,对下封装了各种设备实现,如:nvme, virtio-blk, virtio-scsi, malloc等。本文中创建Malloc0磁盘的json配置段如下:

"subsystem": "bdev",      //创建一个内存设备作为vhost-blk的磁盘
 "config": [
   {
          	"method": "bdev_malloc_create",
          	"params": {
            	"name": "Malloc0",
           		"num_blocks": 32768,
            	"block_size": 512
          	}
   }
   ]

rpc框架根据method名找到初始化方法(rpc_bdev_malloc_create),创建名为Malloc0,大小为32KB,扇区大小512B的内存磁盘, 过程中会调用spdk_io_device_register注册名为bdev_Malloc0的io_device并添加到g_io_devices列表,将磁盘和io_device关联起来,同时代表Malloc0磁盘的spdk_bdev对象被添加到g_bdev_mgr.bdev_names的名字红黑树,g_bdev_mgr.bdevs设备列表,最后将磁盘malloc_disk添加到g_malloc_disks全局列表。Malloc0磁盘与io_device及bdev的关系如下:

注:
g_bdev_mgr是bdev子系统初始化时实例化的bdev管理器,用来管理bdev子系统下的所有bdev实例,通过bdev_mgr名注册到io device全局列表
g_malloc_disks是malloc模块初始化时实例化的malloc设备列表,用来管理malloc内存磁盘,通过bdev_malloc名注册到io device全局列表
io_device和malloc

vhost-blk初始化

vhost子系统(vhost-blk和vhost-scsi)属于spdk的存储协议层,依赖于底层的bdev子系统,依赖关系参考上文。vhost的初始化属于spdk框架初始化的一部分,包括:vhost子系统的初始化以及vhost设备的初始化。

spdk app程序启动时,通过SPDK_VIRTIO_BLK_TRANSPORT_REGISTER注册transport ops,通过全局列表g_spdk_virtio_blk_transport_ops管理transport ops,vhost-blk子系统初始化时,创建名为"vhost_user_blk"的transport(从g_spdk_virtio_blk_transport_ops全局列表找到名为vhost_user_blk的transport ops,创建并初始化transport),初始化后添加到g_virtio_blk_transports的全局列表。

vhost-blk设备初始化使用的json配置段如下:

 {
		“subsystem”: 	"vhost-blk",  //创建vhost-blk设备,名称为VhostBlk.0,磁盘为Malloc0
		"config":[
		{
			"method": "vhost_create_blk_controller",
			"params":{
			    "ctrlr":"VhostBlk.0"
				"dev_name":"Malloc0",
				"transport":"vhost_user_blk"
			}
		}
		]
 }

rpc框架根据method名找到初始化方法(rpc_vhost_create_blk_controller)开始vhost-blk的初始化,主要工作是:创建spdk_vhost_blk_dev实例,关联spdk_bdev设备(Malloc0磁盘),关联transport ops,调用ops中的create_ctrlr方法(vhost_user_blk_create_ctrlr)创建/初始化控制器 ---- 过程中会设置vhost后端操作集合(vhost_blk_device_backend和vhost_blk_user_device_backend),创建监听socket(设置通知回调g_spdk_vhost_ops),最后将设备添加到全局g_vhost_devices列表, 这样vhost监听就准备好了,等待客户端的连接。vhost-blk初始化核心调用如下:

rpc_vhost_create_blk_controller
	spdk_vhost_blk_construct    //初始化spdk_vhost_blk_dev设备,关联tranport ops,spdk_bdev
		vhost_dev_register  // 设置后端操作集vhost_blk_device_backend, spdk_vhost_dev设备添加到全局列表g_vhost_devices
			vhost_user_blk_create_ctrlr //调用transport ops操作集的create_ctrlr方法创建控制器
				vhost_user_dev_register  //创建spdk_thread,添加到全局threads列表,并通过_scheduler_thread调度到reactor
					vhost_register_unix_socket  //创建unix socket监听,设置通知回调g_spdk_vhost_ops,并启动监听回调为:vhost_user_server_new_connection

vhost-blk与spdk_bdev,malloc_disk的关系示意如下:
vhost_blk与spdk_bdev及malloc_disk的关系

vhost客户端连接

当在qemu虚拟机的启动命令中带上如下参数,挂载vhost-blk设备,将向vhost服务程序发起建联请求:

-chardev socket,id=spdk_vhost_blk0,path=/var/tmp/VhostBlk.0 -device vhost-user-blk-pci,chardev=spdk_vhost_blk0

vhost服务程序根据vhost协议进行一系列的消息处理(比如:内存映射,virtqueue等),最后创建io_channel及IO处理线程开始接受客户端IO;

注:
vhost服务程序监听的回调函数为:vhost_user_server_new_connection,收到客户端的连接后,回调该函数处理
客户端与vhost服务程序通讯的回调函数为:vhost_user_read_cb,读取客户端的消息,调用指定的消息处理函数,消息函数映射表:vhost_message_handlers
监听的通知回调g_spdk_vhost_ops在vhost-blk服务初始化的时候设置,vsocket.notify_ops。

本文主要关注vhost协议协商完成后,vhost device和spdk_bdev, spdk-thread的关联部分:首先在vhost服务收到客户端的连接请求后,在vhost_user_server_new_connection回调中会创建一个代表vhost device的对象virtio_net(并不是一个net设备,只是复用了dpdk中的实现),并添加到vhost_devices全局数组中,接着回调g_spdk_vhost_ops中的new_connection方法创建一个session(类型为:spdk_vhost_session),后续的IO都是在这个session进行;

在客户端和vhost服务间的vhost协议协商过程完成后,vhost服务回调g_spdk_vhost_ops中的new_service方法启动session ---- 向vhost_dev线程的msg ring投递一个启动session的消息,消息处理函数为vhost_user_session_start,该方法会调用vhost-blk后端集合中的start_session方法启动session ---- 在该方法中会调用spdk_get_io_channel创建io_channel链, 最后向线程中添加一个poller(方法为:vdev_worker),该poller负责从virtqueue中轮询取IO并处理,这样vhost-blk的挂载就完成了。建联的核心调用如下:

vhost_user_server_new_connection  //和客户端建立连接后,创建vhost device对象virtio_net,创建新的会话,连接句柄fd添加到poll,回调为vhost_user_read_cb,准备接下来的vhost协议协商
	new_connection //初始化spdk_vhost_session会话实例,与vhost_blk_dev关联

vhost_user_read_cb  //根据消息调用vhost_message_handlers中的处理函数进行vhost协议协商处理,
	vhost_user_msg_handler
		start_device  //协商完成后,启动会话
			spdk_thread_send_msg //向spdk_vhost_blk_dev所在的线程投递消息,处理函数为vhost_user_session_start,由reactor调度

vhost_user_session_start  //调用后端的操作集vhost_blk_user_device_backend启动会话vhost_blk_start
	vhost_blk_start
		vhost_blk_get_io_channel   //创建io_channel链
		SPDK_POLLER_REGISTER  //添加poller方法vdev_worker

vhost device与spdk_thread,io_channel及poller的示意关系图:
vhost device与spdk_thread,io_channel及poller的关系

vhost-blk数据面

vhost持续的polling注册的poller函数(vdev_worker)---- 逐个的处理每个virtqueue,对于每个从virtqueue的vring中取出的IO数据会被归集在spdk_vhost_blk_task数据结构中,由函数virtio_blk_process_request提交到vhost-blk关联的spdk bdev设备进行处理,IO数据归集到spdk_bdev_io结构中。依照上节<vhost客户端连接>中建立的io_channel链及io_device链,IO数据最后交由Malloc0设备完整数据落盘,然后沿着链条反向传回应答。IO处理的核心调用如下:

process_blk_task     //将IO数据归集到spdk_vhost_blk_task结构
  vhost_user_process_blk_request
     virtio_blk_process_request   //提交请求到vhost_blk_dev层处理,根据请求type调用不同的例程,如写请求
		spdk_bdev_writev
			bdev_writev_blocks_with_md  //提交请求给关联的spdk_bdev设备处理,IO数据归集到spdk_bdev_io结构
				bdev_submit_request   //提交请求给malloc_disk设备处理,IO数据归集到malloc_task结构
					bdev_malloc_submit_request    //malloc_disk初始化时会给关联的spdk_bdev设置一个操作集malloc_fn_table其中有submit_request接口)
 						_bdev_malloc_submit_request  //根据不同的IO type调用不同的例程处理请求

本文从宏观的角度,以vhost-blk为例,端到端的梳理了spdk控制面的初始化过程,着重介绍了各数据结构的关系及建立过程,希望通过本文各位读者能对spdk的工作原理有个较清晰的认识。

SPDK存储性能开发套件)官方文档中文版。 第一章 简介 1 1.1.什么是SPDK? 1 1.2.入门 1 1.3. Vagrant开发环境 3 1.4.更新日志(略) 6 第二章 概念 6 2.1. 用户空间驱动程序** 6 2.2. 来自用户空间的DMA** 7 2.3. 消息传递和并发** 9 2.4. NAND Flash SSD内部 13 2.5. 将I / O提交到NVMe设备** 15 2.5.1 NVMe规范 15 2.5.2 SPDK NVMe驱动程序I / O路径 15 2.6. 使用Vhost-user进行虚拟化I / O. 16 2.6.1 介绍 16 2.6.2 QEMU 17 2.6.3 设备初始化 18 2.6.4 I / O路径 19 2.6.5 SPDK优化 20 2.7. SPDK目录结构概述 20 2.8. SPDK移植指南 22 第三章 用户指南 22 3.1. 系统配置用户指南 22 3.1.1 IOMMU配置 22 3.2. SPDK应用程序概述 23 3.2.1 配置SPDK应用程序 23 3.3. iSCSI Target 26 3.3.1. iSCSI Target入门指南 26 3.3.2. 通过配置文件配置iSCSI Target 27 3.3.3. 通过RPC方法配置iSCSI Target 28 3.3.4. 配置iSCSI启动器 29 3.3.5. rpc配置示例*** 30 3.3.6. iSCSI 热插拔 32 3.4. NVMe over Fabrics Target 32 3.5. Vhost Target(略) 37 3.6 块设备用户指南 38 3.6.1 bdev介绍 38 3.6.2 通用RPC命令 38 3.6.3 Ceph RBD 39 3.6.4 压缩虚拟Bdev模块 40 3.6.5 加密虚拟Bdev模块 41 3.6.6 延迟vbdev模块 41 3.6.7 GPT(GUID分区表) 42 3.6.8 iSCSI bdev 43 3.6.9 Linux AIO bdev 43 3.6.10 OCF虚拟bdev 43 3.6.11 Malloc bdev 44 3.6.12 NULL bdev 44 3.6.13 NVMe bdev 44 3.6.14 逻辑卷Lvol 45 3.6.15 RAID 46 3.6.16 Passthru 46 3.6.17 Pmem 46 3.6.18 Virtio Block 47 3.6.19 Virtio SCSI 47 3.7 BlobFS(Blobstore文件系统) 48 3.7.1 RocksDB集成 48 3.7.2 FUSE插件 49 3.8 JSON-RPC方法(略) 49 第四章 程序员指南 49 4.1. Blobstore程序员指南 49 4.1.1 介绍 50 4.1.2 运作理论 50 4.1.3 设计注意事项 52 4.1.4 例子 54 4.1.5配置 54 4.1.6 组件细节 54 4.2. 块设备层编程指南 56 4.3 编写自定义块设备模块 58 4.3.1 介绍 58 4.3.2 创建一个新模块 59 4.3.3创建虚拟Bdev 60 4.4 NVMe over Fabrics目标编程指南 61 4.4.1 介绍 61 4.4.2 原语结构体 61 4.4.3 基础函数 62 4.4.4访问控制 62 4.4.5发现子系统 62 4.4.6 传输 63 4.4.7选择线程模型 63 4.4.8 跨CPU核心扩展 63 4.4.9 零拷贝支持 63 4.4.10 RDMA 63 4.5 Flash传输层 64 4.5.1 术语 64 4.5.2 使用方法 67 4.6 GDB宏用户指南 69 4.6.1 介绍 69 4.6.2 加载gdb宏 71 4.6.3 使用gdb数据目录 72 4.6.4 使用.gdbinit加载宏 72 4.6.5 为什么我们需要显式调用spdk_load_macros 72 4.6.6 以上可用的宏总结 73 4.6.7 添加新宏 73 4.7 SPDK “Reduce”块压缩算法 73 4.7.1 介绍 73 4.7.2 例子 74 4.8 通知库 78 第五章 基本信息 79 5.1 事件框架 79 5.1.1 事件框架设计注意事项 80 5.1.2 SPDK事件框架组件 80 5.1.3 应用框架 80 5.2 逻辑卷 81 5.2.1 术语 81 5.2.2 配置逻辑卷 84 5.3 矢量数据包处理(略) 86 第六章 杂项 86 6.1 介绍 86 6.2 NVMe的P2P API 86 6.3 确定设备支持 87 6.4 P2P问题 87 第七章 驱动程序 88 7.1 NVMe驱动程序*** 88 7.1.1 介绍 88 7.1.2 例子 88 7.1.3 公共接口 89 7.1.4 NVMe驱动程序设计 89 7.1.5 NVMe over Fabrics主机支持 91 7.1.6 NVMe多进程 91 7.1.7 NVMe Hotplug 92 7.2 I/OAT驱动程序 93 7.2.1 公共接口 93 7.2.2 关键功能 93 7.3 Virtio驱动程序 93 7.3.1 介绍 93 7.3.2 2MB大页面 93 第八章 工具 94 8.1 SPDK CLI 94 8.1.1 安装所需的依赖项 94 8.1.2 运行SPDK应用程序实例 94 8.1.3 运行SPDK CLI 94 8.1.4 可选 - 创建Python虚拟环境 94 8.2 nvme-CLI 95 8.2.1 nvme-cli with SPDK入门指南 95 8.2.2 使用场景 95 第九章 性能测试报告(略) 96 第十章NVMe-oF Target跟踪点*** 96 10.1 介绍 96 10.2 启用跟踪点 97 10.3 捕获事件的快照 97 10.4 捕获足够的跟踪事件 98 10.5 添加新的跟踪点 99
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值