gcc __attribute__((cleanup))解析

文摘   2024-11-06 08:20   江苏  

引言

在 C/C++ 语言编程中,资源管理是一个十分重要的内容。确保资源在不再需要时被正确释放,可以避免内存泄漏和其他资源管理问题。GCC 提供了一个强大的扩展属性 __attribute__((cleanup(cleanup_function))),用于在变量作用域结束时自动调用一个清理函数。本文将详细介绍这一特性的原理、实际案例以及如何在项目中应用它。

工作原理

__attribute__((cleanup(cleanup_function))) 是 GCC 提供的一个扩展属性,用于在变量作用域结束时自动调用一个指定的清理函数。这个特性特别适用于需要确保资源在变量生命周期结束时被正确释放的场景,例如关闭文件描述符、释放内存等。

当变量的作用域结束时(例如函数返回、块结束或异常抛出),GCC 会自动调用指定的清理函数。清理函数接收一个指向变量的指针,因此可以修改变量的值或执行其他清理操作。

__attribute__((cleanup((cleanup_function))) 语法如下:

void cleanup_function(type *var);
type var __attribute__((cleanup(cleanup_function)));
  • type: 变量的类型。

  • var: 需要应用清理属性的变量。

  • cleanup_function: 清理函数,接受一个指向变量类型的指针作为参数。

代码演示

1. 释放动态分配的内存

常见的用例是确保动态分配的内存被正确释放,让我们通过代码演示一下。首先创建一个未释放申请的动态内存的 ufree_memory.c 文件,再创建一个通过 cleanup 函数释放了申请的动态内存的 free_memory.c 文件,并使用 valgrind 检测一下两个文件内存泄露情况。

源文件 ufree_memory.c

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

int main() {
char *str = malloc(100 * sizeof(char));
if (str == NULL) {
perror("malloc");
return 1;
}

// 使用分配的内存
snprintf(str, 100, "Hello, World!");
printf("%s\n", str);

// 内存将在 main 函数结束时自动释放
return 0;
}

编译上面源文件,生成可执行文件并通过 valgrind 运行如下:

[root@localhost gcc_property]# gcc -g -o ufree_memory ufree_memory.c
[root@localhost gcc_property]# ls
ufree_memory ufree_memory.c

[root@localhost gcc_property]# valgrind --tool=memcheck --leak-check=full ./ufree_memory
==1430643== Memcheck, a memory error detector
==1430643== Copyright (C) 2002-2024, and GNU GPL'd, by Julian Seward et al.
==1430643== Using Valgrind-3.23.0 and LibVEX; rerun with -h for copyright info
==1430643== Command: ./ufree_memory
==1430643==
Hello, World!
==1430643==
==1430643== HEAP SUMMARY:
==1430643== in use at exit: 100 bytes in 1 blocks
==1430643== total heap usage: 2 allocs, 1 frees, 1,124 bytes allocated
==1430643==
==1430643== 100 bytes in 1 blocks are definitely lost in loss record 1 of 1
==1430643== at 0x4C392A1: malloc (vg_replace_malloc.c:446)
==1430643== by 0x4006FB: main (ufree_memory.c:13)
==1430643==
==1430643== LEAK SUMMARY:
==1430643== definitely lost: 100 bytes in 1 blocks
==1430643== indirectly lost: 0 bytes in 0 blocks
==1430643== possibly lost: 0 bytes in 0 blocks
==1430643== still reachable: 0 bytes in 0 blocks
==1430643== suppressed: 0 bytes in 0 blocks
==1430643==
==1430643== For lists of detected and suppressed errors, rerun with: -s
==1430643== ERROR SUMMARY: 1 errors from 1 contexts (suppressed: 0 from 0)

通过 Valgrind 的输出,我们可以得出以下结论:

  • 内存泄漏: 100 bytes in 1 blocks are definitely lost:程序中确实存在内存泄漏,100 字节的内存块在程序结束时没有被释放。 definitely lost: 100 bytes in 1 blocksValgrind 确定这 100 字节的内存是完全丢失的,即没有被释放。

  • 内存分配和释放: total heap usage2 allocs, 1 frees, 1,124 bytes allocated:程序总共进行了 2 次内存分配,1 次内存释放,总共分配了 1,124 字节的内存。 in use at exit100 bytes in 1 blocks:在程序退出时,仍有 100 字节的内存未被释放。

  • 泄漏位置: at 0x4C392A1: malloc (vg_replace_malloc.c:446):内存泄漏发生在 malloc 调用处。 by 0x4006FB: main (ufree_memory.c:13):具体的泄漏位置是在 main 函数的第 13 行,即 char *str = malloc(100 * sizeof(char)); 这一行。

根据上述结论,我们可以确定问题出在 main 函数的第 13 行,即 char *str = malloc(100 * sizeof(char)); 分配的内存没有被释放。为了修复这个问题,可以使用 __attribute__((cleanup(cleanup_function))) 属性来确保内存被自动释放。

 

拓展Valgrind 的输出,为什么会有 2 次内存分配和 1 次内存释放?

  • 2 次内存分配:第 1 次内存分配:在 main 函数中,char *str = malloc(100 * sizeof(char)); 这一行分配了 100 字节的内存。第 2 次内存分配:Valgrind 本身可能在内部进行了一些额外的内存分配。这些分配通常是为了跟踪内存使用情况和检测内存泄漏。这些内部分配不会影响你的程序逻辑,但会出现在 Valgrind 的输出中。

  • 1 次内存释放:唯一的 1 次内存释放:Valgrind 内部分配的内存在程序结束时被释放。在你的源代码中,没有任何地方显式地释放 str 指向的 100 字节内存。因此,这 100 字节的内存没有被释放,导致内存泄漏。

源文件 free_memory.c :

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

// 清理函数,释放内存
void free_memory(char **ptr) {
if (*ptr != NULL) {
free(*ptr);
printf("Memory freed\n");
}
}

int main() {
char *str __attribute__((cleanup(free_memory))) = malloc(100 * sizeof(char));
if (str == NULL) {
perror("malloc");
return 1;
}

// 使用分配的内存
snprintf(str, 100, "Hello, World!");
printf("%s\n", str);

// 内存将在 main 函数结束时自动释放
return 0;
}

在这个例子中,free_memory 函数会在 str 变量的作用域结束时自动调用,确保分配的内存被释放。重新编译并运行 Valgrind 检测:

[root@localhost gcc_property]# gcc -g -o free_memory free_memory.c
[root@localhost gcc_property]# ls
free_memory free_memory.c ufree_memory ufree_memory.c

[root@localhost gcc_property]# valgrind --tool=memcheck --leak-check=full ./free_memory
==1431439== Memcheck, a memory error detector
==1431439== Copyright (C) 2002-2024, and GNU GPL'd, by Julian Seward et al.
==1431439== Using Valgrind-3.23.0 and LibVEX; rerun with -h for copyright info
==1431439== Command: ./free_memory
==1431439==
Hello, World!
Memory freed
==1431439==
==1431439== HEAP SUMMARY:
==1431439== in use at exit: 0 bytes in 0 blocks
==1431439== total heap usage: 2 allocs, 2 frees, 1,124 bytes allocated
==1431439==
==1431439== All heap blocks were freed -- no leaks are possible
==1431439==
==1431439== For lists of detected and suppressed errors, rerun with: -s
==1431439== ERROR SUMMARY: 0 errors from 0 contexts (suppressed: 0 from 0)

通过 Valgrind 的输出,我们可以得出以下结论:

  1. 程序输出: Hello, World!:程序成功打印了字符串 "Hello, World!"。 Memory freed:程序在 main 函数结束时调用了 free_memory 函数,释放了分配的内存,并打印了 "Memory freed"

  2. HEAP SUMMARY: in use at exit: 0 bytes in 0 blocks:程序结束时,所有分配的内存都已被释放,没有内存泄漏。 total heap usage: 2 allocs, 2 frees, 1,124 bytes allocated: 2 allocs:程序进行了两次内存分配。一次是你显式调用的 malloc(100 * sizeof(char)),另一次可能是 Valgrind 内部分配的内存。 2 frees:程序进行了两次内存释放。一次是你显式调用的 free_memory 函数释放的内存,另一次可能是 Valgrind 内部分配的内存被释放。 1,124 bytes allocated:总共分配了 1,124 字节的内存,其中 100 字节是你显式分配的,其余 1,024 字节可能是 Valgrind 内部分配的。

  3. All heap blocks were freed -- no leaks are possible:这表明所有分配的内存块都已成功释放,没有内存泄漏。

  4. ERROR SUMMARY: 0 errors from 0 contexts (suppressed: 0 from 0)Valgrind 没有检测到任何错误,包括内存泄漏、越界访问等。

至此,程序按预期运行,没有检测到任何错误。我们可以确认内存泄漏问题已经得到解决。

2. 关闭文件描述符

假设我们有一个函数,需要在函数退出时确保文件描述符被关闭。可以使用 __attribute__((cleanup(cleanup_function))) 来实现这一点。

源文件 close_fd.c

#include <stdio.h>
#include <unistd.h>
#include <fcntl.h>

// 清理函数,关闭文件描述符
void close_fd(int *fd) {
if (*fd >= 0) {
close(*fd);
printf("File descriptor %d closed\n", *fd);
}
}

int main() {
int fd __attribute__((cleanup(close_fd))) = open("example.txt", O_RDONLY);
if (fd < 0) {
perror("open");
return 1;
}

// 读取文件内容
char buffer[100];
ssize_t bytes_read = read(fd, buffer, sizeof(buffer) - 1);
if (bytes_read < 0) {
perror("read");
return 1;
}
buffer[bytes_read] = '\0';
printf("Read: %s\n", buffer);

// 文件描述符将在 main 函数结束时自动关闭
return 0;
}

编译上面源文件,生成并运行可执行文件,

[root@localhost gcc_property]# gcc -g -o close_fd close_fd.c
[root@localhost gcc_property]# cat example.txt
This is a demo to demonstration the usage of __attribute__((cleanup(cleanup_function)))
[root@localhost gcc_property]# ./close_fd
Read: This is a demo to demonstration the usage of __attribute__((cleanup(cleanup_function)))
File descriptor 3 closed

在这个例子中,close_fd 函数会在 fd 变量的作用域结束时自动调用,确保文件描述符被关闭。

通过打印内容,我们可以知道文件描述符已经成功关闭。但是即使程序输出了 File descriptor 3 closed,也有可能存在某些特殊情况或错误导致文件描述符没有真正关闭。如果我们想进一步确认文件描述符确实被关闭,我们也可以通过 Valgrind ,其不仅可以检测内存泄漏,还可以检测文件描述符等资源的泄漏。可以使用 Valgrind 的 --track-fds=yes 选项来跟踪文件描述符的使用情况。

Valgrind 会输出文件描述符的打开和关闭情况。如果文件描述符被正确关闭,你不会看到任何未关闭的文件描述符的警告。使用 Valgrind 检测输出如下:

[root@localhost gcc_property]# valgrind --tool=memcheck --leak-check=full --track-fds=yes ./close_fd
==1436843== Memcheck, a memory error detector
==1436843== Copyright (C) 2002-2024, and GNU GPL'd, by Julian Seward et al.
==1436843== Using Valgrind-3.23.0 and LibVEX; rerun with -h for copyright info
==1436843== Command: ./close_fd
==1436843==
Read: This is a demo to demonstration the usage of __attribute__((cleanup(cleanup_function)))
File descriptor 3 closed
==1436843==
==1436843== FILE DESCRIPTORS: 3 open (3 std) at exit.
==1436843==
==1436843== HEAP SUMMARY:
==1436843== in use at exit: 0 bytes in 0 blocks
==1436843== total heap usage: 1 allocs, 1 frees, 1,024 bytes allocated
==1436843==
==1436843== All heap blocks were freed -- no leaks are possible
==1436843==
==1436843== For lists of detected and suppressed errors, rerun with: -s
==1436843== ERROR SUMMARY: 0 errors from 0 contexts (suppressed: 0 from 0)

这段 Valgrind 的输出解析如下:

  1. 程序输出: ReadThis is a demo to demonstration the usage of attribute((cleanup(cleanup_function))):程序成功读取并打印了文件内容。 File descriptor 3 closed:程序在 main 函数结束时调用了 close_fd 函数,关闭了文件描述符 3,并打印了关闭信息。

  2. 文件描述符跟踪: FILE DESCRIPTORS: 3 open (3 std) at exit.:程序结束时,有 3 个文件描述符仍然打开,但这 3 个文件描述符都是标准输入(0)、标准输出(1)和标准错误(2)。这意味着除了这三个标准文件描述符外,没有其他文件描述符被打开而未关闭。

  3. HEAP SUMMARY: in use at exit: 0 bytes in 0 blocks:程序结束时,所有分配的内存都已被释放,没有内存泄漏。 total heap usage1 allocs, 1 frees, 1,024 bytes allocated: 1 allocs:程序进行了 1 次内存分配。 1 frees:程序进行了 1 次内存释放。 1,024 bytes allocated:总共分配了 1,024 字节的内存。

  4. 内存泄漏检查: All heap blocks were freed -- no leaks are possible:所有分配的内存块都已成功释放,没有内存泄漏。

  5. 错误总结: ERROR SUMMARY: 0 errors from 0 contexts (suppressed: 0 from 0)Valgrind 没有检测到任何错误,包括内存泄漏、越界访问等。

通过这段 Valgrind 输出,我们可以得出以下结论:

  • 文件描述符管理正确:程序正确地打开了文件描述符 3 并在 main 函数结束时关闭了它。除了标准输入、标准输出和标准错误外,没有其他文件描述符被打开而未关闭。

  • 内存管理正确:程序正确地分配和释放了内存,没有内存泄漏。

  • 程序运行正常:程序按预期运行,没有检测到任何错误。

总结

__attribute__((cleanup(cleanup_function)))是 GCC 提供的一个强大工具,用于在变量作用域结束时自动调用清理函数。通过使用这一特性,可以编写更简洁、更安全的代码,确保资源在不再需要时被正确释放。无论是释放内存,关闭文件描述符、还是其他资源管理任务,__attribute__((cleanup_function)) 都能提供有效的解决方案。希望本文能帮助你在项目中更好地利用这一特性,提高代码的质量和可靠性。


Linux二进制
学习并分享Linux的相关技术,网络、内核、驱动、容器等。
 最新文章