找回密码
立即注册
搜索
热搜: Java Python Linux Go
发回帖 发新帖

5216

积分

0

好友

679

主题
发表于 2 小时前 | 查看: 3| 回复: 0

在 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> // 该文件中已经使用 SCOPE_GUARD_DEFINE 为 k_mutex 做了定义

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 语句,还是因 breakcontinuegoto 跳出代码块时,自动插入调用清理函数的代码。

示例代码:

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 去执行加锁操作,并在退出时通过 cleanup_guard_k_mutex_exit 执行解锁操作。这里的加锁和解锁操作,是在 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> 的清理辅助程序。

  • _typevoid *
  • 退出函数 _exit_fn 部分是 (void)_T; (void)_func(),实际上只是调用了无参数的 _func()
  • 初始化函数 _init_fnNULL
  • 初始化参数为 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 执行资源释放。这两者都是在 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.

参考

https://docs.zephyrproject.org/latest/kernel/cleanup.html




上一篇:Vibe coding不够用了:Karpathy谈agentic engineering与AI编程新挑战
下一篇:Webshell管理工具默连(morelian)测评:多协议远程会话与Web模式实战
您需要登录后才可以回帖 登录 | 立即注册

手机版|小黑屋|网站地图|云栈社区 ( 苏ICP备2022046150号-2 )

GMT+8, 2026-5-17 04:57 , Processed in 0.646416 second(s), 39 queries , Gzip On.

Powered by Discuz! X3.5

© 2025-2026 云栈社区.

快速回复 返回顶部 返回列表