【问题标题】:C getline memory leak different behavioursC getline内存泄漏不同的行为
【发布时间】:2021-01-31 01:01:32
【问题描述】:

我有一个关于函数getline() 的问题,正如valgrind 所报告的那样,它在两种内存使用情况下的行为似乎有所不同。我发布了两种情况的代码并解释了行为。 我希望有人能指出我正确的方向。

第一种情况

getline() 在 while 循环中调用,读取缓冲区中文本文件的所有行。然后在循环结束时只释放一次缓冲区:在这种情况下,valgrind 不会出现错误(不会发生泄漏)。

int main(int argc, char* argv[])
{
    char* buffer = NULL;
    size_t bufsize = 0;
    ssize_t nbytes;
    int counter = 0;
    char error = 0;

    FILE* input_fd = fopen(argv[1], "r");

    while ((nbytes = getline(&buffer, &bufsize, input_fd)) != -1)
    {
        counter += 1;
    }

    free(buffer);
    fclose(input_fd);

    return 0;
}

第二种情况

同一循环调用一个函数,该函数又调用getline(),传递相同的缓冲区。同样,缓冲区仅在循环结束时被释放一次,但在这种情况下,valgrind 报告了内存泄漏。事实上,让程序运行并查看 RSS,我可以看到它随着循环的进行而增加。请注意,在循环内添加一个空闲(每个循环释放缓冲区)问题就消失了。这是代码。

int my_getline(FILE* lf_fd, char** lf_buffer)
{
    ssize_t lf_nbytes = 0;
    size_t lf_bufsiz = 0;
    lf_nbytes = getline(lf_buffer, &lf_bufsiz, lf_fd);
    if (lf_nbytes == -1)
        return 1;
    return 0;
}

int main(int argc, char* argv[])
{
    char* lf_buffer = NULL;
    size_t bufsize = 0;
    ssize_t nbytes;
    int counter = 0;
    int new_line_counter = 0;
    char error = 0;

    FILE* lf_fd = fopen(argv[1], "r");

    while ((my_getline(lf_fd, &lf_buffer)) == 0)
    {
        // Added to allow measuring the RSS
        sleep(2);
   
        // If I uncomment this, no memory leak occurs
        //free(lf_buffer);
    }

    free(lf_buffer);
    fclose(lf_fd);

    return 0;
}

Valgrind 输出

==9604== Memcheck, a memory error detector
==9604== Copyright (C) 2002-2017, and GNU GPL'd, by Julian Seward et al.
==9604== Using Valgrind-3.15.0 and LibVEX; rerun with -h for copyright info
==9604== Command: ./my_getline_x86 /media/sf_Scambio/processes.log
==9604== HEAP SUMMARY:
==9604==     in use at exit: 1,194 bytes in 2 blocks
==9604==   total heap usage: 8 allocs, 6 frees, 11,242 bytes allocated
==9604== 
==9604== 1,194 bytes in 2 blocks are definitely lost in loss record 1 of 1
==9604==    at 0x483DFAF: realloc (in /usr/lib/x86_64-linux-gnu/valgrind/vgpreload_memcheck-amd64-
linux.so)
==9604==    by 0x48E371D: getdelim (iogetdelim.c:102)
==9604==    by 0x1092B3: my_getline (my_getline.c:14)
==9604==    by 0x10956A: main (my_getline.c:38)
==9604== 
==9604== LEAK SUMMARY:
==9604==    definitely lost: 1,194 bytes in 2 blocks
==9604==    indirectly lost: 0 bytes in 0 blocks
==9604==      possibly lost: 0 bytes in 0 blocks
==9604==    still reachable: 0 bytes in 0 blocks
==9604==         suppressed: 0 bytes in 0 blocks
==9604== 
==9604== For lists of detected and suppressed errors, rerun with: -s
==9604== ERROR SUMMARY: 1 errors from 1 contexts (suppressed: 0 from 0)

【问题讨论】:

    标签: c memory-leaks valgrind getline


    【解决方案1】:

    第一个程序很好。

    第二个问题来自getline() 的缓冲区长度参数。您的 my_getline() 始终将其设置为 0,这意味着 getline() 每次都会分配一个新缓冲区(至少,对于您正在使用的 glibc 实现;见下文)。改成

    int my_getline(FILE* lf_fd, char** lf_buffer, size_t* lf_bufsiz)
    {
        ssize_t lf_nbytes = 0;
        lf_nbytes = getline(lf_buffer, lf_bufsiz, lf_fd);
        if (lf_nbytes == -1)
            return 1;
        return 0;
    }
    

    并传递一个指向 size_t 变量的指针,该变量在使用时最初初始化为 0。 main() 中现有的 bufsize 变量看起来适合使用:

    //...
    while ((my_getline(lf_fd, &lf_buffer, &bufsize)) == 0)
    // ...
    

    虽然很容易解决,但您遇到的内存泄漏似乎是getline() 的 glibc 实现中的一个错误。

    来自POSIX documentation

    如果*lineptr 是空指针或者如果*lineptr 指向的对象大小不足,则应像malloc() 或对象一样分配对象应该像 realloc() 那样分别重新分配, 这样对象就足够大,可以容纳要写入它的字符...

    还有glibc manpage

    或者,在调用getline() 之前,*lineptr 可以包含指向malloc(3) 分配的缓冲区*n 字节大小的指针。 如果缓冲区不够大,无法容纳该行,getline() 会使用realloc(3) 调整其大小, 根据需要更新*lineptr*n

    这些建议,在您遇到的情况下,您将有效的非NULL 指针传递给内存并说它的长度为 0,该函数应该使用realloc() 来调整它的大小。但是,glibc implementation 检查*lineptr == NULL || *n == 0,如果为真,则用新分配的缓冲区覆盖*lineptr,从而导致您看到的泄漏。比较NetBSD implementation,它使用realloc() 进行所有分配(realloc(NULL, x) 等效于malloc(x)),因此不会导致原始代码泄漏。这并不理想,因为它在每次使用时都会导致realloc(),而不是仅在缓冲区不足以容纳当前行时(与上面的固定版本不同),但它可以工作。

    【讨论】:

    • 我认为 OP 看到的内存泄漏是 glibc 中的一个错误,顺便说一句。 getline() 的 POSIX 文档和 glibc 手册页似乎都表明,如果 *lineptr 不为空且 *n 为 0,则应使用 realloc() 来增加缓冲区,而不是使用 malloc() 来给它一个新的价值。但是glibc实现使用*lineptr == NULL || *n == 0来决定使用malloc()
    • 不确定这是一个错误。如果*n0,您将如何解释指针*lineptr 的值?有效还是无效?
    • @EugeneSh。来自 POSIX:应用程序应确保 *lineptr 是可以传递给 free() 函数的有效参数。 理论上,malloc(0) 可以返回指针而不是 NULL(即不能安全地取消引用,但仍然可以传递给 free()realloc())。
    • 是的,事实上我在阅读glibc doc 后倾向于同意:如果您将*lineptr 设置为空指针,并且 *n 为零,在调用之前,然后getline 通过调用malloc 为您分配初始缓冲区。
    • NetBSD 实现只使用realloc(),根本没有malloc()。这似乎是更好的方法。 OP 不会看到泄漏。
    猜你喜欢
    • 2014-04-09
    • 1970-01-01
    • 2023-03-25
    • 2015-08-23
    • 2016-01-27
    • 2010-11-11
    • 2017-02-18
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多