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

Posted 尚书左仆射

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Linux内核(linux-5.2.9)--内核对象(对象的引用计数)相关的知识,希望对你有一定的参考价值。

 

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

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

 

以上是关于Linux内核(linux-5.2.9)--内核对象(对象的引用计数)的主要内容,如果未能解决你的问题,请参考以下文章

内核在Linux内核中添加对jffs2的支持

Linux内核简介

Linux内核安全模块学习-内核密钥管理子系统

linux进程为啥有用户栈和内核栈,

RK3399平台开发系列讲解(内核设备树篇)3.5Linux内核对DTB文件的解析

Linux内核分析——第一章 Linux内核简介