由 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 的值不会增加。 因此,一旦它达到零,就没有读者能够或将能够引用该元素。 因此,可以安全地释放该元素。 这反过来又保证了如果任何读者找到该元素,该读者可以安全地获取引用,而无需检查引用计数器的值。
在清单 C 中基于 RCU 的模式优于清单 B 中的模式的一个明显优势是,即使同时调用 delete() 来删除同一对象,任何对 search_and_reference() 的调用,只要找到给定对象,都将成功获取对该对象的引用。 类似地,清单 B 和 C 都优于清单 A 的一个明显优势是,即使有任意数量的 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 中的模式。