由 RCU 保护的列表/数组元素的引用计数设计¶
请注意,如果需要将引用计数和 RCU 结合使用,percpu-ref 功能很可能是您的首选。请参阅 include/linux/percpu-refcount.h 获取更多信息。然而,在那些 percpu-ref 会消耗过多内存的特殊情况下,请继续阅读。
对于受传统读者/写者自旋锁或信号量保护的列表中的元素进行引用计数很简单
代码清单 A
1. 2.
add() search_and_reference()
{ {
alloc_object read_lock(&list_lock);
... search_for_element
atomic_set(&el->rc, 1); atomic_inc(&el->rc);
write_lock(&list_lock); ...
add_element read_unlock(&list_lock);
... ...
write_unlock(&list_lock); }
}
3. 4.
release_referenced() delete()
{ {
... write_lock(&list_lock);
if(atomic_dec_and_test(&el->rc)) ...
kfree(el);
... remove_element
} write_unlock(&list_lock);
...
if (atomic_dec_and_test(&el->rc))
kfree(el);
...
}
如果使用 RCU 使此列表/数组无锁,如将 add() 和 delete() 中的 write_lock() 更改为 spin_lock(),并将 search_and_reference() 中的 read_lock() 更改为 rcu_read_lock()
,则 search_and_reference() 中的 atomic_inc()
可能会持有对已从列表/数组中删除的元素的引用。在这种情况下,请使用 atomic_inc_not_zero()
,如下所示
代码清单 B
1. 2.
add() search_and_reference()
{ {
alloc_object rcu_read_lock();
... search_for_element
atomic_set(&el->rc, 1); if (!atomic_inc_not_zero(&el->rc)) {
spin_lock(&list_lock); rcu_read_unlock();
return FAIL;
add_element }
... ...
spin_unlock(&list_lock); rcu_read_unlock();
} }
3. 4.
release_referenced() delete()
{ {
... spin_lock(&list_lock);
if (atomic_dec_and_test(&el->rc)) ...
call_rcu(&el->head, el_free); remove_element
... spin_unlock(&list_lock);
} ...
if (atomic_dec_and_test(&el->rc))
call_rcu(&el->head, el_free);
...
}
有时,需要在更新(写入)流中获取对元素的引用。在这种情况下,atomic_inc_not_zero()
可能有点过分,因为我们持有更新端的自旋锁。在这种情况下,可以使用 atomic_inc()
。
在 search_and_reference() 代码路径中处理“FAIL”并不总是方便的。在这种情况下,可以将 atomic_dec_and_test()
从 delete() 移动到 el_free(),如下所示
代码清单 C
1. 2.
add() search_and_reference()
{ {
alloc_object rcu_read_lock();
... search_for_element
atomic_set(&el->rc, 1); atomic_inc(&el->rc);
spin_lock(&list_lock); ...
add_element rcu_read_unlock();
... }
spin_unlock(&list_lock); 4.
} delete()
3. {
release_referenced() spin_lock(&list_lock);
{ ...
... remove_element
if (atomic_dec_and_test(&el->rc)) spin_unlock(&list_lock);
kfree(el); ...
... call_rcu(&el->head, el_free);
} ...
5. }
void el_free(struct rcu_head *rhp)
{
release_referenced();
}
关键点在于,add() 添加的初始引用在删除后经过宽限期后才会被删除。这意味着 search_and_reference() 无法找到此元素,这意味着 el->rc 的值无法增加。因此,一旦它达到零,就没有读者可以或将能够引用该元素。因此,可以安全地释放该元素。这反过来保证,如果任何读者找到该元素,该读者可以安全地获取引用,而无需检查引用计数器的值。
与清单 B 中的模式相比,清单 C 中基于 RCU 的模式的明显优势在于,任何调用 search_and_reference() 来定位给定对象的调用都将成功获取对该对象的引用,即使同时调用 delete() 来删除该同一对象。类似地,与清单 A 相比,清单 B 和 C 的一个明显优势在于,即使有大量调用 search_and_reference() 来搜索调用 delete() 的同一对象,对 delete() 的调用也不会被延迟。相反,所有延迟的只是 kfree()
的最终调用,这在现代计算机系统中通常不是问题,即使是小型计算机系统。
在 delete() 可以休眠的情况下,可以从 delete() 调用 synchronize_rcu()
,以便将 el_free() 并入 delete,如下所示
4.
delete()
{
spin_lock(&list_lock);
...
remove_element
spin_unlock(&list_lock);
...
synchronize_rcu();
if (atomic_dec_and_test(&el->rc))
kfree(el);
...
}
作为内核中的其他示例,struct pid 的引用计数使用清单 C 中的模式,而 struct posix_acl 使用清单 B 中的模式。