c-execvp的子进程内存释放问题



以下代码来自《操作系统:三个简单部分》一书。代码让我困惑。我知道execvp在运行良好的情况下永远不会返回。

#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 failedn");
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行时,它肯定会检测到丢失:2个块中有8个字节。为什么会这样?

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个释放,8809个字节分配

什么是"肯定丢失的"内存

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

这意味着像这样的程序:

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);
}

将导致"肯定丢失"的错误。

为什么第一个版本适用于Valgrind?这是因为内存总是在程序退出时由系统释放。如果您一直使用分配的内存块,直到程序结束,那么实际上不需要显式调用它的free(),这可以被认为只是浪费时间。因此,如果你在保留对某个已分配块的引用的同时没有释放该块,Valgrind会认为你这样做是为了避免"无用"的free(),因为你很聪明,知道操作系统无论如何都会处理它。

然而,如果你忘记了free(),并且失去了对它的所有引用,那么Valgrind会警告你,因为你应该有free()的记忆。如果你不这样做,并且程序一直在运行,那么每次进入bug块时都会发生同样的事情,你最终会浪费内存。这就是所谓的"内存泄漏"。一个非常简单的例子如下:

void func(void) {
char *buf = malloc(10);
// ...
} // memory is definitely lost here
int main(void) {
while (1) {
func();
}
exit(0);
}

这个程序会让你的机器内存不足,最终可能会杀死或冻结你的系统(警告:如果你不想冒冻结电脑的风险,请不要测试这个)。如果在func结束之前正确地调用free(buf),那么程序将无限期地运行而不会出现问题。

你的程序中发生了什么

现在,让我们看看在哪里分配内存,以及在哪里声明包含引用的变量。程序中唯一分配内存的部分位于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)替换,父进程的内存空间被操作系统丢弃(对于Valgrind,这与程序终止完全相同)。Valgrind不会将该内存计入丢失,因为调用execvp()时,对分配内存的引用仍然存在。注意:这是而不是,因为您将指向已分配内存的指针传递给execvp(),这是因为原始程序实际上终止了,内存由操作系统保留。

如果没有execvp():您的子进程继续执行,并且在它退出定义myargs的代码块之后,它将丢失对分配内存的任何引用(因为myargs[0]myargs[1]是唯一的引用)。Valgrind随后正确地将其报告为"肯定丢失",在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(),以确保程序在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);(动态存储器TR)

返回指向以null结尾的字节字符串的指针,该字符串与str1所指向的字符串重复必须将返回的指针传递给free,以避免内存泄漏

因此,当程序中没有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 failedn"); // You may want to print the errno as well
free (myargs[0]);
free (myargs[1]);

请注意,不需要分配内存并将字符串文字复制到该内存中。您可以直接将字符串文字分配给myargs,如下所示:

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

最新更新