Как 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.