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