kref

一、kref 介绍


1、kref 简介

  kref 是一个引用计数器,它在使用时通常会嵌套在其它结构体中,用于记录所嵌套结构体的引用计数,并在计数清零时调用相应的 release 函数。简而言之,kref 的创建主要是为了跟踪复杂情况下的结构引用销毁情况。

二、kref 实现


  kref 相关的代码实现主要在include/linux/kref.h中。

1、kref 数据结构

1)struct kref

  可以看到,kref 的结构体中包含了一个 refcount_t 类型,也就是 atomic_t 类型的计数值。atomic_t 是原子操作,操作时有专用的 API 函数,它在一定程度上可以解决内核的并发和竞态问题,省去额外加锁去锁的过程。(include/linux/kref.h

struct kref {
		/* @refcount: 引用计数 */
		refcount_t refcount;
};

2)refcount_t

typedef struct refcount_struct {
		atomic_t refs;
} refcount_t;

2、kref 操作函数

函数描述返回值
kref_init初始化kref的引用计数值为1。
kref_read读取kref的引用计数值。计数值
kref_getkref的引用计数值加1。
kref_putkref的引用计数值减1,当引用计数值为0时,调用相应的release函数计数值非0时返回0,计数值为0时返回1
kref_put_mutex在mutex的保护下执行kref_put操作。kref_put
kref_put_lock在spinlock的保护下执行kref_put操作。kref_put
kref_get_unless_zero执行引用计数值加1操作,除非它已经为0。如果计数值加1成功,则返回非0,否则返回0

1)kref_put_mutex

  该函数需要多提供一个 mutex 作为参数,用于保证减小引用计数值和调用 release 函数操作时在该 mutex 的保护下进行。

static inline int kref_put_mutex(struct kref *kref,
				 void (*release)(struct kref *kref),
				 struct mutex *lock)
{
		if (refcount_dec_and_mutex_lock(&kref->refcount, lock)) {
				release(kref);
				return 1;
		}
		return 0;
}

bool refcount_dec_and_mutex_lock(refcount_t *r, struct mutex *lock)
{
		if (refcount_dec_not_one(r))
				return false;
	
		mutex_lock(lock);
		if (!refcount_dec_and_test(r)) {
				mutex_unlock(lock);
				return false;
		}
	
		return true;
}
EXPORT_SYMBOL(refcount_dec_and_mutex_lock);

2)kref_get_unless_zero()

  这个函数意图简化通过查找到结构体中的某个对象时对计数值的加锁。

static inline int __must_check kref_get_unless_zero(struct kref *kref)
{
		return refcount_inc_not_zero(&kref->refcount);
}

三、kref 使用


  经过分析,其实我们可以发现 kref 的设计是非常简单的,其目的是为了能灵活地运用在各种结构的生命周期管理中。下面我们来介绍下 kref 实际的使用场景和需要注意的一些事项。

1、kref 文档翻译

  为了方便用户的使用,在内核自带的文档(Documentation/kref.txt)中总结了一些 kref 的使用规则和注意事项。以下是文档的翻译:

1)kref 介绍

  对于那些用在各种场合并且被随处传递的对象,你如果没有使用引用计数,那么你的代码很可能存在问题。当然,如果你想要使用引用计数,那么 kref 会是一个不错的选择。
  你可以使用下述的方式将 kref 添加到你的数据结构中:

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

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

2)kref 初始化

  在申请分配 kref 后,你必须对 kref 进行初始化:

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

此时会将 kref 的引用计数值设置为1。

3)kref 使用规则

初始化 kref 后,你必须遵循以下规则:

  • 1、如果你创建了一个非临时的指针,特别是它可能会被传递给另一个线程执行,你必须在传递该指针前执行kref_get()
kref_get(&data->refcount);
  • 如果你已经有一个指向 kref 的合法指针,也就是说引用计数值不会变成0。那么你在执行上述操作的时候可以不用加锁。

  • 2、当你使用完这个指针时,你必须调用kref_put()

kref_put(&data->refcount, data_release);
  • 如果这个指针是某个对象最后的引用,那么相应的 release 函数就会被调用。 如果代码不会在没有拥有合法指针的情况下去获得一个合法的指针,那么在 kref_put() 时就不需要加锁。

  • 3、如果代码试图在没有拥有引用计数的情况下就调用 kref_get() ,那么必须串行化 kref_get()kref_put() 的执行。因为很可能在 kref_get() 执行之前或者执行过程中, kref_put() 就被调用并把整个数据结构释放掉了。

例如,你分配了一些数据并把它传递到其它的线程去处理:

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;

		/* do stuff with data here */
		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;
		}
		/* do stuff with data here */
out:
		kref_put(&data->refcount, data_release);
		return rv;
}

  这样一来,无论两个线程的执行顺序如何都没有关系,kref_put() 知道数据何时不再有引用计数,可以被销毁。
  此处需要注意规则1中的要求,必须在传递指针之前调用 kref_get() ,而不能像下面一样操作:

		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);

  不要认为自己在使用上面代码时知道自己正在做什么。首先,你可能并不清楚自己正在做什么。其次,虽然你可能知道自己在做什么,但一些修改或复制你代码的人可能并不知道他们正在做什么。这是一种糟糕的使用方式。千万不要这样做。

  当然在某些情况下也可以对 kref_putkref_get 做一些优化。例如,你已经完成了对这个数据的处理,并准备把它传递给其它地方,这里就不需要再做多余的 kref_putkref_get 了。

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

直接做 enqueue 操作即可。当然,在这个地方添加一条注释也是非常受欢迎的。

enqueue(obj);
/* We are done with obj, so we pass our refcount off to the queue. DON'T TOUCH obj AFTER HERE! */

  最后一条规则(规则3)处理起来较麻烦。例如,你有一列数据,每个数据都有 kref 计数,同时你想获取第一条数据。但你不能简单地把第一条数据从链表中取出并调用 kref_get()。因为这违背了第三条规则:你并没有拥有一个合法的指针(引用计数)。因此,你必须添加一个锁。如下所示:

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。那么你可以使用下述的方式:

static void release_entry(struct kref *ref)
{
		/* All work is done after the return from 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);
}

如果在 release() 的时候需要花费较长的时间,或者又需要得到这把相同的锁,那么上述的操作就非常有用。但需要注意的是,在 release() 中完成释放工作会使代码看起来更整洁。

  另外,上述的例子还可以通过使用 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);
				/* 如果此时已调用kref_put(),引用计数为0,则entry重新被置为NULL */
				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()周围的锁。

Which is useful to remove the mutex lock around kref_put() in put_entry(), but it’s important that kref_get_unless_zero is enclosed in the same critical section that finds the entry in the lookup table, otherwise kref_get_unless_zero may reference already freed memory. Note that it is illegal to use kref_get_unless_zero without checking its return value. If you are sure (by already having a valid pointer) that kref_get_unless_zero() will return true, then use kref_get() instead.

4)kref 和 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);
}

But note that the struct kref member needs to remain in valid memory for a rcu grace period after release_entry_rcu was called. That can be accomplished by using kfree_rcu(entry, rhead) as done above, or by calling synchronize_rcu() before using kfree, but note that synchronize_rcu() may sleep for a substantial amount of time.

附录


1、kref 疑问

Q1: kref_put_mutex()kref_put_lock()在执行过程中可能会出现加锁后,没有解锁的情况。这个时候会由上层函数对返回值判断并解锁吗?这样操作合理吗?为什么?

2、参考资料

  1. 以上源码内容参考自:Linux-4.15.18
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值