SPDK的应用程序框架深入分析,从原理到代码实现
前面介绍了很多的内容,大多停留在应用层面,没有深入到SPDK最核心的部分。今天我们就介绍一下SPDK最核心的内容之一,基于事件的应用框架。该框架可以提供异步、轮询和无共享的架构。这些特性对于基于NVMe的多核硬件架构是非常重要的,他消除了内核架构中上下文切换的耗时、提高了多核场景下的性能。
多核架构已经称为常规架构,即使是消费级的个人PC也都是多核CPU,而对于高端服务器则全部是多CPU、多核架构。通过增加CPU核数可以提升应用处理事务的能力,但并非线性关系,其根本原因在于软件内部通常存在共享的数据。多核架构共享数据的访问通常需要某种锁机制,任何锁都会导致多核之间的访问冲突,从而影响整体性能。
SPDK解决的首要的问题就是多核架构的问题,在SPDK中实现了对CPU核和线程的管理,企图通过最少的CPU资源处理尽量多的任务。为了实现对CPU核的管理,在应用启动的时候可以手动设置CPU核的绑定情况。
在SPDK中有三个关键的概念,分别是事件(Event)、反应器(Reactor)和轮询器(Poller),三者之间的关系如图所示。事件可以理解为具体的工作内容项,事件中会包含一个函数指针,该函数指针就是具体的工作。
通过上图可以看出,每个CPU核上会有一个反应器,反应器与一个事件队列关联。反应器不断的从队列当中获取事件进行处理。除此之外,反应器还会与轮询器关联,用于轮询IO设备的状态,确定是否与IO设备交互数据。
其中反应器关联到一个线程,可以理解为线程函数。每个CPU内核通常只运行一个线程,这样可以避免线程间切换浪费CPU资源。
我们这里先看一下几个关键的数据结构,这些数据结构与上图中的几个概念相对应。如下图是反应器的数据结构,该数据结构中2所示的是事件队列,就是上图中所示的环形缓冲区,也即无锁队列。
反应器之间通过事件来传递消息,SPDK的时间机制避免了多线程间共享数据访问导致的所碰撞问题。事件结构体如下图所示,事件结构体核心是一个事件函数和对应的参数。接收事件的反应器会从事件队列批量的读取事件,然后调用事件函数处理事件,后文我们会详细的介绍这个过程。
另外一个比较重要的数据结构是线程,如下是线程的数据结构。在该数据结构中比较重要的成员是轮询器列表、消息队列和IO通道等内容。
对于轮询器列表,SPDK中分为两类,一种是简单轮询的轮询器列表,该种类型的轮询器会被逐个执行。另外一种是基于定时的轮询器列表,采用红黑树管理,该种类型的轮询器会根据时间间隔执行。
需要线程执行的具体任务会以消息的方式发送给线程,并且首先会被挂载到消息队列中,线程函数从队列中获取消息并执行。
IO通道是与IO设备交互的接口,比如与NVMe设备交互,需要一个或者多个IO通道。这里大家只需要大概了解一下本文中图一中各个组件与数据结构的相互关系即可,关于上述部分内容的更多细节,我们接下来会详细的介绍。
轮询器也对应着一个数据结构,如下图所示为轮询器的数据结构,其中最为核心的是一个函数指针,通过该函数指针执行具体的任务。除此之外,该数据结构中还有很多与轮询器具体执行相关的成员。比如其中有些时间相关的成员(***ticks)是定时轮询器用来计时的。
在具体实现层面,SPDK通过app实现了对上述概念的封装,通过spdk_app_start启动上述机制。基于SPDK的程序都是通过这个API来启动程序业务逻辑的,比如iscsi_tgt、spdk_tgt、nvmf_tgt和spdk_dd等程序都会首先调用spdk_app_start函数。
接下来我们看一下spdk_app_start函数的内部实现。以bdev的helloword为例,在main函数中调用spdk_app_start函数。