0
  • 聊天消息
  • 系统消息
  • 评论与回复
登录后你可以
  • 下载海量资料
  • 学习在线课程
  • 观看技术视频
  • 写文章/发帖/加入社区
会员中心
创作中心

完善资料让更多小伙伴认识你,还能领取20积分哦,立即完善>

3天内不再提示

SPDK Thread模型设计与实现 NVMe-oF的使用案例

FPGA之家 来源:CSDN技术社区 作者:CSDN技术社区 2022-07-03 16:20 次阅读

SPDK Thread 模型是SPDK诞生以来十分重要的模块,它的设计确保了spdk应用的无锁化编程模型,本文基于spdk最新的release 19.07版本介绍了整体thread模型的设计与实现,并详细分析了NVMe-oF的使用案例。

SPDK Thread 模型设计与实现

Reactor – 单个CPU Core抽象,主要包含了:

  • Lcore对应的CPU Core id

  • Threads在该核心下的线程

  • Events 这是一个spdk ring,用于事件传递接收

Thread – 线程,但它是spdk抽象出来的线程,主要包含了:

  • io_channels资源的抽象,可以是bdev,也可以是具体的tgt

  • tailq 线程队列,用于连接下一个线程

  • name 线程的名称

  • Stats 用于计时统计闲置和忙时时间的

  • active_pollers 轮询使用的poller,非定时

  • timer_pollers 定时的poller

  • messages 这是一个spdk ring,用于消息传递接收

  • msg_cache 事件的缓存

1.1 Reactor

对象g_reactor_state有五个状态对应了应用中reactors运行运行状态,

enum spdk_reactor_state {

SPDK_REACTOR_STATE_INVALID = 0,

SPDK_REACTOR_STATE_INITIALIZED = 1,

SPDK_REACTOR_STATE_RUNNING = 2,

SPDK_REACTOR_STATE_EXITING = 3,

SPDK_REACTOR_STATE_SHUTDOWN = 4,

};

初始情况下是:

SPDK_REACTOR_STATE_INVALID状态,在spdk app(任意一个target,比如nvmf_tgt)启动时,即调用了spdk_app_start方法,会调用spdk_reactors_init,在这个方法中将会初始化所有需要被初始化的reactors(可以在配置文件中指定需要使用的Core,CPU Core 和reactor是一对一的)。并且会将g_reactor_state设置为SPDK_REACTOR_STATE_INITIALIZED。具体代码如下:

Int spdk_reactors_init(void)

{

// 初始化所有的event mempool

g_spdk_event_mempool = spdk_mempool_create(…);

// 为g_reactors分配内存,g_reactors是一个数组,管理了所有的reactors

posix_memalign((void **)&g_reactors, 64, (last_core + 1) * sizeof(struct spdk_reactor));

// 这里设置了reactor创建线程的方法,之后需要初始化线程的时候将会调用该方法

spdk_thread_lib_init(spdk_reactor_schedule_thread, sizeof(struct spdk_lw_thread));

// 对于每一个启动的reactor,将会初始化它们

// 初始化reactor过程,即为绑定lcore,初始化spdk ring、threads,对rusage无操作

SPDK_ENV_FOREACH_CORE(i) {

reactor = spdk_reactor_get(i);

spdk_reactor_construct(reactor, i);

}

// 设置好状态返回

g_reactor_state = SPDK_REACTOR_STATE_INITIALIZED;

return 0;

}

在进入SPDK_REACTOR_STATE_INITIALIZED状态且spdk_app_start在创建了自己的线程并绑定到了reactors后,会调用spdk_reactors_start方法并将g_reactor_state设置为SPDK_REACTOR_STATE_RUNNING状态并会创建所有reactor的线程且轮询。

Void spdk_reactors_start(void) {

SPDK_ENV_FOREACH_CORE(i) {

if (i != current_core) { // 在非master reactor中

reactor = spdk_reactor_get(i); // 得到相应的reactor

// 设置好线程创建后的一个消息,该消息为轮询函数

rc = spdk_env_thread_launch_pinned(reactor->lcore, _spdk_reactor_run, reactor);

// reactor创建好线程并且会自动执行第一个消息

spdk_thread_create(thread_name, tmp_cpumask);

}

}

// 当前CPU core得到reactor,并且开始轮询

reactor = spdk_reactor_get(current_core);

_spdk_reactor_run(reactor);

}

之前提到spdk_reactors_init方法中调用了spdk_thread_lib_init方法传入了创建thread的spdk_reactor_schedule_thread方法,在调用spdk_thread_create会回调该方法。这个方法它主要的功能就是告诉这个新创建的线程绑定创建该线程的reactor。

spdk_reactor_schedule_thread(struct spdk_thread *thread)

{

// 得到该线程设置的cpu mask

cpumask = spdk_thread_get_cpumask(thread);

for (i = 0; i < spdk_env_get_core_count(); i++) {

…. // 遍历cpu core

// 通过cpu mask找到对应的核心,并产生event

if (spdk_cpuset_get_cpu(cpumask, core)) {

evt = spdk_event_allocate(core, _schedule_thread, lw_thread, NULL);

break;

}

}

// 传递该event,即对应的reatcor会调用_schedule_thread方法,

spdk_event_call(evt);

}

_schedule_thread(void *arg1, void *arg2)

{

struct spdk_lw_thread *lw_thread = arg1;

struct spdk_reactor *reactor;

// 消息传递到对应的reactor后将该thread加入到reactor中

reactor = spdk_reactor_get(spdk_env_get_current_core());

TAILQ_INSERT_TAIL(&reactor->threads, lw_thread, link);

}

在SPDK_REACTOR_STATE_RUNNING后,此时所有reactor就进入了轮询状态。_spdk_reactor_run函数为线程提供了轮询方法:

static int _spdk_reactor_run(void *arg) {

while (1) {

// 处理reactor上的event消息,消息会在之后讲到

_spdk_event_queue_run_batch(reactor);

// 每一个reactor上注册的thread进行遍历并且处理poller事件

TAILQ_FOREACH_SAFE(lw_thread, &reactor->threads, link, tmp) {

rc = spdk_thread_poll(thread, 0, now);

}

// 检查reactor的状态

if (g_reactor_state != SPDK_REACTOR_STATE_RUNNING) {

break;

}

}

}

而当spdk app被调用spdk_app_stop方法后将会相应的通知每一个reactor调用spdk_reactors_stop方法,将g_reactor_state赋值为SPDK_REACTOR_STATE_EXITING,即开始退出了。回到_spdk_reactor_run函数中,轮询将会被跳出,并且执行销毁线程的代码。

static int _spdk_reactor_run(void *arg) {

….. // 轮询

TAILQ_FOREACH_SAFE(lw_thread, &reactor->threads, link, tmp) {

thread = spdk_thread_get_from_ctx(lw_thread);

TAILQ_REMOVE(&reactor->threads, lw_thread, link);

spdk_set_thread(thread);

spdk_thread_exit(thread);

spdk_thread_destroy(thread);

}

}

在这之后,主线程的_spdk_reactor_run会返回到spdk_reactors_start中,并将g_reactor_state赋值为SPDK_REACTOR_STATE_SHUTDOWN,返回到spdk_app_start中等待应用退出。

最后,总结一下reactors和CPU core以及spdk thread关系应该如图1所示

1035d500-e91c-11ec-ba43-dac502259ad0.png

图1 CPU cores、reactors和thread关系图

Reactor生命周期流程图则如图2所示

10691eec-e91c-11ec-ba43-dac502259ad0.png

图2 reactor生命周期流程图

1.2 thread

当Reactors进行轮询时,除了处理自己的事件消息之外,还会调用注册在该reactor下面的每一个线程进行轮询。不过通常一个reactor只有一个thread,在spdk应用中,更多的是注册多个poller而不是注册多个thread。具体的轮询方法为:

Int spdk_thread_poll(struct spdk_thread *thread, uint32_t max_msgs, uint64_t now) {

// 首先先处理ring传递过来的消息

msg_count = _spdk_msg_queue_run_batch(thread, max_msgs);

// 调用非定时poller中的方法

TAILQ_FOREACH_REVERSE_SAFE(poller, &thread->active_pollers,

active_pollers_head, tailq, tmp) {

// 调用poller注册的方法之前,会对poller状态检测且转换

if (poller->state == SPDK_POLLER_STATE_UNREGISTERED) {

TAILQ_REMOVE(&thread->active_pollers, poller, tailq);

free(poller);

continue;

}

poller->state = SPDK_POLLER_STATE_RUNNING;

// 调用poller注册的方法

poller_rc = poller->fn(poller->arg);

// poller转换状态

poller->state = SPDK_POLLER_STATE_WAITING;

}

// 调用定时poller中的方法

TAILQ_FOREACH_SAFE(poller, &thread->timer_pollers, tailq, tmp) {

// 类似非定时poller过程,不过会检查是否到了预定的时间

if (now < poller->next_run_tick) break;

}

// 最后统计时间

}

Io_device 和 io_channel在thread中也是非常重要的概念。它们的实现都在thread.c中,io_device是设备的抽象,io_channel是对该设备通道的抽象。一个线程可以创建多个io_channel . io_channel只能和一个io_device绑定,并且这个io_channel是别的线程使用不了的。

10a42758-e91c-11ec-ba43-dac502259ad0.png

图 3 io_device、io_channel和线程关系图

Io_device结构

struct io_device {

void *io_device; // 抽象的device指针

char name[SPDK_MAX_DEVICE_NAME_LEN + 1]; // 名字

spdk_io_channel_create_cb create_cb; // io_channel创建的回调函数

spdk_io_channel_destroy_cb destroy_cb; // io_channel销毁的回调函数

spdk_io_device_unregister_cb unregister_cb; // io_device解绑的回调函数

struct spdk_thread *unregister_thread; // 不使用该device线程

uint32_t ctx_size; // ctx的大小,将会传给io_channel处理

uint32_t for_each_count; // io_channel的数量

TAILQ_ENTRY(io_device) tailq; // device队列头

uint32_t refcnt; // 计数器

bool unregistered; // 是否该device被注册

};

可以看到,io_device实际上只提供了一些自身io_device的操作和io_channel相关的方法,具体的io_device实体其实是那个名字叫io_device的void指针。因为thread中的io_device只提供了thread这一层接口,具体的io操作每一个设备很难被抽象出来,所以这一层的接口只负责管理io_channel的创建、销毁和绑定等。

Io_channel的结构

struct spdk_io_channel {

struct spdk_thread *thread; // 绑定的线程

struct io_device *dev; // 绑定的io_device

uint32_t ref; // io_channel引用计数

uint32_t destroy_ref; // destroy前被引用的次数

TAILQ_ENTRY(spdk_io_channel) tailq; // io_channel 队列头

spdk_io_channel_destroy_cb destroy_cb; // io_channel销毁的回调函数

};

虽然io_channel看起来是很简单的结构体,实际上在创建一个io_device的时候,会要求使用者传入一个io_channel_ctx的大小作为调用的参数,而在给io_channel分配内存的时候,除了分配本身io_channel结构体的大小外,还会额外分配一个io_channel_ctx的大小,这个context可以理解成一个void指针,当用户在使用io_channel的时候,实际上还是通过context的部分去访问io_device。

NVMe-oF实例

nvmf_tgt 是spdk中一个重要的模块,这里详细的写一下它作为一个target实例是如何使用thread、io_device以及io_channel的。

在spdk应用刚启动的时候,reactor模块就会自动加载起来,然后在加载nvmf subsystem的时候,会调用spdk_nvmf_subsystem_init(lib/event/subsystems/nvmf/nvmf_tgt.c)方法,nvmf_tgt其实也是有生命周期,并且有一个状态机去管理它的生命周期。

enum nvmf_tgt_state {

NVMF_TGT_INIT_NONE = 0, // 最初的状态

NVMF_TGT_INIT_PARSE_CONFIG, // 解析配置文件

NVMF_TGT_INIT_CREATE_POLL_GROUPS, // 创建poll groups

NVMF_TGT_INIT_START_SUBSYSTEMS, // 启动subsystem

NVMF_TGT_INIT_START_ACCEPTOR, // 开始接收

NVMF_TGT_RUNNING, // running

NVMF_TGT_FINI_STOP_SUBSYSTEMS,

NVMF_TGT_FINI_DESTROY_POLL_GROUPS,

NVMF_TGT_FINI_STOP_ACCEPTOR,

NVMF_TGT_FINI_FREE_RESOURCES,

NVMF_TGT_STOPPED,

NVMF_TGT_ERROR,

};

首先在NVMF_TGT_INIT_PARSE_CONFIG状态中,nvmf_tgt会去解析启动时传入的配置文件,当解析了[nvmf]这个label后,会调用spdk_nvmf_tgt_create这个方法,这个方法将初始化了全局的g_nvmf_tgt变量,同时也将tgt注册成了一个io_device。

spdk_io_device_register(tgt,

spdk_nvmf_tgt_create_poll_group,

spdk_nvmf_tgt_destroy_poll_group,

sizeof(struct spdk_nvmf_poll_group),

"nvmf_tgt");

spdk_nvmf_tgt_create_poll_group和spdk_nvmf_tgt_destroy_poll_group是io_channel创建和销毁的回调方法。第三个参数是io_channel_ctx的size,既然这里传入了spdk_nvmf_poll_group的大小,那么很明显说明在nvmf中io_channel_ctx对象就是spdk_nvmf_poll_group。

当config文件解析完了之后,nvmf_tgt状态到了NVMF_TGT_INIT_CREATE_POLL_GROUPS,这个状态下会为每一个线程都创建相应的poll group。

spdk_for_each_thread(nvmf_tgt_create_poll_group,

NULL,

nvmf_tgt_create_poll_group_done);

static void nvmf_tgt_create_poll_group(void *ctx)

{

struct nvmf_tgt_poll_group *pg;

….

pg->thread = spdk_get_thread();

pg->group = spdk_nvmf_poll_group_create(g_spdk_nvmf_tgt);

….

}

再看spdk_nvmf_poll_group_create中,

struct spdk_nvmf_poll_group * spdk_nvmf_poll_group_create(struct spdk_nvmf_tgt *tgt)

{

struct spdk_io_channel *ch;

ch = spdk_get_io_channel(tgt);

….

return spdk_io_channel_get_ctx(ch);

}

在spdk_get_io_channel中,会先去检查传入的io_device是不是已经注册好了的,如果已经注册了,将会创建一个新的io_channel返回,创建的过程会回调在注册io_device时注册的io_channel创建方法(即方法spdk_nvmf_tgt_create_poll_group)。

static int spdk_nvmf_tgt_create_poll_group(void *io_device, void *ctx_buf)

{

….. // 初始化transport 、nvmf subsystem等

// 注册一个poller

group->poller = spdk_poller_register(spdk_nvmf_poll_group_poll, group, 0);

group->thread = spdk_get_thread();

return 0;

}

在spdk_nvmf_poll_group_poll中,因为spdk_nvmf_poll_group对象中有transport的poll group,所以它会调用对应的transport的poll_group_poll方法,比如rdma的poll_group_poll就会轮询rdma注册的poller处理每个在相应的qpair来的请求,进入rdma的状态机将请求处理好。

然后这个状态就结束了,之后再初始化好了nvmf subsystem相关的东西之后,到了状态NVMF_TGT_INIT_START_ACCEPTOR。在这个状态中,只注册了一个poller。

g_acceptor_poller = spdk_poller_register(acceptor_poll, g_spdk_nvmf_tgt,

g_spdk_nvmf_tgt_conf->acceptor_poll_rate);

这个poller调用的transport的方法,不断的监听是不是有新的fd连接进来,如果有就调用new_qpair的回调。

总结

spdk thread 模型是spdk无锁化的基础,在一个线程中,当分配一个任务后,一直会运行到任务结束为止,这确保了不需要进行线程之间的切换而带来额外的损耗。同时,高效的spdk ring提供了不同线程之间的消息传递,这就使得任务结束的结果可以高效的传递给别的处理线程。而io_device和io_channel的设计保证了资源的抽象访问以及独立的路径不去争抢资源池,并且块设备由于是对块进行操作的所以也十分适合抽象成io_device。正是因为以上几点才让spdk线程模型能够达到无锁化且为多个target提供了基础线程框架的支持。

原文标题:SPDK线程模型解析

文章出处:【微信公众号:FPGA之家】欢迎添加关注!文章转载请注明出处。

审核编辑:汤梓红
声明:本文内容及配图由入驻作者撰写或者入驻合作网站授权转载。文章观点仅代表作者本人,不代表电子发烧友网立场。文章及其配图仅供工程师学习之用,如有内容侵权或者其他违规问题,请联系本站处理。 举报投诉
  • cpu
    cpu
    +关注

    关注

    68

    文章

    10855

    浏览量

    211601
  • 模型
    +关注

    关注

    1

    文章

    3229

    浏览量

    48811
  • Thread
    +关注

    关注

    2

    文章

    83

    浏览量

    25923
  • nvme
    +关注

    关注

    0

    文章

    221

    浏览量

    22624

原文标题:SPDK线程模型解析

文章出处:【微信号:zhuyandz,微信公众号:FPGA之家】欢迎添加关注!文章转载请注明出处。

收藏 人收藏

    评论

    相关推荐

    TH58NVG4S0HTA20闪存芯片TH58NVG4S0HTAK0

    TH58NVG4S0HTA20闪存芯片TH58NVG4S0HTAK0铠侠宣布其 EM6 系列企业级 NVMe-oF 固态硬盘 (SSD) 可用于以太网束闪存 (EBOF) 系统,采用Marvell
    发表于 01-25 09:00

    IBM大波存储阵列新增对NVMe-oF的支持

    IBM近日进行了大量的产品发布,其中闪存阵列增加了容量,一系列使用FC加速数据访问的阵列新增了对NVMe-oF的支持。
    的头像 发表于 11-05 08:53 3918次阅读

    业界首款NVMe-oF SSD转换器控制器能够降低总体拥有成本

    Marvell 88SN2400能够将NVMe SSD转换为NVMe-oF SSD,实现最佳的高性能分类式存储,从而降低云和企业数据中心应用的总体拥有成本
    发表于 11-23 17:10 1105次阅读

    超全的SPDK性能评估指南

    SPDK采用异步I/O(Asynchronous I/O)加轮询(Polling)的工作模式,通常与Kernel的异步I/O作为对比。在此,主要介绍通过使用fio评估Kernel异步I/O,以及spdk fio_plugin的两种模式。
    的头像 发表于 11-26 09:58 8996次阅读

    FCoE曾经想一统江湖,但现在已经无人问津,NVMe/TCP会重蹈覆辙吗?

    我们知道,NVMe采用PCIe连接,是基于内存映射的协议,而NVMe-oF却实基于消息的,但也可以支持共享内存。其中RDMA(包括IB,RoCE,iWarp)这两种方式都支持,但FC和TCP,只支持消息。
    的头像 发表于 03-21 14:17 1.1w次阅读
    FCoE曾经想一统江湖,但现在已经无人问津,<b class='flag-5'>NVMe</b>/TCP会重蹈覆辙吗?

    Marvell推出NVMe-oF以太网SSD技术和新一代SSD控制器解决方案

    Marvell(NASDAQ:MRVL)今日宣布扩展其革命性的NVMe over Fabrics™(NVMe-oF™)产品组合。这些突破性的解决方案,包括由Marvell NVMe-oF SSD转换
    的头像 发表于 08-19 01:19 4876次阅读

    西部数据新款数据存储解决方案助力企业实现NVMe的过渡

    西部数据公司拥有完整的企业级数据存储解决方案,6月29日,西部数据最新推出了基于Ultrastar NVMe SSD设计的新款数据存储解决方案,并由NVMe-oF提供强化支持,为下一代数据基础架构奠定根基。
    的头像 发表于 07-07 16:33 2945次阅读

    NVMe-oF的优势及未来

    (NVIDIA)等关键技术合作伙伴的合作,共同解锁NVMe-oF技术可以带来的优势。我们还在开发创新技术,如异构存储器存储引擎(HSE),这将有助于使用NVMe-oF优化对闪存的大规模访问。 NVMe-oF
    的头像 发表于 01-05 17:42 9492次阅读
    <b class='flag-5'>NVMe-oF</b>的优势及未来

    整体thread模型的设计与实现

    ,并详细分析了NVMe-oF的使用案例。 P1 SPDK Thread 模型设计与实现 Reactor – 单个CPU Core抽象,主要包
    的头像 发表于 03-29 14:39 2117次阅读
    整体<b class='flag-5'>thread</b><b class='flag-5'>模型</b>的设计与<b class='flag-5'>实现</b>

    RT-Thread AI kit开源:轻松实现一键部署AI模型至 RT-Thread

    RT-AK 是 RT-Thread 团队为 RT-Thread 实时操作系统所开发的 AI 套件,能够一键将 AI 模型部署到 RT-Thread 项目中,让用户可以 在统一...
    发表于 01-25 18:18 3次下载
    RT-<b class='flag-5'>Thread</b> AI kit开源:轻松<b class='flag-5'>实现</b>一键部署AI<b class='flag-5'>模型</b>至 RT-<b class='flag-5'>Thread</b>

    为什么NVMe/TCP是数据中心的更优选择

    与SCSI、ISCSI、SAS或SATA 接口不同,NVMe实现了针对多核服务器 CPU 优化的简化命令模式和多队列体系结构。NVMe-oF规范扩展了NVMe
    的头像 发表于 04-18 10:22 1478次阅读
    为什么<b class='flag-5'>NVMe</b>/TCP是数据中心的更优选择

    RT-Thread设备模型框架及创建注册设备的实现

    RT-Thread设备模型框架及创建注册设备的实现方式介绍如下:
    的头像 发表于 05-28 10:38 2179次阅读
    RT-<b class='flag-5'>Thread</b>设备<b class='flag-5'>模型</b>框架及创建注册设备的<b class='flag-5'>实现</b>

    DPU应用场景系列(二) 存储功能卸载

    据中心的计算和存储进行分解。NVMe-oF协议定义了使用各种通用的传输协议来实现NVMe功能的方式。在NVMe-oF诞生之前,数据存储协议可以分为三种:(1)iSCSI
    的头像 发表于 05-19 14:34 2401次阅读
    DPU应用场景系列(二) 存储功能卸载

    vSphere 7.0 U1中的NVMe-oF的性能表征

    电子发烧友网站提供《vSphere 7.0 U1中的NVMe-oF的性能表征.pdf》资料免费下载
    发表于 08-04 09:15 0次下载
    vSphere 7.0 U1中的<b class='flag-5'>NVMe-oF</b>的性能表征

    SPDK在虚拟化场景下的使用方法

    SPDK(全称Storage Performance Development Kit),提供了一整套工具和库,以实现高性能、扩展性强、全用户态的存储应用程序。它是继DPDK之后,intel在存储领域
    的头像 发表于 11-10 10:12 1489次阅读
    <b class='flag-5'>SPDK</b>在虚拟化场景下的使用方法