Как работает copy-on-write в fork()?
Я хочу знать, как происходит копирование на запись в fork ().
предполагая, что у нас есть процесс A, который имеет динамический массив int:
int *array = malloc(1000000*sizeof(int));
элементы в массиве инициализируются некоторыми значимыми значениями. Затем мы используем fork() для создания дочернего процесса, а именно B. B будет перебирать массив и выполнять некоторые вычисления:
for(a in array){
a = a+1;
}
- Я знаю, что B не будет копировать весь массив сразу, но когда дочерний B выделяет память для массива? в течение вызов Fork()?
- это выделить весь массив сразу, или только одно целое число к
a = a+1
? -
a = a+1;
как это происходит? Читает ли B данные из A и записывает новые данные в свой собственный массив?
я написал код, чтобы изучить, как работает корова. Моя среда: ubuntu 14.04, gcc4.8.2
#include <stdlib.h>
#include <stdio.h>
#include <sys/sysinfo.h>
void printMemStat(){
struct sysinfo si;
sysinfo(&si);
printf("===n");
printf("Total: %llun", si.totalram);
printf("Free: %llun", si.freeram);
}
int main(){
long len = 200000000;
long *array = malloc(len*sizeof(long));
long i = 0;
for(; i<len; i++){
array[i] = i;
}
printMemStat();
if(fork()==0){
/*child*/
printMemStat();
i = 0;
for(; i<len/2; i++){
array[i] = i+1;
}
printMemStat();
i = 0;
for(; i<len; i++){
array[i] = i+1;
}
printMemStat();
}else{
/*parent*/
int times=10;
while(times-- > 0){
sleep(1);
}
}
return 0;
}
после fork () дочерний процесс изменяет половину чисел в массиве, а затем изменяет весь массив. Выход являются:
===
Total: 16694571008
Free: 2129162240
===
Total: 16694571008
Free: 2126106624
===
Total: 16694571008
Free: 1325101056
===
Total: 16694571008
Free: 533794816
кажется, что массив не выделяется в целом. Если я немного изменю первый этап модификации:
i = 0;
for(; i<len/2; i++){
array[i*2] = i+1;
}
выходы будут:
===
Total: 16694571008
Free: 2129924096
===
Total: 16694571008
Free: 2126868480
===
Total: 16694571008
Free: 526987264
===
Total: 16694571008
Free: 526987264
2 ответов
зависит от операционной системы, аппаратной архитектуры и libc. Но да в случае недавнего Linux с MMU вилка(2) будет работать с copy-on-write. Он будет только (выделять и) копировать несколько системных структур и таблицу страниц, но страницы кучи фактически указывают на родительские, пока не будут написаны.
больше контроля над этим можно осуществлять с помощью клон(2) звонок. И vfork(2) beeing специальный вариант, который не ожидает страницы, которые будут использоваться. Обычно это используется перед exec ().
Что касается распределения: malloc() имеет метаинформацию по запрошенным блокам памяти (адрес и размер), а переменная C является указателем (как в куче памяти процесса, так и в стеках). Эти два выглядят одинаково для ребенка (одинаковые значения, потому что одна и та же базовая страница памяти видна в адресном пространстве обоих процессов). Таким образом, с точки зрения программы C массив уже выделен и переменная инициализирована, когда возникает процесс. Однако базовые страницы памяти указывают на исходные физические страницы родительского процесса, поэтому дополнительные страницы памяти не требуются, пока они не будут изменены.
Если дочерний элемент выделяет новый массив, это зависит от того, вписывается ли он в уже существующие страницы кучи или необходимо увеличить brk процесса. В обоих случаях копируются только измененные страницы, а новые страницы выделяются только для ребенка.
Это также означает, что физическая память может закончиться после malloc (). (Что плохо, так как программа не может проверить код возврата ошибки "операция в строке случайного кода"). Некоторые операционные системы не позволят эту форму overcommit: поэтому, если вы разветвляете процесс, он не будет выделять страницы, но он требует, чтобы они были доступны в данный момент (вид резервирует их) на всякий случай. В Linux это настраиваемый и называется overcommit-учет.
некоторые системы имеют системный вызов vfork()
, который был изначально
разработанный как более низкая-накладные расходы версия fork()
. С
fork()
участвует копирование всего адресного пространства процесса,
и поэтому было довольно дорого, был
введено (в 3.0 BSD).
vfork()
было введено,
реализация fork()
значительно улучшилось, в частности
с введением 'copy-on-write',где копирование из
адресное пространство процесса прозрачно подделано, позволяя обоим процессам
ссылаться на одну и ту же физическую память, пока они не изменятся
он. это в значительной степени устраняет оправдание vfork();
действительно, a
большая часть систем имеют оригинальный функционал
vfork()
полностью. Для совместимости, однако, все еще может быть
а vfork()
вызов присутствует, что просто называет fork()
без
попытка подражать всем vfork()
семантика.
в результате, очень неразумно фактически использовать любой из
различия между fork()
и vfork()
. Действительно, это
вероятно, неразумно использовать vfork()
вообще, если вы точно не знаете
почему ты хочешь.
основное различие между ними заключается в том, что при создании нового процесса с помощью vfork()
родительский процесс временно приостановлен, и дочерний процесс может занимать адресное пространство родителя. Это странное положение вещей продолжается до тех пор, пока процесс ребенка либо не завершится, либо звонки execve()
, в этот момент родительский процесс продолжается.
это означает, что дочерний процесс vfork()
должны быть осторожны, чтобы
избегайте неожиданного изменения переменных родительского процесса. В
в частности, дочерний процесс не должен возвращаться из функции
содержащий vfork()
вызов, и он не должен вызывать
exit()
(если ему нужно выйти, он должен использовать _exit();
на самом деле, это также верно для ребенка нормального fork()
).