大数跨境
0
0

内核中的guard宏

内核中的guard宏 YZWDDSG
2025-11-02
1

背景

最近看内核代码的时候,注意到之前不少加锁的地方都替换成了guard()

比如,我们看一下曾经某个commit的改动,很明显,这里就是把成对的preempt_{en,dis}able替换成了一个guard()。

@@ -2417,10 +2404,9 @@ voidmigrate_disable(void)
return
;
        }

-       preempt_disable();
+       guard(preempt)();
        this_rq()->nr_pinned++;
        p->migration_disabled = 1;
-       preempt_enable();
language-c复制代码

但是,为什么可以这么替换?这个guard是怎么实现的?作用是什么?我们需要看一看,研究一下。

从gcc的cleanup属性开始

在https://gcc.gnu.org/onlinedocs/gcc/Common-Variable-Attributes.html 中,我们可以看到gcc支持cleanup这么一个属性

这里我没有查到具体哪个gcc版本引入的这个属性支持,但是我们可以明确,很早之前的版本就有了,https://gcc.gnu.org/onlinedocs/gcc-4.0.0/gcc/Variable-Attributes.html#index-g_t_0040code_007bcleanup_007d-attribute-1768 可以看到gcc 4.0.0就支持了这个属性,关于它的解释如下:

cleanup (cleanup_function)
The cleanup attribute runs a function when the variable goes out of scope. This attribute
can only be applied to auto function scope variables; it may not be applied to parameters
or variables with static storage duration. The function must take one parameter, a pointer
to a type compatible with the variable. The return value of the function(if any) is ignored.

When multiple variables in the same scope have cleanup attributes, at exit from the scope
their associated cleanup functions are run in reverse order of definition(last defined,
first cleanup)
.
language-c复制代码

什么意思呢?cleanup这个属性会在一个变量离开它的作用域后自动调用一个释放函数。注意:本属性只能作用域function auto scope的变量,也就是说函数中定义的栈上分配的在函数作用域中的变量,参数或者static变量是不行的。自动调用的函数必须有一个参数,该参数是一个指向与该变量类型兼容的指针。如果某个作用域内有多个变量定义了cleanup属性,从作用域离开时,这些cleanup的函数的执行顺序是与变量的定义顺序相反的。看到这个是不是立刻想到了cpp的析构函数。

怎么使用呢?直接上一段代码看看:

#include<stdio.h>
#include<stdlib.h>
#include<string.h>

intmain()
{
char *A = malloc(8);

strncpy(A, "yzwddsg"7);
    A[7] = '\0';

printf("%s\n", A);
free(A);
printf("%s\n", A);
}
language-c复制代码

运行结果如下:

[root@yzwddsg tmp]# ./a
yzwddsg
//这里其实有内容,是空,不建议这么写,测试用
language-c复制代码

上面的代码,我们申请了malloc的内存之后,需要手动使用free释放,否则就会发生内存泄漏。当然,上面的代码片段很简单,我们肯定不会忘记free,但是在复杂的大型的项目中,尤其是判断分支多的情况下,其实很在该释放内存的时候忘记使用free,如果改成下面这样,即使main中有大量判断分支,编译器也不会忘记在char *A离开作用域时自动调用free_A去释放内存。

#include<stdio.h>
#include<stdlib.h>
#include<string.h>

voidfree_A(char **A)
{
free(*A);
}

intmain()
{
    __attribute__((cleanup(free_A))) char *A = malloc(8);

strncpy(A, "yzwddsg"7);
    A[7] = '\0';

printf("%s\n", A);
}

//运行结果如下
[root@yzwddsg tmp]# ./a
yzwddsg
language-c复制代码

我们把上面两段没用的代码都去掉,只留主要的malloc

//第一段
#include<stdio.h>
#include<stdlib.h>

intmain()
{
char *A = malloc(8);

free(A);
}

//第二段
#include<stdio.h>
#include<stdlib.h>

voidfree_A(char **A)
{
free(*A);
}

intmain()
{
    __attribute__((cleanup(free_A))) char *A = malloc(8);
}
language-c复制代码

然后我们分别反汇编第一段和第二段代码,查看其main函数

//第一段
00000000004011a5 <main>:
4011a5: 55                    push   %rbp
4011a6: 4889 e5             mov    %rsp,%rbp
4011a9: 4883 ec 10          sub    $0x10,%rsp
4011ad: bf 08000000        mov    $0x8,%edi
4011b2: e8 a9 fe ff ff       callq  401060 <malloc@plt>
4011b7: 488945 f8           mov    %rax,-0x8(%rbp)
4011bb: 488b45 f8           mov    -0x8(%rbp),%rax
4011bf: 4889 c7             mov    %rax,%rdi
4011c2: e8 79 fe ff ff       callq  401040 <free@plt>
4011c7: b8 00000000        mov    $0x0,%eax
4011cc: c9                   leaveq
4011cd: c3                   retq
4011ce: 6690                xchg   %ax,%ax


//第二段
00000000004011c3 <main>:
4011c3: 55                    push   %rbp
4011c4: 4889 e5             mov    %rsp,%rbp
4011c7: 4883 ec 10          sub    $0x10,%rsp
4011cb: bf 08000000        mov    $0x8,%edi
4011d0: e8 8b fe ff ff       callq  401060 <malloc@plt>
4011d5: 488945 f8           mov    %rax,-0x8(%rbp)
4011d9: 48845 f8           lea    -0x8(%rbp),%rax
4011dd: 4889 c7             mov    %rax,%rdi
4011e0: e8 c0 ff ff ff       callq  4011a5 <free_A>
4011e5: b8 00000000        mov    $0x0,%eax
4011ea: c9                   leaveq
4011eb: c3                   retq
4011ec: 0f1f4000          nopl   0x0(%rax)
language-c复制代码

可以看到,使用cleanup属性之后,gcc编译器会自动在cleanup修饰的变量离开作用域的地方插入一个函数调用,指向我们自己写的free函数。不错,其实就是相当于我们自己手动写一个释放函数,然后在初始化变量的时候使用cleanup属性带上这个释放函数,编译器在编译时就会自动在所有需要释放的地方插入对改函数的掉调用以正确释放该变量,防止内存泄漏

内核中的guard

内核作为一个极其复杂的超超超大型c项目,一直都是需要手动在每个return或者goto去处理资源释放之类的操作,如果能用上cleanup岂不是美滋滋。

哎,https://lwn.net/Articles/934679/ ,guard就是用上了这个属性。

我们直接看最初的commit吧,54da6a092431 (“locking: Introduce __cleanup() based infrastructure”),看看这个实现过程:

  • attribute((__cleanup()))与清理函数的封装
+#define DEFINE_FREE(_name, _type, _free) \
+       static inline void __free_##_name(void *p) { _type _T = *(_type *)p; _free; }

+
+#define __free(_name)  __cleanup(__free_##_name)
+
//这里解释下为什么这样封装一下
//如果需要封装一个指针变量,那么返回的时候是不是就自动释放了呢?
//为了防止上面这种情况,需要先把该指针赋值给一个新变量,然后返回这个新的变量,旧的指针置NULL
//这样,return的时候就返回了新的指针,cleanup调用函数去释放旧的NULL
+#define no_free_ptr(p) \
+       ({ __auto_type __ptr = (p); (p) = NULL; __ptr; })

+
+#define return_ptr(p)  return no_free_ptr(p)



+#define __cleanup(func)                        __attribute__((__cleanup__(func)))
language-c复制代码

上面先定义了一些宏,使用这些宏,我们可以很容易构造出一个自动释放的函数(就是cleanup属性自动调用的释放函数,比如上面例子中的free_A)

比如,还是我们之前的例子:

//之前是这样写的
voidfree_A(char **A)
{
free(*A);
}

intmain()
{
    __attribute__((cleanup(free_A))) char *A = malloc(8);
}


//使用这些封装之后,在内核里可以这样写(举个例子,其实不能直接写到内核中)
DEFINE_FREE(free_A, char *, free(_T))  //注意这里是char *,不是char **了,已经对类型做了封装

intmain()
{
    __cleanup(free_A) char *A = malloc(8);   //这里直接使用__cleanup就把__attribute__((__cleanup__(XXX)))封装了
}
language-c复制代码
  • class的实现

能不能再抽象一点呢?搞一个类,为一个类型定义destructor和constructor

+#define DEFINE_CLASS(_name, _type, _exit, _init, _init_args...)                \
+typedef _type class_##_name##_t;                                       \
+static inline void class_##_name##_destructor(_type *p)                        \
+{ _type _T = *p; _exit; }                                              \
+static inline _type class_##_name##_constructor(_init_args)            \
+{ _type t = _init; return t; }

+
+#define EXTEND_CLASS(_name, ext, _init, _init_args...)                 \
+typedef class_##_name##_t class_##_name##ext##_t;                      \
+static inline void class_##_name##ext##_destructor(class_##_name##_t *p)\
+{ class_##_name##_destructor(p); }                                     \
+static inline class_##_name##_t class_##_name##ext##_constructor(_init_args) \
+{ class_##_name##_t t = _init; return t; }

+
+#define CLASS(_name, var)                                              \
+       class_##_name##_t var __cleanup(class_##_name##_destructor) =   \
+               class_##_name##_constructor


//比如,这样使用
//DEFINE_CLASS中,为struct fd定义了一个别名class_fdget_t类型
//定一个一个class_fdget_destructor函数,执行fdput(_T)
//定义了一个class_fdget_constructor函数,执行fdget(fd)函数
//CLASS则直接初始化了一个class_fdget_t类型的f变量,并附带上了cleanup属性,也就是离开作用域自动释放
//这样,使用f的时候就不用担心有忘记释放的情况了
+ * DEFINE_CLASS(fdget, struct fd, fdput(_T), fdget(fd), int fd)
+ *
+ *     CLASS(fdget, f)(fd);
+ *     if (!f.file)
+ *             return -EBADF;
+ *
+ *     // use 'f' without concern
+ */
language-c复制代码
  • 在内核中用起来

上面虽然实现了对cleanup的封装和更高层的抽象,但是我们总不想每次使用都这么麻烦吧,而且用在哪里呢?有什么使用场景呢?

下面是对lock的一层封装的实现

+#define DEFINE_GUARD(_name, _type, _lock, _unlock) \
+       DEFINE_CLASS(_name, _type, _unlock, ({ _lock; _T; }), _type _T)

+
+#define guard(_name) \
+       CLASS(_name, __UNIQUE_ID(guard))     //这里的__UNIQUE_ID是为了生成唯一的标识符名称,不做展开

+
+#define scoped_guard(_name, args...)                                   \
+       for (CLASS(_name, scope)(args),                                 \
+            *done = NULL; !done; done = (void *)1)



//所以,这里可以明白,只要再对一些类型的锁做一层DEFINE_GUARD的封装,就可以使用起来了
//比如,我们看看最开始提到的guard(preempt)();,看看它是怎么实现的
//我们看最开始就是使用DEFINE_LOCK_GUARD_0把preempt、preempt_disable(), preempt_enable()进行了封装
//DEFINE_LOCK_GUARD_0会调用__DEFINE_UNLOCK_GUARD宏和__DEFINE_LOCK_GUARD_0宏
//__DEFINE_UNLOCK_GUARD宏定义了一个class_preempt_t类型,然后定义了class_preempt_destructor函数,执行的其实就是preempt_enable()
//__DEFINE_LOCK_GUARD_0就是定义一个 class_preempt_constructor函数,初始化class_preempt_t结构体,其实里面就一个把void *lock赋值一下,然后执行preempt_disable()

//然后,当我们再去调用guard(preempt)();的时候,其实就是初始化了一个class_preempt_t结构体(在这里没什么用),然后执行preempt_disable()
//然后因为初始化的时候带上了cleanup属性,所以在离开class_preempt_t结构体变量的作用域的时候,就自动调用preempt_enable()

+#define DEFINE_LOCK_GUARD_0(_name, _lock, _unlock, ...)                        \
+__DEFINE_UNLOCK_GUARD(_name, void, _unlock, __VA_ARGS__)               \
+__DEFINE_LOCK_GUARD_0(_name, _lock)


+#define __DEFINE_UNLOCK_GUARD(_name, _type, _unlock, ...)              \
+typedef struct {                                                       \
+       _type *lock;                                                    \
+       __VA_ARGS__;                                                    \
+} class_##_name##_t;                                                   \
+                                                                       \
+static inline void class_##_name##_destructor(class_##_name##_t *_T)   \
+{                                                                      \
+       if (_T->lock) { _unlock; }                                      \
+}


+#define __DEFINE_LOCK_GUARD_0(_name, _lock)                            \
+static inline class_##_name##_t class_##_name##_constructor(void)      \
+{                                                                      \
+       class_##_name##_t _t = { .lock = (void*)1 },                    \
+                        *_T __maybe_unused = &_t;                      \
+       _lock;                                                          \
+       return _t;                                                      \
+}



DEFINE_LOCK_GUARD_0(preempt, preempt_disable(), preempt_enable())
language-c复制代码

所以,其实guard就是对一些变量(并不一定局限于变量,比如上面这个preempt_{en,dis}able这种,其实就在class的封装下可以使用上cleanup的特性)做一下封装,主要是带上cleanup属性,能够让我们避免在goto return等复杂函数体中自己维护这些变量的初始化及释放等操作。

参考

https://gcc.gnu.org/onlinedocs/gcc/Common-Variable-Attributes.html

https://echorand.me/site/notes/articles/c_cleanup/cleanup_attribute_c.html

https://omeranson.github.io/blog/2022/06/12/cleanup-attribute-in-C

https://matteocroce.it/blog/c-cleanups/

https://lwn.net/Articles/934679/


【声明】内容源于网络
0
0
YZWDDSG
内核开发
内容 31
粉丝 0
YZWDDSG 内核开发
总阅读49
粉丝0
内容31