Linux内核(linux-5.2.9)–内核对象(对象的引用计数)

  • Post author:
  • Post category:linux




一个分配好的内存对象可能会在多种场景中被多次传递并使用,在这种情况下,为了能够正确的使用内存对象,引入了“引用计数”功能


  1. 防止内存泄漏:确保已分配的对象最终都会被正确释放;
  2. 防止访问已释放的内存:确保不会使用已经被释放的对象。

数据结构 kref 为任何内存数据结构添加引用计数提供了一种简单但有效的方法。Kref相关的定义如下:

struct kref {
	refcount_t refcount;
};

typedef struct refcount_struct {
	atomic_t refs;
} refcount_t;

typedef struct {
	int counter;
} atomic_t;

从以上的结构定义可知,kref本质上就是一个封装了int成员的结构体。

在使用内核对象之前,必须创建或初始化kobject结构体。对应的函数分别是kobject_create或kobject_init。如果使用者已经为kobject自行分配了空间,则只需要调用kobject_init。kobject_init定义如下:

/**
 * kobject_init() - Initialize a kobject structure.
 * @kobj: pointer to the kobject to initialize
 * @ktype: pointer to the ktype for this kobject.
 *
 * This function will properly initialize a kobject such that it can then
 * be passed to the kobject_add() call.
 *
 * After this function is called, the kobject MUST be cleaned up by a call
 * to kobject_put(), not by a call to kfree directly to ensure that all of
 * the memory is cleaned up properly.
 */
void kobject_init(struct kobject *kobj, struct kobj_type *ktype)
{
	char *err_str;

	if (!kobj) {
		err_str = "invalid kobject pointer!";
		goto error;
	}
	if (!ktype) {
		err_str = "must have a ktype to be initialized properly!\n";
		goto error;
	}
	if (kobj->state_initialized) {
		/* do not error out as sometimes we can recover */
		pr_err("kobject (%p): tried to init an initialized object, something is seriously wrong.\n",
		       kobj);
		dump_stack();
	}

	kobject_init_internal(kobj);
	kobj->ktype = ktype;
	return;

error:
	pr_err("kobject (%p): %s\n", kobj, err_str);
	dump_stack();
}
EXPORT_SYMBOL(kobject_init);

由定义可知,该函数主要通过kobject_init_internal操作来完成具体的成员初始化工作。该函数的定义如下:

static void kobject_init_internal(struct kobject *kobj)
{
	if (!kobj)
		return;
	kref_init(&kobj->kref);
	INIT_LIST_HEAD(&kobj->entry);
	kobj->state_in_sysfs = 0;
	kobj->state_add_uevent_sent = 0;
	kobj->state_remove_uevent_sent = 0;
	kobj->state_initialized = 1;
}

说明:

þ 初始化内核对象的内嵌引用计数

þ 初始化内核对象用于链接到kset的连接件;

þ 当前内核对象还没有被添加到sysfs文件系统,此外,也没有向用户空间发送任何uevent事件,因此对应域都置为0;

þ 最后,这个函数执行完,就意味着内核对象已经初始化好,设置其state_initialized域为1。

其中INIT_LIST_HEAD的定义如下:

static inline void INIT_LIST_HEAD(struct list_head *list)
{
	WRITE_ONCE(list->next, list);
	list->prev = list;
}

就是将

list_head

结构体进行简单的初始化。

而涉及引用计数的操作由函数


kref_init


负责。该函数定义如下:

static inline void kref_init(struct kref *kref)
{
	refcount_set(&kref->refcount, 1);
}

由上述代码可知,其作用就是将引用计数的值置为1。其中相关的函数定义如下:

static inline void refcount_set(refcount_t *r, unsigned int n)
{
	atomic_set(&r->refs, n);
}
static inline void
atomic_set(atomic_t *v, int i)
{
	kasan_check_write(v, sizeof(*v));
	arch_atomic_set(v, i);
}
#define atomic_set atomic_set


其中


kasan_check_write


函数根据条件会有不同的定义:

#if defined(__SANITIZE_ADDRESS__) || defined(__KASAN_INTERNAL)
void kasan_check_read(const volatile void *p, unsigned int size);
void kasan_check_write(const volatile void *p, unsigned int size);
#else
static inline void kasan_check_read(const volatile void *p, unsigned int size)
{ }
static inline void kasan_check_write(const volatile void *p, unsigned int size)
{ }
#endif


kasan_check_write和 kasan_check_read函数都是同步定义,就一起列出来了。


kasan_check_write


不为空时定义如下,主要是进行内存访问的安全性检查,后续还调用了一系列的函数就不一一列出了。

void kasan_check_write(const volatile void *p, unsigned int size)
{
	check_memory_region((unsigned long)p, size, true, _RET_IP_);
}
EXPORT_SYMBOL(kasan_check_write);


arch_atomic_set


相关的函数定义情况如下:

#define arch_atomic_set(v, i)			WRITE_ONCE(((v)->counter), (i))
#define WRITE_ONCE(x, val) \
({							\
	union { typeof(x) __val; char __c[1]; } __u =	\
		{ .__val = (__force typeof(x)) (val) }; \
	__write_once_size(&(x), __u.__c, sizeof(x));	\
	__u.__val;					\
})


由上可知,最后是通过


__write_once_size


函数来执行的具体操作。其定义如下:

static __always_inline void __write_once_size(volatile void *p, void *res, int size)
{
	switch (size) {
	case 1: *(volatile __u8 *)p = *(__u8 *)res; break;
	case 2: *(volatile __u16 *)p = *(__u16 *)res; break;
	case 4: *(volatile __u32 *)p = *(__u32 *)res; break;
	case 8: *(volatile __u64 *)p = *(__u64 *)res; break;
	default:
		barrier();
// 执行内存拷贝操作
		__builtin_memcpy((void *)p, (const void *)res, size);
		barrier();
	}
}
#define barrier() __asm__ __volatile__("": : :"memory")




对于barrier宏,在《深入理解lunix内核》中:



优化屏障barrier宏,指令asm告诉编译器程序要插入汇编语言片段(这种情况为空)。Volatile关键字禁止编译器把asm指令与程序中的其他指令重新组合。Memory关键字强制编译器假定RAM中的所有内存单元已经被汇编指令修改;因此,编译器不能使用放在CPU寄存器中的内存单元的值来优化asm指令前的代码。注意,优化屏障并不保证不使用当前CPU把汇编语言指令混在一起执行——这是内存屏障的工作。


内存屏障(memory barrier)原语确保,在原语之后的操作开始执行之前,原语之前的操作已经完成。因此内存屏障类似于防火墙,让任何汇编语言指令都不能透过。


就是说在执行内存拷贝


__builtin_memcpy


之前,确保前面的准备操作都已经完成,而后续的操作都没有开始。总之就是确保正确的执行顺序,防止指令优化后出现问题。


完成引用计数的初始化后就可以使用


kref_get





kref_put


来分别执行引用计数的增加和减少操作。


kref_get


相关


定义如下:

static inline void kref_get(struct kref *kref)
{
	refcount_inc(&kref->refcount);
}
#define refcount_inc		refcount_inc_checked
void refcount_inc_checked(refcount_t *r)
{
	WARN_ONCE(!refcount_inc_not_zero_checked(r), "refcount_t: increment on 0; use-after-free.\n");
}
EXPORT_SYMBOL(refcount_inc_checked);


说明一下


EXPORT_SYMBOL的作用






EXPORT_SYMBOL标签内定义的函数或者符号对全部内核代码公开,不用修改内核代码就可以在您的内核模块中直接调用,即使用EXPORT_SYMBOL可以将一个函数以符号的方式导出给其他模块使用。


其中使用的


WARN_ONCE


宏相关定义如下:

#define WARN_ONCE(condition, format...)	({	\
	static int __warned;			\
	int __ret_warn_once = !!(condition);	\
						\
	if (unlikely(__ret_warn_once))		\
		if (WARN(!__warned, format)) 	\
			__warned = 1;		\
	unlikely(__ret_warn_once);		\
})
#define WARN(condition, format...) ({		\
	int __ret_warn_on = !!(condition);	\
	if (unlikely(__ret_warn_on))		\
		__WARN_printf(format);		\
	unlikely(__ret_warn_on);		\
})
#define __WARN_printf(arg...)	do { fprintf(stderr, arg); } while (0)

整体来说

WARN_ONCE


宏的作用就是满足判断条件,打印对应的提示信息。


其中,宏定义 do{…}while(0) 的妙用可以参考以下链接:


https://www.jianshu.com/p/99efda8dfec9


关键函数


refcount_inc_not_zero_checked


的相关定义如下:

bool refcount_inc_not_zero_checked(refcount_t *r)
{
	unsigned int new, val = atomic_read(&r->refs);

	do {
		new = val + 1;

		if (!val)
			return false;

		if (unlikely(!new))
			return true;

	} while (!atomic_try_cmpxchg_relaxed(&r->refs, &val, new));

	WARN_ONCE(new == UINT_MAX, "refcount_t: saturated; leaking memory.\n");

	return true;
}
EXPORT_SYMBOL(refcount_inc_not_zero_checked);

除了一些有效性的判断之外,该函数的主要工作就是提取对象当前的引用计数值,然后对该值+1,再保存到引用计数中。

其中 atomic_read相关定义如下:

#define atomic_read(v)		READ_ONCE((v)->counter)
#define READ_ONCE(x) __READ_ONCE(x, 1)
#define __READ_ONCE(x, check)						\
({									\
	union { typeof(x) __val; char __c[1]; } __u;			\
	if (check)							\
		__read_once_size(&(x), __u.__c, sizeof(x));		\
	else								\
		__read_once_size_nocheck(&(x), __u.__c, sizeof(x));	\
	smp_read_barrier_depends(); /* Enforce dependency ordering from x */ \
	__u.__val;							\
})

static __always_inline
void __read_once_size(const volatile void *p, void *res, int size)
{
	__READ_ONCE_SIZE;
}
static __no_kasan_or_inline
void __read_once_size_nocheck(const volatile void *p, void *res, int size)
{
	__READ_ONCE_SIZE;
}
#define __READ_ONCE_SIZE						\
({									\
	switch (size) {							\
	case 1: *(__u8 *)res = *(volatile __u8 *)p; break;		\
	case 2: *(__u16 *)res = *(volatile __u16 *)p; break;		\
	case 4: *(__u32 *)res = *(volatile __u32 *)p; break;		\
	case 8: *(__u64 *)res = *(volatile __u64 *)p; break;		\
	default:							\
		barrier();						\
		__builtin_memcpy((void *)res, (const void *)p, size);	\
		barrier();						\
	}								\
})
#ifdef CONFIG_SMP  /*多处理器时使用*/
#ifndef smp_read_barrier_depends
#define smp_read_barrier_depends()	__smp_read_barrier_depends()
#endif
#else	/* !CONFIG_SMP */
#ifndef smp_read_barrier_depends
#define smp_read_barrier_depends()	do { } while (0)
#endif
#endif	/* CONFIG_SMP */
#ifndef __smp_read_barrier_depends
#define __smp_read_barrier_depends()	read_barrier_depends()
#endif
/**
 * read_barrier_depends - Flush all pending reads that subsequents reads
 * depend on.
 *
 * No data-dependent reads from memory-like regions are ever reordered
 * over this barrier.  All reads preceding this primitive are guaranteed
 * to access memory (but not necessarily other CPUs' caches) before any
 * reads following this primitive that depend on the data return by
 * any of the preceding reads.  This primitive is much lighter weight than
 * rmb() on most CPUs, and is never heavier weight than is
 * rmb().
 *
 * These ordering constraints are respected by both the local CPU
 * and the compiler.
 *
 * Ordering is not guaranteed by anything other than these primitives,
 * not even by data dependencies.  See the documentation for
 * memory_barrier() for examples and URLs to more information.
 *
 * For example, the following code would force ordering (the initial
 * value of "a" is zero, "b" is one, and "p" is "&a"):
 *
 * <programlisting>
 *	CPU 0				CPU 1
 *
 *	b = 2;
 *	memory_barrier();
 *	p = &b;				q = p;
 *					read_barrier_depends();
 *					d = *q;
 * </programlisting>
 *
 * because the read of "*q" depends on the read of "p" and these
 * two reads are separated by a read_barrier_depends().  However,
 * the following code, with the same initial values for "a" and "b":
 *
 * <programlisting>
 *	CPU 0				CPU 1
 *
 *	a = 2;
 *	memory_barrier();
 *	b = 3;				y = b;
 *					read_barrier_depends();
 *					x = a;
 * </programlisting>
 *
 * does not enforce ordering, since there is no data dependency between
 * the read of "a" and the read of "b".  Therefore, on some CPUs, such
 * as Alpha, "y" could be set to 3 and "x" to 0.  Use rmb()
 * in cases like this where there are no data dependencies.
 */
#define read_barrier_depends() __asm__ __volatile__("mb": : :"memory")

定义了这么些函数,核心思想就是正确的操作对应的引用计数值。


关于


atomic_try_cmpxchg_relaxed


的定义如下:

#ifndef atomic_try_cmpxchg_relaxed
static inline bool
atomic_try_cmpxchg_relaxed(atomic_t *v, int *old, int new)
{
	int r, o = *old;
	r = atomic_cmpxchg_relaxed(v, o, new);
	if (unlikely(r != o))
		*old = r;
	return likely(r == o);
}
#define atomic_try_cmpxchg_relaxed atomic_try_cmpxchg_relaxed
#endif
static inline int atomic_cmpxchg_relaxed(atomic_t *ptr, int old, int new)
{
	int oldval;
	unsigned long res;

	prefetchw(&ptr->counter);

	do {
		__asm__ __volatile__("@ atomic_cmpxchg\n"
		"ldrex	%1, [%3]\n"
		"mov	%0, #0\n"
		"teq	%1, %4\n"
		"strexeq %0, %5, [%3]\n"
		    : "=&r" (res), "=&r" (oldval), "+Qo" (ptr->counter)
		    : "r" (&ptr->counter), "Ir" (old), "r" (new)
		    : "cc");
	} while (res);

	return oldval;
}
#define atomic_cmpxchg_relaxed		atomic_cmpxchg_relaxed
extern inline void prefetchw(const void *ptr)  
{
	__builtin_prefetch(ptr, 1, 3);
}

__builtin_prefetch() 是 gcc 的一个内置函数。它通过对数据手工预取的方法,减少了读取延迟,从而提高了性能,但该函数也需要 CPU 的支持。

第一个参数: 是个内存指针,它指向要预取的数据;

第二个参数:是个编译时的常数,或 1 或 0 。1 时表示写(w),0 时表示读(r) ;

第三个参数:必须是编译时的常数,也称为“时间局部性”(temporal locality) 。时间局部性是指,如果程序中某一条指令一旦执行,则不久之后该指令可能再被执行;如果某数据被访问,则不久之后该数据会被再次访问。该值的范围在 0 – 3 之间。为 0 时表示,它没有时间局部性,也就是说,要访问的数据或地址被访问之后的不长的时间里不会再被访问;为 3 时表示,被访问的数据或地址具有高 时间局部性,也就是说,在被访问不久之后非常有可能再次访问;对于值 1 和 2,则分别表示具有低 时间局部性 和中等 时间局部性。该值默认为 3 。


kref_put


相关定义如下:

static inline int kref_put(struct kref *kref, void (*release)(struct kref *kref))
{
	if (refcount_dec_and_test(&kref->refcount)) {
		release(kref);
		return 1;
	}
	return 0;
}
#ifdef NDEBUG
#define REFCOUNT_WARN(cond, str) (void)(cond)
#define __refcount_check
#else
#define REFCOUNT_WARN(cond, str) BUG_ON(cond)
#define __refcount_check	__must_check
#endif

static inline __refcount_check
bool refcount_dec_and_test(refcount_t *r)
{
	return refcount_sub_and_test(1, r);
}
static inline __refcount_check
bool refcount_sub_and_test(unsigned int i, refcount_t *r)
{
	unsigned int old, new, val = atomic_read(&r->refs);

	for (;;) {
		if (unlikely(val == UINT_MAX))
			return false;

		new = val - i;
		if (new > val) {
			REFCOUNT_WARN(new > val, "refcount_t: underflow; use-after-free.\n");
			return false;
		}

		old = atomic_cmpxchg_release(&r->refs, val, new);
		if (old == val)
			break;

		val = old;
	}

	return !new;
}

从定义逻辑可知,该函数先读取了当前的引用计数值,判断对应的有效性,然后减去相应引用数量,满足判断则将得到的值更新到引用计数中。最后返回时,若new值为0,则返回true,到

kref_put

中会执行相应的release操作;否则返回false。

使用kref_init、kref_get和kref_put函数,再结合调用者提供的release函数,可以在任何内核结构中加入完整的引用计数。

首先,将kref结构嵌入到需要使用引用计数的结构之中。

struct foo {
    ...
    struct kref refcount;
    ...
};

定义时,需要将整个kref结构嵌入foo中,而不是一个指向kref结构的指针。

如果有一个foo结构,找到嵌入到其中的kref很简单,只需要使用refcount成员。但是,处理kref的代码(例如release回调函数)常常遇到相反的问题:给出一个struct kref 指针,如何找到包含他的结构的指针?我们不建议将refcount作为foo结构的第一个域,不鼓励程序员在两种对象类型间进行愚蠢的强制转换。而应该使用定义在 <linux/kernel.h>中的container_of 宏。


container_of(pointer, type, member)

其中type为宿主对象的数据结构类型,member为结构中某个内嵌域(成员)的名字,pointer是指向宿主对象的member域的指针,这个宏返回值是指向宿主对象的指针。

contain_of 宏的实现原理:考虑成员member相对于类型为type的宿主结构的起始地址的偏移量。对于所有该类型的宿主对象,这个偏移量是固定的。并且可以在假设宿主对象地址为0,通过返回member域的地址获得,即等于 (unsigned long)(&((type *)0)->member) 。这样,将宿主对象的member域的地址 (pointer)减去这个偏移量,就可以得到宿主对象的地址,再将它转换为type类型的指针。

对应上面的例子:

foo = container_of(kref, struct foo, refcount);

在创建foo对象时,除了为它分配内存空间,kref域也必须被初始化,否则无法使用引用计数功能。

struct foo *foo_create(void) {
    struct foo *foo;
    foo =kzalloc(struct foo, GFP_KERNEL);
    if (!foo) {
        return NULL;
    } 
    kref_init(&foo->kref);
    return foo;
}

在foo被创建时,该结构的内部引用计数被设置为1。因此,设置这个内核对象的代码(模块)必须调用一次kref_put()以最终释放这个引用。

现在就可以随意地递增或递减结构的引用计数了。在使用foo结构之前,调用函数kref_get。在使用完后,应该调用kref_put函数释放掉这个引用。

一般来说,大多数代码选择继续封装kref_get和kref_put以方便使用。

struct foo *foo_get(struct foo *foo)
{
    if(foo)
        kref_get(&foo->kref);
    return foo;
}
void foo_put(struct foo *foo)
{
    if(foo) {
        kref_put(&foo->kref, foo_release);
    }
}

在最后一个引用计数被释放时,被传入kref_put的函数foo_release将被调用,这个函数的目的是释放已分配的foo结构的内存。foo_release函数的原型接收的是指向struct kref的指针,因此需要用前面提到的container_of宏转换为指向struct foo结构的指针,然后再调用内存释放函数kfree。

void foo_release(struct kref *kref)
{
    struct foo *foo;
    foo = container_of(foo, struct foo, kref);
    kfree(foo);
}



版权声明:本文为wzxq123原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。