并行编程之RCU简单介绍

一、 背景知识

随着硬件晶体管的尺寸越来越小,CPU的频率上限基本保持在4G左右。这说明虽然摩尔定律仍然在晶体管密度方面有效,但是在提高单线程性能方面已经不再有效,也意味着编写单线程代码并简单的等待 CPUs 一两年时间不再是一个可行的方法。所有主流厂商最近的趋势是朝多核/多线程系统发展,并行是充分利用这些系统性能的好办法,以此让软件更能符合硬件的变化从而提高软件的性能。

并行编程领域是一个革新式的变化,同时也涉及众多的领域——它需要考虑硬件平台的实现方式、软件任务间的通信、任务同步等多种复杂的情况。

Linux中的RCU是并行编程的一个典型应用,下面我们通过RCU的简易实现来了解并行编程。

二、 RCU介绍

RCU全称read-copy-update,是一种同步机制。当多个线程同时读取和更新通过指针链接且属于共享数据结构(例如,链表、树,哈希表)时,避免使用锁。每当线程在共享内存中插入或删除数据结构的元素时,所有读者都可以保证看到并遍历旧结构或新结构,从而避免不一致(例如,取消引用空指针)。

它主要用在读取性能至关重要的场合,以更多空间为代价实现快速操作;这使得所有读者继续进行,就好像不涉及同步一样,因此它们会很快,但也会使更新更加困难。

三、名词解释

  • 宽限期:grace period,简写gp,这是一个等待期,以确保所有与执行删除的数据相关的 reader访问完毕。
  • 静止状态:Quiescent State,简写 qs,在任意时刻,一个特定的CPU只要看起来处于 阻塞状态、IDLE循环、或者离开了内核后,我们就知道所有RCU读端临界区已 经完成。这些状态被称为“静止状态”。

四、 RCU的基本执行过程

1、基本过程

  • 采用发布——订阅机制添加新的数据;
  • 等待已有的 RCU 读者退出临界区;
  • 允许在不影响或者延迟其他并发 RCU 读者的前提下改变数据;

2、基本思想

2.1 示例代码

q = kmalloc(sizeof(*p), GFP_KERNEL);
*q = *p;
q->b = 11;
list_replace_rcu(&p->list, &q->list);
synchronize_rcu();
kfree(p);
}

2.2 示意图
RCU基本流程示意图

1)先给指针q分配一个空间;
2)给指针q赋值;
3)将新生成的q指向p的下一个节点;
4)再将p的上一个节点指向q;
5)等宽限期结束后,在将p释放掉;

注:第三第四步的顺序不能反,否则会造成读端在读取数据的时候出现指针错误;

3、基本流程

3.1 流程
+ 读端通过rcu_read_lock()进入临界区;
+ 读端通过rcu_read_unlock()退出临界区;
+ 更新端通过rcu_assign_pointer()等发布原语发布数据;
+ 更新端通过synchronize_rcu()同步原语等待宽限期结束;

3.2 示例代码

  • 读端
rcu_read_lock();
p = rcu_dereference(head);
 ... ...
rcu_read_unlock();

该段代码通过rcu_read_lock()进入临界区,然后获取指针p,并对p进行一些列操作,最后通过rcu_read_unlock()退出临界区。

  • 更新端
spin_lock(&lock);
p = head;
rcu_assign_pointer(head, NULL);
spin_unlock(&lock);
synchronize_rcu();
kfree(p);

该段代码先通过rcu_assign_pointer()函数将head置为NULL,然后通过synchronize_rcu()等待宽限期结束。当synchronize_rcu()函数返回时,表示宽限期结束,所有的读端都已经退出临界区,此时可以释放p(原来head)指向的空间。

3.3 并发执行示意图
读端和更新端示意图

3.4 宽限期示意图
宽限期示意图

五、RCU特性

  • RCU读端和更新端可以同时操作,并发执行;
  • RCU采用数据担保的方式,为读端提供可靠数据;
  • 读端读到的数据有可能是旧的也可能是新的;
  • 宽限期结束时,读端要退出临界区;
  • 典型RCU读端在临界区内不允许中断、休眠等操作;

六、简易RCU实现

为了能让大家更好的理解并行编成的思想,下面我们通过几种RCU的实现来说明这点。RCU的宽限期开始和结束都是要看是否有读端在临界区内,所以RCU最重要的过程也就是如下3个:

  • 读端进入临界区
  • 读端退出临界区
  • 等待宽限期结束(同步方式)
    下面几个简单的实现来也重点从这三个方面来实现——这些例子不具有实用性,读端和更新端的调用见前面的示例代码部分。
  1. 基于spinlock的实现
DEFINE_SPINLOCK(rcu_gp_lock);

static void rcu_read_lock(void)
{
	spin_lock(&rcu_gp_lcok);
}

static void rcu_read_unlock(void)
{
	spin_unlock(&rcu_gp_lock);
}

static void synchronize_rcu(void)
{
	spin_lock(&rcu_gp_lock);
	spin_unlock(&rcu_gp_lock);
}

此例子中通过spinlock的方式来实现。

rcu_read_lock()函数和rcu_read_unlock()函数通过对spinlock上锁和解锁可以很好的指示读端当前是否处于临界区内;在synchronize_rcu()函数中通过检查spinlock是否上锁来判断是否有读端在临界区内,即宽限期是否结束。所以本例子很好的实现rcu_read_lock()函数、rcu_read_unlock()函数和synchronize_rcu()函数的语义。

但是spinlock具有强烈的排他性,所以每次只能有一个读端进入临界区,本例中的读端不具有并发性。

  1. 基于计数器的实现
atomic_t rcu_refcnt;

static void rcu_read_lock(void)
{
	atomic_inc(&rcu_refcnt);
	smp_mb();
}

static void rcu_read_unlock(void)
{
	smp_mb();
	atomic_dec(&rcu_refcnt);
}

static void synchronize_rcu(void)
{
	smp_mb();
	while(atomic_read(&rcu_refcnt) != 0) {
		poll(NULL, 0, 10);
	}
	smp_mb();
}

为了更好的提高读端的并发行,我们首先想到的是计数器——通过计数器的值来判断是否有所有的读端退出了临界区。

这里读端的计数器采用原子变量。首先原子操作的性能开销要远远小于锁的性能开销,其次原子操作也只是对计数器变量进行保护,所以读端可以并发执行。

  1. 基于线程变量的实现
static void rcu_read_lock(void)
{
    spin_lock(&__get_thread_var(rcu_gp_lock));
}

static void rcu_read_unlock(void)
{
    spin_unlock(&__get_thread_var(rcu_gp_lock));
}

static void synchronize_rcu(void)
{
	for_each_running_thread(t){
		spin_lock(&per_thread(rcu_gp_lock, t));
		spin_unlock(&per_thread(ruc_gp_lock, t));
	}
}

上面的使用原子计数器的方式已经可以大幅度的提高读端的并发性能,但是该原子计数器仍然是一个全局变量,是被所有的读端共享的一个变量;所以会随着并发数量的增加,对原子计数器的竞争机率也会增加——扩展性不足。

假如我们通过空间换性能的方式,将读端进入临界区的标记变成每个线程一个,这样在读端之间就不存在共享进入临界区标记的情况,从而提高了读端的并发性和扩展性。

七、Linux内核中RCU实现介绍

在Linux内核中RCU有多种实现方式:

  • 基于单核的tiny实现
  • 基于静止状态的实现
  • 基于任务的实现
  • 可睡眠的SRCU
  • … …

这些实现主要是跟踪宽限期的方式不同。下面主要对基于静止状态的RCU的宽限期跟踪实现的介绍。

经典RCU 读端临界区限制其中的内核代码不允许阻塞。这意味着在任意时刻,一个特定的CPU只要看起来处于阻塞状态、IDLE循环、或者离开了内核后,我们就知道所有RCU读端临界区已经完成——这些状态被称为“静止状态”;当每一个CPU已经经历过至少一次静止状态时,RCU宽限期结束。

在Linux中经典的RCU巧妙的利用了上述的原则:通过对静止状态的检测来减少读端的竞争。为了减少CPU上报时对数据锁的竞争,RCU采用了分级控制,如下图:
RCU静止状态更新

在一个rcu_node中,第一个上报静止状态的CPU会将数据保存在该层的节点中;只有另外一个CPU也上报了静止状态的时候,才会将该节点的两个CPU的静止状态同时上报的上一层的rcu_node中。这样做的好处是:在底层同一时间至多有两个CPU会竞争锁,在顶层锁的竞争也相对较少;这样大大减少了CPU相互竞争的机会。

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
RCU stall是指在RCU(Read-Copy-Update)机制下,由于某些情况导致系统性能下降或出现延迟的情况。RCU stall的机制涉及到RCU的核心原理和一些潜在问题。下面是对RCU stall机制的详细介绍RCU机制是一种用于实现并发读写数据结构的技术,它的核心思想是允许多个线程同时读取数据,而不需要加锁或阻塞其他线程。它通过复制数据结构并使用多版本并发控制来实现读操作的并发性。 在RCU机制中,读操作是无锁的,因为每个版本都是一个瞬时快照,读取线程可以自由地访问数据结构。但是,写操作需要进行一些额外的处理。 当一个线程进行写操作时,它会创建一个新的数据版本,并将更新后的数据写入新版本中。然后,它将原来的数据版本标记为废弃,并等待所有正在进行读操作的线程完成后,才会释放废弃版本的内存资源。这个等待过程称为“Grace Period”。 然而,有些情况下可能会引发RCU stall: 1. 长时间的更新操作:如果写操作非常耗时,那么正在进行读操作的线程需要等待更新操作完成,从而导致RCU stall。 2. 读写冲突:当一个线程正在进行读操作时,如果另一个线程正在进行写操作,读操作线程需要等待写操作完成才能继续进行读操作,也可能导致RCU stall。 3. 延迟回收:在RCU机制中,内存资源的释放是通过延迟回收来实现的。如果回收操作被延迟,可能会导致内存占用过高,进而影响系统性能。 4. 内存压力:当系统内存资源不足,无法及时回收废弃版本的内存时,也可能导致RCU stall。 为了解决或减轻RCU stall问题,可以采取以下措施: 1. 合理设计数据结构:避免读写冲突,减少RCU stall的发生。 2. 控制更新频率:合理控制数据更新的频率,避免过于频繁的更新操作。 3. 使用适当的同步机制:在RCU机制中,可以使用一些同步机制来避免读写冲突,如读写锁、自旋锁等。 4. 增加系统资源:如果系统资源不足导致RCU stall,可以适当增加系统资源,如内存、处理器等。 5. 优化写操作的性能:减少写操作的耗时,避免长时间的更新操作。 总而言之,RCU stall是在RCU机制下由于长时间的更新操作、读写冲突、延迟回收和内存压力等因素导致的系统性能下降或延迟的情况。通过合理设计数据结构、控制更新频率、使用适当的同步机制以及增加系统资源等措施,可以减轻RCU stall的发生。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值