RCU安全引用计数

本文介绍了Linux内核中的kref引用计数机制,用于管理对象生命周期,防止竞态条件。Ravikiran的补丁引入refcount_t锁免费机制,使用cmpxchg函数提高并发性能。tiobench测试显示性能提升。同时,文章讨论了RCU和锁机制在优化中的作用。
摘要由CSDN通过智能技术生成

原文网址:https://lwn.net/Articles/93617

原文作者:Corbet

原文时间:2004年7月14日

内核提供了一种用于实现引用计数的简单机制kref;该机制是今年3月份完成的。kref机制的核心思想是,提供支持原子操作的计数器,用于对未决引用【outstanding references】进行计数。如果计数器数值为零,内核不再需要引用对象了,引用对象可以被释放掉。

kref机制的函数很简单,在引用对象数据结构内直接包含一个struct kref计数器或struct kref *计数器指针,在引用对象被操作之前调用kref_get函数,引用计数器递增。

struct kref *kref_get(struct kref *kref)
{
	
    WARN_ON(!atomic_read(&kref->refcount));
	atomic_inc(&kref->refcount);
	return kref;
}

在对对象操作完成之后调用kref_put函数,引用计数器递减,如果计数器数值为零,就调用回调函数释放引用对象相关资源。

void kref_put(struct kref *kref)
{
	if (atomic_dec_and_test(&kref->refcount)) {
	    kref->release(kref);   //release函数是回调函数
	}
}

对引用计数refcount域进行原子操作,使得上述两个函数可以安全地在多CPU或抢断环境下直接调用,也就是说在这两个环境下,引用计数器的数值总能获得正确的结果。但是,如果两个内核线程在使用kref机制时,存在下面情况,kref机制也会出错。

内核线程1内核线程2
/* In kref_get() */
WARN_ON(!atomic_read(&kref->refcount));
kref_put(&kref);
atomic_inc(&kref->refcount);
return kref;

在上面的例子中,内核线程1在调用atomic_inc之前的那一刻,被引用对象的相关资源很可能被释放掉了。kref代码强制要求:对同一个引用对象不允许kref_get和kref_put并行运行。也就是说,这种强制性要求上述两个函数都需要用锁来避免对同一个引用对象的并行访问。

但是关注高可扩展性的程序员经常会使用免锁算法。因为在线程数量比较大的时候,锁往往会成为性能瓶颈,因此尽可能不用锁,内核的可扩展性会更好。这也是内核提供seqlock和RCU这两种技术的原因。kref机制对锁机制的需求,使得seqlock和RCU很难派上用途。

Ravikiran G Thirumalai最近提交了一份题为“Refcounting of objects part of a lockfree collection”的补丁,实现了一个新的锁机制refcount_t,用于对象的免锁管理。并用大量篇幅介绍了和RCU一起工作时引用计数过程,所有补丁构建了一种类似kref的数据类型,这种数据类型不需要用锁就能避免前面提到的竞争问题。

伴随并行写的过程【as currently written】,kref_get首先检查引用计数数值;如果计数数值为零,表示对象已经被释放了。当前的实现是,检查到数值为零时,仅仅是抱怨一下【我理解为信息输出,而不做更多的处理】;有人可能要说了,这种情况下应该做进一步的处理才好。然而,真正的问题是,对引用计数的测试和递增如果不能在一个原子操作中实现,那么在这两个操作之间就有可能插入其他操作。Ravikiran的补丁通过提供另一个XXXX_get函数来解决这个问题:

 static inline int refcount_get_rcu(refcount_t *rc)
 {
	int c, old;
	c = atomic_read(&rc->count);
	while ( c && (old = cmpxchg(&rc->count.counter, c, c+1)) != c) 
		c = old;
	return c;
 }

上面函数的核心是cmpxchg函数,这是一个内联汇编函数,可以直接使用CPU的cmpxchg指令。这个函数的原型是:

int cmpxchg(int *location, int old, int new);

cmpxchg函数实现了以下基本功能:

1)用原子操作实现:比较location内存单元数值和old变量数值;如果两者数值相等,将location内存单元设置为new变量数值。

2)如果上述原子操作成功,即判断两者数值相等后location内存单元被修改,cmpxchg函数返回old变量数值;如果上述原子操作不成功,cmpxchg返回location内存单元的数值。

cmpxchg指令是CPU提供的测试-设置原子指令。用cmpxchg实现的XXXX_get函数在不用锁的情况下就可以实现引用计数器的获取。

这里还是有点小问题。考虑一种情况:内核线程2对引用计数对象释放后又重新使用该对象,然后内核线程1才试图去获取引用计数。在这种情况下,内核线程1可能看到的是一个随机的引用计数,就误以为成功获取了引用计数。引入RCU机制,可以避免这种情况发生。引用对象的释放是通过RCU回调函数来实现;这样一来,引用对象就不会被真正释放直到每一个处理器都发生了调度。只要内核线程能通过指针找到引用对象,那么这个对象就一直存在,即使对象的引用计数数值为零。经过一个完整静默期,没有内核线程去访问这样的指针了,引用对象才会被安全地删除。

另一个潜在的问题是,并不是所有的体系结构都提供cmpxchg原子指令。针对这样的系统,Ravikiran用到了一个从未见过但相当巧妙的方案,用到了自旋锁的哈希数组;如果你们好奇就自己去看补丁好了。

这些努力都是值得的;这个技术已经用于文件描述符查找了,tiobench测试性能提高了13% ~ 21%。内核系统里还有类似kref API一样的对象,也有创建新的引用计数API。因此,补丁还可能会重写。

当一个更新操作阻塞了RCU的快速路径,使得其他读操作无法顺利进行时,也会引发RCU stall。下面将详细讲解这种情况的工作原理和可能的影响: 1. RCU的快速路径: - RCU的快速路径是指读操作在没有锁竞争的情况下可以并行地访问共享数据的路径。在快速路径中,读操作不会被阻塞,不需要获取锁,也不需要等待写操作的完成。 - 快速路径中的读操作通过增加引用计数来保证对共享数据的访问安全。当一个线程进行读操作时,它会增加共享数据的引用计数,并在读操作完成后减少引用计数。 2. 更新操作阻塞快速路径: - 当一个更新操作需要修改共享数据时,它必须进入RCU的保护区域,这个保护区域称为临界区。在临界区中,更新操作需要获取锁或者进行其他同步操作来确保数据的一致性。 - 如果一个更新操作阻塞了RCU的快速路径,那么其他正在进行的读操作将无法继续进行,因为它们无法获得所需的数据。 3. 引发RCU stall的影响: - 当一个更新操作阻塞了RCU的快速路径时,其他读操作将被长时间阻塞,导致RCU stall的发生。 - RCU stall会导致读操作的延迟,从而降低系统的性能和响应时间。 - 如果有大量的读操作被阻塞,可能会导致系统出现僵死状态,无法继续正常工作。 4. 解决RCU stall的方法: - 内核提供了一些机制来解决RCU stall的问题。例如,通过使用RCU回调函数处理机制,可以将某些需要延迟执行的任务放入回调函数中,以减轻RCU stall的影响。 - 另外,内核还会定期检测是否发生了长时间的阻塞,并尝试唤醒被阻塞的CPU,以解决RCU stall的问题。 总之,当一个更新操作阻塞了RCU的快速路径时,其他读操作将被长时间阻塞,导致RCU stall的发生。这会降低系统的性能和响应时间,并有可能导致系统僵死。内核通过使用RCU回调函数处理机制和RCU stall检测机制来解决这个问题,以确保系统的正常运行和高效并发访问共享数据。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值