本文内容为读书笔记,摘自《深入浅出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无锁环形缓冲
基于无锁环形缓冲的的原理,Intel DPDK提供了一套无锁环形缓冲区队列管理代码,支持单生产者产品入列,单消费者产品出列;多名生产者产品入列,多名消费者出列操作。
系列文章
《《深入浅出DPDK》读书笔记(三):NUMA - Non Uniform Memory Architecture 非统一内存架构》
《《深入浅出DPDK》读书笔记(四):并行计算-SIMD是Single-Instruction Multiple-Data(单指令多数据)》