《深入浅出DPDK》读书笔记(五):同步互斥机制

本文内容为读书笔记,摘自《深入浅出DPDK》


56.原子操作

原子(atom)本意是“不能被进一步分割的最小粒子”,而原子操作(atomic operation)意为“不可被中断的一个或一系列操作”。对原子操作的简单描述就是:多个线程执行一个操作时,其中任何一个线程要么完全执行完此操作,要么没有执行此操作的任何步骤,那么这个操作就是原子的。原子操作是其他内核同步方法的基石。


57.在单处理器系统(UniProcessor)中,能够在单条指令中完成的操作都可以认为是“原子操作”。


58.CMPXCHG这条指令,它的语义是比较并交换操作数(CAS, Compare And Set)。而用XCHG类的指令做内存操作,处理器会自动地遵循LOCK的语义,可见该指令是一条原子的CAS单指令操作。


59.DPDK原子操作实现和应用

在理解原子操作在DPDK的实现之前,建议读者仔细阅读并且能够理解第2章的内容,那部分是我们理解内存操作的基础,因为原子操作的最终反映也是对内存资源的操作。

原子操作在DPDK代码中的定义都在rte_atomic.h文件中,主要包含两部分:内存屏蔽和原16、32和64位的原子操作API。

1. 内存屏障API

  • rte_mb():内存屏障读写API
  • rte_wmb():内存屏障写API
  • rte_rmb():内存屏障读API

这三个API的实现在DPDK代码中没有什么区别,都是直接调用__sync_synchronize(),而__sync_synchronize()函数对应着MFENCE这个序列化加载与存储操作汇编指令。

我们在这里给出一个内存屏障的应用在DPDK中的实例,在virtio_dev_rx()函数中,在读取avail->flags之前,加入内存屏障API以防止乱序的执行。

*(volatile uint16_t *)&vq->used->idx += count; 
vq->last_used_idx = res_end_idx; 
/* flush used->idx update before we read avail->flags. */ 
rte_mb(); 
/* Kick the guest if necessary. */ 
if (! (vq->avail->flags & VRING_AVAIL_F_NO_INTERRUPT)) 
      eventfd_write(vq->callfd, (eventfd_t)1); 

2. 原子操作API

DPDK代码中提供了16、32和64位原子操作的API,以rte_atomic64_add() API源代码为例,讲解一下DPDK中原子操作的实现,其代码如下:

static inline void 
rte_atomic64_add(rte_atomic64_t *v, int64_t inc) 
{ 
    int success = 0;3 
    uint64_t tmp; 
    while (success == 0) { 
        tmp = v->cnt; 
        success = rte_atomic64_cmpset((volatile uint64_t *)&v->cnt, 
                                            tmp, tmp + inc); 
    } 
} 

我们可以看到这个API中主要是使用了比较和交换的原子操作API,关于比较和交换指令的原理我们已经在前面解释了,这里我们只是来看DPDK是如何嵌入汇编指令来使用它的。

rte_atomic64_cmpset(volatile uint64_t *dst, uint64_t exp, uint64_t src) 
{ 
    uint8_t res; 
    asm volatile( 
            MPLOCKED 
            "cmpxchgq %[src], %[dst]; " 
            "sete %[res]; " 
            :[res] "=a" (res), 
              [dst] "=m" (*dst) 
            :[src] "r" (src), 
              "a" (exp), 
              "m" (*dst) 
            :"memory"); 
    return res; 
} 

在VXLAN例子代码中,使用了64位的原子操作API来进行校验码和错误包的统计;这样,在多核系统中,加上原子操作的数据包统计才准确无误。

int 
vxlan_rx_pkts(struct virtio_net *dev, struct rte_mbuf **pkts_burst, 
      uint32_t rx_count) 
{ 
  uint32_t i = 0; 
  uint32_t count = 0; 
  int ret; 
  struct rte_mbuf *pkts_valid[rx_count]; 
  for (i = 0; i < rx_count; i++) { 
      if (enable_stats) { 
          rte_atomic64_add( 
              &dev_statistics[dev->device_fh].rx_bad_ip_csum, 
              (pkts_burst[i]->ol_flags & PKT_RX_IP_CKSUM_BAD) 
              != 0); 
          rte_atomic64_add( 
              &dev_statistics[dev->device_fh].rx_bad_ip_csum, 
              (pkts_burst[i]->ol_flags & PKT_RX_L4_CKSUM_BAD) 
              != 0); 
      } 
      ret = vxlan_rx_process(pkts_burst[i]); 
      if (unlikely(ret < 0)) 
          continue; 
      pkts_valid[count] = pkts_burst[i]; 
          count++; 
  } 
  ret = rte_vhost_enqueue_burst(dev, VIRTIO_RXQ, pkts_valid, count); 
  return ret; 
} 

60.DPDK读写锁实现和应用

DPDK读写锁的定义在rte_rwlock.h文件中,

  • rte_rwlock_init(rte_rwlock_t *rwl):初始化读写锁到unlocked状态。
  • rte_rwlock_read_lock(rte_rwlock_t *rwl):尝试获取读锁直到锁被占用。
  • rte_rwlock_read_unlock(rte_rwlock_t *rwl):释放读锁。
  • rte_rwlock_write_lock(rte_rwlock_t *rwl):获取写锁。
  • rte_rwlock_write_unlock(rte_rwlock_t *rwl):释放写锁。

读写锁在DPDK中主要应用在下面几个地方,对操作的对象进行保护。

  • ❑在查找空闲的memory segment的时候,使用读写锁来保护memseg结构。LPM表创建、查找和释放。
  • ❑Memory ring的创建、查找和释放。
  • ❑ACL表的创建、查找和释放。
  • ❑Memzone的创建、查找和释放等。

下面是查找空闲的memory segment的时候,使用读写锁来保护memseg结构的代码实例。

/* 
  * Lookup for the memzone identified by the given name 
  */ 
const struct rte_memzone * 
rte_memzone_lookup(const char *name) 
{ 
        struct rte_mem_config *mcfg; 
        const struct rte_memzone *memzone = NULL; 
        mcfg = rte_eal_get_configuration()->mem_config; 
        rte_rwlock_read_lock(&mcfg->mlock); 
        memzone = memzone_lookup_thread_unsafe(name); 
        rte_rwlock_read_unlock(&mcfg->mlock); 
        return memzone; 
} 

61.自旋锁的缺点

自旋锁必须基于CPU的数据总线锁定,它通过读取一个内存单元(spinlock_t)来判断这个自旋锁是否已经被别的CPU锁住。

  • 1)自旋锁一直占用CPU,它在未获得锁的情况下,一直运行——自旋,所以占用着CPU,如果不能在很短的时间内获得锁,这无疑会使CPU效率降低。
  • 2)在用自旋锁时有可能造成死锁,当递归调用时有可能造成死锁,调用有些其他函数(如copy_to_user()、copy_from_user()、kmalloc()等)也可能造成死锁。

自旋锁使用时有两点需要注意:

  • 1)自旋锁是不可递归的,递归地请求同一个自旋锁会造成死锁。
  • 2)线程获取自旋锁之前,要禁止当前处理器上的中断。(防止获取锁的线程和中断形成竞争条件)

62.DPDK自旋锁实现和应用

DPDK中自旋锁API的定义在rte_spinlock.h文件中,其中下面三个API被广泛的应用在告警、日志、中断机制、内存共享和link bonding的代码中,用于临界资源的保护。

rte_spinlock_init(rte_spinlock_t *sl); 
rte_spinlock_lock(rte_spinlock_t *sl); 
rte_spinlock_unlock (rte_spinlock_t *sl); 

其中rte_spinlock_t定义如下,简洁并且简单。

/** 
  * The rte_spinlock_t type. 
  */ 
typedef struct { 
    volatile int locked; /**< lock status 0 = unlocked, 1 = locked */ 
} rte_spinlock_t; 

下面的代码是DPDK中的vm_power_manager应用程序中的set_channel_status_all()函数,在自旋锁临界区更新了channel的状态和变化的channel的数量,这种保护在像DPDK这种支持多核的应用中是非常必要的。

int 
set_channel_status_all(const char *vm_name, enum channel_status status) 
{ 
    … 
    rte_spinlock_lock(&(vm_info->config_spinlock)); 
    mask = vm_info->channel_mask; 
    ITERATIVE_BITMASK_CHECK_64(mask, i) { 
        vm_info->channels[i]->status = status; 
        num_channels_changed++; 
    } 
    rte_spinlock_unlock(&(vm_info->config_spinlock)); 
    return num_channels_changed; 
} 

63.无锁机制

Linux内核无锁环形缓冲

环形缓冲区通常有一个读指针和一个写指针。读指针指向环形缓冲区中可读的数据,写指针指向环形缓冲区中可写的数据。

在Linux内核代码中,kfifo就是采用无锁环形缓冲的实现,kfifo是一种“First In First Out”数据结构,它采用了前面提到的环形缓冲区来实现,提供一个无边界的字节流服务。


64.DPDK无锁环形缓冲

DPDK ring库:环形缓冲区的解剖

基于无锁环形缓冲的的原理,Intel DPDK提供了一套无锁环形缓冲区队列管理代码,支持单生产者产品入列,单消费者产品出列;多名生产者产品入列,多名消费者出列操作。


系列文章

《深入浅出DPDK》读书笔记(一):基础部分知识点

《深入浅出DPDK》读书笔记(二):网卡的读写数据操作

《深入浅出DPDK》读书笔记(三):NUMA - Non Uniform Memory Architecture 非统一内存架构

《深入浅出DPDK》读书笔记(四):并行计算-SIMD是Single-Instruction Multiple-Data(单指令多数据)

 

已标记关键词 清除标记
相关推荐
©️2020 CSDN 皮肤主题: 酷酷鲨 设计师:CSDN官方博客 返回首页