为内核对象添加引用计数器(krefs)【ChatGPT】

发布时间 2023-12-09 20:10:01作者: 摩斯电码

为内核对象添加引用计数器(krefs)

作者 Corey Minyard minyard@acm.org
作者 Thomas Hellstrom thellstrom@vmware.com

其中很多内容都是从Greg Kroah-Hartman的2004年OLS论文和关于krefs的演示中借鉴而来的,可以在以下链接找到:

介绍

krefs允许您为对象添加引用计数器。如果您的对象在多个地方使用并传递,并且没有引用计数,那么您的代码几乎肯定是有问题的。如果您想要引用计数,krefs是一种不错的选择。

要使用kref,请在数据结构中添加一个,如下所示:

struct my_data
{
    .
    .
    struct kref refcount;
    .
    .
};

kref可以出现在数据结构的任何位置。

初始化

在分配kref后,必须对其进行初始化。为此,请调用kref_init,如下所示:

struct my_data *data;

data = kmalloc(sizeof(*data), GFP_KERNEL);
if (!data)
       return -ENOMEM;
kref_init(&data->refcount);

这将将kref中的refcount设置为1。

Kref规则

一旦您有了初始化的kref,您必须遵循以下规则:

  1. 如果您复制指针的非临时副本,特别是如果它可以传递给另一个执行线程,则必须在传递之前使用kref_get()增加引用计数:

    kref_get(&data->refcount);
    

    如果您已经有一个指向具有kref的结构的有效指针(refcount不能为零),则可以在没有锁的情况下执行此操作。

  2. 当您使用完指针后,必须调用kref_put():

    kref_put(&data->refcount, data_release);
    

    如果这是指针的最后一个引用,则将调用释放例程。如果代码从不尝试在没有已经持有有效指针的情况下获取对kref结构的有效指针,那么可以在没有锁的情况下执行此操作。

  3. 如果代码尝试在没有已经持有有效指针的情况下获取对kref结构的引用,则必须对kref_get()期间无法发生kref_put()的访问进行串行化,并且在kref_get()期间结构必须保持有效。

例如,如果您分配了一些数据,然后将其传递给另一个线程进行处理:

void data_release(struct kref *ref)
{
    struct my_data *data = container_of(ref, struct my_data, refcount);
    kfree(data);
}

void more_data_handling(void *cb_data)
{
    struct my_data *data = cb_data;
    .
    . 在此处处理数据
    .
    kref_put(&data->refcount, data_release);
}

int my_data_handler(void)
{
    int rv = 0;
    struct my_data *data;
    struct task_struct *task;
    data = kmalloc(sizeof(*data), GFP_KERNEL);
    if (!data)
            return -ENOMEM;
    kref_init(&data->refcount);

    kref_get(&data->refcount);
    task = kthread_run(more_data_handling, data, "more_data_handling");
    if (task == ERR_PTR(-ENOMEM)) {
            rv = -ENOMEM;
            kref_put(&data->refcount, data_release);
            goto out;
    }

    .
    . 在此处处理数据
    .
out:
    kref_put(&data->refcount, data_release);
    return rv;
}

这样,两个线程处理数据的顺序无关紧要,kref_put()负责知道何时不再引用数据并释放它。由于我们已经拥有一个拥有引用计数的有效指针,因此kref_get()不需要锁。由于没有任何尝试在没有已经持有指针的情况下获取数据的操作,因此put不需要锁。

在上面的示例中,无论成功还是错误路径,kref_put()都会被调用2次。这是必要的,因为kref_init()和kref_get()都会增加引用计数2次。

请注意,规则1中的“before”非常重要。您不应该做以下操作:

task = kthread_run(more_data_handling, data, "more_data_handling");
if (task == ERR_PTR(-ENOMEM)) {
        rv = -ENOMEM;
        goto out;
} else
        /* BAD BAD BAD - get is after the handoff */
        kref_get(&data->refcount);

不要假设您知道自己在做什么并使用上述结构。首先,您可能不知道自己在做什么。其次,您可能知道自己在做什么(在涉及锁定的某些情况下,上述操作可能是合法的),但是其他人可能不知道自己在做什么并更改代码或复制代码。这是不好的风格,请不要这样做。

在某些情况下,您可以优化获取和释放操作。例如,如果您已经完成了一个对象并将其排队给其他对象或传递给其他对象,那么无需进行获取和释放操作:

/* Silly extra get and put */
kref_get(&obj->ref);
enqueue(obj);
kref_put(&obj->ref, obj_cleanup);

只需进行排队操作。对此进行注释总是受欢迎的:

enqueue(obj);
/* 我们已经完成了obj,所以我们将我们的引用计数传递给队列。在此之后不要再操作obj! */

最后一条规则(规则3)是最棘手的。例如,假设您有一个包含多个kref的项目列表,并且希望获取第一个项目。您不能只是从列表中取出第一个项目并对其进行kref_get()。这违反了规则3,因为您没有已经持有有效指针。您必须添加互斥锁(或其他锁)。例如:

最后一个规则(规则3)是最难处理的。比如说,你有一个每个都被kref引用的项目列表,你希望获取第一个项目。你不能只是从列表中取出第一个项目并kref_get()它。这违反了规则3,因为你没有持有有效的指针。你必须添加一个互斥锁(或其他锁)。例如:

static DEFINE_MUTEX(mutex);
static LIST_HEAD(q);
struct my_data
{
        struct kref      refcount;
        struct list_head link;
};

static struct my_data *get_entry()
{
        struct my_data *entry = NULL;
        mutex_lock(&mutex);
        if (!list_empty(&q)) {
                entry = container_of(q.next, struct my_data, link);
                kref_get(&entry->refcount);
        }
        mutex_unlock(&mutex);
        return entry;
}

static void release_entry(struct kref *ref)
{
        struct my_data *entry = container_of(ref, struct my_data, refcount);

        list_del(&entry->link);
        kfree(entry);
}

static void put_entry(struct my_data *entry)
{
        mutex_lock(&mutex);
        kref_put(&entry->refcount, release_entry);
        mutex_unlock(&mutex);
}

如果你不想在整个释放操作期间持有锁,kref_put()的返回值是有用的。比如说,你不想在上面的示例中持有锁时调用kfree()(因为这样做有点无意义)。你可以使用kref_put()如下:

static void release_entry(struct kref *ref)
{
        /* 从kref_put()返回后完成所有工作。 */
}

static void put_entry(struct my_data *entry)
{
        mutex_lock(&mutex);
        if (kref_put(&entry->refcount, release_entry)) {
                list_del(&entry->link);
                mutex_unlock(&mutex);
                kfree(entry);
        } else
                mutex_unlock(&mutex);
}

如果你必须调用其他例程作为释放操作的一部分,这种方式更有用,这些例程可能需要很长时间或可能要求相同的锁。注意,仍然更喜欢在释放例程中完成所有操作,因为这样更整洁。

上面的示例还可以使用kref_get_unless_zero()进行优化,如下所示:

static struct my_data *get_entry()
{
        struct my_data *entry = NULL;
        mutex_lock(&mutex);
        if (!list_empty(&q)) {
                entry = container_of(q.next, struct my_data, link);
                if (!kref_get_unless_zero(&entry->refcount))
                        entry = NULL;
        }
        mutex_unlock(&mutex);
        return entry;
}

static void release_entry(struct kref *ref)
{
        struct my_data *entry = container_of(ref, struct my_data, refcount);

        mutex_lock(&mutex);
        list_del(&entry->link);
        mutex_unlock(&mutex);
        kfree(entry);
}

static void put_entry(struct my_data *entry)
{
        kref_put(&entry->refcount, release_entry);
}

这对于在put_entry()中移除kref_put()周围的互斥锁很有用,但重要的是kref_get_unless_zero()被包含在相同的临界区中,以便在查找表中找到条目时,kref_get_unless_zero()不会引用已经释放的内存。注意,使用kref_get_unless_zero()而不检查其返回值是非法的。如果你确定(通过已经有一个有效指针)kref_get_unless_zero()将返回true,那么请使用kref_get()。

Krefs和RCU

函数kref_get_unless_zero还使得在上面的示例中可以使用RCU锁进行查找:

struct my_data
{
        struct rcu_head rhead;
        .
        struct kref refcount;
        .
        .
};

static struct my_data *get_entry_rcu()
{
        struct my_data *entry = NULL;
        rcu_read_lock();
        if (!list_empty(&q)) {
                entry = container_of(q.next, struct my_data, link);
                if (!kref_get_unless_zero(&entry->refcount))
                        entry = NULL;
        }
        rcu_read_unlock();
        return entry;
}

static void release_entry_rcu(struct kref *ref)
{
        struct my_data *entry = container_of(ref, struct my_data, refcount);

        mutex_lock(&mutex);
        list_del_rcu(&entry->link);
        mutex_unlock(&mutex);
        kfree_rcu(entry, rhead);
}

static void put_entry(struct my_data *entry)
{
        kref_put(&entry->refcount, release_entry_rcu);
}

但请注意,在调用release_entry_rcu后,struct kref成员需要在RCU宽限期内保持在有效内存中。这可以通过像上面那样使用kfree_rcu(entry, rhead)来实现,或者在使用kfree之前调用synchronize_rcu(),但请注意,synchronize_rcu()可能会睡眠很长时间。