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的工作原理有个较清晰的认识。

  • 3
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
《Python学习笔记》是由皮大庆编写的一本关于Python语言学习的教材。在这本书中,作者详细介绍了Python语言的基础知识、语法规则以及常用的编程技巧。 首先,作者简要介绍了Python语言的特点和优势。他提到,Python是一种易于学习和使用的编程语言,受到了广大程序员的喜爱。Python具有简洁、清晰的语法结构,使得代码可读性极高,同时也提供了丰富的库和模块,能够快速实现各种功能。 接着,作者详细讲解了Python的基本语法。他从变量、数据类型、运算符等基础知识开始,逐步介绍了条件语句、循环控制、函数、模块等高级概念。同时,作者通过大量的示例代码和实践案例,帮助读者加深对Python编程的理解和应用。 在书中,作者还特别强调了编写规范和良好的编程习惯。他从命名规范、注释风格、代码缩进等方面指导读者如何写出清晰、可读性强的Python代码。作者认为,良好的编程习惯对于提高代码质量和提高工作效率非常重要。 此外,作者还介绍了Python的常用库和模块。他提到了一些常用的库,如Numpy、Pandas、Matplotlib等。这些库在数据处理、科学计算、可视化等领域有广泛的应用,帮助读者更好地解决实际问题。 总的来说,《Python学习笔记》是一本非常实用和全面的Python学习教材。通过学习这本书,读者可以系统地学习和掌握Python编程的基础知识和高级应用技巧,为以后的编程学习和工作打下坚实的基础。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值