execvp 的子进程内存释放问题

Child process memory free problem with execvp

以下代码来自"Operating Systems: Three Easy Pieces"一书。代码让我很困惑。 我知道 execvp 永远不会 returns 当它运作良好时。

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <fcntl.h>
#include <assert.h>
#include <sys/wait.h>

int
main(int argc, char *argv[])
{
    int rc = fork();
    if (rc < 0) {
        // fork failed; exit
        fprintf(stderr, "fork failed\n");
        exit(1);
    } else if (rc == 0) {
        // child: redirect standard output to a file
        close(STDOUT_FILENO); 
        open("./p4.output", O_CREAT|O_WRONLY|O_TRUNC, S_IRWXU);

        // now exec "wc"...
        char *myargs[3];
        myargs[0] = strdup("wc");   // program: "wc" (word count)
        myargs[1] = strdup("p4.c"); // argument: file to count
        myargs[2] = NULL;           // marks end of array
        execvp(myargs[0], myargs);  // runs word count
    } else {
        // parent goes down this path (original process)
        int wc = wait(NULL);
    assert(wc >= 0);
    }
    return 0;
}

我使用 Valgrind 检查内存泄漏。上面的代码没有内存泄漏。当我删除 execvp 行时,它会检测到 definitely lost: 8 bytes in 2 blocks。为什么是这样?

Valgrind 命令:

valgrind --leak-check=full ./a.out

当我使用命令 valgrind --trace-children=yes --leak-check=full ./p4

==15091== Memcheck, a memory error detector
==15091== Copyright (C) 2002-2017, and GNU GPL'd, by Julian Seward et al.
==15091== Using Valgrind-3.13.0 and LibVEX; rerun with -h for copyright info
==15091== Command: ./p4
==15091==
==15092== Memcheck, a memory error detector
==15092== Copyright (C) 2002-2017, and GNU GPL'd, by Julian Seward et al.
==15092== Using Valgrind-3.13.0 and LibVEX; rerun with -h for copyright info
==15092== Command: /usr/bin/wc p4.c
==15092==
==15092==
==15092== HEAP SUMMARY:
==15092==     in use at exit: 0 bytes in 0 blocks
==15092==   total heap usage: 36 allocs, 36 frees, 8,809 bytes allocated
==15092==
==15092== All heap blocks were freed -- no leaks are possible
==15092==
==15092== For counts of detected and suppressed errors, rerun with: -v
==15092== ERROR SUMMARY: 0 errors from 0 contexts (suppressed: 0 from 0)
==15091==
==15091== HEAP SUMMARY:
==15091==     in use at exit: 0 bytes in 0 blocks
==15091==   total heap usage: 0 allocs, 0 frees, 0 bytes allocated
==15091==
==15091== All heap blocks were freed -- no leaks are possible
==15091==
==15091== For counts of detected and suppressed errors, rerun with: -v
==15091== ERROR SUMMARY: 0 errors from 0 contexts (suppressed: 0 from 0)
[root cpu-api]#

无论我 malloc 多少字节,堆摘要总是说: ==15092== 堆总使用量:36 次分配,36 次释放,8,809 字节分配

什么是 "definitely lost" 内存?

首先,让我们讨论一下 Valgrind 报告为 "definitely lost" 的内容:如果在程序终止之前所有对已分配内存的引用都丢失,Valgrind 将报告已分配内存为 "definitely lost"。换句话说,如果您的程序达到这样一种状态,即由于不存在指向它的有效指针而无法释放分配的内存,这将算作 "definitely lost".

这意味着像这样的程序:

int main(void) {
    char *buf = malloc(10);
    // ...
    exit(0);
}

将导致 Valgrind 无错误,而像这样的程序:

void func(void) {
    char *buf = malloc(10);
    // ...
} // memory is definitely lost here

int main(void) {
    func();
    exit(0);
}

会导致 "definitely lost" 错误。

为什么第一个版本适用于 Valgrind?这是因为系统在程序退出时 总是 释放内存。如果你一直使用分配的内存块直到程序结束,就真的没有必要显式调用 free() 了,它可以被认为只是浪费时间。出于这个原因,如果你不释放一些分配的块,同时仍然持有对它的引用,Valgrind 假设你这样做是为了避免 "useless" free() 因为你很聪明并且知道 OS 无论如何都会处理它。

但是,如果您忘记 free() 某些内容,并且丢失了对它的所有引用,那么 Valgrind 会警告您,因为您应该 free()d 内存。如果你不这样做,并且程序保持 运行ning,那么每次进入错误块时都会发生同样的事情,你最终会浪费内存。这就是所谓的"memory leak"。一个非常简单的例子如下:

void func(void) {
    char *buf = malloc(10);
    // ...
} // memory is definitely lost here

int main(void) {
    while (1) {
        func();
    }
    exit(0);
}

此程序会使您的机器 运行 内存不足,并最终可能导致您的系统停止运行或冻结(警告:如果您不想冒冻结您的 PC 的风险,请不要测试此程序)。如果您在 func 结束之前正确调用 free(buf),则程序会无限期地保持 运行ning 而不会出现问题。

你的程序发生了什么

现在让我们看看您在何处分配内存以及在何处声明保存引用的变量。程序分配内存的唯一部分是在 if (rc == 0) 块内,通过 strdup,此处:

char *myargs[3];
myargs[0] = strdup("wc");   // program: "wc" (word count)
myargs[1] = strdup("p4.c"); // argument: file to count

strdup() 的两次调用复制字符串并为此分配新内存。然后,您将对新分配内存的引用保存在 myargs 数组中,该数组在 if 块 中声明为 。如果您的程序在没有释放分配的内存的情况下退出该块,那么这些引用将会丢失,并且您的程序将无法释放内存。

execvp():你的子进程被新进程(wc p4.c)取代,父进程的内存space进程被操作系统丢弃(对于 Valgrind,这与程序终止完全​​相同)。此内存 而不是 被 Valgrind 计为丢失,因为在调用 execvp() 时,对已分配内存的引用仍然存在。注意:这是 而不是 因为您将指向已分配内存的指针传递给 execvp(),这是因为原始程序有效终止并且内存由 OS 保留。

没有execvp():你的子进程继续执行,并且在它退出定义myargs的代码块后,它 失去对已分配内存的任何引用 (因为 myargs[0]myargs[1] 是唯一的引用)。然后 Valgrind 将其正确报告为 "definitely lost",2 个块(2 个分配)中的 8 个字节("wc" 为 3 个字节,"p4.c" 为 5 个字节)。如果对 execvp() 的调用因任何原因失败,也会发生同样的事情。

其他注意事项

公平地说,没有必要在您显示的程序中调用 strdup()。这不像是因为它们在其他地方(或类似的地方)使用而需要复制这些字符串。代码可能只是:

myargs[0] = "wc";   // program: "wc" (word count)
myargs[1] = "p4.c"; // argument: file to count

无论如何,使用 exec*() 系列函数时的一个好习惯是直接在它后面放一个 exit(),以确保程序不会保留 运行 ning 以防 exec*() 失败。像这样:

execvp(myargs[0], myargs); 
perror("execvp failed");
exit(1);

exec() 系列函数用新的过程映像替换当前过程映像。这意味着调用进程当前正在 运行 的程序将被新程序替换,具有新初始化的堆栈、堆和(已初始化和未初始化的)数据段。

在子进程中,当你调用execvp()

execvp(myargs[0], myargs);

子进程替换为新进程(假设execvp()成功)和子进程中分配的内存

    myargs[0] = strdup("wc");   // program: "wc" (word count)
    myargs[1] = strdup("p4.c"); // argument: file to count

将被新进程有效回收。因此,当您在子进程中有 execvp() 时,valgrind 不会报告任何内存泄漏。

When I delete the execvp line, it will detect definitely lost: 8 bytes in 2 blocks. Why is this?

来自 strdup()[强调]

char * strdup(const char *str1); (dynamic memory TR)

Returns a pointer to a null-terminated byte string, which is a duplicate of the string pointed to by str1. The returned pointer must be passed to free to avoid a memory leak.

因此,当您的程序中没有 execvp() 调用时,子进程正在泄漏由 strdup() 分配的内存。要解决此问题,释放由 strdup() 分配的内存可能在 execvp() 之后,这样如果 execvp() 偶然失败或您明确地从程序中删除 execvp() 调用,它不应泄漏内存:

    myargs[0] = strdup("wc");   // program: "wc" (word count)
    myargs[1] = strdup("p4.c"); // argument: file to count
    myargs[2] = NULL;           // marks end of array
    execvp(myargs[0], myargs);  // runs word count
    printf ("execvp failed\n"); // You may want to print the errno as well
    free (myargs[0]);
    free (myargs[1]);

请注意,无需分配内存并将字符串文字复制到该内存。您可以直接将字符串文字分配给 myargs,像这样:

    myargs[0] = "wc";
    myargs[1] = "p4.c";