图解|RCU原理
Linux 内核设计了多种锁机制,比如
读写锁
、自旋锁
和 信号量
等。为什么要设计这么多锁机制呢?这是因为不同的锁机制适用于不同的场景,比如 读写锁
适用于读多写少的场景;而 信号量
适用于进程长时间占用锁,并且允许上下文切换的场景。本文主要介绍一种 Linux 内核中性能非常高的锁机制:
RCU锁机制
。RCU
是 Read Copy Update
的缩写,中文意思是 读取
、复制
、更新
。RCU锁机制 就是通过读取、复制和更新这三个操作来实现锁功能。在介绍 RCU锁
之前,我们先来看看下面的实例。structfoo {
int a;
char b;
long c;
};
structfoo *gbl_foo;
voidfoo_read(void)
{
foo *fp = gbl_foo;
if (fp != NULL)
do_something(fp->a, fp->b, fp->c);
}
voidfoo_update(foo *new_fp)
{
foo *old_fp = gbl_foo;
gbl_foo = new_fp;
free(old_fp);
}
假如有线程 A 和线程 B 同时执行
foo_read()
,而另线程 C 执行 foo_update()
,那么会出现以下几种情况:线程 A 和线程 B 同时读取到旧的 gbl_foo 的指针。 线程 A 和线程 B 同时读取到新的 gbl_foo 的指针。 线程 A 和线程 B 有一个读取到新的 gbl_foo 的指针,另外一个读取到旧的 gbl_foo 的指针。
如果线程 A 或线程 B 在读取旧的 gbl_foo 数据还没完成时,线程 C 释放了旧的 gbl_foo 指针,那么将会导致程序奔溃。
也就是说,在不加锁的情况下,对公共数据的访问是危险的。当然,我们可以使用
读写锁
、信号量
或者 自旋锁
来对公共数据进行保护。但这些锁都有各自的弊端,比如:读写锁
:对于写操作较多的场景,性能会非常差。信号量
:上锁失败的进程将会切换上下文,从而导致系统的性能下降。自旋锁
:获得锁的 CPU 将会阻塞其他 CPU 的允许,从而导致系统的并行能力下降。
那么有没有一种锁机制,对系统的性能影响不大的呢?所以,Linux 内核黑客们就创造出
RCU锁
。RCU锁原理
如果能够保证所有使用某个公共数据的线程不再使用它,那么就可以安全删除此公共数据。
1. 宽限期
在上面的例子中,如果能够保证线程 A 和线程 B 不再使用旧数据,那么线程 C 就能安全删除旧数据。
如下图所示(旧数据对应对象A,新数据对应对象B):
从上图的时间线可以看出,线程 A 和线程 B 从 glb_foo 指针获取的都是对象 A 的引用。
提示:因为 glb_foo 指针在时间点 B 才被替换成对象 B,而线程 A 和线程 B 都是在时间点 B 前获取 glb_foo 指针指向的对象,所以它们获取到的都是对象 A 的引用。
而在
安全点
后,线程 A 和线程 B 便不再使用旧数据(对象A)。所以此时,线程 C 便可以安全释放旧数据(对象A)。线程 A 和线程 B 使用旧数据的这段期间,被称为
宽限期
。如下图所示:所以,
RCU锁
的核心思想就是怎么确定 宽限期
。因为确定宽限期后,就可以随心所欲地释放旧数据。2. 宽限期确认
RCU锁
的原理虽然比较简单,但是实现却有点小复杂,主要是因为 宽限期
的确定比较麻烦。为了能够确认
宽限期
,使用 RCU 锁时有以下限制:使用 RCU 锁前,必须禁止内核抢占。 在 RCU 锁保护的临界区中,不能使用可能触发调度的函数(如不能调用 alloc_pages 函数)。
由于在 RCU 临界区是禁止调度的,所以如果 CPU 发生了调度,就可以确定当前线程已经退出了临界区(也就是说当前线程不再引用旧对象)。如果所有的 CPU 都至少发生过一次调度,那么也就说明没有任何线程引用旧对象,此时就可以安全释放旧对象了。
所以,RCU 锁的核心原理是:在释放旧对象前,必须等待所有 CPU 核心至少调度一次。如下代码所示:
voidfoo_update(struct foo *new_fp)
{
// 1. 将 gbl_foo 指向新对象
spin_lock(&foo_mutex);
foo *old_fp = gbl_foo;
gbl_foo = new_fp;
spin_unlock(&foo_mutex);
// 2. 等待所有 CPU 核心至少调度一次
synchronize_kernel();
// 3. 释放旧对象
free(old_fp);
}
foo_update()
函数释放旧对象的步骤如下:使用新对象替换旧对象,在替换前必须使用自旋锁进行保护,避免多个 CPU 同时修改 gbl_foo 指针的值。 等待所有 CPU 核心至少调度一次。 由于所有 CPU 核心都至少调度过一次,那么可以确认现在没有线程引用旧对象,所以可以安全释放旧对象。
3. RCU临界区
通过前面的分析可知,在 RCU 临界区中是不能发生调度的。要保证临界区不发生调度,首先要确保在临界区中不能调用可能触发调度的函数,如:
alloc_pages()
。这点需要 RCU 使用者自己保证。另外一点要保证的是,内核不能发生抢占,这点可以通过调用
preempt_disable()
函数实现。内核定义了一个名为 rcu_read_lock()
的宏,如下所示:#define rcu_read_lock() preempt_disable()
可以看出,
rcu_read_lock()
宏其实就是 preempt_disable()
函数的别名。所以,使用 RCU 锁时,可以使用 rcu_read_lock()
宏对临界区进行保护。当退出临界区时,需要调用
rcu_read_unlock()
把内核抢占打开。rcu_read_unlock()
的定义如下:#define rcu_read_unlock() preempt_enable()
可以看出,
rcu_read_unlock()
宏就是 preempt_enable()
的别名。所以,当我们使用 RCU 锁对临界区进行保护时,必须将需要保护的代码放置在
rcu_read_lock()
和 rcu_read_unlock()
之间,如下所示:voidfoo_read(void)
{
// 1. 保护临界区
rcu_read_lock();
foo *fp = gbl_foo;
if (fp != NULL)
do_something(fp->a, fp->b, fp->c);
// 2. 退出临界区
rcu_read_unlock();
}
最新评论
推荐文章
作者最新文章
你可能感兴趣的文章
Copyright Disclaimer: The copyright of contents (including texts, images, videos and audios) posted above belong to the User who shared or the third-party website which the User shared from. If you found your copyright have been infringed, please send a DMCA takedown notice to [email protected]. For more detail of the source, please click on the button "Read Original Post" below. For other communications, please send to [email protected].
版权声明:以上内容为用户推荐收藏至CareerEngine平台,其内容(含文字、图片、视频、音频等)及知识版权均属用户或用户转发自的第三方网站,如涉嫌侵权,请通知[email protected]进行信息删除。如需查看信息来源,请点击“查看原文”。如需洽谈其它事宜,请联系[email protected]。
版权声明:以上内容为用户推荐收藏至CareerEngine平台,其内容(含文字、图片、视频、音频等)及知识版权均属用户或用户转发自的第三方网站,如涉嫌侵权,请通知[email protected]进行信息删除。如需查看信息来源,请点击“查看原文”。如需洽谈其它事宜,请联系[email protected]。