16. SPDK应用框架
仅仅提供用户态NVMe驱动的一些操作函数是不够的,如果在某些应用场景中使用不当,不仅不能发挥出用户态NVMe驱动的高性能的作用,甚至会导致程序出现错误。
虽然NVMe的底层函数有一些说明,但为了更好地发挥出底层NVMe的性能,SPDK提供了一套编程框架(Application Framework)如下图所示,用于指导软件开发人员基于SPDK的用户态NVMe驱动及用户态块设备层构造高效的存储应用。用户可以有以下两种选择。
- 直接使用SPDK应用编程框架实现应用的逻辑。
- 利用SPDK编程框架的思想,改造已有应用的编程逻辑,以更好地适配SPDK的用户态NVMe驱动。

总的来说,SPDK的应用框架可以分为:
- 对CPU core和线程的管理
- 线程间的高效通信
- I/O的处理模型
- 数据路径的无锁化机制
对CPU core和线程的管理
SPDK的原则是使用最少的CPU核和线程来完成最多的任务。为此SPDK在初始化程序的时候限定使用绑定CPU的哪些核。可以在配置文件或命名行中配置,如在命令行中使用“-c 0x5”,这是指使用core 0和core 2来启动程序。(其中0x5对应的二进制表示为 00000101,从低位到高位分别代表对应的核的编号)
通过CPU核绑定函数的亲和性,可以限制对CPU的使用,并且在每个核上运行一个thread,这个thread在SPDK中叫作Reactor。目前SPDK的环境库默认使用了DPDK的EAL库来进行管理。
总的来说,这个Reactorthread 执行一个函数_spdk_reactor_run , 这个函数的主体包含一个“while(1){}”,直到这个Reactor的state被改变。
为了提高效率,这个循环中也会有一些相应的机制让出CPU资源,如sleep,这样的机制在很多时候会导致CPU使用100%的情况,类似于DPDK。
也就是说,一个使用SPDK编程框架的应用,假设使用了两个CPUcore,每个core上就会启动一个Reactor thread,那么用户怎么执行自己的函数呢?
为了解决这个问题,SPDK提供了一个Poller机制。所谓Poller,其实就是用户定义函数的封装。SPDK提供的Poller分为两种:基于定时器的Poller和基于非定时器的Poller。SPDK的Reactor thread对应的数据结构由相应的列表来维护Poller的机制,比如一个链表维护定时器的Poller,另一个链表维护非定时器的Poller,并且提供Poller的注册及销毁函数。
在Reactor的while循环中,会不停地检查这些Poller的状态,并且进行相应的调用,这样用户的函数就可以进行相应的执行了。由于单个CPU核上,只有一个Reactor thread,所以同一个Reactor thread中不需要一些锁的机制来保护资源。当然位于不同CPU核上的thread还是有通信的必要的。为此,SPDK封装了线程间异步传递消息(Async Messaging Passing)的功能。
线程间的高效通信
SPDK放弃使用传统的、低效的加锁方式来进行线程间的通信。为了使同一个thread 只执行自己所管理的资源, SPDK 提供了事件调用(Event)的机制。这个机制的本质是每个Reactor对应的数据结构维护了一个Event事件的环,这个环是多生产者和单消费者(Multiple Producer Single Consumer,MPSC)的模型,意思是每个Reactor thread可以接收来自任何其他Reactor thread(包括当前的Reactor thread)的事件消息进行处理。
目前SPDK中这个Event环的默认实现依赖于DPDK的机制,这个环应该有线性的锁的机制,但是相比较于线程间采用锁的机制进行同步,要高效得多。毫无疑问的是, 这个Event 环其实也在Reactor 的函数_spdk_reactor_run中进行处理。每个Event事件的数据结构包括了需要执行
的函数和相应的参数,以及要执行的core。
简单来说,一个Reactor A向另外一个Reactor B通信,其实就是需要 Reactor B执行函数F(X),X是相应的参数。基于这样的机制,SPDK就 实现了一套比较高效的线程间通信的机制,具体例子可以参照SPDK NVMe-oF Target内部的一些实现,主要代码位于lib/nvmf目录下。
I/O的处理模型及数据路径的无锁化机制
SPDK主要的I/O处理模型是运行直到完成。如前所述,使用SPDK应用框架, 一个CPU core 只拥有一个thread , 这个thread 可以执行很多Poller(包括定时器和非定时器)。运行直到完成的原则是让一个线程最好执行完所有的任务。
显而易见,SPDK的编程框架满足了这个需要。如果不使用SPDK应用编程框架,则需要编程者自己注意这个事项。比如使用SPDK用户态NVMe驱动访问相应的I/O QPair进行读/写操作,SPDK提供了异步读/写的函数spdk_nvme_ns_cmd_read , 以及检查是否完成的函数spdk_nvme_qpair_process_completions,这些函数的调用应当由一个线程去完成,而不应该跨线程去处理。