valor - ¿Cómo sabe Fork() cuándo devolver 0?




valor del tag (4)

Toma el siguiente ejemplo:

int main(void)
{
     pid_t  pid;

     pid = fork();
     if (pid == 0) 
          ChildProcess();
     else 
          ParentProcess();
}

Así que corrígeme si estoy equivocado, una vez que fork () ejecuta un proceso secundario se crea. Ahora, yendo por esta answer fork () regresa dos veces. Eso es una vez para el proceso principal y una vez para el proceso hijo.

Lo que significa que dos procesos separados entran en existencia DURANTE la llamada de horquilla y no después de que termine.

Ahora no entiendo cómo entiende cómo devolver 0 para el proceso secundario y el PID correcto para el proceso principal.

Esto es realmente confuso. Esta answer indica que fork () funciona copiando la información de contexto del proceso y configurando manualmente el valor de retorno en 0.

Primero, ¿tengo razón al decir que el retorno a cualquier función se coloca en un solo registro? Dado que en un entorno de procesador único un proceso puede llamar a una sola subrutina que devuelve solo un valor (corríjame si me equivoco aquí).

Digamos que invoco una función foo () dentro de una rutina y esa función devuelve un valor, ese valor se almacenará en un registro, por ejemplo, BAR. Cada vez que una función quiere devolver un valor, usará un registro de procesador particular. Entonces, si soy capaz de cambiar manualmente el valor de retorno en el bloque de proceso, puedo cambiar el valor devuelto a la función ¿correcto?

Entonces, ¿estoy en lo cierto al pensar que es así como funciona fork ()?


En Linux fork() ocurre en kernel; el lugar real es el _do_fork aquí . Simplificado, la llamada al sistema fork() podría ser algo así como

pid_t sys_fork() {
    pid_t child = create_child_copy();
    wait_for_child_to_start();
    return child;
}

Entonces en el kernel, fork() realmente regresa una vez , al proceso padre. Sin embargo, el kernel también crea el proceso hijo como una copia del proceso principal; pero en lugar de regresar de una función ordinaria, crearía sintéticamente una nueva pila de kernel para el subproceso recién creado del proceso hijo; y luego cambiar de contexto a ese hilo (y proceso); como el proceso recién creado vuelve de la función de cambio de contexto, haría que el proceso hijo 'finalizara retornando al modo de usuario con 0 como el valor de retorno de fork() .

Básicamente, fork() en userland es solo una envoltura delgada que devuelve el valor que el núcleo pone en su pila / en el registro de devolución. El kernel configura el nuevo proceso hijo para que devuelva 0 a través de este mecanismo desde su único hilo; y el niño pid se devuelve en la llamada al sistema principal como cualquier otro valor de retorno de cualquier llamada al sistema, como read(2) .


La llamada al sistema fork crea un nuevo proceso y copia una gran cantidad de estado del proceso principal. Se copian cosas como la tabla de descriptores de archivos, las asignaciones de memoria y sus contenidos, etc. Ese estado está dentro del kernel.

Una de las cosas que el núcleo realiza un seguimiento de cada proceso son los valores de los registros que este proceso necesita restaurar a la vuelta de una llamada al sistema, interrupción, interrupción o cambio de contexto (la mayoría de los cambios de contexto ocurren en las llamadas o interrupciones del sistema). Esos registros se guardan en un syscall / trap / interrupt y luego se restauran al regresar a userland. El sistema llama valores devueltos al escribir en ese estado. Que es lo que hace el tenedor. El tenedor principal obtiene un valor, el proceso secundario es diferente.

Como el proceso bifurcado es diferente del proceso principal, el kernel podría hacer algo al respecto. Dale cualquier valor en los registros, dale cualquier mapeo de memoria. Para asegurarse de que casi todo, excepto el valor de retorno es el mismo que en el proceso principal requiere un mayor esfuerzo.


Primero necesita saber cómo funciona la multitarea. No es útil comprender todos los detalles, pero cada proceso se ejecuta en una especie de máquina virtual controlada por el núcleo: un proceso tiene su propia memoria, procesador y registros, etc. Hay un mapeo de estos objetos virtuales sobre los reales. (la magia está en el kernel), y hay alguna maquinaria que intercambia contextos virtuales (procesos) a la máquina física a medida que pasa el tiempo.

Luego, cuando el núcleo bifurca un proceso ( fork() es una entrada al kernel) y crea una copia de casi todo en el proceso principal para el proceso secundario , puede modificar todo lo necesario. Una de ellas es la modificación de las estructuras correspondientes para devolver 0 para el niño y el pid del niño en el padre de la llamada actual al tenedor.

Nota: nether say "fork vuelve dos veces", una llamada a función solo vuelve una vez.

Solo piense en una máquina de clonación: ingresa solo, pero dos personas salen, una es usted y la otra es su clon (muy ligeramente diferente); durante la clonación, la máquina puede establecer un nombre diferente al suyo para el clon.


Cómo funciona es en gran medida irrelevante: como desarrollador que trabaja en un cierto nivel (es decir, que codifica las API de UNIX), solo necesita saber que funciona.

Sin embargo, una vez dicho esto, y reconociendo que la curiosidad o la necesidad de comprender a cierta profundidad es generalmente un buen rasgo, hay varias maneras en que esto se puede hacer.

En primer lugar, su afirmación de que una función solo puede devolver un valor es correcta hasta donde sea posible, pero debe recordar que, después de la división del proceso, en realidad hay dos instancias de la función ejecutándose, una en cada proceso. Son en su mayoría independientes entre sí y pueden seguir diferentes rutas de código. El siguiente diagrama puede ayudar a entender esto:

Process 314159 | Process 271828
-------------- | --------------
runs for a bit |
calls fork     |
               | comes into existence
returns 271828 | returns 0

Es de esperar que vea que una sola instancia de fork solo puede devolver un valor (como cualquier otra función C), pero en realidad hay varias instancias en ejecución, por lo que se dice que devuelve múltiples valores en la documentación.

Aquí hay una posibilidad sobre cómo podría funcionar.

Cuando la función fork() comienza a ejecutarse, almacena la ID del proceso actual (PID).

Luego, cuando llega el momento de regresar, si el PID es el mismo que el almacenado, es el padre. De lo contrario, es el niño. El pseudo-código sigue:

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

Tenga en cuenta que hay una gran cantidad de magia en la llamada split_proc_into_two() y es casi seguro que no funcionará de esa manera bajo las cubiertas (a) . Es solo para ilustrar los conceptos a su alrededor, que es básicamente:

  • obtener el PID original antes de la división, que seguirá siendo idéntico para ambos procesos después de que se dividan.
  • haz la división
  • obtener el PID actual después de la división, que será diferente en los dos procesos.

También es posible que desee echar un vistazo a esta respuesta , que explica la filosofía fork/exec .

(a) Es casi seguro más complejo de lo que he explicado. Por ejemplo, en MINIX, la llamada al fork termina ejecutándose en el kernel, que tiene acceso a todo el árbol de procesos.

Simplemente copia la estructura del proceso principal en un espacio libre para el niño, a lo largo de las líneas de:

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++;

Luego realiza ligeras modificaciones en la estructura del niño para asegurarse de que sea adecuada, incluida la línea:

chld->p_reg[RET_REG] = 0;       // make sure child receives zero

Por lo tanto, básicamente idéntico al esquema que propuse, pero usando modificaciones de datos en lugar de selección de ruta de código para decidir qué devolver a la persona que llama, en otras palabras, vería algo como:

return rpc->p_reg[RET_REG];

al final de la fork() para que se devuelva el valor correcto según si se trata del proceso principal o secundario.







internals