Как fork() знает, когда возвращать 0?
Возьмем следующий пример:
int main(void)
{
pid_t pid;
pid = fork();
if (pid == 0)
ChildProcess();
else
ParentProcess();
}
Так исправьте меня, если я ошибаюсь, один раз fork() выполняет дочерний процесс. Теперь переход от answer fork() возвращается дважды. Это один раз для родительского процесса и один раз для дочернего процесса.
Это означает, что возникают два отдельных процесса ВО ВРЕМЯ вызова вилки, а не после его окончания.
Теперь я не понимаю, как он понимает, как вернуть 0 для дочернего процесса и правильный PID для родительского процесса.
Это, где это становится действительно запутанным. В этом ответе указано, что fork() работает, копируя контекстную информацию процесса и вручную устанавливая возвращаемое значение на 0.
Вначале я правильно говорю, что возврат к любой функции помещается в один регистр? Поскольку в одной процессорной среде процесс может вызывать только одну подпрограмму, которая возвращает только одно значение (исправьте меня, если я ошибаюсь здесь).
Предположим, что я вызываю функцию foo() внутри подпрограммы и эта функция возвращает значение, это значение будет храниться в регистре, скажем, BAR. Каждый раз, когда функция хочет вернуть значение, он будет использовать конкретный регистр процессора. Итак, если я могу вручную изменить возвращаемое значение в блоке процесса, я могу изменить значение, возвращаемое функции справа?
Как я правильно понимаю, как работает fork()?
Ответы
Ответ 1
Как это работает, в значительной степени не имеет значения - как разработчик, работающий на определенном уровне (например, кодирование для UNIX API), вам действительно нужно только знать, что он работает.
Сказав, что, однако, и признавая, что любопытство или необходимость понимать на некоторой глубине, как правило, является хорошей чертой, существует множество способов, которыми это можно было бы сделать.
Во-первых, ваше утверждение о том, что функция может возвращать только одно значение, верна, насколько это возможно, но вам нужно помнить, что после разделения процесса на самом деле есть два экземпляра функции, по одному в каждом процессе. Они в основном независимы друг от друга и могут следовать различным кодам. Следующая схема может помочь в понимании этого:
Process 314159 | Process 271828
-------------- | --------------
runs for a bit |
calls fork |
| comes into existence
returns 271828 | returns 0
Можно с уверенностью видеть, что один экземпляр fork
может возвращать только одно значение (как и любая другая функция C), но на самом деле выполняется несколько экземпляров, поэтому он сказал, что возвращает несколько значений в документации.
Здесь есть одна возможность о том, как она может работать.
Когда функция fork()
запускается, она сохраняет текущий идентификатор процесса (PID).
Затем, когда приходит время для возврата, если PID совпадает с сохраненным, это родительский. В противном случае это ребенок. Ниже следует псевдокод:
def fork():
saved_pid = getpid()
# Magic here, returns PID of other process or -1 on failure.
other_pid = split_proc_into_two();
if other_pid == -1: # fork failed -> return -1
return -1
if saved_pid == getpid(): # pid same, parent -> return child PID
return other_pid
return 0 # pid changed, child, return zero
Обратите внимание, что в вызове split_proc_into_two()
есть много магии, и почти наверняка это не будет работать под обложками (a). Это просто для иллюстрации концепций вокруг него, которое в основном:
- получить исходный PID до разделения, который останется идентичным для обоих процессов после их разделения.
- выполните разделение.
- получить текущий PID после раскола, который будет отличаться в двух процессах.
Вы также можете взглянуть на этот ответ, он объясняет философию fork/exec
.
(a) Это почти наверняка сложнее, чем я объяснил. Например, в MINIX вызов fork
завершается в ядре, которое имеет доступ ко всему дереву процесса.
Он просто копирует родительскую структуру процесса в свободный слот для дочернего элемента по строкам:
sptr = (char *) proc_addr (k1); // parent pointer
chld = (char *) proc_addr (k2); // child pointer
dptr = chld;
bytes = sizeof (struct proc); // bytes to copy
while (bytes--) // copy the structure
*dptr++ = *sptr++;
Затем он вносит небольшие изменения в дочернюю структуру, чтобы гарантировать, что он будет подходящим, включая строку:
chld->p_reg[RET_REG] = 0; // make sure child receives zero
Итак, в основном, идентичная схеме, которую я поставил, но используя модификации данных, а не выбор пути кода, чтобы решить, что вернуться к вызывающей стороне - другими словами, вы увидите что-то вроде:
return rpc->p_reg[RET_REG];
в конце fork()
, чтобы возвращаемое значение возвращалось в зависимости от того, является ли это родительским или дочерним процессом.
Ответ 2
В Linux fork()
происходит в ядре; фактическое место здесь _do_fork
здесь. Упрощенный, системный вызов fork()
может быть чем-то вроде
pid_t sys_fork() {
pid_t child = create_child_copy();
wait_for_child_to_start();
return child;
}
Итак, в ядре fork()
действительно возвращает один раз в родительский процесс. Однако ядро также создает дочерний процесс как копию родительского процесса; но вместо того, чтобы возвращаться из обычной функции, он синтетически создавал новый стек ядра для вновь созданного потока дочернего процесса; а затем контекст-переключиться на этот поток (и процесс); поскольку вновь созданный процесс возвращается из функции переключения контекста, это приведет к тому, что поток дочернего процесса вернется в пользовательский режим с 0 в качестве возвращаемого значения из fork()
.
В основном fork()
в userland только тонкая обертка возвращает значение, которое ядро помещает в свой стек/в регистр возврата. Ядро устанавливает новый дочерний процесс так, чтобы он возвращал 0 через этот механизм из единственного потока; и дочерний pid возвращается в родительском системном вызове, поскольку любое другое возвращаемое значение из любого системного вызова, такого как read(2)
, будет.
Ответ 3
Сначала вам нужно знать, как работает многозадачность. Нецелесообразно понимать все детали, но каждый процесс выполняется на какой-то виртуальной машине, управляемой ядром: процесс имеет свою собственную память, процессор и регистры и т.д. Существует сопоставление этих виртуальных объектов с реальными (магия находится в ядре), и есть несколько механизмов, которые меняют виртуальные контексты (процессы) на физическую машину с течением времени.
Затем, когда ядро запускает процесс (fork()
- это запись в ядро) и создает копию почти всего в родительском процессе дочернего процесса, он может модифицировать все необходимое. Одним из них является модификация соответствующих структур для возврата 0 для дочернего элемента и pid дочернего элемента родителя из текущего вызова в fork.
Примечание: nether говорит, что "fork возвращается дважды", вызов функции возвращается только один раз.
Просто подумайте о клонирующей машине: вы входите в одиночку, но два человека выходят, один - вы, а другой - ваш клон (очень немного другой); в то время как клонирование машины способно установить имя, отличное от вашего, к клону.
Ответ 4
Системный вызов fork создает новый процесс и копирует много состояний из родительского процесса. Такие вещи, как таблица дескриптора файла, копируются, сопоставления памяти и их содержимое и т.д. Это состояние находится внутри ядра.
Одна из вещей, которую ядро отслеживает для каждого процесса, - это значения регистров, которые этот процесс должен восстановить при возврате из системного вызова, прерывания, прерывания или контекстного переключателя (большинство контекстных переключений происходит при системных вызовах или прерываниях), Эти регистры сохраняются в syscall/trap/interrupt и затем восстанавливаются при возврате в пользовательскую область. Системные вызовы возвращают значения, записывая в это состояние. Это то, что делает вилка. Родительская вилка получает одно значение, дочерний процесс - другой.
Так как разветвленный процесс отличается от родительского процесса, ядро может что-то сделать для него. Дайте ему любые значения в регистрах, дайте ему любые сопоставления памяти. Чтобы убедиться в том, что почти все, кроме возвращаемого значения, те же, что и в родительском процессе, требуют больше усилий.
Ответ 5
Для каждого запущенного процесса ядро имеет таблицу регистров, для загрузки обратно при создании контекстного переключателя. fork()
- системный вызов; специальный вызов, который, когда он сделан, обрабатывает контекстный переключатель, а код ядра, выполняющий вызов, запускается в другом (ядро) потоке.
Значение, возвращаемое системными вызовами, помещается в специальный регистр (EAX в x86), который ваше приложение считывает после вызова. Когда выполняется вызов fork()
, ядро копирует процесс, и в каждой таблице регистров каждого дескриптора процесса записывается соответствующее значение: 0 и pid.