由 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 中的模式。