dpdk无锁队列实现


难度 中等

rte_ring是dpdk内部提供的一种无锁队列实现。

vpp线程模型

  • Main Thread做管理。常常使用协程驱动实现单线程多任务(VPP内部实现了一套类似于协程的调度机制,以此来实现单线程多任务的调度)
  • fwd Thread做纯转发。通常为了性能考虑,在转发路径上严禁有内存拷贝和系统调用(但是凡事都有例外)。

dpdk ring使用

// dpdk19.11
void rte_ring_free(struct rte_ring *r) //释放已经创建的dpdk的rte_ring
    
struct rte_ring * rte_ring_lookup(const char *name) //去寻找一个已经创建好的dpdk的rte_ring
    
static __rte_always_inline unsigned int __rte_ring_do_enqueue(struct rte_ring *r, void * const *obj_table, unsigned int n, enum rte_ring_queue_behavior behavior, unsigned int is_sp, unsigned int *free_space) //此函数为内部方法,所有入队函数都是此函数的上层封装
    
static __rte_always_inline unsigned int __rte_ring_do_dequeue(struct rte_ring *r, void **obj_table, unsigned int n, enum rte_ring_queue_behavior behavior, unsigned int is_sc, unsigned int *available) //此函数为内部方法,所有出队函数都是此函数的上层封装
    
static __rte_always_inline unsigned int rte_ring_mp_enqueue_bulk(struct rte_ring *r, void * const *obj_table, unsigned int n, unsigned int *free_space) //此函数为批量入队函数,为多生产者安全(multi producer)
    
static __rte_always_inline unsigned int
rte_ring_sp_enqueue_bulk(struct rte_ring *r, void * const *obj_table, unsigned int n, unsigned int *free_space) //此函数为批量入队函数,为单生产者安全(single producer)
    
static __rte_always_inline unsigned int rte_ring_enqueue_bulk(struct rte_ring *r, void * const *obj_table, unsigned int n, unsigned int *free_space) //此函数为批量入队函数,具体安全性质取决于创建队列时的标志(flags)
    
static __rte_always_inline unsigned int rte_ring_mc_dequeue_bulk(struct rte_ring *r, void **obj_table, unsigned int n, unsigned int *available) //此函数为批量出队函数,为多消费者安全(multi consumer)
    
static __rte_always_inline unsigned int rte_ring_sc_dequeue_bulk(struct rte_ring *r, void **obj_table, unsigned int n, unsigned int *available) //此函数为批量出队函数,为单消费者安全(single consumer)
    
static __rte_always_inline unsigned int rte_ring_dequeue_bulk(struct rte_ring *r, void **obj_table, unsigned int n, unsigned int *available) //此函数为批量出队函数,具体安全性质取决于创建队列时的标志(flags)
    
static inline unsigned rte_ring_count(const struct rte_ring *r) //此函数用于查看队列中元素的数量

无锁队列实现

无锁的实现依赖于一个汇编指令: cmpxchg
翻译过来就是compare and change

生产者消费者问题

  1. 多个生产者,生产位置有冲突,比如生产者A要push 3个元素,生产者B要push 3个元素,如何做到不冲突不覆盖?
  2. 生产者和消费者,生产了之后要让消费者可以消费,消费了之后要让生产者进行生产。
  3. 多消费者,和多生产者的问题类似,消费位置冲突,比如消费者A要消费3个元素,消费者B要消费3个元素,如何做到消费不冲突让每一个消费者都能有元素可以消费?
struct rte_ring {
    char name[RTE_MEMZONE_NAMESIZE] __rte_cache_aligned; // ring的名称,lookup的时候就是根据名称进行查找对应的ring

    int flags;                                           // 标记,用来描述队列是单/多生产者还是单/多消费者安全

    const struct rte_memzone *memzone;                     // 所属的memzone,memzone是dpdk内存管理底层的数据结构

    uint32_t size;                                        // 队列长,为2^n。如果flags为RING_F_EXACT_SZ
                                                         // 队列size为初始化时队列长度的向上取2的n次幂,例如如果为
                                                         // 7,那么向上取最近的2^n幂的数为8.如果flags不为
                                                         // RING_F_EXACT_SZ,那么初始化队列的时候队列长必须为2^n幂                                                         

    uint32_t mask;                                         // 掩码,为队列长 - 1,用来计算位置的时候取余用
    uint32_t capacity;                                    // 队列容量,一般不等于队列长度,把队列容量理解为实际可以
                                                         // 使用的元素个数即可。例如初始化时count为7并且指定标志为
                                                         // RING_F_EXACT_SZ,那么count最后为8,但是capacity为7,因为
                                                         // 8是向上取2^n幂取出来的,实际上仍然是创建时所需的个数,8.

    char pad0 __rte_cache_aligned;                          // 填充,考虑到性能,要使用填充法保证cache line

    struct rte_ring_headtail prod __rte_cache_aligned;   // 生产者位置,里面有一个生产者头,即prod.head,还有一个生
                                                         // 产者尾,即prod.tail。prod.head代表着下一次生产时的起始
                                                         // 生产位置。prod.tail代表消费者可以消费的位置界限,到达
                                                         // prod.tail后就无法继续消费,通常情况下生产完成后,
                                                         // prod.tail = prod.head,意味着刚生产的元素皆可以被消费

    char pad1 __rte_cache_aligned;  
    struct rte_ring_headtail cons __rte_cache_aligned;   // 消费者位置,里面有一个消费者头,即cons.head,还有一个消
                                                         // 费者尾,即cons.tail。cons.head代表着下一次消费时的起始
                                                         // 消费位置。cons.tail代表生产者可以生产的位置界限,到达
                                                         // cons.tail后就无法继续生产,通常情况下消费完成后,
                                                         // cons.tail = cons.head,意味着刚消费的位置皆可以被生产
                                                         
    char pad2 __rte_cache_aligned; /**< empty cache line */
};
  • 生产者:先抢占,再写入,再更新指针

    1. 先偏移头指针,说白了就是抢位置。这步主要是为了对付多生产者的情况。

    2. 抢到位置后写数据。

    3. 更新尾指针,让消费者可以消费刚塞入的数据

    static __rte_always_inline unsigned int
    __rte_ring_move_prod_head(struct rte_ring *r, unsigned int is_sp,
            unsigned int n, enum rte_ring_queue_behavior behavior,
            uint32_t *old_head, uint32_t *new_head,
            uint32_t *free_entries)
    {
        const uint32_t capacity = r->capacity;
        unsigned int max = n;
        int success;
    
        do {
            //1.先确定生产者要生产多少个元素
            n = max;
            //2.拿到现在生产者的head位置,也就是即将生产的位置
            *old_head = r->prod.head;
    
            //内存屏障
            rte_smp_rmb();
    
            //3.计算剩余的空间
            *free_entries = (capacity + r->cons.tail - *old_head);
    
            //4.比较生产的元素个数和剩余空间
            if (unlikely(n > *free_entries))
                n = (behavior == RTE_RING_QUEUE_FIXED) ?
                        0 : *free_entries;
    
            if (n == 0)
                return 0;
            //5.计算生产后的新位置
            *new_head = *old_head + n;
            if (is_sp)
                r->prod.head = *new_head, success = 1;
            else //6.如果是多生产者的话调用cpmset函数实现生产位置抢占
                success = rte_atomic32_cmpset(&r->prod.head,
                        *old_head, *new_head);
        } while (unlikely(success == 0));
        return n;
    }
    static inline int
    rte_atomic32_cmpset(volatile uint32_t *dst, uint32_t exp, uint32_t src)
    {
        uint8_t res;
    
        asm volatile(
                MPLOCKED
                "cmpxchgl %[src], %[dst];"
                "sete %[res];"
                : [res] "=a" (res),     /* output */
                  [dst] "=m" (*dst)
                : [src] "r" (src),      /* input */
                  "a" (exp),
                  "m" (*dst)
                : "memory");            /* no-clobber list */
        return res;
    }
    >cmpxchg指令的意思就是“compare and change”,即“比较并交换”。
    >举个例子,如果A等于B,则将C赋值给A;如果A不等于B,则拒绝将C赋值给A。
    >如果生产位置没有变化(A等于B),那么就将最新的生产位置(计算偏移后的生产位置)赋值给生产者指针;如果生产位置发生了变化(有其他生产者也在生产),那么就取消更新生产者指针
    static __rte_always_inline void
    update_tail(struct rte_ring_headtail *ht, uint32_t old_val, uint32_t new_val,
            uint32_t single, uint32_t enqueue)
    {
        //1.内存屏障
        if (enqueue)
            rte_smp_wmb();
        else
            rte_smp_rmb();
        //2.如果有其他生产者生产数据,那么需要等待其将数据生产完更新tail指针后,本生产者才能更新tail指针
        if (!single)
            while (unlikely(ht->tail != old_val))
                rte_pause();
        //3.更新tail指针,更新的位置为最新的生产位置,意味着刚刚生产的数据已经全部可以被消费者消费
        ht->tail = new_val;
    }
  • 消费者:先标记,再回收

reference

  1. https://www.cnblogs.com/jungle1996/p/12194243.html

文章作者: growdu
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 growdu !
  目录
分类导航
随笔2 AI27 算法1 计算机基础13 博客搭建7 ChatGPT2 集群63 计算机通信1 数据库34 数据库深入80 DPDK26 Docker11 Elasticsearch4 编辑工具4 FAQ1 Go Web1 hometown2 编程语言16 网络9 OPC1 Linux38 openGauss4 页面12 PostgreSQL54 程序员自我修养1 协议11 成长之路1 stock1 存储5 工具20 VPP18 视频作品1 Vue13 Web1 代码示例11 数据库15 BenchmarkSQL1 PostgreSQL 源码修炼之路14
最热文章
1
13 逻辑复制深入
数据库深入🔥 1570
2
0 Postgresql存储、索引及系统优化、主备切换
PostgreSQL🔥 1495
3
一文读懂openguass dcf网络模块
集群🔥 1420
4
逻辑复制源码分析
数据库深入🔥 1327
5
PostgreSQL 分区表:从一行 `PARTITION BY` 到路由热路径的全链路拆解
数据库🔥 1094
6
applyparallelworker.c 之 LA 端源码深度解析:Leader Apply Worker 的指挥中枢
数据库深入🔥 1082
7
PostgreSQL Background Worker 全解:从 `RegisterBackgroundWorker` 到逻辑复制 4 类 worker 的全生命周期
数据库🔥 1078
8
PostgreSQL的后台进程walsender分析 - 关系型数据库 - 亿速云
PostgreSQL🔥 1033
9
PostgreSQL 逻辑复制的监控:六张视图 + 一组可执行 SQL,把 publisher/subscriber 的速率与健康度彻底看透
数据库🔥 1032
10
PostgreSQL 逻辑复制支持 DDL 之后:DDL 与 DML 的时序难题(重点:分区表)
数据库🔥 999
11
reorderbuffer.c 源码深度解析:PostgreSQL 逻辑复制的"事务重组引擎
数据库深入🔥 953
12
PostgreSQL 内核开发:读取一张表的 9 步标准流程与缓存全景
数据库🔥 938
13
从 `postgres` 二进制到生产级守护 —— PostgreSQL 最外层模块与启动全流程拆解
数据库🔥 936
14
支持逻辑复制同步 DDL 适配 SQL Server 方案
数据库深入🔥 934
15
PostgreSQL 逻辑复制的 ReorderBuffer 与事务机制:从一行 WAL 到一致性变更流的全链路绑定
数据库🔥 913
16
DDL同步架构(美化版)
数据库深入🔥 908
17
PostgreSQL Latch 机制详解:从一行 SetLatch 到 epoll 的内核之旅
数据库🔥 871
18
pgbench 源码全解:一个 C 文件如何撑起 PostgreSQL 官方压测工具
数据库🔥 860
19
PostgreSQL libpq 机制与缓冲区详解
数据库🔥 850
20
PostgreSQL 逻辑复制 spill 文件深度剖析:从 `xid-*.spill` 到 TPC-C 的增长方程
数据库🔥 845