我明白写时复制背后的想法。当我 fork 时,堆被标记为 CoW,并且当任何进程试图更改它时,都会制作一个副本。问题是:我是否必须在孩子的过程中释放它?假设一个父级有一个动态的 char *array,那么它会分叉。一个子进程打印一些 const char,然后退出。子进程根本没有改变堆。会不会有内存泄漏?
编辑:我的子进程在堆上打印数组,但不修改它。Valgrind 说如果我不释放那个数组就会有泄漏。释放它时没有泄漏/内存错误。
我明白写时复制背后的想法。当我 fork 时,堆被标记为 CoW,并且当任何进程试图更改它时,都会制作一个副本。问题是:我是否必须在孩子的过程中释放它?假设一个父级有一个动态的 char *array,那么它会分叉。一个子进程打印一些 const char,然后退出。子进程根本没有改变堆。会不会有内存泄漏?
编辑:我的子进程在堆上打印数组,但不修改它。Valgrind 说如果我不释放那个数组就会有泄漏。释放它时没有泄漏/内存错误。
CoW 只是一个惰性优化。您可能会自由地认为,fork()
总是会毫不拖延地制作过程的完整副本(至少在内存方面)。但是……</p>
如果您确实准备了动态数据块以“传递”给 fork 的子代,那么在 fork 之后您有两个带有两个动态数据块的进程:父代和子代(都是副本)。当孩子退出时,它的内存副本被回收,但父母应该在 fork 之后立即释放该块。
为了更清楚,这里有一个例子:
char *buf = malloc(123456);
// … fill buf for child …
int res = fork();
if (res == -1) {
fprintf(stderr, "fork failed\n");
exit(EXIT_FAILURE);
}
if (res == 0) {
// this is child process
// … do work with buf …
_Exit(EXIT_SUCCESS); // child reclaims buf by means of exit
}
// this is parent process
free(buf); // we don't need it in parent
// … other parent tasks here …
CoW 在 fork-exec 技术中也是非常有用的优化,在这种技术中,child 只做exec
准备好的参数。exec
用指定的可执行映像替换当前进程,保留打开的描述符和其他内容(更多内容见man 2 execve
)。在这样的 fork 之后复制的唯一页面只是当前堆栈帧,这使得 fork-exec 非常有效。
一些系统还提供vfork
了非常严格的不公平的 fork 版本,但在没有 CoW 的系统上,这是有效执行 vfork-exec 的唯一方法。
首先是逻辑(以流程为中心)视图:
当你 fork 一个进程时,整个地址空间被原样复制到一个新进程中。您的堆在两个进程中基本上是重复的,并且两个进程都可以继续使用它,就像一个进程fork()
从未被调用过一样。两个进程都可以释放在 之前完成的分配,fork()
如果他们想重用连接到分配的地址范围,它们必须这样做。CoW 映射只是一种不会改变这些语义的优化。
现在是物理(以系统为中心)视图:
您的系统内核不知道您使用分配的数据范围malloc()
,它只知道它在请求时分配给进程的内存页malloc()
。当您调用fork()
它时,它将所有这些页面标记为 CoW,并从两个进程中引用它们。如果两个进程中的任何一个写入任何 CoW 页面,而另一个进程仍然存在,它将陷入复制整个页面的系统。如果其中一个进程退出,它至少会降低这些页面的引用计数,这样就不必再复制它们了。
那么,当您free()
在退出之前调用孩子时会发生什么?
好吧,该free()
函数很可能会写入包含内存分配的页面,以告知malloc()
该块再次可用。这将陷入系统并复制页面,预计此操作需要一两微秒。free()
如果你的父进程在孩子还活着的时候调用,同样的情况也会发生。但是,如果您的孩子没有释放页面并退出,内核将知道它不再需要执行 CoW。如果父级之后释放并重用内存区域,则不需要进行复制。
我假设,您的孩子所做的只是检查某些错误条件,如果满足则立即退出。在这种情况下,最谨慎的做法是忘记呼唤free()
孩子,让系统完成它的工作。