Zephyr 作用域 Cleanup 机制
在 C 语言编程中,资源管理(如内存分配、互斥锁获取、信号量获取等)通常需要开发者手动处理。如果函数中有多个退出点,很容易漏写资源释放动作,从而导致内存泄漏或死锁。
为了解决这个问题,Zephyr 引入了 作用域清理(Scope-based Cleanup) 机制。该机制利用 GCC/Clang 编译器的特性,实现类似 C++ RAII(Resource Acquisition Is Initialization)或 Go 语言 defer 的功能:当变量离开其作用域(如函数返回、代码块结束)时,系统会自动调用预定义的清理函数。
使用 链接到标题
宏 链接到标题
Zephyr 使用宏对 cleanup 底层属性封装成了更加易用、可读性更好的 API。API 定义在 zephyr/include/zephyr/cleanup.h 中。
定义宏 链接到标题
SCOPE_VAR_DEFINE(_name, _type, _exit_fn, _init_fn, ...): 定义一个新的作用域变量类型。SCOPE_GUARD_DEFINE(_name, _type, _lock, _unlock): 定义一个作用域保护(常用于锁)。SCOPE_DEFER_DEFINE(_func, ...): 定义一个延迟清理操作。
操作宏 链接到标题
scope_var(_name, _var)(init_args): 声明变量并调用初始化函数。scope_var_init(_name, _var, _init_expr): 声明变量并使用直接表达式初始化(绕过 init 函数)。scope_guard(_name)(obj): 使用保护(如加锁)。scope_defer(_name)(args): 注册延迟执行的任务。
示例 链接到标题
要使用作用域清理功能,需启用 CONFIG_SCOPE_CLEANUP_HELPERS=y。
自定义变量的自动初始化和清除 链接到标题
static inline struct flash_area *flash_area_init(int area_id)
{
struct flash_area *fa;
if (flash_area_open(area_id, &fa) < 0) {
return NULL;
}
return fa;
}
static inline void flash_area_exit(struct flash_area *fa)
{
if (fa != NULL) {
flash_area_close(fa);
}
}
// 定义自动清除变量类型flash_area
SCOPE_VAR_DEFINE(flash_area, struct flash_area *, flash_area_exit(_T),
flash_area_init(area_id), int area_id);
static int some_function(void)
{
// 声明变量fa,自动调用flash_area_init初始化
scope_var(flash_area, fa)(PARTITION_ID(storage_partition));
if (fa == NULL) {
return -EINVAL; // 退出自动调用flash_area_exit(fa)
}
printk("Has driver: %d\n", flash_area_has_driver(fa));
// 退出自动调用flash_area_exit(fa)
return 0;
}
互斥锁自动释放(Guard) 链接到标题
这是最常见的用法,能有效防止因提前 return 导致的死锁。
#include <zephyr/cleanup/kernel.h> // 该文件中会使用针对k_mutex使用SCOPE_GUARD_DEFINE
void critical_function(struct k_mutex *mutex) {
// 加锁mutex,函数任何出口会自动解锁
scope_guard(k_mutex)(mutex);
if (error_occurred) {
return; // 自动释放互斥锁mutex
}
// 自动释放互斥锁mutex
}
动态内存自动回收 (Defer) 链接到标题
#include <zephyr/cleanup/kernel.h>
void process_data(void) {
void *ptr = k_malloc(128);
if (!ptr) return;
// 对ptr注册延迟释放
scope_defer(k_free)(ptr);
if (work_failed(ptr)) {
return; // 自动调用 k_free(ptr)
}
// 自动调用 k_free(ptr)
}
带多参数的延迟调用 链接到标题
当清理函数需要多个参数时(如从特定堆中释放内存):
SCOPE_DEFER_DEFINE(k_heap_free, struct k_heap *, void *);
void task(struct k_heap *my_heap) {
void *p = k_heap_alloc(my_heap, 64, K_NO_WAIT);
if (!p) return;
scope_defer(k_heap_free)(my_heap, p);
// 自动调用 k_heap_free(my_heap, p)
}
原理 链接到标题
背景知识 链接到标题
GCC 和 Clang 编译器提供的一个特殊属性:__attribute__((cleanup(cleanup_function)))。通过 cleanup 属性告诉编译器:“在变量被销毁之前,调用指定的清理函数 cleanup_function。”
当为一个局部变量指定了清理属性时,编译器会在该变量生命周期结束的所有出口(包括函数末尾、return 语句、break、continue、goto 跳出代码块时)自动插入调用代码。
示例代码:
void my_cleanup_func(int *p) {
printf("Cleaning up variable with value: %d\n", *p);
}
void test_cleanup(void) {
// 告诉编译器:当 x 离开作用域时,自动调用 my_cleanup_func(&x)
int __attribute__((cleanup(my_cleanup_func))) x = 42;
printf("Inside scope\n");
if (some_condition) {
return; // 这里编译器会自动插入 my_cleanup_func(&x)
}
} // 函数正常结束也会自动插入调用
关键点:
- 传递指针:编译器传递给清理函数的参数始终是该变量的地址(指针)。
- LIFO 执行顺序:如果一个作用域内有多个带清理属性的变量,它们的清理函数会按照后进先出的顺序执行,以确保资源依赖的正确释放。
Zephyr实现解析 链接到标题
Zephyr 的宏设计非常巧妙,它解决了编译器 cleanup 属性的一些原始限制,将动作和变量进行绑定,实现各种形式的自动清理功能。
自定义变量类型 链接到标题
在 include/zephyr/cleanup.h 中,所有的基础都是 SCOPE_VAR_DEFINE:
#define SCOPE_VAR_DEFINE(_name, _type, _exit_fn, _init_fn, ...) \
static inline void cleanup_##_name##_exit(_type *p) \
{ \
_type _T = *p; \
_exit_fn; \
} \
static inline _type cleanup_##_name##_init(__VA_ARGS__) \
{ \
_type t = _init_fn; \
return t; \
} \
typedef _type cleanup_##_name##_t
- 生成了一个辅助退出函数
_exit,该函数解引用指针并调用用户定义的_exit_fn。 - 定义了一个初始化函数
_init和一个类型别名。
_T:用户在写 _exit_fn 时,将 _T 作为参数传入,通过宏也就把变量 p 传入到 _exit_fn 中。
SCOPE_VAR_DEFINE(flash_area, struct flash_area *, flash_area_exit(_T),
flash_area_init(area_id), int area_id);
当调用 scope_var(flash_area, fa)(PARTITION_ID(storage_partition)) 时,宏展开为:
cleanup_flash_area_t fa __attribute__((cleanup(cleanup_flash_area_exit))) = cleanup_flash_area_init(PARTITION_ID(storage_partition));
这完成了变量声明、编译器属性绑定和初始化的三位一体,后续 fa 退出作用域时就会去调用 cleanup_flash_area_exit 函数。
Guard 链接到标题
SCOPE_GUARD_DEFINE 利用 SCOPE_VAR_DEFINE 实现 Guard 的原子化获取,生成初始化函数进行 lock,退出函数进行 unlock。
#define SCOPE_GUARD_DEFINE(_name, _type, _lock, _unlock) \
SCOPE_VAR_DEFINE( \
guard_##_name, _type, if (_T != NULL) { _unlock; }, ({ \
_lock; \
_T; \
}), \
_type _T)
展开宏
static inline void cleanup_##guard_##_name##_exit(_type *p) \
{ \
_type _T = *p; \
if (_T != NULL) { _unlock; }; \ //这里解锁
} \
static inline _type cleanup_##guard_##_name##_init(__VA_ARGS__) \
{ \
_type t = ({_lock; _T;}); \ //这里上锁
return t; \
} \
typedef _type cleanup_##guard_##_name##_t
scope_guard 同样利用scope_var实现:
#define scope_guard(_name) scope_var(guard_##_name, Z_UNIQUE_ID(guard))
scope_guard(k_mutex)(mutex); 展开后
cleanup_flash_area_t mutex __attribute__((cleanup(cleanup_guard_k_mutex_exit))) = cleanup_guard_k_mutex_init(Z_UNIQUE_ID(guard));
也就是会通过 cleanup_guard_k_mutex_init 去执行 lock,退出时通过 cleanup_guard_k_mutex_exit 去执行 unlock。这里的 lock 和 unlock 是在 zephyr/include/zephyr/cleanup/kernel.h 中用 SCOPE_GUARD_DEFINE 进行定义
SCOPE_GUARD_DEFINE(k_mutex, struct k_mutex *, (void)k_mutex_lock(_T, K_FOREVER),
(void)k_mutex_unlock(_T));
信号量的 guard 也定义在其中
SCOPE_GUARD_DEFINE(k_sem, struct k_sem *, (void)k_sem_take(_T, K_FOREVER), k_sem_give(_T));
Defer 释放 链接到标题
SCOPE_DEFER_DEFINE 宏用于定义一个 Defer 的清理辅助程序。当一个变量离开其作用域时会自动执行一个指定的清理函数。与 SCOPE_GUARD_DEFINE 不同,SCOPE_DEFER_DEFINE 仅仅是注册一个在作用域结束时要调用的函数,并不会做初始化动作。
定义如下:
#define SCOPE_DEFER_DEFINE(_func, ...) \
COND_CODE_0(NUM_VA_ARGS(__VA_ARGS__), \
(Z_SCOPE_DEFER_DEFINE_VOID(_func)), \
(Z_SCOPE_DEFER_DEFINE_N(_func, __VA_ARGS__)))
这个宏根据是否提供了参数来选择两种不同的实现:
无参数情况 (Z_SCOPE_DEFER_DEFINE_VOID): 如果 SCOPE_DEFER_DEFINE 只传入了函数名,没有参数类型,NUM_VA_ARGS(__VA_ARGS__) 会返回 0。COND_CODE_0 会选择 Z_SCOPE_DEFER_DEFINE_VOID(_func)。
#define Z_SCOPE_DEFER_DEFINE_VOID(_func) \
SCOPE_VAR_DEFINE(defer_##_func, void *, (void)_T; (void)_func(), NULL, void)
它最终展开为 SCOPE_VAR_DEFINE,定义了一个名为 defer_<func> 的清理辅助程序。
_type是void *。- 退出函数
_exit_fn是(void)_T; (void)_func(),这里_T是要被清理的变量,但由于是void*且初始化为NULL,所以实际上只是调用了无参数的_func()。 - 初始化函数
_init_fn是NULL。 - 初始化参数是
void。
有参数情况 (Z_SCOPE_DEFER_DEFINE_N): 如果 SCOPE_DEFER_DEFINE 传入了参数类型,NUM_VA_ARGS(__VA_ARGS__) 会返回一个非零值。COND_CODE_0 会选择 Z_SCOPE_DEFER_DEFINE_N(_func, __VA_ARGS__)。
#define Z_SCOPE_DEFER_DEFINE_N(_func, ...) \
struct defer_##_func##_ctx { \
FOR_EACH_IDX(Z_DEFER_CTX_ARG, (), __VA_ARGS__) \
}; \
SCOPE_VAR_DEFINE(defer_##_func, struct defer_##_func##_ctx, \
(void)_func(FOR_EACH_IDX(Z_DEFER_EXIT_ARG, (Z_COMMA), __VA_ARGS__)), \
{FOR_EACH_IDX(Z_DEFER_INIT_ARG, (Z_COMMA), __VA_ARGS__)}, \
FOR_EACH_IDX(Z_DEFER_FUNC_ARG, (Z_COMMA), __VA_ARGS__))
这个宏会创建一个上下文结构体 defer_<func>_ctx 来保存传递给清理函数的参数。然后它同样使用 SCOPE_VAR_DEFINE 来定义清理辅助程序。
- 创建一个名为
defer_<func>_ctx的结构体,其成员是_func函数所需要的参数。 - 退出函数
_exit_fn会从结构体中取出参数并调用_func。 - 初始化函数
_init_fn会将传入的参数填充到结构体中。
例如
SCOPE_DEFER_DEFINE(k_free, void *);
展开后就如下
// 1. 上下文结构体,用于保存传递给 k_free 的参数
struct defer_k_free_ctx {
void * arg0;
};
// 2. 清理函数,在作用域结束时被调用
static inline void __maybe_unused cleanup_defer_k_free_exit(struct defer_k_free_ctx *p)
{
struct defer_k_free_ctx _T = *p;
(void)k_free(_T.arg0);
}
// 3. 初始化函数,在声明 defer 变量时被调用
static inline struct defer_k_free_ctx __maybe_unused cleanup_defer_k_free_init(void * arg0)
{
struct defer_k_free_ctx t = { .arg0 = arg0 };
return t;
}
// 4. 为上下文结构体创建一个类型别名
typedef struct defer_k_free_ctx cleanup_defer_k_free_t;
对应的 scope_defer 利用scope_var实现。
scope_defer 宏的作用是声明一个变量,这个变量利用了 SCOPE_DEFER_DEFINE 所定义的清理辅助程序。当这个变量离开作用域时,之前注册的清理函数就会被自动调用。
scope_defer 的定义很可能类似于这样(这在 v3.4 之后的 Zephyr 版本中可以找到):
#define scope_defer(_name) scope_var(defer_##_name, Z_UNIQUE_ID(defer))
scope_defer(k_free)(ptr)展开后
cleanup_defer_k_free_t Z_UNIQUE_ID_defer0 __attribute__((__cleanup__(cleanup_defer_k_free_exit))) = cleanup_defer_k_free_init(ptr);
也就是会通过 cleanup_defer_k_free_init 去执行初始化,退出时通过 cleanup_defer_k_free_exit 去执行 free。二者都是在 zephyr/include/zephyr/cleanup/kernel.h 中用 SCOPE_DEFER_DEFINE 进行定义(前文已经有给出定义原型)
总结 链接到标题
Zephyr 的 Scope-based Cleanup 即用编译器特性实现的一种编程模式,具有以下优势:
- 安全性:强制 LIFO 清理,消除绝大部分资源泄漏和死锁隐患。
- 整洁性:不再需要满屏的
goto cleanup。 - 零开销:完全基于编译器的静态代码插入,几乎没有运行时开销。
也有如下限制和风险:
- 工具链锁定:使用 GCC/Clang 非标准属性,移植性受限。
- 双重释放风险:如果手写代码释放,可能和自动释放重叠,需要特别注意。
- 释放顺序限定:Cleanup 释放顺序是 LIFO,如果需要按其他顺序释放,需要手动管理。
- 子系统和内核不适用:这一点是官方明确的,但给的原因并没有说服性,我能想到的是子系统/内核和应用用不同的工具链才会有这种因果关系。
The cleanup mechanism is implemented using the __cleanup attribute. If the toolchain doesn’t support this attribute, the API is not available. For this reason this API is intended solely for user applications and not for the kernel itself or other subsystems.