具体来说,fork() 如何处理 Linux 中 malloc() 动态分配的内存?

Posted

技术标签:

【中文标题】具体来说,fork() 如何处理 Linux 中 malloc() 动态分配的内存?【英文标题】:Specifically, how does fork() handle dynamically allocated memory from malloc() in Linux? 【发布时间】:2011-06-03 15:07:42 【问题描述】:

我有一个包含父进程和子进程的程序。在 fork() 之前,父进程调用 malloc() 并用一些数据填充了一个数组。在 fork() 之后,孩子需要该数据。我知道我可以使用管道,但以下代码似乎可以工作:

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

int main( int argc, char *argv[] ) 
    char *array;
    array = malloc( 20 );
    strcpy( array, "Hello" );
    switch( fork() ) 
    case 0:
        printf( "Child array: %s\n", array );
        strcpy( array, "Goodbye" );
        printf( "Child array: %s\n", array );
        free( array );
        break;
    case -1:
        printf( "Error with fork()\n" );
        break;
    default:
        printf( "Parent array: %s\n", array );
        sleep(1);
        printf( "Parent array: %s\n", array );
        free( array );
    
    return 0;

输出是:

Parent array: Hello
Child array: Hello
Child array: Goodbye
Parent array: Hello

我知道在堆栈上分配的数据在子进程中可用,但在堆上分配的数据似乎也对子进程可用。同样,子进程不能修改父进程栈上的数据,子进程也不能修改父进程堆上的数据。所以我假设孩子获得了自己的堆栈和堆数据副本。

在 Linux 中总是这样吗?如果是这样,支持这一点的文档在哪里?我查看了 fork() 手册页,但没有特别提到堆上动态分配的内存。

【问题讨论】:

【参考方案1】:

为进程分配的每个页面(无论是上面有堆栈的虚拟内存页面还是堆)都会被复制,以便分叉的进程能够访问它。

实际上,它并没有在一开始就被复制,它被设置为 Copy-on-Write,这意味着一旦某个进程(父进程或子进程)尝试修改一个页面,它就会被复制,这样它们就不会伤害一个进程-another,并且仍然可以访问来自 fork() 点的所有数据。

例如,代码页,即实际可执行文件映射到内存中的代码页,通常是只读的,因此可以在所有分叉的进程中重用——它们不会被再次复制,因为没有人在那里写,只有读,因此永远不需要写时复制。

更多信息请见here 和here。

【讨论】:

明确一点,OP 的代码中没有 no 竞争条件,对吗? 不可能 - 进程无法再使用此内存进行通信。 吹毛求疵:考虑mmap(MAP_SHARED)shmat。这超出了 C 的“堆栈”和“堆”的范围,但您确实说过“每一页”...... @ephemient - 好吧,总是有一个挑剔的。你是对的,但我不想开始记录内核代码边缘情况:) 谢谢!这对我来说很有意义。就挑剔而言,共享内存是一种特殊情况,因为它必须可以跨多个进程进行读写访问。在我的程序中,我只需要读取权限,这就是为什么常规 malloc 对我有用,即使我正在从复制的页面中读取。【参考方案2】:

在 fork 之后,子节点完全独立于父节点,但可以继承某些父节点的副本。在堆的情况下,子节点在分叉时从概念上将拥有父堆的副本。但是,修改子地址空间中的头部只会修改子副本(例如通过写时复制)。

至于文档:我注意到文档通常会声明所有内容都是复制的,除了等等等等。

【讨论】:

【参考方案3】:

简短的回答是“写的很脏” - 较长的答案是......更长。

但是出于所有意图和目的-在 C 级别可以安全假设的工作模型是,在 fork() 之后,两个进程是绝对相同的 -- 即子进程获得 100% 精确的副本 --(但是在 fork()) 的返回值附近有一点点 - 然后随着每一方修改其内存、堆栈和堆而开始发散。

所以你的结论有点偏离 - 孩子开始时将与父母相同的数据复制到自己的空间中 - 然后对其进行修改 - 并将其视为已修改 - 而父母继续使用自己的副本。

实际上事情要复杂一些——因为它试图通过做一些肮脏的事情来避免完整的副本;避免复制,直到必须复制。

Dw.

【讨论】:

【参考方案4】:

该示例不起作用,因为父项未由子项更新。这是一个解决方案:

#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/shm.h>
#include <sys/stat.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/wait.h>

typedef struct

  int id;
  size_t size;
 shm_t;

shm_t *shm_new(size_t size)

  shm_t *shm = calloc(1, sizeof *shm);
  shm->size = size;

  if ((shm->id = shmget(IPC_PRIVATE, size, IPC_CREAT | IPC_EXCL | S_IRUSR | S_IWUSR)) < 0)
  
    perror("shmget");
    free(shm);
    return NULL;
  

  return shm;


void shm_write(shm_t *shm, void *data)

  void *shm_data;

  if ((shm_data = shmat(shm->id, NULL, 0)) == (void *) -1)
  
    perror("write");
    return;
  

  memcpy(shm_data, data, shm->size);
  shmdt(shm_data);


void shm_read(void *data, shm_t *shm)

  void *shm_data;

  if ((shm_data = shmat(shm->id, NULL, 0)) == (void *) -1)
  
    perror("read");
    return;
  
  memcpy(data, shm_data, shm->size);
  shmdt(shm_data);


void shm_del(shm_t *shm)

  shmctl(shm->id, IPC_RMID, 0);
  free(shm);


int main()

  void *array = malloc(20);
  strcpy((char *) array, "Hello");
  printf("parent: %s\n", (char *) array);
  shm_t *shm = shm_new(sizeof array);

  int pid;
  if ((pid = fork()) == 0)
   /* child */
    strcpy((char *) array, "Goodbye");
    shm_write(shm, array);
    printf("child: %s\n", (char *) array);
    return 0;
  
  /* Wait for child to return */
  int status;
  while (wait(&status) != pid);
  /* */
  shm_read(array, shm);
  /* Parent is updated by child */
  printf("parent: %s\n", (char *) array);
  shm_del(shm);
  free(array);
  return 0;

【讨论】:

以上是关于具体来说,fork() 如何处理 Linux 中 malloc() 动态分配的内存?的主要内容,如果未能解决你的问题,请参考以下文章

linux kernel 如何处理大小端

Nginx如何处理一个连接

如何处理 JavaScript Fetch 错误?

Linux中木马如何处理

Linux内核17-硬件如何处理中断和异常

Linux中的find和grep命令对查找到的文件如何处理啊