Prenons l'exemple suivant:
int main(void)
{
pid_t pid;
pid = fork();
if (pid == 0)
ChildProcess();
else
ParentProcess();
}
Donc corrigez-moi si je me trompe, une fois que fork () exécute un processus enfant est créé. Maintenant, par ceci answer fork () renvoie deux fois. C'est une fois pour le processus parent et une fois pour le processus enfant.
Ce qui signifie que deux processus distincts naissent PENDANT l'appel fork et non après sa fin.
Maintenant, je ne comprends pas comment il comprend comment retourner 0 pour le processus enfant et le PID correct pour le processus parent.
C'est là que ça devient vraiment déroutant. Cette réponse indique que fork () fonctionne en copiant les informations de contexte du processus et en définissant manuellement la valeur de retour sur 0.
D'abord, ai-je raison de dire que le retour à n'importe quelle fonction est placé dans un seul registre? Étant donné que dans un environnement à processeur unique, un processus ne peut appeler qu'un seul sous-programme qui ne renvoie qu'une seule valeur (corrigez-moi si je me trompe ici).
Disons que j'appelle une fonction foo () dans une routine et que cette fonction retourne une valeur, cette valeur sera stockée dans un registre disons BAR. Chaque fois qu'une fonction veut retourner une valeur, elle utilisera un registre de processeur particulier. Donc, si je suis capable de changer manuellement la valeur de retour dans le bloc de processus, je suis capable de changer la valeur retournée à la fonction non?
Alors ai-je raison de penser que c'est ainsi que fork () fonctionne?
Comment cela fonctionne est largement hors de propos - en tant que développeur travaillant à un certain niveau (c.-à-d. En codant pour les API UNIX), vous n'avez vraiment besoin que de savoir que cela fonctionne.
Cela dit cependant, et reconnaissant que la curiosité ou le besoin de comprendre à une certaine profondeur est généralement un bon trait à avoir, il existe un certain nombre de façons que cela pourrait être terminé.
Tout d'abord, votre affirmation selon laquelle une fonction ne peut renvoyer qu'une seule valeur est correcte dans la mesure où elle va, mais vous devez vous rappeler qu'après la division du processus, il y a en fait deux instances de la fonction en cours d'exécution, une dans chaque processus. Ils sont pour la plupart indépendants les uns des autres et peuvent suivre différents chemins de code. Le diagramme suivant peut aider à comprendre cela:
Process 314159 | Process 271828
-------------- | --------------
runs for a bit |
calls fork |
| comes into existence
returns 271828 | returns 0
Vous pouvez y voir, espérons-le, qu'une ( seule instance de fork
ne peut renvoyer qu'une seule valeur (comme pour toute autre fonction C), mais il y a en fait plusieurs instances en cours d'exécution, c'est pourquoi il est dit qu'il renvoie plusieurs valeurs dans la documentation.
Voici une possibilité sur la façon dont cela pourrait fonctionner.
Lorsque la fonction fork()
démarre, elle stocke l'ID de processus actuel (PID).
Ensuite, quand vient le temps de revenir, si le PID est le même que celui stocké, c'est le parent. Sinon, c'est l'enfant. Le pseudo-code suit:
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
Notez qu'il y a beaucoup de magie dans l'appel split_proc_into_two()
et cela ne fonctionnera certainement pas du tout de cette façon sous les couvertures(une). C'est juste pour illustrer les concepts qui l'entourent, qui est essentiellement:
Vous pouvez également consulter cette réponse , cela explique la philosophie fork/exec
.
(une) C'est presque certainement plus complexe que je ne l'ai expliqué. Par exemple, dans MINIX, l'appel à fork
finit par s'exécuter dans le noyau, qui a accès à l'arborescence de processus entière.
Il copie simplement la structure du processus parent dans un emplacement libre pour l'enfant, sur le modèle 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++;
Ensuite, il apporte de légères modifications à la structure enfant pour s'assurer qu'elle conviendra, y compris la ligne:
chld->p_reg[RET_REG] = 0; // make sure child receives zero
Donc, fondamentalement identique au schéma que j'ai posé, mais en utilisant des modifications de données plutôt que la sélection du chemin de code pour décider quoi retourner à l'appelant - en d'autres termes, vous verriez quelque chose comme:
return rpc->p_reg[RET_REG];
à la fin de fork()
afin que la valeur correcte soit renvoyée selon qu'il s'agit du processus parent ou enfant.
Sous Linux, fork()
se produit dans le noyau; l'endroit réel est le _do_fork
ici . Simplifié, l'appel système fork()
pourrait être quelque chose comme
pid_t sys_fork() {
pid_t child = create_child_copy();
wait_for_child_to_start();
return child;
}
Ainsi, dans le noyau, fork()
renvoie vraiment une fois, dans le processus parent. Cependant, le noyau crée également le processus enfant en tant que copie du processus parent; mais au lieu de revenir d'une fonction ordinaire, il créerait synthétiquement une nouvelle pile de noyau pour le thread nouvellement créé du processus enfant; puis changer de contexte sur ce thread (et ce processus); lorsque le processus nouvellement créé revient de la fonction de changement de contexte, le thread du processus enfant finit par revenir en mode utilisateur avec 0 comme valeur de retour de fork()
.
Fondamentalement, fork()
dans l'espace utilisateur n'est qu'un simple wrapper mince renvoie la valeur que le noyau a mise dans sa pile/dans le registre de retour. Le noyau configure le nouveau processus enfant afin qu'il renvoie 0 via ce mécanisme à partir de son seul thread; et le pid enfant est renvoyé dans l'appel système parent comme le serait toute autre valeur de retour de tout appel système tel que read(2)
.
Vous devez d'abord savoir comment fonctionne le multitâche. Il n'est pas utile de comprendre tous les détails, mais chaque processus s'exécute dans une sorte de machine virtuelle contrôlée par le noyau: un processus a sa propre mémoire, processeur et registres, etc. Il y a une correspondance de ces objets virtuels avec les vrais (la magie est dans le noyau), et il existe des machines qui échangent des contextes virtuels (processus) à la machine physique au fil du temps.
Ensuite, lorsque le noyau bifurque un processus (fork()
est une entrée du noyau), et crée une copie de presque tout le processus parent vers enfant processus, il est capable de modifier tout ce qui est nécessaire. L'un d'eux est la modification des structures correspondantes pour retourner 0 pour l'enfant et le pid de l'enfant dans le parent de l'appel en cours à fork.
Remarque: ne dites pas "fork renvoie deux fois", un appel de fonction ne renvoie qu'une seule fois.
Pensez à une machine de clonage: vous entrez seul, mais deux personnes sortent, l'une est vous et l'autre est votre clone (très légèrement différent); lors du clonage, la machine peut définir un nom différent du vôtre pour le clone.
L'appel système fork crée un nouveau processus et copie beaucoup d'état à partir du processus parent. Des choses comme la table des descripteurs de fichiers sont copiées, les mappages de mémoire et leur contenu, etc. Cet état est à l'intérieur du noyau.
L'une des choses que le noyau garde pour chaque processus sont les valeurs des registres que ce processus doit avoir restaurés au retour d'un appel système, d'une interruption, d'une interruption ou d'un changement de contexte (la plupart des changements de contexte se produisent lors d'appels système ou d'interruptions). Ces registres sont enregistrés sur un syscall/trap/interruption puis restaurés lors du retour au pays utilisateur. Les appels système retournent des valeurs en écrivant dans cet état. C'est ce que fait Fork. Le fork parent obtient une valeur, le processus enfant en a une autre.
Étant donné que le processus bifurqué est différent du processus parent, le noyau pourrait y faire n'importe quoi. Donnez-lui des valeurs dans les registres, donnez-lui des mappages de mémoire. Pour être sûr que presque tout sauf la valeur de retour est le même que dans le processus parent, il faut plus d'efforts.
Pour chaque processus en cours d'exécution, le noyau dispose d'une table de registres, à recharger lorsqu'un changement de contexte est effectué. fork()
est un appel système; un appel spécial qui, une fois effectué, le processus obtient un changement de contexte et le code du noyau exécutant l'appel s'exécute dans un thread différent (noyau).
La valeur renvoyée par les appels système est placée dans un registre spécial (EAX en x86) que votre application lit après l'appel. Lorsque l'appel fork()
est effectué, le noyau fait une copie du processus et dans chaque table de registres de chaque descripteur de processus écrit la valeur appropriée: 0 et le pid.