Perché dobbiamo creare fork per creare nuovi processi?


95

In Unix ogni volta che vogliamo creare un nuovo processo, eseguiamo il fork del processo corrente, creando un nuovo processo figlio che è esattamente lo stesso del processo padre; quindi eseguiamo una chiamata di sistema exec per sostituire tutti i dati dal processo padre con quelli per il nuovo processo.

Perché in primo luogo creiamo una copia del processo genitore e non creiamo direttamente un nuovo processo?


Risposte:


61

La risposta breve è, forkè in Unix perché all'epoca era facile adattarsi al sistema esistente e perché un sistema precedente a Berkeley aveva usato il concetto di forcelle.

Da The Evolution of the Unix Time-sharing System (è stato evidenziato il testo pertinente ):

Il controllo di processo nella sua forma moderna è stato progettato e implementato in un paio di giorni. È sorprendente quanto facilmente si inserisse nel sistema esistente; allo stesso tempo è facile vedere come alcune delle caratteristiche leggermente insolite del design siano presenti proprio perché rappresentavano piccole modifiche facilmente codificabili a ciò che esisteva . Un buon esempio è la separazione delle funzioni fork e exec. Il modello più comune per la creazione di nuovi processi prevede la specifica di un programma per l'esecuzione del processo; in Unix, un processo a fork continua a eseguire lo stesso programma del suo genitore fino a quando non esegue un exec esplicito. La separazione delle funzioni non è certamente unica per Unix, e infatti era presente nel sistema di condivisione del tempo Berkeley, che era ben noto a Thompson. Tuttavia, sembra ragionevole supporre che esista in Unix principalmente a causa della facilità con cui il fork potrebbe essere implementato senza cambiare molto altro . Il sistema gestiva già più processi (ovvero due); c'era una tabella dei processi e i processi sono stati scambiati tra la memoria principale e il disco. È richiesta solo l'implementazione iniziale di fork

1) Espansione della tabella dei processi

2) Aggiunta di una chiamata fork che ha copiato il processo corrente nell'area di scambio del disco, usando le primitive IO di scambio già esistenti e apportato alcune modifiche alla tabella del processo.

In effetti, il fork call del PDP-7 ha richiesto esattamente 27 righe di codice assembly. Naturalmente, erano necessari altri cambiamenti nel sistema operativo e nei programmi utente, e alcuni di essi erano piuttosto interessanti e inaspettati. Ma un fork-exec combinato sarebbe stato notevolmente più complicato , se non altro perché exec in quanto tale non esisteva; la sua funzione era già stata eseguita, usando IO esplicito, dalla shell.

Da quel momento, Unix si è evoluto. forkseguito da execnon è più l'unico modo per eseguire un programma.

  • vfork è stato creato per essere un fork più efficiente nel caso in cui il nuovo processo intenda eseguire un exec subito dopo il fork. Dopo aver eseguito un vfork, i processi padre e figlio condividono lo stesso spazio dati e il processo padre viene sospeso fino a quando il processo figlio non esegue un programma o esce.

  • posix_spawn crea un nuovo processo ed esegue un file in una singola chiamata di sistema. Prende una serie di parametri che consentono di condividere selettivamente i file aperti del chiamante e di copiarne la disposizione del segnale e altri attributi nel nuovo processo.


5
Bella risposta ma aggiungerei che vfork non dovrebbe più essere usato. La differenza di prestazioni è ora marginale e il suo utilizzo può essere pericoloso. Vedi questa domanda SO stackoverflow.com/questions/4856255/… , questo sito ewontfix.com/7 e "Advanced Unix Programming" pagina 299 su vfork
Raphael Ahrens,

4
Le macchinazioni (impostazione della struttura dei dati) necessarie per posix_spawn()eseguire gli stessi lavori di sostituzione post-fork che possono essere eseguiti facilmente utilizzando il fork()codice inline rendono un argomento convincente per fork()essere molto più semplice da utilizzare.
Jonathan Leffler,

34

[Ripeterò parte della mia risposta da qui .]

Perché non avere semplicemente un comando che crea un nuovo processo da zero? Non è assurdo e inefficiente copiarne uno che verrà sostituito immediatamente?

In effetti, probabilmente non sarebbe altrettanto efficiente per alcuni motivi:

  1. La "copia" prodotta da fork()è un po 'di un'astrazione, dal momento che il kernel usa un copy-on-write del sistema ; tutto ciò che deve davvero essere creato è una mappa di memoria virtuale. Se la copia chiama immediatamente exec(), la maggior parte dei dati che sarebbero stati copiati se fosse stata modificata dall'attività del processo non deve mai essere effettivamente copiata / creata perché il processo non fa nulla che richieda il suo utilizzo.

  2. Vari aspetti significativi del processo figlio (ad esempio, il suo ambiente) non devono essere duplicati individualmente o impostati sulla base di un'analisi complessa del contesto, ecc. Si presume che siano gli stessi di quelli del processo chiamante e questo è il sistema abbastanza intuitivo che conosciamo.

Per spiegare un po 'di più il numero 1, la memoria che viene "copiata" ma a cui non si accede successivamente non viene mai realmente copiata, almeno nella maggior parte dei casi. Un'eccezione in questo contesto potrebbe essere se si è proceduto a un fork di un processo, quindi il processo padre è stato chiuso prima che il figlio si sostituisse exec(). Dico che potrebbe essere perché la maggior parte dei genitori potrebbe essere memorizzata nella cache se c'è abbastanza memoria libera, e non sono sicuro di quanto questo sarebbe sfruttato (che dipenderebbe dall'implementazione del sistema operativo).

Ovviamente, ciò non rende in superficie più efficiente l' uso di una copia rispetto all'utilizzo di una lavagna vuota, tranne per il fatto che "la lavagna vuota" non è letteralmente nulla e deve comportare un'allocazione. Il sistema potrebbe avere un modello di processo vuoto / nuovo generico che copia allo stesso modo, 1 ma che quindi non salverebbe realmente nulla rispetto al fork di copia su scrittura. Quindi il n. 1 dimostra solo che l'utilizzo di un "nuovo" processo vuoto non sarebbe più efficiente.

Il punto 2 spiega perché l'uso della forcella è probabilmente più efficiente. L'ambiente di un bambino viene ereditato dal suo genitore, anche se è un eseguibile completamente diverso. Ad esempio, se il processo padre è una shell e il figlio un browser Web $HOMEè sempre lo stesso per entrambi, ma poiché entrambi potrebbero successivamente modificarlo, devono essere due copie separate. Quello nel bambino è prodotto dall'originale fork().

1. Una strategia che potrebbe non avere molto senso letterale, ma il mio punto è che la creazione di un processo implica più che copiare la sua immagine in memoria dal disco.


3
Sebbene entrambi i punti siano veri, nessuno dei due supporta il motivo per cui è stato scelto il metodo di fork piuttosto che ripetere un nuovo processo da un determinato eseguibile.
SkyDan,

3
Penso che questo risponda alla domanda. Fork viene utilizzato perché, nei casi in cui la creazione di un nuovo processo è la strada più efficiente, il costo dell'utilizzo di Fork è invece banale (probabilmente meno dell'1% del costo di creazione del processo). D'altra parte, ci sono molti posti in cui fork è notevolmente più efficiente o molto più semplice di un'API (come la gestione degli handle di file). La decisione presa da Unix è stata quella di supportare solo un'API, semplificando le specifiche.
Cort Ammon,

1
@SkyDan Hai ragione, è più una risposta al perché non piuttosto al perché , a cui Mark Plotnick risponde più direttamente - che interpreterei nel senso non solo che questa era la scelta più semplice, ma anche che era probabilmente la più efficiente scelta (secondo la citazione di Dennis Richie: "il fork call del PDP-7 ha richiesto esattamente 27 linee di assemblaggio ... exec in quanto tale non esisteva; la sua funzione era già stata eseguita"). Quindi questo "perché no" è davvero una riflessione su due strategie in cui una appare superficialmente più semplice ed efficiente, quando forse non lo è (testimonia il destino
incerto

1
Goldilocks è corretto. Ci sono situazioni in cui il fork e la modifica sono più economici della creazione di uno nuovo da zero. L'esempio più estremo, ovviamente, è ogni volta che vuoi un comportamento a forcella. fork()può farlo molto rapidamente (come menzionato GL, nell'ordine di 27 linee di assemblaggio). Guardando nella direzione opposta, se si desidera un "creare un processo da zero", fork()costa solo un po 'di più rispetto a partire da un processo vuoto creato (27 linee di assemblaggio + costo di chiusura degli handle di file). Quindi forkgestisce sia la forcella che crea bene, mentre createpuò solo gestire creare bene.
Cort Ammon,

2
La tua risposta si riferiva ai miglioramenti dell'hardware: memoria virtuale, copia su scrittura. Prima di questi, forkeffettivamente copiato tutta la memoria di processo, ed era molto costoso.
Barmar,

6

Penso che il motivo per cui Unix abbia avuto solo la forkfunzione di creare nuovi processi sia il risultato della filosofia Unix

Costruiscono una funzione che fa bene una cosa. Crea un processo figlio.

Ciò che si fa con il nuovo processo spetta quindi al programmatore. Può usare una delle exec*funzioni e avviare un programma diverso, oppure non può usare exec e usare le due istanze dello stesso programma, il che può essere utile.

Quindi puoi ottenere un maggior grado di libertà da quando puoi usarlo

  1. fork senza exec *
  2. fork con exec * o
  3. solo exec * senza fork

e in aggiunta è sufficiente memorizzare i forke le exec*chiamate di funzione, che nel 1970 si doveva fare.


3
Capisco come funzionano le forcelle e come usarle. Ma perché dovrei voler creare un nuovo processo, quando posso fare la stessa cosa ma con meno sforzo? Ad esempio, il mio insegnante mi ha assegnato un compito in cui dovevo creare un processo per ciascun numero passato ad argv, per verificare se il numero è un numero primo. Ma non è solo una deviazione che alla fine fa la stessa cosa? Avrei potuto semplicemente usare un array e usare una funzione per ogni numero ... Quindi perché creiamo processi figlio, invece di fare tutto il processo nel processo principale?
user1534664

2
Mi permetto di dire che capisci come funzionano le forcelle e come usarle, perché una volta avevi un insegnante che ti aveva assegnato un compito in cui dovevi creare un mucchio di processi (con il numero che veniva specificato in fase di esecuzione), controllali, coordinali e comunica tra loro. Naturalmente nessuno nella vita reale farebbe qualcosa di banale. Tuttavia, se si riscontra un grosso problema che può essere facilmente scomposto in pezzi che possono essere gestiti in parallelo (ad esempio, rilevamento dei bordi in un'immagine), il fork consente di utilizzare più core CPU contemporaneamente.
Scott,

5

Esistono due filosofie della creazione di processi: fork con l'eredità e creazione con argomenti. Unix usa fork, ovviamente. (OSE, ad esempio, e VMS utilizzano il metodo di creazione.) Unix ha MOLTE caratteristiche ereditabili e altre vengono aggiunte periodicamente. Tramite l'ereditarietà, queste nuove caratteristiche possono essere aggiunte SENZA MODIFICARE I PROGRAMMI ESISTENTI! Utilizzando un modello di creazione con argomenti, aggiungere nuove caratteristiche significherebbe aggiungere nuovi argomenti alla chiamata di creazione. Il modello Unix è più semplice.

Offre inoltre l'utilissimo modello fork-without-exec, in cui un processo può dividersi in più pezzi. Ciò era di vitale importanza quando non esisteva alcuna forma di I / O asincrono ed è utile quando si utilizzano più CPU in un sistema. (Pre-thread.) L'ho fatto molto nel corso degli anni, anche di recente. In sostanza, consente di containerizzare più "programmi" in un singolo programma, quindi non c'è assolutamente spazio per corruzione o disallineamenti di versione, ecc.

Il modello fork / exec offre anche la possibilità per un bambino specifico di ereditare un ambiente radicalmente strano, impostato tra fork e exec. Cose come i descrittori di file ereditati, in particolare. (Un'estensione di stdio fd's.) Il modello di creazione non offre la possibilità di ereditare tutto ciò che non è stato immaginato dai creatori della chiamata di creazione.

Alcuni sistemi possono anche supportare la compilazione dinamica di codice nativo, in cui il processo sta in effetti scrivendo il proprio programma in codice nativo. In altre parole, vuole un nuovo programma che sta scrivendo al volo, SENZA dover passare attraverso il ciclo codice sorgente / compilatore / linker e occupare spazio su disco. (Credo che ci sia un sistema di linguaggio Verilog che lo fa.) Il modello fork supporta questo, il modello creato normalmente non lo farebbe.


I descrittori di file non sono "un'estensione di stdio"; I puntatori ai file stdio sono un wrapper per i descrittori di file. I descrittori di file sono arrivati ​​per primi e sono gli handle I / O Unix fondamentali. Ma, in caso contrario, questo è un buon punto.
Scott,

2

La funzione fork () non è solo per copiare il processo padre, ma restituisce un valore che indica che il processo è il processo padre o figlio, l'immagine sotto spiega come utilizzare fork () come padre e un figlio:

inserisci qui la descrizione dell'immagine

come mostrato quando il processo è il padre fork () restituisce l'ID del processo figlio PID altrimenti restituisce0

ad esempio puoi utilizzarlo se hai un processo (web server) che riceve le richieste e su ogni richiesta crea un son processprocesso per elaborare questa richiesta, qui il padre e i suoi figli hanno lavori diversi.

Quindi, nessuna esecuzione di una copia di un processo non è la cosa esatta come fork ().


5
Mentre è vero, questo non risponde alla domanda. Perché il fork è necessario per la creazione del processo, se voglio eseguire un altro eseguibile?
SkyDan,

1
Sono d'accordo con SkyDan - questo non risponde alla domanda. posix_spawn è una versione un po 'più elaborata di ciò che avrebbe potuto essere immaginato 30 anni fa (prima che Posix esistesse) come una funzione fork_execve ; uno che crea un nuovo processo, inizializzando la sua immagine da un eseguibile, senza nemmeno suggerire di copiare l'immagine del processo genitore (tranne per l'elenco degli argomenti, l'ambiente e gli attributi del processo (ad esempio, directory di lavoro)) e restituisce il PID del nuovo processo per il chiamante (il processo principale) .
Scott,

1
Esistono altri modi per passare informazioni "parent" a un bambino. La tecnica del valore restituito sembra essere il modo più efficiente di farlo da un fork se supponi di voler forkprima di tutto
Cort Ammon,

0

Il reindirizzamento degli I / O viene implementato più facilmente dopo il fork e prima di exec. Il bambino, consapevole del fatto che è il bambino, può chiudere i descrittori di file, aprirne di nuovi, dup () o dup2 () per ottenerli sul giusto numero fd, ecc., Senza influire sul genitore. Dopo averlo fatto, e forse qualsiasi modifica della variabile d'ambiente desiderata (anche senza influire sul genitore), può eseguire il nuovo programma nell'ambiente su misura.


Tutto quello che stai facendo qui è ripetere il terzo paragrafo della risposta di Jim Cathey con un po 'più di dettaglio.
Scott,

-2

Penso che tutti qui sappiano come funziona il fork, ma la domanda è: perché dobbiamo creare un duplicato esatto del genitore usando fork? Risposta ==> Prendi un esempio di server (senza fork), mentre client-1 sta accedendo al server, se allo stesso tempo è arrivato il secondo client-2 e vuole accedere al server ma il server non dà l'autorizzazione al nuovo arrivato client-2 perché il server è occupato a servire client-1, quindi client-2 deve attendere. Dopo che tutti i servizi al client-1 sono terminati, client-2 è ora in grado di accedere al server. Adesso considera se allo stesso tempo client-3 arriva, quindi client-3 deve attendere fino al termine di tutti i servizi al client-2. Prendi lo scenario in cui le migliaia di client devono accedere al server contemporaneamente ... quindi tutti i client devono aspetta (il server è occupato !!).

Questo viene evitato creando (usando fork) una copia duplicata esatta (cioè figlio) del server, dove ogni figlio (che è una copia duplicata esatta del suo genitore cioè server) è dedicato al client appena arrivato, quindi contemporaneamente tutti i client accedono allo stesso server.


Questo è il motivo per cui i processi server non dovrebbero essere a thread singolo, gestendo le richieste client consecutivamente quando possono essere gestite contemporaneamente - ad esempio, in processi separati. Ma il modello di server multi-thread può essere facilmente implementato con un processo di ascolto che accetta le richieste dei client e crea un nuovo processo in cui eseguire il programma di servizio client. L'unico vantaggio offerto dalla forkchiamata che copia il processo padre è che non è necessario disporre di due programmi separati, ma avere programmi separati (ad es. inetd) Può rendere il sistema più modulare.
Scott,
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.