В частности, как fork() обрабатывает динамически выделяемую память из malloc() в Linux?

У меня есть программа с родительским и дочерним процессом. Перед 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(), но там конкретно не упоминалась динамическая память в куче.


person pcd6623    schedule 04.01.2011    source источник


Ответы (4)


Каждая страница, выделенная для процесса (будь то страница виртуальной памяти, на которой находится стек или куча), копируется, чтобы разветвленный процесс мог получить к ней доступ.

На самом деле, он не копируется в самом начале, он настроен на копирование при записи, что означает, что как только один из процессов (родительский или дочерний) попытается изменить страницу, она будет скопирована, чтобы они не причиняли вреда друг другу, и по-прежнему иметь все данные с точки fork() доступными для них.

Например, кодовые страницы, на которые фактический исполняемый файл был сопоставлен в памяти, обычно доступны только для чтения и, таким образом, повторно используются всеми разветвленными процессами — они не будут копироваться повторно, так как туда никто не пишет, а только читает и т. д. копирование при записи никогда не понадобится.

Дополнительная информация доступна здесь и здесь.

person abyx    schedule 04.01.2011
comment
Просто для ясности: в коде OP нет состояния гонки, верно? - person SiegeX; 04.01.2011
comment
Не может быть - процессы больше не могут общаться, используя эту память. - person abyx; 04.01.2011
comment
Придирка: рассмотрите mmap(MAP_SHARED) и shmat. Что выходит за рамки стека и кучи C, но вы сказали, что каждая страница... - person ephemient; 05.01.2011
comment
@ephemient - Ну, всегда должна быть придирка. Вы правы, но я не хотел начинать документировать крайние случаи кода ядра :) - person abyx; 05.01.2011
comment
Спасибо! Это имеет смысл для меня. Что касается придирки, разделяемая память — это особый случай, поскольку она должна быть доступна для чтения и записи для нескольких процессов. В моей программе мне нужен только доступ на чтение, поэтому у меня работает обычный malloc, даже если я читаю с скопированной страницы. - person pcd6623; 05.01.2011

После разветвления потомок полностью независим от родителя, но может наследовать некоторые вещи, являющиеся копиями родителя. В случае кучи дочерний элемент концептуально будет иметь копию родительской кучи во время разветвления. Однако изменения заголовка в дочернем адресном пространстве будут изменять только дочернюю копию (например, посредством копирования при записи).

Что касается документации: я заметил, что в документации обычно указывается, что копируется все, кроме бла-бла-бла.

person Noah Watkins    schedule 04.01.2011

Короткий ответ: «грязно при записи», а более длинный ответ… намного длиннее.

Но для всех намерений и целей - рабочая модель, которую можно с уверенностью предположить на уровне C, состоит в том, что сразу после fork() два процесса абсолютно идентичны - т. е. дочерний процесс получает 100% точную копию - (но для крошечного немного вокруг возвращаемого значения fork()), а затем начинают расходиться, поскольку каждая сторона изменяет свою память, стек и кучи.

Таким образом, ваш вывод немного неверен: дочерний элемент начинает с тех же данных, что и родитель, скопированных в свое собственное пространство, затем изменяет их и видит их измененными, в то время как родитель продолжает свою собственную копию.

На самом деле все немного сложнее - поскольку он пытается избежать полной копии, делая что-то грязное; избегая копирования до тех пор, пока это необходимо.

Dw.

person Dirk-Willem van Gulik    schedule 04.01.2011

Пример не работает, потому что родитель не обновляется потомком. Вот решение:

#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;
}
person OS2    schedule 14.08.2020